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 _logger.info("All search components have been executed for request '{}' in {}ms", reqIdentifier, System.currentTimeMillis() - allCmpStart); 185 _handleErrors(errors); 186 187 contentHandler.endDocument(); 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}