using System.Collections.ObjectModel;
using JSONAPI.Attributes;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using JSONAPI.Extensions;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
namespace JSONAPI.Core
{
public class ModelManager : IModelManager
{
public ModelManager(IPluralizationService pluralizationService)
{
_pluralizationService = pluralizationService;
RegistrationsByName = new Dictionary();
RegistrationsByType = new Dictionary();
}
protected IPluralizationService _pluralizationService = null;
public IPluralizationService PluralizationService
{
get
{
return _pluralizationService;
}
}
#region Cache storage
///
/// Represents a type's registration with a model manager
///
protected sealed class TypeRegistration
{
internal TypeRegistration() { }
///
/// The type that has been registered
///
public Type Type { get; internal set; }
///
/// The serialized format of the type's name
///
public string ResourceTypeName { get; internal set; }
///
/// The property to be used as this type's ID.
///
public PropertyInfo IdProperty { get; internal set; }
///
/// A resource's properties, keyed by name.
///
public IReadOnlyDictionary Properties { get; internal set; }
}
protected readonly IDictionary RegistrationsByName;
protected readonly IDictionary RegistrationsByType;
protected Lazy> _isSerializedAsMany
= new Lazy>(
() => new Dictionary()
);
protected Lazy> _getElementType
= new Lazy>(
() => new Dictionary()
);
#endregion
public PropertyInfo GetIdProperty(Type type)
{
return GetRegistrationByType(type).IdProperty;
}
public ModelProperty[] GetProperties(Type type)
{
var typeRegistration = GetRegistrationByType(type);
return typeRegistration.Properties.Values.ToArray();
}
public ModelProperty GetPropertyForJsonKey(Type type, string jsonKey)
{
var typeRegistration = GetRegistrationByType(type);
ModelProperty property;
return (typeRegistration.Properties.TryGetValue(jsonKey, out property)) ? property : null;
}
public string GetResourceTypeNameForType(Type type)
{
return GetRegistrationByType(type).ResourceTypeName;
}
public Type GetTypeByResourceTypeName(string resourceTypeName)
{
lock (RegistrationsByName)
{
TypeRegistration typeRegistration;
if (RegistrationsByName.TryGetValue(resourceTypeName, out typeRegistration))
return typeRegistration.Type;
throw new InvalidOperationException(String.Format("The resource type name `{0}` was not registered.",
resourceTypeName));
}
}
public bool TypeIsRegistered(Type type)
{
var registration = FindRegistrationForType(type);
return registration != null;
}
private TypeRegistration FindRegistrationForType(Type type)
{
lock (RegistrationsByType)
{
if (IsSerializedAsMany(type))
type = GetElementType(type);
var currentType = type;
while (currentType != null && currentType != typeof(Object))
{
TypeRegistration registration;
if (RegistrationsByType.TryGetValue(currentType, out registration))
return registration;
// This particular type wasn't registered, but maybe the base type was.
currentType = currentType.BaseType;
}
}
return null;
}
private TypeRegistration GetRegistrationByType(Type type)
{
var registration = FindRegistrationForType(type);
if (registration != null) return registration;
throw new InvalidOperationException(String.Format("The type `{0}` was not registered.", type.FullName));
}
///
/// Registers a type with this ModelManager.
///
/// The type to register.
public ModelManager RegisterResourceType(Type type)
{
var resourceTypeName = CalculateResourceTypeNameForType(type);
return RegisterResourceType(type, resourceTypeName);
}
///
/// Registeres a type with this ModelManager, using a default resource type name.
///
/// The type to register.
/// The resource type name to use
public ModelManager RegisterResourceType(Type type, string resourceTypeName)
{
lock (RegistrationsByType)
{
lock (RegistrationsByName)
{
if (RegistrationsByType.ContainsKey(type))
throw new InvalidOperationException(String.Format("The type `{0}` has already been registered.",
type.FullName));
if (RegistrationsByName.ContainsKey(resourceTypeName))
throw new InvalidOperationException(
String.Format("The resource type name `{0}` has already been registered.", resourceTypeName));
var registration = new TypeRegistration
{
Type = type,
ResourceTypeName = resourceTypeName
};
var propertyMap = new Dictionary();
var idProperty = CalculateIdProperty(type);
if (idProperty == null)
throw new InvalidOperationException(String.Format(
"Unable to determine Id property for type `{0}`.", resourceTypeName));
registration.IdProperty = idProperty;
var props = type.GetProperties();
foreach (var prop in props)
{
var jsonKey = prop == registration.IdProperty
? "id"
: CalculateJsonKeyForProperty(prop);
if (propertyMap.ContainsKey(jsonKey))
throw new InvalidOperationException(
String.Format("The type `{0}` already contains a property keyed at `{1}`.",
resourceTypeName, jsonKey));
var property = CreateModelProperty(prop, jsonKey);
propertyMap[jsonKey] = property;
}
registration.Properties = new ReadOnlyDictionary(propertyMap);
RegistrationsByType.Add(type, registration);
RegistrationsByName.Add(resourceTypeName, registration);
}
}
return this;
}
///
/// Creates a cacheable model property representation from a PropertyInfo
///
/// The property
/// The key that this model property will be serialized as
/// A model property represenation
protected virtual ModelProperty CreateModelProperty(PropertyInfo prop, string jsonKey)
{
var type = prop.PropertyType;
var ignoreByDefault =
prop.CustomAttributes.Any(c => c.AttributeType == typeof(JsonIgnoreAttribute));
if (prop.PropertyType.CanWriteAsJsonApiAttribute())
return new FieldModelProperty(prop, jsonKey, ignoreByDefault);
var isToMany =
type.IsArray ||
(type.GetInterfaces().Contains(typeof(System.Collections.IEnumerable)) && type.IsGenericType);
Type relatedType;
if (isToMany)
{
relatedType = type.IsGenericType ? type.GetGenericArguments()[0] : type.GetElementType();
}
else
{
relatedType = type;
}
return new RelationshipModelProperty(prop, jsonKey, ignoreByDefault, relatedType, isToMany);
}
///
/// Determines the resource type name for a given type.
///
/// The type to calculate the resouce type name for
/// The type's resource type name
protected virtual string CalculateResourceTypeNameForType(Type type)
{
var attrs = type.CustomAttributes.Where(x => x.AttributeType == typeof(Newtonsoft.Json.JsonObjectAttribute)).ToList();
string title = type.Name;
if (attrs.Any())
{
var titles = attrs.First().NamedArguments.Where(arg => arg.MemberName == "Title")
.Select(arg => arg.TypedValue.Value.ToString()).ToList();
if (titles.Any()) title = titles.First();
}
return FormatPropertyName(PluralizationService.Pluralize(title)).Dasherize();
}
///
/// Determines the key that a property will be serialized as.
///
/// The property
/// The key to serialize the given property as
protected internal virtual string CalculateJsonKeyForProperty(PropertyInfo propInfo)
{
var jsonPropertyAttribute = (JsonPropertyAttribute)propInfo.GetCustomAttributes(typeof (JsonPropertyAttribute)).FirstOrDefault();
return jsonPropertyAttribute != null ? jsonPropertyAttribute.PropertyName : FormatPropertyName(propInfo.Name);
}
private static string FormatPropertyName(string propertyName)
{
string result = propertyName.Substring(0, 1).ToLower() + propertyName.Substring(1);
return result;
}
public bool IsSerializedAsMany(Type type)
{
bool isMany;
var isManyCache = _isSerializedAsMany.Value;
lock (isManyCache)
{
if (isManyCache.TryGetValue(type, out isMany)) return isMany;
isMany =
type.IsArray ||
(type.GetInterfaces().Contains(typeof(System.Collections.IEnumerable)) && type.IsGenericType);
isManyCache.Add(type, isMany);
}
return isMany;
}
public Type GetElementType(Type manyType)
{
Type etype = null;
var etypeCache = _getElementType.Value;
lock (etypeCache)
{
if (etypeCache.TryGetValue(manyType, out etype)) return etype;
if (manyType.IsGenericType)
etype = manyType.GetGenericArguments()[0];
else
etype = manyType.GetElementType();
etypeCache.Add(manyType, etype);
}
return etype;
}
///
/// Calculates the ID property for a given resource type.
///
/// The type to use to calculate the ID for
/// The ID property to use for this type
protected virtual PropertyInfo CalculateIdProperty(Type type)
{
return
type
.GetProperties()
.FirstOrDefault(p => p.CustomAttributes.Any(attr => attr.AttributeType == typeof(UseAsIdAttribute)))
?? type.GetProperty("Id");
}
}
}