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