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, 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 enumeratorEntry the enumerator entry
259     * @throws Exception if an exception occurs
260     */
261    protected void saxEnumeratorEntry(ContentHandler contentHandler, Map.Entry<Object, I18nizableText> enumeratorEntry) throws Exception
262    {
263        AttributesImpl attrs = new AttributesImpl();
264        attrs.addCDATAAttribute("value", String.valueOf(enumeratorEntry.getKey()));
265        XMLUtils.startElement(contentHandler, "item", attrs);
266        enumeratorEntry.getValue().toSAX(contentHandler, "label");
267        XMLUtils.endElement(contentHandler, "item");
268    }
269    
270    /**
271     * SAX the form search criteria values
272     * @param contentHandler the content handler
273     * @param parameters the parameters
274     * @param nonStaticCriteria the non-{@link FOSearchCriterionMode#STATIC static} criteria
275     * @param userCriteria The user input criteria
276     * @throws SAXException if an error occurs while SAXing
277     */
278    protected void saxFormValues(ContentHandler contentHandler, Parameters parameters, List<FOSearchCriterion> nonStaticCriteria, Map<String, Object> userCriteria) throws SAXException
279    {
280        for (FOSearchCriterion criterion : nonStaticCriteria)
281        {
282            String id = criterion.getId();
283            Object userCriterion = userCriteria.get(id);
284            if (userCriterion != null)
285            {
286                AttributesImpl attrs = new AttributesImpl();
287                String name = FormSearchUserInputs.CRITERION_PREFIX + id;
288                attrs.addCDATAAttribute("name", name);
289                saxFormValue(contentHandler, attrs, userCriterion);
290            }
291        }
292    }
293    
294    /**
295     * SAX the values of a form search criterion
296     * @param contentHandler the content handler
297     * @param attrs The XML attributes
298     * @param userCriterion The user input criterion value (is a List if multiple)
299     * @throws SAXException if an error occurs while SAXing
300     */
301    protected void saxFormValue(ContentHandler contentHandler, Attributes attrs, Object userCriterion) throws SAXException
302    {
303        if (userCriterion instanceof List<?>)
304        {
305            List<?> multipleUserCriterion = (List<?>) userCriterion;
306            for (Object singleUserCriterion : multipleUserCriterion)
307            {
308                saxFormSingleValue(contentHandler, attrs, singleUserCriterion);
309            }
310        }
311        else
312        {
313            saxFormSingleValue(contentHandler, attrs, userCriterion);
314        }
315    }
316    
317    /**
318     * SAX a single value of a form search criterion
319     * @param contentHandler the content handler
320     * @param attrs The XML attributes
321     * @param singleUserCriterion The user input criterion single value
322     * @throws SAXException if an error occurs while SAXing
323     */
324    protected void saxFormSingleValue(ContentHandler contentHandler, Attributes attrs, Object singleUserCriterion) throws SAXException
325    {
326        XMLUtils.createElement(contentHandler, "criterion", attrs, String.valueOf(singleUserCriterion));
327    }
328    
329    /**
330     * SAX the facets
331     * @param contentHandler the content handler
332     * @param parameters the parameters
333     * @param facets The facets
334     * @param userFacets The user input facets
335     * @param searchResults The search results
336     * @param currentLang The current lang
337     * @throws SAXException if an error occurs while SAXing
338     */
339    protected void saxFacets(ContentHandler contentHandler, Parameters parameters, Collection<FacetDefinition> facets, Map<String, List<String>> userFacets, Optional<SearchResults<AmetysObject>> searchResults, String currentLang) throws SAXException
340    {
341        boolean formSubmitted = searchResults.isPresent();
342        Map<String, Map<String, Integer>> facetResults = Collections.EMPTY_MAP;
343        Map<String, Integer> valuesForCurrentFacetDef = Collections.EMPTY_MAP;
344        if (formSubmitted)
345        {
346            facetResults = searchResults.get().getFacetResults();
347        }
348        
349        for (FacetDefinition facet : facets)
350        {
351            String id = facet.getId();
352            List<String> selectedFacets = Optional.ofNullable(userFacets.get(id)).orElse(Collections.emptyList());
353            String name = FormSearchUserInputs.FACET_PREFIX + id;
354            AttributesImpl attrs = new AttributesImpl();
355            attrs.addCDATAAttribute("name", name);
356            if (formSubmitted)
357            {
358                if (facetResults.containsKey(id))
359                {
360                    valuesForCurrentFacetDef = facetResults.get(id);
361                }
362                attrs.addCDATAAttribute("total", String.valueOf(valuesForCurrentFacetDef.values()
363                        .stream()
364                        .mapToInt(Integer::intValue)
365                        .sum()));
366            }
367            
368            XMLUtils.startElement(contentHandler, "facet", attrs);
369            facet.getLabel().toSAX(contentHandler, "label");
370            if (formSubmitted)
371            {
372                for (String value : valuesForCurrentFacetDef.keySet())
373                {
374                    Integer count = valuesForCurrentFacetDef.get(value);
375                    AttributesImpl valueAttrs = new AttributesImpl();
376                    valueAttrs.addCDATAAttribute("value", value);
377                    valueAttrs.addCDATAAttribute("count", count.toString());
378                    valueAttrs.addCDATAAttribute("selected", String.valueOf(selectedFacets.contains(value)));
379                    
380                    XMLUtils.startElement(contentHandler, "item", valueAttrs);
381                    Optional.ofNullable(facet.getFacetLabel(value, currentLang))
382                        .ifPresent(LambdaUtils.wrapConsumer(i18n -> i18n.toSAX(contentHandler)));
383                    XMLUtils.endElement(contentHandler, "item");
384                }
385            }
386            XMLUtils.endElement(contentHandler, "facet");
387        }
388    }
389    
390    /**
391     * SAX the sorts
392     * @param contentHandler the content handler
393     * @param parameters the parameters
394     * @param initialSorts The initial sorts
395     * @param sortsToPropose The sorts to propose
396     * @param userSorts The user input sorts
397     * @throws SAXException if an error occurs while SAXing
398     */
399    protected void saxSorts(ContentHandler contentHandler, Parameters parameters, List<Pair<SortDefinition, Sort.Order>> initialSorts, Collection<SortDefinition> sortsToPropose, List<Pair<String, Sort.Order>> userSorts) throws SAXException
400    {
401        Map<String, Pair<Integer, Sort.Order>> initialSortMap = new HashMap<>();
402        for (int i = 0; i < initialSorts.size(); i++)
403        {
404            Pair<SortDefinition, Sort.Order> initialSort = initialSorts.get(i);
405            initialSortMap.put(initialSort.getLeft().getId(), Pair.of(i, initialSort.getRight()));
406        }
407        
408        Map<String, Sort.Order> userSortMap = userSorts.stream()
409                .collect(Collectors.toMap(Pair::getLeft, Pair::getRight));
410        
411        for (SortDefinition sort : sortsToPropose)
412        {
413            String id = sort.getId();
414            AttributesImpl attrs = new AttributesImpl();
415            String name = FormSearchUserInputs.SORT_PREFIX + id;
416            if (userSortMap.containsKey(id))
417            {
418                // selected by the user
419                int number = ListUtils.indexOf(userSorts, pair -> pair.getLeft().equals(id));
420                _addAttributesForSelectedSort(attrs, userSortMap.get(id), number);
421            }
422            else if (userSortMap.isEmpty() && initialSortMap.containsKey(id))
423            {
424                // initial sort
425                Pair<Integer, Sort.Order> numberAndDirection = initialSortMap.get(id);
426                _addAttributesForSelectedSort(attrs, numberAndDirection.getRight(), numberAndDirection.getLeft());
427            }
428            
429            attrs.addCDATAAttribute("name", name);
430            XMLUtils.startElement(contentHandler, "sort", attrs);
431            sort.getLabel().toSAX(contentHandler, "label");
432            for (Sort.Order order : sort.orders())
433            {
434                AttributesImpl valueAttrs = new AttributesImpl();
435                valueAttrs.addCDATAAttribute("value", order.name());
436                XMLUtils.createElement(contentHandler, "item", valueAttrs);
437            }
438            XMLUtils.endElement(contentHandler, "sort");
439        }
440    }
441    
442    private void _addAttributesForSelectedSort(AttributesImpl attrs, Sort.Order direction, int number)
443    {
444        attrs.addCDATAAttribute("selected", Boolean.toString(true));
445        attrs.addCDATAAttribute("direction", direction.name());
446        attrs.addCDATAAttribute("number", Integer.toString(number));
447    }
448}