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