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