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.ArrayList;
019import java.util.Collection;
020import java.util.List;
021import java.util.Map.Entry;
022import java.util.Optional;
023import java.util.Set;
024import java.util.stream.Collectors;
025import java.util.stream.Stream;
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.indexing.solr.AdditionalDataIndexer;
039import org.ametys.cms.indexing.solr.AdditionalDataIndexerExtensionPoint;
040import org.ametys.cms.model.CMSDataContext;
041import org.ametys.cms.repository.Content;
042import org.ametys.cms.search.model.SystemPropertyExtensionPoint;
043import org.ametys.plugins.repository.AmetysObject;
044import org.ametys.plugins.repository.data.type.ModelItemTypeConstants;
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 extension point for ContentVisibleAttachmentIndexers */
062    protected ContentVisibleAttachmentIndexerExtensionPoint _contentVisibleAttachmentIndexerEP;
063    /** The additional property indexer extension point. */
064    protected AdditionalDataIndexerExtensionPoint _additionalDataIndexerEP;
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        _additionalDataIndexerEP = (AdditionalDataIndexerExtensionPoint) manager.lookup(AdditionalDataIndexerExtensionPoint.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 the content indexation
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        CMSDataContext context = CMSDataContext.newInstance();
100        Optional.ofNullable(content.getLanguage())
101                .filter(StringUtils::isNotBlank)
102                .map(LocaleUtils::toLocale)
103                .ifPresent(context::withLocale);
104        List<SolrInputDocument> contentIndexData = content.indexData(document, context);
105        
106        List<SolrInputDocument> additionalDocuments = _populateAdditionalData(content, document);
107        
108        return Stream.concat(contentIndexData.stream(), additionalDocuments.stream())
109                .toList();
110    }
111    
112    private void _indexOutgoingReferences(Content content, SolrInputDocument document)
113    {
114        content.getOutgoingReferences().values() // key is the data path, we do not care what data it comes from
115                .parallelStream()
116                .map(OutgoingReferences::entrySet)
117                .flatMap(Set::parallelStream)
118                .filter(outgoingRefs -> outgoingRefs.getKey().equals("explorer")) // only references of the resource explorer
119                .map(Entry::getValue)
120                .flatMap(List::parallelStream) // flat the resource ids
121                .forEach(resourceId -> document.addField(CONTENT_OUTGOING_REFEERENCES_RESOURCE_IDS, resourceId));
122        
123        // Attachments of the content (just the root folder)
124        Optional.ofNullable(content.getRootAttachments())
125                .map(AmetysObject::getId)
126                .ifPresent(id -> document.addField(CONTENT_OUTGOING_REFEERENCES_RESOURCE_IDS, id));
127    }
128    
129    private void _indexVisibleAttachments(Content content, SolrInputDocument document)
130    {
131        Collection<String> values = _contentVisibleAttachmentIndexerEP.getExtensionsIds()
132                .stream()
133                .map(_contentVisibleAttachmentIndexerEP::getExtension)
134                .map(attachmentIndexer -> attachmentIndexer.getVisibleAttachmentIds(content))
135                .flatMap(Collection::stream)
136                .collect(Collectors.toList());
137        document.addField(CONTENT_VISIBLE_ATTACHMENT_RESOURCE_IDS, values);
138    }
139    
140    /**
141     * Index the content title
142     * @param content The title
143     * @param document The main solr document to index into
144     */
145    protected void indexContentTitle(Content content, SolrInputDocument document)
146    {
147        if (!ModelItemTypeConstants.MULTILINGUAL_STRING_ELEMENT_TYPE_ID.equals(content.getType(Content.ATTRIBUTE_TITLE).getId()))
148        {
149            String title = _contentHelper.getTitle(content);
150            document.addField(TITLE, SolrIndexer.truncateUtf8StringValue(title, getLogger(), content.getId(), TITLE));
151            document.addField(TITLE_SORT, title);
152        }
153    }
154    
155    /**
156     * Populate the solr input document by adding fields to index.
157     * @param content the content to index.
158     * @param document the solr input document
159     * @return Additional documents created by additional property indexers
160     * @throws Exception if something goes wrong when processing the indexation of the content
161     */
162    protected List<SolrInputDocument> _populateAdditionalData(Content content, SolrInputDocument document) throws Exception
163    {
164        List<SolrInputDocument> additionnalDocs = new ArrayList<>();
165        
166        Collection<AdditionalDataIndexer> indexers = _additionalDataIndexerEP.getIndexers(AdditionalDataIndexer.TYPE_CONTENT);
167        for (AdditionalDataIndexer indexer : indexers)
168        {
169            additionnalDocs.addAll(indexer.indexAdditionalDocuments(content, document));
170        }
171        
172        return additionnalDocs;
173    }
174}