using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using Umbraco.Core.Deploy; using Umbraco.Core.Composing; namespace Umbraco.Core { /// /// Represents an entity identifier. /// /// An Udi can be fully qualified or "closed" eg umb://document/{guid} or "open" eg umb://document. [TypeConverter(typeof(UdiTypeConverter))] public abstract class Udi : IComparable { // notes - see U4-10409 // if this class is used during application pre-start it cannot scans the assemblies, // this is addressed by lazily-scanning, with the following caveats: // - parsing a root udi still requires a scan and therefore still breaks // - parsing an invalid udi ("umb://should-be-guid/") corrupts KnowUdiTypes private static volatile bool _scanned; private static readonly object ScanLocker = new object(); private static ConcurrentDictionary _udiTypes; private static readonly ConcurrentDictionary RootUdis = new ConcurrentDictionary(); internal readonly Uri UriValue; // internal for UdiRange /// /// Initializes a new instance of the Udi class. /// /// The entity type part of the identifier. /// The string value of the identifier. protected Udi(string entityType, string stringValue) { EntityType = entityType; UriValue = new Uri(stringValue); } /// /// Initializes a new instance of the Udi class. /// /// The uri value of the identifier. protected Udi(Uri uriValue) { EntityType = uriValue.Host; UriValue = uriValue; } static Udi() { // initialize with known (built-in) Udi types // we will add scanned types later on _udiTypes = new ConcurrentDictionary(Constants.UdiEntityType.GetTypes()); } // for tests, totally unsafe internal static void ResetUdiTypes() { _udiTypes = new ConcurrentDictionary(Constants.UdiEntityType.GetTypes()); _scanned = false; } /// /// Gets the entity type part of the identifier. /// public string EntityType { get; private set; } public int CompareTo(Udi other) { return string.Compare(UriValue.ToString(), other.UriValue.ToString(), StringComparison.InvariantCultureIgnoreCase); } public override string ToString() { // UriValue is created in the ctor and is never null // use AbsoluteUri here and not ToString else it's not encoded! return UriValue.AbsoluteUri; } /// /// 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) { Udi udi; ParseInternal(s, false, false, out 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) { Udi udi; ParseInternal(s, false, knownTypes, out 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, out Udi udi) { return ParseInternal(s, true, false, out 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. /// 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, out Udi udi) { return ParseInternal(s, true, knownTypes, out udi); } private static bool ParseInternal(string s, bool tryParse, bool knownTypes, out Udi udi) { if (knownTypes == false) EnsureScanForUdiTypes(); udi = null; Uri uri; if (Uri.IsWellFormedUriString(s, UriKind.Absolute) == false || Uri.TryCreate(s, UriKind.Absolute, out uri) == false) { if (tryParse) return false; throw new FormatException(string.Format("String \"{0}\" is not a valid udi.", s)); } var entityType = uri.Host; UdiType udiType; if (_udiTypes.TryGetValue(entityType, out 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; } Guid guid; if (Guid.TryParse(path, out 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)); } private static Udi GetRootUdi(string entityType) { EnsureScanForUdiTypes(); return RootUdis.GetOrAdd(entityType, x => { UdiType udiType; if (_udiTypes.TryGetValue(x, out udiType) == false) throw new ArgumentException(string.Format("Unknown entity type \"{0}\".", entityType)); return udiType == UdiType.StringUdi ? (Udi)new StringUdi(entityType, string.Empty) : new GuidUdi(entityType, Guid.Empty); }); } /// /// When required scan assemblies for known UDI types based on instances /// /// /// This is only required when needing to resolve root udis /// private static void EnsureScanForUdiTypes() { if (_scanned) return; lock (ScanLocker) { // Scan for unknown UDI types // there is no way we can get the "registered" service connectors, as registration // happens in Deploy, not in Core, and the Udi class belongs to Core - therefore, we // just pick every service connectors - just making sure that not two of them // would register the same entity type, with different udi types (would not make // much sense anyways). var connectors = Current.TypeLoader.GetTypes(); var result = new Dictionary(); foreach (var connector in connectors) { var attrs = connector.GetCustomAttributes(false); foreach (var attr in attrs) { UdiType udiType; if (result.TryGetValue(attr.EntityType, out udiType) && udiType != attr.UdiType) throw new Exception(string.Format("Entity type \"{0}\" is declared by more than one IServiceConnector, with different UdiTypes.", attr.EntityType)); result[attr.EntityType] = attr.UdiType; } } // merge these into the known list foreach (var item in result) _udiTypes.TryAdd(item.Key, item.Value); _scanned = true; } } /// /// Creates a root Udi for an entity type. /// /// The entity type. /// The root Udi for the entity type. public static Udi Create(string entityType) { return GetRootUdi(entityType); } /// /// Creates a string Udi. /// /// The entity type. /// The identifier. /// The string Udi for the entity type and identifier. public static Udi Create(string entityType, string id) { UdiType udiType; if (_udiTypes.TryGetValue(entityType, out udiType) == false) throw new ArgumentException(string.Format("Unknown entity type \"{0}\".", entityType), "entityType"); if (string.IsNullOrWhiteSpace(id)) throw new ArgumentException("Value cannot be null or whitespace.", "id"); if (udiType != UdiType.StringUdi) throw new InvalidOperationException(string.Format("Entity type \"{0}\" does not have string udis.", entityType)); return new StringUdi(entityType, id); } /// /// Creates a Guid Udi. /// /// The entity type. /// The identifier. /// The Guid Udi for the entity type and identifier. public static Udi Create(string entityType, Guid id) { UdiType udiType; if (_udiTypes.TryGetValue(entityType, out udiType) == false) throw new ArgumentException(string.Format("Unknown entity type \"{0}\".", entityType), "entityType"); if (udiType != UdiType.GuidUdi) throw new InvalidOperationException(string.Format("Entity type \"{0}\" does not have guid udis.", entityType)); if (id == default(Guid)) throw new ArgumentException("Cannot be an empty guid.", "id"); return new GuidUdi(entityType, id); } internal static Udi Create(Uri uri) { // if it's a know type go fast and use ctors // else fallback to parsing the string (and guess the type) UdiType udiType; if (_udiTypes.TryGetValue(uri.Host, out udiType) == false) throw new ArgumentException(string.Format("Unknown entity type \"{0}\".", uri.Host), "uri"); if (udiType == UdiType.GuidUdi) return new GuidUdi(uri); if (udiType == UdiType.GuidUdi) return new StringUdi(uri); throw new ArgumentException(string.Format("Uri \"{0}\" is not a valid udi.", uri)); } public void EnsureType(params string[] validTypes) { if (validTypes.Contains(EntityType) == false) throw new Exception(string.Format("Unexpected entity type \"{0}\".", EntityType)); } /// /// Gets a value indicating whether this Udi is a root Udi. /// /// A root Udi points to the "root of all things" for a given entity type, e.g. the content tree root. public abstract bool IsRoot { get; } /// /// Ensures that this Udi is not a root Udi. /// /// This Udi. /// When this Udi is a Root Udi. public Udi EnsureNotRoot() { if (IsRoot) throw new Exception("Root Udi."); return this; } public override bool Equals(object obj) { var other = obj as Udi; return other != null && GetType() == other.GetType() && UriValue == other.UriValue; } public override int GetHashCode() { return UriValue.GetHashCode(); } public static bool operator ==(Udi udi1, Udi udi2) { if (ReferenceEquals(udi1, udi2)) return true; if ((object)udi1 == null || (object)udi2 == null) return false; return udi1.Equals(udi2); } public static bool operator !=(Udi udi1, Udi udi2) { return (udi1 == udi2) == false; } private class UnknownTypeUdi : Udi { private UnknownTypeUdi() : base("unknown", "umb://unknown/") { } public static readonly UnknownTypeUdi Instance = new UnknownTypeUdi(); public override bool IsRoot { get { return false; } } } } }