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