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}