001/*
002 *  Copyright 2020 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.Collections;
019import java.util.HashMap;
020import java.util.List;
021import java.util.Map;
022import java.util.Optional;
023import java.util.stream.Collectors;
024
025import org.apache.avalon.framework.parameters.Parameters;
026import org.apache.avalon.framework.service.ServiceException;
027import org.apache.avalon.framework.service.ServiceManager;
028import org.apache.avalon.framework.service.Serviceable;
029import org.apache.cocoon.xml.AttributesImpl;
030import org.apache.cocoon.xml.XMLUtils;
031import org.xml.sax.ContentHandler;
032import org.xml.sax.SAXException;
033
034import org.ametys.cms.search.SearchResults;
035import org.ametys.cms.search.advanced.AbstractTreeNode;
036import org.ametys.cms.search.advanced.TreeLeaf;
037import org.ametys.cms.search.solr.SearcherFactory;
038import org.ametys.cms.search.solr.SearcherFactory.Searcher;
039import org.ametys.core.right.AllowedUsers;
040import org.ametys.core.right.RightManager;
041import org.ametys.plugins.repository.AmetysObject;
042import org.ametys.runtime.i18n.I18nizableText;
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.RightCheckingMode;
046import org.ametys.web.frontoffice.search.metamodel.EnumeratedValues;
047import org.ametys.web.frontoffice.search.metamodel.FacetDefinition;
048import org.ametys.web.frontoffice.search.metamodel.SearchCriterionDefinition;
049import org.ametys.web.frontoffice.search.metamodel.impl.ContentFacetDefinition;
050import org.ametys.web.frontoffice.search.metamodel.impl.ContentSearchCriterionDefinition;
051import org.ametys.web.frontoffice.search.requesttime.SearchComponent;
052import org.ametys.web.frontoffice.search.requesttime.SearchComponentArguments;
053import org.ametys.web.frontoffice.search.requesttime.input.impl.FormSearchUserInputs;
054
055/**
056 * {@link SearchComponent} for saxing number of results for each values of enumerated criteria
057 */
058public class SaxEnumeratedCriteriaComponent implements SearchComponent, Serviceable
059{
060    /** The searcher factory */
061    protected SearcherFactory _searcherFactory;
062    
063    /** The helper for search component */
064    protected SearchComponentHelper _searchComponentHelper;
065    
066    /** The right manager */
067    protected RightManager _rightManager;
068    
069    public void service(ServiceManager manager) throws ServiceException
070    {
071        _searcherFactory = (SearcherFactory) manager.lookup(SearcherFactory.ROLE);
072        _searchComponentHelper = (SearchComponentHelper) manager.lookup(SearchComponentHelper.ROLE);
073        _rightManager = (RightManager) manager.lookup(RightManager.ROLE);
074    }
075    
076    @Override
077    public int priority()
078    {
079        return SEARCH_PRIORITY + 2500;
080    }
081
082    @Override
083    public boolean supports(SearchComponentArguments args)
084    {
085        return args.serviceInstance().computeCriteriaCounts();
086    }
087
088    @Override
089    public void execute(SearchComponentArguments args) throws Exception
090    {
091        ContentHandler contentHandler = args.contentHandler();
092        XMLUtils.startElement(contentHandler, "enumerated-criteria");
093        
094        SearchServiceInstance serviceInstance = args.serviceInstance();
095        
096        // Transform each enumerated criteria in facet definition to have the number of result for each enumerated values
097        Map<FacetDefinition, FOSearchCriterion> serviceFacets = _getFacetDefinitions(serviceInstance);
098        if (!serviceFacets.isEmpty())
099        {
100            Searcher searcher = _searcherFactory.create();
101            
102            // Set right
103            _setRight(searcher, args);
104            
105            // Add criterion query
106            searcher.withQuery(_searchComponentHelper.getCriterionTreeQuery(args, false, false));
107            
108            searcher.withFacets(serviceFacets.keySet().stream()
109                    .map(FacetDefinition::getSearchField)
110                    .collect(Collectors.toList()));
111            
112            // Add filter query
113            searcher.addFilterQuery(_searchComponentHelper.getFilterQuery(args));
114            
115            // Launch search with facets
116            SearchResults<AmetysObject> results = searcher.searchWithFacets();
117            args.setEnumeratedResults(results);
118            
119            // Sax the number of result for each values of enumerated criteria
120            Map<String, Object> contextualParameters = SearchComponentHelper.getSearchComponentContextualParameters(args);
121            _saxCountEnumeratedCriteria(contentHandler, args.generatorParameters(), serviceFacets, results, contextualParameters);
122        }
123        
124        XMLUtils.endElement(contentHandler, "enumerated-criteria");
125    }
126    
127    /**
128     * Transform each enumerated criteria in facet definition to have the number of result for each enumerated values
129     * @param serviceInstance the service instance
130     * @return the collection of facet definition
131     */
132    protected Map<FacetDefinition, FOSearchCriterion> _getFacetDefinitions(SearchServiceInstance serviceInstance)
133    {
134        Map<FacetDefinition, FOSearchCriterion> facets = new HashMap<>();
135        List<FOSearchCriterion> criteria = serviceInstance.getCriterionTree()
136                                                          .map(AbstractTreeNode::getFlatLeaves)
137                                                          .orElseGet(Collections::emptyList)
138                                                          .stream()
139                                                          .map(TreeLeaf::getValue)
140                                                          .filter(c -> !c.getMode().isStatic())
141                                                          .collect(Collectors.toList());
142        
143        for (FOSearchCriterion criterion : criteria)
144        {
145            Optional.of(criterion.getCriterionDefinition())
146                    .filter(ContentSearchCriterionDefinition.class::isInstance)
147                    .map(ContentSearchCriterionDefinition.class::cast)
148                    .filter(critDef -> critDef.getSearchUICriterion().isFacetable())
149                    .map(critDef -> new ContentFacetDefinition(critDef, null))
150                    .ifPresent(facet -> facets.put(facet, criterion));
151        }
152        
153        return facets;
154    }
155    
156    /**
157     * Set right to the searcher
158     * @param searcher the searcher
159     * @param args the arguments
160     */
161    protected void _setRight(Searcher searcher, SearchComponentArguments args)
162    {
163        RightCheckingMode rightCheckingMode = args.serviceInstance().getRightCheckingMode();
164        switch (rightCheckingMode)
165        {
166            case EXACT:
167                searcher.setCheckRights(true);
168                break;
169            case FAST:
170                AllowedUsers allowedUsersOnPage = _rightManager.getReadAccessAllowedUsers(args.currentPage());
171                searcher.checkRightsComparingTo(allowedUsersOnPage);
172                break;
173            case NONE:
174                searcher.setCheckRights(false);
175                break;
176            default:
177                throw new IllegalStateException("Unhandled right checking mode: " + rightCheckingMode);
178        }
179    }
180    
181    /**
182     * SAX the enumerated criteria
183     * @param contentHandler the content handler
184     * @param parameters the parameters
185     * @param facets The facets
186     * @param searchResults The search results
187     * @param contextualParameters The contextual parameters
188     * @throws SAXException if an error occurs while generating SAX events
189     */
190    protected void _saxCountEnumeratedCriteria(ContentHandler contentHandler, Parameters parameters, Map<FacetDefinition, FOSearchCriterion> facets, SearchResults<AmetysObject> searchResults, Map<String, Object> contextualParameters) throws SAXException
191    {
192        Map<String, Integer> valuesForCurrentFacetDef = Collections.EMPTY_MAP;
193        Map<String, Map<String, Integer>> facetResults = searchResults.getFacetResults();
194        
195        for (FacetDefinition facet : facets.keySet())
196        {
197            FOSearchCriterion criterion = facets.get(facet);
198            contextualParameters.put("criterion", criterion);
199
200            String id = facet.getId();
201            String name = FormSearchUserInputs.FACET_PREFIX + id;
202            AttributesImpl attrs = new AttributesImpl();
203            attrs.addCDATAAttribute("name", name);
204            if (facetResults.containsKey(id))
205            {
206                valuesForCurrentFacetDef = _getFacetValues(facetResults.get(id), criterion, contextualParameters);
207            }
208            attrs.addCDATAAttribute("total", String.valueOf(valuesForCurrentFacetDef.values()
209                    .stream()
210                    .mapToInt(Integer::intValue)
211                    .sum()));
212            
213            XMLUtils.startElement(contentHandler, "criterion", attrs);
214            facet.getLabel().toSAX(contentHandler, "label");
215            _saxFacetItemsWithCount(contentHandler, facet, valuesForCurrentFacetDef, contextualParameters);
216            XMLUtils.endElement(contentHandler, "criterion");
217        }
218    }
219    
220    /**
221     * Retrieves the values of the current facet
222     * @param facetResult the result of the current facet
223     * @param criterion the criterion corresponding to the current facet
224     * @param contextualParameters the contextual parameters
225     * @return the values of the current facet
226     */
227    protected Map<String, Integer> _getFacetValues(Map<String, Integer> facetResult, FOSearchCriterion criterion, Map<String, Object> contextualParameters)
228    {
229        SearchCriterionDefinition criterionDefinition = criterion.getCriterionDefinition();
230        
231        if (criterionDefinition.isEnumerated())
232        {
233            Map<String, Integer> valuesForCurrentFacetDef = new HashMap<>();
234            Optional<EnumeratedValues> enumeratedValues = criterionDefinition.getEnumeratedValues(contextualParameters);
235            
236            for (Map.Entry<String, Integer> facetResultEntry : facetResult.entrySet())
237            {
238                String facetResultValue = facetResultEntry.getKey();
239                Integer facetResultCount = facetResultEntry.getValue();
240                
241                enumeratedValues.flatMap(eV -> eV.getValue(facetResultValue))
242                                             .map(Map.Entry::getKey)
243                                             .map(String::valueOf)
244                                             .ifPresent(value -> valuesForCurrentFacetDef.put(value, facetResultCount));
245            }
246            
247            return valuesForCurrentFacetDef;
248        }
249            
250        return facetResult;
251    }
252    
253    /**
254     * SAX the facet items with the count
255     * @param contentHandler the content handler
256     * @param facet the facet definition
257     * @param valuesForCurrentFacetDef The values for the current facet definition
258     * @param contextualParameters The contextual parameters
259     * @throws SAXException if an error occurs while generating SAX events
260     */
261    protected void _saxFacetItemsWithCount(ContentHandler contentHandler, FacetDefinition facet, Map<String, Integer> valuesForCurrentFacetDef, Map<String, Object> contextualParameters) throws SAXException
262    {
263        for (String value : valuesForCurrentFacetDef.keySet())
264        {
265            Integer count = valuesForCurrentFacetDef.get(value);
266            Optional<I18nizableText> facetLabel = facet.getFacetLabel(value, contextualParameters);
267            if (facetLabel.isPresent())
268            {
269                _saxFacetItemWithCount(contentHandler, facetLabel.get(), value, count);
270            }
271        }
272    }
273    
274    /**
275     * SAX the facet item with the count
276     * @param contentHandler the content handler
277     * @param facetLabel the facet label
278     * @param value the value for the current facet item
279     * @param count the value count
280     * @throws SAXException if an error occurs while generating SAX events
281     */
282    protected void _saxFacetItemWithCount(ContentHandler contentHandler, I18nizableText facetLabel, String value, Integer count) throws SAXException
283    {
284        AttributesImpl valueAttrs = new AttributesImpl();
285        valueAttrs.addCDATAAttribute("value", value);
286        valueAttrs.addCDATAAttribute("count", String.valueOf(count));
287        
288        XMLUtils.startElement(contentHandler, "item", valueAttrs);
289        facetLabel.toSAX(contentHandler);
290        XMLUtils.endElement(contentHandler, "item");
291    }
292}