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