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.List;
019import java.util.Optional;
020
021import org.apache.cocoon.xml.AttributesImpl;
022import org.apache.cocoon.xml.XMLUtils;
023import org.slf4j.Logger;
024import org.xml.sax.ContentHandler;
025import org.xml.sax.SAXException;
026
027import org.ametys.cms.search.SearchResult;
028import org.ametys.cms.search.SearchResults;
029import org.ametys.cms.search.SearchResultsIterable;
030import org.ametys.cms.search.SearchResultsIterator;
031import org.ametys.plugins.repository.AmetysObject;
032import org.ametys.web.frontoffice.search.instance.SearchServiceInstance;
033import org.ametys.web.frontoffice.search.metamodel.AdditionalParameterValueMap;
034import org.ametys.web.frontoffice.search.metamodel.Returnable;
035import org.ametys.web.frontoffice.search.metamodel.ReturnableSaxer;
036import org.ametys.web.frontoffice.search.requesttime.AbstractSearchComponent;
037import org.ametys.web.frontoffice.search.requesttime.SearchComponent;
038import org.ametys.web.frontoffice.search.requesttime.SearchComponentArguments;
039import org.ametys.web.frontoffice.search.requesttime.pagination.Pagination;
040
041/**
042 * {@link SearchComponent} for saxing results
043 */
044public class SaxResultsSearchComponent extends AbstractSearchComponent
045{
046    @Override
047    public int getPriority()
048    {
049        return SEARCH_PRIORITY + 3000;
050    }
051
052    @Override
053    public boolean supports(SearchComponentArguments args)
054    {
055        return args.launchSearch() && !args.generatorParameters().getParameterAsBoolean(DISABLE_DEFAULT_SAX_PARAMETER_NAME, false);
056    }
057
058    @Override
059    public void execute(SearchComponentArguments args) throws Exception
060    {
061        SearchResults<AmetysObject> results = getResults(args);
062        
063        ContentHandler handler = args.contentHandler();
064        
065        int total = total(results, args.serviceInstance());
066        
067        Pagination pagination = args.pagination();
068        saxPagination(total, pagination, handler);
069        
070        AttributesImpl atts = new AttributesImpl();
071        atts.addCDATAAttribute("total", String.valueOf(total));
072        XMLUtils.startElement(handler, "hits", atts);
073        SearchServiceInstance service = args.serviceInstance();
074        saxHits(results, pagination.currentStartDocIndex(), args, service.getReturnables(), service.getAdditionalParameterValues());
075        XMLUtils.endElement(handler, "hits");
076    }
077    
078    /**
079     * Gets the results
080     * @param args The arguments
081     * @return the results
082     */
083    protected SearchResults<AmetysObject> getResults(SearchComponentArguments args)
084    {
085        return  args.results()
086                .orElseThrow(() -> new IllegalStateException("Results have not been set yet. You can refer to previous logs to find the cause. This is likely due to the Solr server which could not be reached."));
087    }
088    
089    /**
090     * The total of results
091     * @param results The results
092     * @param serviceInstance The search service instance
093     * @return The total of results
094     */
095    protected int total(SearchResults<AmetysObject> results, SearchServiceInstance serviceInstance)
096    {
097        int solrTotalCount = Math.toIntExact(results.getTotalCount());
098        int total = serviceInstance
099                .maxResults()
100                .map(maxResults -> Math.min(solrTotalCount, maxResults)) // if a maxResults is defined, it should not exceed it
101                .orElse(solrTotalCount);
102        return total;
103    }
104    
105    /**
106     * SAX the result hits
107     * @param results The search results
108     * @param start The start index of search
109     * @param args The arguments
110     * @param returnables The returnables
111     * @param additionalParameterValues The additional parameter values
112     * @throws SAXException If an error occurs while SAXing
113     */
114    protected void saxHits(SearchResults<AmetysObject> results, int start, SearchComponentArguments args, List<Returnable> returnables, AdditionalParameterValueMap additionalParameterValues) throws SAXException
115    {
116        SearchResultsIterable<SearchResult<AmetysObject>> resultsIterable = results.getResults();
117        SearchResultsIterator<SearchResult<AmetysObject>> resultsIterator = resultsIterable.iterator();
118        for (int number = start; resultsIterator.hasNext(); number++)
119        {
120            SearchResult<AmetysObject> result = resultsIterator.next();
121            saxHit(result, number, args, returnables, additionalParameterValues);
122        }
123    }
124    
125    /**
126     * SAX the result hit
127     * @param result The search result
128     * @param number The hit number
129     * @param args The arguments
130     * @param returnables The returnables
131     * @param additionalParameterValues The additional parameter values
132     * @throws SAXException If an error occurs while SAXing
133     */
134    protected void saxHit(SearchResult<AmetysObject> result, int number, SearchComponentArguments args, List<Returnable> returnables, AdditionalParameterValueMap additionalParameterValues) throws SAXException
135    {
136        ContentHandler handler = args.contentHandler();
137        Logger logger = args.logger();
138        
139        AmetysObject ametysObject = result.getObject();
140        Optional<ReturnableSaxer> saxer = getSaxer(ametysObject, args, returnables, additionalParameterValues);
141        
142        AttributesImpl attrs = new AttributesImpl();
143        attrs.addCDATAAttribute("number", Integer.toString(number));
144        attrs.addCDATAAttribute("className", ametysObject.getClass().getCanonicalName());
145        saxer.ifPresent(s -> attrs.addCDATAAttribute("saxer", s.getIdentifier()));
146        XMLUtils.startElement(handler, "hit", attrs);
147        XMLUtils.createElement(handler, "id", ametysObject.getId());
148        
149        if (saxer.isPresent())
150        {
151            saxer.get().sax(handler, ametysObject, logger, args);
152        }
153        
154        XMLUtils.createElement(handler, "score", Float.toString(result.getScore()));
155        XMLUtils.endElement(handler, "hit");
156    }
157    
158    /**
159     * Gets the {@link ReturnableSaxer Saxer} to use for SAXing the given hit
160     * @param hit The hit to SAX
161     * @param args The arguments
162     * @param returnables The returnables for the current search service instance
163     * @param additionalParameterValues The additional parameter values
164     * @return the {@link ReturnableSaxer Saxer} to use for SAXing the given hit
165     */
166    protected Optional<ReturnableSaxer> getSaxer(AmetysObject hit, SearchComponentArguments args, List<Returnable> returnables, AdditionalParameterValueMap additionalParameterValues)
167    {
168        Logger logger = args.logger();
169        
170        // By default, take the first returnable which can sax the hit
171        return returnables.stream()
172                .map(returnable -> returnable.getSaxer(returnables, additionalParameterValues))
173                .filter(saxer -> saxer.canSax(hit, logger, args))
174                .findFirst();
175    }
176    
177    /**
178     * SAX elements for pagination
179     * @param totalHits The total number of result
180     * @param pagination The pagination object
181     * @param contentHandler The content handler
182     * @throws SAXException SAXException If an error occurs while SAXing
183     */
184    protected void saxPagination(int totalHits, Pagination pagination, ContentHandler contentHandler) throws SAXException
185    {
186        int currentPage = pagination.currentPageIndex();
187        int start = pagination.currentStartDocIndex();
188        int nbPages = pagination.numberOfPages(totalHits);
189        
190        AttributesImpl atts = new AttributesImpl();
191        atts.addCDATAAttribute("total", String.valueOf(nbPages)); // Number of pages
192        atts.addCDATAAttribute("current", String.valueOf(currentPage)); // Current page
193        atts.addCDATAAttribute("start", String.valueOf(start)); // Index of the first hit
194        
195        // Index of the last hit (
196        //   when no result start=0 and end=0,
197        //   when 1 result start=0 and end=1,
198        //   when 2 results start=0 and end=2 etc.
199        // )
200        int endIndexExclusive = pagination.currentEndDocExclusiveIndex(totalHits);
201        atts.addCDATAAttribute("end", String.valueOf(endIndexExclusive));
202        
203        XMLUtils.createElement(contentHandler, "pagination", atts);
204    }
205}