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