001/* 002 * Copyright 2022 Anyware Services 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.ametys.cms.data.holder.impl; 017 018import java.util.ArrayList; 019import java.util.Collections; 020import java.util.List; 021import java.util.Optional; 022 023import org.apache.avalon.framework.activity.Disposable; 024import org.apache.avalon.framework.component.Component; 025import org.apache.avalon.framework.service.ServiceException; 026import org.apache.avalon.framework.service.ServiceManager; 027import org.apache.avalon.framework.service.Serviceable; 028import org.apache.commons.lang3.StringUtils; 029import org.apache.commons.lang3.Strings; 030import org.apache.solr.common.SolrInputDocument; 031 032import org.ametys.cms.content.indexing.solr.SolrFieldNames; 033import org.ametys.cms.data.ContentValue; 034import org.ametys.cms.data.ametysobject.IndexableAmetysObject; 035import org.ametys.cms.data.holder.IndexableDataHolder; 036import org.ametys.cms.data.holder.group.IndexableComposite; 037import org.ametys.cms.data.holder.group.IndexableRepeater; 038import org.ametys.cms.data.holder.group.IndexableRepeaterEntry; 039import org.ametys.cms.data.type.indexing.IndexableElementType; 040import org.ametys.cms.model.CMSDataContext; 041import org.ametys.cms.model.ContentElementDefinition; 042import org.ametys.cms.model.properties.Property; 043import org.ametys.cms.search.model.IndexationAwareElementDefinition; 044import org.ametys.cms.search.model.SystemProperty; 045import org.ametys.cms.search.model.SystemPropertyExtensionPoint; 046import org.ametys.plugins.repository.data.holder.group.Repeater; 047import org.ametys.plugins.repository.model.CompositeDefinition; 048import org.ametys.plugins.repository.model.RepeaterDefinition; 049import org.ametys.plugins.repository.model.RepositoryDataContext; 050import org.ametys.runtime.model.ElementDefinition; 051import org.ametys.runtime.model.ModelItem; 052import org.ametys.runtime.model.ModelViewItem; 053import org.ametys.runtime.model.View; 054import org.ametys.runtime.model.ViewHelper; 055import org.ametys.runtime.model.ViewItem; 056import org.ametys.runtime.model.ViewItemAccessor; 057import org.ametys.runtime.model.exception.BadItemTypeException; 058import org.ametys.runtime.model.exception.UndefinedItemPathException; 059import org.ametys.runtime.model.type.DataContext; 060import org.ametys.runtime.model.type.ElementType; 061 062/** 063 * Helper for implementations of indexable data holder 064 */ 065public final class IndexableDataHolderHelper implements Component, Serviceable, Disposable 066{ 067 private static SystemPropertyExtensionPoint _contentSystemPropertyExtensionPoint; 068 069 @Override 070 public void service(ServiceManager manager) throws ServiceException 071 { 072 _contentSystemPropertyExtensionPoint = (SystemPropertyExtensionPoint) manager.lookup(SystemPropertyExtensionPoint.ROLE); 073 } 074 075 public void dispose() 076 { 077 _contentSystemPropertyExtensionPoint = null; 078 } 079 080 /** 081 * Indexes data of the given {@link IndexableDataHolder} 082 * @param dataHolder the {@link IndexableDataHolder} to index 083 * @param viewItemAccessor the view item accessor to explore 084 * @param document the solr document representing this {@link IndexableDataHolder} 085 * @param rootDocument the solr document of the root object. 086 * @param solrFieldPrefix the prefix of the solr field 087 * @param context The context of the data to index 088 * @return additional solr documents that may have been created (ex: repeater entries) 089 * @throws BadItemTypeException if the saxed value's type does not matches the stored data 090 */ 091 public static List<SolrInputDocument> indexData(IndexableDataHolder dataHolder, ViewItemAccessor viewItemAccessor, SolrInputDocument document, SolrInputDocument rootDocument, String solrFieldPrefix, CMSDataContext context) throws BadItemTypeException 092 { 093 ViewItemAccessor mergedViewItemAccessor = _mergeDuplicatedItemsInView(viewItemAccessor); 094 095 List<SolrInputDocument> additionalDocuments = new ArrayList<>(); 096 097 for (ViewItem viewItem : mergedViewItemAccessor.getViewItems()) 098 { 099 additionalDocuments.addAll(_indexDataFromViewItem(dataHolder, viewItem, document, rootDocument, solrFieldPrefix, context)); 100 } 101 102 return additionalDocuments; 103 } 104 105 private static ViewItemAccessor _mergeDuplicatedItemsInView(ViewItemAccessor viewItemAccessor) 106 { 107 return viewItemAccessor instanceof View 108 ? org.ametys.runtime.model.ViewHelper.mergeDuplicatedItems(viewItemAccessor) 109 : viewItemAccessor; 110 } 111 112 @SuppressWarnings("unchecked") 113 private static List<SolrInputDocument> _indexDataFromViewItem(IndexableDataHolder dataHolder, ViewItem viewItem, SolrInputDocument document, SolrInputDocument rootDocument, String solrFieldPrefix, CMSDataContext context) 114 { 115 if (viewItem instanceof ModelViewItem modelViewItem) 116 { 117 ModelItem modelItem = modelViewItem.getDefinition(); 118 String dataName = modelItem.getName(); 119 CMSDataContext newContext = context.cloneContext() 120 .addSegmentToDataPath(dataName) 121 .withModelItem(modelItem) 122 .withViewItem(viewItem); 123 124 if (modelItem instanceof IndexationAwareElementDefinition indexationAwareElementDefinition) 125 { 126 indexationAwareElementDefinition.indexValue(document, getAmetysObjectFromContext(newContext), newContext); 127 return Collections.EMPTY_LIST; 128 } 129 else if (!(modelItem instanceof Property) && dataHolder.hasValue(dataName)) 130 { 131 if (modelItem instanceof ElementDefinition definition) 132 { 133 ElementType type = definition.getType(); 134 135 if (type instanceof IndexableElementType indexingType) 136 { 137 Object value = dataHolder.getValue(dataName); 138 139 String solrFieldName = solrFieldPrefix + dataName; 140 indexingType.indexValue(document, rootDocument, solrFieldName, value, newContext); 141 } 142 143 return Collections.EMPTY_LIST; 144 } 145 else if (modelItem instanceof CompositeDefinition) 146 { 147 IndexableComposite composite = dataHolder.getValue(dataName); 148 String newSolrFieldPrefix = solrFieldPrefix + dataName + ModelItem.ITEM_PATH_SEPARATOR; 149 150 return indexData(composite, (ViewItemAccessor) viewItem, document, rootDocument, newSolrFieldPrefix, newContext); 151 } 152 else if (modelItem instanceof RepeaterDefinition) 153 { 154 IndexableRepeater repeater = dataHolder.getValue(dataName); 155 return repeater.indexData(document, rootDocument, solrFieldPrefix, newContext); 156 } 157 } 158 } 159 else if (viewItem instanceof ViewItemAccessor accessor) 160 { 161 return indexData(dataHolder, accessor, document, rootDocument, solrFieldPrefix, context); 162 } 163 return Collections.EMPTY_LIST; 164 } 165 166 /** 167 * Indexes all data and properties of the given {@link Repeater} 168 * @param repeater the {@link Repeater} to index 169 * @param document the solr document representing the {@link Repeater} 170 * @param rootDocument the solr document of the root object. 171 * @param solrFieldPrefix the prefix of the solr field 172 * @param context The context of the data to index 173 * @return additional solr documents that may have been created (ex: repeater entries) 174 * @throws BadItemTypeException if the saxed value's type does not matches the stored data 175 */ 176 public static List<SolrInputDocument> indexRepeaterData(IndexableRepeater repeater, SolrInputDocument document, SolrInputDocument rootDocument, String solrFieldPrefix, CMSDataContext context) throws BadItemTypeException 177 { 178 List<SolrInputDocument> additionalDocuments = new ArrayList<>(); 179 String solrFieldName = solrFieldPrefix + context.getDataPathLastSegment(); 180 181 for (IndexableRepeaterEntry entry : repeater.getEntries()) 182 { 183 // Update the context with entry position 184 CMSDataContext newContext = context.cloneContext() 185 .addSuffixToLastSegment("[" + entry.getPosition() + "]"); 186 187 SolrInputDocument repeaterEntryDoc = new SolrInputDocument(); 188 189 if (!context.indexForFullTextField()) 190 { 191 // Creates a new Solr document for each entry 192 String repeaterEntryDocId = document.getField("id").getFirstValue().toString() + "/" + solrFieldName + "/" + entry.getPosition(); 193 repeaterEntryDoc.addField("id", repeaterEntryDocId); 194 repeaterEntryDoc.addField(SolrFieldNames.DOCUMENT_TYPE, SolrFieldNames.TYPE_REPEATER); 195 repeaterEntryDoc.addField(SolrFieldNames.REPEATER_ENTRY_POSITION, entry.getPosition()); 196 197 document.addField(solrFieldName + "_s_dv", repeaterEntryDocId); 198 } 199 200 // Add the created document to additional documents 201 additionalDocuments.add(repeaterEntryDoc); 202 203 ViewItemAccessor viewItemAccessor = context.getViewItem() 204 .map(ViewItemAccessor.class::cast) 205 .orElse(ViewHelper.createViewItemAccessor(entry.getModel())); 206 additionalDocuments.addAll(indexData(entry, viewItemAccessor, repeaterEntryDoc, rootDocument, StringUtils.EMPTY, newContext)); 207 } 208 209 return additionalDocuments; 210 } 211 212 /** 213 * Retrieves the {@link IndexableAmetysObject} from the given {@link DataContext} 214 * @param context the context containing the ametys object 215 * @return the ametys object, or <code>null</code> if there is no object id in the context 216 */ 217 public static IndexableAmetysObject getAmetysObjectFromContext(DataContext context) 218 { 219 RepositoryDataContext repoContext = context instanceof RepositoryDataContext rc ? rc : RepositoryDataContext.newInstance(context); 220 221 return repoContext.getObject() 222 .filter(IndexableAmetysObject.class::isInstance) 223 .map(IndexableAmetysObject.class::cast) 224 .orElse(null); 225 } 226 227 /** 228 * Checks if the given property has a value on the root data holder of the given data holder 229 * @param dataHolder the data holder to check the property on 230 * @param property the property to check 231 * @return <code>true</code> if the property has a value, <code>false</code> otherwise 232 */ 233 @SuppressWarnings("unchecked") 234 public static boolean hasPropertyValue(IndexableDataHolder dataHolder, Property property) 235 { 236 return dataHolder.getRootDataHolder() instanceof IndexableAmetysObject ametysObject 237 && property.getValue(ametysObject) != null; 238 } 239 240 /** 241 * Retrieves the value of the given property from the root data holder of the given data holder 242 * @param <T> the type of the property value 243 * @param dataHolder the data holder to get the property value from 244 * @param property the property to get the value of 245 * @return the value of the property, or <code>null</code> if the property has no value 246 */ 247 @SuppressWarnings("unchecked") 248 public static <T> T getPropertyValue(IndexableDataHolder dataHolder, Property property) 249 { 250 return dataHolder.getRootDataHolder() instanceof IndexableAmetysObject ametysObject 251 ? (T) property.getValue(ametysObject) 252 : null; 253 } 254 255 /** 256 * Retrieves the {@link SystemProperty} corresponding to the given path, if it exists 257 * @param dataHolder the data holder to check the system property on 258 * @param path the path of the system property, relative to the given data holder 259 * @return the {@link SystemProperty} corresponding to the given path, or an empty {@link Optional} if it does not exist 260 */ 261 public static Optional<SystemProperty> getSystemProperty(IndexableDataHolder dataHolder, String path) 262 { 263 if (Strings.CS.contains(path, ModelItem.ITEM_PATH_SEPARATOR)) 264 { 265 String parentDataPath = StringUtils.substringBeforeLast(path, ModelItem.ITEM_PATH_SEPARATOR); 266 ModelItem modelItem = dataHolder.getDefinition(parentDataPath); 267 if (modelItem instanceof ContentElementDefinition) 268 { 269 String propertyName = StringUtils.substringAfterLast(path, ModelItem.ITEM_PATH_SEPARATOR); 270 if (_contentSystemPropertyExtensionPoint.hasExtension(propertyName)) 271 { 272 return Optional.of(_contentSystemPropertyExtensionPoint.getExtension(propertyName)); 273 } 274 } 275 } 276 else if (dataHolder.getParentDataHolder().isEmpty() 277 && dataHolder.getRootDataHolder() instanceof IndexableAmetysObject ametysObject 278 && ametysObject.getSystemPropertyExtensionPoint().isPresent()) 279 { 280 SystemPropertyExtensionPoint systemPropertyExtensionPoint = ametysObject.getSystemPropertyExtensionPoint().get(); 281 if (systemPropertyExtensionPoint.hasExtension(path)) 282 { 283 return Optional.of(systemPropertyExtensionPoint.getExtension(path)); 284 } 285 } 286 287 return Optional.empty(); 288 } 289 290 /** 291 * Retrieves the ametys object containing the property at the given path 292 * @param dataHolder the data holder to check the property on 293 * @param dataPath the path of the property 294 * @return the ametys object containing the property 295 * @throws UndefinedItemPathException if the given path does not represent a property 296 */ 297 public static IndexableAmetysObject getPropertyAmetysObject(IndexableDataHolder dataHolder, String dataPath) throws UndefinedItemPathException 298 { 299 if (Strings.CS.contains(dataPath, ModelItem.ITEM_PATH_SEPARATOR)) 300 { 301 String parentDataPath = StringUtils.substringBeforeLast(dataPath, ModelItem.ITEM_PATH_SEPARATOR); 302 ContentValue value = dataHolder.getValue(parentDataPath); 303 return value.getContent(); 304 } 305 else if (dataHolder.getRootDataHolder() instanceof IndexableAmetysObject ametysObject) 306 { 307 return ametysObject; 308 } 309 else 310 { 311 throw new UndefinedItemPathException("There is no property at path '" + dataPath + "'"); 312 } 313 } 314}