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