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