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/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 13fc678b8e..74e77e3fd2 100644 --- a/src/Umbraco.Core/Udi.cs +++ b/src/Umbraco.Core/Udi.cs @@ -3,8 +3,6 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.ComponentModel; using System.Linq; -using System.Reflection; -using System.Threading; using Umbraco.Core.Deploy; namespace Umbraco.Core @@ -16,9 +14,15 @@ namespace Umbraco.Core [TypeConverter(typeof(UdiTypeConverter))] public abstract class Udi : IComparable { - private static volatile bool _scanned = false; + // 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 readonly Lazy> KnownUdiTypes; + private static readonly Lazy> KnownUdiTypes; private static readonly ConcurrentDictionary RootUdis = new ConcurrentDictionary(); internal readonly Uri UriValue; // internal for UdiRange @@ -45,33 +49,9 @@ namespace Umbraco.Core static Udi() { - KnownUdiTypes = 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(); - - 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; - } - } - - //For non-known UDI types we'll try to parse a GUID and if that doesn't work, we'll decide that it's a string - - return new ConcurrentDictionary(result); - }); + // initialize with known (built-in) Udi types + // for non-known Udi types we'll try to parse a GUID and if that doesn't work, we'll decide that it's a string + KnownUdiTypes = new Lazy>(() => new ConcurrentDictionary(Constants.UdiEntityType.GetTypes())); } /// @@ -117,12 +97,13 @@ namespace Umbraco.Core { return udiType; } - - //if it's empty and it's not in our known list then we don't know + + // if it's empty and it's not in our known list then we don't know if (path.IsNullOrWhiteSpace()) return UdiType.Unknown; - //try to parse into a Guid + // try to parse into a Guid + // (note: root udis use Guid.Empty so this is ok) Guid guidId; if (Guid.TryParse(path, out guidId)) { @@ -131,7 +112,7 @@ namespace Umbraco.Core return UdiType.GuidUdi; } - //add it to our known list - if it's not a GUID then it must a string + // add it to our known list - if it's not a GUID then it must a string KnownUdiTypes.Value.TryAdd(uri.Host, UdiType.StringUdi); return UdiType.StringUdi; } @@ -148,19 +129,21 @@ namespace Umbraco.Core throw new FormatException(string.Format("String \"{0}\" is not a valid udi.", s)); } + // if it's a known entity type, GetUdiType will return it + // else it will try to guess based on the path, and register the type as known string path; var udiType = GetUdiType(uri, out path); if (path.IsNullOrWhiteSpace()) { - //in this case it's because the path is empty which indicates we need to return the root udi + // path is empty which indicates we need to return the root udi udi = GetRootUdi(uri.Host); return true; } - //This should never happen, if it's an empty path that would have been taken care of above + // if the path is not empty, type should not be unknown if (udiType == UdiType.Unknown) - throw new InvalidOperationException("Internal error."); + throw new FormatException(string.Format("Could not determine the Udi type for string \"{0}\".", s)); if (udiType == UdiType.GuidUdi) { @@ -256,10 +239,14 @@ namespace Umbraco.Core /// The identifier. /// The string Udi for the entity type and identifier. public static Udi Create(string entityType, string id) - { + { + UdiType udiType; + if (KnownUdiTypes.Value.TryGetValue(entityType, out udiType) && udiType != UdiType.StringUdi) + throw new InvalidOperationException(string.Format("Entity type \"{0}\" is not a StringUdi.", entityType)); + if (string.IsNullOrWhiteSpace(id)) throw new ArgumentException("Value cannot be null or whitespace.", "id"); - + return new StringUdi(entityType, id); } @@ -270,7 +257,11 @@ namespace Umbraco.Core /// The identifier. /// The Guid Udi for the entity type and identifier. public static Udi Create(string entityType, Guid id) - { + { + UdiType udiType; + if (KnownUdiTypes.Value.TryGetValue(entityType, out udiType) && udiType != UdiType.GuidUdi) + throw new InvalidOperationException(string.Format("Entity type \"{0}\" is not a GuidUdi.", entityType)); + if (id == default(Guid)) throw new ArgumentException("Cannot be an empty guid.", "id"); return new GuidUdi(entityType, id); @@ -278,9 +269,13 @@ namespace Umbraco.Core 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 (KnownUdiTypes.Value.TryGetValue(uri.Host, out udiType) == false) - throw new ArgumentException(string.Format("Unknown entity type \"{0}\".", uri.Host), "uri"); + return Parse(uri.ToString()); + if (udiType == UdiType.GuidUdi) return new GuidUdi(uri); if (udiType == UdiType.GuidUdi) diff --git a/src/Umbraco.Core/UdiEntityType.cs b/src/Umbraco.Core/UdiEntityType.cs index 8fb5d99338..691cba4f7d 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,81 +13,93 @@ 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 }, + { Media, UdiType.GuidUdi }, + { Member, UdiType.GuidUdi }, + { DictionaryItem, UdiType.GuidUdi }, + { Macro, UdiType.GuidUdi }, + { Template, UdiType.GuidUdi }, + { DocumentType, UdiType.GuidUdi }, + { DocumentTypeContainer, 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 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 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) @@ -179,16 +191,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.Tests/UdiTests.cs b/src/Umbraco.Tests/UdiTests.cs index 88eba62baf..9c46b27a89 100644 --- a/src/Umbraco.Tests/UdiTests.cs +++ b/src/Umbraco.Tests/UdiTests.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Reflection; using Newtonsoft.Json; using NUnit.Framework; using Umbraco.Core; @@ -11,7 +12,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 +21,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 +30,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 +87,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 +97,7 @@ namespace Umbraco.Tests } [Test] - public void GuidEntityParseTest() + public void GuidUdiParseTest() { var guid = Guid.NewGuid(); var s = "umb://" + Constants.UdiEntityType.AnyGuid + "/" + guid.ToString("N"); @@ -147,7 +151,43 @@ namespace Umbraco.Tests var udi = Udi.Create(Constants.UdiEntityType.AnyGuid, guid); Assert.AreEqual(Constants.UdiEntityType.AnyGuid, udi.EntityType); Assert.AreEqual(guid, ((GuidUdi)udi).Guid); - + + // *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] + public void NotKnownTypeTest() + { + var udi1 = Udi.Parse("umb://not-known-1/DA845952BE474EE9BD6F6194272AC750"); + Assert.IsInstanceOf(udi1); + + var udi2 = Udi.Parse("umb://not-known-2/this-is-not-a-guid"); + Assert.IsInstanceOf(udi2); } [Test] @@ -199,5 +239,31 @@ 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 + "\""))); + } } } \ No newline at end of file