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