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; using Umbraco.Core.Collections; using Umbraco.Core.Exceptions; using Umbraco.Core.Models.Entities; namespace Umbraco.Core.Models { /// /// Represents an abstract class for base Content properties and methods /// [Serializable] [DataContract(IsReference = true)] [DebuggerDisplay("Id: {Id}, Name: {Name}, ContentType: {ContentTypeBase.Alias}")] public abstract class ContentBase : TreeEntityBase, IContentBase { protected static readonly CultureNameCollection NoNames = new CultureNameCollection(); private static readonly Lazy Ps = new Lazy(); private int _contentTypeId; protected IContentTypeComposition ContentTypeBase; private int _writerId; private PropertyCollection _properties; private CultureNameCollection _cultureInfos; /// /// Initializes a new instance of the class. /// 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; } /// /// Initializes a new instance of the class. /// 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 SetCultureName(name, culture); _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(x => x.ContentTypeId); public readonly PropertyInfo PropertyCollectionSelector = ExpressionHelper.GetPropertyInfo(x => x.Properties); public readonly PropertyInfo WriterSelector = ExpressionHelper.GetPropertyInfo(x => x.WriterId); public readonly PropertyInfo CultureNamesSelector = ExpressionHelper.GetPropertyInfo>(x => x.CultureNames); } protected void PropertiesChanged(object sender, NotifyCollectionChangedEventArgs e) { OnPropertyChanged(Ps.Value.PropertyCollectionSelector); } /// /// Id of the user who wrote/updated this entity /// [DataMember] public virtual int WriterId { get => _writerId; set => SetPropertyValueAndDetectChanges(value, ref _writerId, Ps.Value.WriterSelector); } [IgnoreDataMember] public int VersionId { get; internal set; } /// /// Integer Id of the default ContentType /// [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); } /// /// Gets or sets the collection of properties for the entity. /// [DataMember] public virtual PropertyCollection Properties { get => _properties; set { _properties = value; _properties.CollectionChanged += PropertiesChanged; } } /// /// Gets the enumeration of property groups for the entity. /// fixme is a proxy, kill this /// [IgnoreDataMember] public IEnumerable PropertyGroups => ContentTypeBase.CompositionPropertyGroups; /// /// Gets the numeration of property types for the entity. /// fixme is a proxy, kill this /// [IgnoreDataMember] public IEnumerable PropertyTypes => ContentTypeBase.CompositionPropertyTypes; #region Cultures // 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 /// public IEnumerable AvailableCultures => _cultureInfos?.Keys ?? Enumerable.Empty(); /// public bool IsCultureAvailable(string culture) => _cultureInfos != null && _cultureInfos.Contains(culture); /// [DataMember] public virtual IReadOnlyKeyedCollection CultureNames => _cultureInfos ?? NoNames; /// public string GetCultureName(string culture) { 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; } /// public DateTime? GetUpdateDate(string culture) { 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; } /// public void SetCultureName(string name, string culture) { if (ContentTypeBase.VariesByCulture()) // set on variant content type { 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); } } else // set on invariant content type { if (!culture.IsNullOrWhiteSpace()) // invariant is NOT ok throw new NotSupportedException("Content type does not vary by culture."); Name = name; // may be null } } //fixme: this isn't used anywhere internal void TouchCulture(string culture) { if (ContentTypeBase.VariesByCulture() && _cultureInfos != null && _cultureInfos.TryGetValue(culture, out var infos)) _cultureInfos.AddOrUpdate(culture, infos.Name, DateTime.Now); } protected void ClearCultureInfos() { if (_cultureInfos != null) _cultureInfos.Clear(); _cultureInfos = null; } protected void ClearCultureInfo(string culture) { if (culture.IsNullOrWhiteSpace()) throw new ArgumentNullOrEmptyException(nameof(culture)); if (_cultureInfos == null) return; _cultureInfos.Remove(culture); if (_cultureInfos.Count == 0) _cultureInfos = null; } // internal for repository internal void SetCultureInfo(string culture, string name, DateTime date) { if (name.IsNullOrWhiteSpace()) throw new ArgumentNullOrEmptyException(nameof(name)); if (culture.IsNullOrWhiteSpace()) throw new ArgumentNullOrEmptyException(nameof(culture)); if (_cultureInfos == null) { _cultureInfos = new CultureNameCollection(); _cultureInfos.CollectionChanged += CultureNamesCollectionChanged; } _cultureInfos.AddOrUpdate(culture, name, date); } /// /// Event handler for when the culture names collection is modified /// /// /// private void CultureNamesCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { OnPropertyChanged(Ps.Value.CultureNamesSelector); } #endregion #region Has, Get, Set, Publish Property Value /// public virtual bool HasProperty(string propertyTypeAlias) => Properties.Contains(propertyTypeAlias); /// 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; } /// public virtual TValue GetValue(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(); return convertAttempt.Success ? convertAttempt.Result : default; } /// 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 #region Copy /// 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 #region Validation /// public virtual Property[] ValidateProperties(string culture = "*") { var alsoInvariant = culture != null && culture != "*"; return Properties.Where(x => // select properties... x.PropertyType.SupportsVariation(culture, "*", true) && // that support the variation (!x.IsValid(culture) || (alsoInvariant && !x.IsValid(null)))) // and are not valid .ToArray(); } #endregion #region Dirty /// /// Overriden to include user properties. 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); // take care of culture names if (_cultureInfos != null) foreach (var cultureName in _cultureInfos) cultureName.ResetDirtyProperties(rememberDirty); } /// /// Overriden to include user properties. public override bool IsDirty() { return IsEntityDirty() || this.IsAnyUserPropertyDirty(); } /// /// Overriden to include user properties. public override bool WasDirty() { return WasEntityDirty() || this.WasAnyUserPropertyDirty(); } /// /// Gets a value indicating whether the current entity's own properties (not user) are dirty. /// public bool IsEntityDirty() { return base.IsDirty(); } /// /// Gets a value indicating whether the current entity's own properties (not user) were dirty. /// public bool WasEntityDirty() { return base.WasDirty(); } /// /// Overriden to include user properties. public override bool IsPropertyDirty(string propertyName) { if (base.IsPropertyDirty(propertyName)) return true; return Properties.Contains(propertyName) && Properties[propertyName].IsDirty(); } /// /// Overriden to include user properties. public override bool WasPropertyDirty(string propertyName) { if (base.WasPropertyDirty(propertyName)) return true; return Properties.Contains(propertyName) && Properties[propertyName].WasDirty(); } /// /// Overriden to include user properties. public override IEnumerable GetDirtyProperties() { var instanceProperties = base.GetDirtyProperties(); var propertyTypes = Properties.Where(x => x.IsDirty()).Select(x => x.Alias); return instanceProperties.Concat(propertyTypes); } /// /// Overriden to include user properties. public override IEnumerable GetWereDirtyProperties() { var instanceProperties = base.GetWereDirtyProperties(); var propertyTypes = Properties.Where(x => x.WasDirty()).Select(x => x.Alias); return instanceProperties.Concat(propertyTypes); } #endregion } }