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        DefaultConfiguration confWithAdditionalParameters = new DefaultConfiguration(parametersConfiguration);
197        
198        Configuration[] groups = confWithAdditionalParameters.getChildren("group");
199        Map<String, ParsedAdditionalParameterConf> parsedAdditionalParameterConfs = new LinkedHashMap<>();
200        
201        // first group 'General'
202        DefaultConfiguration firstGroupConf = (DefaultConfiguration) groups[0];
203        parsedAdditionalParameterConfs.putAll(_parseAndInjectGeneralAdditionalParameters(firstGroupConf));
204        
205        // last group 'Display'
206        DefaultConfiguration lastGroupConf = (DefaultConfiguration) groups[groups.length - 1];
207        parsedAdditionalParameterConfs.putAll(_parseAndInjectDisplayAdditionalParameters(lastGroupConf));
208        
209        // configure of superclass
210        super.configureParameters(confWithAdditionalParameters);
211
212        // parameters are ready => save the additional ones
213        Collection<AdditionalSearchServiceParameter> additionalParameters = _buildAdditionalSearchServiceParameters(parsedAdditionalParameterConfs);
214        _creationHelper.setAdditionalParameters(additionalParameters);
215        
216        // Default value of returnables
217        String defaultValue = StringUtils.join(_creationHelper.selectedReturnables(), ',');
218        @SuppressWarnings("unchecked")
219        ServiceParameter<String> paramReturnables = (ServiceParameter<String>) _modelItems.get(PARAM_NAME_RETURNABLES);
220        paramReturnables.setDefaultValue(defaultValue);
221        
222        // Criteria, facets, initial sorts and proposed sorts need widget parameters
223        List<String> fieldsReloadingWidget = _fieldsReloadingWidget(additionalParameters);
224        _setWidgetParameters(PARAM_NAME_CRITERIA, fieldsReloadingWidget);
225        _setWidgetParameters(PARAM_NAME_FACETS, fieldsReloadingWidget);
226        _setWidgetParameters(PARAM_NAME_INITIAL_SORTS, fieldsReloadingWidget);
227        _setWidgetParameters(PARAM_NAME_PROPOSED_SORTS, fieldsReloadingWidget);
228    }
229    
230    static final class ParsedAdditionalParameterConf
231    {
232        String _name;
233        boolean _reload;
234        
235        private ParsedAdditionalParameterConf(String name, boolean reload)
236        {
237            _name = name;
238            _reload = reload;
239        }
240        
241        static ParsedAdditionalParameterConf _fromConf(Configuration c)
242        {
243            String name = c.getAttribute("name", null);
244            boolean reload = c.getAttributeAsBoolean("reloadCriteriaOnChange", false);
245            return new ParsedAdditionalParameterConf(name, reload);
246        }
247        
248        String name()
249        {
250            return _name;
251        }
252    }
253    
254    /**
255     * Retrieve the configurations of additional parameters for 'General' group, inject them into the group configuration and return their parsed representation.
256     * @param firstGroupConf The first group configuration ('General') for injecting.
257     * @return the {@link ParsedAdditionalParameterConf}s
258     */
259    protected Map<String, ParsedAdditionalParameterConf> _parseAndInjectGeneralAdditionalParameters(DefaultConfiguration firstGroupConf)
260    {
261        List<Configuration> additionalParameterConfigurations = _creationHelper.getAdditionalParameterConfigurationsForGeneral()
262                .stream()
263                .sorted(Comparator.comparingLong(c -> c.getAttributeAsLong("order", Long.MAX_VALUE)))
264                .collect(Collectors.toList());
265        
266        // add additional confs to the 'General' group conf
267        additionalParameterConfigurations.forEach(firstGroupConf::addChild);
268        
269        return additionalParameterConfigurations
270                .stream()
271                .map(ParsedAdditionalParameterConf::_fromConf)
272                .collect(LambdaUtils.Collectors.toLinkedHashMap(
273                    ParsedAdditionalParameterConf::name, 
274                    Function.identity()));
275    }
276    
277    /**
278     * Retrieve the configurations of additional parameters for 'Display' group, inject them into the group configuration and return their parsed representation.
279     * @param lastGroupConf The last group configuration ('Display') for injecting.
280     * @return the {@link ParsedAdditionalParameterConf}s
281     */
282    protected Map<String, ParsedAdditionalParameterConf> _parseAndInjectDisplayAdditionalParameters(DefaultConfiguration lastGroupConf)
283    {
284        List<Configuration> additionalParameterConfigurations = _creationHelper.getAdditionalParameterConfigurationsForDisplay()
285                .stream()
286                .sorted(Comparator.comparingLong(c -> c.getAttributeAsLong("order", Long.MAX_VALUE)))
287                .collect(Collectors.toList());
288        
289        // add additional confs to the 'Display' group conf
290        additionalParameterConfigurations.forEach(lastGroupConf::addChild);
291        
292        return additionalParameterConfigurations
293                .stream()
294                .map(ParsedAdditionalParameterConf::_fromConf)
295                .collect(LambdaUtils.Collectors.toLinkedHashMap(
296                    ParsedAdditionalParameterConf::name, 
297                    Function.identity()));
298    }
299    
300    /**
301     * Build the {@link AdditionalSearchServiceParameter}s from intermediate {@link ParsedAdditionalParameterConf} objects.
302     * @param parsedAdditionalParameterConfs The parsed configurations for each additional parameter
303     * @return the {@link AdditionalSearchServiceParameter}s
304     */
305    protected Collection<AdditionalSearchServiceParameter> _buildAdditionalSearchServiceParameters(Map<String, ParsedAdditionalParameterConf> parsedAdditionalParameterConfs)
306    {
307        List<String> additionalParameterNames = parsedAdditionalParameterConfs.keySet()
308                .stream()
309                .collect(Collectors.toList());
310        
311        return _modelItems.entrySet()
312                .stream()
313                .filter(p -> additionalParameterNames.contains(p.getKey()))
314                .map(Map.Entry::getValue)
315                .filter(ServiceParameter.class::isInstance)
316                .map(p -> 
317                {
318                    @SuppressWarnings("unchecked")
319                    ServiceParameter<Object> typedParam = (ServiceParameter<Object>) p;
320                    String paramName = typedParam.getName();
321                    ParsedAdditionalParameterConf parsedConf = parsedAdditionalParameterConfs.get(paramName);
322                    return new AdditionalSearchServiceParameter<>(typedParam, parsedConf._reload);
323                })
324                .collect(Collectors.toList());
325    }
326    
327    /**
328     * Set the widget parameters of the given parameter, by adding the list of the fields reloading the widget
329     * @param parameterName The parameter name
330     * @param fieldsReloadingCriteriaWidget the names of the fields which will launch a reload of the widget when their value change
331     */
332    protected void _setWidgetParameters(String parameterName, List<String> fieldsReloadingCriteriaWidget)
333    {
334        @SuppressWarnings("unchecked")
335        ServiceParameter<String> parameter = (ServiceParameter<String>) _modelItems.get(parameterName);
336        Map<String, I18nizableText> params = new HashMap<>();
337        Optional.ofNullable(parameter.getWidgetParameters())
338                .ifPresent(params::putAll);
339        params.put("fieldsReloadingWidget", new I18nizableText(StringUtils.join(fieldsReloadingCriteriaWidget, ",")));
340        parameter.setWidgetParameters(params);
341    }
342    
343    /**
344     * Gets the names of the fields which will launch a reload of this widget when their value change
345     * @param additionalParameters The additional parameters
346     * @return the names of the fields which will launch a reload of this widget when their value change
347     */
348    protected List<String> _fieldsReloadingWidget(Collection<AdditionalSearchServiceParameter> additionalParameters)
349    {
350        return additionalParameters
351                .stream()
352                .filter(AdditionalSearchServiceParameter::reloadCriteriaOnChange)
353                .map(AdditionalSearchServiceParameter::getParameter)
354                .map(ServiceParameter::getName)
355                .collect(Collectors.toList());
356    }
357}