2018-06-29 19:52:40 +02:00
using System ;
using System.Collections.Generic ;
using System.Collections.Specialized ;
using System.Diagnostics ;
using System.Linq ;
using System.Reflection ;
using System.Runtime.Serialization ;
using System.Web ;
2018-10-17 18:09:52 +11:00
using Umbraco.Core.Collections ;
2018-06-29 19:52:40 +02:00
using Umbraco.Core.Exceptions ;
using Umbraco.Core.Models.Entities ;
namespace Umbraco.Core.Models
{
/// <summary>
/// Represents an abstract class for base Content properties and methods
/// </summary>
[Serializable]
[DataContract(IsReference = true)]
[DebuggerDisplay("Id: {Id}, Name: {Name}, ContentType: {ContentTypeBase.Alias}")]
public abstract class ContentBase : TreeEntityBase , IContentBase
{
2018-10-17 18:09:52 +11:00
protected static readonly CultureNameCollection NoNames = new CultureNameCollection ( ) ;
2018-06-29 19:52:40 +02:00
private static readonly Lazy < PropertySelectors > Ps = new Lazy < PropertySelectors > ( ) ;
private int _contentTypeId ;
protected IContentTypeComposition ContentTypeBase ;
private int _writerId ;
private PropertyCollection _properties ;
2018-10-17 18:09:52 +11:00
private CultureNameCollection _cultureInfos ;
2018-06-29 19:52:40 +02:00
/// <summary>
/// Initializes a new instance of the <see cref="ContentBase"/> class.
/// </summary>
protected ContentBase ( string name , int parentId , IContentTypeComposition contentType , PropertyCollection properties , string culture = null )
: this ( name , contentType , properties , culture )
{
if ( parentId = = 0 ) throw new ArgumentOutOfRangeException ( nameof ( parentId ) ) ;
ParentId = parentId ;
}
/// <summary>
/// Initializes a new instance of the <see cref="ContentBase"/> class.
/// </summary>
protected ContentBase ( string name , IContentBase parent , IContentTypeComposition contentType , PropertyCollection properties , string culture = null )
: this ( name , contentType , properties , culture )
{
if ( parent = = null ) throw new ArgumentNullException ( nameof ( parent ) ) ;
SetParent ( parent ) ;
}
private ContentBase ( string name , IContentTypeComposition contentType , PropertyCollection properties , string culture = null )
{
ContentTypeBase = contentType ? ? throw new ArgumentNullException ( nameof ( contentType ) ) ;
// initially, all new instances have
Id = 0 ; // no identity
VersionId = 0 ; // no versions
2018-06-20 14:18:57 +02:00
SetCultureName ( name , culture ) ;
2018-06-29 19:52:40 +02:00
_contentTypeId = contentType . Id ;
_properties = properties ? ? throw new ArgumentNullException ( nameof ( properties ) ) ;
_properties . EnsurePropertyTypes ( PropertyTypes ) ;
}
// ReSharper disable once ClassNeverInstantiated.Local
private class PropertySelectors
{
public readonly PropertyInfo DefaultContentTypeIdSelector = ExpressionHelper . GetPropertyInfo < ContentBase , int > ( x = > x . ContentTypeId ) ;
public readonly PropertyInfo PropertyCollectionSelector = ExpressionHelper . GetPropertyInfo < ContentBase , PropertyCollection > ( x = > x . Properties ) ;
public readonly PropertyInfo WriterSelector = ExpressionHelper . GetPropertyInfo < ContentBase , int > ( x = > x . WriterId ) ;
2018-10-17 18:09:52 +11:00
public readonly PropertyInfo CultureNamesSelector = ExpressionHelper . GetPropertyInfo < ContentBase , IReadOnlyCollection < CultureName > > ( x = > x . CultureNames ) ;
2018-06-29 19:52:40 +02:00
}
protected void PropertiesChanged ( object sender , NotifyCollectionChangedEventArgs e )
{
OnPropertyChanged ( Ps . Value . PropertyCollectionSelector ) ;
}
/// <summary>
/// Id of the user who wrote/updated this entity
/// </summary>
[DataMember]
public virtual int WriterId
{
get = > _writerId ;
set = > SetPropertyValueAndDetectChanges ( value , ref _writerId , Ps . Value . WriterSelector ) ;
}
[IgnoreDataMember]
public int VersionId { get ; internal set ; }
/// <summary>
/// Integer Id of the default ContentType
/// </summary>
[DataMember]
public virtual int ContentTypeId
{
get
{
//There will be cases where this has not been updated to reflect the true content type ID.
//This will occur when inserting new content.
if ( _contentTypeId = = 0 & & ContentTypeBase ! = null & & ContentTypeBase . HasIdentity )
{
_contentTypeId = ContentTypeBase . Id ;
}
return _contentTypeId ;
}
protected set = > SetPropertyValueAndDetectChanges ( value , ref _contentTypeId , Ps . Value . DefaultContentTypeIdSelector ) ;
}
/// <summary>
/// Gets or sets the collection of properties for the entity.
/// </summary>
[DataMember]
public virtual PropertyCollection Properties
{
get = > _properties ;
set
{
_properties = value ;
_properties . CollectionChanged + = PropertiesChanged ;
}
}
/// <summary>
/// Gets the enumeration of property groups for the entity.
/// fixme is a proxy, kill this
/// </summary>
[IgnoreDataMember]
public IEnumerable < PropertyGroup > PropertyGroups = > ContentTypeBase . CompositionPropertyGroups ;
/// <summary>
/// Gets the numeration of property types for the entity.
/// fixme is a proxy, kill this
/// </summary>
[IgnoreDataMember]
public IEnumerable < PropertyType > PropertyTypes = > ContentTypeBase . CompositionPropertyTypes ;
#region Cultures
2018-06-20 14:18:57 +02:00
// notes - common rules
// - setting a variant value on an invariant content type throws
// - getting a variant value on an invariant content type returns null
// - setting and getting the invariant value is always possible
// - setting a null value clears the value
/// <inheritdoc />
public IEnumerable < string > AvailableCultures
2018-10-17 18:09:52 +11:00
= > _cultureInfos ? . Keys ? ? Enumerable . Empty < string > ( ) ;
2018-06-20 14:18:57 +02:00
/// <inheritdoc />
public bool IsCultureAvailable ( string culture )
2018-10-17 18:09:52 +11:00
= > _cultureInfos ! = null & & _cultureInfos . Contains ( culture ) ;
2018-06-20 14:18:57 +02:00
2018-06-29 19:52:40 +02:00
/// <inheritdoc />
[DataMember]
2018-10-17 18:09:52 +11:00
public virtual IReadOnlyKeyedCollection < string , CultureName > CultureNames = > _cultureInfos ? ? NoNames ;
2018-06-20 14:18:57 +02:00
/// <inheritdoc />
2018-10-17 18:09:52 +11:00
public string GetCultureName ( string culture )
2018-06-29 19:52:40 +02:00
{
2018-06-20 14:18:57 +02:00
if ( culture . IsNullOrWhiteSpace ( ) ) return Name ;
if ( ! ContentTypeBase . VariesByCulture ( ) ) return null ;
if ( _cultureInfos = = null ) return null ;
return _cultureInfos . TryGetValue ( culture , out var infos ) ? infos . Name : null ;
}
2018-06-29 19:52:40 +02:00
2018-06-20 14:18:57 +02:00
/// <inheritdoc />
2018-09-25 18:05:14 +02:00
public DateTime ? GetUpdateDate ( string culture )
2018-06-20 14:18:57 +02:00
{
if ( culture . IsNullOrWhiteSpace ( ) ) return null ;
if ( ! ContentTypeBase . VariesByCulture ( ) ) return null ;
if ( _cultureInfos = = null ) return null ;
return _cultureInfos . TryGetValue ( culture , out var infos ) ? infos . Date : ( DateTime ? ) null ;
2018-06-29 19:52:40 +02:00
}
/// <inheritdoc />
2018-10-17 18:09:52 +11:00
public void SetCultureName ( string name , string culture )
2018-06-29 19:52:40 +02:00
{
2018-06-20 14:18:57 +02:00
if ( ContentTypeBase . VariesByCulture ( ) ) // set on variant content type
2018-06-29 19:52:40 +02:00
{
2018-06-20 14:18:57 +02:00
if ( culture . IsNullOrWhiteSpace ( ) ) // invariant is ok
{
Name = name ; // may be null
}
else if ( name . IsNullOrWhiteSpace ( ) ) // clear
{
ClearCultureInfo ( culture ) ;
}
else // set
{
SetCultureInfo ( culture , name , DateTime . Now ) ;
}
2018-06-29 19:52:40 +02:00
}
2018-06-20 14:18:57 +02:00
else // set on invariant content type
2018-06-29 19:52:40 +02:00
{
2018-06-20 14:18:57 +02:00
if ( ! culture . IsNullOrWhiteSpace ( ) ) // invariant is NOT ok
throw new NotSupportedException ( "Content type does not vary by culture." ) ;
2018-06-29 19:52:40 +02:00
2018-06-20 14:18:57 +02:00
Name = name ; // may be null
}
2018-06-29 19:52:40 +02:00
}
2018-10-17 18:09:52 +11:00
//fixme: this isn't used anywhere
2018-09-25 18:05:14 +02:00
internal void TouchCulture ( string culture )
{
if ( ContentTypeBase . VariesByCulture ( ) & & _cultureInfos ! = null & & _cultureInfos . TryGetValue ( culture , out var infos ) )
2018-10-17 18:09:52 +11:00
_cultureInfos . AddOrUpdate ( culture , infos . Name , DateTime . Now ) ;
2018-09-25 18:05:14 +02:00
}
2018-06-20 14:18:57 +02:00
protected void ClearCultureInfos ( )
2018-06-29 19:52:40 +02:00
{
2018-10-17 18:09:52 +11:00
if ( _cultureInfos ! = null )
_cultureInfos . Clear ( ) ;
2018-06-20 14:18:57 +02:00
_cultureInfos = null ;
2018-06-29 19:52:40 +02:00
}
2018-06-20 14:18:57 +02:00
protected void ClearCultureInfo ( string culture )
2018-06-29 19:52:40 +02:00
{
2018-06-20 14:18:57 +02:00
if ( culture . IsNullOrWhiteSpace ( ) )
throw new ArgumentNullOrEmptyException ( nameof ( culture ) ) ;
2018-06-29 19:52:40 +02:00
if ( _cultureInfos = = null ) return ;
_cultureInfos . Remove ( culture ) ;
if ( _cultureInfos . Count = = 0 )
_cultureInfos = null ;
}
2018-06-20 14:18:57 +02:00
// internal for repository
internal void SetCultureInfo ( string culture , string name , DateTime date )
2018-06-29 19:52:40 +02:00
{
2018-06-20 14:18:57 +02:00
if ( name . IsNullOrWhiteSpace ( ) )
throw new ArgumentNullOrEmptyException ( nameof ( name ) ) ;
2018-06-29 19:52:40 +02:00
2018-06-20 14:18:57 +02:00
if ( culture . IsNullOrWhiteSpace ( ) )
throw new ArgumentNullOrEmptyException ( nameof ( culture ) ) ;
2018-06-29 19:52:40 +02:00
2018-06-20 14:18:57 +02:00
if ( _cultureInfos = = null )
2018-10-17 18:09:52 +11:00
{
_cultureInfos = new CultureNameCollection ( ) ;
_cultureInfos . CollectionChanged + = CultureNamesCollectionChanged ;
}
2018-06-20 14:18:57 +02:00
2018-10-17 18:09:52 +11:00
_cultureInfos . AddOrUpdate ( culture , name , date ) ;
}
/// <summary>
/// Event handler for when the culture names collection is modified
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void CultureNamesCollectionChanged ( object sender , NotifyCollectionChangedEventArgs e )
{
OnPropertyChanged ( Ps . Value . CultureNamesSelector ) ;
2018-06-29 19:52:40 +02:00
}
#endregion
#region Has , Get , Set , Publish Property Value
/// <inheritdoc />
public virtual bool HasProperty ( string propertyTypeAlias )
= > Properties . Contains ( propertyTypeAlias ) ;
/// <inheritdoc />
public virtual object GetValue ( string propertyTypeAlias , string culture = null , string segment = null , bool published = false )
{
return Properties . TryGetValue ( propertyTypeAlias , out var property )
? property . GetValue ( culture , segment , published )
: null ;
}
/// <inheritdoc />
public virtual TValue GetValue < TValue > ( string propertyTypeAlias , string culture = null , string segment = null , bool published = false )
{
if ( ! Properties . TryGetValue ( propertyTypeAlias , out var property ) )
return default ;
var convertAttempt = property . GetValue ( culture , segment , published ) . TryConvertTo < TValue > ( ) ;
return convertAttempt . Success ? convertAttempt . Result : default ;
}
/// <inheritdoc />
public virtual void SetValue ( string propertyTypeAlias , object value , string culture = null , string segment = null )
{
if ( Properties . Contains ( propertyTypeAlias ) )
{
Properties [ propertyTypeAlias ] . SetValue ( value , culture , segment ) ;
return ;
}
var propertyType = PropertyTypes . FirstOrDefault ( x = > x . Alias . InvariantEquals ( propertyTypeAlias ) ) ;
if ( propertyType = = null )
throw new InvalidOperationException ( $"No PropertyType exists with the supplied alias \" { propertyTypeAlias } \ "." ) ;
var property = propertyType . CreateProperty ( ) ;
property . SetValue ( value , culture , segment ) ;
Properties . Add ( property ) ;
}
#endregion
2018-06-22 21:03:47 +02:00
#region Copy
/// <inheritdoc />
public virtual void CopyFrom ( IContent other , string culture = "*" )
{
if ( other . ContentTypeId ! = ContentTypeId )
throw new InvalidOperationException ( "Cannot copy values from a different content type." ) ;
culture = culture ? . ToLowerInvariant ( ) . NullOrWhiteSpaceAsNull ( ) ;
// the variation should be supported by the content type properties
// if the content type is invariant, only '*' and 'null' is ok
// if the content type varies, everything is ok because some properties may be invariant
if ( ! ContentTypeBase . SupportsPropertyVariation ( culture , "*" , true ) )
throw new NotSupportedException ( $"Culture \" { culture } \ " is not supported by content type \"{ContentTypeBase.Alias}\" with variation \"{ContentTypeBase.Variations}\"." ) ;
// copying from the same Id and VersionPk
var copyingFromSelf = Id = = other . Id & & VersionId = = other . VersionId ;
var published = copyingFromSelf ;
// note: use property.SetValue(), don't assign pvalue.EditValue, else change tracking fails
// clear all existing properties for the specified culture
foreach ( var property in Properties )
{
// each property type may or may not support the variation
if ( ! property . PropertyType . SupportsVariation ( culture , "*" , wildcards : true ) )
continue ;
foreach ( var pvalue in property . Values )
if ( property . PropertyType . SupportsVariation ( pvalue . Culture , pvalue . Segment , wildcards : true ) & &
( culture = = "*" | | pvalue . Culture . InvariantEquals ( culture ) ) )
{
property . SetValue ( null , pvalue . Culture , pvalue . Segment ) ;
}
}
// copy properties from 'other'
var otherProperties = other . Properties ;
foreach ( var otherProperty in otherProperties )
{
if ( ! otherProperty . PropertyType . SupportsVariation ( culture , "*" , wildcards : true ) )
continue ;
var alias = otherProperty . PropertyType . Alias ;
foreach ( var pvalue in otherProperty . Values )
{
if ( otherProperty . PropertyType . SupportsVariation ( pvalue . Culture , pvalue . Segment , wildcards : true ) & &
( culture = = "*" | | pvalue . Culture . InvariantEquals ( culture ) ) )
{
var value = published ? pvalue . PublishedValue : pvalue . EditedValue ;
SetValue ( alias , value , pvalue . Culture , pvalue . Segment ) ;
}
}
}
// copy names, too
if ( culture = = "*" )
ClearCultureInfos ( ) ;
if ( culture = = null | | culture = = "*" )
Name = other . Name ;
foreach ( var ( otherCulture , otherName ) in other . CultureNames )
{
if ( culture = = "*" | | culture = = otherCulture )
SetCultureName ( otherName , otherCulture ) ;
}
}
#endregion
2018-06-29 19:52:40 +02:00
#region Validation
2018-07-31 17:50:24 +10:00
2018-06-29 19:52:40 +02:00
/// <inheritdoc />
2018-06-22 21:03:47 +02:00
public virtual Property [ ] ValidateProperties ( string culture = "*" )
2018-06-29 19:52:40 +02:00
{
2018-06-22 21:03:47 +02:00
var alsoInvariant = culture ! = null & & culture ! = "*" ;
2018-06-20 14:18:57 +02:00
return Properties . Where ( x = > // select properties...
2018-06-22 21:03:47 +02:00
x . PropertyType . SupportsVariation ( culture , "*" , true ) & & // that support the variation
( ! x . IsValid ( culture ) | | ( alsoInvariant & & ! x . IsValid ( null ) ) ) ) // and are not valid
2018-06-20 14:18:57 +02:00
. ToArray ( ) ;
2018-06-29 19:52:40 +02:00
}
#endregion
#region Dirty
/// <inheritdoc />
/// <remarks>Overriden to include user properties.</remarks>
public override void ResetDirtyProperties ( bool rememberDirty )
{
base . ResetDirtyProperties ( rememberDirty ) ;
// also reset dirty changes made to user's properties
foreach ( var prop in Properties )
prop . ResetDirtyProperties ( rememberDirty ) ;
2018-10-17 18:09:52 +11:00
// take care of culture names
if ( _cultureInfos ! = null )
foreach ( var cultureName in _cultureInfos )
cultureName . ResetDirtyProperties ( rememberDirty ) ;
2018-06-29 19:52:40 +02:00
}
/// <inheritdoc />
/// <remarks>Overriden to include user properties.</remarks>
public override bool IsDirty ( )
{
return IsEntityDirty ( ) | | this . IsAnyUserPropertyDirty ( ) ;
}
/// <inheritdoc />
/// <remarks>Overriden to include user properties.</remarks>
public override bool WasDirty ( )
{
return WasEntityDirty ( ) | | this . WasAnyUserPropertyDirty ( ) ;
}
/// <summary>
/// Gets a value indicating whether the current entity's own properties (not user) are dirty.
/// </summary>
public bool IsEntityDirty ( )
{
return base . IsDirty ( ) ;
}
/// <summary>
/// Gets a value indicating whether the current entity's own properties (not user) were dirty.
/// </summary>
public bool WasEntityDirty ( )
{
return base . WasDirty ( ) ;
}
/// <inheritdoc />
/// <remarks>Overriden to include user properties.</remarks>
public override bool IsPropertyDirty ( string propertyName )
{
if ( base . IsPropertyDirty ( propertyName ) )
return true ;
return Properties . Contains ( propertyName ) & & Properties [ propertyName ] . IsDirty ( ) ;
}
/// <inheritdoc />
/// <remarks>Overriden to include user properties.</remarks>
public override bool WasPropertyDirty ( string propertyName )
{
if ( base . WasPropertyDirty ( propertyName ) )
return true ;
return Properties . Contains ( propertyName ) & & Properties [ propertyName ] . WasDirty ( ) ;
}
/// <inheritdoc />
/// <remarks>Overriden to include user properties.</remarks>
public override IEnumerable < string > GetDirtyProperties ( )
{
var instanceProperties = base . GetDirtyProperties ( ) ;
var propertyTypes = Properties . Where ( x = > x . IsDirty ( ) ) . Select ( x = > x . Alias ) ;
return instanceProperties . Concat ( propertyTypes ) ;
}
/// <inheritdoc />
/// <remarks>Overriden to include user properties.</remarks>
public override IEnumerable < string > GetWereDirtyProperties ( )
{
var instanceProperties = base . GetWereDirtyProperties ( ) ;
var propertyTypes = Properties . Where ( x = > x . WasDirty ( ) ) . Select ( x = > x . Alias ) ;
return instanceProperties . Concat ( propertyTypes ) ;
}
#endregion
}
}