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