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}