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