using System.Text; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Infrastructure.Examine; using Umbraco.Extensions; namespace Umbraco.Cms.Core.PropertyEditors; internal abstract class NestedPropertyIndexValueFactoryBase : JsonPropertyIndexValueFactoryBase { private readonly PropertyEditorCollection _propertyEditorCollection; protected NestedPropertyIndexValueFactoryBase( PropertyEditorCollection propertyEditorCollection, IJsonSerializer jsonSerializer) : base(jsonSerializer) { _propertyEditorCollection = propertyEditorCollection; } protected override IEnumerable>> Handle( TSerialized deserializedPropertyValue, IProperty property, string? culture, string? segment, bool published) { var result = new List>>(); foreach (TItem nestedContentRowValue in GetDataItems(deserializedPropertyValue)) { IContentType? contentType = GetContentTypeOfNestedItem(nestedContentRowValue); if (contentType is null) { continue; } var propertyTypeDictionary = contentType .PropertyGroups .SelectMany(x => x.PropertyTypes!) .Select(propertyType => { // We want to ensure that the nested properties are set vary by culture if the parent is // This is because it's perfectly valid to have a nested property type that's set to invariant even if the parent varies. // For instance in a block list, the list it self can vary, but the elements can be invariant, at the same time. if (culture is not null) { propertyType.Variations |= ContentVariation.Culture; } if (segment is not null) { propertyType.Variations |= ContentVariation.Segment; } return propertyType; }) .ToDictionary(x => x.Alias); result.AddRange(GetNestedResults( property.Alias, culture, segment, published, propertyTypeDictionary, nestedContentRowValue)); } return RenameKeysToEnsureRawSegmentsIsAPrefix(result); } /// /// Rename keys that count the RAW-constant, to ensure the RAW-constant is a prefix. /// private IEnumerable>> RenameKeysToEnsureRawSegmentsIsAPrefix( List>> indexContent) { foreach (KeyValuePair> indexedKeyValuePair in indexContent) { // Tests if key includes the RawFieldPrefix and it is not in the start if (indexedKeyValuePair.Key.Substring(1).Contains(UmbracoExamineFieldNames.RawFieldPrefix)) { var newKey = UmbracoExamineFieldNames.RawFieldPrefix + indexedKeyValuePair.Key.Replace(UmbracoExamineFieldNames.RawFieldPrefix, string.Empty); yield return new KeyValuePair>(newKey, indexedKeyValuePair.Value); } else { yield return indexedKeyValuePair; } } } /// /// Gets the content type using the nested item. /// protected abstract IContentType? GetContentTypeOfNestedItem(TItem nestedItem); /// /// Gets the raw data from a nested item. /// protected abstract IDictionary GetRawProperty(TItem nestedItem); /// /// Get the data times of a parent item. E.g. block list have contentData. /// protected abstract IEnumerable GetDataItems(TSerialized input); /// /// Index a key with the name of the property, using the relevant content of all the children. /// protected override IEnumerable>> HandleResume( List>> indexedContent, IProperty property, string? culture, string? segment, bool published) { yield return new KeyValuePair>( property.Alias, GetResumeFromAllContent(indexedContent).Yield()); } /// /// Gets a resume as string of all the content in this nested type. /// /// All the indexed content for this property. /// the string with all relevant content from private static string GetResumeFromAllContent(List>> indexedContent) { var stringBuilder = new StringBuilder(); foreach ((var indexKey, IEnumerable? indexedValue) in indexedContent) { // Ignore Raw fields if (indexKey.Contains(UmbracoExamineFieldNames.RawFieldPrefix)) { continue; } foreach (var value in indexedValue) { if (value is not null) { stringBuilder.AppendLine(value.ToString()); } } } return stringBuilder.ToString(); } /// /// Gets the content to index for the nested type. E.g. Block list, Nested Content, etc.. /// private IEnumerable>> GetNestedResults( string keyPrefix, string? culture, string? segment, bool published, IDictionary propertyTypeDictionary, TItem nestedContentRowValue) { var blockIndex = 0; foreach ((var propertyAlias, var propertyValue) in GetRawProperty(nestedContentRowValue)) { if (propertyTypeDictionary.TryGetValue(propertyAlias, out IPropertyType? propertyType)) { IProperty subProperty = new Property(propertyType); subProperty.SetValue(propertyValue, culture, segment); if (published) { subProperty.PublishValues(culture, segment ?? "*"); } IDataEditor? editor = _propertyEditorCollection[propertyType.PropertyEditorAlias]; if (editor is null) { continue; } IEnumerable>> indexValues = editor.PropertyIndexValueFactory.GetIndexValues(subProperty, culture, segment, published); foreach ((var nestedAlias, IEnumerable nestedValue) in indexValues) { yield return new KeyValuePair>( $"{keyPrefix}.items[{blockIndex}].{nestedAlias}", nestedValue!); } } blockIndex++; } } }