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        return instance;
167    }
168    
169    private SearchContext _createSearchContext(Map<String, Object> config)
170    {
171        Pair<List<String>, Boolean> tags = _getTags(config.get("tags"));
172        
173        SitemapContext sitemapContext = _createSitemapContext(config.get("search-sitemap-context"));
174        return new SearchContext(
175                _createSiteContext(config.get("sites")),
176                sitemapContext,
177                _getContextLang(config.get("context-lang"), sitemapContext),
178                tags.getLeft(),
179                tags.getRight()
180        );
181    }
182    
183    @SuppressWarnings("unchecked")
184    private SiteContext _createSiteContext(Object sitesObj)
185    {
186        Map<String, Object> sitesAsMap = _json.convertJsonToMap((String) sitesObj);
187        SiteContextType siteContextType = SiteContextType.fromClientSideName((String) sitesAsMap.get("context"));
188        List<String> sites = null;
189        if (siteContextType == SiteContextType.AMONG)
190        {
191            sites = (List<String>) sitesAsMap.get("sites");
192        }
193        
194        return new SiteContext(siteContextType, sites, _siteManager);
195    }
196    
197    @SuppressWarnings("unchecked")
198    private SitemapContext _createSitemapContext(Object sitemapObj)
199    {
200        Map<String, Object> sitemapAsMap =  _json.convertJsonToMap((String) sitemapObj);
201        String sitemapContextTypeStr = (String) sitemapAsMap.get("context");
202        SitemapContextType sitemapContextType = sitemapContextTypeStr == null ? SitemapContextType.CURRENT_SITE : SitemapContextType.valueOf(sitemapContextTypeStr);
203        List<String> pageList = null;
204        if (sitemapContextType == SitemapContextType.CHILD_PAGES_OF || sitemapContextType == SitemapContextType.DIRECT_CHILD_PAGES_OF)
205        {
206            Object pagesObj = sitemapAsMap.get("page");
207            if (pagesObj instanceof String)
208            {
209                pageList = Collections.singletonList((String) pagesObj);
210            }
211            else
212            {
213                pageList = (List<String>) pagesObj;
214            }
215        }
216        
217        return new SitemapContext(sitemapContextType, pageList, _resolver);
218    }
219    
220    private ContextLang _getContextLang(Object langObj, SitemapContext sitemapContext)
221    {
222        if (sitemapContext.getType() == SitemapContextType.CURRENT_SITE)
223        {
224            return ContextLang.valueOf((String) langObj);
225        }
226        else
227        {
228            return ContextLang.ALL;
229        }
230    }
231    
232    @SuppressWarnings("unchecked")
233    private Pair<List<String>, Boolean> _getTags(Object tagsObj)
234    {
235        List<String> tagIds;
236        boolean autoposting = false;
237        if (tagsObj instanceof Map<?, ?>)
238        {
239            Map<String, Object> tagsAsMap = (Map<String, Object>) tagsObj;
240            tagIds = (List<String>) tagsAsMap.get("value");
241            autoposting = (Boolean) tagsAsMap.get("autoposting");
242        }
243        else
244        {
245            tagIds = (List<String>) tagsObj;
246        }
247        
248        return Pair.of(tagIds, autoposting);
249    }
250    
251    private AbstractTreeNode<FOSearchCriterion> _createCriterionTree(Map<String, Object> criteriaValues, Map<String, SearchCriterionDefinition> searchCriterionDefinitions, Incrementor incrementor, Map<String, Object> contextualParameters)
252    {
253        Function<ClientSideCriterionWrapper, FOSearchCriterion> leafValueMaker = crit -> _createSearchCriterion(crit, searchCriterionDefinitions, incrementor, contextualParameters);
254        return _treeMaker.create(criteriaValues, leafValueMaker);
255    }
256    
257    private FOSearchCriterion _createSearchCriterion(ClientSideCriterionWrapper critWrapper, Map<String, SearchCriterionDefinition> searchCriterionDefinitions, Incrementor incrementor, Map<String, Object> contextualParameters)
258    {
259        String criterionDefId = critWrapper.getId();
260        SearchCriterionDefinition criterionDefinition = searchCriterionDefinitions.get(criterionDefId);
261        Objects.requireNonNull(criterionDefinition, String.format("The SearchCriterionDefinition for id '%s' must be non null", criterionDefId));
262        
263        // Generate an id
264        incrementor.increment();
265        String id = criterionDefId + "$" + incrementor.getCount();
266        
267        Map<String, Object> otherProperties = critWrapper.getOtherProperties();
268        String mode = (String) otherProperties.get("mode");
269        
270        @SuppressWarnings("unchecked")
271        RestrictedValues restrictedValues = "RESTRICTED_USER_INPUT".equals(mode) ? _restrictedValues((List<Object>) otherProperties.get("restrictedValues"), criterionDefinition, contextualParameters) : null;
272        Object staticValue = "STATIC".equals(mode) ? critWrapper.getValue() : null;
273        
274        return new FOSearchCriterion(
275                id,
276                criterionDefinition,
277                critWrapper.getStringOperator(),
278                FOSearchCriterionMode.valueOf(mode),
279                restrictedValues,
280                staticValue
281        );
282    }
283    
284    private RestrictedValues _restrictedValues(List<Object> values, SearchCriterionDefinition criterionDefinition, Map<String, Object> contextualParameters)
285    {
286        return criterionDefinition.getEnumeratedValues(contextualParameters)
287                .map(enumeratedValues -> enumeratedValues.getRestrictedValuesFor(values))
288                .orElseThrow(() -> new IllegalStateException("An unexpected error occured. There must be restricted values at this point."));
289    }
290    
291    private Map<String, Object> _contextualParameters(Site currentSite)
292    {
293        return new HashMap<>(ImmutableMap.of("siteName", currentSite.getName()));
294    }
295    
296    private Collection<FacetDefinition> _createFacetDefinitions(ModelAwareDataHolder serviceParameters, Map<String, FacetDefinition> availableFacets)
297    {
298        String[] facetStrs = serviceParameters.getValue(SearchService.PARAM_NAME_FACETS, false, new String[0]);
299        return Stream.of(facetStrs)
300                .map(availableFacets::get)
301                .filter(Objects::nonNull)
302                .collect(Collectors.toList());
303    }
304    
305    private List<Pair<SortDefinition, Sort.Order>> _createInitialSorts(ModelAwareDataHolder serviceParameters, Map<String, SortDefinition> availableSorts)
306    {
307        String[] initialSortIds = serviceParameters.getValue(SearchService.PARAM_NAME_INITIAL_SORTS, false, new String[0]);
308        return Stream.of(initialSortIds)
309                .map(_json::convertJsonToMap)
310                .map(json -> _initialSort(json, availableSorts))
311                .collect(Collectors.toList());
312    }
313    
314    private Pair<SortDefinition, Sort.Order> _initialSort(Map<String, Object> json, Map<String, SortDefinition> availableSorts)
315    {
316        String sortDefId = (String) json.get("name");
317        Sort.Order direction = Sort.Order.valueOf((String) json.get("sort"));
318        SortDefinition sortDef = availableSorts.get(sortDefId);
319        Objects.requireNonNull(sortDef);
320        return Pair.of(sortDef, direction);
321    }
322    
323    private Collection<SortDefinition> _createProposedSortDefinitions(ModelAwareDataHolder serviceParameters, Map<String, SortDefinition> availableSorts)
324    {
325        String[] proposedSortStrs = serviceParameters.getValue(SearchService.PARAM_NAME_PROPOSED_SORTS, false, new String[0]);
326        return Stream.of(proposedSortStrs)
327                .map(availableSorts::get)
328                .filter(Objects::nonNull)
329                .collect(Collectors.toList());
330    }
331    
332    private RightCheckingMode _rightCheckingMode(ModelAwareDataHolder serviceParameters, boolean hasUserInput)
333    {
334        String rightCheckingModeStr = serviceParameters.getValue(SearchService.PARAM_NAME_RIGHT_CHECKING_MODE);
335        RightCheckingMode rightCheckingMode = RightCheckingMode.valueOf(rightCheckingModeStr.toUpperCase());
336        if (hasUserInput && rightCheckingMode == RightCheckingMode.FAST)
337        {
338            // It cannot be cached with FAST => force to EXACT
339            return RightCheckingMode.EXACT;
340        }
341        else
342        {
343            return rightCheckingMode;
344        }
345    }
346    
347    private ResultDisplay _createResultDisplay(ModelAwareDataHolder serviceParameters, boolean hasUserCriteria)
348    {
349        ResultDisplayType type;
350        if (hasUserCriteria)
351        {
352            type = ResultDisplayType.valueOf(serviceParameters.getValue(SearchService.PARAM_NAME_RESULT_PLACE));
353        }
354        else
355        {
356            type = ResultDisplayType.ABOVE_CRITERIA;
357        }
358        
359        String pageId = null;
360        if (type == ResultDisplayType.ON_PAGE)
361        {
362            pageId = serviceParameters.getValue(SearchService.PARAM_NAME_RESULT_PAGE);
363        }
364        
365        Boolean launchSearchAtStartup = null;
366        if (!hasUserCriteria)
367        {
368            // Force to true
369            launchSearchAtStartup = true;
370        }
371        else if (type != ResultDisplayType.ON_PAGE)
372        {
373            launchSearchAtStartup = serviceParameters.getValue(SearchService.PARAM_NAME_LAUNCH_SEARCH_AT_STARTUP);
374        }
375        
376        String serviceGroupId = "";
377        if (hasUserCriteria && serviceParameters.hasValue(SearchService.PARAM_NAME_SERVICE_GROUP_ID))
378        {
379            serviceGroupId = serviceParameters.getValue(SearchService.PARAM_NAME_SERVICE_GROUP_ID);
380        }
381        
382        return new ResultDisplay(type, pageId, launchSearchAtStartup, serviceGroupId, _resolver);
383    }
384    
385    private Link _createLink(ModelAwareDataHolder serviceParameters)
386    {
387        String targetPageId = serviceParameters.getValue(SearchService.PARAM_NAME_LINK_PAGE);
388        String targetPage = StringUtils.isNotEmpty(targetPageId) ? targetPageId : null;
389        String title = serviceParameters.getValue(SearchService.PARAM_NAME_LINK_TITLE, false, "");
390        return new Link(targetPage, title, _resolver);
391    }
392}
393