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