diff --git a/src/Umbraco.Core/GuidUdi.cs b/src/Umbraco.Core/GuidUdi.cs index 7a740ef58f..5f4f119901 100644 --- a/src/Umbraco.Core/GuidUdi.cs +++ b/src/Umbraco.Core/GuidUdi.cs @@ -32,7 +32,11 @@ namespace Umbraco.Core public GuidUdi(Uri uriValue) : base(uriValue) { - Guid = Guid.Parse(uriValue.AbsolutePath.TrimStart('/')); + Guid guid; + if (Guid.TryParse(uriValue.AbsolutePath.TrimStart('/'), out guid) == false) + throw new FormatException("Url \"" + uriValue + "\" is not a guid entity id."); + + Guid = guid; } /// @@ -43,16 +47,17 @@ namespace Umbraco.Core public new static GuidUdi Parse(string s) { var udi = Udi.Parse(s); - if (!(udi is GuidUdi)) + if (udi is GuidUdi == false) throw new FormatException("String \"" + s + "\" is not a guid entity id."); - return (GuidUdi)udi; + + return (GuidUdi) udi; } public static bool TryParse(string s, out GuidUdi udi) { Udi tmp; udi = null; - if (!TryParse(s, out tmp)) return false; + if (TryParse(s, out tmp) == false) return false; udi = tmp as GuidUdi; return udi != null; } @@ -75,10 +80,9 @@ namespace Umbraco.Core get { return Guid == Guid.Empty; } } - /// public GuidUdi EnsureClosed() { - base.EnsureNotRoot(); + EnsureNotRoot(); return this; } } diff --git a/src/Umbraco.Core/Serialization/KnownTypeUdiJsonConverter.cs b/src/Umbraco.Core/Serialization/KnownTypeUdiJsonConverter.cs new file mode 100644 index 0000000000..e6473e7f8e --- /dev/null +++ b/src/Umbraco.Core/Serialization/KnownTypeUdiJsonConverter.cs @@ -0,0 +1,26 @@ +using System; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Umbraco.Core.Serialization +{ + public class KnownTypeUdiJsonConverter : JsonConverter + { + public override bool CanConvert(Type objectType) + { + return typeof(Udi).IsAssignableFrom(objectType); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + writer.WriteValue(value.ToString()); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + var jo = JToken.ReadFrom(reader); + var val = jo.ToObject(); + return val == null ? null : Udi.Parse(val, true); + } + } +} diff --git a/src/Umbraco.Core/Serialization/UdiJsonConverter.cs b/src/Umbraco.Core/Serialization/UdiJsonConverter.cs index ff62535825..f3dc678ce6 100644 --- a/src/Umbraco.Core/Serialization/UdiJsonConverter.cs +++ b/src/Umbraco.Core/Serialization/UdiJsonConverter.cs @@ -4,12 +4,11 @@ using Newtonsoft.Json.Linq; namespace Umbraco.Core.Serialization { - public class UdiJsonConverter : JsonConverter { public override bool CanConvert(Type objectType) { - return typeof(Udi).IsAssignableFrom(objectType); + return typeof (Udi).IsAssignableFrom(objectType); } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) diff --git a/src/Umbraco.Core/StringUdi.cs b/src/Umbraco.Core/StringUdi.cs index 59eb40af7e..7df791219a 100644 --- a/src/Umbraco.Core/StringUdi.cs +++ b/src/Umbraco.Core/StringUdi.cs @@ -57,17 +57,18 @@ namespace Umbraco.Core public new static StringUdi Parse(string s) { var udi = Udi.Parse(s); - if (!(udi is StringUdi)) + if (udi is StringUdi == false) throw new FormatException("String \"" + s + "\" is not a string entity id."); - return (StringUdi)udi; + + return (StringUdi) udi; } public static bool TryParse(string s, out StringUdi udi) { udi = null; Udi tmp; - if (!TryParse(s, out tmp) || !(tmp is StringUdi)) return false; - udi = (StringUdi)tmp; + if (TryParse(s, out tmp) == false || tmp is StringUdi == false) return false; + udi = (StringUdi) tmp; return true; } @@ -77,10 +78,9 @@ namespace Umbraco.Core get { return Id == string.Empty; } } - /// public StringUdi EnsureClosed() { - base.EnsureNotRoot(); + EnsureNotRoot(); return this; } } diff --git a/src/Umbraco.Core/Udi.cs b/src/Umbraco.Core/Udi.cs index 142bf025a9..a76c692661 100644 --- a/src/Umbraco.Core/Udi.cs +++ b/src/Umbraco.Core/Udi.cs @@ -3,7 +3,6 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.ComponentModel; using System.Linq; -using System.Reflection; using Umbraco.Core.Deploy; namespace Umbraco.Core @@ -15,7 +14,15 @@ namespace Umbraco.Core [TypeConverter(typeof(UdiTypeConverter))] public abstract class Udi : IComparable { - private static readonly Lazy> UdiTypes; + // 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 @@ -42,50 +49,16 @@ namespace Umbraco.Core static Udi() { - UdiTypes = new Lazy>(() => - { - var result = new Dictionary(); - - // known types: - foreach (var fi in typeof(Constants.UdiEntityType).GetFields(BindingFlags.Public | BindingFlags.Static)) - { - // IsLiteral determines if its value is written at - // compile time and not changeable - // IsInitOnly determine if the field can be set - // in the body of the constructor - // for C# a field which is readonly keyword would have both true - // but a const field would have only IsLiteral equal to true - if (fi.IsLiteral && fi.IsInitOnly == false) - { - var udiType = fi.GetCustomAttribute(); + // initialize with known (built-in) Udi types + // we will add scanned types later on + _udiTypes = new ConcurrentDictionary(Constants.UdiEntityType.GetTypes()); + } - if (udiType == null) - throw new InvalidOperationException("All Constants listed in UdiEntityType must be attributed with " + typeof(Constants.UdiTypeAttribute)); - result[fi.GetValue(null).ToString()] = udiType.UdiType; - } - } - - // 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 = PluginManager.Current.ResolveTypes(); - 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; - } - } - - return result; - }); + // for tests, totally unsafe + internal static void ResetUdiTypes() + { + _udiTypes = new ConcurrentDictionary(Constants.UdiEntityType.GetTypes()); + _scanned = false; } /// @@ -113,17 +86,65 @@ namespace Umbraco.Core public static Udi Parse(string s) { Udi udi; - ParseInternal(s, false, out udi); + ParseInternal(s, false, false, out udi); return udi; } - public static bool TryParse(string s, out Udi 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) { - return ParseInternal(s, true, out udi); + Udi udi; + ParseInternal(s, false, knownTypes, out udi); + return udi; } - private static bool ParseInternal(string s, bool tryParse, out Udi 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; @@ -136,12 +157,21 @@ namespace Umbraco.Core var entityType = uri.Host; UdiType udiType; - if (UdiTypes.Value.TryGetValue(entityType, out udiType) == false) + 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) @@ -158,21 +188,25 @@ namespace Umbraco.Core 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("Internal error."); + 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.Value.TryGetValue(x, out udiType) == false) + 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) @@ -180,6 +214,46 @@ namespace Umbraco.Core }); } + /// + /// 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 = PluginManager.Current.ResolveTypes(); + 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. /// @@ -199,13 +273,14 @@ namespace Umbraco.Core public static Udi Create(string entityType, string id) { UdiType udiType; - if (UdiTypes.Value.TryGetValue(entityType, out udiType) == false) + 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); } @@ -218,24 +293,31 @@ namespace Umbraco.Core public static Udi Create(string entityType, Guid id) { UdiType udiType; - if (UdiTypes.Value.TryGetValue(entityType, out udiType) == false) + 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)); + 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.Value.TryGetValue(uri.Host, out udiType) == false) + 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)); } @@ -284,6 +366,19 @@ namespace Umbraco.Core { 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; } + } + } + } } diff --git a/src/Umbraco.Core/UdiEntityType.cs b/src/Umbraco.Core/UdiEntityType.cs index fcaf718813..63cb818852 100644 --- a/src/Umbraco.Core/UdiEntityType.cs +++ b/src/Umbraco.Core/UdiEntityType.cs @@ -1,9 +1,9 @@ using System; +using System.Collections.Generic; using Umbraco.Core.Models; namespace Umbraco.Core { - public static partial class Constants { /// @@ -13,87 +13,100 @@ namespace Umbraco.Core /// but entity types are strings and so can be extended beyond what is defined here. public static class UdiEntityType { - [UdiType(UdiType.Unknown)] + // note: const fields in this class MUST be consistent with what GetTypes returns + // this is validated by UdiTests.ValidateUdiEntityType + + internal static Dictionary GetTypes() + { + return new Dictionary + { + { Unknown, UdiType.Unknown }, + + { AnyGuid, UdiType.GuidUdi }, + { Document, UdiType.GuidUdi }, + { DocumentBluePrint, UdiType.GuidUdi }, + { Media, UdiType.GuidUdi }, + { Member, UdiType.GuidUdi }, + { DictionaryItem, UdiType.GuidUdi }, + { Macro, UdiType.GuidUdi }, + { Template, UdiType.GuidUdi }, + { DocumentType, UdiType.GuidUdi }, + { DocumentTypeContainer, UdiType.GuidUdi }, + { DocumentTypeBluePrints, UdiType.GuidUdi }, + { MediaType, UdiType.GuidUdi }, + { MediaTypeContainer, UdiType.GuidUdi }, + { DataType, UdiType.GuidUdi }, + { DataTypeContainer, UdiType.GuidUdi }, + { MemberType, UdiType.GuidUdi }, + { MemberGroup, UdiType.GuidUdi }, + { RelationType, UdiType.GuidUdi }, + { FormsForm, UdiType.GuidUdi }, + { FormsPreValue, UdiType.GuidUdi }, + { FormsDataSource, UdiType.GuidUdi }, + + { AnyString, UdiType.StringUdi}, + { Language, UdiType.StringUdi}, + { MacroScript, UdiType.StringUdi}, + { MediaFile, UdiType.StringUdi}, + { TemplateFile, UdiType.StringUdi}, + { Script, UdiType.StringUdi}, + { PartialView, UdiType.StringUdi}, + { PartialViewMacro, UdiType.StringUdi}, + { Stylesheet, UdiType.StringUdi}, + { UserControl, UdiType.StringUdi}, + { Xslt, UdiType.StringUdi}, + }; + } + public const string Unknown = "unknown"; // guid entity types - [UdiType(UdiType.GuidUdi)] public const string AnyGuid = "any-guid"; // that one is for tests - [UdiType(UdiType.GuidUdi)] public const string Document = "document"; - [UdiType(UdiType.GuidUdi)] public const string DocumentBluePrint = "document-blueprint"; - [UdiType(UdiType.GuidUdi)] public const string Media = "media"; - [UdiType(UdiType.GuidUdi)] public const string Member = "member"; - [UdiType(UdiType.GuidUdi)] public const string DictionaryItem = "dictionary-item"; - [UdiType(UdiType.GuidUdi)] public const string Macro = "macro"; - [UdiType(UdiType.GuidUdi)] public const string Template = "template"; - [UdiType(UdiType.GuidUdi)] public const string DocumentType = "document-type"; - [UdiType(UdiType.GuidUdi)] public const string DocumentTypeContainer = "document-type-container"; - [UdiType(UdiType.GuidUdi)] + public const string DocumentTypeBluePrints = "document-type-blueprints"; - [UdiType(UdiType.GuidUdi)] public const string MediaType = "media-type"; - [UdiType(UdiType.GuidUdi)] public const string MediaTypeContainer = "media-type-container"; - [UdiType(UdiType.GuidUdi)] public const string DataType = "data-type"; - [UdiType(UdiType.GuidUdi)] public const string DataTypeContainer = "data-type-container"; - [UdiType(UdiType.GuidUdi)] public const string MemberType = "member-type"; - [UdiType(UdiType.GuidUdi)] public const string MemberGroup = "member-group"; - [UdiType(UdiType.GuidUdi)] public const string RelationType = "relation-type"; // forms - [UdiType(UdiType.GuidUdi)] public const string FormsForm = "forms-form"; - [UdiType(UdiType.GuidUdi)] public const string FormsPreValue = "forms-prevalue"; - [UdiType(UdiType.GuidUdi)] public const string FormsDataSource = "forms-datasource"; // string entity types - [UdiType(UdiType.StringUdi)] public const string AnyString = "any-string"; // that one is for tests - [UdiType(UdiType.StringUdi)] public const string Language = "language"; - [UdiType(UdiType.StringUdi)] public const string MacroScript = "macroscript"; - [UdiType(UdiType.StringUdi)] public const string MediaFile = "media-file"; - [UdiType(UdiType.StringUdi)] public const string TemplateFile = "template-file"; - [UdiType(UdiType.StringUdi)] public const string Script = "script"; - [UdiType(UdiType.StringUdi)] public const string Stylesheet = "stylesheet"; - [UdiType(UdiType.StringUdi)] public const string PartialView = "partial-view"; - [UdiType(UdiType.StringUdi)] public const string PartialViewMacro = "partial-view-macro"; - [UdiType(UdiType.StringUdi)] public const string UserControl = "usercontrol"; - [UdiType(UdiType.StringUdi)] public const string Xslt = "xslt"; public static string FromUmbracoObjectType(UmbracoObjectTypes umbracoObjectType) @@ -185,16 +198,5 @@ namespace Umbraco.Core string.Format("EntityType \"{0}\" does not have a matching UmbracoObjectType.", entityType)); } } - - [AttributeUsage(AttributeTargets.Field)] - internal class UdiTypeAttribute : Attribute - { - public UdiType UdiType { get; private set; } - - public UdiTypeAttribute(UdiType udiType) - { - UdiType = udiType; - } - } } } \ No newline at end of file diff --git a/src/Umbraco.Core/UdiRange.cs b/src/Umbraco.Core/UdiRange.cs index a708220066..f87887a6f8 100644 --- a/src/Umbraco.Core/UdiRange.cs +++ b/src/Umbraco.Core/UdiRange.cs @@ -62,8 +62,8 @@ namespace Umbraco.Core { Uri uri; - if (!Uri.IsWellFormedUriString(s, UriKind.Absolute) - || !Uri.TryCreate(s, UriKind.Absolute, out 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 range.", s)); diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 59cb3d71d0..94d6116d85 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -700,6 +700,7 @@ + diff --git a/src/Umbraco.Tests/UdiTests.cs b/src/Umbraco.Tests/UdiTests.cs index 24db54a98f..75f4536fd4 100644 --- a/src/Umbraco.Tests/UdiTests.cs +++ b/src/Umbraco.Tests/UdiTests.cs @@ -1,8 +1,11 @@ using System; +using System.Collections.Generic; using System.Linq; +using System.Reflection; using Newtonsoft.Json; using NUnit.Framework; using Umbraco.Core; +using Umbraco.Core.Deploy; using Umbraco.Core.Serialization; namespace Umbraco.Tests @@ -11,7 +14,7 @@ namespace Umbraco.Tests public class UdiTests { [Test] - public void StringEntityCtorTest() + public void StringUdiCtorTest() { var udi = new StringUdi(Constants.UdiEntityType.AnyString, "test-id"); Assert.AreEqual(Constants.UdiEntityType.AnyString, udi.EntityType); @@ -20,7 +23,7 @@ namespace Umbraco.Tests } [Test] - public void StringEntityParseTest() + public void StringUdiParseTest() { var udi = Udi.Parse("umb://" + Constants.UdiEntityType.AnyString + "/test-id"); Assert.AreEqual(Constants.UdiEntityType.AnyString, udi.EntityType); @@ -29,6 +32,9 @@ namespace Umbraco.Tests Assert.IsNotNull(stringEntityId); Assert.AreEqual("test-id", stringEntityId.Id); Assert.AreEqual("umb://" + Constants.UdiEntityType.AnyString + "/test-id", udi.ToString()); + + udi = Udi.Parse("umb://" + Constants.UdiEntityType.AnyString + "/DA845952BE474EE9BD6F6194272AC750"); + Assert.IsInstanceOf(udi); } [Test] @@ -83,7 +89,7 @@ namespace Umbraco.Tests } [Test] - public void GuidEntityCtorTest() + public void GuidUdiCtorTest() { var guid = Guid.NewGuid(); var udi = new GuidUdi(Constants.UdiEntityType.AnyGuid, guid); @@ -93,7 +99,7 @@ namespace Umbraco.Tests } [Test] - public void GuidEntityParseTest() + public void GuidUdiParseTest() { var guid = Guid.NewGuid(); var s = "umb://" + Constants.UdiEntityType.AnyGuid + "/" + guid.ToString("N"); @@ -148,9 +154,32 @@ namespace Umbraco.Tests Assert.AreEqual(Constants.UdiEntityType.AnyGuid, udi.EntityType); Assert.AreEqual(guid, ((GuidUdi)udi).Guid); - Assert.Throws(() => Udi.Create(Constants.UdiEntityType.AnyString, guid)); - Assert.Throws(() => Udi.Create(Constants.UdiEntityType.AnyGuid, "foo")); - Assert.Throws(() => Udi.Create("barf", "foo")); + // *not* testing whether Udi.Create(type, invalidValue) throws + // because we don't throw anymore - see U4-10409 + } + + [Test] + public void RootUdiTest() + { + var stringUdi = new StringUdi(Constants.UdiEntityType.AnyString, string.Empty); + Assert.IsTrue(stringUdi.IsRoot); + Assert.AreEqual("umb://any-string/", stringUdi.ToString()); + + var guidUdi = new GuidUdi(Constants.UdiEntityType.AnyGuid, Guid.Empty); + Assert.IsTrue(guidUdi.IsRoot); + Assert.AreEqual("umb://any-guid/00000000000000000000000000000000", guidUdi.ToString()); + + var udi = Udi.Parse("umb://any-string/"); + Assert.IsTrue(udi.IsRoot); + Assert.IsInstanceOf(udi); + + udi = Udi.Parse("umb://any-guid/00000000000000000000000000000000"); + Assert.IsTrue(udi.IsRoot); + Assert.IsInstanceOf(udi); + + udi = Udi.Parse("umb://any-guid/"); + Assert.IsTrue(udi.IsRoot); + Assert.IsInstanceOf(udi); } [Test] @@ -202,5 +231,112 @@ namespace Umbraco.Tests Assert.AreEqual(string.Format("umb://any-guid/{0:N}", guid), drange.Udi.UriValue.ToString()); Assert.AreEqual(Constants.DeploySelector.ChildrenOfThis, drange.Selector); } + + [Test] + public void ValidateUdiEntityType() + { + var types = Constants.UdiEntityType.GetTypes(); + + foreach (var fi in typeof (Constants.UdiEntityType).GetFields(BindingFlags.Public | BindingFlags.Static)) + { + // IsLiteral determines if its value is written at + // compile time and not changeable + // IsInitOnly determine if the field can be set + // in the body of the constructor + // for C# a field which is readonly keyword would have both true + // but a const field would have only IsLiteral equal to true + if (fi.IsLiteral && fi.IsInitOnly == false) + { + var value = fi.GetValue(null).ToString(); + + if (types.ContainsKey(value) == false) + Assert.Fail("Error in class Constants.UdiEntityType, type \"{0}\" is not declared by GetTypes.", value); + types.Remove(value); + } + } + + Assert.AreEqual(0, types.Count, "Error in class Constants.UdiEntityType, GetTypes declares types that don't exist ({0}).", string.Join(",", types.Keys.Select(x => "\"" + x + "\""))); + } + + [Test] + public void KnownTypes() + { + Udi udi; + + // cannot parse an unknown type, udi is null + // this will scan + Assert.IsFalse(Udi.TryParse("umb://whatever/1234", out udi)); + Assert.IsNull(udi); + + Udi.ResetUdiTypes(); + + // unless we want to know + Assert.IsFalse(Udi.TryParse("umb://whatever/1234", true, out udi)); + Assert.AreEqual(Constants.UdiEntityType.Unknown, udi.EntityType); + Assert.AreEqual("Umbraco.Core.Udi+UnknownTypeUdi", udi.GetType().FullName); + + Udi.ResetUdiTypes(); + + // not known + Assert.IsFalse(Udi.TryParse("umb://foo/A87F65C8D6B94E868F6949BA92C93045", true, out udi)); + Assert.AreEqual(Constants.UdiEntityType.Unknown, udi.EntityType); + Assert.AreEqual("Umbraco.Core.Udi+UnknownTypeUdi", udi.GetType().FullName); + + // scanned + Assert.IsTrue(Udi.TryParse("umb://foo/A87F65C8D6B94E868F6949BA92C93045", out udi)); + Assert.IsInstanceOf(udi); + + // known + Assert.IsTrue(Udi.TryParse("umb://foo/A87F65C8D6B94E868F6949BA92C93045", true, out udi)); + Assert.IsInstanceOf(udi); + + // can get method for Deploy compatibility + var method = typeof (Udi).GetMethod("Parse", BindingFlags.Static | BindingFlags.Public, null, new[] { typeof (string), typeof (bool) }, null); + Assert.IsNotNull(method); + } + + [UdiDefinition("foo", UdiType.GuidUdi)] + public class FooConnector : IServiceConnector + { + public IArtifact GetArtifact(Udi udi) + { + throw new NotImplementedException(); + } + + public IArtifact GetArtifact(object entity) + { + throw new NotImplementedException(); + } + + public ArtifactDeployState ProcessInit(IArtifact art, IDeployContext context) + { + throw new NotImplementedException(); + } + + public void Process(ArtifactDeployState dart, IDeployContext context, int pass) + { + throw new NotImplementedException(); + } + + public void Explode(UdiRange range, List udis) + { + throw new NotImplementedException(); + } + + public NamedUdiRange GetRange(Udi udi, string selector) + { + throw new NotImplementedException(); + } + + public NamedUdiRange GetRange(string entityType, string sid, string selector) + { + throw new NotImplementedException(); + } + + public bool Compare(IArtifact art1, IArtifact art2, ICollection differences = null) + { + throw new NotImplementedException(); + } + } } } \ No newline at end of file