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