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;
017
018import java.util.Collection;
019import java.util.Collections;
020import java.util.Comparator;
021import java.util.HashMap;
022import java.util.LinkedHashMap;
023import java.util.List;
024import java.util.Map;
025import java.util.Optional;
026import java.util.function.Function;
027import java.util.stream.Collectors;
028
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.service.ServiceException;
033import org.apache.avalon.framework.service.ServiceManager;
034import org.apache.cocoon.components.ContextHelper;
035import org.apache.cocoon.environment.Request;
036import org.apache.commons.lang3.StringUtils;
037
038import org.ametys.cms.search.advanced.AbstractTreeNode;
039import org.ametys.cms.search.advanced.TreeLeaf;
040import org.ametys.core.util.LambdaUtils;
041import org.ametys.runtime.i18n.I18nizableText;
042import org.ametys.web.frontoffice.search.instance.SearchServiceInstance;
043import org.ametys.web.frontoffice.search.instance.SearchServiceInstanceManager;
044import org.ametys.web.frontoffice.search.instance.model.FOSearchCriterion;
045import org.ametys.web.frontoffice.search.instance.model.FOSearchCriterionMode;
046import org.ametys.web.frontoffice.search.instance.model.ResultDisplay;
047import org.ametys.web.frontoffice.search.instance.model.ResultDisplayType;
048import org.ametys.web.frontoffice.search.instance.model.RightCheckingMode;
049import org.ametys.web.frontoffice.search.metamodel.AdditionalSearchServiceParameter;
050import org.ametys.web.frontoffice.search.metamodel.FacetDefinition;
051import org.ametys.web.frontoffice.search.metamodel.SearchServiceCreationHelper;
052import org.ametys.web.frontoffice.search.metamodel.SortDefinition;
053import org.ametys.web.frontoffice.search.requesttime.SearchServiceDebugModeHelper;
054import org.ametys.web.renderingcontext.RenderingContextHandler;
055import org.ametys.web.repository.page.Page;
056import org.ametys.web.repository.page.ZoneItem;
057import org.ametys.web.service.ServiceParameter;
058import org.ametys.web.service.StaticService;
059
060/**
061 * Front search service.
062 */
063public class SearchService extends StaticService
064{
065    /** Avalon Role */
066    public static final String ROLE = "org.ametys.web.service.SearchService";
067    
068    /** The parameter name for header */
069    public static final String PARAM_NAME_HEADER = "header";
070    /** The parameter name for returnables */
071    public static final String PARAM_NAME_RETURNABLES = "returnables";
072    /** The parameter name for contexts */
073    public static final String PARAM_NAME_CONTEXTS = "contexts";
074    /** The parameter name for criteria */
075    public static final String PARAM_NAME_CRITERIA = "criteria";
076    /** The parameter name for computing counts */
077    public static final String PARAM_NAME_COMPUTE_COUNTS = "computeCounts";
078    /** The parameter name for facets */
079    public static final String PARAM_NAME_FACETS = "facets";
080    /** The parameter name for initial sorts */
081    public static final String PARAM_NAME_INITIAL_SORTS = "initialSorts";
082    /** The parameter name for proposed sorts */
083    public static final String PARAM_NAME_PROPOSED_SORTS = "proposedSorts";
084    /** The parameter name for number of results per page */
085    public static final String PARAM_NAME_RESULTS_PER_PAGE = "resultsPerPage";
086    /** The parameter name for maximum number of results */
087    public static final String PARAM_NAME_MAX_RESULTS = "maxResults";
088    /** The parameter name for right checking mode */
089    public static final String PARAM_NAME_RIGHT_CHECKING_MODE = "rightCheckingMode";
090    /** The parameter name for XSLT */
091    public static final String PARAM_NAME_XSLT = "xslt";
092    /** The parameter name for result place */
093    public static final String PARAM_NAME_RESULT_PLACE = "resultPlace";
094    /** The parameter name for result page */
095    public static final String PARAM_NAME_RESULT_PAGE = "resultPage";
096    /** The parameter name for launching search at startup */
097    public static final String PARAM_NAME_LAUNCH_SEARCH_AT_STARTUP = "launchSearchAtStartup";
098    /** The parameter name for service group id */
099    public static final String PARAM_NAME_SERVICE_GROUP_ID = "serviceGroupId";
100    /** The parameter name for link page */
101    public static final String PARAM_NAME_LINK_PAGE = "link";
102    /** The parameter name for link title */
103    public static final String PARAM_NAME_LINK_TITLE = "linkTitle";
104    /** The parameter name for handling RSS */
105    public static final String PARAM_NAME_RSS = "rss";
106    /** The parameter name for saving user preferences */
107    public static final String PARAM_NAME_SAVE_USER_PREFS = "saveUserPrefs";
108    
109    /** The helper component for defining this service */
110    protected SearchServiceCreationHelper _creationHelper;
111    /** The handler of rendering context */
112    protected RenderingContextHandler _renderingContextHandler;
113    /** The manager for {@link SearchServiceInstance} */
114    protected SearchServiceInstanceManager _searchServiceInstanceManager;
115
116    @Override
117    public void service(ServiceManager smanager) throws ServiceException
118    {
119        super.service(smanager);
120        _creationHelper = (SearchServiceCreationHelper) smanager.lookup(SearchServiceCreationHelper.ROLE);
121        _renderingContextHandler = (RenderingContextHandler) smanager.lookup(RenderingContextHandler.ROLE);
122        _searchServiceInstanceManager = (SearchServiceInstanceManager) smanager.lookup(SearchServiceInstanceManager.ROLE);
123    }
124    
125    /**
126     * Returns <code>true</code> if the given instance of search service has some user input
127     * @param criterionTree The tree of criteria of the search service instance
128     * @param facets The facets of the search service instance
129     * @param proposedSorts The proposed sorts of the search service instance
130     * @return <code>true</code> if the given instance of search service has some user input
131     */
132    public static boolean hasUserInput(Optional<AbstractTreeNode<FOSearchCriterion>> criterionTree, Collection<FacetDefinition> facets, Collection<SortDefinition> proposedSorts)
133    {
134        boolean hasUserCriteria = hasUserCriteria(criterionTree);
135        
136        boolean hasUserFacet = !facets.isEmpty();
137        
138        boolean hasUserSort = !proposedSorts.isEmpty();
139        
140        return hasUserCriteria || hasUserFacet || hasUserSort;
141    }
142    
143    /**
144     * Returns <code>true</code> if the given instance of search service has at least one user criterion
145     * @param criterionTree The tree of criteria of the search service instance
146     * @return <code>true</code> if the given instance of search service has at least one user criterion
147     */
148    public static boolean hasUserCriteria(Optional<AbstractTreeNode<FOSearchCriterion>> criterionTree)
149    {
150        return criterionTree
151                .map(AbstractTreeNode::getFlatLeaves)
152                .orElse(Collections.emptySet())
153                .stream()
154                .map(TreeLeaf::getValue)
155                .anyMatch(crit -> crit.getMode() != FOSearchCriterionMode.STATIC);
156    }
157    
158    private static boolean _hasUserInput(SearchServiceInstance serviceInstance)
159    {
160        return hasUserInput(serviceInstance.getCriterionTree(), serviceInstance.getFacets(), serviceInstance.getProposedSorts());
161    }
162    
163    @Override
164    public boolean isCacheable(Page currentPage, ZoneItem zoneItem)
165    {
166        boolean isDebug = _isDebug(ContextHelper.getRequest(_context), _renderingContextHandler);
167        SearchServiceInstance serviceInstance = _searchServiceInstanceManager.get(zoneItem.getId());
168        ResultDisplay resultDisplay = serviceInstance.getResultDisplay();
169        if (!isDebug && resultDisplay.getType() == ResultDisplayType.ON_PAGE)
170        {
171            String resultPageId = resultDisplay
172                    .resultPage()
173                    .map(Page::getId)
174                    .orElseThrow(() -> new IllegalArgumentException("Error. resultPage() cannot return empty at this time."));
175            // Cacheable if results are on ANOTHER PAGE
176            return !resultPageId.equals(currentPage.getId());
177        }
178        
179        if (!isDebug)
180        {
181            // Cacheable if no user input and right checking mode is on 'FAST'
182            return !_hasUserInput(serviceInstance) && serviceInstance.getRightCheckingMode() == RightCheckingMode.FAST;
183        }
184        
185        return false;
186    }
187    
188    private boolean _isDebug(Request request, RenderingContextHandler renderingContextHandler)
189    {
190        return SearchServiceDebugModeHelper.debugMode(request, renderingContextHandler) != null;
191    }
192    
193    @Override
194    protected void configureParameters(Configuration parametersConfiguration) throws ConfigurationException
195    {
196        _ensureCreationHelperIsReady();
197        
198        DefaultConfiguration confWithAdditionalParameters = new DefaultConfiguration(parametersConfiguration);
199        
200        Configuration[] groups = confWithAdditionalParameters.getChildren("group");
201        Map<String, ParsedAdditionalParameterConf> parsedAdditionalParameterConfs = new LinkedHashMap<>();
202        
203        // first group 'General'
204        DefaultConfiguration firstGroupConf = (DefaultConfiguration) groups[0];
205        parsedAdditionalParameterConfs.putAll(_parseAndInjectGeneralAdditionalParameters(firstGroupConf));
206        
207        // last group 'Display'
208        DefaultConfiguration lastGroupConf = (DefaultConfiguration) groups[groups.length - 1];
209        parsedAdditionalParameterConfs.putAll(_parseAndInjectDisplayAdditionalParameters(lastGroupConf));
210        
211        // configure of superclass
212        super.configureParameters(confWithAdditionalParameters);
213
214        // parameters are ready => save the additional ones
215        Collection<AdditionalSearchServiceParameter> additionalParameters = _buildAdditionalSearchServiceParameters(parsedAdditionalParameterConfs);
216        _creationHelper.setAdditionalParameters(additionalParameters);
217        
218        // Default value of returnables
219        String defaultValue = StringUtils.join(_creationHelper.selectedReturnables(), ',');
220        @SuppressWarnings("unchecked")
221        ServiceParameter<String> paramReturnables = (ServiceParameter<String>) _modelItems.get(PARAM_NAME_RETURNABLES);
222        paramReturnables.setDefaultValue(defaultValue);
223        
224        // Criteria, facets, initial sorts and proposed sorts need widget parameters
225        List<String> fieldsReloadingWidget = _fieldsReloadingWidget(additionalParameters);
226        _setWidgetParameters(PARAM_NAME_CRITERIA, fieldsReloadingWidget);
227        _setWidgetParameters(PARAM_NAME_FACETS, fieldsReloadingWidget);
228        _setWidgetParameters(PARAM_NAME_INITIAL_SORTS, fieldsReloadingWidget);
229        _setWidgetParameters(PARAM_NAME_PROPOSED_SORTS, fieldsReloadingWidget);
230    }
231    
232    private void _ensureCreationHelperIsReady() throws ConfigurationException
233    {
234        if (!_creationHelper.isReady())
235        {
236            try
237            {
238                _creationHelper.initialize();
239            }
240            catch (Exception e)
241            {
242                throw new ConfigurationException("An error occured when initializing " + SearchServiceCreationHelper.ROLE, e);
243            }
244        }
245    }
246    
247    static final class ParsedAdditionalParameterConf
248    {
249        String _name;
250        boolean _reload;
251        
252        private ParsedAdditionalParameterConf(String name, boolean reload)
253        {
254            _name = name;
255            _reload = reload;
256        }
257        
258        static ParsedAdditionalParameterConf _fromConf(Configuration c)
259        {
260            String name = c.getAttribute("name", null);
261            boolean reload = c.getAttributeAsBoolean("reloadCriteriaOnChange", false);
262            return new ParsedAdditionalParameterConf(name, reload);
263        }
264        
265        String name()
266        {
267            return _name;
268        }
269    }
270    
271    /**
272     * Retrieve the configurations of additional parameters for 'General' group, inject them into the group configuration and return their parsed representation.
273     * @param firstGroupConf The first group configuration ('General') for injecting.
274     * @return the {@link ParsedAdditionalParameterConf}s
275     */
276    protected Map<String, ParsedAdditionalParameterConf> _parseAndInjectGeneralAdditionalParameters(DefaultConfiguration firstGroupConf)
277    {
278        List<Configuration> additionalParameterConfigurations = _creationHelper.getAdditionalParameterConfigurationsForGeneral()
279                .stream()
280                .sorted(Comparator.comparingLong(c -> c.getAttributeAsLong("order", Long.MAX_VALUE)))
281                .collect(Collectors.toList());
282        
283        // add additional confs to the 'General' group conf
284        additionalParameterConfigurations.forEach(firstGroupConf::addChild);
285        
286        return additionalParameterConfigurations
287                .stream()
288                .map(ParsedAdditionalParameterConf::_fromConf)
289                .collect(LambdaUtils.Collectors.toLinkedHashMap(
290                    ParsedAdditionalParameterConf::name, 
291                    Function.identity()));
292    }
293    
294    /**
295     * Retrieve the configurations of additional parameters for 'Display' group, inject them into the group configuration and return their parsed representation.
296     * @param lastGroupConf The last group configuration ('Display') for injecting.
297     * @return the {@link ParsedAdditionalParameterConf}s
298     */
299    protected Map<String, ParsedAdditionalParameterConf> _parseAndInjectDisplayAdditionalParameters(DefaultConfiguration lastGroupConf)
300    {
301        List<Configuration> additionalParameterConfigurations = _creationHelper.getAdditionalParameterConfigurationsForDisplay()
302                .stream()
303                .sorted(Comparator.comparingLong(c -> c.getAttributeAsLong("order", Long.MAX_VALUE)))
304                .collect(Collectors.toList());
305        
306        // add additional confs to the 'Display' group conf
307        additionalParameterConfigurations.forEach(lastGroupConf::addChild);
308        
309        return additionalParameterConfigurations
310                .stream()
311                .map(ParsedAdditionalParameterConf::_fromConf)
312                .collect(LambdaUtils.Collectors.toLinkedHashMap(
313                    ParsedAdditionalParameterConf::name, 
314                    Function.identity()));
315    }
316    
317    /**
318     * Build the {@link AdditionalSearchServiceParameter}s from intermediate {@link ParsedAdditionalParameterConf} objects.
319     * @param parsedAdditionalParameterConfs The parsed configurations for each additional parameter
320     * @return the {@link AdditionalSearchServiceParameter}s
321     */
322    protected Collection<AdditionalSearchServiceParameter> _buildAdditionalSearchServiceParameters(Map<String, ParsedAdditionalParameterConf> parsedAdditionalParameterConfs)
323    {
324        List<String> additionalParameterNames = parsedAdditionalParameterConfs.keySet()
325                .stream()
326                .collect(Collectors.toList());
327        
328        return _modelItems.entrySet()
329                .stream()
330                .filter(p -> additionalParameterNames.contains(p.getKey()))
331                .map(Map.Entry::getValue)
332                .filter(ServiceParameter.class::isInstance)
333                .map(p -> 
334                {
335                    @SuppressWarnings("unchecked")
336                    ServiceParameter<Object> typedParam = (ServiceParameter<Object>) p;
337                    String paramName = typedParam.getName();
338                    ParsedAdditionalParameterConf parsedConf = parsedAdditionalParameterConfs.get(paramName);
339                    return new AdditionalSearchServiceParameter<>(typedParam, parsedConf._reload);
340                })
341                .collect(Collectors.toList());
342    }
343    
344    /**
345     * Set the widget parameters of the given parameter, by adding the list of the fields reloading the widget
346     * @param parameterName The parameter name
347     * @param fieldsReloadingCriteriaWidget the names of the fields which will launch a reload of the widget when their value change
348     */
349    protected void _setWidgetParameters(String parameterName, List<String> fieldsReloadingCriteriaWidget)
350    {
351        @SuppressWarnings("unchecked")
352        ServiceParameter<String> parameter = (ServiceParameter<String>) _modelItems.get(parameterName);
353        Map<String, I18nizableText> params = new HashMap<>();
354        Optional.ofNullable(parameter.getWidgetParameters())
355                .ifPresent(params::putAll);
356        params.put("fieldsReloadingWidget", new I18nizableText(StringUtils.join(fieldsReloadingCriteriaWidget, ",")));
357        parameter.setWidgetParameters(params);
358    }
359    
360    /**
361     * Gets the names of the fields which will launch a reload of this widget when their value change
362     * @param additionalParameters The additional parameters
363     * @return the names of the fields which will launch a reload of this widget when their value change
364     */
365    protected List<String> _fieldsReloadingWidget(Collection<AdditionalSearchServiceParameter> additionalParameters)
366    {
367        return additionalParameters
368                .stream()
369                .filter(AdditionalSearchServiceParameter::reloadCriteriaOnChange)
370                .map(AdditionalSearchServiceParameter::getParameter)
371                .map(ServiceParameter::getName)
372                .collect(Collectors.toList());
373    }
374}