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}