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