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