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.web.frontoffice.search.instance.SearchServiceInstance;
043import org.ametys.web.frontoffice.search.instance.model.RightCheckingMode;
044import org.ametys.web.frontoffice.search.instance.model.SearchServiceCriterion;
045import org.ametys.web.frontoffice.search.metamodel.SearchServiceCriterionDefinition;
046import org.ametys.web.frontoffice.search.metamodel.SearchServiceFacetDefinition;
047import org.ametys.web.frontoffice.search.requesttime.AbstractSearchComponent;
048import org.ametys.web.frontoffice.search.requesttime.SearchComponent;
049import org.ametys.web.frontoffice.search.requesttime.SearchComponentArguments;
050import org.ametys.web.frontoffice.search.requesttime.input.impl.FormSearchUserInputs;
051
052/**
053 * {@link SearchComponent} for saxing number of results for each values of enumerated criteria
054 */
055public class SaxEnumeratedCriteriaComponent extends AbstractSearchComponent implements Serviceable
056{
057    /** The searcher factory */
058    protected SearcherFactory _searcherFactory;
059    
060    /** The helper for search component */
061    protected SearchComponentHelper _searchComponentHelper;
062    
063    /** The right manager */
064    protected RightManager _rightManager;
065    
066    public void service(ServiceManager manager) throws ServiceException
067    {
068        _searcherFactory = (SearcherFactory) manager.lookup(SearcherFactory.ROLE);
069        _searchComponentHelper = (SearchComponentHelper) manager.lookup(SearchComponentHelper.ROLE);
070        _rightManager = (RightManager) manager.lookup(RightManager.ROLE);
071    }
072    
073    @Override
074    public int getPriority()
075    {
076        return SEARCH_PRIORITY + 2500;
077    }
078
079    @Override
080    public boolean supports(SearchComponentArguments args)
081    {
082        return args.serviceInstance().computeCriteriaCounts();
083    }
084
085    @Override
086    public void execute(SearchComponentArguments args) throws Exception
087    {
088        ContentHandler contentHandler = args.contentHandler();
089        XMLUtils.startElement(contentHandler, "enumerated-criteria");
090        
091        SearchServiceInstance serviceInstance = args.serviceInstance();
092        
093        // Transform each enumerated criteria in facet definition to have the number of result for each enumerated values
094        Map<SearchServiceFacetDefinition, SearchServiceCriterion> serviceFacets = _getFacetDefinitions(serviceInstance);
095        if (!serviceFacets.isEmpty())
096        {
097            Searcher searcher = _searcherFactory.create();
098            
099            // Set right
100            _setRight(searcher, args);
101            
102            // Add criterion query
103            searcher.withQuery(_searchComponentHelper.getCriterionTreeQuery(args, false, false));
104            
105            searcher.withFacets(serviceFacets.keySet().stream()
106                    .map(SearchServiceFacetDefinition::getFacetDefinition)
107                    .collect(Collectors.toList()));
108            
109            // Add filter query
110            searcher.addFilterQuery(_searchComponentHelper.getFilterQuery(args));
111            
112            // Launch search with facets
113            SearchResults<AmetysObject> results = searcher.searchWithFacets();
114            args.setEnumeratedResults(results);
115            
116            // Sax the number of result for each values of enumerated criteria
117            Map<String, Object> contextualParameters = SearchComponentHelper.getSearchComponentContextualParameters(args);
118            _saxCountEnumeratedCriteria(contentHandler, args.generatorParameters(), serviceFacets, results, contextualParameters);
119        }
120        
121        XMLUtils.endElement(contentHandler, "enumerated-criteria");
122    }
123    
124    /**
125     * Transform each enumerated criteria in facet definition to have the number of result for each enumerated values
126     * @param serviceInstance the service instance
127     * @return the collection of facet definition
128     */
129    protected Map<SearchServiceFacetDefinition, SearchServiceCriterion> _getFacetDefinitions(SearchServiceInstance serviceInstance)
130    {
131        Map<SearchServiceFacetDefinition, SearchServiceCriterion> facets = new HashMap<>();
132        List<SearchServiceCriterion> criteria = serviceInstance.getCriterionTree()
133                                                               .map(AbstractTreeNode::getFlatLeaves)
134                                                               .orElseGet(Collections::emptyList)
135                                                               .stream()
136                                                               .map(TreeLeaf::getValue)
137                                                               .filter(c -> !c.getMode().isStatic())
138                                                               .collect(Collectors.toList());
139        
140        for (SearchServiceCriterion<?> criterion : criteria)
141        {
142            serviceInstance.getReturnables()
143                           .stream()
144                           .map(returnable -> returnable.getFacetDefinition(criterion.getCriterionDefinition()))
145                           .filter(Optional::isPresent)
146                           .map(Optional::get)
147                           // Criterion are linked to a searchable, not a returnable. But here we use the returnable to build a facet from it.
148                           // We need a way to choose the facet when multiple definitions are available.
149                           // We don't know of any real use case so we use findFirst here but it may require a more deterministic algorithm.
150                           .findFirst()
151                           .ifPresent(facet -> facets.put(facet, criterion));
152        }
153        
154        return facets;
155    }
156    
157    /**
158     * Set right to the searcher
159     * @param searcher the searcher
160     * @param args the arguments
161     */
162    protected void _setRight(Searcher searcher, SearchComponentArguments args)
163    {
164        RightCheckingMode rightCheckingMode = args.serviceInstance().getRightCheckingMode();
165        switch (rightCheckingMode)
166        {
167            case EXACT:
168                searcher.setCheckRights(true);
169                break;
170            case FAST:
171                AllowedUsers allowedUsersOnPage = _rightManager.getReadAccessAllowedUsers(args.currentPage());
172                searcher.checkRightsComparingTo(allowedUsersOnPage);
173                break;
174            case NONE:
175                searcher.setCheckRights(false);
176                break;
177            default:
178                throw new IllegalStateException("Unhandled right checking mode: " + rightCheckingMode);
179        }
180    }
181    
182    /**
183     * SAX the enumerated criteria
184     * @param contentHandler the content handler
185     * @param parameters the parameters
186     * @param facets The facets
187     * @param searchResults The search results
188     * @param contextualParameters The contextual parameters
189     * @throws SAXException if an error occurs while generating SAX events
190     */
191    protected void _saxCountEnumeratedCriteria(ContentHandler contentHandler, Parameters parameters, Map<SearchServiceFacetDefinition, SearchServiceCriterion> facets, SearchResults<AmetysObject> searchResults, Map<String, Object> contextualParameters) throws SAXException
192    {
193        Map<String, Integer> valuesForCurrentFacetDef = Collections.EMPTY_MAP;
194        Map<String, Map<String, Integer>> facetResults = searchResults.getFacetResults();
195        
196        for (SearchServiceFacetDefinition facet : facets.keySet())
197        {
198            SearchServiceCriterion<?> criterion = facets.get(facet);
199            contextualParameters.put("criterion", criterion);
200
201            String facetName = facet.getName();
202            String criterionName = FormSearchUserInputs.CRITERION_PREFIX + criterion.getName();
203            AttributesImpl attrs = new AttributesImpl();
204            attrs.addCDATAAttribute("name", criterionName);
205            if (facetResults.containsKey(facetName))
206            {
207                valuesForCurrentFacetDef = _getFacetValues(facetResults.get(facetName), criterion, contextualParameters);
208            }
209            attrs.addCDATAAttribute("total", String.valueOf(valuesForCurrentFacetDef.values()
210                    .stream()
211                    .mapToInt(Integer::intValue)
212                    .sum()));
213            
214            XMLUtils.startElement(contentHandler, "criterion", attrs);
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     * @param <T> Type of the criterion value
226     * @return the values of the current facet
227     */
228    protected <T> Map<String, Integer> _getFacetValues(Map<String, Integer> facetResult, SearchServiceCriterion<T> criterion, Map<String, Object> contextualParameters)
229    {
230        SearchServiceCriterionDefinition<T> criterionDefinition = criterion.getCriterionDefinition();
231        Map<String, Integer> valuesForCurrentFacetDef = new HashMap<>();
232        
233        for (Map.Entry<String, Integer> facetResultEntry : facetResult.entrySet())
234        {
235            String facetResultValue = facetResultEntry.getKey();
236            Integer facetResultCount = facetResultEntry.getValue();
237            
238            String value = criterionDefinition.facetValueToSAX(facetResultValue, contextualParameters);
239            valuesForCurrentFacetDef.put(value, facetResultCount);
240        }
241        
242        return valuesForCurrentFacetDef;
243    }
244    
245    /**
246     * SAX the facet items with the count
247     * @param contentHandler the content handler
248     * @param facet the facet definition
249     * @param valuesForCurrentFacetDef The values for the current facet definition
250     * @param contextualParameters The contextual parameters
251     * @throws SAXException if an error occurs while generating SAX events
252     */
253    protected void _saxFacetItemsWithCount(ContentHandler contentHandler, SearchServiceFacetDefinition facet, Map<String, Integer> valuesForCurrentFacetDef, Map<String, Object> contextualParameters) throws SAXException
254    {
255        for (String value : valuesForCurrentFacetDef.keySet())
256        {
257            Integer count = valuesForCurrentFacetDef.get(value);
258            _saxFacetItemWithCount(contentHandler, value, count);
259        }
260    }
261    
262    /**
263     * SAX the facet item with the count
264     * @param contentHandler the content handler
265     * @param value the value for the current facet item
266     * @param count the value count
267     * @throws SAXException if an error occurs while generating SAX events
268     */
269    protected void _saxFacetItemWithCount(ContentHandler contentHandler, String value, Integer count) throws SAXException
270    {
271        AttributesImpl valueAttrs = new AttributesImpl();
272        valueAttrs.addCDATAAttribute("value", value);
273        valueAttrs.addCDATAAttribute("count", String.valueOf(count));
274        
275        XMLUtils.createElement(contentHandler, "item", valueAttrs);
276    }
277}