diff --git a/spkl/CrmSvcUtilFilteringService/CrmSvcUtil.FilteringService.csproj b/spkl/CrmSvcUtilFilteringService/CrmSvcUtil.FilteringService.csproj index 4c4e5be2..3d11626e 100644 --- a/spkl/CrmSvcUtilFilteringService/CrmSvcUtil.FilteringService.csproj +++ b/spkl/CrmSvcUtilFilteringService/CrmSvcUtil.FilteringService.csproj @@ -46,6 +46,7 @@ + False diff --git a/spkl/CrmSvcUtilFilteringService/NamingService.cs b/spkl/CrmSvcUtilFilteringService/NamingService.cs new file mode 100644 index 00000000..d28cf3dd --- /dev/null +++ b/spkl/CrmSvcUtilFilteringService/NamingService.cs @@ -0,0 +1,275 @@ +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) + //{ + // var name = DefaultNamingService.GetNameForOption(optionSetMetadata, optionMetadata, services); + // Trace.TraceInformation(String.Format("The name of this option is {0}", name)); + // name = EnsureValidIdentifier(name); + // name = EnsureUniqueOptionName(optionSetMetadata, name); + // return name; + //} + + /// + /// Handles building the name for an Option of an OptionSet. + /// + public string GetNameForOption(OptionSetMetadataBase optionSetMetadata, OptionMetadata optionMetadata, IServiceProvider services) + { + int lngCode = 0; + + //TODO I don't know is possible add argument to preffered language code + + //var arguments = Environment.GetCommandLineArgs(); + //foreach (var arg in arguments) + //{ + // 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; + } + } + + 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 = RemoveDiacritics(name); + name = RemoveAllWhiteSpace(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(); + + 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) + { + name = RemoveDiacritics(name); + name = RemoveAllWhiteSpace(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 RemoveAllWhiteSpace(string s) + { + return Regex.Replace(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 5f21dd4c..96960f23 100644 --- a/spkl/SparkleXrm.Tasks/Tasks/EarlyBoundClassGeneratorTask.cs +++ b/spkl/SparkleXrm.Tasks/Tasks/EarlyBoundClassGeneratorTask.cs @@ -118,7 +118,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"" /codewritermessagefilter:""spkl.CrmSvcUtilExtensions.MessageFilteringService,spkl.CrmSvcUtilExtensions"" /codegenerationservice:""spkl.CrmSvcUtilExtensions.CodeGenerationService, spkl.CrmSvcUtilExtensions"" /metadataproviderqueryservice:""spkl.CrmSvcUtilExtensions.MetadataProviderQueryService,spkl.CrmSvcUtilExtensions"" /namingservice:""spkl.CrmSvcUtilExtensions.NamingService,spkl.CrmSvcUtilExtensions"" "; var procStart = new ProcessStartInfo(crmsvcutilPath, parameters) {