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.io.IOException;
019import java.nio.ByteBuffer;
020import java.nio.CharBuffer;
021import java.nio.charset.CharsetDecoder;
022import java.nio.charset.CodingErrorAction;
023import java.nio.charset.StandardCharsets;
024import java.util.ArrayList;
025import java.util.Arrays;
026import java.util.Collection;
027import java.util.Collections;
028import java.util.Comparator;
029import java.util.HashMap;
030import java.util.HashSet;
031import java.util.List;
032import java.util.Map;
033import java.util.Objects;
034import java.util.Optional;
035import java.util.Set;
036import java.util.concurrent.Future;
037import java.util.function.Function;
038import java.util.stream.Collectors;
039import java.util.stream.StreamSupport;
040
041import javax.jcr.RepositoryException;
042
043import org.apache.avalon.framework.activity.Initializable;
044import org.apache.avalon.framework.component.Component;
045import org.apache.avalon.framework.context.Context;
046import org.apache.avalon.framework.context.ContextException;
047import org.apache.avalon.framework.context.Contextualizable;
048import org.apache.avalon.framework.service.ServiceException;
049import org.apache.avalon.framework.service.ServiceManager;
050import org.apache.avalon.framework.service.Serviceable;
051import org.apache.cocoon.Constants;
052import org.apache.cocoon.components.ContextHelper;
053import org.apache.cocoon.environment.Request;
054import org.apache.commons.collections4.IterableUtils;
055import org.apache.commons.lang3.ObjectUtils;
056import org.apache.commons.lang3.StringUtils;
057import org.apache.solr.client.solrj.SolrClient;
058import org.apache.solr.client.solrj.SolrResponse;
059import org.apache.solr.client.solrj.SolrServerException;
060import org.apache.solr.client.solrj.request.CoreAdminRequest;
061import org.apache.solr.client.solrj.request.CoreAdminRequest.Create;
062import org.apache.solr.client.solrj.request.schema.FieldTypeDefinition;
063import org.apache.solr.client.solrj.request.schema.SchemaRequest;
064import org.apache.solr.client.solrj.request.schema.SchemaRequest.Update;
065import org.apache.solr.client.solrj.response.CoreAdminResponse;
066import org.apache.solr.client.solrj.response.SolrResponseBase;
067import org.apache.solr.client.solrj.response.UpdateResponse;
068import org.apache.solr.client.solrj.response.schema.SchemaRepresentation;
069import org.apache.solr.client.solrj.response.schema.SchemaResponse;
070import org.apache.solr.client.solrj.util.ClientUtils;
071import org.apache.solr.common.SolrInputDocument;
072import org.apache.solr.common.params.CoreAdminParams;
073import org.apache.solr.common.params.CoreAdminParams.CoreAdminAction;
074import org.apache.solr.common.params.ModifiableSolrParams;
075import org.apache.solr.common.params.SolrParams;
076import org.apache.solr.common.util.NamedList;
077import org.slf4j.Logger;
078
079import org.ametys.cms.data.ametysobject.IndexableAmetysObject;
080import org.ametys.cms.indexing.IndexingException;
081import org.ametys.cms.indexing.solr.AbstractIndexerCallable;
082import org.ametys.cms.indexing.solr.AdditionalDataIndexer;
083import org.ametys.cms.indexing.solr.AdditionalDataIndexerExtensionPoint;
084import org.ametys.cms.indexing.solr.IndexationResult;
085import org.ametys.cms.indexing.solr.ReloadAclCacheRequest;
086import org.ametys.cms.indexing.solr.ThreadIndexerHelper;
087import org.ametys.cms.indexing.solr.UpdateAclCacheRequest;
088import org.ametys.cms.indexing.solr.UpdateCorePropertyRequest;
089import org.ametys.cms.model.CMSDataContext;
090import org.ametys.cms.model.properties.Property;
091import org.ametys.cms.repository.Content;
092import org.ametys.cms.repository.ContentQueryHelper;
093import org.ametys.cms.repository.WorkflowAwareContent;
094import org.ametys.cms.rights.solrchecking.ReadAccessHelper;
095import org.ametys.cms.search.model.IndexationAwareElementDefinition;
096import org.ametys.cms.search.model.SystemProperty;
097import org.ametys.cms.search.query.ContentAttachmentQuery;
098import org.ametys.cms.search.query.DocumentTypeQuery;
099import org.ametys.cms.search.query.OrQuery;
100import org.ametys.cms.search.query.Query;
101import org.ametys.cms.search.query.ResourceLocationQuery;
102import org.ametys.cms.search.solr.NoAutoCommitUpdateClient;
103import org.ametys.cms.search.solr.SolrClientProvider;
104import org.ametys.cms.search.solr.schema.SchemaDefinition;
105import org.ametys.cms.search.solr.schema.SchemaDefinitionProvider;
106import org.ametys.cms.search.solr.schema.SchemaDefinitionProviderExtensionPoint;
107import org.ametys.cms.search.solr.schema.SchemaFields;
108import org.ametys.cms.search.solr.schema.SchemaHelper;
109import org.ametys.cms.trash.element.DefaultTrashElement;
110import org.ametys.cms.trash.element.TrashElementFactory;
111import org.ametys.core.group.GroupIdentity;
112import org.ametys.core.right.AllowedUsers;
113import org.ametys.core.schedule.progression.ContainerProgressionTracker;
114import org.ametys.core.schedule.progression.ProgressionTrackerFactory;
115import org.ametys.core.schedule.progression.SimpleProgressionTracker;
116import org.ametys.core.user.UserIdentity;
117import org.ametys.plugins.explorer.resources.Resource;
118import org.ametys.plugins.explorer.resources.ResourceCollection;
119import org.ametys.plugins.repository.AmetysObject;
120import org.ametys.plugins.repository.AmetysObjectIterable;
121import org.ametys.plugins.repository.AmetysObjectResolver;
122import org.ametys.plugins.repository.RepositoryConstants;
123import org.ametys.plugins.repository.TraversableAmetysObject;
124import org.ametys.plugins.repository.UnknownAmetysObjectException;
125import org.ametys.plugins.repository.provider.AbstractRepository;
126import org.ametys.plugins.repository.provider.JackrabbitRepository;
127import org.ametys.plugins.repository.provider.RequestAttributeWorkspaceSelector;
128import org.ametys.plugins.repository.provider.WorkspaceSelector;
129import org.ametys.plugins.repository.query.QueryHelper;
130import org.ametys.runtime.config.Config;
131import org.ametys.runtime.i18n.I18nizableText;
132import org.ametys.runtime.model.ModelItem;
133import org.ametys.runtime.plugin.component.AbstractLogEnabled;
134
135/**
136 * Solr indexer.
137 */
138public class SolrIndexer extends AbstractLogEnabled implements Component, Serviceable, Initializable, Contextualizable
139{
140    /** The component role. */
141    public static final String ROLE = SolrIndexer.class.getName();
142    
143    private static final String _CONFIGSET_NAME_PREFIX = "configset-";
144    
145    private static final List<String> _READ_ONLY_FIELDS = Arrays.asList("id", "_version_", "_text_");
146    private static final List<String> _READ_ONLY_FIELDTYPES = Arrays.asList("string", "plong", "text_general");
147    
148    private static final int __SOLR_STRING_NB_BYTES_LIMIT = 32766;
149    
150    /** The service manager. */
151    protected ServiceManager _manager;
152    /** The ametys object resolver. */
153    protected AmetysObjectResolver _resolver;
154    /** The schema definition provider extension point. */
155    protected SchemaDefinitionProviderExtensionPoint _schemaDefProviderEP;
156    /** The schema helper. */
157    protected SchemaHelper _schemaHelper;
158    /** Solr Ametys contents indexer */
159    protected SolrContentIndexer _solrContentIndexer;
160    /** Solr workflow indexer. */
161    protected SolrWorkflowIndexer _solrWorkflowIndexer;
162    /** Solr resource indexer. */
163    protected SolrResourceIndexer _solrResourceIndexer;
164    /** Solr trash element indexer. */
165    protected SolrTrashElementIndexer _solrTrashElementIndexer;
166    
167    /** The Solr client provider */
168    protected SolrClientProvider _solrClientProvider;
169    
170    /** The solr core prefix. */
171    protected String _solrCorePrefix;
172    /** The Ametys internal URL used by Solr to query Ametys */
173    protected String _ametysInternalUrl;
174    
175    /** The workspace selector. */
176    protected WorkspaceSelector _workspaceSelector;
177    /** The JCR repository */
178    protected JackrabbitRepository _repository;
179    /** The helper for read access */
180    protected ReadAccessHelper _readAccessHelper;
181    /** The thread indexer helper */
182    protected ThreadIndexerHelper _threadIndexerHelper;
183    
184    /** The avalon context */
185    protected Context _context;
186    /** Cocoon Context */
187    protected org.apache.cocoon.environment.Context _cocoonContext;
188
189    /** The additional data indexer extension point */
190    protected AdditionalDataIndexerExtensionPoint _additionalDataIndexerEP;
191
192    /**
193     * Truncates (if needed) the given string in order to be indexed without <i>immense term</i> error by Solr.
194     * Only the {@value #__SOLR_STRING_NB_BYTES_LIMIT} first bytes of the String will be kept.
195     * @param value The string value to index
196     * @param logger The logger for logging in WARN level in case the given string is too long and will be truncated. Can be null if you do not want to log.
197     * @param documentId The id of the document being indexed. Can be null if you do not want to log.
198     * @param fieldName The name of the field being indexed. Can be null if you do not want to log.
199     * @return The given string value, or its truncation if it is too long (greater than {@value #__SOLR_STRING_NB_BYTES_LIMIT} bytes)
200     */
201    public static String truncateUtf8StringValue(String value, Logger logger, String documentId , String fieldName)
202    {
203        if (value.length() * 4 <= __SOLR_STRING_NB_BYTES_LIMIT)
204        {
205            // With UTF-8, a character is encoded using 1, 2, 3 or 4 bytes, so (value.length() <= value.getBytes().length <= 4 * value.length())
206            // As a result, value.getBytes().length <= limit
207            return value;
208        }
209        
210        // There is a doubt, the string may need to be truncated (or not)
211        byte[] valueBytes = value.getBytes(StandardCharsets.UTF_8);
212        int bytesLength = valueBytes.length;
213        if (bytesLength <= __SOLR_STRING_NB_BYTES_LIMIT)
214        {
215            return value;
216        }
217        
218        if (ObjectUtils.allNotNull(logger, documentId, fieldName))
219        {
220            logger.warn("The string value for document '{}' and field name '{}' is longer ({}) than the max bytes length {}. It will be truncated to prevent Solr error, but you should consider verifying why this string is so long.", documentId, fieldName, bytesLength, __SOLR_STRING_NB_BYTES_LIMIT);
221        }
222        
223        // Need a truncation (inspired by https://stackoverflow.com/questions/119328/how-do-i-truncate-a-java-string-to-fit-in-a-given-number-of-bytes-once-utf-8-en#answer-35148974)
224        CharBuffer charBuffer = CharBuffer.allocate(__SOLR_STRING_NB_BYTES_LIMIT);
225        CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder()
226                                                       .onMalformedInput(CodingErrorAction.IGNORE);
227        decoder.decode(ByteBuffer.wrap(valueBytes, 0, __SOLR_STRING_NB_BYTES_LIMIT), charBuffer, true);
228        decoder.flush(charBuffer);
229        return new String(charBuffer.array(), 0, charBuffer.position());
230    }
231    
232    @Override
233    public void service(ServiceManager serviceManager) throws ServiceException
234    {
235        _manager = serviceManager;
236        _resolver = (AmetysObjectResolver) serviceManager.lookup(AmetysObjectResolver.ROLE);
237        _schemaDefProviderEP = (SchemaDefinitionProviderExtensionPoint) serviceManager.lookup(SchemaDefinitionProviderExtensionPoint.ROLE);
238        _schemaHelper = (SchemaHelper) serviceManager.lookup(SchemaHelper.ROLE);
239        _solrContentIndexer = (SolrContentIndexer) serviceManager.lookup(SolrContentIndexer.ROLE);
240        _solrWorkflowIndexer = (SolrWorkflowIndexer) serviceManager.lookup(SolrWorkflowIndexer.ROLE);
241        _solrResourceIndexer = (SolrResourceIndexer) serviceManager.lookup(SolrResourceIndexer.ROLE);
242        _solrTrashElementIndexer = (SolrTrashElementIndexer) serviceManager.lookup(SolrTrashElementIndexer.ROLE);
243        _solrClientProvider = (SolrClientProvider) serviceManager.lookup(SolrClientProvider.ROLE);
244        _workspaceSelector = (WorkspaceSelector) serviceManager.lookup(WorkspaceSelector.ROLE);
245        _readAccessHelper = (ReadAccessHelper) serviceManager.lookup(ReadAccessHelper.ROLE);
246        _repository = (JackrabbitRepository) serviceManager.lookup(AbstractRepository.ROLE);
247        _threadIndexerHelper = (ThreadIndexerHelper) serviceManager.lookup(ThreadIndexerHelper.ROLE);
248        _additionalDataIndexerEP = (AdditionalDataIndexerExtensionPoint) serviceManager.lookup(AdditionalDataIndexerExtensionPoint.ROLE);
249    }
250    
251    @Override
252    public void initialize() throws Exception
253    {
254        Config config = Config.getInstance();
255        _solrCorePrefix = config.getValue("cms.solr.core.prefix");
256        
257        _ametysInternalUrl = config.getValue("cms.solr.core.ametys.internal.url");
258        if (StringUtils.isBlank(_ametysInternalUrl))
259        {
260            // fallback to CMS URL as internal URL is an optional parameter
261            _ametysInternalUrl = config.getValue("cms.url");
262        }
263    }
264    
265    @Override
266    public void contextualize(Context context) throws ContextException
267    {
268        _context = context;
269        _cocoonContext = (org.apache.cocoon.environment.Context) context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT);
270    }
271    
272    /**
273     * Gets the 'autocommit' Solr client
274     * @param workspaceName The name of the workspace
275     * @return the Solr client
276     */
277    protected SolrClient _getAutoCommitSolrClient(String workspaceName)
278    {
279        return _solrClientProvider.getUpdateClient(workspaceName, true);
280    }
281    
282    /**
283     * Gets the 'no autocommit' Solr client
284     * @param workspaceName The name of the workspace
285     * @return the Solr client
286     */
287    protected SolrClient _getNoAutoCommitSolrClient(String workspaceName)
288    {
289        return _solrClientProvider.getUpdateClient(workspaceName, false);
290    }
291    
292    // for admin operations
293    private SolrClient _defaultSolrClient()
294    {
295        return _solrClientProvider.getUpdateClient(RepositoryConstants.DEFAULT_WORKSPACE);
296    }
297    
298    /**
299     * Get the names of the Solr cores.
300     * @return The names of the Solr cores.
301     * @throws IOException If an I/O error occurs.
302     * @throws SolrServerException If a Solr error occurs.
303     */
304    public Set<String> getCoreNames() throws IOException, SolrServerException
305    {
306        Set<String> coreNames = new HashSet<>();
307        
308        getLogger().debug("Getting core list.");
309        
310        CoreAdminRequest req = new CoreAdminRequest();
311        req.setAction(CoreAdminAction.STATUS);
312        
313        NamedList<NamedList<Object>> status = req.process(_defaultSolrClient()).getCoreStatus();
314        for (Map.Entry<String, NamedList<Object>> core : status)
315        {
316            String fullName = (String) core.getValue().get("name");
317            if (fullName.startsWith(_solrCorePrefix))
318            {
319                coreNames.add(fullName.substring(_solrCorePrefix.length()));
320            }
321        }
322        
323        return coreNames;
324    }
325    
326    /**
327     * Get the names of the Solr cores.
328     * @return The names of the Solr cores.
329     * @throws IOException If an I/O error occurs.
330     * @throws SolrServerException If a Solr error occurs.
331     */
332    protected Set<String> getRealCoreNames() throws IOException, SolrServerException
333    {
334        Set<String> coreNames = new HashSet<>();
335        
336        getLogger().debug("Getting core list.");
337        
338        CoreAdminResponse response = new CoreAdminRequest().process(_defaultSolrClient());
339        
340        NamedList<NamedList<Object>> status = response.getCoreStatus();
341        for (Map.Entry<String, NamedList<Object>> core : status)
342        {
343            String fullName = (String) core.getValue().get("name");
344            if (fullName.startsWith(_solrCorePrefix))
345            {
346                coreNames.add(fullName.substring(_solrCorePrefix.length()));
347            }
348        }
349        
350        return coreNames;
351    }
352    
353    /**
354     * Create a Solr core.
355     * @param name The name of the core to create.
356     * @throws IOException If an I/O error occurs.
357     * @throws SolrServerException If a Solr error occurs.
358     */
359    public void createCore(String name) throws IOException, SolrServerException
360    {
361        Set<String> cores = getCoreNames();
362        if (!cores.contains(name))
363        {
364            String fullName = _solrCorePrefix + name;
365            String configsetName = _CONFIGSET_NAME_PREFIX + (_solrCorePrefix.endsWith("-") ? _solrCorePrefix.substring(0, _solrCorePrefix.length() - 1) : _solrCorePrefix);
366            _createConfigset(configsetName);
367            
368            getLogger().info("Creating core '{}' (full name: '{}').", name, fullName);
369            
370            Create createRequest = new Create() {
371                @Override
372                public SolrParams getParams()
373                {
374                    ModifiableSolrParams params = new ModifiableSolrParams();
375                    
376                    params.set(CoreAdminParams.ACTION, CoreAdminAction.CREATE.toString());
377                    params.set(CoreAdminParams.NAME, fullName);
378                    params.set(CoreAdminParams.CONFIGSET, configsetName);
379                    params.set("property.ametys.url", _ametysInternalUrl);
380                    
381                    return params;
382                }
383            };
384            
385            NamedList<?> results = createRequest.process(_defaultSolrClient()).getResponse();
386            
387            NamedList<?> error = (NamedList<?>) results.get("error");
388            if (error != null)
389            {
390                throw new IOException("Error creating the core: " + error.get("msg"));
391            }
392        }
393        else
394        {
395            if (getLogger().isDebugEnabled())
396            {
397                getLogger().debug("Core '" + name + "' already exists, skipping it.");
398            }
399        }
400    }
401    
402    /**
403     * Updates the ametys.url property of the Solr cores.
404     */
405    public void updateAmetysUrlCoreProperty()
406    {
407        Set<String> coreNames;
408        try
409        {
410            coreNames = getCoreNames();
411        }
412        catch (SolrServerException | IOException e)
413        {
414            getLogger().error("Cannot get Solr core names. As a result, the internal Ametys URL could not be updated on Solr server.", e);
415            return;
416        }
417        
418        for (String coreName : coreNames)
419        {
420            String collection = _solrClientProvider.getCollectionName(coreName);
421            SolrResponseBase response;
422            try
423            {
424                response = new UpdateCorePropertyRequest("ametys.url", _ametysInternalUrl).process(_getAutoCommitSolrClient(coreName), collection);
425            }
426            catch (SolrServerException | IOException e)
427            {
428                getLogger().error("'core.properties' file updating for workspace '{}' did not succeed as expected.", coreName, e);
429                continue;
430            }
431            
432            NamedList<Object> responseParams = response.getResponse();
433            if ("ok".equals(responseParams.get("result")))
434            {
435                Boolean valueChanged = responseParams.getBooleanArg("valueChanged");
436                if (valueChanged)
437                {
438                    getLogger().info("'core.properties' file updated with the up-to-date Ametys URL for workspace '{}'", coreName);
439                }
440                else
441                {
442                    getLogger().info("'core.properties' file already has the up-to-date Ametys URL for workspace '{}', it was not modified.", coreName);
443                }
444            }
445            else
446            {
447                getLogger().error("'core.properties' file updating for workspace '{}' did not succeed as expected.", coreName);
448            }
449        }
450    }
451    
452    private void _createConfigset(String name) throws IOException, SolrServerException
453    {
454        getLogger().info("Creating (if necessary) configset '{}'", name);
455        
456        // This request handler will check if configset exists. If not it will be created.
457        CoreAdminRequest request = new CoreAdminRequest() {
458            @Override
459            public SolrParams getParams()
460            {
461                ModifiableSolrParams params = new ModifiableSolrParams();
462                params.set(CoreAdminParams.ACTION, "createConfigset");
463                params.set(CoreAdminParams.NAME, name);
464                return params;
465            }
466        };
467        
468        NamedList<?> results = request.process(_defaultSolrClient()).getResponse();
469        
470        NamedList<?> error = (NamedList<?>) results.get("error");
471        if (error != null)
472        {
473            throw new IOException("Error creating the core: " + error.get("msg"));
474        }
475    }
476    
477    /**
478     * Delete a Solr core.
479     * @param name The name of the core to delete.
480     * @throws IOException If an I/O error occurs.
481     * @throws SolrServerException If a Solr error occurs.
482     */
483    public void deleteCore(String name) throws IOException, SolrServerException
484    {
485        String fullName = _solrCorePrefix + name;
486        
487        getLogger().info("Deleting core '{}' (full name: '{}').", name, fullName);
488        
489        CoreAdminResponse response = CoreAdminRequest.unloadCore(fullName, true, true, _defaultSolrClient());
490        NamedList<?> results = response.getResponse();
491        
492        NamedList<?> error = (NamedList<?>) results.get("error");
493        if (error != null)
494        {
495            throw new IOException("Error deleting core" + name + ": " + error.get("msg"));
496        }
497    }
498    
499    /**
500     * Send the schema.
501     * @throws IOException If a communication error occurs.
502     * @throws SolrServerException If a solr error occurs.
503     */
504    public void sendSchema() throws IOException, SolrServerException
505    {
506        getLogger().info("Computing and sending the schema to the solr server.");
507        
508        String workspaceName = _workspaceSelector.getWorkspace();
509        String collection = _solrClientProvider.getCollectionName(workspaceName);
510        SolrClient solrClient = _getAutoCommitSolrClient(workspaceName);
511        
512//        SchemaRepresentation staticSchema = _schemaHelper.getStaticSchema();
513        SchemaRepresentation staticSchema = _schemaHelper.getSchema("resource://org/ametys/cms/search/solr/schema/schema.xml");
514        
515        // TODO Clear the schema except fields marked ametysReadOnly="true".
516        
517        // Clear the current schema.
518        clearSchema(solrClient, collection);
519        
520        SchemaRequest schemaRequest = new SchemaRequest();
521        SchemaResponse schemaResponse = schemaRequest.process(solrClient, collection);
522        
523        // The cleared schema contains only the basic fields which can't be deleted.
524        SchemaRepresentation clearedSchema = schemaResponse.getSchemaRepresentation();
525        SchemaFields schemaFields = new SchemaFields(clearedSchema);
526        
527        getLogger().debug("Schema after clear: \n{}", schemaFields.toString());
528        
529        // Add the static schema types and fields.
530        List<SchemaRequest.Update> updates = new ArrayList<>();
531        
532        // Set "add field" definitions from the static schema to the update list.
533        addStaticSchemaUpdates(updates, staticSchema, schemaFields);
534        
535        getLogger().debug("Temporary schema after static add: \n{}", schemaFields.toString());
536        
537        // Set "add field" definitions from the static schema to the update list.
538        addCustomUpdates(updates, schemaFields);
539        
540        updates.sort(new SchemaRequestComparator());
541        
542        SchemaRequest.MultiUpdate multiUpdate = new SchemaRequest.MultiUpdate(updates);
543        SchemaResponse.UpdateResponse updateResponse = multiUpdate.process(solrClient, collection);
544        
545        getLogger().debug("Send schema response: {}", updateResponse.toString());
546        Object errors = updateResponse.getResponse().get("errors");
547        if (errors != null && errors instanceof List && !((List) errors).isEmpty())
548        {
549            String msg = "An error occured with the sent schema to Solr, it contains errors:\n" + errors.toString();
550            throw new SolrServerException(msg);
551        }
552    
553        getLogger().info("Schema sent to the solr server.");
554        
555        reloadCores();
556    }
557    
558    /**
559     * Compute the list of {@link Update} directives from the static schema.
560     * @param updates The list of {@link Update} directives to fill.
561     * @param staticSchema The static schema representation.
562     * @param schemaFields The current schema fields, used to track the existing fields (to be filled).
563     */
564    protected void addStaticSchemaUpdates(List<SchemaRequest.Update> updates, SchemaRepresentation staticSchema, SchemaFields schemaFields)
565    {
566        List<FieldTypeDefinition> fieldTypes = staticSchema.getFieldTypes();
567        for (FieldTypeDefinition fieldType : fieldTypes)
568        {
569            String name = (String) fieldType.getAttributes().get("name");
570            if (!schemaFields.hasFieldType(name))
571            {
572                updates.add(new SchemaRequest.AddFieldType(fieldType));
573                schemaFields.addFieldType(name);
574            }
575        }
576        for (Map<String, Object> field : staticSchema.getFields())
577        {
578            String name = (String) field.get("name");
579            if (!schemaFields.hasField(name))
580            {
581                updates.add(new SchemaRequest.AddField(field));
582                schemaFields.addField(name);
583            }
584        }
585        for (Map<String, Object> field : staticSchema.getDynamicFields())
586        {
587            String name = (String) field.get("name");
588            if (!schemaFields.hasDynamicField(name))
589            {
590                updates.add(new SchemaRequest.AddDynamicField(field));
591                schemaFields.addDynamicField(name);
592            }
593        }
594        for (Map<String, Object> field : staticSchema.getCopyFields())
595        {
596            String source = (String) field.get("source");
597            String dest = (String) field.get("dest");
598            if (!schemaFields.hasCopyField(source, dest))
599            {
600                updates.add(new SchemaRequest.AddCopyField(source, Arrays.asList(dest)));
601                schemaFields.addCopyField(source, dest);
602            }
603        }
604    }
605    
606    /**
607     * Compute the list of custom {@link Update} directives.
608     * @param updates The list of {@link Update} directives to fill.
609     * @param schemaFields The current schema fields, used to track the existing fields (to be filled).
610     */
611    protected void addCustomUpdates(List<SchemaRequest.Update> updates, SchemaFields schemaFields)
612    {
613        // Add all our property-managed fields.
614        for (String providerId : _schemaDefProviderEP.getExtensionsIds())
615        {
616            SchemaDefinitionProvider definitionProvider = _schemaDefProviderEP.getExtension(providerId);
617            
618            for (SchemaDefinition definition : definitionProvider.getDefinitions())
619            {
620                if (!definition.exists(schemaFields))
621                {
622                    SchemaRequest.Update update = definition.getSchemaUpdate();
623                    if (update != null)
624                    {
625                        updates.add(update);
626                    }
627                }
628            }
629        }
630    }
631    
632    /**
633     * Delete all the fields of the existing schema in the given collection.
634     * @param solrClient The Solr client
635     * @param collection The collection.
636     * @throws IOException If a communication error occurs.
637     * @throws SolrServerException If a solr error occurs.
638     */
639    protected void clearSchema(SolrClient solrClient, String collection) throws IOException, SolrServerException
640    {
641        try
642        {
643            getLogger().info("Clearing the existing schema on the solr server.");
644            
645            SchemaRequest schemaRequest = new SchemaRequest();
646            SchemaResponse schemaResponse = schemaRequest.process(solrClient, collection);
647            
648            SchemaRepresentation schema = schemaResponse.getSchemaRepresentation();
649            
650            List<SchemaRequest.Update> deletions = new ArrayList<>();
651            
652            // First the copy fields, then dynamic and simple fields, and field types in the end.
653            for (Map<String, Object> field : schema.getCopyFields())
654            {
655                String source = (String) field.get("source");
656                String dest = (String) field.get("dest");
657                deletions.add(new SchemaRequest.DeleteCopyField(source, Arrays.asList(dest)));
658            }
659            for (Map<String, Object> field : schema.getDynamicFields())
660            {
661                String name = (String) field.get("name");
662                deletions.add(new SchemaRequest.DeleteDynamicField(name));
663            }
664            for (Map<String, Object> field : schema.getFields())
665            {
666                String name = (String) field.get("name");
667                if (!_READ_ONLY_FIELDS.contains(name))
668                {
669                    deletions.add(new SchemaRequest.DeleteField(name));
670                }
671            }
672            for (FieldTypeDefinition fieldType : schema.getFieldTypes())
673            {
674                String name = (String) fieldType.getAttributes().get("name");
675                if (!_READ_ONLY_FIELDTYPES.contains(name))
676                {
677                    deletions.add(new SchemaRequest.DeleteFieldType(name));
678                }
679            }
680            
681            SchemaRequest.MultiUpdate multiUpdate = new SchemaRequest.MultiUpdate(deletions);
682            SchemaResponse.UpdateResponse updateResponse = multiUpdate.process(solrClient, collection);
683            
684            Object errors = updateResponse.getResponse().get("errors");
685            if (errors != null && errors instanceof List && !((List) errors).isEmpty())
686            {
687                String msg = "An error occured when clearing Solr schema, it contains errors:\n" + errors.toString();
688                throw new SolrServerException(msg);
689            }
690            else
691            {
692                getLogger().debug("Clear schema response: {}", updateResponse.toString());
693            }
694            
695            getLogger().info("Solr schema cleared.");
696        }
697        catch (SolrServerException | IOException e)
698        {
699            getLogger().error("Error clearing schema in collection " + collection, e);
700            throw e;
701        }
702    }
703    
704    /**
705     * Reload the solr cores.
706     * @throws IOException If a communication error occurs.
707     * @throws SolrServerException If a solr error occurs.
708     */
709    protected void reloadCores() throws IOException, SolrServerException
710    {
711        getLogger().info("Reloading solr cores.");
712        
713        for (String coreName : getCoreNames())
714        {
715            String fullName = _solrCorePrefix + coreName;
716            
717            CoreAdminResponse reloadResponse = CoreAdminRequest.reloadCore(fullName, _defaultSolrClient());
718            
719            getLogger().debug("Reload core response: {}", reloadResponse.toString());
720        }
721        
722        getLogger().info("All cores reloaded.");
723    }
724    
725    /**
726     * Reloads the ACL Solr cache for all users
727     * @throws IOException If an I/O error occurs.
728     * @throws SolrServerException If a Solr error occurs.
729     * @throws RepositoryException If a repository exception occurs when retrieving workspaces.
730     */
731    public void reloadAclCache() throws IOException, SolrServerException, RepositoryException
732    {
733        String[] workspaceNames = _repository.getWorkspaces();
734        for (String workspaceName : workspaceNames)
735        {
736            reloadAclCache(workspaceName);
737        }
738    }
739    
740    /**
741     * Reloads the ACL Solr cache for all users
742     * @param workspaceName The workspace name
743     * @throws IOException If an I/O error occurs.
744     * @throws SolrServerException If a Solr error occurs.
745     */
746    public void reloadAclCache(String workspaceName) throws IOException, SolrServerException
747    {
748        reloadAclCache(workspaceName, false);
749    }
750    
751    /**
752     * Reloads the ACL Solr cache for all users
753     * @param workspaceName The workspace name
754     * @param checkIfNecessary true to check if the reload is necessary for each segment (i.e. reload only the segments not already in cache)
755     * @throws IOException If an I/O error occurs.
756     * @throws SolrServerException If a Solr error occurs.
757     */
758    public void reloadAclCache(String workspaceName, boolean checkIfNecessary) throws IOException, SolrServerException
759    {
760        getLogger().info("Reloading read ACL Solr cache for workspace '{}' (checkIfNecessary={})", workspaceName, checkIfNecessary);
761        
762        String collection = _solrClientProvider.getCollectionName(workspaceName);
763        SolrResponseBase responseBase = new ReloadAclCacheRequest(checkIfNecessary).process(_getAutoCommitSolrClient(workspaceName), collection);
764        NamedList<Object> responseObj = responseBase.getResponse();
765        
766        if ("ok".equals(responseObj.get("result")))
767        {
768            getLogger().info("Read-ACL Solr cache reloaded for workspace '{}' (checkIfNecessary={})", workspaceName, checkIfNecessary);
769        }
770        else
771        {
772            Object error = responseObj.get("error");
773            getLogger().error("The reloading of Read-ACL Solr Cache for workspace '{}' (checkIfNecessary={}) did not succeed as expected.\n Error code is the following: {}", workspaceName, checkIfNecessary, error);
774        }
775    }
776    
777    /**
778     * Updates the ACL Solr cache for some {@link AmetysObject}s for all workspaces.
779     * @param objects the {@link AmetysObject}s to update.
780     * @throws IOException If an I/O error occurs.
781     * @throws SolrServerException If a Solr error occurs.
782     * @throws RepositoryException If a repository exception occurs when retrieving workspaces.
783     */
784    public void updateAclCache(Iterable<? extends AmetysObject> objects) throws IOException, SolrServerException, RepositoryException
785    {
786        String[] workspaceNames = _repository.getWorkspaces();
787        for (String workspaceName : workspaceNames)
788        {
789            updateAclCache(objects, workspaceName);
790        }
791    }
792    
793    /**
794     * Updates the ACL Solr cache for some {@link AmetysObject}s.
795     * @param objects the {@link AmetysObject}s to update.
796     * @param workspaceName The workspace name
797     * @throws IOException If an I/O error occurs.
798     * @throws SolrServerException If a Solr error occurs.
799     * @throws RepositoryException If a repository exception occurs when retrieving workspaces.
800     */
801    public void updateAclCache(Iterable<? extends AmetysObject> objects, String workspaceName) throws IOException, SolrServerException, RepositoryException
802    {
803        Map<String, Map<String, Object>> solrParams = new HashMap<>();
804        
805        for (AmetysObject object : objects)
806        {
807            AllowedUsers allowedUsers = _readAccessHelper.allowedUsers(object);
808            
809            solrParams.put(object.getId(), Map.of("anonymous", allowedUsers.isAnonymousAllowed(),
810                                                  "anyConnectedUser", allowedUsers.isAnyConnectedUserAllowed(),
811                                                  "allowedUsers", allowedUsers.getAllowedUsers().stream().map(UserIdentity::userIdentityToString).collect(Collectors.toList()),
812                                                  "deniedUsers", allowedUsers.getDeniedUsers().stream().map(UserIdentity::userIdentityToString).collect(Collectors.toList()),
813                                                  "allowedGroups", allowedUsers.getAllowedGroups().stream().map(GroupIdentity::groupIdentityToString).collect(Collectors.toList()),
814                                                  "deniedGroups", allowedUsers.getDeniedGroups().stream().map(GroupIdentity::groupIdentityToString).collect(Collectors.toList())));
815        }
816        
817        String collection = _solrClientProvider.getCollectionName(workspaceName);
818        SolrResponse response = new UpdateAclCacheRequest(solrParams).process(_getAutoCommitSolrClient(workspaceName), collection);
819        NamedList<Object> responseObj = response.getResponse();
820        
821        if ("ok".equals(responseObj.get("result")))
822        {
823            getLogger().info("Read-ACL Solr cache updated for workspace '{}' and objects {}", workspaceName, solrParams.keySet());
824        }
825        else
826        {
827            @SuppressWarnings("unchecked")
828            List<String> unHandledObjects = (List<String>) responseObj.get("unhandled-objects");
829            getLogger().info("The updating of Read-ACL Solr Cache for workspace '{}' did not succeed as expected.\n Following objects have not been updated: {}", workspaceName, unHandledObjects);
830        }
831    }
832    
833    /**
834     * Index all the contents in a given workspace.
835     * @param workspaceName the workspace where to index
836     * @param indexAttachments to index content attachments
837     * @param solrClient The solr client to use
838     * @return The indexation result as a Map.
839     * @throws Exception if an error occurs while indexing.
840     */
841    public Map<String, Object> indexAllContents(String workspaceName, boolean indexAttachments, SolrClient solrClient) throws Exception
842    {
843        return indexAllContents(workspaceName, indexAttachments, solrClient, ProgressionTrackerFactory.createSimpleProgressionTracker("Index all contents", getLogger()));
844    }
845    
846    /**
847     * Index all the contents in a given workspace.
848     * @param workspaceName the workspace where to index
849     * @param indexAttachments to index content attachments
850     * @param solrClient The solr client to use
851     * @param progressionTracker The progression of the indexation
852     * @return The indexation result as a Map.
853     * @throws Exception if an error occurs while indexing.
854     */
855    public Map<String, Object> indexAllContents(String workspaceName, boolean indexAttachments, SolrClient solrClient, SimpleProgressionTracker progressionTracker) throws Exception
856    {
857        Request request = ContextHelper.getRequest(_context);
858        
859        // Retrieve the current workspace.
860        String currentWsp = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
861        
862        try
863        {
864            // Force the workspace.
865            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, workspaceName);
866            
867            getLogger().info("Starting the indexation of all contents for workspace {}", workspaceName);
868            
869            long start = System.currentTimeMillis();
870            
871            // Delete all contents
872            unindexAllContents(workspaceName, indexAttachments, solrClient);
873
874            String query = ContentQueryHelper.getContentXPathQuery(null);
875            AmetysObjectIterable<Content> contents = _resolver.query(query);
876            
877            IndexationResult result = doIndexContents(contents, workspaceName, indexAttachments, solrClient, progressionTracker);
878            
879            long end = System.currentTimeMillis();
880            
881            if (!result.hasErrors())
882            {
883                getLogger().info("{} contents indexed without error in {} milliseconds.", result.successCount(), end - start);
884            }
885            else
886            {
887                getLogger().info("Content indexation ended, the process took {} milliseconds. {} contents were not indexed successfully, please review the error logs above for more details.", end - start, result.errorCount());
888            }
889            
890            Map<String, Object> results = new HashMap<>();
891            results.put("successCount", result.successCount());
892            if (result.hasErrors())
893            {
894                results.put("errorCount", result.errorCount());
895            }
896            
897            return results;
898        }
899        catch (Exception e)
900        {
901            String error = String.format("Failed to index all contents in workspace %s", workspaceName);
902            getLogger().error(error, e);
903            throw new IndexingException(error, e);
904        }
905        finally
906        {
907            // Restore context
908            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWsp);
909        }
910    }
911    
912    /**
913     * Unindex all content documents.
914     * @param workspaceName The workspace name
915     * @param unindexAttachments also unindex content attachments
916     * @param solrClient The solr client to use
917     * @throws Exception if an error occurs while unindexing.
918     */
919    protected void unindexAllContents(String workspaceName, boolean unindexAttachments, SolrClient solrClient) throws Exception
920    {
921        String collection = _solrClientProvider.getCollectionName(workspaceName);
922        
923        Query contents = new DocumentTypeQuery(SolrFieldNames.TYPE_CONTENT);
924        Query query;
925        if (unindexAttachments)
926        {
927            Query contentResourceAttachments = new DocumentTypeQuery(SolrFieldNames.TYPE_CONTENT_ATTACHMENT_RESOURCE);
928            Query contentResourceAttributes = new DocumentTypeQuery(SolrFieldNames.TYPE_CONTENT_ATTRIBUTE_RESOURCE);
929            query = new OrQuery(contentResourceAttachments, contentResourceAttributes, contents);
930        }
931        else
932        {
933            query = contents;
934        }
935        
936        Query additionalQuery = unindexAllAdditionnalData(AdditionalDataIndexer.TYPE_CONTENT);
937        if (additionalQuery != null)
938        {
939            query = new OrQuery(query, additionalQuery);
940        }
941        
942        solrClient.deleteByQuery(collection, query.build());
943    }
944    
945    /**
946     * Add or update a content into Solr index on all workspaces and commit
947     * @param contentId The id of the content to index
948     * @param indexAttachments to index content attachments
949     * @throws Exception if an error occurs while indexing.
950     */
951    public void indexContent(String contentId, boolean indexAttachments) throws Exception
952    {
953        String[] workspaceNames = _repository.getWorkspaces();
954        for (String workspaceName : workspaceNames)
955        {
956            SolrClient solrClient = _getAutoCommitSolrClient(workspaceName);
957            indexContent(contentId, workspaceName, indexAttachments, solrClient);
958        }
959    }
960    
961    /**
962     * Add or update a content into Solr index
963     * @param contentId The id of the content to index
964     * @param workspaceName the workspace where to index
965     * @param indexAttachments to index content attachments
966     * @throws Exception if an error occurs while indexing.
967     */
968    public void indexContent(String contentId, String workspaceName, boolean indexAttachments) throws Exception
969    {
970        SolrClient solrClient = _getAutoCommitSolrClient(workspaceName);
971        indexContent(contentId, workspaceName, indexAttachments, solrClient);
972    }
973    
974    /**
975     * Add or update a content into Solr index
976     * @param contentId The id of the content to index
977     * @param workspaceName the workspace where to index
978     * @param indexAttachments to index content attachments
979     * @param solrClient The solr client to use
980     * @throws Exception if an error occurs while indexing.
981     */
982    public void indexContent(String contentId, String workspaceName, boolean indexAttachments, SolrClient solrClient) throws Exception
983    {
984        Request request = ContextHelper.getRequest(_context);
985        
986        // Retrieve the current workspace.
987        String currentWsp = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
988        
989        try
990        {
991            // Force the workspace.
992            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, workspaceName);
993            
994            if (_resolver.hasAmetysObjectForId(contentId))
995            {
996                Content content = _resolver.resolveById(contentId);
997                _doIndexContent(content, workspaceName, indexAttachments, solrClient);
998            }
999        }
1000        finally
1001        {
1002            // Restore context
1003            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWsp);
1004        }
1005    }
1006    
1007    private void _doIndexContent(Content content, String workspaceName, boolean indexAttachments, SolrClient solrClient) throws IndexingException
1008    {
1009        try
1010        {
1011            long time_0 = System.currentTimeMillis();
1012            
1013            getLogger().debug("Indexing content {} into Solr for workspace {}", content.getId(), workspaceName);
1014            
1015            deleteRepeaterDocs(content.getId(), workspaceName, solrClient);
1016            doIndexContent(content, workspaceName, solrClient);
1017            doIndexContentWorkflow(content, workspaceName, solrClient);
1018            if (indexAttachments)
1019            {
1020                indexContentAttachments(content.getRootAttachments(), content, solrClient);
1021            }
1022            
1023            getLogger().debug("Successfully indexed content {} in Solr in {} ms", content.getId(), System.currentTimeMillis() - time_0);
1024        }
1025        catch (Exception e)
1026        {
1027            String error = String.format("Failed to index content %s in workspace %s", content.getId(), workspaceName);
1028            getLogger().error(error, e);
1029            throw new IndexingException(error, e);
1030        }
1031    }
1032    
1033    /**
1034     * Send a collection of contents for indexation in the solr server on all workspaces and commit
1035     * @param contents the collection of contents to index.
1036     * @return the indexation result.
1037     * @throws Exception if an error occurs while indexing.
1038     */
1039    public IndexationResult indexContents(Iterable<Content> contents) throws Exception
1040    {
1041        return indexContents(contents, ProgressionTrackerFactory.createContainerProgressionTracker("Index contents", getLogger()));
1042    }
1043    
1044    
1045    /**
1046     * Send a collection of contents for indexation in the solr server on all workspaces and commit
1047     * @param contents the collection of contents to index.
1048     * @param progressionTracker The progression of the indexation
1049     * @return the indexation result.
1050     * @throws Exception if an error occurs while indexing.
1051     */
1052    public IndexationResult indexContents(Iterable<Content> contents, ContainerProgressionTracker progressionTracker) throws Exception
1053    {
1054        IndexationResult result = new IndexationResult(0, 0);
1055        
1056        String[] workspaceNames = _repository.getWorkspaces();
1057        
1058        for (String workspaceName : workspaceNames)
1059        {
1060            progressionTracker.addSimpleStep(workspaceName, new I18nizableText("plugin.cms", "PLUGINS_CMS_SCHEDULER_GLOBAL_INDEXATION_CONTENT_WORKSPACE_STEP_LABEL", List.of(workspaceName)));
1061        }
1062        
1063        for (String workspaceName : workspaceNames)
1064        {
1065            SolrClient solrClient = _getAutoCommitSolrClient(workspaceName);
1066            IndexationResult wResult = indexContents(contents, workspaceName, true, solrClient, progressionTracker.getStep(workspaceName));
1067            
1068            result = new IndexationResult(result.successCount() + wResult.successCount(), result.errorCount() + wResult.errorCount());
1069        }
1070        
1071        return result;
1072    }
1073    
1074    /**
1075     * Send a collection of contents for indexation in the solr server.
1076     * @param contents the collection of contents to index.
1077     * @param workspaceName the workspace where to index
1078     * @param indexAttachments to index content attachments
1079     * @param solrClient The solr client to use
1080     * @return the indexation result.
1081     * @throws Exception if an error occurs while indexing.
1082     */
1083    public IndexationResult indexContents(Iterable<Content> contents, String workspaceName, boolean indexAttachments, SolrClient solrClient) throws Exception
1084    {
1085        return indexContents(contents, workspaceName, indexAttachments, solrClient, ProgressionTrackerFactory.createSimpleProgressionTracker("Index contents for workspace " + workspaceName, getLogger()));
1086    }
1087    
1088    /**
1089     * Send a collection of contents for indexation in the solr server.
1090     * @param contents the collection of contents to index.
1091     * @param workspaceName the workspace where to index
1092     * @param indexAttachments to index content attachments
1093     * @param solrClient The solr client to use
1094     * @param progressionTracker The progression of the indexation
1095     * @return the indexation result.
1096     * @throws Exception if an error occurs while indexing.
1097     */
1098    public IndexationResult indexContents(Iterable<Content> contents, String workspaceName, boolean indexAttachments, SolrClient solrClient, SimpleProgressionTracker progressionTracker) throws Exception
1099    {
1100        Request request = ContextHelper.getRequest(_context);
1101        
1102        // Retrieve the current workspace.
1103        String currentWsp = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
1104        
1105        try
1106        {
1107            // Force the workspace.
1108            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, workspaceName);
1109            
1110            getLogger().info("Starting indexation of several contents for workspace {}", workspaceName);
1111            
1112            long start = System.currentTimeMillis();
1113            
1114            List<Content> contentsInWorkspace = StreamSupport.stream(contents.spliterator(), false)
1115                .map(Content::getId)
1116                .map(this::_resolveSilently)
1117                .filter(Objects::nonNull)
1118                .collect(Collectors.toList());
1119            
1120            IndexationResult result = doIndexContents(contentsInWorkspace, workspaceName, indexAttachments, solrClient, progressionTracker);
1121            
1122            long end = System.currentTimeMillis();
1123            
1124            if (!result.hasErrors())
1125            {
1126                getLogger().info("{} contents indexed without error in {} milliseconds.", result.successCount(), end - start);
1127            }
1128            else
1129            {
1130                getLogger().info("Content indexation ended, the process took {} milliseconds. {} contents were not indexed successfully, please review the error logs above for more details.", end - start, result.errorCount());
1131            }
1132            
1133            return result;
1134        }
1135        catch (Exception e)
1136        {
1137            String error = String.format("Failed to index several contents in workspace %s", workspaceName);
1138            getLogger().error(error, e);
1139            throw new IndexingException(error, e);
1140        }
1141        finally
1142        {
1143            // Restore context
1144            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWsp);
1145        }
1146    }
1147    
1148    private Content _resolveSilently(String contentId)
1149    {
1150        try
1151        {
1152            return _resolver.resolveById(contentId);
1153        }
1154        catch (UnknownAmetysObjectException e)
1155        {
1156            return null;
1157        }
1158    }
1159    
1160    /**
1161     * Send some contents for indexation in the solr server.
1162     * @param contents the contents to index.
1163     * @param workspaceName The workspace name
1164     * @param indexAttachments to index content attachments
1165     * @param solrClient The solr client to use
1166     * @param progressionTracker The progression of the indexation
1167     * @return the indexation result.
1168     * @throws Exception if an error occurs committing the results.
1169     */
1170    protected IndexationResult doIndexContents(Iterable<Content> contents, String workspaceName, boolean indexAttachments, SolrClient solrClient, SimpleProgressionTracker progressionTracker) throws Exception
1171    {
1172        int numberOfContents = IterableUtils.size(contents);
1173        progressionTracker.setSize(numberOfContents);
1174        
1175        // Add callable for each content to index
1176        List<Future<Void>> tasks = new ArrayList<>();
1177        for (Content content : contents)
1178        {
1179            tasks.add(_threadIndexerHelper.submitCallable(new ContentIndexerCallable(content, workspaceName, indexAttachments, solrClient, progressionTracker)));
1180        }
1181        
1182        // Now that everything is submitted, we can iterate and wait for result
1183        return IndexationResult.fromTasks(tasks, getLogger());
1184    }
1185    
1186    /**
1187     * Update the value of a specific system property in a ametys object document.
1188     * @param ao The ametys object to update.
1189     * @param propertyId The system property ID.
1190     * @param workspaceName The workspace name
1191     * @throws Exception if an error occurs while indexing.
1192     */
1193    public void updateSystemProperty(IndexableAmetysObject ao, String propertyId, String workspaceName) throws Exception
1194    {
1195        SolrClient solrClient = _getAutoCommitSolrClient(workspaceName);
1196        updateSystemProperty(ao, propertyId, workspaceName, solrClient);
1197    }
1198    
1199    /**
1200     * Update the value of a specific system property in a ametys object document.
1201     * @param ao The ametys object to update.
1202     * @param propertyId The system property ID.
1203     * @param workspaceName The workspace name
1204     * @param solrClient The solr client to use
1205     * @throws Exception if an error occurs while indexing.
1206     */
1207    public void updateSystemProperty(IndexableAmetysObject ao, String propertyId, String workspaceName, SolrClient solrClient) throws Exception
1208    {
1209        getLogger().debug("Updating the system property '{}' for ametys object {} into Solr.", propertyId, ao);
1210        
1211        SolrInputDocument document = new SolrInputDocument();
1212        boolean hasUpdate = populatePartialSystemProperty(ao, propertyId, document);
1213        
1214        if (!hasUpdate)
1215        {
1216            getLogger().debug("Did not index '{}' system property for ametys object {} in Solr because no update to apply.", propertyId, ao);
1217            return;
1218        }
1219        
1220        int status = _pushSolrDocument(document, workspaceName, solrClient);
1221        if (status != 0)
1222        {
1223            throw new IOException("Indexing of system property '" + propertyId + "': got status code '" + status + "'.");
1224        }
1225        
1226        getLogger().debug("Succesfully indexed '{}' system property for ametys object {} in Solr.", propertyId, ao);
1227    }
1228    
1229    /**
1230     * Populate a Solr input document by adding fields for a single system property.
1231     * @param ao The ametys object to index
1232     * @param propertyId The system property ID.
1233     * @param document The solr document
1234     * @return true if there are partial update to apply
1235     * @throws Exception if an error occurred
1236     */
1237    public boolean populatePartialSystemProperty(IndexableAmetysObject ao, String propertyId, SolrInputDocument document) throws Exception
1238    {
1239        Optional<SystemProperty> property = ao.getSystemPropertyExtensionPoint()
1240            .filter(s -> s.hasExtension(propertyId))
1241            .map(s -> s.getExtension(propertyId));
1242        
1243        if (property.isEmpty())
1244        {
1245            throw new IllegalStateException("The system property '" + propertyId + "' can't be indexed for ametys object with id '" + ao.getId() + "' as it does not exist.");
1246        }
1247        
1248        return populatePartialProperty(ao, property.get(), document);
1249    }
1250    
1251    /**
1252     * Populate a Solr input document by adding fields for a single property.
1253     * @param ao The ametys object to index
1254     * @param propertyId The property ID.
1255     * @param document The solr document
1256     * @return true if there are partial update to apply
1257     * @throws Exception if an error occurred
1258     */
1259    public boolean populatePartialProperty(IndexableAmetysObject ao, String propertyId, SolrInputDocument document) throws Exception
1260    {
1261        ModelItem modelItem = ao.getDefinition(propertyId);
1262        if (modelItem instanceof Property property)
1263        {
1264            return populatePartialProperty(ao, property, document);
1265        }
1266        
1267        return false;
1268    }
1269    
1270    /**
1271     * Populate a Solr input document by adding fields for a single property.
1272     * @param ao The ametys object to index
1273     * @param property The property to index.
1274     * @param document The solr document
1275     * @return true if there are partial update to apply
1276     * @throws Exception if an error occurred
1277     */
1278    @SuppressWarnings("unchecked")
1279    public boolean populatePartialProperty(IndexableAmetysObject ao, Property property, SolrInputDocument document) throws Exception
1280    {
1281        SolrInputDocument tempDocument = new SolrInputDocument();
1282        
1283        if (property instanceof IndexationAwareElementDefinition indexationAwareElementDefinition)
1284        {
1285            indexationAwareElementDefinition.indexValue(tempDocument, ao, CMSDataContext.newInstance());
1286        }
1287        
1288        if (tempDocument.isEmpty())
1289        {
1290            // 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)
1291            return false;
1292        }
1293        
1294        // Copy the indexed values as partial updates.
1295        for (String fieldName : tempDocument.getFieldNames())
1296        {
1297            Collection<Object> fieldValues = tempDocument.getFieldValues(fieldName);
1298            
1299            Map<String, Object> partialUpdate = new HashMap<>();
1300            partialUpdate.put("set", fieldValues);
1301            document.addField(fieldName, partialUpdate);
1302        }
1303        
1304        document.addField("id", ao.getId());
1305        
1306        return true;
1307    }
1308    
1309    /**
1310     * Update the value of a specific property in a content document.
1311     * @param content The content to update.
1312     * @param propertyId The system property ID.
1313     * @param workspaceName The workspace name
1314     * @throws Exception if an error occurs while indexing.
1315     */
1316    public void updateProperty(Content content, String propertyId, String workspaceName) throws Exception
1317    {
1318        SolrClient solrClient = _getAutoCommitSolrClient(workspaceName);
1319        updateProperty(content, propertyId, workspaceName, solrClient);
1320    }
1321    
1322    /**
1323     * Update the value of a specific property in a content document.
1324     * @param content The content to update.
1325     * @param propertyId The system property ID.
1326     * @param workspaceName The workspace name
1327     * @param solrClient The solr client to use
1328     * @throws Exception if an error occurs while indexing.
1329     */
1330    public void updateProperty(Content content, String propertyId, String workspaceName, SolrClient solrClient) throws Exception
1331    {
1332        getLogger().debug("Updating the property '{}' for content {} into Solr.", propertyId, content);
1333        
1334        SolrInputDocument document = new SolrInputDocument();
1335        boolean hasUpdate = populatePartialProperty(content, propertyId, document);
1336        
1337        if (!hasUpdate)
1338        {
1339            getLogger().debug("Did not index '{}' property for content {} in Solr because no update to apply.", propertyId, content);
1340            return;
1341        }
1342        
1343        int status = _pushSolrDocument(document, workspaceName, solrClient);
1344        if (status != 0)
1345        {
1346            throw new IOException("Indexing of property '" + propertyId + "': got status code '" + status + "'.");
1347        }
1348        
1349        getLogger().debug("Succesfully indexed '{}' property for content {} in Solr.", propertyId, content);
1350    }
1351    
1352    private int _pushSolrDocument(SolrInputDocument document, String workspaceName, SolrClient solrClient) throws Exception
1353    {
1354        String collection = _solrClientProvider.getCollectionName(workspaceName);
1355        UpdateResponse solrResponse = solrClient.add(collection, document);
1356        return solrResponse.getStatus();
1357    }
1358    
1359    /**
1360     * Remove a content from Solr index for all workspaces and commit
1361     * @param contentId The id of content to unindex
1362     * @param unindexAttachments also unindex content attachments
1363     * @throws Exception if an error occurs while indexing.
1364     */
1365    public void unindexContent(String contentId, boolean unindexAttachments) throws Exception
1366    {
1367        String[] workspaceNames = _repository.getWorkspaces();
1368        for (String workspaceName : workspaceNames)
1369        {
1370            unindexContent(contentId, workspaceName, unindexAttachments);
1371        }
1372    }
1373    
1374    /**
1375     * Remove a content from Solr index
1376     * @param contentId The id of content to unindex
1377     * @param workspaceName The workspace where to work in
1378     * @param unindexAttachments also unindex content attachments
1379     * @throws Exception if an error occurs while indexing.
1380     */
1381    public void unindexContent(String contentId, String workspaceName, boolean unindexAttachments) throws Exception
1382    {
1383        SolrClient solrClient = _getAutoCommitSolrClient(workspaceName);
1384        unindexContent(contentId, workspaceName, unindexAttachments, solrClient);
1385    }
1386    
1387    /**
1388     * Remove a content from Solr index
1389     * @param contentId The id of content to unindex
1390     * @param workspaceName The workspace where to work in
1391     * @param unindexAttachments also unindex content attachments
1392     * @param solrClient The solr client to use
1393     * @throws Exception if an error occurs while indexing.
1394     */
1395    public void unindexContent(String contentId, String workspaceName, boolean unindexAttachments, SolrClient solrClient) throws Exception
1396    {
1397        Request request = ContextHelper.getRequest(_context);
1398        
1399        // Retrieve the current workspace.
1400        String currentWsp = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
1401        
1402        try
1403        {
1404            // Force the workspace.
1405            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, workspaceName);
1406            
1407            getLogger().debug("Unindexing content {} from Solr for workspace {}", contentId, workspaceName);
1408            
1409            deleteRepeaterDocs(contentId, workspaceName, solrClient);
1410            doUnindexDocument(contentId, workspaceName, solrClient);
1411            if (unindexAttachments)
1412            {
1413                doUnindexContentAttachments(contentId, workspaceName, solrClient);
1414            }
1415            _solrWorkflowIndexer.unindexAmetysObjectWorkflow(contentId, workspaceName, solrClient);
1416            unindexAdditionnalData("content", contentId, workspaceName, solrClient);
1417            
1418            getLogger().debug("Succesfully deleted content {} from Solr.", contentId);
1419        }
1420        catch (Exception e)
1421        {
1422            String error = String.format("Failed to unindex content %s in workspace %s", contentId, workspaceName);
1423            getLogger().error(error, e);
1424            throw new IndexingException(error, e);
1425        }
1426        finally
1427        {
1428            // Restore workspace
1429            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWsp);
1430        }
1431    }
1432    
1433    /**
1434     * Remove a content from Solr index for all workspaces and commit
1435     * @param contentIds The id of content to unindex
1436     * @throws Exception if an error occurs while indexing.
1437     */
1438    public void unindexContents(Collection<String> contentIds) throws Exception
1439    {
1440        String[] workspaceNames = _repository.getWorkspaces();
1441        for (String workspaceName : workspaceNames)
1442        {
1443            unindexContents(contentIds, workspaceName);
1444        }
1445    }
1446    
1447    /**
1448     * Remove a content from Solr index
1449     * @param contentIds The id of content to unindex
1450     * @param workspaceName The workspace where to work in
1451     * @throws Exception if an error occurs while indexing.
1452     */
1453    public void unindexContents(Collection<String> contentIds, String workspaceName) throws Exception
1454    {
1455        Request request = ContextHelper.getRequest(_context);
1456        
1457        // Retrieve the current workspace.
1458        String currentWsp = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
1459        
1460        try
1461        {
1462            // Force the workspace.
1463            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, workspaceName);
1464            SolrClient solrClient = _getAutoCommitSolrClient(workspaceName);
1465            
1466            getLogger().debug("Unindexing several contents from Solr.");
1467            
1468            for (String contentId : contentIds)
1469            {
1470                deleteRepeaterDocs(contentId, workspaceName, solrClient);
1471                doUnindexDocument(contentId, workspaceName, solrClient);
1472                _solrWorkflowIndexer.unindexAmetysObjectWorkflow(contentId, workspaceName, solrClient);
1473                unindexAdditionnalData("content", contentId, workspaceName, solrClient);
1474            }
1475            
1476            getLogger().debug("Succesfully unindexed content from Solr.");
1477        }
1478        catch (Exception e)
1479        {
1480            String error = String.format("Failed to unindex several contents in workspace %s", workspaceName);
1481            getLogger().error(error, e);
1482            throw new IndexingException(error, e);
1483        }
1484        finally
1485        {
1486            // Restore workspace
1487            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWsp);
1488        }
1489    }
1490    
1491    /**
1492     * Add or update a content into Solr index
1493     * @param content The content to index
1494     * @param workspaceName The workspace where to index
1495     * @param solrClient The solr client to use
1496     * @throws Exception if an error occurs while indexing.
1497     */
1498    protected void doIndexContent(Content content, String workspaceName, SolrClient solrClient) throws Exception
1499    {
1500        long time_0 = System.currentTimeMillis();
1501        
1502        SolrInputDocument document = new SolrInputDocument();
1503        List<SolrInputDocument> additionalDocuments = _solrContentIndexer.indexContent(content, document);
1504        
1505        long time_1 = System.currentTimeMillis();
1506        getLogger().debug("Populate indexing fields for content {} for workspace {} in {} ms", content.getId(), workspaceName, time_1 - time_0);
1507        
1508        indexAclInitValues(content, document);
1509        
1510        long time_2 = System.currentTimeMillis();
1511        getLogger().debug("Populate ACL for content {} for workspace {} in {} ms", content.getId(), workspaceName, time_2 - time_1);
1512        
1513        List<SolrInputDocument> documents = new ArrayList<>(additionalDocuments);
1514        documents.add(document);
1515        
1516        UpdateResponse solrResponse = solrClient.add(_solrClientProvider.getCollectionName(workspaceName), documents);
1517        int status = solrResponse.getStatus();
1518        
1519        long time_3 = System.currentTimeMillis();
1520        getLogger().debug("Update document for content {} for workspace {} in {} ms", content.getId(), workspaceName, time_3 - time_2);
1521        
1522        if (status != 0)
1523        {
1524            throw new IOException("Content indexation: got status code '" + status + "'.");
1525        }
1526    }
1527    
1528    /**
1529     * Indexes read-ACl initial values for object
1530     * @param ametysObject The object
1531     * @param document The Solr document
1532     */
1533    public void indexAclInitValues(AmetysObject ametysObject, SolrInputDocument document)
1534    {
1535        // Indexation of AmetysObject property
1536        document.addField(SolrFieldNames.IS_AMETYS_OBJECT, true);
1537        
1538        // Indexation of initValues for AllowedUsers
1539        AllowedUsers allowedUsers = _readAccessHelper.allowedUsers(ametysObject);
1540        document.addField(SolrFieldNames.ACL_INIT_VALUE_ANONYMOUS, allowedUsers.isAnonymousAllowed());
1541        document.addField(SolrFieldNames.ACL_INIT_VALUE_ANYCONNECTED, allowedUsers.isAnyConnectedUserAllowed());
1542        _addField(document, SolrFieldNames.ACL_INIT_VALUE_ALLOWED_USERS, allowedUsers.getAllowedUsers(), UserIdentity::userIdentityToString);
1543        _addField(document, SolrFieldNames.ACL_INIT_VALUE_DENIED_USERS, allowedUsers.getDeniedUsers(), UserIdentity::userIdentityToString);
1544        _addField(document, SolrFieldNames.ACL_INIT_VALUE_ALLOWED_GROUPS, allowedUsers.getAllowedGroups(), GroupIdentity::groupIdentityToString);
1545        _addField(document, SolrFieldNames.ACL_INIT_VALUE_DENIED_GROUPS, allowedUsers.getDeniedGroups(), GroupIdentity::groupIdentityToString);
1546    }
1547    
1548    private <T> void _addField(SolrInputDocument document, String fieldName, Set<T> values, Function<T, String> stringifier)
1549    {
1550        if (values == null)
1551        {
1552            return;
1553        }
1554        
1555        for (T value : values)
1556        {
1557            document.addField(fieldName, stringifier.apply(value));
1558        }
1559    }
1560    
1561    /**
1562     * Index the whole workflow of a content.
1563     * @param content The content.
1564     * @param workspaceName The workspace name
1565     * @param solrClient The solr client to use
1566     * @throws Exception if an error occurs while indexing.
1567     */
1568    protected void doIndexContentWorkflow(Content content, String workspaceName, SolrClient solrClient) throws Exception
1569    {
1570        if (content instanceof WorkflowAwareContent)
1571        {
1572            _solrWorkflowIndexer.indexAmetysObjectWorkflow((WorkflowAwareContent) content, workspaceName, solrClient);
1573        }
1574    }
1575    
1576    /**
1577     * Index content attachments as new entries in the idnex
1578     * @param collection the collection of attachments
1579     * @param content the content whose attachments will be indexed
1580     * @throws Exception if something goes wrong when indexing the attachments of the content
1581     */
1582    public void indexContentAttachments(ResourceCollection collection, Content content) throws Exception
1583    {
1584        Request request = ContextHelper.getRequest(_context);
1585        String workspaceName = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
1586        SolrClient solrClient = _getAutoCommitSolrClient(workspaceName);
1587        indexContentAttachments(collection, content, solrClient);
1588    }
1589    
1590    /**
1591     * Index content attachments as new entries in the idnex
1592     * @param collection the collection of attachments
1593     * @param content the content whose attachments will be indexed
1594     * @param solrClient The solr client to use
1595     * @throws Exception if something goes wrong when indexing the attachments of the content
1596     */
1597    public void indexContentAttachments(ResourceCollection collection, Content content, SolrClient solrClient) throws Exception
1598    {
1599        if (collection == null)
1600        {
1601            return;
1602        }
1603        
1604        try (AmetysObjectIterable<AmetysObject> children = collection.getChildren())
1605        {
1606            for (AmetysObject object : children)
1607            {
1608                if (object instanceof ResourceCollection)
1609                {
1610                    indexContentAttachments((ResourceCollection) object, content, solrClient);
1611                }
1612                else if (object instanceof Resource)
1613                {
1614                    Resource resource = (Resource) object;
1615                    indexContentAttachment(resource, content, solrClient);
1616                }
1617            }
1618        }
1619    }
1620    
1621    /**
1622     * Index a content attachment
1623     * @param resource the content attachment as a {@link Resource}
1624     * @param content the content whose attachment is going to be indexed
1625     * @throws Exception if something goes wrong when processing the indexation of the content attachment
1626     */
1627    public void indexContentAttachment(Resource resource, Content content) throws Exception
1628    {
1629        Request request = ContextHelper.getRequest(_context);
1630        String workspaceName = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
1631        SolrClient solrClient = _getAutoCommitSolrClient(workspaceName);
1632        indexContentAttachment(resource, content, solrClient);
1633    }
1634    
1635    /**
1636     * Index a content attachment
1637     * @param resource the content attachment as a {@link Resource}
1638     * @param content the content whose attachment is going to be indexed
1639     * @param solrClient The solr client to use
1640     * @throws Exception if something goes wrong when processing the indexation of the content attachment
1641     */
1642    public void indexContentAttachment(Resource resource, Content content, SolrClient solrClient) throws Exception
1643    {
1644        SolrInputDocument document = new SolrInputDocument();
1645        
1646        // Prepare resource doc
1647        List<SolrInputDocument> additionalDocuments = _indexContentAttachment(resource, document, content);
1648        List<SolrInputDocument> documents = new ArrayList<>(additionalDocuments);
1649        documents.add(document);
1650        
1651        // Indexation of the document
1652        _indexResourceDocument(resource, documents, solrClient);
1653    }
1654    
1655    private List<SolrInputDocument> _indexContentAttachment(Resource resource, SolrInputDocument document, Content content) throws Exception
1656    {
1657        String language = content.getLanguage();
1658        
1659        List<SolrInputDocument> additionalDocuments = _solrResourceIndexer.indexResource(resource, document, SolrFieldNames.TYPE_CONTENT_ATTACHMENT_RESOURCE, language);
1660        
1661        // Need the id of the content for unindexing attachment during the unindexing of the content
1662        document.addField(SolrFieldNames.ATTACHMENT_CONTENT_ID, content.getId());
1663        
1664        return additionalDocuments;
1665    }
1666    
1667    /**
1668     * Index a populated solr input document of type Resource.
1669     * @param resource the resource from which the input document is created
1670     * @param documents the input documents
1671     * @param solrClient The solr client to use
1672     * @throws SolrServerException if there is an error on the server
1673     * @throws IOException if there is a communication error with the server
1674     */
1675    protected void _indexResourceDocument(Resource resource, List<SolrInputDocument> documents, SolrClient solrClient) throws SolrServerException, IOException
1676    {
1677        String resourceId = resource.getId();
1678        
1679        // Retrieve appropriate collection name
1680        Request request = ContextHelper.getRequest(_context);
1681        String workspaceName = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
1682        String collectionName = _solrClientProvider.getCollectionName(workspaceName);
1683        
1684        // Add document
1685        UpdateResponse solrResponse = solrClient.add(collectionName, documents);
1686        int status = solrResponse.getStatus();
1687        
1688        if (status != 0)
1689        {
1690            throw new IOException("Ametys resource indexing - Expecting status code of '0' in the Solr response but got : '" + status + "'. Resource id : " + resourceId);
1691        }
1692        
1693        getLogger().debug("Successful resource indexing. Resource identifier : {}", resourceId);
1694    }
1695    
1696    /**
1697     * Delete repeater documents of a specified content.
1698     * @param workspaceName The workspace name
1699     * @param contentId the content ID.
1700     * @param solrClient The solr client to use
1701     * @throws Exception if an error occurs while indexing.
1702     */
1703    protected void deleteRepeaterDocs(String contentId, String workspaceName, SolrClient solrClient) throws Exception
1704    {
1705        long time_0 = System.currentTimeMillis();
1706        
1707        // _documentType:repeater AND id:content\://xxx/*
1708        StringBuilder query = new StringBuilder();
1709        query.append(SolrFieldNames.DOCUMENT_TYPE).append(':').append(SolrFieldNames.TYPE_REPEATER)
1710             .append(" AND id:").append(ClientUtils.escapeQueryChars(contentId)).append("/*");
1711        
1712        solrClient.deleteByQuery(_solrClientProvider.getCollectionName(workspaceName), query.toString());
1713        
1714        getLogger().debug("Successfully delete repeaters documents for content {} in {} ms", contentId, System.currentTimeMillis() - time_0);
1715    }
1716    
1717    /**
1718     * Index all the resources in a given workspace.
1719     * @param workspaceName The workspace where to index
1720     * @param solrClient The solr client to use
1721     * @param progressionTracker The progression of the indexation
1722     * @return The indexation result as a Map.
1723     * @throws Exception if an error occurs while indexing.
1724     */
1725    public Map<String, Object> indexAllResources(String workspaceName, SolrClient solrClient, SimpleProgressionTracker progressionTracker) throws Exception
1726    {
1727        Request request = ContextHelper.getRequest(_context);
1728        
1729        // Retrieve the current workspace.
1730        String currentWsp = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
1731        
1732        try
1733        {
1734            // Force the workspace.
1735            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, workspaceName);
1736            
1737            getLogger().info("Starting the indexation of all resources for workspace {}", workspaceName);
1738            
1739            long start = System.currentTimeMillis();
1740            
1741            // Delete all resources
1742            _unindexAllResources(workspaceName, solrClient);
1743            
1744            Map<String, Object> results = new HashMap<>();
1745            
1746            try
1747            {
1748                TraversableAmetysObject resourceRoot = _resolver.resolveByPath(RepositoryConstants.NAMESPACE_PREFIX + ":resources");
1749                AmetysObjectIterable<Resource> resources = resourceRoot.getChildren();
1750                
1751                IndexationResult result = doIndexResources(resources, SolrFieldNames.TYPE_RESOURCE, resourceRoot, workspaceName, solrClient, progressionTracker);
1752                
1753                long end = System.currentTimeMillis();
1754                
1755                if (!result.hasErrors())
1756                {
1757                    getLogger().info("{} resources indexed without error in {} milliseconds.", result.successCount(), end - start);
1758                }
1759                else
1760                {
1761                    getLogger().info("Resource indexation ended, the process took {} milliseconds. {} resources were not indexed successfully, please review the error logs above for more details.", end - start, result.errorCount());
1762                }
1763                
1764                results.put("successCount", result.successCount());
1765                if (result.hasErrors())
1766                {
1767                    results.put("errorCount", result.errorCount());
1768                }
1769            }
1770            catch (UnknownAmetysObjectException e)
1771            {
1772                getLogger().info("There is no root for resources in current workspace.");
1773                progressionTracker.setSize(0);
1774            }
1775            
1776            return results;
1777        }
1778        catch (Exception e)
1779        {
1780            String error = String.format("Failed to index all resources in workspace %s", workspaceName);
1781            getLogger().error(error, e);
1782            throw new IndexingException(error, e);
1783        }
1784        finally
1785        {
1786            // Restore context
1787            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWsp);
1788        }
1789    }
1790    
1791    /**
1792     * Send a collection of contents for indexation in the solr server.
1793     * @param resources the collection of contents to index.
1794     * @param documentType The document type of the resource
1795     * @param workspaceName The workspace where to index
1796     * @return the indexation result.
1797     * @throws Exception if an error occurs while indexing.
1798     */
1799    public IndexationResult indexResources(Iterable<AmetysObject> resources, String documentType, String workspaceName) throws Exception
1800    {
1801        SolrClient solrClient = _getAutoCommitSolrClient(workspaceName);
1802        return indexResources(resources, documentType, null, workspaceName, solrClient);
1803    }
1804    
1805    /**
1806     * Send a collection of contents for indexation in the solr server.
1807     * @param resources the collection of contents to index.
1808     * @param documentType The document type of the resource
1809     * @param resourceRoot The resource root, can be null. In that case, it will be computed for each resource.
1810     * @param workspaceName The workspace where to index
1811     * @param solrClient The solr client to use
1812     * @return the indexation result.
1813     * @throws Exception if an error occurs while indexing.
1814     */
1815    public IndexationResult indexResources(Iterable<? extends AmetysObject> resources, String documentType, TraversableAmetysObject resourceRoot, String workspaceName, SolrClient solrClient) throws Exception
1816    {
1817        return indexResources(resources, documentType, resourceRoot, workspaceName, solrClient, ProgressionTrackerFactory.createSimpleProgressionTracker("Index resources", getLogger()));
1818    }
1819    
1820    /**
1821     * Send a collection of contents for indexation in the solr server.
1822     * @param resources the collection of contents to index.
1823     * @param documentType The document type of the resource
1824     * @param resourceRoot The resource root, can be null. In that case, it will be computed for each resource.
1825     * @param workspaceName The workspace where to index
1826     * @param solrClient The solr client to use
1827     * @param progressionTracker The progression of the indexation
1828     * @return the indexation result.
1829     * @throws Exception if an error occurs while indexing.
1830     */
1831    public IndexationResult indexResources(Iterable<? extends AmetysObject> resources, String documentType, TraversableAmetysObject resourceRoot, String workspaceName, SolrClient solrClient, SimpleProgressionTracker progressionTracker) throws Exception
1832    {
1833        Request request = ContextHelper.getRequest(_context);
1834        
1835        // Retrieve the current workspace.
1836        String currentWsp = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
1837        
1838        try
1839        {
1840            // Force the workspace.
1841            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, workspaceName);
1842            
1843            getLogger().info("Starting indexation of several resources for workspace {}", workspaceName);
1844            
1845            long start = System.currentTimeMillis();
1846            
1847            IndexationResult result = doIndexResources(resources, documentType, resourceRoot, workspaceName, solrClient, progressionTracker);
1848            
1849            long end = System.currentTimeMillis();
1850            
1851            if (!result.hasErrors())
1852            {
1853                getLogger().info("{} resources indexed without error in {} milliseconds.", result.successCount(), end - start);
1854            }
1855            else
1856            {
1857                getLogger().info("Resource indexation ended, the process took {} milliseconds. {} resources were not indexed successfully, please review the error logs above for more details.", end - start, result.errorCount());
1858            }
1859            
1860            return result;
1861        }
1862        catch (Exception e)
1863        {
1864            String error = String.format("Failed to index several resources in workspace %s", workspaceName);
1865            getLogger().error(error, e);
1866            throw new IndexingException(error, e);
1867        }
1868        finally
1869        {
1870            // Restore context
1871            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWsp);
1872        }
1873    }
1874    
1875    /**
1876     * Send some resources for indexation in the solr server.
1877     * @param resources the resources to index.
1878     * @param documentType The document type of the resource
1879     * @param resourceRoot The resource root, can be null. In that case, it will be computed for each resource.
1880     * @param workspaceName The workspace where to index
1881     * @param solrClient The solr client to use
1882     * @param progressionTracker The progression of the indexation
1883     * @return the indexation result.
1884     * @throws Exception if an error occurs committing the results.
1885     */
1886    protected IndexationResult doIndexResources(Iterable<? extends AmetysObject> resources, String documentType, TraversableAmetysObject resourceRoot, String workspaceName, SolrClient solrClient, SimpleProgressionTracker progressionTracker) throws Exception
1887    {
1888        try
1889        {
1890            // Add callable for each content to index
1891            List<Future<Void>> tasks = new ArrayList<>();
1892            for (AmetysObject resource : resources)
1893            {
1894                tasks.addAll(_asyncIndexExplorerItem(resource, documentType, resourceRoot, solrClient));
1895            }
1896            
1897            // Now that everything is submitted, we can iterate and wait for result
1898            return IndexationResult.fromTasks(tasks, getLogger());
1899        }
1900        finally
1901        {
1902            progressionTracker.increment();
1903        }
1904    }
1905    
1906    /**
1907     * Add or update a resource into Solr index
1908     * @param resource The resource to index
1909     * @param documentType The document type of the resource
1910     * @param workspaceName The workspace where to index
1911     * @throws Exception if an error occurs while indexing.
1912     */
1913    public void indexResource(Resource resource, String documentType, String workspaceName) throws Exception
1914    {
1915        SolrClient solrClient = _getAutoCommitSolrClient(workspaceName);
1916        doIndexResources(Collections.singleton(resource), documentType, null, workspaceName, solrClient, ProgressionTrackerFactory.createSimpleProgressionTracker("Index resource", getLogger()));
1917    }
1918    
1919    private void _unindexAllResources(String workspaceName, SolrClient solrClient) throws Exception
1920    {
1921        getLogger().debug("Unindexing all resources from Solr.");
1922        
1923        String collection = _solrClientProvider.getCollectionName(workspaceName);
1924        solrClient.deleteByQuery(collection, SolrFieldNames.DOCUMENT_TYPE + ':' + SolrFieldNames.TYPE_RESOURCE);
1925        
1926        getLogger().debug("Succesfully deleted all resource documents from Solr.");
1927    }
1928    
1929    /**
1930     * Delete all resource documents at a given path for all workspaces and commit
1931     * @param rootId The resource root ID, must not be null.
1932     * @param path The resource path relative to the given root, must start with a slash.
1933     * @throws Exception If an error occurs while unindexing.
1934     */
1935    public void unindexResourcesByPath(String rootId, String path) throws Exception
1936    {
1937        String[] workspaceNames = _repository.getWorkspaces();
1938        for (String workspaceName : workspaceNames)
1939        {
1940            unindexResourcesByPath(rootId, path, workspaceName);
1941        }
1942    }
1943    
1944    /**
1945     * Delete all resource documents at a given path.
1946     * @param rootId The resource root ID, must not be null.
1947     * @param path The resource path relative to the given root, must start with a slash.
1948     * @param workspaceName The workspace where to work in
1949     * @throws Exception If an error occurs while unindexing.
1950     */
1951    public void unindexResourcesByPath(String rootId, String path, String workspaceName) throws Exception
1952    {
1953        Request request = ContextHelper.getRequest(_context);
1954        
1955        // Retrieve the current workspace.
1956        String currentWsp = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
1957        
1958        try
1959        {
1960            // Force the workspace.
1961            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, workspaceName);
1962            
1963            getLogger().debug("Unindexing all resources at path {} in root {}", path, rootId);
1964            
1965            Query query = new ResourceLocationQuery(rootId, path);
1966            
1967            String collection = _solrClientProvider.getCollectionName(workspaceName);
1968            _getAutoCommitSolrClient(workspaceName).deleteByQuery(collection, query.build());
1969            
1970            // unindexAdditionnalDataIndexer("resources", ???, workspaceName, solrClient);
1971
1972            getLogger().debug("Succesfully deleted resource document from Solr.");
1973        }
1974        catch (Exception e)
1975        {
1976            String error = String.format("Failed to unindex resource %s in workspace %s", path, workspaceName);
1977            getLogger().error(error, e);
1978            throw new IndexingException(error, e);
1979        }
1980        finally
1981        {
1982            // Restore workspace
1983            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWsp);
1984        }
1985    }
1986    
1987    /**
1988     * Remove a resource from Solr index for all workspaces and commit
1989     * @param resourceId The id of resource to unindex
1990     * @throws Exception if an error occurs while indexing.
1991     */
1992    public void unindexResource(String resourceId) throws Exception
1993    {
1994        String[] workspaceNames = _repository.getWorkspaces();
1995        for (String workspaceName : workspaceNames)
1996        {
1997            unindexResource(resourceId, workspaceName);
1998        }
1999    }
2000    
2001    /**
2002     * Remove a resource from the Solr index.
2003     * @param resourceId The id of resource to unindex
2004     * @param workspaceName The workspace where to work in
2005     * @throws Exception if an error occurs while unindexing.
2006     */
2007    public void unindexResource(String resourceId, String workspaceName) throws Exception
2008    {
2009        Request request = ContextHelper.getRequest(_context);
2010        
2011        // Retrieve the current workspace.
2012        String currentWsp = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
2013        
2014        try
2015        {
2016            // Force the workspace.
2017            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, workspaceName);
2018            SolrClient solrClient = _getAutoCommitSolrClient(workspaceName);
2019            
2020            getLogger().debug("Unindexing resource {} from Solr.", resourceId);
2021            
2022            doUnindexDocument(resourceId, workspaceName, solrClient);
2023            unindexAdditionnalData("resource", resourceId, workspaceName, solrClient);
2024            
2025            getLogger().debug("Succesfully deleted resource {} from Solr.", resourceId);
2026        }
2027        catch (Exception e)
2028        {
2029            String error = String.format("Failed to unindex resource  %s in workspace %s", resourceId, workspaceName);
2030            getLogger().error(error, e);
2031            throw new IndexingException(error, e);
2032        }
2033        finally
2034        {
2035            // Restore workspace
2036            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWsp);
2037        }
2038    }
2039    
2040    private List<Future<Void>> _asyncIndexExplorerItem(AmetysObject node, String documentType, TraversableAmetysObject resourceRoot, SolrClient solrClient) throws Exception
2041    {
2042        List<Future<Void>> tasks = new ArrayList<>();
2043        
2044        if (node instanceof ResourceCollection resourceCollection)
2045        {
2046            try (AmetysObjectIterable<AmetysObject> children = resourceCollection.getChildren())
2047            {
2048                for (AmetysObject child : children)
2049                {
2050                    tasks.addAll(_asyncIndexExplorerItem(child, documentType, resourceRoot, solrClient));
2051                }
2052            }
2053            catch (Throwable t)
2054            {
2055                getLogger().error("Failed to index resource collection {}. Skipping it.", resourceCollection.getId(), t);
2056            }
2057        }
2058        else if (node instanceof Resource resource)
2059        {
2060            tasks.add(_threadIndexerHelper.submitCallable(new ResourceIndexerCallable(resource, _workspaceSelector.getWorkspace(), documentType, resourceRoot, solrClient)));
2061        }
2062        
2063        return tasks;
2064    }
2065    
2066    /**
2067     * Process a Solr commit operation in all workspaces.
2068     * <br>Use this only after a long operation with updates sent via {@link NoAutoCommitUpdateClient}
2069     * @throws SolrServerException if there is an error on the server
2070     * @throws IOException if there is a communication error with the server
2071     */
2072    public void commit() throws SolrServerException, IOException
2073    {
2074        String[] workspaceNames;
2075        try
2076        {
2077            workspaceNames = _repository.getWorkspaces();
2078            for (String workspaceName : workspaceNames)
2079            {
2080                SolrClient solrClient = _getNoAutoCommitSolrClient(workspaceName);
2081                commit(workspaceName, solrClient);
2082            }
2083        }
2084        catch (RepositoryException e)
2085        {
2086            throw new RuntimeException("An exception occured while retrieving JCR workspaces. Cannot commited to Solr.", e);
2087        }
2088    }
2089    
2090    /**
2091     * Process a Solr commit operation in given workspace.
2092     * @param workspaceName The workspace's name
2093     * @param solrClient The solr client to use
2094     * @throws SolrServerException if there is an error on the server
2095     * @throws IOException if there is a communication error with the server
2096     */
2097    public void commit(String workspaceName, SolrClient solrClient) throws SolrServerException, IOException
2098    {
2099        long time_0 = System.currentTimeMillis();
2100        
2101        // Commit
2102        UpdateResponse solrResponse = solrClient.commit(_solrClientProvider.getCollectionName(workspaceName));
2103        int status = solrResponse.getStatus();
2104        
2105        if (status != 0)
2106        {
2107            throw new IOException("Ametys indexing: Solr commit operation - Expecting status code of '0' in the Solr response but got : '" + status + "'.");
2108        }
2109        
2110        getLogger().debug("Successful Solr commit operation during an Ametys indexing process in {} ms", System.currentTimeMillis() - time_0);
2111    }
2112    
2113    /**
2114     * Launch a solr index optimization.
2115     * @param workspaceName The workspace's name
2116     * @param solrClient The solr client to use
2117     * @throws SolrServerException if there is an error on the server
2118     * @throws IOException if there is a communication error with the server
2119     */
2120    public void optimize(String workspaceName, SolrClient solrClient) throws SolrServerException, IOException
2121    {
2122        UpdateResponse solrResponse = solrClient.optimize(_solrClientProvider.getCollectionName(workspaceName));
2123        int status = solrResponse.getStatus();
2124        
2125        if (status != 0)
2126        {
2127            throw new IOException("Ametys indexing: Solr optimize operation - Expecting status code of '0' in the Solr response but got : '" + status + "'.");
2128        }
2129        
2130        getLogger().debug("Successful Solr optimize operation during an Ametys indexing process.");
2131    }
2132    
2133    /**
2134     * Delete all documents from the solr index.
2135     * @param workspaceName The workspace name
2136     * @param solrClient The solr client to use
2137     * @throws Exception if an error occurs while unindexing.
2138     */
2139    public void unindexAllDocuments(String workspaceName, SolrClient solrClient) throws Exception
2140    {
2141        getLogger().debug("Deleting all documents from Solr.");
2142        
2143        String collection = _solrClientProvider.getCollectionName(workspaceName);
2144        solrClient.deleteByQuery(collection, "*:*");
2145        
2146        getLogger().debug("Successfully deleted all documents from Solr.");
2147    }
2148    
2149    /**
2150     * Delete a document from the Solr server.
2151     * @param id The id of the document to delete from Solr
2152     * @param workspaceName The workspace name
2153     * @param solrClient The solr client to use
2154     * @throws Exception if an error occurs while indexing.
2155     */
2156    protected void doUnindexDocument(String id, String workspaceName, SolrClient solrClient) throws Exception
2157    {
2158        UpdateResponse solrResponse = solrClient.deleteById(_solrClientProvider.getCollectionName(workspaceName), id);
2159        int status = solrResponse.getStatus();
2160        
2161        if (status != 0)
2162        {
2163            throw new IOException("Deletion of document " + id + ": got status code '" + status + "'.");
2164        }
2165    }
2166    
2167    /**
2168     * Delete content attachments documents of a given content from the Solr server.
2169     * @param contentId The id of the content
2170     * @param workspaceName The workspace name
2171     * @param solrClient The solr client to use
2172     * @throws Exception if an error occurs while indexing.
2173     */
2174    protected void doUnindexContentAttachments(String contentId, String workspaceName, SolrClient solrClient) throws Exception
2175    {
2176        String collectionName = _solrClientProvider.getCollectionName(workspaceName);
2177        
2178        Query query = new ContentAttachmentQuery(contentId);
2179        UpdateResponse solrResponse = solrClient.deleteByQuery(collectionName, query.build());
2180        int status = solrResponse.getStatus();
2181        
2182        if (status != 0)
2183        {
2184            throw new IOException("Deletion of content attachments of content " + contentId + ": got status code '" + status + "'.");
2185        }
2186    }
2187
2188    /**
2189     * Add or update a trash element into Solr index on all workspaces and commit
2190     * @param trashElementId The id of the trash element to index
2191     * @throws Exception if an error occurs while indexing.
2192     */
2193    public void indexTrashElement(String trashElementId) throws Exception
2194    {
2195        String[] workspaceNames = _repository.getWorkspaces();
2196        for (String workspaceName : workspaceNames)
2197        {
2198            indexTrashElement(trashElementId, workspaceName);
2199        }
2200    }
2201
2202    /**
2203     * Add or update a trash element into Solr index
2204     * @param trashElementId The id of the trash element to index
2205     * @param workspaceName the workspace where to index
2206     * @throws Exception if an error occurs while indexing.
2207     */
2208    public void indexTrashElement(String trashElementId, String workspaceName) throws Exception
2209    {
2210        Request request = ContextHelper.getRequest(_context);
2211        
2212        SolrClient solrClient = _getAutoCommitSolrClient(workspaceName);
2213
2214        // Retrieve the current workspace.
2215        String currentWsp = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
2216        
2217        try
2218        {
2219            // Force the workspace.
2220            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, workspaceName);
2221            
2222            if (_resolver.hasAmetysObjectForId(trashElementId))
2223            {
2224                DefaultTrashElement trashElement = _resolver.resolveById(trashElementId);
2225                doIndexTrashElements(Collections.singleton(trashElement), workspaceName, solrClient, ProgressionTrackerFactory.createSimpleProgressionTracker("Index trash element", getLogger()));
2226            }
2227        }
2228        finally
2229        {
2230            // Restore context
2231            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWsp);
2232        }
2233    }
2234
2235    /**
2236     * Index all the trash elements in a given workspace.
2237     * @param workspaceName the workspace where to index
2238     * @param solrClient The solr client to use
2239     * @param progressionTracker The progression of the indexation
2240     * @return The indexation result as a Map.
2241     * @throws Exception if an error occurs while indexing.
2242     */
2243    public Map<String, Object> indexAllTrashElements(String workspaceName, SolrClient solrClient, SimpleProgressionTracker progressionTracker) throws Exception
2244    {
2245        Request request = ContextHelper.getRequest(_context);
2246        
2247        // Retrieve the current workspace.
2248        String currentWsp = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
2249        
2250        try
2251        {
2252            // Force the workspace.
2253            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, workspaceName);
2254            
2255            getLogger().info("Starting the indexation of all trash elements for workspace {}", workspaceName);
2256            
2257            long start = System.currentTimeMillis();
2258            
2259            // Delete all trash elements
2260            unindexAllTrashElements(workspaceName, solrClient);
2261
2262            String query = QueryHelper.getXPathQuery(null, TrashElementFactory.TRASH_ELEMENT_NODETYPE, null);
2263            AmetysObjectIterable<DefaultTrashElement> trashElements = _resolver.query(query);
2264            
2265            IndexationResult result = doIndexTrashElements(trashElements, workspaceName, solrClient, progressionTracker);
2266            
2267            long end = System.currentTimeMillis();
2268            
2269            if (!result.hasErrors())
2270            {
2271                getLogger().info("{} trash elements indexed without error in {} milliseconds.", result.successCount(), end - start);
2272            }
2273            else
2274            {
2275                getLogger().info("Trash elements indexation ended, the process took {} milliseconds. {} trash elements were not indexed successfully, please review the error logs above for more details.", end - start, result.errorCount());
2276            }
2277            
2278            Map<String, Object> results = new HashMap<>();
2279            results.put("successCount", result.successCount());
2280            if (result.hasErrors())
2281            {
2282                results.put("errorCount", result.errorCount());
2283            }
2284            
2285            return results;
2286        }
2287        catch (Exception e)
2288        {
2289            String error = String.format("Failed to index all trash elements in workspace %s", workspaceName);
2290            getLogger().error(error, e);
2291            throw new IndexingException(error, e);
2292        }
2293        finally
2294        {
2295            // Restore context
2296            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWsp);
2297        }
2298    }
2299
2300    /**
2301     * Remove a trash element from Solr index for all workspaces and commit
2302     * @param trashElementId The id of trash element to unindex
2303     * @throws Exception if an error occurs while indexing.
2304     */
2305    public void unindexTrashElement(String trashElementId) throws Exception
2306    {
2307        String[] workspaceNames = _repository.getWorkspaces();
2308        for (String workspaceName : workspaceNames)
2309        {
2310            unindexTrashElement(trashElementId, workspaceName);
2311        }
2312    }
2313
2314    /**
2315     * Remove a trash element from Solr index
2316     * @param trashElementId The id of trash element to unindex
2317     * @param workspaceName The workspace where to work in
2318     * @throws Exception if an error occurs while indexing.
2319     */
2320    public void unindexTrashElement(String trashElementId, String workspaceName) throws Exception
2321    {
2322        Request request = ContextHelper.getRequest(_context);
2323        
2324        SolrClient solrClient = _getAutoCommitSolrClient(workspaceName);
2325
2326        // Retrieve the current workspace.
2327        String currentWsp = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
2328        
2329        try
2330        {
2331            // Force the workspace.
2332            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, workspaceName);
2333            
2334            UpdateResponse solrResponse = solrClient.deleteById(_solrClientProvider.getCollectionName(workspaceName), trashElementId);
2335            int status = solrResponse.getStatus();
2336            
2337            if (status != 0)
2338            {
2339                throw new IOException("Deletion of document " + trashElementId + ": got status code '" + status + "'.");
2340            }
2341            
2342            unindexResourcesByPath("trashelement", trashElementId, workspaceName);
2343        }
2344        finally
2345        {
2346            // Restore context
2347            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWsp);
2348        }
2349    }
2350    
2351    /**
2352     * Unindex all trash elements documents.
2353     * @param workspaceName The workspace name
2354     * @param solrClient The solr client to use
2355     * @throws Exception if an error occurs while unindexing.
2356     */
2357    protected void unindexAllTrashElements(String workspaceName, SolrClient solrClient) throws Exception
2358    {
2359        getLogger().debug("Unindexing all trash elements from Solr.");
2360        
2361        String collection = _solrClientProvider.getCollectionName(workspaceName);
2362        Query query = new DocumentTypeQuery(SolrFieldNames.TYPE_TRASH_ELEMENT);
2363        
2364        Query unindexAllAdditionnalDataIndexer = unindexAllAdditionnalData("trashelement");
2365        if (unindexAllAdditionnalDataIndexer != null)
2366        {
2367            query = new OrQuery(query, unindexAllAdditionnalDataIndexer);
2368        }
2369        
2370        solrClient.deleteByQuery(collection, query.build());
2371        
2372        getLogger().debug("Succesfully deleted all trash elements documents from Solr.");
2373    }
2374    
2375    /**
2376     * Send some trash elements for indexation in the solr server.
2377     * @param trashElements the trash elements to index.
2378     * @param workspaceName The workspace name
2379     * @param solrClient The solr client to use
2380     * @param progressionTracker The progression of the indexation
2381     * @return the indexation result.
2382     * @throws Exception if an error occurs committing the results.
2383     */
2384    protected IndexationResult doIndexTrashElements(Iterable<DefaultTrashElement> trashElements, String workspaceName, SolrClient solrClient, SimpleProgressionTracker progressionTracker) throws Exception
2385    {
2386        int numberOfTrashElements = IterableUtils.size(trashElements);
2387        progressionTracker.setSize(numberOfTrashElements);
2388        
2389        // Add callable for each content to index
2390        List<Future<Void>> tasks = new ArrayList<>();
2391        for (DefaultTrashElement trashElement : trashElements)
2392        {
2393            tasks.add(_threadIndexerHelper.submitCallable(new TrashElementIndexerCallable(trashElement, workspaceName, solrClient, progressionTracker)));
2394        }
2395        
2396        // Now that everything is submitted, we can iterate and wait for result
2397        return IndexationResult.fromTasks(tasks, getLogger());
2398    }
2399    
2400//    /**
2401//     * Get the collection to use.
2402//     * @return The name of the collection to index into.
2403//     */
2404//    protected String getCollection()
2405//    {
2406//        return _solrCorePrefix + _workspaceSelector.getWorkspace();
2407//    }
2408
2409    
2410    /**
2411     * Unindex additional data indexed by additional data indexers.
2412     * @param type The type of the additional data indexer
2413     * @param ametysObjectId The id of the ametys object to unindex
2414     * @param workspaceName The workspace where to work in
2415     * @param solrClient The solr client to use
2416     * @throws Exception If an error occurs while unindexing.
2417     */
2418    protected void unindexAdditionnalData(String type, String ametysObjectId, String workspaceName, SolrClient solrClient) throws Exception
2419    {
2420        String collectionName = _solrClientProvider.getCollectionName(workspaceName);
2421        
2422        List<Query> queries = new ArrayList<>();
2423        
2424        Collection<AdditionalDataIndexer> indexers = _additionalDataIndexerEP.getIndexers(type);
2425        for (AdditionalDataIndexer indexer : indexers)
2426        {
2427            queries.addAll(indexer.getUnindexObjectQuery(ametysObjectId));
2428        }
2429        
2430        if (queries.isEmpty())
2431        {
2432            return;
2433        }
2434        
2435        Query query = new OrQuery(queries);
2436        UpdateResponse solrResponse = solrClient.deleteByQuery(collectionName, query.build());
2437        int status = solrResponse.getStatus();
2438        
2439        if (status != 0)
2440        {
2441            throw new IOException("Deletion of additionnal data of ametys object " + ametysObjectId + ": got status code '" + status + "'.");
2442        }
2443    }
2444    
2445    /**
2446     * Get the query to unindex all additional data indexed by additional data indexers for a given type.
2447     * @param type The type of the additional data indexer
2448     * @return The query to unindex all additional data indexed by additional data indexers for the given type.
2449     * @throws Exception If an error occurs while unindexing.
2450     */
2451    protected Query unindexAllAdditionnalData(String type) throws Exception
2452    {
2453        List<Query> queries = new ArrayList<>();
2454        
2455        Collection<AdditionalDataIndexer> indexers = _additionalDataIndexerEP.getIndexers(type);
2456        for (AdditionalDataIndexer indexer : indexers)
2457        {
2458            queries.add(indexer.getUnindexAllQuery(type));
2459        }
2460        
2461        List<Query> nonNullQueries = queries.stream().filter(Objects::nonNull).toList();
2462        if (nonNullQueries.isEmpty())
2463        {
2464            return null;
2465        }
2466        else
2467        {
2468            return new OrQuery(nonNullQueries);
2469        }
2470    }
2471
2472    
2473    private static final class SchemaRequestComparator implements Comparator<SchemaRequest.Update>
2474    {
2475        public int compare(SchemaRequest.Update u1, SchemaRequest.Update u2)
2476        {
2477            return Integer.compare(_getOrder(u1), _getOrder(u2));
2478        }
2479        
2480        private int _getOrder(SchemaRequest.Update update)
2481        {
2482            // Field types are needed first to define fields
2483            if (update instanceof SchemaRequest.AddFieldType)
2484            {
2485                return 1;
2486            }
2487            
2488            // Fields or dynamic fields can be defined at the same time
2489            if (update instanceof SchemaRequest.AddField || update instanceof SchemaRequest.AddDynamicField)
2490            {
2491                return 2;
2492            }
2493            
2494            // Copy fields can be based on fields or dynamic fields
2495            if (update instanceof SchemaRequest.AddCopyField)
2496            {
2497                return 3;
2498            }
2499            
2500            return 0;
2501        }
2502    }
2503    
2504    private class ContentIndexerCallable extends AbstractIndexerCallable<Content>
2505    {
2506        private boolean _indexAttachments;
2507        private SimpleProgressionTracker _tracker;
2508
2509        @SuppressWarnings("synthetic-access")
2510        public ContentIndexerCallable(Content content, String workspaceName, boolean indexAttachments, SolrClient solrClient, SimpleProgressionTracker progressionTracker)
2511        {
2512            super(content, workspaceName, solrClient, _manager, _cocoonContext, _resolver, getLogger());
2513            this._indexAttachments = indexAttachments;
2514            this._tracker = progressionTracker;
2515        }
2516
2517        @Override
2518        protected void process(Content content) throws Exception
2519        {
2520            try
2521            {
2522                doIndexContent(content, _workspaceName, _solrClient);
2523                doIndexContentWorkflow(content, _workspaceName, _solrClient);
2524                if (_indexAttachments)
2525                {
2526                    indexContentAttachments(content.getRootAttachments(), content, _solrClient);
2527                }
2528            }
2529            finally
2530            {
2531                _tracker.increment();
2532            }
2533        }
2534
2535        @Override
2536        protected String getObjectLabel()
2537        {
2538            return "content";
2539        }
2540    }
2541    
2542    private class ResourceIndexerCallable extends AbstractIndexerCallable<Resource>
2543    {
2544        private String _documentType;
2545        private TraversableAmetysObject _resourceRoot;
2546        
2547        @SuppressWarnings("synthetic-access")
2548        public ResourceIndexerCallable(Resource resource, String workspaceName, String documentType, TraversableAmetysObject resourceRoot, SolrClient solrClient)
2549        {
2550            super(resource, workspaceName, solrClient, _manager, _cocoonContext, _resolver, getLogger());
2551            this._documentType = documentType;
2552            this._resourceRoot = resourceRoot;
2553        }
2554        
2555        @Override
2556        protected void process(Resource resource) throws Exception
2557        {
2558            SolrInputDocument document = new SolrInputDocument();
2559            
2560            List<SolrInputDocument> additionalDocuments = _solrResourceIndexer.indexResource(resource, document, _documentType, _resourceRoot);
2561            List<SolrInputDocument> documents = new ArrayList<>(additionalDocuments);
2562            documents.add(document);
2563            
2564            UpdateResponse solrResponse = _solrClient.add(_solrClientProvider.getCollectionName(_workspaceName), documents);
2565            int status = solrResponse.getStatus();
2566            
2567            if (status != 0)
2568            {
2569                throw new IOException("Resource indexation: got status code '" + status + "'.");
2570            }
2571        }
2572
2573        @Override
2574        protected String getObjectLabel()
2575        {
2576            return "resource";
2577        }
2578    }
2579    
2580    private class TrashElementIndexerCallable extends AbstractIndexerCallable<DefaultTrashElement>
2581    {
2582        private SimpleProgressionTracker _tracker;
2583
2584        @SuppressWarnings("synthetic-access")
2585        public TrashElementIndexerCallable(DefaultTrashElement trashElement, String workspaceName, SolrClient solrClient, SimpleProgressionTracker progressionTracker)
2586        {
2587            super(trashElement, workspaceName, solrClient, _manager, _cocoonContext, _resolver, getLogger());
2588            this._tracker = progressionTracker;
2589        }
2590
2591        @Override
2592        protected void process(DefaultTrashElement trashElement) throws Exception
2593        {
2594            try
2595            {
2596                SolrInputDocument document = new SolrInputDocument();
2597                List<SolrInputDocument> additionalDocuments = _solrTrashElementIndexer.indexTrashElement(trashElement, document);
2598                
2599                List<SolrInputDocument> documents = new ArrayList<>(additionalDocuments);
2600                documents.add(document);
2601                
2602                UpdateResponse solrResponse = _solrClient.add(_solrClientProvider.getCollectionName(_workspaceName), documents);
2603                int status = solrResponse.getStatus();
2604                
2605                if (status != 0)
2606                {
2607                    throw new IOException("Trash element indexation: got status code '" + status + "'.");
2608                }
2609            }
2610            finally
2611            {
2612                _tracker.increment();
2613            }
2614        }
2615
2616        @Override
2617        protected String getObjectLabel()
2618        {
2619            return "trashElement";
2620        }
2621    }
2622}