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            getLogger().info("Read-ACL Solr cache updated for workspace '{}' and objects {}", workspaceName, solrParams.keySet());
831        }
832        else
833        {
834            @SuppressWarnings("unchecked")
835            List<String> unHandledObjects = (List<String>) responseObj.get("unhandled-objects");
836            getLogger().info("The updating of Read-ACL Solr Cache for workspace '{}' did not succeed as expected.\n Following objects have not been updated: {}", workspaceName, unHandledObjects);
837        }
838    }
839    
840    /**
841     * Index all the contents in a given workspace.
842     * @param workspaceName the workspace where to index
843     * @param indexAttachments to index content attachments
844     * @param solrClient The solr client to use
845     * @return The indexation result as a Map.
846     * @throws Exception if an error occurs while indexing.
847     */
848    public Map<String, Object> indexAllContents(String workspaceName, boolean indexAttachments, SolrClient solrClient) throws Exception
849    {
850        return indexAllContents(workspaceName, indexAttachments, solrClient, ProgressionTrackerFactory.createSimpleProgressionTracker("Index all contents", getLogger()));
851    }
852    
853    /**
854     * Index all the contents in a given workspace.
855     * @param workspaceName the workspace where to index
856     * @param indexAttachments to index content attachments
857     * @param solrClient The solr client to use
858     * @param progressionTracker The progression of the indexation
859     * @return The indexation result as a Map.
860     * @throws Exception if an error occurs while indexing.
861     */
862    public Map<String, Object> indexAllContents(String workspaceName, boolean indexAttachments, SolrClient solrClient, SimpleProgressionTracker progressionTracker) throws Exception
863    {
864        Request request = ContextHelper.getRequest(_context);
865        
866        // Retrieve the current workspace.
867        String currentWsp = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
868        
869        try
870        {
871            // Force the workspace.
872            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, workspaceName);
873            
874            getLogger().info("Starting the indexation of all contents for workspace {}", workspaceName);
875            
876            long start = System.currentTimeMillis();
877            
878            // Delete all contents
879            unindexAllContents(workspaceName, indexAttachments, solrClient);
880
881            String query = ContentQueryHelper.getContentXPathQuery(null);
882            AmetysObjectIterable<Content> contents = _resolver.query(query);
883            
884            IndexationResult result = doIndexContents(contents, workspaceName, indexAttachments, solrClient, progressionTracker);
885            
886            long end = System.currentTimeMillis();
887            
888            if (!result.hasErrors())
889            {
890                getLogger().info("{} contents indexed without error in {} milliseconds.", result.successCount(), end - start);
891            }
892            else
893            {
894                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());
895            }
896            
897            Map<String, Object> results = new HashMap<>();
898            results.put("successCount", result.successCount());
899            if (result.hasErrors())
900            {
901                results.put("errorCount", result.errorCount());
902            }
903            
904            return results;
905        }
906        catch (Exception e)
907        {
908            String error = String.format("Failed to index all contents in workspace %s", workspaceName);
909            getLogger().error(error, e);
910            throw new IndexingException(error, e);
911        }
912        finally
913        {
914            // Restore context
915            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWsp);
916        }
917    }
918    
919    /**
920     * Unindex all content documents.
921     * @param workspaceName The workspace name
922     * @param unindexAttachments also unindex content attachments
923     * @param solrClient The solr client to use
924     * @throws Exception if an error occurs while unindexing.
925     */
926    protected void unindexAllContents(String workspaceName, boolean unindexAttachments, SolrClient solrClient) throws Exception
927    {
928        String collection = _solrClientProvider.getCollectionName(workspaceName);
929        
930        Query contents = new DocumentTypeQuery(SolrFieldNames.TYPE_CONTENT);
931        Query query;
932        if (unindexAttachments)
933        {
934            Query contentResourceAttachments = new DocumentTypeQuery(SolrFieldNames.TYPE_CONTENT_ATTACHMENT_RESOURCE);
935            Query contentResourceAttributes = new DocumentTypeQuery(SolrFieldNames.TYPE_CONTENT_ATTRIBUTE_RESOURCE);
936            query = new OrQuery(contentResourceAttachments, contentResourceAttributes, contents);
937        }
938        else
939        {
940            query = contents;
941        }
942        solrClient.deleteByQuery(collection, query.build());
943    }
944    
945    /**
946     * Add or update the child contents of a {@link AmetysObjectCollection} into Solr index, for all workspaces and commit
947     * @param collectionId The id of collection
948     * @param indexAttachments to index content attachments
949     * @throws Exception if an error occurs while indexing.
950     */
951    public void indexSubcontents(String collectionId, boolean indexAttachments) throws Exception
952    {
953        String[] workspaceNames = _repository.getWorkspaces();
954        for (String workspaceName : workspaceNames)
955        {
956            indexSubcontents(collectionId, workspaceName, indexAttachments);
957        }
958    }
959    
960    /**
961     * Index the child contents of a {@link AmetysObjectCollection}
962     * @param collectionId The id of collection
963     * @param workspaceName the workspace where to index
964     * @param indexAttachments to index content attachments
965     * @throws Exception if an error occurs while unindexing.
966     */
967    public void indexSubcontents(String collectionId, String workspaceName, boolean indexAttachments) throws Exception
968    {
969        Request request = ContextHelper.getRequest(_context);
970        
971        // Retrieve the current workspace.
972        String currentWsp = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
973        
974        try
975        {
976            // Force the workspace.
977            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, workspaceName);
978            
979            if (_resolver.hasAmetysObjectForId(collectionId))
980            {
981                SolrClient solrClient = _getAutoCommitSolrClient(workspaceName);
982                AmetysObjectCollection collection = _resolver.resolveById(collectionId);
983                AmetysObjectIterable<AmetysObject> children = collection.getChildren();
984                
985                for (AmetysObject child : children)
986                {
987                    if (child instanceof Content)
988                    {
989                        Content content = (Content) child;
990                        
991                        _doIndexContent(content, workspaceName, indexAttachments, solrClient);
992                    }
993                }
994            }
995        }
996        finally
997        {
998            // Restore context
999            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWsp);
1000        }
1001    }
1002    
1003    /**
1004     * Add or update a content into Solr index on all workspaces and commit
1005     * @param contentId The id of the content to index
1006     * @param indexAttachments to index content attachments
1007     * @throws Exception if an error occurs while indexing.
1008     */
1009    public void indexContent(String contentId, boolean indexAttachments) throws Exception
1010    {
1011        String[] workspaceNames = _repository.getWorkspaces();
1012        for (String workspaceName : workspaceNames)
1013        {
1014            SolrClient solrClient = _getAutoCommitSolrClient(workspaceName);
1015            indexContent(contentId, workspaceName, indexAttachments, solrClient);
1016        }
1017    }
1018    
1019    /**
1020     * Add or update a content into Solr index
1021     * @param contentId The id of the content to index
1022     * @param workspaceName the workspace where to index
1023     * @param indexAttachments to index content attachments
1024     * @throws Exception if an error occurs while indexing.
1025     */
1026    public void indexContent(String contentId, String workspaceName, boolean indexAttachments) throws Exception
1027    {
1028        SolrClient solrClient = _getAutoCommitSolrClient(workspaceName);
1029        indexContent(contentId, workspaceName, indexAttachments, solrClient);
1030    }
1031    
1032    /**
1033     * Add or update a content into Solr index
1034     * @param contentId The id of the content to index
1035     * @param workspaceName the workspace where to index
1036     * @param indexAttachments to index content attachments
1037     * @param solrClient The solr client to use
1038     * @throws Exception if an error occurs while indexing.
1039     */
1040    public void indexContent(String contentId, String workspaceName, boolean indexAttachments, SolrClient solrClient) throws Exception
1041    {
1042        Request request = ContextHelper.getRequest(_context);
1043        
1044        // Retrieve the current workspace.
1045        String currentWsp = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
1046        
1047        try
1048        {
1049            // Force the workspace.
1050            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, workspaceName);
1051            
1052            if (_resolver.hasAmetysObjectForId(contentId))
1053            {
1054                Content content = _resolver.resolveById(contentId);
1055                _doIndexContent(content, workspaceName, indexAttachments, solrClient);
1056            }
1057        }
1058        finally
1059        {
1060            // Restore context
1061            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWsp);
1062        }
1063    }
1064    
1065    private void _doIndexContent(Content content, String workspaceName, boolean indexAttachments, SolrClient solrClient) throws IndexingException
1066    {
1067        try
1068        {
1069            long time_0 = System.currentTimeMillis();
1070            
1071            getLogger().debug("Indexing content {} into Solr for workspace {}", content.getId(), workspaceName);
1072            
1073            deleteRepeaterDocs(content.getId(), workspaceName, solrClient);
1074            doIndexContent(content, workspaceName, solrClient);
1075            doIndexContentWorkflow(content, workspaceName, solrClient);
1076            if (indexAttachments)
1077            {
1078                indexContentAttachments(content.getRootAttachments(), content, solrClient);
1079            }
1080            
1081            getLogger().debug("Successfully indexed content {} in Solr in {} ms", content.getId(), System.currentTimeMillis() - time_0);
1082        }
1083        catch (Exception e)
1084        {
1085            String error = String.format("Failed to index content %s in workspace %s", content.getId(), workspaceName);
1086            getLogger().error(error, e);
1087            throw new IndexingException(error, e);
1088        }
1089    }
1090    
1091    /**
1092     * Send a collection of contents for indexation in the solr server on all workspaces and commit
1093     * @param contents the collection of contents to index.
1094     * @return the indexation result.
1095     * @throws Exception if an error occurs while indexing.
1096     */
1097    public IndexationResult indexContents(Iterable<Content> contents) throws Exception
1098    {
1099        return indexContents(contents, ProgressionTrackerFactory.createContainerProgressionTracker("Index contents", getLogger()));
1100    }
1101    
1102    
1103    /**
1104     * Send a collection of contents for indexation in the solr server on all workspaces and commit
1105     * @param contents the collection of contents to index.
1106     * @param progressionTracker The progression of the indexation
1107     * @return the indexation result.
1108     * @throws Exception if an error occurs while indexing.
1109     */
1110    public IndexationResult indexContents(Iterable<Content> contents, ContainerProgressionTracker progressionTracker) throws Exception
1111    {
1112        IndexationResult result = new IndexationResult(0, 0);
1113        
1114        String[] workspaceNames = _repository.getWorkspaces();
1115        
1116        for (String workspaceName : workspaceNames)
1117        {
1118            progressionTracker.addSimpleStep(workspaceName, new I18nizableText("plugin.cms", "PLUGINS_CMS_SCHEDULER_GLOBAL_INDEXATION_CONTENT_WORKSPACE_STEP_LABEL", List.of(workspaceName)));
1119        }
1120        
1121        for (String workspaceName : workspaceNames)
1122        {
1123            SolrClient solrClient = _getAutoCommitSolrClient(workspaceName);
1124            IndexationResult wResult = indexContents(contents, workspaceName, true, solrClient, progressionTracker.getStep(workspaceName));
1125            
1126            result = new IndexationResult(result.successCount() + wResult.successCount(), result.errorCount() + wResult.errorCount());
1127        }
1128        
1129        return result;
1130    }
1131    
1132    /**
1133     * Send a collection of contents for indexation in the solr server.
1134     * @param contents the collection of contents to index.
1135     * @param workspaceName the workspace where to index
1136     * @param indexAttachments to index content attachments
1137     * @param solrClient The solr client to use
1138     * @return the indexation result.
1139     * @throws Exception if an error occurs while indexing.
1140     */
1141    public IndexationResult indexContents(Iterable<Content> contents, String workspaceName, boolean indexAttachments, SolrClient solrClient) throws Exception
1142    {
1143        return indexContents(contents, workspaceName, indexAttachments, solrClient, ProgressionTrackerFactory.createSimpleProgressionTracker("Index contents for workspace " + workspaceName, getLogger()));
1144    }
1145    
1146    /**
1147     * Send a collection of contents for indexation in the solr server.
1148     * @param contents the collection of contents to index.
1149     * @param workspaceName the workspace where to index
1150     * @param indexAttachments to index content attachments
1151     * @param solrClient The solr client to use
1152     * @param progressionTracker The progression of the indexation
1153     * @return the indexation result.
1154     * @throws Exception if an error occurs while indexing.
1155     */
1156    public IndexationResult indexContents(Iterable<Content> contents, String workspaceName, boolean indexAttachments, SolrClient solrClient, SimpleProgressionTracker progressionTracker) throws Exception
1157    {
1158        Request request = ContextHelper.getRequest(_context);
1159        
1160        // Retrieve the current workspace.
1161        String currentWsp = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
1162        
1163        try
1164        {
1165            // Force the workspace.
1166            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, workspaceName);
1167            
1168            getLogger().info("Starting indexation of several contents for workspace {}", workspaceName);
1169            
1170            long start = System.currentTimeMillis();
1171            
1172            List<Content> contentsInWorkspace = StreamSupport.stream(contents.spliterator(), false)
1173                .map(Content::getId)
1174                .map(this::_resolveSilently)
1175                .filter(Objects::nonNull)
1176                .collect(Collectors.toList());
1177            
1178            IndexationResult result = doIndexContents(contentsInWorkspace, workspaceName, indexAttachments, solrClient, progressionTracker);
1179            
1180            long end = System.currentTimeMillis();
1181            
1182            if (!result.hasErrors())
1183            {
1184                getLogger().info("{} contents indexed without error in {} milliseconds.", result.successCount(), end - start);
1185            }
1186            else
1187            {
1188                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());
1189            }
1190            
1191            return result;
1192        }
1193        catch (Exception e)
1194        {
1195            String error = String.format("Failed to index several contents in workspace %s", workspaceName);
1196            getLogger().error(error, e);
1197            throw new IndexingException(error, e);
1198        }
1199        finally
1200        {
1201            // Restore context
1202            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWsp);
1203        }
1204    }
1205    
1206    private Content _resolveSilently(String contentId)
1207    {
1208        try
1209        {
1210            return _resolver.resolveById(contentId);
1211        }
1212        catch (UnknownAmetysObjectException e)
1213        {
1214            return null;
1215        }
1216    }
1217    
1218    /**
1219     * Send some contents for indexation in the solr server.
1220     * @param contents the contents to index.
1221     * @param workspaceName The workspace name
1222     * @param indexAttachments to index content attachments
1223     * @param solrClient The solr client to use
1224     * @param progressionTracker The progression of the indexation
1225     * @return the indexation result.
1226     * @throws Exception if an error occurs committing the results.
1227     */
1228    protected IndexationResult doIndexContents(Iterable<Content> contents, String workspaceName, boolean indexAttachments, SolrClient solrClient, SimpleProgressionTracker progressionTracker) throws Exception
1229    {
1230        int numberOfContents = IterableUtils.size(contents);
1231        progressionTracker.setSize(numberOfContents);
1232        
1233        // Add callable for each content to index
1234        List<Future<Void>> tasks = new ArrayList<>();
1235        for (Content content : contents)
1236        {
1237            tasks.add(_threadIndexerHelper.submitCallable(new ContentIndexerCallable(content, workspaceName, indexAttachments, solrClient, progressionTracker)));
1238        }
1239        
1240        // Now that everything is submitted, we can iterate and wait for result
1241        return IndexationResult.fromTasks(tasks, getLogger());
1242    }
1243    
1244    /**
1245     * Update the value of a specific system property in a content document.
1246     * @param content The content to update.
1247     * @param propertyId The system property ID.
1248     * @param workspaceName The workspace name
1249     * @throws Exception if an error occurs while indexing.
1250     */
1251    public void updateSystemProperty(Content content, String propertyId, String workspaceName) throws Exception
1252    {
1253        SolrClient solrClient = _getAutoCommitSolrClient(workspaceName);
1254        updateSystemProperty(content, propertyId, workspaceName, solrClient);
1255    }
1256    
1257    /**
1258     * Update the value of a specific system property in a content document.
1259     * @param content The content to update.
1260     * @param propertyId The system property ID.
1261     * @param workspaceName The workspace name
1262     * @param solrClient The solr client to use
1263     * @throws Exception if an error occurs while indexing.
1264     */
1265    public void updateSystemProperty(Content content, String propertyId, String workspaceName, SolrClient solrClient) throws Exception
1266    {
1267        getLogger().debug("Updating the system property '{}' for content {} into Solr.", propertyId, content);
1268        
1269        SolrInputDocument document = new SolrInputDocument();
1270        boolean hasUpdate = _solrContentIndexer.indexPartialSystemProperty(content, propertyId, document);
1271        
1272        if (!hasUpdate)
1273        {
1274            getLogger().debug("Did not index '{}' system property for content {} in Solr because no update to apply.", propertyId, content);
1275            return;
1276        }
1277        
1278        int status = _pushSolrDocument(document, workspaceName, solrClient);
1279        if (status != 0)
1280        {
1281            throw new IOException("Indexing of system property '" + propertyId + "': got status code '" + status + "'.");
1282        }
1283        
1284        getLogger().debug("Succesfully indexed '{}' system property for content {} in Solr.", propertyId, content);
1285    }
1286    
1287    /**
1288     * Update the value of a specific property in a content document.
1289     * @param content The content to update.
1290     * @param propertyId The system property ID.
1291     * @param workspaceName The workspace name
1292     * @throws Exception if an error occurs while indexing.
1293     */
1294    public void updateProperty(Content content, String propertyId, String workspaceName) throws Exception
1295    {
1296        SolrClient solrClient = _getAutoCommitSolrClient(workspaceName);
1297        updateProperty(content, propertyId, workspaceName, solrClient);
1298    }
1299    
1300    /**
1301     * Update the value of a specific property in a content document.
1302     * @param content The content to update.
1303     * @param propertyId The system property ID.
1304     * @param workspaceName The workspace name
1305     * @param solrClient The solr client to use
1306     * @throws Exception if an error occurs while indexing.
1307     */
1308    public void updateProperty(Content content, String propertyId, String workspaceName, SolrClient solrClient) throws Exception
1309    {
1310        getLogger().debug("Updating the property '{}' for content {} into Solr.", propertyId, content);
1311        
1312        SolrInputDocument document = new SolrInputDocument();
1313        boolean hasUpdate = _solrContentIndexer.indexPartialProperty(content, propertyId, document);
1314        
1315        if (!hasUpdate)
1316        {
1317            getLogger().debug("Did not index '{}' property for content {} in Solr because no update to apply.", propertyId, content);
1318            return;
1319        }
1320        
1321        int status = _pushSolrDocument(document, workspaceName, solrClient);
1322        if (status != 0)
1323        {
1324            throw new IOException("Indexing of property '" + propertyId + "': got status code '" + status + "'.");
1325        }
1326        
1327        getLogger().debug("Succesfully indexed '{}' property for content {} in Solr.", propertyId, content);
1328    }
1329    
1330    private int _pushSolrDocument(SolrInputDocument document, String workspaceName, SolrClient solrClient) throws Exception
1331    {
1332        String collection = _solrClientProvider.getCollectionName(workspaceName);
1333        UpdateResponse solrResponse = solrClient.add(collection, document);
1334        return solrResponse.getStatus();
1335    }
1336    
1337    /**
1338     * Remove a content from Solr index for all workspaces and commit
1339     * @param contentId The id of content to unindex
1340     * @param unindexAttachments also unindex content attachments
1341     * @throws Exception if an error occurs while indexing.
1342     */
1343    public void unindexContent(String contentId, boolean unindexAttachments) throws Exception
1344    {
1345        String[] workspaceNames = _repository.getWorkspaces();
1346        for (String workspaceName : workspaceNames)
1347        {
1348            unindexContent(contentId, workspaceName, unindexAttachments);
1349        }
1350    }
1351    
1352    /**
1353     * Remove a content from Solr index
1354     * @param contentId The id of content to unindex
1355     * @param workspaceName The workspace where to work in
1356     * @param unindexAttachments also unindex content attachments
1357     * @throws Exception if an error occurs while indexing.
1358     */
1359    public void unindexContent(String contentId, String workspaceName, boolean unindexAttachments) throws Exception
1360    {
1361        SolrClient solrClient = _getAutoCommitSolrClient(workspaceName);
1362        unindexContent(contentId, workspaceName, unindexAttachments, solrClient);
1363    }
1364    
1365    /**
1366     * Remove a content from Solr index
1367     * @param contentId The id of content to unindex
1368     * @param workspaceName The workspace where to work in
1369     * @param unindexAttachments also unindex content attachments
1370     * @param solrClient The solr client to use
1371     * @throws Exception if an error occurs while indexing.
1372     */
1373    public void unindexContent(String contentId, String workspaceName, boolean unindexAttachments, SolrClient solrClient) throws Exception
1374    {
1375        Request request = ContextHelper.getRequest(_context);
1376        
1377        // Retrieve the current workspace.
1378        String currentWsp = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
1379        
1380        try
1381        {
1382            // Force the workspace.
1383            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, workspaceName);
1384            
1385            getLogger().debug("Unindexing content {} from Solr for workspace {}", contentId, workspaceName);
1386            
1387            deleteRepeaterDocs(contentId, workspaceName, solrClient);
1388            doUnindexDocument(contentId, workspaceName, solrClient);
1389            if (unindexAttachments)
1390            {
1391                doUnindexContentAttachments(contentId, workspaceName, solrClient);
1392            }
1393            _solrWorkflowIndexer.unindexAmetysObjectWorkflow(contentId, workspaceName, solrClient);
1394            
1395            getLogger().debug("Succesfully deleted content {} from Solr.", contentId);
1396        }
1397        catch (Exception e)
1398        {
1399            String error = String.format("Failed to unindex content %s in workspace %s", contentId, workspaceName);
1400            getLogger().error(error, e);
1401            throw new IndexingException(error, e);
1402        }
1403        finally
1404        {
1405            // Restore workspace
1406            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWsp);
1407        }
1408    }
1409    
1410    /**
1411     * Remove a content from Solr index for all workspaces and commit
1412     * @param contentIds The id of content to unindex
1413     * @throws Exception if an error occurs while indexing.
1414     */
1415    public void unindexContents(Collection<String> contentIds) throws Exception
1416    {
1417        String[] workspaceNames = _repository.getWorkspaces();
1418        for (String workspaceName : workspaceNames)
1419        {
1420            unindexContents(contentIds, workspaceName);
1421        }
1422    }
1423    
1424    /**
1425     * Remove a content from Solr index
1426     * @param contentIds The id of content to unindex
1427     * @param workspaceName The workspace where to work in
1428     * @throws Exception if an error occurs while indexing.
1429     */
1430    public void unindexContents(Collection<String> contentIds, String workspaceName) throws Exception
1431    {
1432        Request request = ContextHelper.getRequest(_context);
1433        
1434        // Retrieve the current workspace.
1435        String currentWsp = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
1436        
1437        try
1438        {
1439            // Force the workspace.
1440            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, workspaceName);
1441            SolrClient solrClient = _getAutoCommitSolrClient(workspaceName);
1442            
1443            getLogger().debug("Unindexing several contents from Solr.");
1444            
1445            for (String contentId : contentIds)
1446            {
1447                deleteRepeaterDocs(contentId, workspaceName, solrClient);
1448                doUnindexDocument(contentId, workspaceName, solrClient);
1449                _solrWorkflowIndexer.unindexAmetysObjectWorkflow(contentId, workspaceName, solrClient);
1450            }
1451            
1452            getLogger().debug("Succesfully unindexed content from Solr.");
1453        }
1454        catch (Exception e)
1455        {
1456            String error = String.format("Failed to unindex several contents in workspace %s", workspaceName);
1457            getLogger().error(error, e);
1458            throw new IndexingException(error, e);
1459        }
1460        finally
1461        {
1462            // Restore workspace
1463            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWsp);
1464        }
1465    }
1466    
1467    /**
1468     * Add or update a content into Solr index
1469     * @param content The content to index
1470     * @param workspaceName The workspace where to index
1471     * @param solrClient The solr client to use
1472     * @throws Exception if an error occurs while indexing.
1473     */
1474    protected void doIndexContent(Content content, String workspaceName, SolrClient solrClient) throws Exception
1475    {
1476        long time_0 = System.currentTimeMillis();
1477        
1478        SolrInputDocument document = new SolrInputDocument();
1479        List<SolrInputDocument> additionalDocuments = _solrContentIndexer.indexContent(content, document);
1480        
1481        long time_1 = System.currentTimeMillis();
1482        getLogger().debug("Populate indexing fields for content {} for workspace {} in {} ms", content.getId(), workspaceName, time_1 - time_0);
1483        
1484        indexAclInitValues(content, document);
1485        
1486        long time_2 = System.currentTimeMillis();
1487        getLogger().debug("Populate ACL for content {} for workspace {} in {} ms", content.getId(), workspaceName, time_2 - time_1);
1488        
1489        List<SolrInputDocument> documents = new ArrayList<>(additionalDocuments);
1490        documents.add(document);
1491        
1492        UpdateResponse solrResponse = solrClient.add(_solrClientProvider.getCollectionName(workspaceName), documents);
1493        int status = solrResponse.getStatus();
1494        
1495        long time_3 = System.currentTimeMillis();
1496        getLogger().debug("Update document for content {} for workspace {} in {} ms", content.getId(), workspaceName, time_3 - time_2);
1497        
1498        if (status != 0)
1499        {
1500            throw new IOException("Content indexation: got status code '" + status + "'.");
1501        }
1502    }
1503    
1504    /**
1505     * Indexes read-ACl initial values for object
1506     * @param ametysObject The object
1507     * @param document The Solr document
1508     */
1509    public void indexAclInitValues(AmetysObject ametysObject, SolrInputDocument document)
1510    {
1511        // Indexation of AmetysObject property
1512        document.addField(SolrFieldNames.IS_AMETYS_OBJECT, true);
1513        
1514        // Indexation of initValues for AllowedUsers
1515        AllowedUsers allowedUsers = _readAccessHelper.allowedUsers(ametysObject);
1516        document.addField(SolrFieldNames.ACL_INIT_VALUE_ANONYMOUS, allowedUsers.isAnonymousAllowed());
1517        document.addField(SolrFieldNames.ACL_INIT_VALUE_ANYCONNECTED, allowedUsers.isAnyConnectedUserAllowed());
1518        _addField(document, SolrFieldNames.ACL_INIT_VALUE_ALLOWED_USERS, allowedUsers.getAllowedUsers(), UserIdentity::userIdentityToString);
1519        _addField(document, SolrFieldNames.ACL_INIT_VALUE_DENIED_USERS, allowedUsers.getDeniedUsers(), UserIdentity::userIdentityToString);
1520        _addField(document, SolrFieldNames.ACL_INIT_VALUE_ALLOWED_GROUPS, allowedUsers.getAllowedGroups(), GroupIdentity::groupIdentityToString);
1521        _addField(document, SolrFieldNames.ACL_INIT_VALUE_DENIED_GROUPS, allowedUsers.getDeniedGroups(), GroupIdentity::groupIdentityToString);
1522    }
1523    
1524    private <T> void _addField(SolrInputDocument document, String fieldName, Set<T> values, Function<T, String> stringifier)
1525    {
1526        if (values == null)
1527        {
1528            return;
1529        }
1530        
1531        for (T value : values)
1532        {
1533            document.addField(fieldName, stringifier.apply(value));
1534        }
1535    }
1536    
1537    /**
1538     * Index the whole workflow of a content.
1539     * @param content The content.
1540     * @param workspaceName The workspace name
1541     * @param solrClient The solr client to use
1542     * @throws Exception if an error occurs while indexing.
1543     */
1544    protected void doIndexContentWorkflow(Content content, String workspaceName, SolrClient solrClient) throws Exception
1545    {
1546        if (content instanceof WorkflowAwareContent)
1547        {
1548            _solrWorkflowIndexer.indexAmetysObjectWorkflow((WorkflowAwareContent) content, workspaceName, solrClient);
1549        }
1550    }
1551    
1552    /**
1553     * Index content attachments as new entries in the idnex
1554     * @param collection the collection of attachments
1555     * @param content the content whose attachments will be indexed
1556     * @throws Exception if something goes wrong when indexing the attachments of the content
1557     */
1558    public void indexContentAttachments(ResourceCollection collection, Content content) throws Exception
1559    {
1560        Request request = ContextHelper.getRequest(_context);
1561        String workspaceName = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
1562        SolrClient solrClient = _getAutoCommitSolrClient(workspaceName);
1563        indexContentAttachments(collection, content, solrClient);
1564    }
1565    
1566    /**
1567     * Index content attachments as new entries in the idnex
1568     * @param collection the collection of attachments
1569     * @param content the content whose attachments will be indexed
1570     * @param solrClient The solr client to use
1571     * @throws Exception if something goes wrong when indexing the attachments of the content
1572     */
1573    public void indexContentAttachments(ResourceCollection collection, Content content, SolrClient solrClient) throws Exception
1574    {
1575        if (collection == null)
1576        {
1577            return;
1578        }
1579        
1580        try (AmetysObjectIterable<AmetysObject> children = collection.getChildren())
1581        {
1582            for (AmetysObject object : children)
1583            {
1584                if (object instanceof ResourceCollection)
1585                {
1586                    indexContentAttachments((ResourceCollection) object, content, solrClient);
1587                }
1588                else if (object instanceof Resource)
1589                {
1590                    Resource resource = (Resource) object;
1591                    indexContentAttachment(resource, content, solrClient);
1592                }
1593            }
1594        }
1595    }
1596    
1597    /**
1598     * Index a content attachment
1599     * @param resource the content attachment as a {@link Resource}
1600     * @param content the content whose attachment is going to be indexed
1601     * @throws Exception if something goes wrong when processing the indexation of the content attachment
1602     */
1603    public void indexContentAttachment(Resource resource, Content content) throws Exception
1604    {
1605        Request request = ContextHelper.getRequest(_context);
1606        String workspaceName = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
1607        SolrClient solrClient = _getAutoCommitSolrClient(workspaceName);
1608        indexContentAttachment(resource, content, solrClient);
1609    }
1610    
1611    /**
1612     * Index a content attachment
1613     * @param resource the content attachment as a {@link Resource}
1614     * @param content the content whose attachment is going to be indexed
1615     * @param solrClient The solr client to use
1616     * @throws Exception if something goes wrong when processing the indexation of the content attachment
1617     */
1618    public void indexContentAttachment(Resource resource, Content content, SolrClient solrClient) throws Exception
1619    {
1620        SolrInputDocument document = new SolrInputDocument();
1621        
1622        // Prepare resource doc
1623        _indexContentAttachment(resource, document, content);
1624        
1625        // Indexation of the document
1626        _indexResourceDocument(resource, document, solrClient);
1627    }
1628    
1629    private void _indexContentAttachment(Resource resource, SolrInputDocument document, Content content) throws Exception
1630    {
1631        String language = content.getLanguage();
1632        
1633        _solrResourceIndexer.indexResource(resource, document, SolrFieldNames.TYPE_CONTENT_ATTACHMENT_RESOURCE, language);
1634        
1635        // Need the id of the content for unindexing attachment during the unindexing of the content
1636        document.addField(SolrFieldNames.ATTACHMENT_CONTENT_ID, content.getId());
1637    }
1638    
1639    /**
1640     * Index a populated solr input document of type Resource.
1641     * @param resource the resource from which the input document is created
1642     * @param document the input document
1643     * @param solrClient The solr client to use
1644     * @throws SolrServerException if there is an error on the server
1645     * @throws IOException if there is a communication error with the server
1646     */
1647    protected void _indexResourceDocument(Resource resource, SolrInputDocument document, SolrClient solrClient) throws SolrServerException, IOException
1648    {
1649        String resourceId = resource.getId();
1650        
1651        // Retrieve appropriate collection name
1652        Request request = ContextHelper.getRequest(_context);
1653        String workspaceName = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
1654        String collectionName = _solrClientProvider.getCollectionName(workspaceName);
1655        
1656        // Add document
1657        UpdateResponse solrResponse = solrClient.add(collectionName, document);
1658        int status = solrResponse.getStatus();
1659        
1660        if (status != 0)
1661        {
1662            throw new IOException("Ametys resource indexing - Expecting status code of '0' in the Solr response but got : '" + status + "'. Resource id : " + resourceId);
1663        }
1664        
1665        getLogger().debug("Successful resource indexing. Resource identifier : {}", resourceId);
1666    }
1667    
1668    /**
1669     * Delete repeater documents of a specified content.
1670     * @param workspaceName The workspace name
1671     * @param contentId the content ID.
1672     * @param solrClient The solr client to use
1673     * @throws Exception if an error occurs while indexing.
1674     */
1675    protected void deleteRepeaterDocs(String contentId, String workspaceName, SolrClient solrClient) throws Exception
1676    {
1677        long time_0 = System.currentTimeMillis();
1678        
1679        // _documentType:repeater AND id:content\://xxx/*
1680        StringBuilder query = new StringBuilder();
1681        query.append(SolrFieldNames.DOCUMENT_TYPE).append(':').append(SolrFieldNames.TYPE_REPEATER)
1682             .append(" AND id:").append(ClientUtils.escapeQueryChars(contentId)).append("/*");
1683        
1684        solrClient.deleteByQuery(_solrClientProvider.getCollectionName(workspaceName), query.toString());
1685        
1686        getLogger().debug("Successfully delete repeaters documents for content {} in {} ms", contentId, System.currentTimeMillis() - time_0);
1687    }
1688    
1689    /**
1690     * Index all the resources in a given workspace.
1691     * @param workspaceName The workspace where to index
1692     * @param solrClient The solr client to use
1693     * @param progressionTracker The progression of the indexation
1694     * @return The indexation result as a Map.
1695     * @throws Exception if an error occurs while indexing.
1696     */
1697    public Map<String, Object> indexAllResources(String workspaceName, SolrClient solrClient, SimpleProgressionTracker progressionTracker) throws Exception
1698    {
1699        Request request = ContextHelper.getRequest(_context);
1700        
1701        // Retrieve the current workspace.
1702        String currentWsp = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
1703        
1704        try
1705        {
1706            // Force the workspace.
1707            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, workspaceName);
1708            
1709            getLogger().info("Starting the indexation of all resources for workspace {}", workspaceName);
1710            
1711            long start = System.currentTimeMillis();
1712            
1713            // Delete all resources
1714            _unindexAllResources(workspaceName, solrClient);
1715            
1716            Map<String, Object> results = new HashMap<>();
1717            
1718            try
1719            {
1720                TraversableAmetysObject resourceRoot = _resolver.resolveByPath(RepositoryConstants.NAMESPACE_PREFIX + ":resources");
1721                AmetysObjectIterable<Resource> resources = resourceRoot.getChildren();
1722                
1723                IndexationResult result = doIndexResources(resources, SolrFieldNames.TYPE_RESOURCE, resourceRoot, workspaceName, solrClient, progressionTracker);
1724                
1725                long end = System.currentTimeMillis();
1726                
1727                if (!result.hasErrors())
1728                {
1729                    getLogger().info("{} resources indexed without error in {} milliseconds.", result.successCount(), end - start);
1730                }
1731                else
1732                {
1733                    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());
1734                }
1735                
1736                results.put("successCount", result.successCount());
1737                if (result.hasErrors())
1738                {
1739                    results.put("errorCount", result.errorCount());
1740                }
1741            }
1742            catch (UnknownAmetysObjectException e)
1743            {
1744                getLogger().info("There is no root for resources in current workspace.");
1745                progressionTracker.setSize(0);
1746            }
1747            
1748            return results;
1749        }
1750        catch (Exception e)
1751        {
1752            String error = String.format("Failed to index all resources in workspace %s", workspaceName);
1753            getLogger().error(error, e);
1754            throw new IndexingException(error, e);
1755        }
1756        finally
1757        {
1758            // Restore context
1759            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWsp);
1760        }
1761    }
1762    
1763    /**
1764     * Send a collection of contents for indexation in the solr server.
1765     * @param resources the collection of contents to index.
1766     * @param documentType The document type of the resource
1767     * @param workspaceName The workspace where to index
1768     * @return the indexation result.
1769     * @throws Exception if an error occurs while indexing.
1770     */
1771    public IndexationResult indexResources(Iterable<AmetysObject> resources, String documentType, String workspaceName) throws Exception
1772    {
1773        SolrClient solrClient = _getAutoCommitSolrClient(workspaceName);
1774        return indexResources(resources, documentType, null, workspaceName, solrClient);
1775    }
1776    
1777    /**
1778     * Send a collection of contents for indexation in the solr server.
1779     * @param resources the collection of contents to index.
1780     * @param documentType The document type of the resource
1781     * @param resourceRoot The resource root, can be null. In that case, it will be computed for each resource.
1782     * @param workspaceName The workspace where to index
1783     * @param solrClient The solr client to use
1784     * @return the indexation result.
1785     * @throws Exception if an error occurs while indexing.
1786     */
1787    public IndexationResult indexResources(Iterable<? extends AmetysObject> resources, String documentType, TraversableAmetysObject resourceRoot, String workspaceName, SolrClient solrClient) throws Exception
1788    {
1789        return indexResources(resources, documentType, resourceRoot, workspaceName, solrClient, ProgressionTrackerFactory.createSimpleProgressionTracker("Index resources", getLogger()));
1790    }
1791    
1792    /**
1793     * Send a collection of contents for indexation in the solr server.
1794     * @param resources the collection of contents to index.
1795     * @param documentType The document type of the resource
1796     * @param resourceRoot The resource root, can be null. In that case, it will be computed for each resource.
1797     * @param workspaceName The workspace where to index
1798     * @param solrClient The solr client to use
1799     * @param progressionTracker The progression of the indexation
1800     * @return the indexation result.
1801     * @throws Exception if an error occurs while indexing.
1802     */
1803    public IndexationResult indexResources(Iterable<? extends AmetysObject> resources, String documentType, TraversableAmetysObject resourceRoot, String workspaceName, SolrClient solrClient, SimpleProgressionTracker progressionTracker) throws Exception
1804    {
1805        Request request = ContextHelper.getRequest(_context);
1806        
1807        // Retrieve the current workspace.
1808        String currentWsp = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
1809        
1810        try
1811        {
1812            // Force the workspace.
1813            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, workspaceName);
1814            
1815            getLogger().info("Starting indexation of several resources for workspace {}", workspaceName);
1816            
1817            long start = System.currentTimeMillis();
1818            
1819            IndexationResult result = doIndexResources(resources, documentType, resourceRoot, workspaceName, solrClient, progressionTracker);
1820            
1821            long end = System.currentTimeMillis();
1822            
1823            if (!result.hasErrors())
1824            {
1825                getLogger().info("{} resources indexed without error in {} milliseconds.", result.successCount(), end - start);
1826            }
1827            else
1828            {
1829                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());
1830            }
1831            
1832            return result;
1833        }
1834        catch (Exception e)
1835        {
1836            String error = String.format("Failed to index several resources in workspace %s", workspaceName);
1837            getLogger().error(error, e);
1838            throw new IndexingException(error, e);
1839        }
1840        finally
1841        {
1842            // Restore context
1843            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWsp);
1844        }
1845    }
1846    
1847    /**
1848     * Send some resources for indexation in the solr server.
1849     * @param resources the resources to index.
1850     * @param documentType The document type of the resource
1851     * @param resourceRoot The resource root, can be null. In that case, it will be computed for each resource.
1852     * @param workspaceName The workspace where to index
1853     * @param solrClient The solr client to use
1854     * @param progressionTracker The progression of the indexation
1855     * @return the indexation result.
1856     * @throws Exception if an error occurs committing the results.
1857     */
1858    protected IndexationResult doIndexResources(Iterable<? extends AmetysObject> resources, String documentType, TraversableAmetysObject resourceRoot, String workspaceName, SolrClient solrClient, SimpleProgressionTracker progressionTracker) throws Exception
1859    {
1860        try
1861        {
1862            // Add callable for each content to index
1863            List<Future<Void>> tasks = new ArrayList<>();
1864            for (AmetysObject resource : resources)
1865            {
1866                tasks.addAll(_asyncIndexExplorerItem(resource, documentType, resourceRoot, solrClient));
1867            }
1868            
1869            // Now that everything is submitted, we can iterate and wait for result
1870            return IndexationResult.fromTasks(tasks, getLogger());
1871        }
1872        finally
1873        {
1874            progressionTracker.increment();
1875        }
1876    }
1877    
1878    /**
1879     * Add or update a resource into Solr index
1880     * @param resource The resource to index
1881     * @param documentType The document type of the resource
1882     * @param workspaceName The workspace where to index
1883     * @throws Exception if an error occurs while indexing.
1884     */
1885    public void indexResource(Resource resource, String documentType, String workspaceName) throws Exception
1886    {
1887        SolrClient solrClient = _getAutoCommitSolrClient(workspaceName);
1888        doIndexResources(Collections.singleton(resource), documentType, null, workspaceName, solrClient, ProgressionTrackerFactory.createSimpleProgressionTracker("Index resource", getLogger()));
1889    }
1890    
1891    private void _unindexAllResources(String workspaceName, SolrClient solrClient) throws Exception
1892    {
1893        getLogger().debug("Unindexing all resources from Solr.");
1894        
1895        String collection = _solrClientProvider.getCollectionName(workspaceName);
1896        solrClient.deleteByQuery(collection, SolrFieldNames.DOCUMENT_TYPE + ':' + SolrFieldNames.TYPE_RESOURCE);
1897        
1898        getLogger().debug("Succesfully deleted all resource documents from Solr.");
1899    }
1900    
1901    /**
1902     * Delete all resource documents at a given path for all workspaces and commit
1903     * @param rootId The resource root ID, must not be null.
1904     * @param path The resource path relative to the given root, must start with a slash.
1905     * @throws Exception If an error occurs while unindexing.
1906     */
1907    public void unindexResourcesByPath(String rootId, String path) throws Exception
1908    {
1909        String[] workspaceNames = _repository.getWorkspaces();
1910        for (String workspaceName : workspaceNames)
1911        {
1912            unindexResourcesByPath(rootId, path, workspaceName);
1913        }
1914    }
1915    
1916    /**
1917     * Delete all resource documents at a given path.
1918     * @param rootId The resource root ID, must not be null.
1919     * @param path The resource path relative to the given root, must start with a slash.
1920     * @param workspaceName The workspace where to work in
1921     * @throws Exception If an error occurs while unindexing.
1922     */
1923    public void unindexResourcesByPath(String rootId, String path, String workspaceName) throws Exception
1924    {
1925        Request request = ContextHelper.getRequest(_context);
1926        
1927        // Retrieve the current workspace.
1928        String currentWsp = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
1929        
1930        try
1931        {
1932            // Force the workspace.
1933            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, workspaceName);
1934            
1935            getLogger().debug("Unindexing all resources at path {} in root {}", path, rootId);
1936            
1937            Query query = new ResourceLocationQuery(rootId, path);
1938            
1939            String collection = _solrClientProvider.getCollectionName(workspaceName);
1940            _getAutoCommitSolrClient(workspaceName).deleteByQuery(collection, query.build());
1941            
1942            getLogger().debug("Succesfully deleted resource document from Solr.");
1943        }
1944        catch (Exception e)
1945        {
1946            String error = String.format("Failed to unindex resource %s in workspace %s", path, workspaceName);
1947            getLogger().error(error, e);
1948            throw new IndexingException(error, e);
1949        }
1950        finally
1951        {
1952            // Restore workspace
1953            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWsp);
1954        }
1955    }
1956    
1957    /**
1958     * Remove a resource from Solr index for all workspaces and commit
1959     * @param resourceId The id of resource to unindex
1960     * @throws Exception if an error occurs while indexing.
1961     */
1962    public void unindexResource(String resourceId) throws Exception
1963    {
1964        String[] workspaceNames = _repository.getWorkspaces();
1965        for (String workspaceName : workspaceNames)
1966        {
1967            unindexResource(resourceId, workspaceName);
1968        }
1969    }
1970    
1971    /**
1972     * Remove a resource from the Solr index.
1973     * @param resourceId The id of resource to unindex
1974     * @param workspaceName The workspace where to work in
1975     * @throws Exception if an error occurs while unindexing.
1976     */
1977    public void unindexResource(String resourceId, String workspaceName) throws Exception
1978    {
1979        Request request = ContextHelper.getRequest(_context);
1980        
1981        // Retrieve the current workspace.
1982        String currentWsp = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
1983        
1984        try
1985        {
1986            // Force the workspace.
1987            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, workspaceName);
1988            SolrClient solrClient = _getAutoCommitSolrClient(workspaceName);
1989            
1990            getLogger().debug("Unindexing resource {} from Solr.", resourceId);
1991            
1992            doUnindexDocument(resourceId, workspaceName, solrClient);
1993            
1994            getLogger().debug("Succesfully deleted resource {} from Solr.", resourceId);
1995        }
1996        catch (Exception e)
1997        {
1998            String error = String.format("Failed to unindex resource  %s in workspace %s", resourceId, workspaceName);
1999            getLogger().error(error, e);
2000            throw new IndexingException(error, e);
2001        }
2002        finally
2003        {
2004            // Restore workspace
2005            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWsp);
2006        }
2007    }
2008    
2009    private List<Future<Void>> _asyncIndexExplorerItem(AmetysObject node, String documentType, TraversableAmetysObject resourceRoot, SolrClient solrClient) throws Exception
2010    {
2011        List<Future<Void>> tasks = new ArrayList<>();
2012        
2013        if (node instanceof ResourceCollection resourceCollection)
2014        {
2015            try (AmetysObjectIterable<AmetysObject> children = resourceCollection.getChildren())
2016            {
2017                for (AmetysObject child : children)
2018                {
2019                    tasks.addAll(_asyncIndexExplorerItem(child, documentType, resourceRoot, solrClient));
2020                }
2021            }
2022        }
2023        else if (node instanceof Resource resource)
2024        {
2025            tasks.add(_threadIndexerHelper.submitCallable(new ResourceIndexerCallable(resource, _workspaceSelector.getWorkspace(), documentType, resourceRoot, solrClient)));
2026        }
2027        
2028        return tasks;
2029    }
2030    
2031    /**
2032     * Process a Solr commit operation in all workspaces.
2033     * <br>Use this only after a long operation with updates sent via {@link NoAutoCommitUpdateClient}
2034     * @throws SolrServerException if there is an error on the server
2035     * @throws IOException if there is a communication error with the server
2036     */
2037    public void commit() throws SolrServerException, IOException
2038    {
2039        String[] workspaceNames;
2040        try
2041        {
2042            workspaceNames = _repository.getWorkspaces();
2043            for (String workspaceName : workspaceNames)
2044            {
2045                SolrClient solrClient = _getNoAutoCommitSolrClient(workspaceName);
2046                commit(workspaceName, solrClient);
2047            }
2048        }
2049        catch (RepositoryException e)
2050        {
2051            throw new RuntimeException("An exception occured while retrieving JCR workspaces. Cannot commited to Solr.", e);
2052        }
2053    }
2054    
2055    /**
2056     * Process a Solr commit operation in given workspace.
2057     * @param workspaceName The workspace's name
2058     * @param solrClient The solr client to use
2059     * @throws SolrServerException if there is an error on the server
2060     * @throws IOException if there is a communication error with the server
2061     */
2062    public void commit(String workspaceName, SolrClient solrClient) throws SolrServerException, IOException
2063    {
2064        long time_0 = System.currentTimeMillis();
2065        
2066        // Commit
2067        UpdateResponse solrResponse = solrClient.commit(_solrClientProvider.getCollectionName(workspaceName));
2068        int status = solrResponse.getStatus();
2069        
2070        if (status != 0)
2071        {
2072            throw new IOException("Ametys indexing: Solr commit operation - Expecting status code of '0' in the Solr response but got : '" + status + "'.");
2073        }
2074        
2075        getLogger().debug("Successful Solr commit operation during an Ametys indexing process in {} ms", System.currentTimeMillis() - time_0);
2076    }
2077    
2078    /**
2079     * Launch a solr index optimization.
2080     * @param workspaceName The workspace's name
2081     * @param solrClient The solr client to use
2082     * @throws SolrServerException if there is an error on the server
2083     * @throws IOException if there is a communication error with the server
2084     */
2085    public void optimize(String workspaceName, SolrClient solrClient) throws SolrServerException, IOException
2086    {
2087        UpdateResponse solrResponse = solrClient.optimize(_solrClientProvider.getCollectionName(workspaceName));
2088        int status = solrResponse.getStatus();
2089        
2090        if (status != 0)
2091        {
2092            throw new IOException("Ametys indexing: Solr optimize operation - Expecting status code of '0' in the Solr response but got : '" + status + "'.");
2093        }
2094        
2095        getLogger().debug("Successful Solr optimize operation during an Ametys indexing process.");
2096    }
2097    
2098    /**
2099     * Delete all documents from the solr index.
2100     * @param workspaceName The workspace name
2101     * @param solrClient The solr client to use
2102     * @throws Exception if an error occurs while unindexing.
2103     */
2104    public void unindexAllDocuments(String workspaceName, SolrClient solrClient) throws Exception
2105    {
2106        getLogger().debug("Deleting all documents from Solr.");
2107        
2108        String collection = _solrClientProvider.getCollectionName(workspaceName);
2109        solrClient.deleteByQuery(collection, "*:*");
2110        
2111        getLogger().debug("Successfully deleted all documents from Solr.");
2112    }
2113    
2114    /**
2115     * Delete a document from the Solr server.
2116     * @param id The id of the document to delete from Solr
2117     * @param workspaceName The workspace name
2118     * @param solrClient The solr client to use
2119     * @throws Exception if an error occurs while indexing.
2120     */
2121    protected void doUnindexDocument(String id, String workspaceName, SolrClient solrClient) throws Exception
2122    {
2123        UpdateResponse solrResponse = solrClient.deleteById(_solrClientProvider.getCollectionName(workspaceName), id);
2124        int status = solrResponse.getStatus();
2125        
2126        if (status != 0)
2127        {
2128            throw new IOException("Deletion of document " + id + ": got status code '" + status + "'.");
2129        }
2130    }
2131    
2132    /**
2133     * Delete content attachments documents of a given content from the Solr server.
2134     * @param contentId The id of the content
2135     * @param workspaceName The workspace name
2136     * @param solrClient The solr client to use
2137     * @throws Exception if an error occurs while indexing.
2138     */
2139    protected void doUnindexContentAttachments(String contentId, String workspaceName, SolrClient solrClient) throws Exception
2140    {
2141        String collectionName = _solrClientProvider.getCollectionName(workspaceName);
2142        
2143        Query query = new ContentAttachmentQuery(contentId);
2144        UpdateResponse solrResponse = solrClient.deleteByQuery(collectionName, query.build());
2145        int status = solrResponse.getStatus();
2146        
2147        if (status != 0)
2148        {
2149            throw new IOException("Deletion of content attachments of content " + contentId + ": got status code '" + status + "'.");
2150        }
2151    }
2152
2153    /**
2154     * Add or update a trash element into Solr index on all workspaces and commit
2155     * @param trashElementId The id of the trash element to index
2156     * @throws Exception if an error occurs while indexing.
2157     */
2158    public void indexTrashElement(String trashElementId) throws Exception
2159    {
2160        String[] workspaceNames = _repository.getWorkspaces();
2161        for (String workspaceName : workspaceNames)
2162        {
2163            indexTrashElement(trashElementId, workspaceName);
2164        }
2165    }
2166
2167    /**
2168     * Add or update a trash element into Solr index
2169     * @param trashElementId The id of the trash element to index
2170     * @param workspaceName the workspace where to index
2171     * @throws Exception if an error occurs while indexing.
2172     */
2173    public void indexTrashElement(String trashElementId, String workspaceName) throws Exception
2174    {
2175        Request request = ContextHelper.getRequest(_context);
2176        
2177        SolrClient solrClient = _getAutoCommitSolrClient(workspaceName);
2178
2179        // Retrieve the current workspace.
2180        String currentWsp = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
2181        
2182        try
2183        {
2184            // Force the workspace.
2185            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, workspaceName);
2186            
2187            if (_resolver.hasAmetysObjectForId(trashElementId))
2188            {
2189                DefaultTrashElement trashElement = _resolver.resolveById(trashElementId);
2190                doIndexTrashElements(Collections.singleton(trashElement), workspaceName, solrClient, ProgressionTrackerFactory.createSimpleProgressionTracker("Index trash element", getLogger()));
2191            }
2192        }
2193        finally
2194        {
2195            // Restore context
2196            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWsp);
2197        }
2198    }
2199
2200    /**
2201     * Index all the trash elements in a given workspace.
2202     * @param workspaceName the workspace where to index
2203     * @param solrClient The solr client to use
2204     * @param progressionTracker The progression of the indexation
2205     * @return The indexation result as a Map.
2206     * @throws Exception if an error occurs while indexing.
2207     */
2208    public Map<String, Object> indexAllTrashElements(String workspaceName, SolrClient solrClient, SimpleProgressionTracker progressionTracker) throws Exception
2209    {
2210        Request request = ContextHelper.getRequest(_context);
2211        
2212        // Retrieve the current workspace.
2213        String currentWsp = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
2214        
2215        try
2216        {
2217            // Force the workspace.
2218            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, workspaceName);
2219            
2220            getLogger().info("Starting the indexation of all trash elements for workspace {}", workspaceName);
2221            
2222            long start = System.currentTimeMillis();
2223            
2224            // Delete all trash elements
2225            unindexAllTrashElements(workspaceName, solrClient);
2226
2227            String query = QueryHelper.getXPathQuery(null, TrashElementFactory.TRASH_ELEMENT_NODETYPE, null);
2228            AmetysObjectIterable<DefaultTrashElement> trashElements = _resolver.query(query);
2229            
2230            IndexationResult result = doIndexTrashElements(trashElements, workspaceName, solrClient, progressionTracker);
2231            
2232            long end = System.currentTimeMillis();
2233            
2234            if (!result.hasErrors())
2235            {
2236                getLogger().info("{} trash elements indexed without error in {} milliseconds.", result.successCount(), end - start);
2237            }
2238            else
2239            {
2240                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());
2241            }
2242            
2243            Map<String, Object> results = new HashMap<>();
2244            results.put("successCount", result.successCount());
2245            if (result.hasErrors())
2246            {
2247                results.put("errorCount", result.errorCount());
2248            }
2249            
2250            return results;
2251        }
2252        catch (Exception e)
2253        {
2254            String error = String.format("Failed to index all trash elements in workspace %s", workspaceName);
2255            getLogger().error(error, e);
2256            throw new IndexingException(error, e);
2257        }
2258        finally
2259        {
2260            // Restore context
2261            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWsp);
2262        }
2263    }
2264
2265    /**
2266     * Remove a trash element from Solr index for all workspaces and commit
2267     * @param trashElementId The id of trash element to unindex
2268     * @throws Exception if an error occurs while indexing.
2269     */
2270    public void unindexTrashElement(String trashElementId) throws Exception
2271    {
2272        String[] workspaceNames = _repository.getWorkspaces();
2273        for (String workspaceName : workspaceNames)
2274        {
2275            unindexTrashElement(trashElementId, workspaceName);
2276        }
2277    }
2278
2279    /**
2280     * Remove a trash element from Solr index
2281     * @param trashElementId The id of trash element to unindex
2282     * @param workspaceName The workspace where to work in
2283     * @throws Exception if an error occurs while indexing.
2284     */
2285    public void unindexTrashElement(String trashElementId, String workspaceName) throws Exception
2286    {
2287        Request request = ContextHelper.getRequest(_context);
2288        
2289        SolrClient solrClient = _getAutoCommitSolrClient(workspaceName);
2290
2291        // Retrieve the current workspace.
2292        String currentWsp = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
2293        
2294        try
2295        {
2296            // Force the workspace.
2297            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, workspaceName);
2298            
2299            UpdateResponse solrResponse = solrClient.deleteById(_solrClientProvider.getCollectionName(workspaceName), trashElementId);
2300            int status = solrResponse.getStatus();
2301            
2302            if (status != 0)
2303            {
2304                throw new IOException("Deletion of document " + trashElementId + ": got status code '" + status + "'.");
2305            }
2306        }
2307        finally
2308        {
2309            // Restore context
2310            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWsp);
2311        }
2312    }
2313    
2314    /**
2315     * Unindex all trash elements documents.
2316     * @param workspaceName The workspace name
2317     * @param solrClient The solr client to use
2318     * @throws Exception if an error occurs while unindexing.
2319     */
2320    protected void unindexAllTrashElements(String workspaceName, SolrClient solrClient) throws Exception
2321    {
2322        getLogger().debug("Unindexing all trash elements from Solr.");
2323        
2324        String collection = _solrClientProvider.getCollectionName(workspaceName);
2325        Query query = new DocumentTypeQuery(SolrFieldNames.TYPE_TRASH_ELEMENT);
2326        solrClient.deleteByQuery(collection, query.build());
2327        
2328        getLogger().debug("Succesfully deleted all trash elements documents from Solr.");
2329    }
2330    
2331    /**
2332     * Send some trash elements for indexation in the solr server.
2333     * @param trashElements the trash elements to index.
2334     * @param workspaceName The workspace name
2335     * @param solrClient The solr client to use
2336     * @param progressionTracker The progression of the indexation
2337     * @return the indexation result.
2338     * @throws Exception if an error occurs committing the results.
2339     */
2340    protected IndexationResult doIndexTrashElements(Iterable<DefaultTrashElement> trashElements, String workspaceName, SolrClient solrClient, SimpleProgressionTracker progressionTracker) throws Exception
2341    {
2342        int numberOfTrashElements = IterableUtils.size(trashElements);
2343        progressionTracker.setSize(numberOfTrashElements);
2344        
2345        // Add callable for each content to index
2346        List<Future<Void>> tasks = new ArrayList<>();
2347        for (DefaultTrashElement trashElement : trashElements)
2348        {
2349            tasks.add(_threadIndexerHelper.submitCallable(new TrashElementIndexerCallable(trashElement, workspaceName, solrClient, progressionTracker)));
2350        }
2351        
2352        // Now that everything is submitted, we can iterate and wait for result
2353        return IndexationResult.fromTasks(tasks, getLogger());
2354    }
2355    
2356//    /**
2357//     * Get the collection to use.
2358//     * @return The name of the collection to index into.
2359//     */
2360//    protected String getCollection()
2361//    {
2362//        return _solrCorePrefix + _workspaceSelector.getWorkspace();
2363//    }
2364    
2365    private static final class SchemaRequestComparator implements Comparator<SchemaRequest.Update>
2366    {
2367        public int compare(SchemaRequest.Update u1, SchemaRequest.Update u2)
2368        {
2369            return Integer.compare(_getOrder(u1), _getOrder(u2));
2370        }
2371        
2372        private int _getOrder(SchemaRequest.Update update)
2373        {
2374            // Field types are needed first to define fields
2375            if (update instanceof SchemaRequest.AddFieldType)
2376            {
2377                return 1;
2378            }
2379            
2380            // Fields or dynamic fields can be defined at the same time
2381            if (update instanceof SchemaRequest.AddField || update instanceof SchemaRequest.AddDynamicField)
2382            {
2383                return 2;
2384            }
2385            
2386            // Copy fields can be based on fields or dynamic fields
2387            if (update instanceof SchemaRequest.AddCopyField)
2388            {
2389                return 3;
2390            }
2391            
2392            return 0;
2393        }
2394    }
2395    
2396    private class ContentIndexerCallable extends AbstractIndexerCallable<Content>
2397    {
2398        private boolean _indexAttachments;
2399        private SimpleProgressionTracker _tracker;
2400
2401        @SuppressWarnings("synthetic-access")
2402        public ContentIndexerCallable(Content content, String workspaceName, boolean indexAttachments, SolrClient solrClient, SimpleProgressionTracker progressionTracker)
2403        {
2404            super(content, workspaceName, solrClient, _manager, _cocoonContext, _resolver, getLogger());
2405            this._indexAttachments = indexAttachments;
2406            this._tracker = progressionTracker;
2407        }
2408
2409        @Override
2410        protected void process(Content content) throws Exception
2411        {
2412            try
2413            {
2414                doIndexContent(content, _workspaceName, _solrClient);
2415                doIndexContentWorkflow(content, _workspaceName, _solrClient);
2416                if (_indexAttachments)
2417                {
2418                    indexContentAttachments(content.getRootAttachments(), content, _solrClient);
2419                }
2420            }
2421            finally
2422            {
2423                _tracker.increment();
2424            }
2425        }
2426
2427        @Override
2428        protected String getObjectLabel()
2429        {
2430            return "content";
2431        }
2432    }
2433    
2434    private class ResourceIndexerCallable extends AbstractIndexerCallable<Resource>
2435    {
2436        private String _documentType;
2437        private TraversableAmetysObject _resourceRoot;
2438        
2439        @SuppressWarnings("synthetic-access")
2440        public ResourceIndexerCallable(Resource resource, String workspaceName, String documentType, TraversableAmetysObject resourceRoot, SolrClient solrClient)
2441        {
2442            super(resource, workspaceName, solrClient, _manager, _cocoonContext, _resolver, getLogger());
2443            this._documentType = documentType;
2444            this._resourceRoot = resourceRoot;
2445        }
2446        
2447        @Override
2448        protected void process(Resource resource) throws Exception
2449        {
2450            SolrInputDocument document = new SolrInputDocument();
2451            
2452            _solrResourceIndexer.indexResource(resource, document, _documentType, _resourceRoot);
2453            
2454            UpdateResponse solrResponse = _solrClient.add(_solrClientProvider.getCollectionName(_workspaceName), document);
2455            int status = solrResponse.getStatus();
2456            
2457            if (status != 0)
2458            {
2459                throw new IOException("Resource indexation: got status code '" + status + "'.");
2460            }
2461        }
2462
2463        @Override
2464        protected String getObjectLabel()
2465        {
2466            return "resource";
2467        }
2468    }
2469    
2470    private class TrashElementIndexerCallable extends AbstractIndexerCallable<DefaultTrashElement>
2471    {
2472        private SimpleProgressionTracker _tracker;
2473
2474        @SuppressWarnings("synthetic-access")
2475        public TrashElementIndexerCallable(DefaultTrashElement trashElement, String workspaceName, SolrClient solrClient, SimpleProgressionTracker progressionTracker)
2476        {
2477            super(trashElement, workspaceName, solrClient, _manager, _cocoonContext, _resolver, getLogger());
2478            this._tracker = progressionTracker;
2479        }
2480
2481        @Override
2482        protected void process(DefaultTrashElement trashElement) throws Exception
2483        {
2484            try
2485            {
2486                SolrInputDocument document = new SolrInputDocument();
2487                _solrTrashElementIndexer.indexTrashElement(trashElement, document);
2488                
2489                UpdateResponse solrResponse = _solrClient.add(_solrClientProvider.getCollectionName(_workspaceName), document);
2490                int status = solrResponse.getStatus();
2491                
2492                if (status != 0)
2493                {
2494                    throw new IOException("Trash element indexation: got status code '" + status + "'.");
2495                }
2496            }
2497            finally
2498            {
2499                _tracker.increment();
2500            }
2501        }
2502
2503        @Override
2504        protected String getObjectLabel()
2505        {
2506            return "trashElement";
2507        }
2508    }
2509}