using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Runtime.Serialization; using Umbraco.Core.Collections; using Umbraco.Core.Models.Entities; namespace Umbraco.Core.Models { /// /// Represents a property. /// [Serializable] [DataContract(IsReference = true)] public class Property : EntityBase, IProperty { // _values contains all property values, including the invariant-neutral value private List _values = new List(); // _pvalue contains the invariant-neutral property value private IPropertyValue _pvalue; // _vvalues contains the (indexed) variant property values private Dictionary _vvalues; /// /// Initializes a new instance of the class. /// protected Property() { } /// /// Initializes a new instance of the class. /// public Property(IPropertyType propertyType) { PropertyType = propertyType; } /// /// Initializes a new instance of the class. /// public Property(int id, IPropertyType propertyType) { Id = id; PropertyType = propertyType; } /// /// Represents a property value. /// public class PropertyValue : IPropertyValue { // TODO: Either we allow change tracking at this class level, or we add some special change tracking collections to the Property // class to deal with change tracking which variants have changed private string _culture; private string _segment; /// /// Gets or sets the culture of the property. /// /// The culture is either null (invariant) or a non-empty string. If the property is /// set with an empty or whitespace value, its value is converted to null. public string Culture { get => _culture; set => _culture = value.IsNullOrWhiteSpace() ? null : value.ToLowerInvariant(); } /// /// Gets or sets the segment of the property. /// /// The segment is either null (neutral) or a non-empty string. If the property is /// set with an empty or whitespace value, its value is converted to null. public string Segment { get => _segment; set => _segment = value?.ToLowerInvariant(); } /// /// Gets or sets the edited value of the property. /// public object EditedValue { get; set; } /// /// Gets or sets the published value of the property. /// public object PublishedValue { get; set; } /// /// Clones the property value. /// public IPropertyValue Clone() => new PropertyValue { _culture = _culture, _segment = _segment, PublishedValue = PublishedValue, EditedValue = EditedValue }; } private static readonly DelegateEqualityComparer PropertyValueComparer = new DelegateEqualityComparer( (o, o1) => { if (o == null && o1 == null) return true; // custom comparer for strings. // if one is null and another is empty then they are the same if (o is string || o1 is string) return ((o as string).IsNullOrWhiteSpace() && (o1 as string).IsNullOrWhiteSpace()) || (o != null && o1 != null && o.Equals(o1)); if (o == null || o1 == null) return false; // custom comparer for enumerable // ReSharper disable once MergeCastWithTypeCheck if (o is IEnumerable && o1 is IEnumerable enumerable) return ((IEnumerable)o).Cast().UnsortedSequenceEqual(enumerable.Cast()); return o.Equals(o1); }, o => o.GetHashCode()); /// /// Returns the PropertyType, which this Property is based on /// [IgnoreDataMember] public IPropertyType PropertyType { get; private set; } /// /// Gets the list of values. /// [DataMember] public IReadOnlyCollection Values { get => _values; set { // make sure we filter out invalid variations // make sure we leave _vvalues null if possible _values = value.Where(x => PropertyType.SupportsVariation(x.Culture, x.Segment)).ToList(); _pvalue = _values.FirstOrDefault(x => x.Culture == null && x.Segment == null); _vvalues = _values.Count > (_pvalue == null ? 0 : 1) ? _values.Where(x => x != _pvalue).ToDictionary(x => new CompositeNStringNStringKey(x.Culture, x.Segment), x => x) : null; } } /// /// Returns the Alias of the PropertyType, which this Property is based on /// [DataMember] public string Alias => PropertyType.Alias; /// /// Returns the Id of the PropertyType, which this Property is based on /// [IgnoreDataMember] public int PropertyTypeId => PropertyType.Id; /// /// Returns the DatabaseType that the underlaying DataType is using to store its values /// /// /// Only used internally when saving the property value. /// [IgnoreDataMember] public ValueStorageType ValueStorageType => PropertyType.ValueStorageType; /// /// Gets the value. /// public object GetValue(string culture = null, string segment = null, bool published = false) { // ensure null or whitespace are nulls culture = culture.NullOrWhiteSpaceAsNull(); segment = segment.NullOrWhiteSpaceAsNull(); if (!PropertyType.SupportsVariation(culture, segment)) return null; if (culture == null && segment == null) return GetPropertyValue(_pvalue, published); if (_vvalues == null) return null; return _vvalues.TryGetValue(new CompositeNStringNStringKey(culture, segment), out var pvalue) ? GetPropertyValue(pvalue, published) : null; } private object GetPropertyValue(IPropertyValue pvalue, bool published) { if (pvalue == null) return null; return PropertyType.SupportsPublishing ? (published ? pvalue.PublishedValue : pvalue.EditedValue) : pvalue.EditedValue; } // internal - must be invoked by the content item // does *not* validate the value - content item must validate first public void PublishValues(string culture = "*", string segment = "*") { culture = culture.NullOrWhiteSpaceAsNull(); segment = segment.NullOrWhiteSpaceAsNull(); // if invariant or all, and invariant-neutral is supported, publish invariant-neutral if ((culture == null || culture == "*") && (segment == null || segment == "*") && PropertyType.SupportsVariation(null, null)) PublishValue(_pvalue); // then deal with everything that varies if (_vvalues == null) return; // get the property values that are still relevant (wrt the property type variation), // and match the specified culture and segment (or anything when '*'). var pvalues = _vvalues.Where(x => PropertyType.SupportsVariation(x.Value.Culture, x.Value.Segment, true) && // the value variation is ok (culture == "*" || x.Value.Culture.InvariantEquals(culture)) && // the culture matches (segment == "*" || x.Value.Segment.InvariantEquals(segment))) // the segment matches .Select(x => x.Value); foreach (var pvalue in pvalues) PublishValue(pvalue); } // internal - must be invoked by the content item public void UnpublishValues(string culture = "*", string segment = "*") { culture = culture.NullOrWhiteSpaceAsNull(); segment = segment.NullOrWhiteSpaceAsNull(); // if invariant or all, and invariant-neutral is supported, publish invariant-neutral if ((culture == null || culture == "*") && (segment == null || segment == "*") && PropertyType.SupportsVariation(null, null)) UnpublishValue(_pvalue); // then deal with everything that varies if (_vvalues == null) return; // get the property values that are still relevant (wrt the property type variation), // and match the specified culture and segment (or anything when '*'). var pvalues = _vvalues.Where(x => PropertyType.SupportsVariation(x.Value.Culture, x.Value.Segment, true) && // the value variation is ok (culture == "*" || x.Value.Culture.InvariantEquals(culture)) && // the culture matches (segment == "*" || x.Value.Segment.InvariantEquals(segment))) // the segment matches .Select(x => x.Value); foreach (var pvalue in pvalues) UnpublishValue(pvalue); } private void PublishValue(IPropertyValue pvalue) { if (pvalue == null) return; if (!PropertyType.SupportsPublishing) throw new NotSupportedException("Property type does not support publishing."); var origValue = pvalue.PublishedValue; pvalue.PublishedValue = PropertyType.ConvertAssignedValue(pvalue.EditedValue); DetectChanges(pvalue.EditedValue, origValue, nameof(Values), PropertyValueComparer, false); } private void UnpublishValue(IPropertyValue pvalue) { if (pvalue == null) return; if (!PropertyType.SupportsPublishing) throw new NotSupportedException("Property type does not support publishing."); var origValue = pvalue.PublishedValue; pvalue.PublishedValue = PropertyType.ConvertAssignedValue(null); DetectChanges(pvalue.EditedValue, origValue, nameof(Values), PropertyValueComparer, false); } /// /// Sets a value. /// public void SetValue(object value, string culture = null, string segment = null) { culture = culture.NullOrWhiteSpaceAsNull(); segment = segment.NullOrWhiteSpaceAsNull(); if (!PropertyType.SupportsVariation(culture, segment)) throw new NotSupportedException($"Variation \"{culture??""},{segment??""}\" is not supported by the property type."); var (pvalue, change) = GetPValue(culture, segment, true); var origValue = pvalue.EditedValue; var setValue = PropertyType.ConvertAssignedValue(value); pvalue.EditedValue = setValue; DetectChanges(setValue, origValue, nameof(Values), PropertyValueComparer, change); } // bypasses all changes detection and is the *only* way to set the published value internal void FactorySetValue(string culture, string segment, bool published, object value) { var (pvalue, _) = GetPValue(culture, segment, true); if (published && PropertyType.SupportsPublishing) pvalue.PublishedValue = value; else pvalue.EditedValue = value; } private (IPropertyValue, bool) GetPValue(bool create) { var change = false; if (_pvalue == null) { if (!create) return (null, false); _pvalue = new PropertyValue(); _values.Add(_pvalue); change = true; } return (_pvalue, change); } private (IPropertyValue, bool) GetPValue(string culture, string segment, bool create) { if (culture == null && segment == null) return GetPValue(create); var change = false; if (_vvalues == null) { if (!create) return (null, false); _vvalues = new Dictionary(); change = true; } var k = new CompositeNStringNStringKey(culture, segment); if (!_vvalues.TryGetValue(k, out var pvalue)) { if (!create) return (null, false); pvalue = _vvalues[k] = new PropertyValue(); pvalue.Culture = culture; pvalue.Segment = segment; _values.Add(pvalue); change = true; } return (pvalue, change); } protected override void PerformDeepClone(object clone) { base.PerformDeepClone(clone); var clonedEntity = (Property)clone; //need to manually assign since this is a readonly property clonedEntity.PropertyType = (PropertyType) PropertyType.DeepClone(); } } }