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.requesttime.impl;
017
018import java.util.Collection;
019import java.util.Collections;
020import java.util.HashMap;
021import java.util.List;
022import java.util.Map;
023import java.util.Optional;
024import java.util.stream.Collectors;
025
026import org.apache.avalon.framework.parameters.Parameters;
027import org.apache.avalon.framework.service.ServiceException;
028import org.apache.avalon.framework.service.ServiceManager;
029import org.apache.avalon.framework.service.Serviceable;
030import org.apache.cocoon.xml.AttributesImpl;
031import org.apache.cocoon.xml.XMLUtils;
032import org.apache.commons.collections4.ListUtils;
033import org.apache.commons.lang3.tuple.Pair;
034import org.xml.sax.Attributes;
035import org.xml.sax.ContentHandler;
036import org.xml.sax.SAXException;
037
038import org.ametys.cms.content.ContentHelper;
039import org.ametys.cms.repository.Content;
040import org.ametys.cms.search.SearchResults;
041import org.ametys.cms.search.Sort;
042import org.ametys.cms.search.advanced.AbstractTreeNode;
043import org.ametys.cms.search.advanced.TreeLeaf;
044import org.ametys.core.util.LambdaUtils;
045import org.ametys.plugins.repository.AmetysObject;
046import org.ametys.plugins.repository.AmetysObjectResolver;
047import org.ametys.runtime.i18n.I18nizableText;
048import org.ametys.runtime.parameter.Enumerator;
049import org.ametys.runtime.plugin.component.AbstractLogEnabled;
050import org.ametys.web.frontoffice.search.instance.SearchServiceInstance;
051import org.ametys.web.frontoffice.search.instance.model.FOSearchCriterion;
052import org.ametys.web.frontoffice.search.instance.model.FOSearchCriterionMode;
053import org.ametys.web.frontoffice.search.instance.model.ResultDisplayType;
054import org.ametys.web.frontoffice.search.metamodel.FacetDefinition;
055import org.ametys.web.frontoffice.search.metamodel.FrontEnumerableSearchCriterionDefinition;
056import org.ametys.web.frontoffice.search.metamodel.SearchCriterionDefinition;
057import org.ametys.web.frontoffice.search.metamodel.SortDefinition;
058import org.ametys.web.frontoffice.search.metamodel.impl.ContentAttributeContentSearchCriterionDefinition;
059import org.ametys.web.frontoffice.search.requesttime.SearchComponent;
060import org.ametys.web.frontoffice.search.requesttime.SearchComponentArguments;
061import org.ametys.web.frontoffice.search.requesttime.input.SearchUserInputs;
062import org.ametys.web.frontoffice.search.requesttime.input.impl.FormSearchUserInputs;
063import org.ametys.web.repository.site.Site;
064
065import com.google.common.collect.ImmutableMap;
066
067/**
068 * {@link SearchComponent} for saxing form
069 */
070public class SaxFormSearchComponent extends AbstractLogEnabled implements SearchComponent, Serviceable
071{
072    private static final String __IS_MINIMAL_FORM_SAX_PARAMETER_NAME = "minimalFormSax";
073
074    /** The Ametys object resolver */
075    protected AmetysObjectResolver _resolver;
076    
077    /** The content helper */
078    protected ContentHelper _contentHelper;
079    
080    public void service(ServiceManager manager) throws ServiceException
081    {
082        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
083        _contentHelper = (ContentHelper) manager.lookup(ContentHelper.ROLE);
084    }
085    
086    @Override
087    public int priority()
088    {
089        return SEARCH_PRIORITY + 2000;
090    }
091
092    @Override
093    public boolean supports(SearchComponentArguments args)
094    {
095        if (args.generatorParameters().getParameterAsBoolean(DISABLE_DEFAULT_SAX_PARAMETER_NAME, false))
096        {
097            return false;
098        }
099        
100        ResultDisplayType resultDisplayType = args.serviceInstance().getResultDisplay().getType();
101        return resultDisplayType == ResultDisplayType.ABOVE_CRITERIA // results are displayed above criteria => we always need criteria
102                || resultDisplayType == ResultDisplayType.IN_PLACE_OF_CRITERIA // if there is a result count of 0, the XSL may choose to display criteria anyway
103                || !args.launchSearch(); // form was not submitted yet => need criteria
104    }
105
106    @Override
107    public void execute(SearchComponentArguments args) throws Exception
108    {
109        ContentHandler contentHandler = args.contentHandler();
110        SearchServiceInstance serviceInstance = args.serviceInstance();
111        List<FOSearchCriterion> nonStaticCriteria = serviceInstance.getCriterionTree()
112                .map(AbstractTreeNode::getFlatLeaves)
113                .orElseGet(Collections::emptyList)
114                .stream()
115                .map(TreeLeaf::getValue)
116                .filter(c -> !c.getMode().isStatic())
117                .collect(Collectors.toList());
118        Parameters parameters = args.generatorParameters();
119        
120        XMLUtils.startElement(contentHandler, "form");
121        
122        XMLUtils.startElement(contentHandler, "fields");
123        saxFormFields(contentHandler, parameters, nonStaticCriteria, args.currentLang(), args.currentSite(), serviceInstance.computeCriteriaCounts());
124        XMLUtils.endElement(contentHandler, "fields");
125        
126        SearchUserInputs userInputs = args.userInputs();
127        
128        Map<String, Object> userCriteria = userInputs.criteria();
129        XMLUtils.startElement(contentHandler, "values");
130        saxFormValues(contentHandler, parameters, nonStaticCriteria, userCriteria);
131        XMLUtils.endElement(contentHandler, "values");
132        
133        XMLUtils.startElement(contentHandler, "facets");
134        saxFacets(contentHandler, parameters, serviceInstance.getFacets(), userInputs.facets(), args.results(), args.currentLang());
135        XMLUtils.endElement(contentHandler, "facets");
136        
137        XMLUtils.startElement(contentHandler, "sorts");
138        saxSorts(contentHandler, parameters, serviceInstance.getInitialSorts(), serviceInstance.getProposedSorts(), userInputs.sorts());
139        XMLUtils.endElement(contentHandler, "sorts");
140        
141        XMLUtils.endElement(contentHandler, "form");
142    }
143    
144    /**
145     * Returns <code>true</code> if the Generator is parameterized with a minimal form sax
146     * @param parameters the parameters
147     * @return <code>true</code> if the Generator is parameterized with a minimal form sax
148     */
149    protected boolean isMinimalFormSax(Parameters parameters)
150    {
151        return parameters.getParameterAsBoolean(__IS_MINIMAL_FORM_SAX_PARAMETER_NAME, false);
152    }
153    
154    /**
155     * SAX the form search criteria
156     * @param contentHandler the content handler
157     * @param parameters the parameters
158     * @param nonStaticCriteria the non-{@link FOSearchCriterionMode#STATIC static} criteria
159     * @param language The current language
160     * @param currentSite The current site
161     * @param computeCriteriaCounts true if we compute criteria counts
162     * @throws Exception if an exception occurs
163     */
164    protected void saxFormFields(ContentHandler contentHandler, Parameters parameters, List<FOSearchCriterion> nonStaticCriteria, String language, Site currentSite, boolean computeCriteriaCounts) throws Exception
165    {
166        for (FOSearchCriterion criterion : nonStaticCriteria)
167        {
168            AttributesImpl attrs = new AttributesImpl();
169            attrs.addCDATAAttribute("name", FormSearchUserInputs.CRITERION_PREFIX + criterion.getId());
170            SearchCriterionDefinition criterionDef = criterion.getCriterionDefinition();
171            attrs.addCDATAAttribute("definition", criterionDef.getId());
172            attrs.addCDATAAttribute("operator", criterion.getOperator());
173            attrs.addCDATAAttribute("mandatory", String.valueOf(criterion.isMandatory()));
174            
175            XMLUtils.startElement(contentHandler, "criterion", attrs);
176            
177            criterionDef.getLabel().toSAX(contentHandler, "label");
178            if (!isMinimalFormSax(parameters))
179            {
180                saxEnumeratorEntries(contentHandler, criterion, language, currentSite, computeCriteriaCounts);
181            }
182            
183            XMLUtils.createElement(contentHandler, "type", criterionDef.getType().getId());
184            XMLUtils.endElement(contentHandler, "criterion");
185        }
186    }
187    
188    /**
189     * SAX {@link Enumerator} entries
190     * @param contentHandler the content handler
191     * @param criterion the (non-{@link FOSearchCriterionMode#STATIC static}) criterion
192     * @param language The current language
193     * @param currentSite The current site
194     * @param computeCriteriaCounts true if we compute criteria counts
195     * @throws Exception if an exception occurs
196     */
197    protected void saxEnumeratorEntries(ContentHandler contentHandler, FOSearchCriterion criterion, String language, Site currentSite, boolean computeCriteriaCounts) throws Exception
198    {
199        Map<Object, I18nizableText> enumeratorEntries = null;
200        SearchCriterionDefinition criterionDef = criterion.getCriterionDefinition();
201        if (criterion.getMode() == FOSearchCriterionMode.RESTRICTED_USER_INPUT)
202        {
203            enumeratorEntries = criterion.getRestrictedValues().get().values();
204        }
205        else if (criterionDef instanceof FrontEnumerableSearchCriterionDefinition)
206        {
207            enumeratorEntries = ((FrontEnumerableSearchCriterionDefinition) criterionDef).getEntries(language);
208        }
209        else if (criterionDef.isEnumerated())
210        {
211            enumeratorEntries = criterionDef
212                    .getEnumeratedValues(_contextualParameters(currentSite))
213                    .get()
214                    .getAllValues();
215        }
216        
217        if (enumeratorEntries == null)
218        {
219            return;
220        }
221        
222        XMLUtils.startElement(contentHandler, "enumeration");
223        enumeratorEntries
224            .entrySet()
225            .stream()
226            .filter(e -> computeCriteriaCounts || !_isArchivedCriterionValue(criterionDef, e.getKey()))
227            .sequential()
228            .forEach(LambdaUtils.wrapConsumer(e -> saxEnumeratorEntry(contentHandler, criterionDef, e)));
229        XMLUtils.endElement(contentHandler, "enumeration");
230    }
231    
232    private boolean _isArchivedCriterionValue(SearchCriterionDefinition criterionDef, Object id)
233    {
234        if (criterionDef instanceof ContentAttributeContentSearchCriterionDefinition)
235        {
236            try
237            {
238                Content content = _resolver.resolveById((String) id);
239                return _contentHelper.isArchivedContent(content);
240            }
241            catch (Exception e) 
242            {
243                getLogger().warn("Can't get archived value for criterion {} and value {}", criterionDef.getId(), id, e);
244            }
245        }
246        
247        return false;
248    }
249    
250    private Map<String, Object> _contextualParameters(Site currentSite)
251    {
252        return new HashMap<>(ImmutableMap.of("siteName", currentSite.getName()));
253    }
254    
255    /**
256     * SAX {@link Enumerator} entry
257     * @param contentHandler the content handler
258     * @param criterionDef the search criterion
259     * @param enumeratorEntry the enumerator entry
260     * @throws Exception if an exception occurs
261     */
262    protected void saxEnumeratorEntry(ContentHandler contentHandler, SearchCriterionDefinition criterionDef, Map.Entry<Object, I18nizableText> enumeratorEntry) throws Exception
263    {
264        AttributesImpl attrs = new AttributesImpl();
265        attrs.addCDATAAttribute("value", String.valueOf(enumeratorEntry.getKey()));
266        
267        if (criterionDef instanceof ContentAttributeContentSearchCriterionDefinition)
268        {
269            String contentId = String.valueOf(enumeratorEntry.getKey());
270            Long order = ((ContentAttributeContentSearchCriterionDefinition) criterionDef).getOrder(contentId);
271            if (order != null)
272            {
273                attrs.addCDATAAttribute("order", String.valueOf(order));
274            }
275        }
276        
277        XMLUtils.startElement(contentHandler, "item", attrs);
278        enumeratorEntry.getValue().toSAX(contentHandler, "label");
279        XMLUtils.endElement(contentHandler, "item");
280    }
281    
282    /**
283     * SAX the form search criteria values
284     * @param contentHandler the content handler
285     * @param parameters the parameters
286     * @param nonStaticCriteria the non-{@link FOSearchCriterionMode#STATIC static} criteria
287     * @param userCriteria The user input criteria
288     * @throws SAXException if an error occurs while SAXing
289     */
290    protected void saxFormValues(ContentHandler contentHandler, Parameters parameters, List<FOSearchCriterion> nonStaticCriteria, Map<String, Object> userCriteria) throws SAXException
291    {
292        for (FOSearchCriterion criterion : nonStaticCriteria)
293        {
294            String id = criterion.getId();
295            Object userCriterion = userCriteria.get(id);
296            if (userCriterion != null)
297            {
298                AttributesImpl attrs = new AttributesImpl();
299                String name = FormSearchUserInputs.CRITERION_PREFIX + id;
300                attrs.addCDATAAttribute("name", name);
301                saxFormValue(contentHandler, attrs, userCriterion);
302            }
303        }
304    }
305    
306    /**
307     * SAX the values of a form search criterion
308     * @param contentHandler the content handler
309     * @param attrs The XML attributes
310     * @param userCriterion The user input criterion value (is a List if multiple)
311     * @throws SAXException if an error occurs while SAXing
312     */
313    protected void saxFormValue(ContentHandler contentHandler, Attributes attrs, Object userCriterion) throws SAXException
314    {
315        if (userCriterion instanceof List<?>)
316        {
317            List<?> multipleUserCriterion = (List<?>) userCriterion;
318            for (Object singleUserCriterion : multipleUserCriterion)
319            {
320                saxFormSingleValue(contentHandler, attrs, singleUserCriterion);
321            }
322        }
323        else
324        {
325            saxFormSingleValue(contentHandler, attrs, userCriterion);
326        }
327    }
328    
329    /**
330     * SAX a single value of a form search criterion
331     * @param contentHandler the content handler
332     * @param attrs The XML attributes
333     * @param singleUserCriterion The user input criterion single value
334     * @throws SAXException if an error occurs while SAXing
335     */
336    protected void saxFormSingleValue(ContentHandler contentHandler, Attributes attrs, Object singleUserCriterion) throws SAXException
337    {
338        XMLUtils.createElement(contentHandler, "criterion", attrs, String.valueOf(singleUserCriterion));
339    }
340    
341    /**
342     * SAX the facets
343     * @param contentHandler the content handler
344     * @param parameters the parameters
345     * @param facets The facets
346     * @param userFacets The user input facets
347     * @param searchResults The search results
348     * @param currentLang The current lang
349     * @throws SAXException if an error occurs while SAXing
350     */
351    protected void saxFacets(ContentHandler contentHandler, Parameters parameters, Collection<FacetDefinition> facets, Map<String, List<String>> userFacets, Optional<SearchResults<AmetysObject>> searchResults, String currentLang) throws SAXException
352    {
353        boolean formSubmitted = searchResults.isPresent();
354        Map<String, Map<String, Integer>> facetResults = Collections.EMPTY_MAP;
355        Map<String, Integer> valuesForCurrentFacetDef = Collections.EMPTY_MAP;
356        if (formSubmitted)
357        {
358            facetResults = searchResults.get().getFacetResults();
359        }
360        
361        for (FacetDefinition facet : facets)
362        {
363            String id = facet.getId();
364            List<String> selectedFacets = Optional.ofNullable(userFacets.get(id)).orElse(Collections.emptyList());
365            String name = FormSearchUserInputs.FACET_PREFIX + id;
366            AttributesImpl attrs = new AttributesImpl();
367            attrs.addCDATAAttribute("name", name);
368            if (formSubmitted)
369            {
370                if (facetResults.containsKey(id))
371                {
372                    valuesForCurrentFacetDef = facetResults.get(id);
373                }
374                attrs.addCDATAAttribute("total", String.valueOf(valuesForCurrentFacetDef.values()
375                        .stream()
376                        .mapToInt(Integer::intValue)
377                        .sum()));
378            }
379            
380            XMLUtils.startElement(contentHandler, "facet", attrs);
381            facet.getLabel().toSAX(contentHandler, "label");
382            if (formSubmitted)
383            {
384                for (String value : valuesForCurrentFacetDef.keySet())
385                {
386                    Integer count = valuesForCurrentFacetDef.get(value);
387                    AttributesImpl valueAttrs = new AttributesImpl();
388                    valueAttrs.addCDATAAttribute("value", value);
389                    valueAttrs.addCDATAAttribute("count", count.toString());
390                    valueAttrs.addCDATAAttribute("selected", String.valueOf(selectedFacets.contains(value)));
391                    
392                    XMLUtils.startElement(contentHandler, "item", valueAttrs);
393                    Optional.ofNullable(facet.getFacetLabel(value, currentLang))
394                        .ifPresent(LambdaUtils.wrapConsumer(i18n -> i18n.toSAX(contentHandler)));
395                    XMLUtils.endElement(contentHandler, "item");
396                }
397            }
398            XMLUtils.endElement(contentHandler, "facet");
399        }
400    }
401    
402    /**
403     * SAX the sorts
404     * @param contentHandler the content handler
405     * @param parameters the parameters
406     * @param initialSorts The initial sorts
407     * @param sortsToPropose The sorts to propose
408     * @param userSorts The user input sorts
409     * @throws SAXException if an error occurs while SAXing
410     */
411    protected void saxSorts(ContentHandler contentHandler, Parameters parameters, List<Pair<SortDefinition, Sort.Order>> initialSorts, Collection<SortDefinition> sortsToPropose, List<Pair<String, Sort.Order>> userSorts) throws SAXException
412    {
413        Map<String, Pair<Integer, Sort.Order>> initialSortMap = new HashMap<>();
414        for (int i = 0; i < initialSorts.size(); i++)
415        {
416            Pair<SortDefinition, Sort.Order> initialSort = initialSorts.get(i);
417            initialSortMap.put(initialSort.getLeft().getId(), Pair.of(i, initialSort.getRight()));
418        }
419        
420        Map<String, Sort.Order> userSortMap = userSorts.stream()
421                .collect(Collectors.toMap(Pair::getLeft, Pair::getRight));
422        
423        for (SortDefinition sort : sortsToPropose)
424        {
425            String id = sort.getId();
426            AttributesImpl attrs = new AttributesImpl();
427            String name = FormSearchUserInputs.SORT_PREFIX + id;
428            if (userSortMap.containsKey(id))
429            {
430                // selected by the user
431                int number = ListUtils.indexOf(userSorts, pair -> pair.getLeft().equals(id));
432                _addAttributesForSelectedSort(attrs, userSortMap.get(id), number);
433            }
434            else if (userSortMap.isEmpty() && initialSortMap.containsKey(id))
435            {
436                // initial sort
437                Pair<Integer, Sort.Order> numberAndDirection = initialSortMap.get(id);
438                _addAttributesForSelectedSort(attrs, numberAndDirection.getRight(), numberAndDirection.getLeft());
439            }
440            
441            attrs.addCDATAAttribute("name", name);
442            XMLUtils.startElement(contentHandler, "sort", attrs);
443            sort.getLabel().toSAX(contentHandler, "label");
444            for (Sort.Order order : sort.orders())
445            {
446                AttributesImpl valueAttrs = new AttributesImpl();
447                valueAttrs.addCDATAAttribute("value", order.name());
448                XMLUtils.createElement(contentHandler, "item", valueAttrs);
449            }
450            XMLUtils.endElement(contentHandler, "sort");
451        }
452    }
453    
454    private void _addAttributesForSelectedSort(AttributesImpl attrs, Sort.Order direction, int number)
455    {
456        attrs.addCDATAAttribute("selected", Boolean.toString(true));
457        attrs.addCDATAAttribute("direction", direction.name());
458        attrs.addCDATAAttribute("number", Integer.toString(number));
459    }
460}