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}