U4-10409 UDI Parsing should not require scanning assemblies

This commit is contained in:
Shannon
2017-09-13 14:16:43 +10:00
parent ac391f3bcd
commit 27fc3a9bb3
2 changed files with 97 additions and 51 deletions

View File

@@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Threading;
using Umbraco.Core.Deploy;
namespace Umbraco.Core
@@ -15,7 +16,9 @@ namespace Umbraco.Core
[TypeConverter(typeof(UdiTypeConverter))]
public abstract class Udi : IComparable<Udi>
{
private static readonly Lazy<Dictionary<string, UdiType>> UdiTypes;
private static volatile bool _scanned = false;
private static readonly object ScanLocker = new object();
private static readonly Lazy<ConcurrentDictionary<string, UdiType>> KnownUdiTypes;
private static readonly ConcurrentDictionary<string, Udi> RootUdis = new ConcurrentDictionary<string, Udi>();
internal readonly Uri UriValue; // internal for UdiRange
@@ -42,7 +45,7 @@ namespace Umbraco.Core
static Udi()
{
UdiTypes = new Lazy<Dictionary<string, UdiType>>(() =>
KnownUdiTypes = new Lazy<ConcurrentDictionary<string, UdiType>>(() =>
{
var result = new Dictionary<string, UdiType>();
@@ -65,26 +68,9 @@ namespace Umbraco.Core
}
}
// 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<IServiceConnector>();
foreach (var connector in connectors)
{
var attrs = connector.GetCustomAttributes<UdiDefinitionAttribute>(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;
}
}
//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 result;
return new ConcurrentDictionary<string, UdiType>(result);
});
}
@@ -122,6 +108,34 @@ namespace Umbraco.Core
return ParseInternal(s, true, out udi);
}
private static UdiType GetUdiType(Uri uri, out string path)
{
path = uri.AbsolutePath.TrimStart('/');
UdiType udiType;
if (KnownUdiTypes.Value.TryGetValue(uri.Host, out udiType))
{
return udiType;
}
//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
Guid guidId;
if (Guid.TryParse(path, out guidId))
{
//add it to our known list
KnownUdiTypes.Value.TryAdd(uri.Host, UdiType.GuidUdi);
return UdiType.GuidUdi;
}
//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;
}
private static bool ParseInternal(string s, bool tryParse, out Udi udi)
{
udi = null;
@@ -134,21 +148,22 @@ namespace Umbraco.Core
throw new FormatException(string.Format("String \"{0}\" is not a valid udi.", s));
}
var entityType = uri.Host;
UdiType udiType;
if (UdiTypes.Value.TryGetValue(entityType, out udiType) == false)
string path;
var udiType = GetUdiType(uri, out path);
if (path.IsNullOrWhiteSpace())
{
if (tryParse) return false;
throw new FormatException(string.Format("Unknown entity type \"{0}\".", entityType));
//in this case it's because the path is empty which indicates we need to return the root udi
udi = GetRootUdi(uri.Host);
return true;
}
var path = uri.AbsolutePath.TrimStart('/');
//This should never happen, if it's an empty path that would have been taken care of above
if (udiType == UdiType.Unknown)
throw new InvalidOperationException("Internal error.");
if (udiType == UdiType.GuidUdi)
{
if (path == string.Empty)
{
udi = GetRootUdi(uri.Host);
return true;
}
Guid guid;
if (Guid.TryParse(path, out guid) == false)
{
@@ -160,7 +175,7 @@ namespace Umbraco.Core
}
if (udiType == UdiType.StringUdi)
{
udi = path == string.Empty ? GetRootUdi(uri.Host) : new StringUdi(uri.Host, Uri.UnescapeDataString(path));
udi = new StringUdi(uri.Host, Uri.UnescapeDataString(path));
return true;
}
if (tryParse) return false;
@@ -169,10 +184,12 @@ namespace Umbraco.Core
private static Udi GetRootUdi(string entityType)
{
ScanAllUdiTypes();
return RootUdis.GetOrAdd(entityType, x =>
{
UdiType udiType;
if (UdiTypes.Value.TryGetValue(x, out udiType) == false)
if (KnownUdiTypes.Value.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 +197,48 @@ namespace Umbraco.Core
});
}
/// <summary>
/// When required scan assemblies for known UDI types based on <see cref="IServiceConnector"/> instances
/// </summary>
/// <remarks>
/// This is only required when needing to resolve root udis
/// </remarks>
private static void ScanAllUdiTypes()
{
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<IServiceConnector>();
var result = new Dictionary<string, UdiType>();
foreach (var connector in connectors)
{
var attrs = connector.GetCustomAttributes<UdiDefinitionAttribute>(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)
{
KnownUdiTypes.Value.TryAdd(item.Key, item.Value);
}
_scanned = true;
}
}
/// <summary>
/// Creates a root Udi for an entity type.
/// </summary>
@@ -197,14 +256,9 @@ namespace Umbraco.Core
/// <param name="id">The identifier.</param>
/// <returns>The string Udi for the entity type and identifier.</returns>
public static Udi Create(string entityType, string id)
{
UdiType udiType;
if (UdiTypes.Value.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);
}
@@ -216,12 +270,7 @@ namespace Umbraco.Core
/// <param name="id">The identifier.</param>
/// <returns>The Guid Udi for the entity type and identifier.</returns>
public static Udi Create(string entityType, Guid id)
{
UdiType udiType;
if (UdiTypes.Value.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);
@@ -230,7 +279,7 @@ namespace Umbraco.Core
internal static Udi Create(Uri uri)
{
UdiType udiType;
if (UdiTypes.Value.TryGetValue(uri.Host, out udiType) == false)
if (KnownUdiTypes.Value.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);

View File

@@ -147,10 +147,7 @@ namespace Umbraco.Tests
var udi = Udi.Create(Constants.UdiEntityType.AnyGuid, guid);
Assert.AreEqual(Constants.UdiEntityType.AnyGuid, udi.EntityType);
Assert.AreEqual(guid, ((GuidUdi)udi).Guid);
Assert.Throws<InvalidOperationException>(() => Udi.Create(Constants.UdiEntityType.AnyString, guid));
Assert.Throws<InvalidOperationException>(() => Udi.Create(Constants.UdiEntityType.AnyGuid, "foo"));
Assert.Throws<ArgumentException>(() => Udi.Create("barf", "foo"));
}
[Test]