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