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;
017
018import java.io.IOException;
019import java.text.DecimalFormat;
020import java.util.ArrayList;
021import java.util.Comparator;
022import java.util.List;
023import java.util.Optional;
024import java.util.stream.Collectors;
025
026import org.apache.avalon.framework.parameters.ParameterException;
027import org.apache.avalon.framework.service.ServiceException;
028import org.apache.avalon.framework.service.ServiceManager;
029import org.apache.cocoon.ProcessingException;
030import org.apache.cocoon.environment.ObjectModelHelper;
031import org.apache.cocoon.environment.Request;
032import org.apache.cocoon.environment.Response;
033import org.apache.cocoon.generation.ServiceableGenerator;
034import org.apache.commons.lang3.StringUtils;
035import org.slf4j.Logger;
036import org.xml.sax.SAXException;
037
038import org.ametys.cms.search.solr.SearcherFactory;
039import org.ametys.cms.search.solr.SearcherFactory.Searcher;
040import org.ametys.core.util.AvalonLoggerAdapter;
041import org.ametys.plugins.repository.AmetysObjectResolver;
042import org.ametys.web.WebConstants;
043import org.ametys.web.frontoffice.search.SearchService;
044import org.ametys.web.frontoffice.search.instance.SearchServiceInstance;
045import org.ametys.web.frontoffice.search.instance.SearchServiceInstanceManager;
046import org.ametys.web.frontoffice.search.requesttime.SearchServiceDebugModeHelper.DebugMode;
047import org.ametys.web.frontoffice.search.requesttime.input.InputValidator;
048import org.ametys.web.frontoffice.search.requesttime.input.SearchUserInputs;
049import org.ametys.web.frontoffice.search.requesttime.input.SearchUserInputsRetriever;
050import org.ametys.web.frontoffice.search.requesttime.pagination.Pagination;
051import org.ametys.web.renderingcontext.RenderingContextHandler;
052import org.ametys.web.repository.page.Page;
053import org.ametys.web.repository.page.ZoneItem;
054import org.ametys.web.repository.site.Site;
055import org.ametys.web.repository.site.SiteManager;
056import org.ametys.web.service.Service;
057import org.ametys.web.service.ServiceExtensionPoint;
058
059/**
060 * Generator for the search service.
061 */
062public class SearchServiceGenerator extends ServiceableGenerator
063{
064    static final String __ZONE_ITEM_REQUEST_PARAM_NAME = "zone-item-id";
065    static final String __ZONE_ITEM_GENERATOR_PARAM_NAME = "zone-item-id";
066    static final String __PAGINATION_GENERATOR_PARAM_NAME = "pagination-index";
067    static final String __SUBMIT_FORM_PARAM_NAME = "submit-form";
068    
069    /** The SL4j logger */
070    protected Logger _logger;
071    /** The manager for {@link SearchServiceInstance}s */
072    protected SearchServiceInstanceManager _searchServiceInstanceManager;
073    /** The search {@link Service} */
074    protected SearchService _searchService;
075    /** The site manager */
076    protected SiteManager _siteManager;
077    /** The searcher factory */
078    protected SearcherFactory _searcherFactory;
079    /** The extension point for {@link SearchComponent}s */
080    protected SearchComponentExtensionPoint _searchComponentEP;
081    /** The handler of rendering context */
082    protected RenderingContextHandler _renderingContextHandler;
083    /** The resolver for Ametys objects */
084    protected AmetysObjectResolver _ametysObjectResolver;
085    /** The search user inputs retriever */
086    protected SearchUserInputsRetriever _searchUserInputsRetriever;
087    
088    @Override
089    public void enableLogging(org.apache.avalon.framework.logger.Logger logger)
090    {
091        super.enableLogging(logger);
092        _logger = new AvalonLoggerAdapter(logger);
093    }
094
095    @Override
096    public void service(ServiceManager smanager) throws ServiceException
097    {
098        super.service(smanager);
099        _searchServiceInstanceManager = (SearchServiceInstanceManager) smanager.lookup(SearchServiceInstanceManager.ROLE);
100        ServiceExtensionPoint serviceEP = (ServiceExtensionPoint) smanager.lookup(ServiceExtensionPoint.ROLE);
101        _searchService = (SearchService) serviceEP.getExtension(SearchService.ROLE);
102        _siteManager = (SiteManager) smanager.lookup(SiteManager.ROLE);
103        _searcherFactory = (SearcherFactory) smanager.lookup(SearcherFactory.ROLE);
104        _searchComponentEP = (SearchComponentExtensionPoint) smanager.lookup(SearchComponentExtensionPoint.ROLE);
105        _renderingContextHandler = (RenderingContextHandler) smanager.lookup(RenderingContextHandler.ROLE);
106        _ametysObjectResolver = (AmetysObjectResolver) smanager.lookup(AmetysObjectResolver.ROLE);
107        _searchUserInputsRetriever = (SearchUserInputsRetriever) smanager.lookup(SearchUserInputsRetriever.ROLE);
108    }
109    
110    /**
111     * Builds {@link SearchComponentArguments}
112     * @param request The request
113     * @return The built {@link SearchComponentArguments}
114     * @throws ProcessingException If an error occured when processing the request
115     */
116    protected SearchComponentArguments _buildArguments(Request request) throws ProcessingException
117    {
118        Response response = ObjectModelHelper.getResponse(objectModel);
119        String zoneItemId = _retrieveZoneItemId(request);
120        SearchServiceInstance serviceInstance = getInstance(request, zoneItemId);
121        Page currentPage = _retrieveCurrentPage(request, zoneItemId);
122        Searcher searcher = _searcherFactory.create();
123        
124        int resultPageIndex = _retrievePageIndex(request);
125        Pagination pagination = new Pagination(resultPageIndex, serviceInstance.resultsPerPage(), serviceInstance.maxResults());
126        DebugMode debugMode = _debugMode(request);
127        boolean isFormSubmitted = _isFormSubmitted(request);
128        boolean launchSearch = _launchSearch(isFormSubmitted, request, serviceInstance, debugMode);
129        SearchUserInputs userInputs = _searchUserInputsRetriever.getUserInputs(request, isFormSubmitted, serviceInstance);
130        
131        SearchComponentArguments args = new SearchComponentArguments(
132                contentHandler, 
133                parameters,
134                serviceInstance, 
135                _searchService, 
136                userInputs,
137                request, 
138                response, 
139                pagination,
140                _retrieveCurrentSite(request, currentPage), 
141                currentPage, 
142                _retrieveCurrentLang(request, currentPage), 
143                launchSearch, 
144                searcher, 
145                _logger, 
146                debugMode);
147        return args;
148    }
149    
150    @Override
151    public void generate() throws IOException, SAXException, ProcessingException
152    {
153        Request request = ObjectModelHelper.getRequest(objectModel);
154        SearchComponentArguments args = _buildArguments(request);
155        
156        List<IdAwareSearchComponent> components = _searchComponentEP.getExtensionsIds()
157                .stream()
158                .map(id -> IdAwareSearchComponent.of(id, _searchComponentEP))
159                .filter(c -> c._component.supports(args))
160                .sorted(Comparator.comparingLong(c -> c._component.priority()))
161                .collect(Collectors.toList());
162        
163        long allCmpStart = System.currentTimeMillis();
164        String reqIdentifier = _getRequestIdentifier(request);
165        List<SearchComponentError> errors = new ArrayList<>();
166        contentHandler.startDocument();
167        for (IdAwareSearchComponent idAwareComponent : components)
168        {
169            SearchComponent component = idAwareComponent._component;
170            String id = idAwareComponent._id;
171            try
172            {
173                long currCmpStart = System.currentTimeMillis();
174                component.execute(args);
175                String strPriority = _logger.isInfoEnabled() ? new DecimalFormat().format(component.priority()) : null;
176                _logger.info("Execution finished for search component '{}' (priority {}) and request '{}' in {}ms", id, strPriority, reqIdentifier, System.currentTimeMillis() - currCmpStart);
177            }
178            catch (Exception e)
179            {
180                _logger.error("An error occured while executing search component '{}' for request '{}'. Other components will be executed but the result can be inconsistent", id, reqIdentifier, e);
181                errors.add(new SearchComponentError(component, e));
182            }
183        }
184        _handleErrors(errors);
185        
186        contentHandler.endDocument();
187        _logger.info("All search components have been executed for request '{}' in {}ms", reqIdentifier, System.currentTimeMillis() - allCmpStart);
188    }
189    
190    /**
191     * Retrieves the current zone item id
192     * @param request the request
193     * @return the zone item id
194     */
195    protected String _retrieveZoneItemId(Request request)
196    {
197        if (parameters.isParameter(__ZONE_ITEM_GENERATOR_PARAM_NAME))
198        {
199            return parameters.getParameter(__ZONE_ITEM_GENERATOR_PARAM_NAME, "");
200        }
201        
202        ZoneItem zoneItem = (ZoneItem) request.getAttribute(WebConstants.REQUEST_ATTR_ZONEITEM);
203        return Optional.ofNullable(zoneItem)
204                    .map(ZoneItem::getId)
205                    .orElse(request.getParameter(__ZONE_ITEM_REQUEST_PARAM_NAME));
206    }
207    
208    /**
209     * Gets the {@link SearchServiceInstance} being processed
210     * @param request the request
211     * @param zoneItemId The zone item id
212     * @return the {@link SearchServiceInstance} being processed
213     */
214    protected SearchServiceInstance getInstance(Request request, String zoneItemId)
215    {
216        if (!_searchServiceInstanceManager.isSearchServiceInstance(zoneItemId))
217        {
218            throw new IllegalArgumentException("No search service instance in zone item '" + zoneItemId + "'");
219        }
220        return _searchServiceInstanceManager.get(zoneItemId);
221    }
222    
223    /**
224     * Retrieves the current page index of the search
225     * @param request the request
226     * @return the current page index of the search
227     * @throws ProcessingException If an error occured when retrieving the page index 
228     */
229    protected int _retrievePageIndex(Request request) throws ProcessingException
230    {
231        if (parameters.isParameter(__PAGINATION_GENERATOR_PARAM_NAME))
232        {
233            try
234            {
235                return parameters.getParameterAsInteger(__PAGINATION_GENERATOR_PARAM_NAME);
236            }
237            catch (ParameterException e)
238            {
239                throw new ProcessingException(e);
240            }
241        }
242        
243        return 1;
244    }
245    
246    /**
247     * Retrieves the current page
248     * @param request the request
249     * @param zoneItemId The zone item id
250     * @return the current page
251     */
252    protected Page _retrieveCurrentPage(Request request, String zoneItemId)
253    {
254        Page page = (Page) request.getAttribute(WebConstants.REQUEST_ATTR_PAGE);
255        if (page == null)
256        {
257            return _ametysObjectResolver.<ZoneItem>resolveById(zoneItemId).getZone().getPage();
258        }
259        return page;
260    }
261    
262    /**
263     * Retrieves the current site
264     * @param request the request
265     * @param page the page
266     * @return the current site
267     */
268    protected Site _retrieveCurrentSite(Request request, Page page)
269    {
270        String currentSiteName = page != null ? page.getSiteName() : request.getParameter("siteName");
271        return _siteManager.getSite(currentSiteName);
272    }
273    
274    /**
275     * Retrieves the current lang
276     * @param request the request
277     * @param page the page
278     * @return the current lang
279     */
280    protected String _retrieveCurrentLang(Request request, Page page)
281    {
282        return page != null ? page.getSitemapName() : request.getParameter("lang");
283    }
284    
285    /**
286     * Checks if the form was submitted
287     * @param request The request
288     * @return <code>true</code> if the form was submitted
289     */
290    protected boolean _isFormSubmitted(Request request)
291    {
292        return parameters.getParameterAsBoolean(__SUBMIT_FORM_PARAM_NAME, false) 
293                || request.getParameter(__SUBMIT_FORM_PARAM_NAME) != null;
294    }
295    
296    /**
297     * Tells if the search has to be launched
298     * @param isFormSubmitted <code>true</code> if the form was submitted
299     * @param request The request
300     * @param serviceInstance The service instance being processed
301     * @param debugMode The debug mode
302     * @return <code>true</code> if the search has to be launched
303     */
304    protected boolean _launchSearch(boolean isFormSubmitted, Request request, SearchServiceInstance serviceInstance, DebugMode debugMode)
305    {
306        if (serviceInstance.getResultDisplay()
307                .launchSearchAtStartup()
308                .orElse(false))
309        {
310            // Search has to be launched at startup
311            return true;
312        }
313        
314        return isFormSubmitted
315                && _inputValid(request, serviceInstance, debugMode);
316    }
317    
318    /**
319     * Checks if inputs are valid
320     * @param request The request
321     * @param serviceInstance The service instance being processed
322     * @param debugMode The debug mode
323     * @return <code>true</code> if inputs are valid
324     */
325    protected boolean _inputValid(Request request, SearchServiceInstance serviceInstance, DebugMode debugMode)
326    {
327        ZoneItem zoneItem = (ZoneItem) request.getAttribute(WebConstants.REQUEST_ATTR_ZONEITEM);
328        String submittedFormParamValue = request.getParameter(__SUBMIT_FORM_PARAM_NAME);
329        String fromZoneItemId = request.getParameter(__ZONE_ITEM_REQUEST_PARAM_NAME);
330        
331        InputValidator inputValidator = new InputValidator(serviceInstance, zoneItem, submittedFormParamValue, fromZoneItemId);
332        // if rejected => search has not to be launched
333        // if not rejected => if #validate returns true, it has to be launched. Otherwise the result is unknown, by default we do not launch it
334        return !inputValidator.rejects() && inputValidator.validate();
335    }
336    
337    /**
338     * Gets the debug mode if activated
339     * @param request The request
340     * @return the debug mode if activated
341     */
342    protected DebugMode _debugMode(Request request)
343    {
344        return SearchServiceDebugModeHelper.debugMode(request, _renderingContextHandler);
345    }
346    
347    /**
348     * Gets the identifier of the request for display debug purposes only
349     * @param request the request
350     * @return the identifier of the request for display debug purposes only
351     */
352    protected String _getRequestIdentifier(Request request)
353    {
354        String reqId = request.toString();
355        if (reqId.contains("@"))
356        {
357            reqId = StringUtils.substringAfterLast(reqId, "@");
358        }
359        return reqId;
360    }
361    
362    static class SearchComponentError
363    {
364        SearchComponent _component;
365        Throwable _throwable;
366
367        SearchComponentError(SearchComponent component, Throwable t)
368        {
369            _component = component;
370            _throwable = t;
371        }
372        
373        @Override
374        public String toString()
375        {
376            return "with " + _component;
377        }
378    }
379    
380    /**
381     * Handles errors of {@link SearchComponent}s
382     * @param errors The errors
383     * @throws ProcessingException if the {@link SearchComponentError}s lead to a need to throw an exception
384     */
385    protected void _handleErrors(List<SearchComponentError> errors) throws ProcessingException
386    {
387        if (!errors.isEmpty())
388        {
389            String msg = String.format("Some errors occured during the execution of search components: %s. See the previous error logs to see details.", errors);
390            throw new ProcessingException(msg);
391        }
392    }
393    
394    private static final class IdAwareSearchComponent
395    {
396        final SearchComponent _component;
397        final String _id;
398        
399        private IdAwareSearchComponent(SearchComponent component, String id)
400        {
401            _component = component;
402            _id = id;
403        }
404        
405        static IdAwareSearchComponent of(String id, SearchComponentExtensionPoint searchComponents)
406        {
407            return new IdAwareSearchComponent(searchComponents.getExtension(id), id);
408        }
409        
410        @Override
411        public String toString()
412        {
413            return _id;
414        }
415    }
416}