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