001/*
002 *  Copyright 2015 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.content.indexing.solr;
017
018import java.util.Collection;
019import java.util.HashMap;
020import java.util.List;
021import java.util.Locale;
022import java.util.Map;
023import java.util.Map.Entry;
024import java.util.Optional;
025import java.util.Set;
026import java.util.stream.Collectors;
027
028import org.apache.avalon.framework.component.Component;
029import org.apache.avalon.framework.service.ServiceException;
030import org.apache.avalon.framework.service.ServiceManager;
031import org.apache.avalon.framework.service.Serviceable;
032import org.apache.commons.lang3.StringUtils;
033import org.apache.solr.common.SolrInputDocument;
034
035import org.ametys.cms.content.ContentHelper;
036import org.ametys.cms.content.indexing.solr.content.attachment.ContentVisibleAttachmentIndexerExtensionPoint;
037import org.ametys.cms.content.references.OutgoingReferences;
038import org.ametys.cms.content.references.OutgoingReferencesExtractor;
039import org.ametys.cms.data.type.ModelItemTypeConstants;
040import org.ametys.cms.data.type.indexing.IndexableDataContext;
041import org.ametys.cms.model.properties.Property;
042import org.ametys.cms.repository.Content;
043import org.ametys.cms.search.model.SystemProperty;
044import org.ametys.cms.search.model.SystemPropertyExtensionPoint;
045import org.ametys.plugins.repository.AmetysObject;
046import org.ametys.runtime.model.ModelItem;
047import org.ametys.runtime.plugin.component.AbstractLogEnabled;
048
049/**
050 * Component for {@link Content} indexing into a Solr server.
051 */
052public class SolrContentIndexer extends AbstractLogEnabled implements Component, Serviceable, SolrFieldNames
053{
054    /** The component role. */
055    public static final String ROLE = SolrContentIndexer.class.getName();
056    
057    /** The resource indexer */
058    protected SolrResourceIndexer _resourceIndexer;
059    /** The system property extension point. */
060    protected SystemPropertyExtensionPoint _systemPropEP;
061    /** The content helper */
062    protected ContentHelper _contentHelper;
063    /** The outgoing references extractor */
064    protected OutgoingReferencesExtractor _outgoingReferencesExtractor;
065    /** The extension point for ContentVisibleAttachmentIndexers */
066    protected ContentVisibleAttachmentIndexerExtensionPoint _contentVisibleAttachmentIndexerEP;
067    
068    @Override
069    public void service(ServiceManager manager) throws ServiceException
070    {
071        _resourceIndexer = (SolrResourceIndexer) manager.lookup(SolrResourceIndexer.ROLE);
072        _contentHelper = (ContentHelper) manager.lookup(ContentHelper.ROLE);
073        _systemPropEP = (SystemPropertyExtensionPoint) manager.lookup(SystemPropertyExtensionPoint.ROLE);
074        _outgoingReferencesExtractor = (OutgoingReferencesExtractor) manager.lookup(OutgoingReferencesExtractor.ROLE);
075        _contentVisibleAttachmentIndexerEP = (ContentVisibleAttachmentIndexerExtensionPoint) manager.lookup(ContentVisibleAttachmentIndexerExtensionPoint.ROLE);
076    }
077    
078    /**
079     * Populate a solr input document by adding fields to index into it.
080     * @param content The content to index
081     * @param document The main solr document to index into
082     * @return Additional documents for repeater instances
083     * @throws Exception if an error occurred while indexing
084     */
085    public List<SolrInputDocument> indexContent(Content content, SolrInputDocument document) throws Exception
086    {
087        // Properties specific to a stand-alone indexation.
088        String contentId = content.getId();
089        document.addField(ID, contentId);
090        document.addField(DOCUMENT_TYPE, TYPE_CONTENT);
091        
092        indexContentTitle(content, document); 
093       
094        document.addField(CONTENT_NAME, SolrIndexer.truncateUtf8StringValue(content.getName(), getLogger(), contentId, CONTENT_NAME));
095        _indexOutgoingReferences(content, document);
096        _indexVisibleAttachments(content, document);
097        
098        document.addField(WORKFLOW_REF_DV, contentId + "#workflow");
099        
100        // Index content properties and attributes
101        IndexableDataContext context = IndexableDataContext.newInstance();
102        Optional.ofNullable(content.getLanguage())
103                .filter(StringUtils::isNotBlank)
104                .map(Locale::new)
105                .ifPresent(context::withLocale);
106        return content.indexData(document, context);
107    }
108    
109    private void _indexOutgoingReferences(Content content, SolrInputDocument document)
110    {
111        // Found by the extractor (resource references found in all data of the content)
112        _outgoingReferencesExtractor.getOutgoingReferences(content).values() // key is the data path, we do not care what data it comes from
113                .parallelStream()
114                .map(OutgoingReferences::entrySet)
115                .flatMap(Set::parallelStream)
116                .filter(outgoingRefs -> outgoingRefs.getKey().equals("explorer")) // only references of the resource explorer
117                .map(Entry::getValue)
118                .flatMap(List::parallelStream) // flat the resource ids
119                .forEach(resourceId -> document.addField(CONTENT_OUTGOING_REFEERENCES_RESOURCE_IDS, resourceId));
120        
121        // Attachments of the content (just the root folder)
122        Optional.ofNullable(content.getRootAttachments())
123                .map(AmetysObject::getId)
124                .ifPresent(id -> document.addField(CONTENT_OUTGOING_REFEERENCES_RESOURCE_IDS, id));
125    }
126    
127    private void _indexVisibleAttachments(Content content, SolrInputDocument document)
128    {
129        Collection<String> values = _contentVisibleAttachmentIndexerEP.getExtensionsIds()
130                .stream()
131                .map(_contentVisibleAttachmentIndexerEP::getExtension)
132                .map(attachmentIndexer -> attachmentIndexer.getVisibleAttachmentIds(content))
133                .flatMap(Collection::stream)
134                .collect(Collectors.toList());
135        document.addField(CONTENT_VISIBLE_ATTACHMENT_RESOURCE_IDS, values);
136    }
137    
138    /**
139     * Index the content title
140     * @param content The title
141     * @param document The main solr document to index into
142     */
143    protected void indexContentTitle(Content content, SolrInputDocument document)
144    {
145        if (!ModelItemTypeConstants.MULTILINGUAL_STRING_ELEMENT_TYPE_ID.equals(content.getType(Content.ATTRIBUTE_TITLE).getId()))
146        {
147            String title = _contentHelper.getTitle(content);
148            document.addField(TITLE, SolrIndexer.truncateUtf8StringValue(title, getLogger(), content.getId(), TITLE));
149            document.addField(TITLE_SORT, title);
150        }
151    }
152    
153    /**
154     * Populate a Solr input document by adding fields for a single system property.
155     * @param content The content to index
156     * @param propertyId The system property ID.
157     * @param document The solr document
158     * @return true if there are partial update to apply
159     * @throws Exception if an error occurred
160     */
161    public boolean indexPartialSystemProperty(Content content, String propertyId, SolrInputDocument document) throws Exception
162    {
163        if (!_systemPropEP.hasExtension(propertyId))
164        {
165            throw new IllegalStateException("The system property '" + propertyId + "' can't be indexed as it does not exist.");
166        }
167        
168        SystemProperty property = _systemPropEP.getExtension(propertyId);
169        return indexPartialProperty(content, property, document);
170    }
171    
172    /**
173     * Populate a Solr input document by adding fields for a single property.
174     * @param content The content to index
175     * @param propertyId The property ID.
176     * @param document The solr document
177     * @return true if there are partial update to apply
178     * @throws Exception if an error occurred
179     */
180    public boolean indexPartialProperty(Content content, String propertyId, SolrInputDocument document) throws Exception
181    {
182        ModelItem modelItem = content.getDataHolder().getDefinition(propertyId);
183        if (modelItem instanceof Property property)
184        {
185            return indexPartialProperty(content, property, document);
186        }
187        
188        return false;
189    }
190    
191    /**
192     * Populate a Solr input document by adding fields for a single property.
193     * @param content The content to index
194     * @param property The property to index.
195     * @param document The solr document
196     * @return true if there are partial update to apply
197     * @throws Exception if an error occurred
198     */
199    @SuppressWarnings("unchecked")
200    public boolean indexPartialProperty(Content content, Property property, SolrInputDocument document) throws Exception
201    {
202        SolrInputDocument tempDocument = new SolrInputDocument();
203        
204        property.indexValue(tempDocument, content, IndexableDataContext.newInstance());
205        
206        if (tempDocument.isEmpty())
207        {
208            // Does not have any partial update to apply, avoid to erase all the existing fields on the Solr document corresponding to this content (it would be lost)
209            return false;
210        }
211        
212        // Copy the indexed values as partial updates.
213        for (String fieldName : tempDocument.getFieldNames())
214        {
215            Collection<Object> fieldValues = tempDocument.getFieldValues(fieldName);
216            
217            Map<String, Object> partialUpdate = new HashMap<>();
218            partialUpdate.put("set", fieldValues);
219            document.addField(fieldName, partialUpdate);
220        }
221        
222        document.addField("id", content.getId());
223        
224        return true;
225    }
226    
227}