using System.Collections.Concurrent; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; namespace Umbraco.Cms.Core; public sealed class UdiParser { private static readonly ConcurrentDictionary RootUdis = new(); static UdiParser() => // initialize with known (built-in) Udi types // we will add scanned types later on UdiTypes = new ConcurrentDictionary(GetKnownUdiTypes()); internal static ConcurrentDictionary UdiTypes { get; private set; } /// /// Internal API for tests to resets all udi types back to only the known udi types. /// [EditorBrowsable(EditorBrowsableState.Never)] public static void ResetUdiTypes() => UdiTypes = new ConcurrentDictionary(GetKnownUdiTypes()); /// /// Converts the string representation of an entity identifier into the equivalent Udi instance. /// /// The string to convert. /// An Udi instance that contains the value that was parsed. public static Udi Parse(string s) { ParseInternal(s, false, false, out Udi? udi); return udi!; } /// /// Converts the string representation of an entity identifier into the equivalent Udi instance. /// /// The string to convert. /// A value indicating whether to only deal with known types. /// An Udi instance that contains the value that was parsed. /// /// /// If is true, and the string could not be parsed because /// the entity type was not known, the method succeeds but sets udito an /// value. /// /// /// If is true, assemblies are not scanned for types, /// and therefore only builtin types may be known. Unless scanning already took place. /// /// public static Udi Parse(string s, bool knownTypes) { ParseInternal(s, false, knownTypes, out Udi? udi); return udi!; } /// /// Converts the string representation of an entity identifier into the equivalent Udi instance. /// /// The string to convert. /// An Udi instance that contains the value that was parsed. /// A boolean value indicating whether the string could be parsed. public static bool TryParse(string s, [MaybeNullWhen(false)] out Udi udi) => ParseInternal(s, true, false, out udi); /// /// Converts the string representation of an entity identifier into the equivalent Udi instance. /// /// The string to convert. /// An Udi instance that contains the value that was parsed. /// A boolean value indicating whether the string could be parsed. public static bool TryParse(string? s, [NotNullWhen(true)] out T? udi) where T : Udi { var result = ParseInternal(s, true, false, out Udi? parsed); if (result && parsed is T t) { udi = t; return true; } udi = null; return false; } /// /// Converts the string representation of an entity identifier into the equivalent Udi instance. /// /// The string to convert. /// A value indicating whether to only deal with known types. /// An Udi instance that contains the value that was parsed. /// A boolean value indicating whether the string could be parsed. /// /// /// If is true, and the string could not be parsed because /// the entity type was not known, the method returns false but still sets udi /// to an value. /// /// /// If is true, assemblies are not scanned for types, /// and therefore only builtin types may be known. Unless scanning already took place. /// /// public static bool TryParse(string? s, bool knownTypes, [MaybeNullWhen(false)] out Udi udi) => ParseInternal(s, true, knownTypes, out udi); /// /// Registers a custom entity type. /// /// /// public static void RegisterUdiType(string entityType, UdiType udiType) => UdiTypes.TryAdd(entityType, udiType); internal static Udi GetRootUdi(string entityType) => RootUdis.GetOrAdd(entityType, x => { if (UdiTypes.TryGetValue(x, out UdiType udiType) == false) { throw new ArgumentException(string.Format("Unknown entity type \"{0}\".", entityType)); } return udiType == UdiType.StringUdi ? new StringUdi(entityType, string.Empty) : new GuidUdi(entityType, Guid.Empty); }); private static bool ParseInternal(string? s, bool tryParse, bool knownTypes, [MaybeNullWhen(false)] out Udi udi) { udi = null; if (Uri.IsWellFormedUriString(s, UriKind.Absolute) == false || Uri.TryCreate(s, UriKind.Absolute, out Uri? uri) == false) { if (tryParse) { return false; } throw new FormatException(string.Format("String \"{0}\" is not a valid udi.", s)); } var entityType = uri.Host; if (UdiTypes.TryGetValue(entityType, out UdiType udiType) == false) { if (knownTypes) { // not knowing the type is not an error // just return the unknown type udi udi = UnknownTypeUdi.Instance; return false; } if (tryParse) { return false; } throw new FormatException(string.Format("Unknown entity type \"{0}\".", entityType)); } var path = uri.AbsolutePath.TrimStart('/'); if (udiType == UdiType.GuidUdi) { if (path == string.Empty) { udi = GetRootUdi(uri.Host); return true; } if (Guid.TryParse(path, out Guid guid) == false) { if (tryParse) { return false; } throw new FormatException(string.Format("String \"{0}\" is not a valid udi.", s)); } udi = new GuidUdi(uri.Host, guid); return true; } if (udiType == UdiType.StringUdi) { udi = path == string.Empty ? GetRootUdi(uri.Host) : new StringUdi(uri.Host, Uri.UnescapeDataString(path)); return true; } if (tryParse) { return false; } throw new InvalidOperationException(string.Format("Invalid udi type \"{0}\".", udiType)); } public static Dictionary GetKnownUdiTypes() => new() { { Constants.UdiEntityType.Unknown, UdiType.Unknown }, // GUID UDI types { Constants.UdiEntityType.AnyGuid, UdiType.GuidUdi }, { Constants.UdiEntityType.DataType, UdiType.GuidUdi }, { Constants.UdiEntityType.DataTypeContainer, UdiType.GuidUdi }, { Constants.UdiEntityType.DictionaryItem, UdiType.GuidUdi }, { Constants.UdiEntityType.Document, UdiType.GuidUdi }, { Constants.UdiEntityType.DocumentBlueprint, UdiType.GuidUdi }, { Constants.UdiEntityType.DocumentBlueprintContainer, UdiType.GuidUdi }, { Constants.UdiEntityType.DocumentType, UdiType.GuidUdi }, { Constants.UdiEntityType.DocumentTypeContainer, UdiType.GuidUdi }, { Constants.UdiEntityType.Element, UdiType.GuidUdi }, { Constants.UdiEntityType.Media, UdiType.GuidUdi }, { Constants.UdiEntityType.MediaType, UdiType.GuidUdi }, { Constants.UdiEntityType.MediaTypeContainer, UdiType.GuidUdi }, { Constants.UdiEntityType.Member, UdiType.GuidUdi }, { Constants.UdiEntityType.MemberGroup, UdiType.GuidUdi }, { Constants.UdiEntityType.MemberType, UdiType.GuidUdi }, { Constants.UdiEntityType.Relation, UdiType.GuidUdi }, { Constants.UdiEntityType.RelationType, UdiType.GuidUdi }, { Constants.UdiEntityType.Template, UdiType.GuidUdi }, { Constants.UdiEntityType.User, UdiType.GuidUdi }, { Constants.UdiEntityType.UserGroup, UdiType.GuidUdi }, { Constants.UdiEntityType.Webhook, UdiType.GuidUdi }, // String UDI types { Constants.UdiEntityType.AnyString, UdiType.StringUdi }, { Constants.UdiEntityType.Language, UdiType.StringUdi }, { Constants.UdiEntityType.MediaFile, UdiType.StringUdi }, { Constants.UdiEntityType.PartialView, UdiType.StringUdi }, { Constants.UdiEntityType.Script, UdiType.StringUdi }, { Constants.UdiEntityType.Stylesheet, UdiType.StringUdi }, { Constants.UdiEntityType.TemplateFile, UdiType.StringUdi }, // Forms UDI types { Constants.UdiEntityType.FormsDataSource, UdiType.GuidUdi }, { Constants.UdiEntityType.FormsForm, UdiType.GuidUdi }, { Constants.UdiEntityType.FormsPreValue, UdiType.GuidUdi }, }; }