diff --git a/spkl/CrmSvcUtilFilteringService/CrmSvcUtil.FilteringService.csproj b/spkl/CrmSvcUtilFilteringService/CrmSvcUtil.FilteringService.csproj index 04d907c..f17db8e 100644 --- a/spkl/CrmSvcUtilFilteringService/CrmSvcUtil.FilteringService.csproj +++ b/spkl/CrmSvcUtilFilteringService/CrmSvcUtil.FilteringService.csproj @@ -46,6 +46,7 @@ + False diff --git a/spkl/CrmSvcUtilFilteringService/FilteringService.cs b/spkl/CrmSvcUtilFilteringService/FilteringService.cs index 79572a9..7234e33 100644 --- a/spkl/CrmSvcUtilFilteringService/FilteringService.cs +++ b/spkl/CrmSvcUtilFilteringService/FilteringService.cs @@ -24,7 +24,7 @@ public FilteringService(ICodeWriterFilterService defaultService) public bool GenerateAttribute(AttributeMetadata attributeMetadata, IServiceProvider services) { return this.DefaultService.GenerateAttribute(attributeMetadata, services); - } + } public bool GenerateEntity(EntityMetadata entityMetadata, IServiceProvider services) { @@ -44,22 +44,25 @@ public bool GenerateOption(OptionMetadata optionMetadata, IServiceProvider servi { return this.DefaultService.GenerateOption(optionMetadata, services); } - public bool GenerateOptionSet(OptionSetMetadataBase optionSetMetadata, IServiceProvider services) { - // Should we output a enum optionset or just plain OptionSetValue? var globalOptionsets = Config.GetConfig("globalEnums") == "true"; - var enums = Config.GetConfig("picklistEnums") == "true" && (optionSetMetadata.IsGlobal!=true || globalOptionsets); + var enums = Config.GetConfig("picklistEnums") == "true" && (optionSetMetadata?.IsGlobal != true || globalOptionsets); var states = Config.GetConfig("stateEnums") == "true"; - + var optionsetValues = optionSetMetadata as OptionSetMetadata; - if (optionsetValues!=null) + if (optionsetValues != null) { // check if there are any invalid names foreach (var option in optionsetValues.Options) { - string optionSetName = Regex.Replace(option.Label.UserLocalizedLabel.Label, "[^a-zA-Z0-9]", string.Empty); + if (option.Label.UserLocalizedLabel == null) + { + option.Label.UserLocalizedLabel = new Microsoft.Xrm.Sdk.LocalizedLabel("", 1033); + } + var regexRule = new Regex("[^äöåa-z0-9]", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant); + string optionSetName = regexRule.Replace(option.Label.UserLocalizedLabel.Label, string.Empty); if ((optionSetName.Length > 0) && !char.IsLetter(optionSetName, 0)) { option.Label.UserLocalizedLabel.Label = "Number_" + optionSetName; @@ -81,7 +84,7 @@ public bool GenerateOptionSet(OptionSetMetadataBase optionSetMetadata, IServiceP // check if the names are unique in optionset values var duplicateNames = optionsetValues.Options.GroupBy(x => x.Label.UserLocalizedLabel.Label).Where(g => g.Count() > 1).Select(y => y.Key).ToList(); - duplicateNames.ForEach(delegate(string duplicate){ + duplicateNames.ForEach(delegate (string duplicate) { var option = optionsetValues.Options.Where(x => x.Label.UserLocalizedLabel.Label == duplicate).First(); option.Label.UserLocalizedLabel.Label += option.Value.ToString(); // Also set the other labels @@ -91,7 +94,7 @@ public bool GenerateOptionSet(OptionSetMetadataBase optionSetMetadata, IServiceP // 1033 is hard coded in to the default naming service of CrmSvcUtil label.LanguageCode = 1033; } - }); + }); } var optionType = (OptionSetType)optionSetMetadata.OptionSetType.Value; @@ -104,7 +107,7 @@ public bool GenerateOptionSet(OptionSetMetadataBase optionSetMetadata, IServiceP case OptionSetType.Picklist: return enums; default: - return false; + return false; } } diff --git a/spkl/CrmSvcUtilFilteringService/NamingService.cs b/spkl/CrmSvcUtilFilteringService/NamingService.cs new file mode 100644 index 0000000..0f3cc08 --- /dev/null +++ b/spkl/CrmSvcUtilFilteringService/NamingService.cs @@ -0,0 +1,267 @@ +namespace spkl.CrmSvcUtilExtensions +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Globalization; + using System.Linq; + using System.Text; + using System.Text.RegularExpressions; + using Microsoft.Crm.Services.Utility; + using Microsoft.Xrm.Sdk; + using Microsoft.Xrm.Sdk.Metadata; + + public sealed class NamingService : INamingService + { + /// + /// Implement this class if you want to provide your own logic for building names that + /// will appear in the generated code. + /// + private INamingService DefaultNamingService { get; } + + /// + /// This field keeps track of the number of times that options with the same + /// name would have been defined. + /// + private readonly Dictionary> _optionNames; + public NamingService(INamingService namingService) + { + DefaultNamingService = namingService; + _optionNames = new Dictionary>(); + } + + /// + /// Provide a new implementation for finding a name for an OptionSet. If the + /// OptionSet is not global, we want the name to be the concatenation of the Entity's + /// name and the Attribute's name. Otherwise, we can use the default implementation. + /// + public String GetNameForOptionSet(EntityMetadata entityMetadata, OptionSetMetadataBase optionSetMetadata, IServiceProvider services) + { + // Ensure that the OptionSet is not global before using the custom + // implementation. + if (optionSetMetadata.IsGlobal.HasValue && !optionSetMetadata.IsGlobal.Value) + { + // Find the attribute which uses the specified OptionSet. + var attribute = + (from a in entityMetadata.Attributes + where a.AttributeType == AttributeTypeCode.Picklist && ((EnumAttributeMetadata)a).OptionSet.MetadataId == optionSetMetadata.MetadataId + select a).FirstOrDefault(); + + // Check for null, since statuscode attributes on custom entities are not + // global, but their optionsets are not included in the attribute + // metadata of the entity, either. + if (attribute != null) + { + // Concatenate the name of the entity and the name of the attribute + // together to form the OptionSet name. + return $"{DefaultNamingService.GetNameForEntity(entityMetadata, services)}{DefaultNamingService.GetNameForAttribute(entityMetadata, attribute, services)}"; + } + } + + return DefaultNamingService.GetNameForOptionSet(entityMetadata, optionSetMetadata, services); + } + + #region other INamingService Methods + + public String GetNameForAttribute(EntityMetadata entityMetadata, AttributeMetadata attributeMetadata, IServiceProvider services) + { + return DefaultNamingService.GetNameForAttribute(entityMetadata, attributeMetadata, services); + } + + public String GetNameForEntity(EntityMetadata entityMetadata, IServiceProvider services) + { + return DefaultNamingService.GetNameForEntity(entityMetadata, services); + } + + public String GetNameForEntitySet(EntityMetadata entityMetadata, IServiceProvider services) + { + return DefaultNamingService.GetNameForEntitySet(entityMetadata, services); + } + + public String GetNameForMessagePair(SdkMessagePair messagePair, IServiceProvider services) + { + return DefaultNamingService.GetNameForMessagePair(messagePair, services); + } + + /// + /// Handles building the name for an Option of an OptionSet. + /// + public string GetNameForOption(OptionSetMetadataBase optionSetMetadata, OptionMetadata optionMetadata, IServiceProvider services) + { + int lngCode = 0; + String[] arguments = Environment.GetCommandLineArgs(); + + foreach (var arg in arguments) + { + Console.WriteLine("Argument" + arg); + + if (arg.Contains("lngCode")) + { + var split = arg.Split(':'); + if (split.Length == 2) + { + int.TryParse(split[1], out lngCode); + } + } + } + + + var name = string.Empty; + if (optionMetadata is StateOptionMetadata stateOptionMetadata) + { + name = stateOptionMetadata.InvariantName; + } + else + { + var myLng = optionMetadata.Label.LocalizedLabels.FirstOrDefault(l => l.LanguageCode == lngCode); + + if (myLng != null) + name = myLng.Label; + else + { + var defLng = optionMetadata.Label.LocalizedLabels.FirstOrDefault(); + if (defLng != null) + name = defLng.Label; + } + + foreach (var localizedLabel in optionMetadata.Label.LocalizedLabels) + { + if (localizedLabel.LanguageCode == 1033) // English or Finnish + name = localizedLabel.Label; + } + } + + if (string.IsNullOrEmpty(name)) + { + if (optionMetadata.Value != null) name = string.Format(CultureInfo.InvariantCulture, "UnknownLabel{0}", new object[] { optionMetadata.Value.Value }); + } + + name = CreateValidName(name); + name = EnsureValidIdentifier(name); + name = EnsureUniqueOptionName(optionSetMetadata, name); + + return name; + } + + private static readonly Regex NameRegex = new Regex("[äöåa-z0-9_]*", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant); + private static string CreateValidName(string name) + { + name = TransformToPascalCase(name); + + string input = name.Replace("$", "CurrencySymbol_").Replace("(", "_"); + var stringBuilder = new StringBuilder(); + for (Match match = NameRegex.Match(input); match.Success; match = match.NextMatch()) + stringBuilder.Append(match.Value); + + var newName = stringBuilder.ToString(); + Console.WriteLine(newName); + + return newName; + } + + + /// + /// Checks to make sure that the name begins with a valid character. If the name + /// does not begin with a valid character, then add an underscore to the + /// beginning of the name. + /// + private static String EnsureValidIdentifier(String name) + { + + // Check to make sure that the option set begins with a word character + // or underscore. + var pattern = @"^[ÄäÖöÅåA-Za-z_][ÄäÖöÅåA-Za-z0-9_]*$"; + if (!Regex.IsMatch(name, pattern)) + { + // Prepend an underscore to the name if it is not valid. + name = $"_{name}"; + Trace.TraceInformation($"Name of the option changed to {name}"); + } + return name; + } + + /// + /// Checks to make sure that the name does not already exist for the OptionSet + /// to be generated. + /// + private String EnsureUniqueOptionName(OptionSetMetadataBase metadata, String name) + { + if (_optionNames.ContainsKey(metadata)) + { + if (_optionNames[metadata].ContainsKey(name)) + { + // Increment the number of times that an option with this name has + // been found. + ++_optionNames[metadata][name]; + + // Append the number to the name to create a new, unique name. + var newName = $"{name}_{_optionNames[metadata][name]}"; + + Trace.TraceInformation($"The {metadata.Name} OptionSet already contained a definition for {name}. Changed to {newName}"); + + // Call this function again to make sure that our new name is unique. + return EnsureUniqueOptionName(metadata, newName); + } + } + else + { + // This is the first time this OptionSet has been encountered. Add it to + // the dictionary. + _optionNames[metadata] = new Dictionary(); + } + + // This is the first time this name has been encountered. Begin keeping track + // of the times we've run across it. + _optionNames[metadata][name] = 1; + + return name; + } + + public String GetNameForRelationship(EntityMetadata entityMetadata, RelationshipMetadataBase relationshipMetadata, EntityRole? reflexiveRole, IServiceProvider services) + { + return DefaultNamingService.GetNameForRelationship(entityMetadata, relationshipMetadata, reflexiveRole, services); + } + + public String GetNameForRequestField(SdkMessageRequest request, SdkMessageRequestField requestField, IServiceProvider services) + { + return DefaultNamingService.GetNameForRequestField( + request, requestField, services); + } + + public String GetNameForResponseField(SdkMessageResponse response, SdkMessageResponseField responseField, IServiceProvider services) + { + return DefaultNamingService.GetNameForResponseField(response, responseField, services); + } + + public String GetNameForServiceContext(IServiceProvider services) + { + return DefaultNamingService.GetNameForServiceContext(services); + } + + #endregion + + public static string TransformToPascalCase(string s) + { + TextInfo txtInfo = new CultureInfo(CultureInfo.InvariantCulture.Name, false).TextInfo; + return Regex.Replace(txtInfo.ToTitleCase(s), @"\W", ""); + } + + + public static string RemoveDiacritics(string text) + { + var normalizedString = text.Normalize(NormalizationForm.FormD); + var stringBuilder = new StringBuilder(); + + foreach (var c in normalizedString) + { + var unicodeCategory = CharUnicodeInfo.GetUnicodeCategory(c); + if (unicodeCategory != UnicodeCategory.NonSpacingMark) + { + stringBuilder.Append(c); + } + } + + return stringBuilder.ToString().Normalize(NormalizationForm.FormC); + } + } +} \ No newline at end of file diff --git a/spkl/SparkleXrm.Tasks/Tasks/EarlyBoundClassGeneratorTask.cs b/spkl/SparkleXrm.Tasks/Tasks/EarlyBoundClassGeneratorTask.cs index 585f90b..475e108 100644 --- a/spkl/SparkleXrm.Tasks/Tasks/EarlyBoundClassGeneratorTask.cs +++ b/spkl/SparkleXrm.Tasks/Tasks/EarlyBoundClassGeneratorTask.cs @@ -22,7 +22,7 @@ public class EarlyBoundClassGeneratorTask : BaseTask /// Use method to /// mask password from connection string. /// - public string ConectionString {get;set;} + public string ConectionString { get; set; } private string _folder; public EarlyBoundClassGeneratorTask(IOrganizationService service, ITrace trace) : base(service, trace) { @@ -34,18 +34,19 @@ public EarlyBoundClassGeneratorTask(OrganizationServiceContext ctx, ITrace trace protected override void ExecuteInternal(string folder, OrganizationServiceContext ctx) { - + _trace.WriteLine("Searching for plugin config in '{0}'", folder); + var configs = ServiceLocator.ConfigFileFactory.FindConfig(folder); foreach (var config in configs) { _trace.WriteLine("Using Config '{0}'", config.filePath); - + CreateEarlyBoundTypes(ctx, config); } _trace.WriteLine("Processed {0} config(s)", configs.Count); - + } public void CreateEarlyBoundTypes(OrganizationServiceContext ctx, ConfigFile config) @@ -54,7 +55,7 @@ public void CreateEarlyBoundTypes(OrganizationServiceContext ctx, ConfigFile con // locate the CrmSvcUtil package folder var targetfolder = ServiceLocator.DirectoryService.GetApplicationDirectory(); - + // If we are running in VS, then move up past bin/Debug if (targetfolder.Contains(@"bin\Debug") || targetfolder.Contains(@"bin\Release")) { @@ -73,7 +74,7 @@ public void CreateEarlyBoundTypes(OrganizationServiceContext ctx, ConfigFile con // Copy the filtering assembly var filteringAssemblyPathString = ServiceLocator.DirectoryService.SimpleSearch(targetfolder + @"\..\..", "spkl.CrmSvcUtilExtensions.dll"); - + if (string.IsNullOrEmpty(filteringAssemblyPathString)) { throw new SparkleTaskException(SparkleTaskException.ExceptionTypes.UTILSNOTFOUND, @@ -90,7 +91,7 @@ public void CreateEarlyBoundTypes(OrganizationServiceContext ctx, ConfigFile con var earlyBoundTypeConfigs = config.GetEarlyBoundConfig(this.Profile); foreach (var earlyboundconfig in earlyBoundTypeConfigs) { - if(string.IsNullOrEmpty(earlyboundconfig.entities) && earlyboundconfig.entityCollection?.Length > 0) + if (string.IsNullOrEmpty(earlyboundconfig.entities) && earlyboundconfig.entityCollection?.Length > 0) { earlyboundconfig.entities = string.Join(",", earlyboundconfig.entityCollection); } @@ -126,7 +127,7 @@ public void CreateEarlyBoundTypes(OrganizationServiceContext ctx, ConfigFile con earlyboundconfig.serviceContextName }"" /GenerateActions:""{ !String.IsNullOrEmpty(earlyboundconfig.actions) - }"" /codewriterfilter:""spkl.CrmSvcUtilExtensions.FilteringService,spkl.CrmSvcUtilExtensions"" /codewritermessagefilter:""spkl.CrmSvcUtilExtensions.MessageFilteringService,spkl.CrmSvcUtilExtensions"" /codegenerationservice:""spkl.CrmSvcUtilExtensions.CodeGenerationService, spkl.CrmSvcUtilExtensions"" /metadataproviderqueryservice:""spkl.CrmSvcUtilExtensions.MetadataProviderQueryService,spkl.CrmSvcUtilExtensions"""; + }"" /codewriterfilter:""spkl.CrmSvcUtilExtensions.FilteringService,spkl.CrmSvcUtilExtensions"" /namingservice:""spkl.CrmSvcUtilExtensions.NamingService,spkl.CrmSvcUtilExtensions"" /codewritermessagefilter:""spkl.CrmSvcUtilExtensions.MessageFilteringService,spkl.CrmSvcUtilExtensions"" /codegenerationservice:""spkl.CrmSvcUtilExtensions.CodeGenerationService, spkl.CrmSvcUtilExtensions"" /metadataproviderqueryservice:""spkl.CrmSvcUtilExtensions.MetadataProviderQueryService,spkl.CrmSvcUtilExtensions"""; var procStart = new ProcessStartInfo(crmsvcutilPath, parameters) { @@ -140,10 +141,13 @@ public void CreateEarlyBoundTypes(OrganizationServiceContext ctx, ConfigFile con _trace.WriteLine("Running {0} {1}", crmsvcutilPath, HideConnectionStringPassword(parameters)); + var exitCode = 0; Process proc = null; + try { + proc = Process.Start(procStart); proc.OutputDataReceived += Proc_OutputDataReceived; proc.ErrorDataReceived += Proc_OutputDataReceived; @@ -153,14 +157,14 @@ public void CreateEarlyBoundTypes(OrganizationServiceContext ctx, ConfigFile con proc.CancelOutputRead(); proc.CancelErrorRead(); } + finally { exitCode = proc.ExitCode; - proc.Close(); } - if (exitCode!=0) + if (exitCode != 0) { throw new SparkleTaskException(SparkleTaskException.ExceptionTypes.CRMSVCUTIL_ERROR, $"CrmSvcUtil exited with error {exitCode}"); } @@ -212,7 +216,8 @@ private string HideConnectionStringPassword(string logMessage) var indexOfPwOnConnStr = ConectionString.IndexOf("Password", StringComparison.InvariantCultureIgnoreCase); - if(indexOfPwOnConnStr < 0) { + if (indexOfPwOnConnStr < 0) + { return logMessage; }