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.web.indexing.solr;
017
018import java.io.IOException;
019import java.util.ArrayList;
020import java.util.Collection;
021import java.util.Collections;
022import java.util.Date;
023import java.util.HashSet;
024import java.util.List;
025import java.util.Map;
026import java.util.Set;
027
028import org.apache.avalon.framework.component.Component;
029import org.apache.avalon.framework.context.Context;
030import org.apache.avalon.framework.context.ContextException;
031import org.apache.avalon.framework.context.Contextualizable;
032import org.apache.avalon.framework.service.ServiceException;
033import org.apache.avalon.framework.service.ServiceManager;
034import org.apache.avalon.framework.service.Serviceable;
035import org.apache.cocoon.components.ContextHelper;
036import org.apache.cocoon.environment.Request;
037import org.apache.solr.client.solrj.SolrClient;
038import org.apache.solr.client.solrj.SolrServerException;
039import org.apache.solr.client.solrj.response.UpdateResponse;
040import org.apache.solr.common.SolrInputDocument;
041import org.apache.solr.common.SolrInputField;
042
043import org.ametys.cms.content.indexing.solr.SolrContentIndexer;
044import org.ametys.cms.content.indexing.solr.SolrFieldNames;
045import org.ametys.cms.content.indexing.solr.SolrIndexer;
046import org.ametys.cms.content.indexing.solr.SolrResourceIndexer;
047import org.ametys.cms.indexing.IndexingException;
048import org.ametys.cms.indexing.solr.AdditionalPropertyIndexer;
049import org.ametys.cms.indexing.solr.AdditionalPropertyIndexerExtensionPoint;
050import org.ametys.cms.repository.Content;
051import org.ametys.cms.repository.RequestAttributeWorkspaceSelector;
052import org.ametys.cms.search.query.AndQuery;
053import org.ametys.cms.search.query.DocumentTypeQuery;
054import org.ametys.cms.search.query.JoinQuery;
055import org.ametys.cms.search.query.OrQuery;
056import org.ametys.cms.search.query.Query;
057import org.ametys.cms.search.query.QuerySyntaxException;
058import org.ametys.cms.search.solr.SolrClientProvider;
059import org.ametys.cms.tag.Tag;
060import org.ametys.cms.tag.TagHelper;
061import org.ametys.cms.tag.TagProviderExtensionPoint;
062import org.ametys.plugins.explorer.resources.Resource;
063import org.ametys.plugins.explorer.resources.ResourceCollection;
064import org.ametys.plugins.repository.AmetysObject;
065import org.ametys.plugins.repository.AmetysObjectResolver;
066import org.ametys.plugins.repository.RepositoryConstants;
067import org.ametys.runtime.plugin.component.AbstractLogEnabled;
068import org.ametys.web.WebConstants;
069import org.ametys.web.repository.page.Page;
070import org.ametys.web.repository.page.Page.PageType;
071import org.ametys.web.repository.page.Zone;
072import org.ametys.web.repository.page.ZoneItem;
073import org.ametys.web.repository.page.ZoneItem.ZoneType;
074import org.ametys.web.repository.site.Site;
075import org.ametys.web.repository.sitemap.Sitemap;
076import org.ametys.web.search.query.AttachmentQuery;
077import org.ametys.web.search.query.PageQuery;
078import org.ametys.web.service.Service;
079import org.ametys.web.service.ServiceExtensionPoint;
080
081/**
082 * Component responsible for indexing a page with all its contents.
083 */
084public class SolrPageIndexer extends AbstractLogEnabled implements Component, Serviceable, SolrWebFieldNames, Contextualizable
085{
086    /** The avalon role. */
087    public static final String ROLE = SolrPageIndexer.class.getName();
088    
089    /** The Solr client provider */
090    protected SolrClientProvider _solrClientProvider;
091    /** The Solr indexer */
092    protected SolrIndexer _solrIndexer;
093    /** Solr Ametys contents indexer */
094    protected SolrContentIndexer _solrContentIndexer;
095    /** Solr Ametys resources indexer */
096    protected SolrResourceIndexer _solrResourceIndexer;
097    /** The Solr page right indexer. */
098    protected SolrPageRightIndexer _solrPageRightIndexer;
099    /** The additional property indexer extension point. */
100    protected AdditionalPropertyIndexerExtensionPoint _additionalPropertiesIndexerEP;
101    /** The tag provider extension point. */
102    protected TagProviderExtensionPoint _tagProviderEP;
103    
104    /** The service extension point. */
105    protected ServiceExtensionPoint _serviceExtensionPoint;
106    /** The Ametys object resolver*/
107    protected AmetysObjectResolver _ametysObjectResolver;
108    /** The avalon context */
109    protected Context _context;
110    
111    @Override
112    public void service(ServiceManager manager) throws ServiceException
113    {
114        _ametysObjectResolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
115        _solrIndexer = (SolrIndexer) manager.lookup(SolrIndexer.ROLE);
116        _solrContentIndexer = (SolrContentIndexer) manager.lookup(SolrContentIndexer.ROLE);
117        _solrResourceIndexer = (SolrResourceIndexer) manager.lookup(SolrResourceIndexer.ROLE);
118        _solrPageRightIndexer = (SolrPageRightIndexer) manager.lookup(SolrPageRightIndexer.ROLE);
119        _solrClientProvider = (SolrClientProvider) manager.lookup(SolrClientProvider.ROLE);
120        _serviceExtensionPoint = (ServiceExtensionPoint) manager.lookup(ServiceExtensionPoint.ROLE);
121        _additionalPropertiesIndexerEP = (AdditionalPropertyIndexerExtensionPoint) manager.lookup(AdditionalPropertyIndexerExtensionPoint.ROLE);
122        _tagProviderEP = (TagProviderExtensionPoint) manager.lookup(TagProviderExtensionPoint.ROLE);
123    }
124    
125    public void contextualize(Context context) throws ContextException
126    {
127        _context = context;
128    }
129    
130    /**
131     * Index a page and eventually its children, recursively, in all workspaces and commit<br>
132     * By default, children pages will be actually indexed if indexRecursively is true and if those pages are not already indexed.
133     * @param pageId the page to be indexed.
134     * @param indexRecursively to also process children pages.
135     * @param indexAttachments to index page attachments
136     * @throws Exception if an error occurs during indexation.
137     */
138    public void indexPage(String pageId, boolean indexRecursively, boolean indexAttachments) throws Exception
139    {
140        indexPage(pageId, RepositoryConstants.DEFAULT_WORKSPACE, indexRecursively, indexAttachments, true);
141        indexPage(pageId, WebConstants.LIVE_WORKSPACE, indexRecursively, indexAttachments, true);
142    }
143    
144    /**
145     * Index a page and eventually its children, recursively.<br>
146     * By default, children pages will be actually indexed if indexRecursively is true and if those pages are not already indexed.
147     * @param pageId the page to be indexed.
148     * @param workspaceName the workspace where to index
149     * @param indexRecursively to also process children pages.
150     * @param indexAttachments to index page attachments
151     * @param commit Commit the indexation
152     * @throws Exception if an error occurs during indexation.
153     */
154    public void indexPage(String pageId, String workspaceName, boolean indexRecursively, boolean indexAttachments, boolean commit) throws Exception
155    {
156        Request request = ContextHelper.getRequest(_context);
157        
158        // Retrieve the current workspace.
159        String currentWsp = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
160        // Retrieve the current site name.
161        String currentSiteName = (String) request.getAttribute("siteName");
162        
163        try
164        {
165            // Force the workspace.
166            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, workspaceName);
167    
168            getLogger().debug("Indexing page: {}", pageId);
169            
170            if (_ametysObjectResolver.hasAmetysObjectForId(pageId)) // In 'live' the page may not exist
171            {
172                Page page = _ametysObjectResolver.resolveById(pageId);
173                _indexPage(page, workspaceName, indexRecursively, indexAttachments);
174                
175                if (commit)
176                {
177                    _solrIndexer.commit(workspaceName);
178                }
179            }
180        }
181        catch (Exception e)
182        {
183            String error = String.format("Failed to index page %s in workspace %s", pageId, workspaceName);
184            getLogger().error(error, e);
185            throw new IndexingException(error, e);
186        }
187        finally
188        {
189            // Restore the site name.
190            request.setAttribute("siteName", currentSiteName);
191            // Restore context
192            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWsp);
193        }
194    }
195    
196    private void _indexPage(Page page, String workspaceName, boolean indexRecursively, boolean indexAttachments) throws Exception
197    {
198        getLogger().info("Indexing page: {} in workspace '{}'", page, workspaceName);
199        
200        SolrInputDocument document = new SolrInputDocument();
201        
202        // Prepare the solr input document by adding fields.
203        _populatePageDocument(page, document);
204        
205        // Set the additional properties in the document.
206        _populateAdditionalProperties(page, document);
207        
208        // Indexation of the page access.
209        _indexPageAccess(page, document);
210        
211        // Indexation of the document
212        _indexPageDocument(page, document, workspaceName);
213        
214        // Index page attachments documents
215        if (indexAttachments)
216        {
217            indexPageAttachments(page.getRootAttachments(), page);
218        }
219        
220        if (indexRecursively)
221        {
222            for (Page child : page.getChildrenPages())
223            {
224                // FIXME index child pages if (and only if) not indexed... see original source.
225//                indexPage(child, false, indexRecursively);
226//                indexPage(child, false);
227                _indexPage(child, workspaceName, indexRecursively, indexAttachments);
228            }
229        }
230    }
231    
232    /**
233     * Populate the solr input document by adding fields to index.
234     * @param page the page to index.
235     * @param document the solr input document
236     * @throws Exception if something goes wrong when processing the indexation of the page
237     */
238    protected void _populatePageDocument(Page page, SolrInputDocument document) throws Exception
239    {
240        Sitemap sitemap = page.getSitemap();
241        String sitemapName = sitemap.getName();
242        Site site = page.getSite();
243        String siteName = site.getName();
244        String pageTitle = page.getTitle();
245        String language = sitemapName;
246        
247        // Page id and type
248        document.addField(SolrFieldNames.ID, page.getId());
249        document.addField(SolrFieldNames.DOCUMENT_TYPE, SolrWebFieldNames.TYPE_PAGE);
250        
251        // Fulltext
252        SolrContentIndexer.indexFulltextValue(document, page.getTitle(), language);
253        if (!page.getTitle().equals(page.getLongTitle()))
254        {
255            SolrContentIndexer.indexFulltextValue(document, page.getLongTitle(), language);
256        }
257        
258        // Page title
259        document.addField(TITLE, pageTitle);
260        document.addField(LONG_TITLE, page.getLongTitle());
261        
262        document.addField(TEMPLATE, page.getTemplate());
263        document.addField(PAGE_TYPE, page.getType().name());
264        document.addField(PAGE_DEPTH, page.getDepth());
265        
266        // Contents (page title shoud be indexed before because the main content can override it).
267        _populatePageContentsDocument(page, document);
268
269        // Parents of the page
270        List<String> ancestorIds = new ArrayList<>();
271        AmetysObject parent = page.getParent();
272        while (parent instanceof Page)
273        {
274            ancestorIds.add(parent.getId());
275            parent = parent.getParent();
276        }
277        document.addField(ANCESTOR_IDS, ancestorIds);
278        
279        document.addField(SITE_NAME, siteName);
280        document.addField(SITEMAP_NAME, sitemapName);
281        
282        // Page tags (strict and tags including ancestor pages).
283        document.addField(SolrFieldNames.TAGS, page.getTags());
284        document.addField(SolrFieldNames.ALL_TAGS, _getTagsWithAncestors(page));
285        
286        // Page last modification date - Store.YES, Index.ANALYZED
287        Date lastModified = _getLastModificationDate(page);
288        if (lastModified != null)
289        {
290            document.addField(LAST_MODIFIED + "_dt", SolrIndexer.dateFormat().format(lastModified));
291        }
292
293        // Page last validation date - Store.YES, Index.ANALYZED
294        Date lastValidation = _getLastValidationDate(page);
295        if (lastValidation != null)
296        {
297            document.addField(LAST_VALIDATION, SolrIndexer.dateFormat().format(lastValidation));
298        }
299        
300        // date for sorting
301        SolrInputField dateField = document.getField(DATE_FOR_SORTING);
302        if (dateField == null)
303        {
304            Collection<Object> oDateValues = document.getFieldValues(CONTENT_INTERESTING_DATES);
305            if (oDateValues != null && !oDateValues.isEmpty())
306            {
307                document.setField(DATE_FOR_SORTING, oDateValues.iterator().next());
308            }
309        }
310        
311        // Attachments
312        _solrResourceIndexer.indexResourceCollection(page.getRootAttachments(), document, language);
313    }
314    
315    /**
316     * Get all the page tags with their ancestors.
317     * @param page The page.
318     * @return All the page tags with their ancestors.
319     */
320    protected Set<String> _getTagsWithAncestors(Page page)
321    {
322        Set<String> allTags = new HashSet<>(page.getTags());
323        
324        Map<String, Object> tagParams = Collections.singletonMap("siteName", page.getSiteName());
325        
326        for (String tagName : page.getTags())
327        {
328            allTags.add(tagName);
329            
330            // Get the ancestor tags
331            Tag tag = _tagProviderEP.getTag(tagName, tagParams);
332            for (Tag ancestor : TagHelper.getAncestors(tag, false))
333            {
334                allTags.add(ancestor.getName());
335            }
336        }
337        
338        return allTags;
339    }
340    
341    /**
342     * Index the content of the page.<p>
343     * @param page the page to index.
344     * @param document the document to populate.
345     * @throws Exception if an error occurs.
346     */
347    protected void _populatePageContentsDocument(Page page, SolrInputDocument document) throws Exception
348    {
349        if (page.getType() == PageType.CONTAINER)
350        {
351            for (Zone zone : page.getZones())
352            {
353                for (ZoneItem zoneItem : zone.getZoneItems())
354                {
355                    if (zoneItem.getType() == ZoneType.CONTENT)
356                    {
357                        // Content
358                        Content content = zoneItem.getContent();
359                        document.addField(CONTENT_IDS, content.getId());
360                    }
361                    else if (zoneItem.getType() == ZoneType.SERVICE)
362                    {
363                        // service 
364                        String serviceId = zoneItem.getServiceId();
365                        document.addField(SERVICE_IDS, serviceId);
366
367                        Service service = _serviceExtensionPoint.getExtension(serviceId);
368                        if (service == null)
369                        {
370                            getLogger().error("The service id '{}' does not exist. It is referenced in the page {}/{}/{} ({} in zoneitem {})", serviceId, page.getSiteName(), page.getSitemapName(), page.getPathInSitemap(), page.getId(), zoneItem.getId());
371                        }
372                        else
373                        {
374                            service.index(zoneItem, document);
375                        }
376                    }
377                }
378            }
379        }
380    }
381
382    /**
383     * Computes the last modification date of a page.
384     * @param page the page.
385     * @return the last modification date or <code>null</code>.
386     */
387    protected Date _getLastModificationDate(Page page)
388    {
389        Date lastModified = null;
390
391        if (page.getType() == PageType.CONTAINER)
392        {
393            for (Zone zone : page.getZones())
394            {
395                for (ZoneItem zoneItem : zone.getZoneItems())
396                {
397                    switch (zoneItem.getType())
398                    {
399                        case SERVICE:
400                            // A service has no last modification date
401                            break;
402                        case CONTENT:
403                            Date contentLastModified = zoneItem.getContent().getLastModified();
404
405                            if (lastModified == null || contentLastModified.after(lastModified))
406                            {
407                                // Keep the latest modification date
408                                lastModified = contentLastModified;
409                            }
410                            break;
411                        default:
412                            break;
413                    }
414                }
415            }
416        }
417
418        return lastModified;
419    }
420
421    /**
422     * Computes the last validation date of a page.
423     * @param page the page.
424     * @return the last validation date or <code>null</code>.
425     */
426    protected Date _getLastValidationDate(Page page)
427    {
428        Date lastValidated = null;
429
430        if (page.getType() == PageType.CONTAINER)
431        {
432            for (Zone zone : page.getZones())
433            {
434                for (ZoneItem zoneItem : zone.getZoneItems())
435                {
436                    switch (zoneItem.getType())
437                    {
438                        case SERVICE:
439                            // A service has no last validation date
440                            break;
441                        case CONTENT:
442                            Date contentLastValidation = zoneItem.getContent().getLastValidationDate();
443
444                            if (contentLastValidation != null && (lastValidated == null || contentLastValidation.after(lastValidated)))
445                            {
446                                // Keep the latest modification date
447                                lastValidated = contentLastValidation;
448                            }
449                            break;
450                        default:
451                            break;
452                    }
453                }
454            }
455        }
456        
457        return lastValidated;
458    }
459    
460    /**
461     * Populate the solr input document by adding fields to index.
462     * @param page the page to index.
463     * @param document the solr input document
464     * @throws Exception if something goes wrong when processing the indexation of the page
465     */
466    protected void _populateAdditionalProperties(Page page, SolrInputDocument document) throws Exception
467    {
468        Collection<AdditionalPropertyIndexer> indexers = _additionalPropertiesIndexerEP.getIndexers("page");
469        for (AdditionalPropertyIndexer indexer : indexers)
470        {
471            indexer.index(page, document);
472        }
473    }
474    
475    /**
476     * Index page attachments as new entries in the index.
477     * @param collection the collection of attachments
478     * @param page the page whose attachments will be indexed
479     * @throws Exception if something goes wrong when indexing the attachments of the page
480     */
481    public void indexPageAttachments(ResourceCollection collection, Page page) throws Exception
482    {
483        if (collection == null)
484        {
485            return;
486        }
487        
488        for (AmetysObject object : collection.getChildren())
489        {
490            if (object instanceof ResourceCollection)
491            {
492                indexPageAttachments((ResourceCollection) object, page);
493            }
494            else if (object instanceof Resource)
495            {
496                Resource resource = (Resource) object;
497                indexPageAttachment(resource, page);
498            }
499        }
500    }
501    
502    /**
503     * Index a page attachment
504     * @param resource the page attachment as a {@link Resource}
505     * @param page the page whose attachment is going to be indexed
506     * @throws Exception if something goes wrong when processing the indexation of the page attachment
507     */
508    public void indexPageAttachment(Resource resource, Page page) throws Exception
509    {
510        SolrInputDocument document = new SolrInputDocument();
511        
512        // Prepare resource doc
513        _indexPageAttachment(resource, document, page);
514        
515        // Indexation of the document
516        _indexResourceDocument(resource, document);
517    }
518    
519    private void _indexPageAttachment(Resource resource, SolrInputDocument document, Page page) throws Exception
520    {
521        String language = page.getSitemapName();
522        
523        _solrResourceIndexer.indexResource(resource, document, TYPE_PAGE_RESOURCE, language);
524        
525        // site name - Store.YES, Index.NOT_ANALYZED
526        document.addField(SITE_NAME, page.getSiteName());
527        
528        // Added for Solr.
529        // Page site map name - Store.YES, Index.NOT_ANALYZED
530        document.addField(SITEMAP_NAME, page.getSitemapName());
531        
532        // Need the id of the page for unindexing attachment during the unindexing of the page
533        document.addField(PAGE_ID, page.getId());
534        document.addField(PAGE_ID + "_s_dv", page.getId());
535    }
536    
537    /**
538     * Index a populated solr input document of type Page.
539     * @param page the page from which the input document is created
540     * @param document the input document to add to the solr index
541     * @param workspaceName The workspace name
542     * @throws SolrServerException if there is an error on the Solr server
543     * @throws IOException if there is a communication error with the server
544     */
545    protected void _indexPageDocument(Page page, SolrInputDocument document, String workspaceName) throws SolrServerException, IOException
546    {
547        // Retrieve appropriate solr client
548        String collectionName = _solrClientProvider.getCollectionName(workspaceName);
549        SolrClient solrClient = _solrClientProvider.getUpdateClient(workspaceName);
550        
551        // Add document
552        UpdateResponse solrResponse = solrClient.add(collectionName, document);
553        int status = solrResponse.getStatus();
554        
555        if (status != 0)
556        {
557            throw new IOException("Ametys Page indexing - Expecting status code of '0' in the Solr response but got : '" + status + "'. Page id : " + page.getId());
558        }
559        
560        getLogger().debug("Successful page indexing. Page identifier : {}", page.getId());
561    }
562    
563    /**
564     * Index a populated solr input document of type Resource.
565     * @param resource the resource from which the input document is created
566     * @param document the input document
567     * @throws SolrServerException if there is an error on the server
568     * @throws IOException if there is a communication error with the server
569     */
570    protected void _indexResourceDocument(Resource resource, SolrInputDocument document) throws SolrServerException, IOException
571    {
572        // Retrieve appropriate solr client
573        Request request = ContextHelper.getRequest(_context);
574        String workspaceName = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
575        String collectionName = _solrClientProvider.getCollectionName(workspaceName);
576        SolrClient solrClient = _solrClientProvider.getUpdateClient(workspaceName);
577        
578        // Add document
579        UpdateResponse solrResponse = solrClient.add(collectionName, document);
580        int status = solrResponse.getStatus();
581        
582        if (status != 0)
583        {
584            throw new IOException("Ametys Page indexing - Expecting status code of '0' in the Solr response but got : '" + status + "'. Resource id : " + resource.getId());
585        }
586        
587        getLogger().debug("Successful resource indexing. Resource identifier : {}", resource.getId());
588    }
589    
590    /**
591     * Index the page access.
592     * @param page The page.
593     * @param document The document.
594     * @throws Exception If an error occurs while indexing.
595     */
596    protected void _indexPageAccess(Page page, SolrInputDocument document) throws Exception
597    {
598        _solrPageRightIndexer.indexPageAccess(page, document);
599    }
600    
601    ///////////////////////////////////////////////////////////////////////////
602    
603    /**
604     * Un-index a page by its ID  for all workspaces and commit
605     * @param pageId The page ID.
606     * @param unindexRecursively also unindex child pages if requested.
607     * @param unindexAttachments also unindex page attachments
608     * @throws Exception if an error occurs during index update.
609     */
610    public void unindexPage(String pageId, boolean unindexRecursively, boolean unindexAttachments) throws Exception
611    {
612        unindexPage(pageId, RepositoryConstants.DEFAULT_WORKSPACE, unindexRecursively, unindexAttachments, true);
613        unindexPage(pageId, WebConstants.LIVE_WORKSPACE, unindexRecursively, unindexAttachments, true);
614    }
615    
616    /**
617     * De-index a page (and optionally its children pages).
618     * @param pageId the page to be de-indexed.
619     * @param workspaceName The workspace where to work in 
620     * @param unindexRecursively also unindex child pages if requested.
621     * @param unindexAttachments also unindex page attachments
622     * @param commit Commit the operator to Solr
623     * @throws Exception if an error occurs during index update.
624     */
625    public void unindexPage(String pageId, String workspaceName, boolean unindexRecursively, boolean unindexAttachments, boolean commit) throws Exception
626    {
627        Request request = ContextHelper.getRequest(_context);
628        
629        // Retrieve the current workspace.
630        String currentWsp = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
631        // Retrieve the current site name.
632        String currentSiteName = (String) request.getAttribute("siteName");
633        
634        try
635        {
636            // Force the workspace.
637            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, workspaceName);
638    
639            getLogger().debug("Unindexing page: {}", pageId);
640            
641            _unindexPageDocument(pageId, workspaceName, unindexRecursively, unindexAttachments);
642            
643            if (commit)
644            {
645                _solrIndexer.commit(workspaceName);
646            }
647        }
648        catch (Exception e)
649        {
650            String error = String.format("Failed to unindex page %s in workspace %s", pageId, workspaceName);
651            getLogger().error(error, e);
652            throw new IndexingException(error, e);
653        }
654        finally
655        {
656            // Restore the site name.
657            request.setAttribute("siteName", currentSiteName);
658            // Restore context
659            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWsp);
660        }
661    }
662    
663    /**
664     * Deindex a document of type Page. Also deindex attachments of a page
665     * @param pageId the id of the page to deindex
666     * @param workspaceName The workspace name
667     * @param unindexRecursively also unindex child pages if requested.
668     * @param unindexAttachments also unindex page attachments
669     * @throws SolrServerException if there is an error on the server
670     * @throws IOException if there is a communication error with the server
671     * @throws QuerySyntaxException if the uri query can't be built because of a syntax error.
672     */
673    protected void _unindexPageDocument(String pageId, String workspaceName, boolean unindexRecursively, boolean unindexAttachments) throws SolrServerException, IOException, QuerySyntaxException
674    {
675        // Retrieve appropriate solr client
676        String collectionName = _solrClientProvider.getCollectionName(workspaceName);
677        SolrClient solrClient = _solrClientProvider.getUpdateClient(workspaceName);
678        
679        getLogger().info("Unindexing page {} in workspace '{}'", pageId, workspaceName);
680        
681        Query pages = new AndQuery(new DocumentTypeQuery(TYPE_PAGE), new PageQuery(pageId, unindexRecursively));
682        Query query;
683        if (unindexRecursively && unindexAttachments)
684        {
685            // {!ametys join=pageId q=ancestorIds:"page://xxxx"}
686            Query joinQuery = new JoinQuery(() -> ANCESTOR_IDS + ":\"" + pageId + "\"", PAGE_ID);
687            Query attachments = new AndQuery(new DocumentTypeQuery(TYPE_PAGE_RESOURCE), new OrQuery(new AttachmentQuery(pageId), joinQuery));
688            query = new OrQuery(attachments, pages);
689        }
690        else if (unindexAttachments)
691        {
692            Query attachments = new AndQuery(new DocumentTypeQuery(TYPE_PAGE_RESOURCE), new AttachmentQuery(pageId));
693            query = new OrQuery(attachments, pages);
694        }
695        else
696        {
697            query = pages;
698        }
699        
700        // Delete by query
701        UpdateResponse solrResponse = solrClient.deleteByQuery(collectionName, query.build());
702        int status = solrResponse.getStatus();
703        
704        if (status != 0)
705        {
706            throw new IOException("Ametys Page de-indexing - Expecting status code of '0' in the Solr response but got : '" + status + "'. Page id : " + pageId);
707        }
708        
709        getLogger().debug("Successful page de-indexing{}. Page identifier : {}", unindexRecursively ? " with its children" : "", pageId);
710    }
711    
712    ///////////////////////////////////////////////////////////////////////////
713    
714    /**
715     * Reindex a page by its ID for all workspaces and commit
716     * @param pageId The page ID.
717     * @param reindexRecursively also reindex child pages if requested.
718     * @param reindexAttachments also reindex page attachments
719     * @throws Exception if an error occurs during index update.
720     */
721    public void reindexPage(String pageId, boolean reindexRecursively, boolean reindexAttachments) throws Exception
722    {
723        reindexPage(pageId, RepositoryConstants.DEFAULT_WORKSPACE, reindexRecursively, reindexAttachments, true);
724        reindexPage(pageId, WebConstants.LIVE_WORKSPACE, reindexRecursively, reindexAttachments, true);
725    }
726  
727    
728    /**
729     * Reindex a page by its ID.
730     * @param pageId The page ID.
731     * @param workspaceName The workspace where to work in 
732     * @param reindexRecursively also reindex child pages if requested.
733     * @param reindexAttachments also reindex page attachments
734     * @param commit Commit the operator to Solr
735     * @throws Exception if an error occurs during index update.
736     */
737    public void reindexPage(String pageId, String workspaceName, boolean reindexRecursively, boolean reindexAttachments, boolean commit) throws Exception
738    {
739        Request request = ContextHelper.getRequest(_context);
740        
741        // Retrieve the current workspace.
742        String currentWsp = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
743        // Retrieve the current site name.
744        String currentSiteName = (String) request.getAttribute("siteName");
745        
746        try
747        {
748            // Force the workspace.
749            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, workspaceName);
750    
751            getLogger().debug("Reindexing page: {}", pageId);
752            
753            if (_ametysObjectResolver.hasAmetysObjectForId(pageId)) // In 'live' the page may not exist
754            {
755                Page page = _ametysObjectResolver.resolveById(pageId);
756                _unindexPageDocument(pageId, workspaceName, reindexRecursively, reindexAttachments);
757                _indexPage(page, workspaceName, reindexRecursively, reindexAttachments);
758                
759                if (commit)
760                {
761                    _solrIndexer.commit(workspaceName);
762                }
763            }
764        }
765        catch (Exception e)
766        {
767            String error = String.format("Failed to unindex page %s in workspace %s", pageId, workspaceName);
768            getLogger().error(error, e);
769            throw new IndexingException(error, e);
770        }
771        finally
772        {
773            // Restore the site name.
774            request.setAttribute("siteName", currentSiteName);
775            // Restore context
776            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWsp);
777        }
778    }
779}