001/*
002 *  Copyright 2018 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.frontoffice.search.metamodel.impl;
017
018import java.util.ArrayList;
019import java.util.Arrays;
020import java.util.Collection;
021import java.util.List;
022import java.util.Objects;
023import java.util.Optional;
024
025import org.apache.avalon.framework.activity.Disposable;
026import org.apache.avalon.framework.activity.Initializable;
027import org.apache.avalon.framework.configuration.Configurable;
028import org.apache.avalon.framework.configuration.Configuration;
029import org.apache.avalon.framework.configuration.ConfigurationException;
030import org.apache.avalon.framework.configuration.DefaultConfiguration;
031import org.apache.avalon.framework.context.Context;
032import org.apache.avalon.framework.context.ContextException;
033import org.apache.avalon.framework.context.Contextualizable;
034import org.apache.avalon.framework.service.ServiceException;
035import org.apache.avalon.framework.service.ServiceManager;
036import org.apache.avalon.framework.service.Serviceable;
037
038import org.ametys.cms.content.indexing.solr.SolrFieldNames;
039import org.ametys.cms.contenttype.MetadataType;
040import org.ametys.cms.languages.LanguageEnumerator;
041import org.ametys.cms.search.query.OrQuery;
042import org.ametys.cms.search.query.Query;
043import org.ametys.cms.search.query.QuerySyntaxException;
044import org.ametys.cms.search.query.SolrNativeJoinQuery;
045import org.ametys.plugins.explorer.dublincore.DublinCoreMetadataProvider;
046import org.ametys.plugins.explorer.resources.Resource;
047import org.ametys.runtime.i18n.I18nizableText;
048import org.ametys.runtime.parameter.Enumerator;
049import org.ametys.runtime.plugin.component.AbstractLogEnabled;
050import org.ametys.runtime.plugin.component.PluginAware;
051import org.ametys.runtime.plugin.component.ThreadSafeComponentManager;
052import org.ametys.web.frontoffice.search.metamodel.AdditionalParameterValueMap;
053import org.ametys.web.frontoffice.search.metamodel.Returnable;
054import org.ametys.web.frontoffice.search.metamodel.ReturnableExtensionPoint;
055import org.ametys.web.frontoffice.search.metamodel.SearchCriterionDefinition;
056import org.ametys.web.frontoffice.search.metamodel.Searchable;
057import org.ametys.web.indexing.solr.SolrWebFieldNames;
058import org.ametys.web.search.query.PageContentQuery;
059
060/**
061 * {@link Searchable} for {@link Resource}s
062 */
063public class ResourceSearchable extends AbstractLogEnabled implements Searchable, Serviceable, PluginAware, Contextualizable, Initializable, Disposable, Configurable
064{
065    /** The prefix for the ids of criterion definitions */
066    protected static final String __CRITERION_DEFINITIONS_PREFIX_ID = "ResourceSearchable$";
067    
068    /** The label */
069    protected I18nizableText _label;
070    /** The criteria position */
071    protected int _criteriaPosition;
072    
073    private ServiceManager _manager;
074    private Returnable _resourceReturnable;
075    private Returnable _pageReturnable;
076    private Returnable _contentReturnable;
077    private String _pluginName;
078    private Context _context;
079
080    private DublinCoreMetadataProvider _dcProvider;
081    private ThreadSafeComponentManager<Enumerator> _enumeratorManager;
082    private LanguageEnumerator _sitemapEnumerator;
083
084    @Override
085    public void service(ServiceManager manager) throws ServiceException
086    {
087        ReturnableExtensionPoint resultTypeEP = (ReturnableExtensionPoint) manager.lookup(ReturnableExtensionPoint.ROLE);
088        _resourceReturnable = resultTypeEP.getExtension(ResourceReturnable.ROLE);
089        _pageReturnable = resultTypeEP.getExtension(PageReturnable.ROLE);
090        _contentReturnable = resultTypeEP.getExtension(ContentReturnable.ROLE);
091        
092        _manager = manager;
093        _dcProvider = (DublinCoreMetadataProvider) manager.lookup(DublinCoreMetadataProvider.ROLE);
094    }
095    
096    @Override
097    public void configure(Configuration configuration) throws ConfigurationException
098    {
099        _label = I18nizableText.parseI18nizableText(configuration.getChild("label"), "plugin.web");
100        _criteriaPosition = configuration.getChild("criteriaPosition").getValueAsInteger();
101    }
102    
103    @Override
104    public void setPluginInfo(String pluginName, String featureName, String id)
105    {
106        _pluginName = pluginName;
107    }
108    
109    @Override
110    public void contextualize(Context context) throws ContextException
111    {
112        _context = context;
113    }
114    
115    @Override
116    public void initialize() throws Exception
117    {
118        _enumeratorManager = new ThreadSafeComponentManager<>();
119        _enumeratorManager.setLogger(getLogger());
120        _enumeratorManager.contextualize(_context);
121        _enumeratorManager.service(_manager);
122        
123        String sitemapRole = "sitemap";
124        _enumeratorManager.addComponent(_pluginName, null, sitemapRole, LanguageEnumerator.class, new DefaultConfiguration("enumerator"));
125        _enumeratorManager.initialize();
126        _sitemapEnumerator = (LanguageEnumerator) _enumeratorManager.lookup(sitemapRole);
127        Objects.nonNull(_sitemapEnumerator);
128    }
129    
130    @Override
131    public void dispose()
132    {
133        _enumeratorManager.dispose();
134        _enumeratorManager = null;
135    }
136    
137    @Override
138    public I18nizableText getLabel()
139    {
140        return _label;
141    }
142
143    @Override
144    public Collection<SearchCriterionDefinition> getCriteria(AdditionalParameterValueMap additionalParameterValues)
145    {
146        Collection<SearchCriterionDefinition> criteria = new ArrayList<>();
147        String catalogue = "plugin.web";
148        
149        // Dublin-Core criteria
150        criteria.add(new ResourceFormatSearchCriterionDefinition(__CRITERION_DEFINITIONS_PREFIX_ID + "format", _pluginName, new I18nizableText(catalogue, "PLUGINS_WEB_SERVICE_SEARCH_SEARCHABLE_RESOURCE_CRITERION_FORMAT"), Optional.of(this)));
151        criteria.add(new DublinCoreSearchCriterionDefinition(
152                __CRITERION_DEFINITIONS_PREFIX_ID + "creator",
153                _pluginName,
154                new I18nizableText(catalogue, "PLUGINS_WEB_SERVICE_SEARCH_SEARCHABLE_RESOURCE_CRITERION_CREATOR"),
155                MetadataType.STRING,
156                Optional.empty(),
157                Optional.of(this),
158                "creator"));
159        criteria.add(new DublinCoreSearchCriterionDefinition(
160                __CRITERION_DEFINITIONS_PREFIX_ID + "publisher",
161                _pluginName,
162                new I18nizableText(catalogue, "PLUGINS_WEB_SERVICE_SEARCH_SEARCHABLE_RESOURCE_CRITERION_PUBLISHER"),
163                MetadataType.STRING,
164                Optional.ofNullable(_dcProvider.getEntries("dc_publisher")),
165                Optional.of(this),
166                "publisher"));
167        criteria.add(new DublinCoreSearchCriterionDefinition(
168                __CRITERION_DEFINITIONS_PREFIX_ID + "rights",
169                _pluginName,
170                new I18nizableText(catalogue, "PLUGINS_WEB_SERVICE_SEARCH_SEARCHABLE_RESOURCE_CRITERION_RIGHTS"),
171                MetadataType.STRING,
172                Optional.ofNullable(_dcProvider.getEntries("dc_rights")),
173                Optional.of(this),
174                "rights"));
175        
176        // Sitemap criteria
177        criteria.add(new SitemapSearchCriterionDefinition(__CRITERION_DEFINITIONS_PREFIX_ID + "sitemap", _pluginName, new I18nizableText(catalogue, "PLUGINS_WEB_SERVICE_SEARCH_SEARCHABLE_RESOURCE_CRITERION_SITEMAP"), _sitemapEnumerator, Optional.of(this)));
178        
179        return criteria;
180    }
181    
182    @Override
183    public int criteriaPosition()
184    {
185        return _criteriaPosition;
186    }
187
188    @Override
189    public Optional<Query> joinQuery(Query queryOnCriterion, SearchCriterionDefinition criterion, Collection<Returnable> returnables, AdditionalParameterValueMap additionalParameters)
190    {
191        List<Query> queries = new ArrayList<>();
192        
193        boolean isSubQueryNonEmpty = _isSubQueryNonEmpty(queryOnCriterion);
194        
195        if (returnables.contains(_resourceReturnable))
196        {
197            queries.add(queryOnCriterion);
198        }
199        if (returnables.contains(_pageReturnable))
200        {
201            // join query on visible page attachments
202            if (isSubQueryNonEmpty)
203            {
204                queries.add(_queryOnPage(queryOnCriterion));
205                queries.add(_queryOnPageThroughContents(queryOnCriterion));
206            }
207        }
208        if (returnables.contains(_contentReturnable))
209        {
210            // join query on visible content attachments
211            if (isSubQueryNonEmpty)
212            {
213                queries.add(_queryOnContent(queryOnCriterion));
214            }
215        }
216        
217        return queries.size() > 0 ? Optional.of(new OrQuery(queries)) : Optional.empty();
218    }
219    
220    private boolean _isSubQueryNonEmpty(Query queryOnCriterion)
221    {
222        try
223        {
224            return !queryOnCriterion.build().isEmpty();
225        }
226        catch (QuerySyntaxException e)
227        {
228            // Silently ignore, queryOnCriterion will be built later anyway
229        }
230        return false;
231    }
232    
233    private Query _queryOnContent(Query queryOnVisibleContentAttachments)
234    {
235        return new SolrNativeJoinQuery(SolrFieldNames.RESOURCE_ANCESTOR_AND_SELF_IDS, SolrFieldNames.CONTENT_VISIBLE_ATTACHMENT_RESOURCE_IDS, queryOnVisibleContentAttachments);
236    }
237    private Query _queryOnPage(Query queryOnVisiblePageAttachments)
238    {
239        return new SolrNativeJoinQuery(SolrFieldNames.RESOURCE_ANCESTOR_AND_SELF_IDS, SolrWebFieldNames.PAGE_VISIBLE_ATTACHMENT_RESOURCE_IDS, queryOnVisiblePageAttachments);
240    }
241    private Query _queryOnPageThroughContents(Query queryOnVisibleContentAttachments)
242    {
243        return new PageContentQuery(_queryOnContent(queryOnVisibleContentAttachments));
244    }
245    
246    @Override
247    public Collection<Returnable> relationsWith()
248    {
249        return Arrays.asList(_resourceReturnable, _pageReturnable, _contentReturnable);
250    }
251}