| | 1 | | using System; |
| | 2 | | using System.Collections.Generic; |
| | 3 | | using System.IO; |
| | 4 | | using System.Linq; |
| | 5 | | using System.Net; |
| | 6 | | using System.Runtime.Serialization.Json; |
| | 7 | | using System.Text; |
| | 8 | | using System.Text.RegularExpressions; |
| | 9 | | using System.Threading.Tasks; |
| | 10 | | using Microsoft.Xrm.Sdk; |
| | 11 | | using Microsoft.Xrm.Sdk.Query; |
| | 12 | | using Xrm.Oss.XTL.Interpreter; |
| | 13 | |
|
| | 14 | | namespace Xrm.Oss.XTL.Templating |
| | 15 | | { |
| | 16 | | public class XTLProcessor : IPlugin |
| | 17 | | { |
| | 18 | | private ProcessorConfig _config; |
| | 19 | | private OrganizationConfig _organizationConfig; |
| | 20 | |
|
| 30 | 21 | | public XTLProcessor(): this("", "") { } |
| | 22 | |
|
| 26 | 23 | | public XTLProcessor (string unsecure, string secure) |
| 26 | 24 | | { |
| 26 | 25 | | _config = ProcessorConfig.Parse(unsecure); |
| 26 | 26 | | _organizationConfig = OrganizationConfig.Parse(secure); |
| 26 | 27 | | } |
| | 28 | |
|
| | 29 | | public void Execute(IServiceProvider serviceProvider) |
| 26 | 30 | | { |
| 26 | 31 | | var context = serviceProvider.GetService(typeof(IPluginExecutionContext)) as IPluginExecutionContext; |
| 26 | 32 | | var crmTracing = serviceProvider.GetService(typeof(ITracingService)) as ITracingService; |
| 26 | 33 | | var tracing = new PersistentTracingService(crmTracing); |
| 26 | 34 | | var serviceFactory = serviceProvider.GetService(typeof(IOrganizationServiceFactory)) as IOrganizationService |
| 26 | 35 | | var service = serviceFactory.CreateOrganizationService(null); |
| | 36 | |
|
| 26 | 37 | | if (context.InputParameters.ContainsKey("jsonInput")) |
| 10 | 38 | | { |
| 10 | 39 | | HandleCustomAction(context, tracing, service); |
| 8 | 40 | | } |
| | 41 | | else |
| 16 | 42 | | { |
| 16 | 43 | | HandleNonCustomAction(context, tracing, service); |
| 15 | 44 | | } |
| 23 | 45 | | } |
| | 46 | |
|
| | 47 | | private void TriggerUpdateConditionally(string newValue, Entity target, ProcessorConfig config, IOrganizationSer |
| 21 | 48 | | { |
| 21 | 49 | | if (!config.TriggerUpdate) |
| 12 | 50 | | { |
| 12 | 51 | | return; |
| | 52 | | } |
| | 53 | |
|
| 9 | 54 | | if (string.IsNullOrEmpty(config.TargetField)) |
| 2 | 55 | | { |
| 2 | 56 | | throw new InvalidPluginExecutionException("Target field is required when setting the 'triggerUpdate' fla |
| | 57 | | } |
| | 58 | |
|
| 7 | 59 | | if (config.ForceUpdate || !string.Equals(target.GetAttributeValue<string>(config.TargetField), newValue)) |
| 5 | 60 | | { |
| 5 | 61 | | var updateObject = new Entity |
| 5 | 62 | | { |
| 5 | 63 | | LogicalName = target.LogicalName, |
| 5 | 64 | | Id = target.Id |
| 5 | 65 | | }; |
| | 66 | |
|
| 5 | 67 | | updateObject[config.TargetField] = newValue; |
| | 68 | |
|
| 5 | 69 | | service.Update(updateObject); |
| 5 | 70 | | } |
| 19 | 71 | | } |
| | 72 | |
|
| | 73 | | private void HandleCustomAction(IPluginExecutionContext context, PersistentTracingService tracing, IOrganization |
| 10 | 74 | | { |
| 10 | 75 | | var config = ProcessorConfig.Parse(context.InputParameters["jsonInput"] as string); |
| | 76 | |
|
| 10 | 77 | | if (config.Target == null && config.TargetEntity == null) |
| 1 | 78 | | { |
| 1 | 79 | | throw new InvalidPluginExecutionException("Target property inside JSON parameters is needed for custom a |
| | 80 | | } |
| | 81 | |
|
| | 82 | | ColumnSet columnSet; |
| | 83 | |
|
| 9 | 84 | | if (config.TargetColumns != null) |
| 1 | 85 | | { |
| 1 | 86 | | columnSet = new ColumnSet(config.TargetColumns); |
| 1 | 87 | | } |
| | 88 | | else |
| 8 | 89 | | { |
| 8 | 90 | | columnSet = new ColumnSet(true); |
| 8 | 91 | | } |
| | 92 | |
|
| | 93 | | try |
| 9 | 94 | | { |
| 9 | 95 | | var dataSource = config.TargetEntity != null ? config.TargetEntity : service.Retrieve(config.Target.Logi |
| | 96 | |
|
| 9 | 97 | | if (!CheckExecutionCriteria(config, dataSource, service, tracing)) |
| 1 | 98 | | { |
| 1 | 99 | | tracing.Trace("Execution criteria not met, aborting"); |
| | 100 | |
|
| 1 | 101 | | var abortResult = new ProcessingResult |
| 1 | 102 | | { |
| 1 | 103 | | Success = true, |
| 1 | 104 | | Result = config.Template, |
| 1 | 105 | | TraceLog = tracing.TraceLog |
| 1 | 106 | | }; |
| 1 | 107 | | context.OutputParameters["jsonOutput"] = SerializeResult(abortResult); |
| | 108 | |
|
| 1 | 109 | | return; |
| | 110 | | } |
| | 111 | |
|
| 8 | 112 | | var templateText = RetrieveTemplate(config.Template, config.TemplateField, dataSource, service, tracing) |
| | 113 | |
|
| 8 | 114 | | if (string.IsNullOrEmpty(templateText)) |
| 0 | 115 | | { |
| 0 | 116 | | tracing.Trace("Template is empty, aborting"); |
| 0 | 117 | | return; |
| | 118 | | } |
| | 119 | |
|
| 8 | 120 | | var output = TokenMatcher.ProcessTokens(templateText, dataSource, new OrganizationConfig { OrganizationU |
| | 121 | |
|
| 8 | 122 | | var result = new ProcessingResult |
| 8 | 123 | | { |
| 8 | 124 | | Success = true, |
| 8 | 125 | | Result = output, |
| 8 | 126 | | TraceLog = tracing.TraceLog |
| 8 | 127 | | }; |
| 8 | 128 | | context.OutputParameters["jsonOutput"] = SerializeResult(result); |
| | 129 | |
|
| 8 | 130 | | TriggerUpdateConditionally(output, dataSource, config, service); |
| 6 | 131 | | } |
| 2 | 132 | | catch (Exception ex) |
| 2 | 133 | | { |
| 2 | 134 | | var result = new ProcessingResult |
| 2 | 135 | | { |
| 2 | 136 | | Success = false, |
| 2 | 137 | | Error = ex.Message, |
| 2 | 138 | | TraceLog = tracing.TraceLog |
| 2 | 139 | | }; |
| 2 | 140 | | context.OutputParameters["jsonOutput"] = SerializeResult(result); |
| | 141 | |
|
| 2 | 142 | | if (config.ThrowOnCustomActionError) |
| 1 | 143 | | { |
| 1 | 144 | | throw; |
| | 145 | | } |
| 1 | 146 | | } |
| 8 | 147 | | } |
| | 148 | |
|
| | 149 | | private string SerializeResult(ProcessingResult result) |
| 11 | 150 | | { |
| 11 | 151 | | var serializer = new DataContractJsonSerializer(typeof(ProcessingResult)); |
| | 152 | |
|
| 11 | 153 | | using (var memoryStream = new MemoryStream()) |
| 11 | 154 | | { |
| 11 | 155 | | serializer.WriteObject(memoryStream, result); |
| | 156 | |
|
| 11 | 157 | | memoryStream.Position = 0; |
| | 158 | |
|
| 11 | 159 | | using (var streamReader = new StreamReader(memoryStream)) |
| 11 | 160 | | { |
| 11 | 161 | | return streamReader.ReadToEnd(); |
| | 162 | | } |
| | 163 | | } |
| 11 | 164 | | } |
| | 165 | |
|
| | 166 | | private void HandleNonCustomAction(IPluginExecutionContext context, ITracingService tracing, IOrganizationServic |
| 16 | 167 | | { |
| 16 | 168 | | var target = context.InputParameters.ContainsKey("Target") ? context.InputParameters["Target"] as Entity : n |
| | 169 | |
|
| 16 | 170 | | if (target == null) |
| 0 | 171 | | { |
| 0 | 172 | | return; |
| | 173 | | } |
| | 174 | |
|
| 16 | 175 | | var targetField = _config.TargetField; |
| 16 | 176 | | var template = _config.Template; |
| 16 | 177 | | var templateField = _config.TemplateField; |
| | 178 | |
|
| 16 | 179 | | var dataSource = GenerateDataSource(context, target); |
| | 180 | |
|
| 16 | 181 | | if (!CheckExecutionCriteria(_config, dataSource, service, tracing)) |
| 1 | 182 | | { |
| 1 | 183 | | tracing.Trace("Execution criteria not met, aborting"); |
| 1 | 184 | | return; |
| | 185 | | } |
| | 186 | |
|
| 15 | 187 | | ValidateConfig(targetField, template, templateField); |
| 14 | 188 | | var templateText = RetrieveTemplate(template, templateField, dataSource, service, tracing); |
| | 189 | |
|
| 14 | 190 | | if (string.IsNullOrEmpty(templateText)) |
| 1 | 191 | | { |
| 1 | 192 | | tracing.Trace("Template is empty, aborting"); |
| 1 | 193 | | return; |
| | 194 | | } |
| | 195 | |
|
| 13 | 196 | | var output = TokenMatcher.ProcessTokens(templateText, dataSource, _organizationConfig, service, tracing); |
| | 197 | |
|
| 13 | 198 | | target[targetField] = output; |
| 13 | 199 | | TriggerUpdateConditionally(output, dataSource, _config, service); |
| 15 | 200 | | } |
| | 201 | |
|
| | 202 | | private static string RetrieveTemplate(string template, string templateField, Entity dataSource, IOrganizationSe |
| 22 | 203 | | { |
| | 204 | | string templateText; |
| | 205 | |
|
| 22 | 206 | | if (!string.IsNullOrEmpty(template)) |
| 8 | 207 | | { |
| 8 | 208 | | templateText = template; |
| 8 | 209 | | } |
| 14 | 210 | | else if (!string.IsNullOrEmpty(templateField)) |
| 14 | 211 | | { |
| 14 | 212 | | if (new Regex("^[a-zA-Z_0-9]*$").IsMatch(templateField)) |
| 13 | 213 | | { |
| 13 | 214 | | templateText = dataSource.GetAttributeValue<string>(templateField); |
| 13 | 215 | | } |
| | 216 | | else |
| 1 | 217 | | { |
| 1 | 218 | | templateText = new XTLInterpreter(templateField, dataSource, null, service, tracing).Produce(); |
| 1 | 219 | | } |
| 14 | 220 | | } |
| | 221 | | else |
| 0 | 222 | | { |
| 0 | 223 | | throw new InvalidDataException("You must either pass a template text or define a template field"); |
| | 224 | | } |
| | 225 | |
|
| | 226 | |
|
| | 227 | | // Templates inside e-mails will be HTML encoded |
| 22 | 228 | | templateText = WebUtility.HtmlDecode(templateText); |
| 22 | 229 | | return templateText; |
| 22 | 230 | | } |
| | 231 | |
|
| | 232 | | private void ValidateConfig(string targetField, string template, string templateField) |
| 15 | 233 | | { |
| 15 | 234 | | if (string.IsNullOrEmpty(targetField)) |
| 0 | 235 | | { |
| 0 | 236 | | throw new InvalidPluginExecutionException("Target field was null, please adapt the unsecure config!"); |
| | 237 | | } |
| | 238 | |
|
| 15 | 239 | | if (string.IsNullOrEmpty(template) && string.IsNullOrEmpty(templateField)) |
| 1 | 240 | | { |
| 1 | 241 | | throw new InvalidPluginExecutionException("Both template and template field were null, please set one of |
| | 242 | | } |
| 14 | 243 | | } |
| | 244 | |
|
| | 245 | | private bool CheckExecutionCriteria(ProcessorConfig config, Entity dataSource, IOrganizationService service, ITr |
| 25 | 246 | | { |
| 25 | 247 | | if (!string.IsNullOrEmpty(config.ExecutionCriteria)) |
| 3 | 248 | | { |
| 3 | 249 | | var criteriaInterpreter = new XTLInterpreter(config.ExecutionCriteria, dataSource, _organizationConfig, |
| 3 | 250 | | var result = criteriaInterpreter.Produce(); |
| | 251 | |
|
| 3 | 252 | | var criteriaMatched = false; |
| 3 | 253 | | bool.TryParse(result, out criteriaMatched); |
| | 254 | |
|
| 3 | 255 | | if (!criteriaMatched) |
| 2 | 256 | | { |
| 2 | 257 | | return false; |
| | 258 | | } |
| | 259 | |
|
| 1 | 260 | | return true; |
| | 261 | | } |
| | 262 | |
|
| 22 | 263 | | return true; |
| 25 | 264 | | } |
| | 265 | |
|
| | 266 | | private Entity GenerateDataSource(IPluginExecutionContext context, Entity target) |
| 16 | 267 | | { |
| | 268 | | // "Merge" pre entity images with targets for having all attribute values |
| 16 | 269 | | var dataSource = new Entity |
| 16 | 270 | | { |
| 16 | 271 | | LogicalName = target.LogicalName, |
| 16 | 272 | | Id = target.Id |
| 16 | 273 | | }; |
| | 274 | |
|
| 50 | 275 | | foreach (var image in context.PreEntityImages.Values) |
| 1 | 276 | | { |
| 5 | 277 | | foreach (var property in image.Attributes) |
| 1 | 278 | | { |
| 1 | 279 | | dataSource[property.Key] = property.Value; |
| 1 | 280 | | } |
| 1 | 281 | | } |
| | 282 | |
|
| 218 | 283 | | foreach (var property in target.Attributes) |
| 85 | 284 | | { |
| 85 | 285 | | dataSource[property.Key] = property.Value; |
| 85 | 286 | | } |
| | 287 | |
|
| 16 | 288 | | return dataSource; |
| 16 | 289 | | } |
| | 290 | | } |
| | 291 | | } |