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