001/*
002 *  Copyright 2019 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.instance;
017
018import java.util.Arrays;
019import java.util.Collection;
020import java.util.Collections;
021import java.util.HashMap;
022import java.util.List;
023import java.util.Map;
024import java.util.Objects;
025import java.util.Optional;
026import java.util.function.Function;
027import java.util.stream.Collectors;
028import java.util.stream.Stream;
029
030import org.apache.avalon.framework.component.Component;
031import org.apache.avalon.framework.service.ServiceException;
032import org.apache.avalon.framework.service.ServiceManager;
033import org.apache.avalon.framework.service.Serviceable;
034import org.apache.commons.lang3.StringUtils;
035import org.apache.commons.lang3.tuple.Pair;
036import org.apache.commons.math3.util.IntegerSequence.Incrementor;
037
038import org.ametys.cms.search.Sort;
039import org.ametys.cms.search.advanced.AbstractTreeNode;
040import org.ametys.cms.search.advanced.TreeMaker;
041import org.ametys.cms.search.advanced.TreeMaker.ClientSideCriterionWrapper;
042import org.ametys.core.util.JSONUtils;
043import org.ametys.plugins.repository.AmetysObjectResolver;
044import org.ametys.plugins.repository.data.holder.ModelAwareDataHolder;
045import org.ametys.web.frontoffice.search.SearchService;
046import org.ametys.web.frontoffice.search.instance.model.ContextLang;
047import org.ametys.web.frontoffice.search.instance.model.FOSearchCriterion;
048import org.ametys.web.frontoffice.search.instance.model.FOSearchCriterionMode;
049import org.ametys.web.frontoffice.search.instance.model.Link;
050import org.ametys.web.frontoffice.search.instance.model.ResultDisplay;
051import org.ametys.web.frontoffice.search.instance.model.ResultDisplayType;
052import org.ametys.web.frontoffice.search.instance.model.RightCheckingMode;
053import org.ametys.web.frontoffice.search.instance.model.SearchContext;
054import org.ametys.web.frontoffice.search.instance.model.SiteContext;
055import org.ametys.web.frontoffice.search.instance.model.SiteContextType;
056import org.ametys.web.frontoffice.search.instance.model.SitemapContext;
057import org.ametys.web.frontoffice.search.instance.model.SitemapContextType;
058import org.ametys.web.frontoffice.search.metamodel.AdditionalParameterValueMap;
059import org.ametys.web.frontoffice.search.metamodel.AdditionalSearchServiceParameter;
060import org.ametys.web.frontoffice.search.metamodel.EnumeratedValues.RestrictedValues;
061import org.ametys.web.frontoffice.search.metamodel.FacetDefinition;
062import org.ametys.web.frontoffice.search.metamodel.Returnable;
063import org.ametys.web.frontoffice.search.metamodel.SearchCriterionDefinition;
064import org.ametys.web.frontoffice.search.metamodel.SearchServiceCreationHelper;
065import org.ametys.web.frontoffice.search.metamodel.Searchable;
066import org.ametys.web.frontoffice.search.metamodel.SortDefinition;
067import org.ametys.web.repository.page.ZoneItem;
068import org.ametys.web.repository.site.Site;
069import org.ametys.web.repository.site.SiteManager;
070
071/**
072 * The component able to {@link #createSearchServiceInstance create} some {@link SearchServiceInstance}s.
073 */
074public class SearchServiceInstanceFactory implements Component, Serviceable
075{
076    /** Avalon Role */
077    public static final String ROLE = SearchServiceInstanceFactory.class.getName();
078    
079    private SearchServiceCreationHelper _serviceCreationHelper;
080    private AmetysObjectResolver _resolver;
081    private JSONUtils _json;
082    private SiteManager _siteManager;
083    private TreeMaker _treeMaker;
084    
085    @Override
086    public void service(ServiceManager manager) throws ServiceException
087    {
088        _serviceCreationHelper = (SearchServiceCreationHelper) manager.lookup(SearchServiceCreationHelper.ROLE);
089        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
090        _json = (JSONUtils) manager.lookup(JSONUtils.ROLE);
091        _siteManager = (SiteManager) manager.lookup(SiteManager.ROLE);
092        _treeMaker = (TreeMaker) manager.lookup(TreeMaker.ROLE);
093    }
094    
095    /**
096     * Creates a new {@link SearchServiceInstance}
097     * @param zoneItemId the id of the {@link ZoneItem}
098     * @return the created {@link SearchServiceInstance} which is placed at the given {@link ZoneItem}
099     */
100    public SearchServiceInstance createSearchServiceInstance(String zoneItemId)
101    {
102        ZoneItem zoneItem = _resolver.resolveById(zoneItemId);
103        ModelAwareDataHolder serviceParameters = zoneItem.getServiceParameters();
104        
105        List<Returnable> returnables = _serviceCreationHelper.getReturnables(Arrays.asList(serviceParameters.getValue(SearchService.PARAM_NAME_RETURNABLES)));
106        Collection<AdditionalSearchServiceParameter> additionalParameters = _serviceCreationHelper.getAdditionalParameters();
107        AdditionalParameterValueMap additionalParameterValues = _serviceCreationHelper.getAdditionalParameterValues(additionalParameters, serviceParameters);
108        
109        String[] contextIds = serviceParameters.getValue(SearchService.PARAM_NAME_CONTEXTS, false, new String[0]);
110        Collection<SearchContext> contexts = Stream.of(contextIds)
111                .map(_json::convertJsonToMap)
112                .map(this::_createSearchContext)
113                .collect(Collectors.toList());
114        
115        Collection<Searchable> searchables = _serviceCreationHelper.getSearchables(returnables);
116        Map<String, SearchCriterionDefinition> searchCriterionDefinitions = _serviceCreationHelper.getCriterionDefinitions(searchables, additionalParameterValues);
117        Incrementor incrementor = Incrementor.create()
118                .withStart(0)
119                .withMaximalCount(Integer.MAX_VALUE);
120        String treeStr = serviceParameters.getValue(SearchService.PARAM_NAME_CRITERIA);
121        Site site = zoneItem.getZone().getSitemapElement().getSite();
122        Map<String, Object> contextualParameters = _contextualParameters(contexts, site);
123        AbstractTreeNode<FOSearchCriterion> criterionTree = _createCriterionTree(_json.convertJsonToMap(treeStr), searchCriterionDefinitions, incrementor, contextualParameters);
124        
125        Map<String, FacetDefinition> availableFacets = _serviceCreationHelper.getFacetDefinitions(returnables, additionalParameterValues);
126        Collection<FacetDefinition> facets = _createFacetDefinitions(serviceParameters, availableFacets);
127        
128        Map<String, SortDefinition> availableSorts = _serviceCreationHelper.getSortDefinitions(returnables, additionalParameterValues);
129        List<Pair<SortDefinition, Sort.Order>> initialSorts = _createInitialSorts(serviceParameters, availableSorts);
130        Collection<SortDefinition> proposedSorts = _createProposedSortDefinitions(serviceParameters, availableSorts);
131        
132        Long resultsPerPage = serviceParameters.getValue(SearchService.PARAM_NAME_RESULTS_PER_PAGE);
133        Long maxResults  = serviceParameters.getValue(SearchService.PARAM_NAME_MAX_RESULTS);
134        
135        boolean hasUserCriteria = SearchService.hasUserCriteria(Optional.ofNullable(criterionTree));
136        boolean hasUserInput = SearchService.hasUserInput(Optional.ofNullable(criterionTree), facets, proposedSorts);
137        
138        RightCheckingMode rightCheckingMode = _rightCheckingMode(serviceParameters, hasUserInput);
139        
140        ResultDisplay resultDisplay = _createResultDisplay(serviceParameters, hasUserCriteria);
141        Link link = _createLink(serviceParameters);
142        
143        SearchServiceInstance instance = new SearchServiceInstance(
144                zoneItemId,
145                serviceParameters.getValue(SearchService.PARAM_NAME_HEADER, false, ""), 
146                returnables, 
147                searchables, 
148                additionalParameters, 
149                additionalParameterValues, 
150                contexts, 
151                criterionTree,
152                serviceParameters.getValue(SearchService.PARAM_NAME_COMPUTE_COUNTS, false, false),
153                facets,
154                initialSorts,
155                proposedSorts,
156                resultsPerPage == null ? null : resultsPerPage.intValue(),
157                maxResults == null ? null : maxResults.intValue(),
158                rightCheckingMode,
159                serviceParameters.getValue(SearchService.PARAM_NAME_XSLT),
160                resultDisplay,
161                link,
162                serviceParameters.getValue(SearchService.PARAM_NAME_RSS),
163                serviceParameters.getValue(SearchService.PARAM_NAME_SAVE_USER_PREFS, true, false)
164        );
165        return instance;
166    }
167    
168    private SearchContext _createSearchContext(Map<String, Object> config)
169    {
170        Pair<List<String>, Boolean> tags = _getTags(config.get("tags"));
171        
172        SitemapContext sitemapContext = _createSitemapContext(config.get("search-sitemap-context"));
173        return new SearchContext(
174                _createSiteContext(config.get("sites")),
175                sitemapContext,
176                _getContextLang(config.get("context-lang"), sitemapContext),
177                tags.getLeft(),
178                tags.getRight()
179        );
180    }
181    
182    @SuppressWarnings("unchecked")
183    private SiteContext _createSiteContext(Object sitesObj)
184    {
185        Map<String, Object> sitesAsMap = _json.convertJsonToMap((String) sitesObj);
186        SiteContextType siteContextType = SiteContextType.fromClientSideName((String) sitesAsMap.get("context"));
187        List<String> sites = null;
188        if (siteContextType == SiteContextType.AMONG)
189        {
190            sites = (List<String>) sitesAsMap.get("sites");
191        }
192        
193        return new SiteContext(siteContextType, sites, _siteManager);
194    }
195    
196    @SuppressWarnings("unchecked")
197    private SitemapContext _createSitemapContext(Object sitemapObj)
198    {
199        Map<String, Object> sitemapAsMap =  _json.convertJsonToMap((String) sitemapObj);
200        String sitemapContextTypeStr = (String) sitemapAsMap.get("context");
201        SitemapContextType sitemapContextType = sitemapContextTypeStr == null ? SitemapContextType.CURRENT_SITE : SitemapContextType.valueOf(sitemapContextTypeStr);
202        List<String> pageList = null;
203        if (sitemapContextType == SitemapContextType.CHILD_PAGES_OF || sitemapContextType == SitemapContextType.DIRECT_CHILD_PAGES_OF)
204        {
205            Object pagesObj = sitemapAsMap.get("page");
206            if (pagesObj instanceof String)
207            {
208                pageList = Collections.singletonList((String) pagesObj);
209            }
210            else
211            {
212                pageList = (List<String>) pagesObj;
213            }
214        }
215        
216        return new SitemapContext(sitemapContextType, pageList, _resolver);
217    }
218    
219    private ContextLang _getContextLang(Object langObj, SitemapContext sitemapContext)
220    {
221        if (sitemapContext.getType() == SitemapContextType.CURRENT_SITE)
222        {
223            return ContextLang.valueOf((String) langObj);
224        }
225        else
226        {
227            return ContextLang.ALL;
228        }
229    }
230    
231    @SuppressWarnings("unchecked")
232    private Pair<List<String>, Boolean> _getTags(Object tagsObj)
233    {
234        List<String> tagIds;
235        boolean autoposting = false;
236        if (tagsObj instanceof Map<?, ?>)
237        {
238            Map<String, Object> tagsAsMap = (Map<String, Object>) tagsObj;
239            tagIds = (List<String>) tagsAsMap.get("value");
240            autoposting = (Boolean) tagsAsMap.get("autoposting");
241        }
242        else
243        {
244            tagIds = (List<String>) tagsObj;
245        }
246        
247        return Pair.of(tagIds, autoposting);
248    }
249    
250    private AbstractTreeNode<FOSearchCriterion> _createCriterionTree(Map<String, Object> criteriaValues, Map<String, SearchCriterionDefinition> searchCriterionDefinitions, Incrementor incrementor, Map<String, Object> contextualParameters)
251    {
252        Function<ClientSideCriterionWrapper, FOSearchCriterion> leafValueMaker = crit -> _createSearchCriterion(crit, searchCriterionDefinitions, incrementor, contextualParameters);
253        return _treeMaker.create(criteriaValues, leafValueMaker);
254    }
255    
256    private FOSearchCriterion _createSearchCriterion(ClientSideCriterionWrapper critWrapper, Map<String, SearchCriterionDefinition> searchCriterionDefinitions, Incrementor incrementor, Map<String, Object> contextualParameters)
257    {
258        String criterionDefId = critWrapper.getId();
259        SearchCriterionDefinition criterionDefinition = searchCriterionDefinitions.get(criterionDefId);
260        Objects.requireNonNull(criterionDefinition, String.format("The SearchCriterionDefinition for id '%s' must be non null", criterionDefId));
261        
262        // Generate an id
263        incrementor.increment();
264        String id = criterionDefId + "$" + incrementor.getCount();
265        
266        Map<String, Object> otherProperties = critWrapper.getOtherProperties();
267        String mode = (String) otherProperties.get("mode");
268        
269        @SuppressWarnings("unchecked")
270        RestrictedValues restrictedValues = "RESTRICTED_USER_INPUT".equals(mode) ? _restrictedValues((List<Object>) otherProperties.get("restrictedValues"), criterionDefinition, contextualParameters) : null;
271        Object staticValue = "STATIC".equals(mode) ? critWrapper.getValue() : null;
272        
273        return new FOSearchCriterion(
274                id,
275                criterionDefinition,
276                critWrapper.getStringOperator(),
277                FOSearchCriterionMode.valueOf(mode),
278                restrictedValues,
279                staticValue
280        );
281    }
282    
283    private RestrictedValues _restrictedValues(List<Object> values, SearchCriterionDefinition criterionDefinition, Map<String, Object> contextualParameters)
284    {
285        return criterionDefinition.getEnumeratedValues(contextualParameters)
286                .map(enumeratedValues -> enumeratedValues.getRestrictedValuesFor(values))
287                .orElseThrow(() -> new IllegalStateException("An unexpected error occured. There must be restricted values at this point."));
288    }
289    
290    private Map<String, Object> _contextualParameters(Collection<SearchContext> searchContexts, Site currentSite)
291    {
292        Map<String, Object> contextualParameters = new HashMap<>();
293        
294        contextualParameters.put("searchContexts", searchContexts);
295        contextualParameters.put("siteName", currentSite.getName());
296        
297        return contextualParameters;
298    }
299    
300    private Collection<FacetDefinition> _createFacetDefinitions(ModelAwareDataHolder serviceParameters, Map<String, FacetDefinition> availableFacets)
301    {
302        if (serviceParameters.hasDefinition(SearchService.PARAM_NAME_FACETS))
303        {
304            String[] facetStrs = serviceParameters.getValue(SearchService.PARAM_NAME_FACETS, false, new String[0]);
305            return Stream.of(facetStrs)
306                    .map(availableFacets::get)
307                    .filter(Objects::nonNull)
308                    .collect(Collectors.toList());
309        }
310        return Collections.EMPTY_LIST;
311        
312    }
313    
314    private List<Pair<SortDefinition, Sort.Order>> _createInitialSorts(ModelAwareDataHolder serviceParameters, Map<String, SortDefinition> availableSorts)
315    {
316        if (serviceParameters.hasDefinition(SearchService.PARAM_NAME_INITIAL_SORTS))
317        {
318            String[] initialSortIds = serviceParameters.getValue(SearchService.PARAM_NAME_INITIAL_SORTS, false, new String[0]);
319            return Stream.of(initialSortIds)
320                    .map(_json::convertJsonToMap)
321                    .map(json -> _initialSort(json, availableSorts))
322                    .collect(Collectors.toList());
323        }
324        return Collections.EMPTY_LIST;
325    }
326    
327    private Pair<SortDefinition, Sort.Order> _initialSort(Map<String, Object> json, Map<String, SortDefinition> availableSorts)
328    {
329        String sortDefId = (String) json.get("name");
330        Sort.Order direction = Sort.Order.valueOf((String) json.get("sort"));
331        SortDefinition sortDef = availableSorts.get(sortDefId);
332        Objects.requireNonNull(sortDef);
333        return Pair.of(sortDef, direction);
334    }
335    
336    private Collection<SortDefinition> _createProposedSortDefinitions(ModelAwareDataHolder serviceParameters, Map<String, SortDefinition> availableSorts)
337    {
338        if (serviceParameters.hasDefinition(SearchService.PARAM_NAME_PROPOSED_SORTS))
339        {
340            String[] proposedSortStrs = serviceParameters.getValue(SearchService.PARAM_NAME_PROPOSED_SORTS, false, new String[0]);
341            return Stream.of(proposedSortStrs)
342                    .map(availableSorts::get)
343                    .filter(Objects::nonNull)
344                    .collect(Collectors.toList());
345        }
346        return Collections.EMPTY_LIST;
347    }
348    
349    private RightCheckingMode _rightCheckingMode(ModelAwareDataHolder serviceParameters, boolean hasUserInput)
350    {
351        String rightCheckingModeStr = serviceParameters.getValue(SearchService.PARAM_NAME_RIGHT_CHECKING_MODE);
352        RightCheckingMode rightCheckingMode = RightCheckingMode.valueOf(rightCheckingModeStr.toUpperCase());
353        if (hasUserInput && rightCheckingMode == RightCheckingMode.FAST)
354        {
355            // It cannot be cached with FAST => force to EXACT
356            return RightCheckingMode.EXACT;
357        }
358        else
359        {
360            return rightCheckingMode;
361        }
362    }
363    
364    private ResultDisplay _createResultDisplay(ModelAwareDataHolder serviceParameters, boolean hasUserCriteria)
365    {
366        ResultDisplayType type;
367        if (hasUserCriteria)
368        {
369            type = ResultDisplayType.valueOf(serviceParameters.getValue(SearchService.PARAM_NAME_RESULT_PLACE));
370        }
371        else
372        {
373            type = ResultDisplayType.ABOVE_CRITERIA;
374        }
375        
376        String pageId = null;
377        if (type == ResultDisplayType.ON_PAGE)
378        {
379            pageId = serviceParameters.getValue(SearchService.PARAM_NAME_RESULT_PAGE);
380        }
381        
382        Boolean launchSearchAtStartup = null;
383        if (!hasUserCriteria)
384        {
385            // Force to true
386            launchSearchAtStartup = true;
387        }
388        else if (type != ResultDisplayType.ON_PAGE)
389        {
390            launchSearchAtStartup = serviceParameters.getValue(SearchService.PARAM_NAME_LAUNCH_SEARCH_AT_STARTUP);
391        }
392        
393        String serviceGroupId = "";
394        if (hasUserCriteria && serviceParameters.hasValue(SearchService.PARAM_NAME_SERVICE_GROUP_ID))
395        {
396            serviceGroupId = serviceParameters.getValue(SearchService.PARAM_NAME_SERVICE_GROUP_ID);
397        }
398        
399        return new ResultDisplay(type, pageId, launchSearchAtStartup, serviceGroupId, _resolver);
400    }
401    
402    private Link _createLink(ModelAwareDataHolder serviceParameters)
403    {
404        String targetPageId = serviceParameters.getValue(SearchService.PARAM_NAME_LINK_PAGE);
405        String targetPage = StringUtils.isNotEmpty(targetPageId) ? targetPageId : null;
406        String title = serviceParameters.getValue(SearchService.PARAM_NAME_LINK_TITLE, false, "");
407        return new Link(targetPage, title, _resolver);
408    }
409}
410