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