Summary

Class:Xrm.Oss.XTL.Interpreter.FunctionHandlers
Assembly:Xrm.Oss.XTL.Templating
File(s):D:\Entwicklung\Xrm-Templating-Language\src\lib\Xrm.Oss.XTL.Interpreter\FunctionHandlers.cs
Covered lines:1072
Uncovered lines:8
Coverable lines:1080
Total lines:1181
Line coverage:99.2% (1072 of 1080)
Covered branches:325
Total branches:444
Branch coverage:73.1% (325 of 444)

Metrics

MethodCyclomatic complexity NPath complexity Sequence coverage Branch coverage Crap Score
GetConfig(...)58100%100%30
.cctor()10100%100%2
Compare(...)2275%66.67%6
CheckedCast(...)51666.67%77.78%30
FetchSnippetByUniqueName(...)10100%100%2
FetchSnippet(...)616100%100%42

File(s)

D:\Entwicklung\Xrm-Templating-Language\src\lib\Xrm.Oss.XTL.Interpreter\FunctionHandlers.cs

#LineLine coverage
 1using System;
 2using System.Collections;
 3using System.Collections.Generic;
 4using System.Globalization;
 5using System.IO;
 6using System.Linq;
 7using System.Text;
 8using System.Text.RegularExpressions;
 9using Microsoft.Crm.Sdk.Messages;
 10using Microsoft.Xrm.Sdk;
 11using Microsoft.Xrm.Sdk.Messages;
 12using Microsoft.Xrm.Sdk.Metadata;
 13using Microsoft.Xrm.Sdk.Query;
 14using Xrm.Oss.FluentQuery;
 15using static Xrm.Oss.XTL.Interpreter.XTLInterpreter;
 16
 17namespace Xrm.Oss.XTL.Interpreter
 18{
 19    #pragma warning disable S1104 // Fields should not have public accessibility
 20    public static class FunctionHandlers
 21    {
 22        private static ConfigHandler GetConfig(List<ValueExpression> parameters)
 17623        {
 44124            return new ConfigHandler((Dictionary<string, object>) parameters.LastOrDefault(p => p?.Value is Dictionary<s
 17625        }
 26
 227        public static FunctionHandler Not = (primary, service, tracing, organizationConfig, parameters) =>
 528        {
 529            var target = parameters.FirstOrDefault();
 530            var result = !CheckedCast<bool>(target?.Value, "Not expects a boolean input, consider using one of the Is me
 231
 532            return new ValueExpression(result.ToString(CultureInfo.InvariantCulture), result);
 533        };
 34
 235        public static FunctionHandler First = (primary, service, tracing, organizationConfig, parameters) =>
 636        {
 637            if (parameters.Count != 1)
 238            {
 239                throw new InvalidPluginExecutionException("First expects a list as only parameter!");
 240            }
 241
 742            var firstParam = CheckedCast<List<ValueExpression>>(parameters.FirstOrDefault().Value, string.Empty, false)?
 643            var entityCollection = CheckedCast<EntityCollection>(parameters.FirstOrDefault().Value, string.Empty, false)
 244
 645            if (firstParam == null && entityCollection == null)
 246            {
 247                throw new InvalidPluginExecutionException("First expects a list or EntityCollection as input");
 248            }
 249
 650            return new ValueExpression(string.Empty, firstParam?.FirstOrDefault() ?? entityCollection?.FirstOrDefault())
 651        };
 52
 253        public static FunctionHandler Last = (primary, service, tracing, organizationConfig, parameters) =>
 354        {
 355            if (parameters.Count != 1)
 256            {
 257                throw new InvalidPluginExecutionException("Last expects a list as only parameter!");
 258            }
 259
 360            var firstParam = CheckedCast<List<ValueExpression>>(parameters.FirstOrDefault().Value, string.Empty, false)?
 361            var entityCollection = CheckedCast<EntityCollection>(parameters.FirstOrDefault().Value, string.Empty, false)
 262
 363            if (firstParam == null && entityCollection == null)
 264            {
 265                throw new InvalidPluginExecutionException("Last expects a list or EntityCollection as input");
 266            }
 267
 368            return new ValueExpression(string.Empty, firstParam?.LastOrDefault() ?? entityCollection?.LastOrDefault());
 369        };
 70
 271        public static FunctionHandler IsLess = (primary, service, tracing, organizationConfig, parameters) =>
 872        {
 873            bool result = Compare(parameters) < 0;
 274
 875            return new ValueExpression(result.ToString(CultureInfo.InvariantCulture), result);
 876        };
 77
 278        public static FunctionHandler IsLessEqual = (primary, service, tracing, organizationConfig, parameters) =>
 579        {
 580            bool result = Compare(parameters) <= 0;
 281
 582            return new ValueExpression(result.ToString(CultureInfo.InvariantCulture), result);
 583        };
 84
 285        public static FunctionHandler IsGreater = (primary, service, tracing, organizationConfig, parameters) =>
 886        {
 887            bool result = Compare(parameters) > 0;
 288
 889            return new ValueExpression(result.ToString(CultureInfo.InvariantCulture), result);
 890        };
 91
 292        public static FunctionHandler IsGreaterEqual = (primary, service, tracing, organizationConfig, parameters) =>
 593        {
 594            bool result = Compare(parameters) >= 0;
 295
 596            return new ValueExpression(result.ToString(CultureInfo.InvariantCulture), result);
 597        };
 98
 99        private static int Compare(List<ValueExpression> parameters)
 18100        {
 18101            if (parameters.Count != 2)
 0102            {
 0103                throw new InvalidPluginExecutionException("IsLess expects exactly 2 parameters!");
 104            }
 105
 18106            var actual = CheckedCast<IComparable>(parameters[0].Value, "Actual value is not comparable");
 18107            var expected = CheckedCast<IComparable>(parameters[1].Value, "Expected value is not comparable");
 108
 109            // Negative: actual is less than expected, 0: equal, 1: actual is greater than expected
 18110            return actual.CompareTo(expected);
 18111        }
 112
 2113        public static FunctionHandler IsEqual = (primary, service, tracing, organizationConfig, parameters) =>
 17114        {
 17115            if (parameters.Count != 2)
 2116            {
 2117                throw new InvalidPluginExecutionException("IsEqual expects exactly 2 parameters!");
 2118            }
 2119
 17120            var expected = parameters[0];
 17121            var actual = parameters[1];
 17122            var tempGuid = Guid.Empty;
 2123
 17124            var falseReturn = new ValueExpression(bool.FalseString, false);
 17125            var trueReturn = new ValueExpression(bool.TrueString, true);
 2126
 17127            if (expected.Value == null && actual.Value == null)
 2128            {
 2129                return trueReturn;
 2130            }
 2131
 17132            if (expected.Value == null && actual.Value != null)
 2133            {
 2134                return falseReturn;
 2135            }
 2136
 17137            if (expected.Value != null && actual.Value == null)
 2138            {
 2139                return falseReturn;
 2140            }
 2141
 36142            if (new[] { expected.Value, actual.Value }.All(v => v is int || v is OptionSetValue))
 6143            {
 6144                var values = new[] { expected.Value, actual.Value }
 14145                    .Select(v => v is OptionSetValue ? ((OptionSetValue)v).Value : (int)v)
 6146                    .ToList();
 2147
 6148                var optionSetResult = values[0].Equals(values[1]);
 6149                return new ValueExpression(optionSetResult.ToString(CultureInfo.InvariantCulture), optionSetResult);
 2150            }
 28151            else if (new[] { expected.Value, actual.Value }.All(v => v is Guid || (v is string && Guid.TryParse((string)
 6152            {
 6153                var values = new[] { expected.Value, actual.Value }
 6154                    .Select(v =>
 14155                    {
 14156                        if (v is Guid)
 10157                        {
 10158                            return (Guid) v;
 6159                        }
 6160
 10161                        if (v is string)
 10162                        {
 10163                            return tempGuid;
 6164                        }
 6165
 6166                        if (v is EntityReference)
 6167                        {
 6168                            return ((EntityReference) v).Id;
 6169                        }
 6170
 6171                        if (v is Entity)
 6172                        {
 6173                            return ((Entity) v).Id;
 6174                        }
 6175
 6176                        return Guid.Empty;
 14177                    })
 6178                    .ToList();
 2179
 6180                var guidResult = values[0].Equals(values[1]);
 6181                return new ValueExpression(guidResult.ToString(CultureInfo.InvariantCulture), guidResult);
 2182            }
 2183            else
 9184            {
 9185                var result = expected.Value.Equals(actual.Value);
 2186
 9187                return new ValueExpression(result.ToString(CultureInfo.InvariantCulture), result);
 2188            }
 17189        };
 190
 2191        public static FunctionHandler And = (primary, service, tracing, organizationConfig, parameters) =>
 8192        {
 8193            if (parameters.Count < 2)
 2194            {
 2195                throw new InvalidPluginExecutionException("And expects at least 2 conditions!");
 2196            }
 2197
 26198            if (parameters.Any(p => !(p.Value is bool)))
 2199            {
 2200                throw new InvalidPluginExecutionException("And: All conditions must be booleans!");
 2201            }
 2202
 21203            if (parameters.All(p => (bool)p.Value))
 4204            {
 4205                return new ValueExpression(bool.TrueString, true);
 2206            }
 2207
 6208            return new ValueExpression(bool.FalseString, false);
 8209        };
 210
 2211        public static FunctionHandler Or = (primary, service, tracing, organizationConfig, parameters) =>
 6212        {
 6213            if (parameters.Count < 2)
 2214            {
 2215                throw new InvalidPluginExecutionException("Or expects at least 2 conditions!");
 2216            }
 2217
 17218            if (parameters.Any(p => !(p.Value is bool)))
 2219            {
 2220                throw new InvalidPluginExecutionException("Or: All conditions must be booleans!");
 2221            }
 2222
 14223            if (parameters.Any(p => (bool)p.Value))
 5224            {
 5225                return new ValueExpression(bool.TrueString, true);
 2226            }
 2227
 3228            return new ValueExpression(bool.FalseString, false);
 6229        };
 230
 2231        public static FunctionHandler IsNull = (primary, service, tracing, organizationConfig, parameters) =>
 22232        {
 22233            var target = parameters.FirstOrDefault();
 2234
 22235            if (target.Value == null)
 10236            {
 10237                return new ValueExpression(bool.TrueString, true);
 2238            }
 2239
 14240            return new ValueExpression(bool.FalseString, false);
 22241        };
 242
 2243        public static FunctionHandler If = (primary, service, tracing, organizationConfig, parameters) =>
 27244        {
 27245            if (parameters.Count != 3)
 2246            {
 2247                throw new InvalidPluginExecutionException("If-Then-Else expects exactly three parameters: Condition, Tru
 2248            }
 2249
 27250            var condition = CheckedCast<bool>(parameters[0]?.Value, "If condition must be a boolean!");
 27251            var trueAction = parameters[1];
 27252            var falseAction = parameters[2];
 2253
 27254            if (condition)
 15255            {
 15256                tracing.Trace("Executing true condition");
 28257                return new ValueExpression(new Lazy<ValueExpression>(() => trueAction));
 2258            }
 2259
 14260            tracing.Trace("Executing false condition");
 26261            return new ValueExpression(new Lazy<ValueExpression>(() => falseAction));
 27262        };
 263
 2264        public static FunctionHandler GetPrimaryRecord = (primary, service, tracing, organizationConfig, parameters) =>
 12265        {
 12266            if (primary == null)
 2267            {
 2268                return new ValueExpression(null);
 2269            }
 2270
 12271            return new ValueExpression(string.Empty, primary);
 12272        };
 273
 2274        public static FunctionHandler GetRecordUrl = (primary, service, tracing, organizationConfig, parameters) =>
 13275        {
 13276            if (organizationConfig == null || string.IsNullOrEmpty(organizationConfig.OrganizationUrl))
 2277            {
 2278                throw new InvalidPluginExecutionException("GetRecordUrl can't find the Organization Url inside the plugi
 2279            }
 2280
 32281            if (!parameters.All(p => p.Value is EntityReference || p.Value is Entity || p.Value is Dictionary<string, ob
 2282            {
 2283                throw new InvalidPluginExecutionException("Only Entity Reference and Entity ValueExpressions are support
 2284            }
 2285
 32286            var refs = parameters.Where(p => p != null && !(p?.Value is Dictionary<string, object>)).Select(e =>
 24287            {
 24288                var entityReference = e.Value as EntityReference;
 13289
 26290                if (entityReference != null) {
 15291                    return new
 15292                    {
 15293                        Id = entityReference.Id,
 15294                        LogicalName = entityReference.LogicalName
 15295                    };
 13296                }
 13297
 22298                var entity = e.Value as Entity;
 13299
 22300                return new
 22301                {
 22302                    Id = entity.Id,
 22303                    LogicalName = entity.LogicalName
 22304                };
 24305            });
 13306            var organizationUrl = organizationConfig.OrganizationUrl.EndsWith("/") ? organizationConfig.OrganizationUrl 
 2307
 13308            var config = GetConfig(parameters);
 13309            var linkText = config.GetValue<string>("linkText", "linkText must be a string");
 13310            var appId = config.GetValue<string>("appId", "appId must be a string");
 2311
 13312            var urls = string.Join(Environment.NewLine, refs.Select(e =>
 24313            {
 24314                var url = $"{organizationUrl}main.aspx?etn={e.LogicalName}&id={e.Id}&newWindow=true&pagetype=entityrecor
 24315                return $"<a href=\"{url}\">{(string.IsNullOrEmpty(linkText) ? url : linkText)}</a>";
 24316            }));
 2317
 13318            return new ValueExpression(urls, urls);
 13319        };
 320
 2321        public static FunctionHandler GetOrganizationUrl = (primary, service, tracing, organizationConfig, parameters) =
 4322        {
 4323            if (organizationConfig == null || string.IsNullOrEmpty(organizationConfig.OrganizationUrl))
 2324            {
 2325                throw new InvalidPluginExecutionException("GetOrganizationUrl can't find the Organization Url inside the
 2326            }
 2327
 4328            var config = GetConfig(parameters);
 4329            var linkText = config.GetValue<string>("linkText", "linkText must be a string", string.Empty);
 4330            var urlSuffix = config.GetValue<string>("urlSuffix", "urlSuffix must be a string", string.Empty);
 4331            var asHtml = config.GetValue<bool>("asHtml", "asHtml must be a boolean");
 2332
 4333            if (asHtml)
 4334            {
 4335                var url = $"{organizationConfig.OrganizationUrl}{urlSuffix}";
 4336                var href = $"<a href=\"{url}\">{(string.IsNullOrEmpty(linkText) ? url : linkText)}</a>";
 4337                return new ValueExpression(href, href);
 2338            }
 2339            else
 2340            {
 2341                return new ValueExpression(organizationConfig.OrganizationUrl, organizationConfig.OrganizationUrl);
 2342            }
 4343        };
 344
 2345        public static FunctionHandler Union = (primary, service, tracing, organizationConfig, parameters) =>
 6346        {
 6347            if (parameters.Count < 2)
 2348            {
 2349                throw new InvalidPluginExecutionException("Union function needs at least two parameters: Arrays to union
 2350            }
 2351
 6352            var union = parameters.Select(p =>
 16353            {
 16354                if (p == null)
 6355                {
 6356                    return null;
 6357                }
 6358
 16359                return p.Value as List<ValueExpression>;
 16360            })
 16361            .Where(p => p != null)
 16362            .SelectMany(p => p)
 6363            .ToList();
 2364
 6365            return new ValueExpression(null, union);
 6366        };
 367
 2368        public static FunctionHandler Map = (primary, service, tracing, organizationConfig, parameters) =>
 3369        {
 3370            if (parameters.Count < 2)
 2371            {
 2372                throw new InvalidPluginExecutionException("Map function needs at least an array with data and a function
 2373            }
 2374
 3375            var config = GetConfig(parameters);
 2376
 3377            var values = parameters[0].Value as List<ValueExpression>;
 2378
 3379            if (!(values is IEnumerable))
 2380            {
 2381                throw new InvalidPluginExecutionException("Map needs an array as first parameter.");
 2382            }
 2383
 3384            var lambda = parameters[1].Value as Func<List<ValueExpression>, ValueExpression>;
 2385
 3386            if (lambda == null)
 2387            {
 2388                throw new InvalidPluginExecutionException("Lambda function must be a proper arrow function");
 2389            }
 2390
 7391            return new ValueExpression(null, values.Select(v => lambda(new List<ValueExpression> { v })).ToList());
 3392        };
 393
 2394        public static FunctionHandler Sort = (primary, service, tracing, organizationConfig, parameters) =>
 6395        {
 6396            if (parameters.Count < 1)
 2397            {
 2398                throw new InvalidPluginExecutionException("Sort function needs at least an array to sort and optionally 
 2399            }
 2400
 6401            var config = GetConfig(parameters);
 2402
 6403            var values = parameters[0].Value as List<ValueExpression>;
 2404
 6405            if (!(values is IEnumerable))
 2406            {
 2407                throw new InvalidPluginExecutionException("Sort needs an array as first parameter.");
 2408            }
 2409
 6410            var descending = config.GetValue<bool>("descending", "descending must be a bool");
 6411            var property = config.GetValue<string>("property", "property must be a string");
 2412
 6413            if (string.IsNullOrEmpty(property))
 3414            {
 3415                if (descending)
 2416                {
 2417                    return new ValueExpression(null, values.OrderByDescending(v => v.Value).ToList());
 2418                }
 2419                else
 3420                {
 7421                    return new ValueExpression(null, values.OrderBy(v => v.Value).ToList());
 2422                }
 2423            }
 2424            else
 5425            {
 5426                if (descending)
 3427                {
 5428                    return new ValueExpression(null, values.OrderByDescending(v => (v.Value as Entity)?.GetAttributeValu
 2429                }
 2430                else
 4431                {
 8432                    return new ValueExpression(null, values.OrderBy(v => (v.Value as Entity)?.GetAttributeValue<object>(
 2433                }
 2434            }
 6435        };
 436
 2437        private static Func<string, IOrganizationService, Dictionary<string, string>> RetrieveColumnNames = (entityName,
 19438        {
 19439            return ((RetrieveEntityResponse)service.Execute(new RetrieveEntityRequest
 19440            {
 19441                EntityFilters = EntityFilters.Attributes,
 19442                LogicalName = entityName,
 19443                RetrieveAsIfPublished = false
 19444            }))
 19445            .EntityMetadata
 19446            .Attributes
 87447            .ToDictionary(a => a.LogicalName, a => a?.DisplayName?.UserLocalizedLabel?.Label ?? a.LogicalName);
 19448        };
 449
 2450        public static FunctionHandler RenderRecordTable = (primary, service, tracing, organizationConfig, parameters) =>
 19451        {
 19452            tracing.Trace("Parsing parameters");
 2453
 19454            if (parameters.Count < 3)
 2455            {
 2456                throw new InvalidPluginExecutionException("RecordTable needs at least 3 parameters: Entities, entity nam
 2457            }
 2458
 19459            var records = CheckedCast<List<ValueExpression>>(parameters[0].Value, "RecordTable requires the first parame
 48460                .Select(p => (p as ValueExpression)?.Value)
 19461                .Cast<Entity>()
 19462                .ToList();
 2463
 19464            tracing.Trace($"Records: {records.Count}");
 2465
 2466            // We need the entity name although it should be set in the record. If no records are passed, we would fail 
 19467            var entityName = CheckedCast<string>(parameters[1]?.Value, "Second parameter of the RecordTable function nee
 2468
 19469            if (string.IsNullOrEmpty(entityName))
 2470            {
 2471                throw new InvalidPluginExecutionException("Second parameter of the RecordTable function needs to be the 
 2472            }
 2473
 2474            // We need the column names explicitly, since CRM does not return new ValueExpression(null)-valued columns, 
 19475            var displayColumns = CheckedCast<List<ValueExpression>>(parameters[2]?.Value, "List of column names for reco
 48476                .Select(p => p.Value)
 48477                .Select(p => p is Dictionary<string, object> ? (Dictionary<string, object>) p : new Dictionary<string, o
 19478                .ToList();
 2479
 19480            tracing.Trace("Retrieving column names");
 19481            var columnNames = RetrieveColumnNames(entityName, service);
 19482            tracing.Trace($"Column names done");
 2483
 19484            var config = GetConfig(parameters);
 19485            var addRecordUrl = config.GetValue<bool>("addRecordUrl", "When setting addRecordUrl, value must be a boolean
 2486
 19487            var tableStyle = config.GetValue("tableStyle", "tableStyle must be a string!", string.Empty);
 2488
 19489            if (!string.IsNullOrEmpty(tableStyle))
 4490            {
 4491                tableStyle = $" style=\"{tableStyle}\"";
 4492            }
 2493
 19494            var tableHeadStyle = config.GetValue("headerStyle", "headerStyle must be a string!", @"border:1px solid blac
 19495            var tableDataStyle = config.GetValue("dataStyle", "dataStyle must be a string!", @"border:1px solid black;pa
 2496
 19497            var evenDataStyle = config.GetValue<string>("evenDataStyle", "evenDataStyle must be a string!");
 19498            var unevenDataStyle = config.GetValue<string>("unevenDataStyle", "unevenDataStyle must be a string!");
 2499
 19500            tracing.Trace("Parsed parameters");
 2501
 2502            // Create table header
 19503            var stringBuilder = new StringBuilder($"<table{tableStyle}>\n<tr>");
 111504            foreach (var column in displayColumns)
 31505            {
 31506                var name = string.Empty;
 31507                var columnName = column.ContainsKey("name") ? column["name"] as string : string.Empty;
 2508
 31509                if (columnName.Contains(":"))
 3510                {
 3511                    name = columnName.Substring(columnName.IndexOf(':') + 1);
 3512                }
 2513                else
 30514                {
 30515                    name = columnNames.ContainsKey(columnName) ? columnNames[columnName] : columnName;
 30516                }
 2517
 31518                if (column.ContainsKey("label"))
 9519                {
 9520                    name = column["label"] as string;
 9521                }
 2522
 31523                if (column.ContainsKey("style"))
 6524                {
 6525                    if (!column.ContainsKey("mergeStyle") || (bool) column["mergeStyle"])
 4526                    {
 4527                        stringBuilder.AppendLine($"<th style=\"{tableHeadStyle}{column["style"]}\">{name}</th>");
 4528                    }
 2529                    else
 4530                    {
 4531                        stringBuilder.AppendLine($"<th style=\"{column["style"]}\">{name}</th>");
 4532                    }
 6533                }
 2534                else
 27535                {
 27536                    stringBuilder.AppendLine($"<th style=\"{tableHeadStyle}\">{name}</th>");
 27537                }
 31538            }
 2539
 2540            // Add column for url if wanted
 19541            if (addRecordUrl)
 5542            {
 5543                stringBuilder.AppendLine($"<th style=\"{tableHeadStyle}\">URL</th>");
 5544            }
 19545            stringBuilder.AppendLine("</tr>");
 2546
 19547            if (records != null)
 19548            {
 94549                for (var i = 0; i < records.Count; i++)
 31550                {
 31551                    var record = records[i];
 31552                    var isEven = i % 2 == 0;
 31553                    var lineStyle = (isEven ? evenDataStyle : unevenDataStyle) ?? tableDataStyle;
 2554
 31555                    stringBuilder.AppendLine("<tr>");
 2556
 191557                    foreach (var column in displayColumns)
 53558                    {
 53559                        var columnName = column.ContainsKey("name") ? column["name"] as string : string.Empty;
 53560                        columnName = columnName.Contains(":") ? columnName.Substring(0, columnName.IndexOf(':')) : colum
 2561
 53562                        var renderFunction = column.ContainsKey("renderFunction") ? column["renderFunction"] as Func<Lis
 53563                        var entityConfig = column.ContainsKey("nameByEntity") ? column["nameByEntity"] as Dictionary<str
 2564
 53565                        if (entityConfig != null && entityConfig.ContainsKey(record.LogicalName))
 4566                        {
 4567                            columnName = entityConfig[record.LogicalName] as string;
 4568                        }
 2569
 53570                        var staticValues = column.ContainsKey("staticValueByEntity") ? column["staticValueByEntity"] as 
 2571
 2572                        string value;
 2573
 53574                        if (staticValues != null && staticValues.ContainsKey(record.LogicalName))
 4575                        {
 4576                            value = staticValues[record.LogicalName] as string;
 4577                        }
 49578                        else if (renderFunction != null)
 3579                        {
 3580                            var rowRecord = new ValueExpression(null, record);
 3581                            var rowColumnName = new ValueExpression(columnName, columnName);
 582
 3583                            value = renderFunction(new List<ValueExpression> { rowRecord, rowColumnName })?.Text;
 3584                        }
 585                        else
 46586                        {
 46587                            value = PropertyStringifier.Stringify(columnName, record, service, config);
 46588                        }
 589
 51590                        if (column.ContainsKey("style"))
 8591                        {
 8592                            if (!column.ContainsKey("mergeStyle") || (bool)column["mergeStyle"])
 4593                            {
 4594                                stringBuilder.AppendLine($"<td style=\"{lineStyle}{column["style"]}\">{value}</td>");
 4595                            }
 596                            else
 4597                            {
 4598                                stringBuilder.AppendLine($"<td style=\"{column["style"]}\">{value}</td>");
 4599                            }
 8600                        }
 601                        else
 43602                        {
 43603                            stringBuilder.AppendLine($"<td style=\"{lineStyle}\">{value}</td>");
 43604                        }
 51605                    }
 606
 29607                    if (addRecordUrl)
 6608                    {
 6609                        stringBuilder.AppendLine($"<td style=\"{lineStyle}\">{GetRecordUrl(primary, service, tracing, or
 6610                    }
 611
 29612                    stringBuilder.AppendLine("</tr>");
 29613                }
 17614            }
 615
 17616            stringBuilder.AppendLine("</table>");
 17617            var table = stringBuilder.ToString();
 618
 17619            return new ValueExpression(table, table);
 17620        };
 621
 2622        public static FunctionHandler Fetch = (primary, service, tracing, organizationConfig, parameters) =>
 24623        {
 24624            if (parameters.Count < 1)
 2625            {
 2626                throw new InvalidPluginExecutionException("Fetch needs at least one parameter: Fetch XML.");
 2627            }
 2628
 24629            var fetch = parameters[0].Value as string;
 2630
 24631            if (string.IsNullOrEmpty(fetch))
 2632            {
 2633                throw new InvalidPluginExecutionException("First parameter of Fetch function needs to be a fetchXml stri
 2634            }
 2635
 24636            var references = new List<object> { primary.Id };
 2637
 24638            if (parameters.Count > 1)
 5639            {
 5640                if (!(parameters[1].Value is List<ValueExpression>))
 2641                {
 2642                    throw new InvalidPluginExecutionException("Fetch parameters must be an array expression");
 2643                }
 2644
 5645                var @params = parameters[1].Value as List<ValueExpression>;
 2646
 5647                if (@params is IEnumerable)
 5648                {
 17649                    foreach (var item in @params)
 5650                    {
 5651                        var reference = item.Value as EntityReference;
 5652                        if (reference != null)
 4653                        {
 4654                            references.Add(reference.Id);
 4655                            continue;
 2656                        }
 2657
 3658                        var entity = item.Value as Entity;
 3659                        if (entity != null)
 2660                        {
 2661                            references.Add(entity.Id);
 2662                            continue;
 2663                        }
 2664
 3665                        var optionSet = item.Value as OptionSetValue;
 3666                        if (optionSet != null)
 2667                        {
 2668                            references.Add(optionSet.Value);
 2669                            continue;
 2670                        }
 2671
 3672                        references.Add(item.Value);
 3673                    }
 5674                }
 5675            }
 2676
 24677            var records = new List<object>();
 2678
 24679            var query = fetch;
 2680
 24681            if (primary != null)
 24682            {
 24683                query = query.Replace("{0}", references[0].ToString());
 24684            }
 2685
 24686            tracing.Trace("Replacing references");
 2687
 24688            var referenceRegex = new Regex("{([0-9]+)}", RegexOptions.Compiled | RegexOptions.CultureInvariant);
 24689            query = referenceRegex.Replace(query, match =>
 27690            {
 27691                var capture = match.Groups[1].Value;
 27692                var referenceNumber = int.Parse(capture);
 24693
 27694                if (referenceNumber >= references.Count)
 24695                {
 24696                    throw new InvalidPluginExecutionException($"You tried using reference {referenceNumber} in fetch, bu
 24697                }
 24698
 27699                return references[referenceNumber]?.ToString();
 27700            });
 2701
 24702            tracing.Trace("References replaced");
 24703            tracing.Trace($"Executing fetch: {query}");
 24704            records.AddRange(service.RetrieveMultiple(new FetchExpression(query)).Entities);
 2705
 54706            return new ValueExpression(string.Empty, records.Select(r => new ValueExpression(string.Empty, r)).ToList())
 24707        };
 708
 2709        public static FunctionHandler GetValue = (primary, service, tracing, organizationConfig, parameters) =>
 108710        {
 108711            if (primary == null)
 2712            {
 2713                return new ValueExpression(null);
 2714            }
 2715
 108716            var field = parameters.FirstOrDefault()?.Text;
 2717
 108718            if (string.IsNullOrEmpty(field))
 2719            {
 2720                throw new InvalidPluginExecutionException("First parameter of Value function needs to be the field name 
 2721            }
 2722
 108723            var target = primary;
 108724            var config = GetConfig(parameters);
 2725
 108726            if (config.Contains("explicitTarget"))
 10727            {
 10728                if (config.IsSet("explicitTarget"))
 9729                {
 9730                    var explicitTarget = config.GetValue<Entity>("explicitTarget", "explicitTarget must be an entity!");
 2731
 9732                    target = explicitTarget;
 9733                }
 2734                else
 3735                {
 3736                    return new ValueExpression(string.Empty, string.Empty);
 2737                }
 9738            }
 2739
 107740            if (field == null)
 2741            {
 2742                throw new InvalidPluginExecutionException("Value requires a field target string as input");
 2743            }
 2744
 107745            return DataRetriever.ResolveTokenValue(field, target, service, config);
 108746        };
 747
 2748        public static FunctionHandler Join = (primary, service, tracing, organizationConfig, parameters) =>
 9749        {
 9750            if (parameters.Count < 2)
 2751            {
 2752                throw new InvalidPluginExecutionException("Join function needs at lease two parameters: Separator and an
 2753            }
 2754
 9755            var separator = parameters.FirstOrDefault()?.Text;
 2756
 9757            if (string.IsNullOrEmpty(separator))
 2758            {
 2759                throw new InvalidPluginExecutionException("First parameter of Join function needs to be the separator st
 2760            }
 2761
 9762            var values = parameters[1].Value as List<ValueExpression>;
 2763
 9764            if (!(values is IEnumerable))
 2765            {
 2766                throw new InvalidPluginExecutionException("The values parameter needs to be an enumerable, please wrap t
 2767            }
 2768
 9769            var config = GetConfig(parameters);
 9770            var removeEmptyEntries = false;
 2771
 9772            if (parameters.Count > 2 && parameters[2].Value is bool)
 5773            {
 5774                removeEmptyEntries = (bool) parameters[2].Value;
 5775            }
 2776
 9777            var valuesToConcatenate = values
 33778                .Where(v => !removeEmptyEntries || !string.IsNullOrEmpty(v.Text))
 31779                .Select(v => v.Text);
 2780
 9781            var joined = string.Join(separator, valuesToConcatenate);
 2782
 9783            return new ValueExpression(joined, joined);
 9784        };
 785
 2786        public static FunctionHandler NewLine = (primary, service, tracing, organizationConfig, parameters) =>
 3787        {
 3788            return new ValueExpression(Environment.NewLine, Environment.NewLine);
 3789        };
 790
 2791        public static FunctionHandler Concat = (primary, service, tracing, organizationConfig, parameters) =>
 5792        {
 5793            var text = "";
 2794
 31795            foreach (var parameter in parameters)
 12796            {
 12797                text += parameter.Text;
 12798            }
 2799
 5800            return new ValueExpression(text, text);
 5801        };
 802
 803        private static T CheckedCast<T>(object input, string errorMessage, bool failOnError = true)
 156804        {
 156805            var value = input;
 806
 156807            if (input is Money)
 0808            {
 0809                value = (input as Money).Value;
 0810            }
 811
 156812            if (input is OptionSetValue)
 0813            {
 0814                value = (input as OptionSetValue).Value;
 0815            }
 816
 156817            if (!(value is T))
 6818            {
 6819                if (failOnError)
 1820                {
 1821                    throw new InvalidPluginExecutionException(errorMessage);
 822                }
 823
 5824                return default(T);
 825            }
 826
 150827            return (T)value;
 155828        }
 829
 2830        public static FunctionHandler Substring = (primary, service, tracing, organizationConfig, parameters) =>
 11831        {
 11832            if (parameters.Count < 2)
 3833            {
 3834                throw new InvalidPluginExecutionException("Substring expects at least two parameters: text, start index 
 2835            }
 2836
 10837            var text = parameters[0].Text;
 10838            var startIndex = CheckedCast<int>(parameters[1].Value, "Start index parameter must be an int!");
 9839            var length = -1;
 2840
 9841            if (parameters.Count > 2)
 8842            {
 8843                length = CheckedCast<int>(parameters[2].Value, "Length parameter must be an int!");
 8844            }
 2845
 9846            var subString = length > -1 ? text.Substring(startIndex, length) : text.Substring(startIndex);
 9847            return new ValueExpression(subString, subString);
 9848        };
 849
 2850        public static FunctionHandler IndexOf = (primary, service, tracing, organizationConfig, parameters) =>
 4851        {
 4852            if (parameters.Count < 2)
 3853            {
 3854                throw new InvalidPluginExecutionException("IndexOf needs a source string and a string to search for");
 2855            }
 2856
 3857            var value = CheckedCast<string>(parameters[0].Value, "Source must be a string");
 3858            var searchText = CheckedCast<string>(parameters[1].Value, "Search text must be a string");
 2859
 3860            var config = GetConfig(parameters);
 3861            var ignoreCase = config.GetValue<bool>("ignoreCase", "ignoreCase must be a boolean!");
 2862
 3863            var index = value.IndexOf(searchText, ignoreCase ? StringComparison.InvariantCultureIgnoreCase : StringCompa
 2864
 3865            return new ValueExpression(index.ToString(), index);
 3866        };
 867
 2868        public static FunctionHandler Replace = (primary, service, tracing, organizationConfig, parameters) =>
 4869        {
 4870            if (parameters.Count < 3)
 3871            {
 3872                throw new InvalidPluginExecutionException("Replace expects three parameters: text input, regex pattern, 
 2873            }
 2874
 3875            var input = parameters[0].Text;
 3876            var pattern = parameters[1].Text;
 3877            var replacement = parameters[2].Text;
 2878
 3879            var replaced = Regex.Replace(input, pattern, replacement);
 2880
 3881            return new ValueExpression(replaced, replaced);
 3882        };
 883
 2884        public static FunctionHandler Array = (primary, service, tracing, organizationConfig, parameters) =>
 34885        {
 95886            return new ValueExpression(string.Join(", ", parameters.Select(p => p.Text)), parameters);
 34887        };
 888
 2889        public static FunctionHandler DateTimeNow = (primary, service, tracing, organizationConfig, parameters) =>
 3890        {
 3891            var date = DateTime.Now;
 3892            return new ValueExpression(date.ToString("o", CultureInfo.InvariantCulture), date);
 3893        };
 894
 2895        public static FunctionHandler DateTimeUtcNow = (primary, service, tracing, organizationConfig, parameters) =>
 5896        {
 5897            var date = DateTime.UtcNow;
 5898            return new ValueExpression(date.ToString("o", CultureInfo.InvariantCulture), date);
 5899        };
 900
 2901        public static FunctionHandler Static = (primary, service, tracing, organizationConfig, parameters) =>
 8902        {
 8903            if (parameters.Count < 1)
 2904            {
 2905                throw new InvalidOperationException("You have to pass a static value");
 2906            }
 2907
 8908            var parameter = parameters[0];
 8909            return new ValueExpression(parameter.Text, parameter.Value);
 8910        };
 911
 2912        public static FunctionHandler DateToString = (primary, service, tracing, organizationConfig, parameters) =>
 5913        {
 5914            if (parameters.Count < 1)
 2915            {
 2916                throw new Exception("No date to stringify");
 2917            }
 2918
 5919            var date = CheckedCast<DateTime>(parameters[0].Value, "You need to pass a date");
 5920            var config = GetConfig(parameters);
 5921            var format = config.GetValue<string>("format", "format must be a string!");
 2922
 5923            if (!string.IsNullOrEmpty(format))
 5924            {
 5925                return new ValueExpression(date.ToString(format), date.ToString(format));
 2926            }
 2927
 2928            return new ValueExpression(date.ToString(CultureInfo.InvariantCulture), date.ToString(CultureInfo.InvariantC
 5929        };
 930
 2931        public static FunctionHandler Format = (primary, service, tracing, organizationConfig, parameters) =>
 8932        {
 8933            if (parameters.Count < 2)
 3934            {
 3935                throw new InvalidPluginExecutionException("Format needs a value to format and a config for defining furt
 2936            }
 2937
 7938            var value = parameters[0].Value;
 7939            var config = GetConfig(parameters);
 7940            var format = config.GetValue<string>("format", "format must be a string!");
 2941
 7942            var knownTypes = new Dictionary<Type, Func<object, ValueExpression>>
 7943            {
 12944                { typeof(Money), (obj) => { var val = obj as Money; var formatted = string.Format(CultureInfo.InvariantC
 7945            };
 2946
 7947            if(knownTypes.ContainsKey(value.GetType()))
 3948            {
 3949                return knownTypes[value.GetType()](value);
 2950            }
 2951            else
 6952            {
 6953                var formatted = string.Format(CultureInfo.InvariantCulture, format, value);
 6954                return new ValueExpression(formatted, formatted);
 2955            }
 7956        };
 957
 958        private static Entity FetchSnippetByUniqueName(string uniqueName, IOrganizationService service)
 6959        {
 6960            var fetch = $@"<fetch no-lock=""true"">
 6961                <entity name=""oss_xtlsnippet"">
 6962                    <attribute name=""oss_xtlexpression"" />
 6963                    <attribute name=""oss_containsplaintext"" />
 6964                    <filter operator=""and"">
 6965                        <condition attribute=""oss_uniquename"" operator=""eq"" value=""{uniqueName}"" />
 6966                    </filter>
 6967                </entity>
 6968            </fetch>";
 969
 6970            var snippet = service.RetrieveMultiple(new FetchExpression(fetch))
 6971                .Entities
 6972                .FirstOrDefault();
 973
 6974            return snippet;
 6975        }
 976
 977        private static Entity FetchSnippet(string name, string filter, Entity primary, OrganizationConfig organizationCo
 6978        {
 6979            var uniqueNameSnippet = FetchSnippetByUniqueName(name, service);
 980
 6981            if (uniqueNameSnippet != null)
 1982            {
 1983                tracing.Trace("Found snippet by unique name");
 1984                return uniqueNameSnippet;
 985            }
 986
 5987            if (!string.IsNullOrEmpty(filter))
 3988            {
 3989                tracing.Trace("Processing tokens in custom snippet filter");
 3990            }
 991
 5992            var fetch = $@"<fetch no-lock=""true"">
 5993                <entity name=""oss_xtlsnippet"">
 5994                    <attribute name=""oss_xtlexpression"" />
 5995                    <attribute name=""oss_containsplaintext"" />
 5996                    <filter operator=""and"">
 5997                        <condition attribute=""oss_name"" operator=""eq"" value=""{name}"" />
 5998                        { (!string.IsNullOrEmpty(filter) ? TokenMatcher.ProcessTokens(filter, primary, organizationConfi
 5999                    </filter>
 51000                </entity>
 51001            </fetch>";
 1002
 51003            if (!string.IsNullOrEmpty(filter))
 31004            {
 31005                tracing.Trace("Done processing tokens in custom snippet filter");
 31006            }
 1007
 51008            var snippet = service.RetrieveMultiple(new FetchExpression(fetch))
 51009                .Entities
 51010                .FirstOrDefault();
 1011
 51012            return snippet;
 61013        }
 1014
 21015        public static FunctionHandler Snippet = (primary, service, tracing, organizationConfig, parameters) =>
 91016        {
 91017            if (parameters.Count < 1)
 31018            {
 31019                throw new InvalidPluginExecutionException("Snippet needs at least a name as first parameter and optional
 21020            }
 21021
 81022            var name = CheckedCast<string>(parameters[0].Value, "Name must be a string!");
 81023            var config = GetConfig(parameters);
 21024
 81025            var filter = config?.GetValue<string>("filter", "filter must be a string containing your fetchXml filter, wh
 21026
 81027            var snippet = FetchSnippet(name, filter, primary, organizationConfig, service, tracing);
 21028
 81029            if (snippet == null)
 21030            {
 21031                tracing.Trace("Failed to find a snippet matching the input");
 21032                return new ValueExpression(string.Empty, null);
 21033            }
 21034
 81035            var containsPlainText = snippet.GetAttributeValue<bool>("oss_containsplaintext");
 81036            var value = snippet.GetAttributeValue<string>("oss_xtlexpression");
 21037
 21038            // Wrap it in ${{ ... }} block
 81039            var processedValue = containsPlainText ? value : $"${{{{ {value} }}}}";
 21040
 81041            tracing.Trace("Processing snippet tokens");
 21042
 81043            var result = TokenMatcher.ProcessTokens(processedValue, primary, organizationConfig, service, tracing);
 21044
 81045            tracing.Trace("Done processing snippet tokens");
 21046
 81047            return new ValueExpression(result, result);
 81048        };
 1049
 21050        public static FunctionHandler ConvertDateTime = (primary, service, tracing, organizationConfig, parameters) =>
 71051        {
 71052            if (parameters.Count < 2)
 21053            {
 21054                throw new InvalidPluginExecutionException("Convert DateTime needs a DateTime and a config for defining f
 21055            }
 21056
 71057            var date = CheckedCast<DateTime>(parameters[0].Value, "You need to pass a date");
 71058            var config = GetConfig(parameters);
 21059
 71060            var timeZoneId = config.GetValue<string>("timeZoneId", "timeZoneId must be a string");
 71061            var userId = config.GetValue<EntityReference>("userId", "userId must be an EntityReference");
 21062
 71063            if (userId == null && string.IsNullOrEmpty(timeZoneId))
 21064            {
 21065                throw new InvalidPluginExecutionException("You need to either set a userId for converting to a user's co
 21066            }
 21067
 71068            if (userId != null)
 51069            {
 51070                var userSettings = service.Retrieve("usersettings", userId.Id, new ColumnSet("timezonecode"));
 51071                var timeZoneCode = userSettings.GetAttributeValue<int>("timezonecode");
 21072
 51073                timeZoneId = service.Query("timezonedefinition")
 51074                    .IncludeColumns("standardname")
 81075                    .Where(e => e
 111076                        .Attribute(a => a
 111077                            .Named("timezonecode")
 111078                            .Is(ConditionOperator.Equal)
 111079                            .To(timeZoneCode)
 81080                        )
 51081                    )
 51082                    .Retrieve()
 51083                    .FirstOrDefault()
 51084                    ?.GetAttributeValue<string>("standardname");
 51085            }
 21086
 71087            if (string.IsNullOrEmpty(timeZoneId))
 21088            {
 21089                throw new InvalidPluginExecutionException("Failed to retrieve timeZoneId, can't convert datetime");
 21090            }
 21091
 71092            var timeZone = TimeZoneInfo.FindSystemTimeZoneById(timeZoneId);
 71093            var localTime = TimeZoneInfo.ConvertTime(date, timeZone);
 71094            var text = localTime.ToString(config.GetValue<string>("format", "format must be a string", "g"), CultureInfo
 21095
 71096            return new ValueExpression(text, localTime);
 71097        };
 1098
 21099        public static FunctionHandler RetrieveAudit = (primary, service, tracing, organizationConfig, parameters) =>
 31100        {
 31101            var firstParam = parameters.FirstOrDefault()?.Value;
 31102            var reference = (firstParam as Entity)?.ToEntityReference() ?? firstParam as EntityReference;
 31103            var config = GetConfig(parameters);
 21104
 31105            if (firstParam != null && reference == null)
 21106            {
 21107                throw new InvalidPluginExecutionException("RetrieveAudit: First Parameter must be an Entity or EntityRef
 21108            }
 21109
 31110            if (reference == null)
 21111            {
 21112                return new ValueExpression(string.Empty, null);
 21113            }
 21114
 31115            var field = CheckedCast<string>(parameters[1]?.Value, "RetrieveAudit: fieldName must be a string");
 21116
 31117            var request = new RetrieveRecordChangeHistoryRequest
 31118            {
 31119                Target = reference
 31120            };
 31121            var audit = service.Execute(request) as RetrieveRecordChangeHistoryResponse;
 21122
 31123            var auditValue = audit.AuditDetailCollection.AuditDetails.Select(d =>
 41124            {
 41125                var detail = d as AttributeAuditDetail;
 31126
 41127                if (detail == null)
 31128                {
 31129                    return null;
 31130                }
 31131
 41132                var oldValue = detail.OldValue.GetAttributeValue<object>(field);
 31133
 41134                return Tuple.Create(PropertyStringifier.Stringify(field, detail.OldValue, service, config), oldValue);
 41135            })
 41136            .FirstOrDefault(t => t != null);
 21137
 31138            return new ValueExpression(auditValue?.Item1 ?? string.Empty, auditValue?.Item2);
 31139        };
 1140
 21141        public static FunctionHandler GetRecordId = (primary, service, tracing, organizationConfig, parameters) =>
 91142        {
 91143            var firstParam = parameters.FirstOrDefault()?.Value;
 91144            var reference = (firstParam as Entity)?.ToEntityReference() ?? firstParam as EntityReference;
 91145            var config = GetConfig(parameters);
 21146
 91147            if (firstParam != null && reference == null)
 21148            {
 21149                throw new InvalidPluginExecutionException("RecordId: First Parameter must be an Entity or EntityReferenc
 21150            }
 21151
 91152            if (reference == null)
 21153            {
 21154                return new ValueExpression(string.Empty, null);
 21155            }
 21156
 91157            var textValue = reference.Id.ToString(config.GetValue<string>("format", "format must be a string", "D"));
 21158
 91159            return new ValueExpression(textValue, reference.Id);
 91160        };
 1161
 21162        public static FunctionHandler GetRecordLogicalName = (primary, service, tracing, organizationConfig, parameters)
 41163        {
 41164            var firstParam = parameters.FirstOrDefault()?.Value;
 41165            var reference = (firstParam as Entity)?.ToEntityReference() ?? firstParam as EntityReference;
 21166
 41167            if (firstParam != null && reference == null)
 21168            {
 21169                throw new InvalidPluginExecutionException("RecordLogicalName: First Parameter must be an Entity or Entit
 21170            }
 21171
 41172            if (reference == null)
 21173            {
 21174                return new ValueExpression(string.Empty, null);
 21175            }
 21176
 41177            return new ValueExpression(reference.LogicalName, reference.LogicalName);
 41178        };
 1179    }
 1180}
 1181#pragma warning restore S1104 // Fields should not have public accessibility