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