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; 017 018import java.util.Arrays; 019import java.util.Collection; 020import java.util.Collections; 021import java.util.Comparator; 022import java.util.HashMap; 023import java.util.LinkedHashMap; 024import java.util.List; 025import java.util.Map; 026import java.util.Optional; 027import java.util.function.Function; 028import java.util.stream.Collectors; 029 030import org.apache.avalon.framework.configuration.Configuration; 031import org.apache.avalon.framework.configuration.ConfigurationException; 032import org.apache.avalon.framework.configuration.DefaultConfiguration; 033import org.apache.avalon.framework.service.ServiceException; 034import org.apache.avalon.framework.service.ServiceManager; 035import org.apache.cocoon.components.ContextHelper; 036import org.apache.cocoon.environment.Request; 037import org.apache.commons.lang3.StringUtils; 038 039import org.ametys.cms.search.advanced.AbstractTreeNode; 040import org.ametys.cms.search.advanced.TreeLeaf; 041import org.ametys.core.util.LambdaUtils; 042import org.ametys.runtime.i18n.I18nizableText; 043import org.ametys.web.frontoffice.search.instance.SearchServiceInstance; 044import org.ametys.web.frontoffice.search.instance.SearchServiceInstanceManager; 045import org.ametys.web.frontoffice.search.instance.model.FOSearchCriterion; 046import org.ametys.web.frontoffice.search.instance.model.ResultDisplay; 047import org.ametys.web.frontoffice.search.instance.model.ResultDisplayType; 048import org.ametys.web.frontoffice.search.instance.model.RightCheckingMode; 049import org.ametys.web.frontoffice.search.metamodel.AdditionalSearchServiceParameter; 050import org.ametys.web.frontoffice.search.metamodel.FacetDefinition; 051import org.ametys.web.frontoffice.search.metamodel.SearchServiceCreationHelper; 052import org.ametys.web.frontoffice.search.metamodel.SortDefinition; 053import org.ametys.web.frontoffice.search.requesttime.SearchServiceDebugModeHelper; 054import org.ametys.web.renderingcontext.RenderingContextHandler; 055import org.ametys.web.repository.page.Page; 056import org.ametys.web.repository.page.ZoneItem; 057import org.ametys.web.service.ServiceParameter; 058import org.ametys.web.service.StaticService; 059 060/** 061 * Front search service. 062 */ 063public class SearchService extends StaticService 064{ 065 /** Avalon Role */ 066 public static final String ROLE = "org.ametys.web.service.SearchService"; 067 068 /** The parameter name for header */ 069 public static final String PARAM_NAME_HEADER = "header"; 070 /** The parameter name for returnables */ 071 public static final String PARAM_NAME_RETURNABLES = "returnables"; 072 /** The parameter name for contexts */ 073 public static final String PARAM_NAME_CONTEXTS = "contexts"; 074 /** The parameter name for criteria */ 075 public static final String PARAM_NAME_CRITERIA = "criteria"; 076 /** The parameter name for computing counts */ 077 public static final String PARAM_NAME_COMPUTE_COUNTS = "computeCounts"; 078 /** The parameter name for facets */ 079 public static final String PARAM_NAME_FACETS = "facets"; 080 /** The parameter name for initial sorts */ 081 public static final String PARAM_NAME_INITIAL_SORTS = "initialSorts"; 082 /** The parameter name for proposed sorts */ 083 public static final String PARAM_NAME_PROPOSED_SORTS = "proposedSorts"; 084 /** The parameter name for number of results per page */ 085 public static final String PARAM_NAME_RESULTS_PER_PAGE = "resultsPerPage"; 086 /** The parameter name for maximum number of results */ 087 public static final String PARAM_NAME_MAX_RESULTS = "maxResults"; 088 /** The parameter name for right checking mode */ 089 public static final String PARAM_NAME_RIGHT_CHECKING_MODE = "rightCheckingMode"; 090 /** The parameter name for XSLT */ 091 public static final String PARAM_NAME_XSLT = "xslt"; 092 /** The parameter name for result place */ 093 public static final String PARAM_NAME_RESULT_PLACE = "resultPlace"; 094 /** The parameter name for result page */ 095 public static final String PARAM_NAME_RESULT_PAGE = "resultPage"; 096 /** The parameter name for launching search at startup */ 097 public static final String PARAM_NAME_LAUNCH_SEARCH_AT_STARTUP = "launchSearchAtStartup"; 098 /** The parameter name for service group id */ 099 public static final String PARAM_NAME_SERVICE_GROUP_ID = "serviceGroupId"; 100 /** The parameter name for link page */ 101 public static final String PARAM_NAME_LINK_PAGE = "link"; 102 /** The parameter name for link title */ 103 public static final String PARAM_NAME_LINK_TITLE = "linkTitle"; 104 /** The parameter name for handling RSS */ 105 public static final String PARAM_NAME_RSS = "rss"; 106 /** The parameter name for saving user preferences */ 107 public static final String PARAM_NAME_SAVE_USER_PREFS = "saveUserPrefs"; 108 109 /** The helper component for defining this service */ 110 protected SearchServiceCreationHelper _creationHelper; 111 /** The handler of rendering context */ 112 protected RenderingContextHandler _renderingContextHandler; 113 /** The manager for {@link SearchServiceInstance} */ 114 protected SearchServiceInstanceManager _searchServiceInstanceManager; 115 116 @Override 117 public void service(ServiceManager smanager) throws ServiceException 118 { 119 super.service(smanager); 120 _creationHelper = (SearchServiceCreationHelper) smanager.lookup(SearchServiceCreationHelper.ROLE); 121 _renderingContextHandler = (RenderingContextHandler) smanager.lookup(RenderingContextHandler.ROLE); 122 _searchServiceInstanceManager = (SearchServiceInstanceManager) smanager.lookup(SearchServiceInstanceManager.ROLE); 123 } 124 125 /** 126 * Returns <code>true</code> if the given instance of search service has some user input 127 * @param criterionTree The tree of criteria of the search service instance 128 * @param facets The facets of the search service instance 129 * @param proposedSorts The proposed sorts of the search service instance 130 * @return <code>true</code> if the given instance of search service has some user input 131 */ 132 public static boolean hasUserInput(Optional<AbstractTreeNode<FOSearchCriterion>> criterionTree, Collection<FacetDefinition> facets, Collection<SortDefinition> proposedSorts) 133 { 134 boolean hasUserCriteria = hasUserCriteria(criterionTree); 135 136 boolean hasUserFacet = !facets.isEmpty(); 137 138 boolean hasUserSort = !proposedSorts.isEmpty(); 139 140 return hasUserCriteria || hasUserFacet || hasUserSort; 141 } 142 143 /** 144 * Returns <code>true</code> if the given instance of search service has at least one user criterion 145 * @param criterionTree The tree of criteria of the search service instance 146 * @return <code>true</code> if the given instance of search service has at least one user criterion 147 */ 148 public static boolean hasUserCriteria(Optional<AbstractTreeNode<FOSearchCriterion>> criterionTree) 149 { 150 return criterionTree 151 .map(AbstractTreeNode::getFlatLeaves) 152 .orElse(Collections.emptySet()) 153 .stream() 154 .map(TreeLeaf::getValue) 155 .anyMatch(crit -> !crit.getMode().isStatic()); 156 } 157 158 /** 159 * Returns <code>true</code> if the given instance of search service has some user input 160 * @param serviceInstance The service instance 161 * @return <code>true</code> if the given instance of search service has some user input 162 */ 163 protected static boolean _hasUserInput(SearchServiceInstance serviceInstance) 164 { 165 return hasUserInput(serviceInstance.getCriterionTree(), serviceInstance.getFacets(), serviceInstance.getProposedSorts()); 166 } 167 168 @Override 169 public boolean isCacheable(Page currentPage, ZoneItem zoneItem) 170 { 171 boolean isDebug = _isDebug(ContextHelper.getRequest(_context), _renderingContextHandler); 172 173 if (isDebug) 174 { 175 return false; 176 } 177 178 SearchServiceInstance serviceInstance = _searchServiceInstanceManager.get(zoneItem.getId()); 179 ResultDisplay resultDisplay = serviceInstance.getResultDisplay(); 180 if (resultDisplay.getType() == ResultDisplayType.ON_PAGE) 181 { 182 String resultPageId = resultDisplay 183 .resultPage() 184 .map(Page::getId) 185 .orElseThrow(() -> new IllegalArgumentException("Error. resultPage() cannot return empty at this time.")); 186 // Cacheable if results are on ANOTHER PAGE 187 return !resultPageId.equals(currentPage.getId()); 188 } 189 190 // Cacheable if no user input and right checking mode is not 'EXACT' 191 return serviceInstance.getRightCheckingMode() != RightCheckingMode.EXACT && !_hasUserInput(serviceInstance); 192 } 193 194 /** 195 * Is debug enabled 196 * @param request the request 197 * @param renderingContextHandler the rendering context handler 198 * @return true if debug enable 199 */ 200 protected boolean _isDebug(Request request, RenderingContextHandler renderingContextHandler) 201 { 202 return SearchServiceDebugModeHelper.debugMode(request, renderingContextHandler) != null; 203 } 204 205 @Override 206 protected void configureParameters(Configuration parametersConfiguration) throws ConfigurationException 207 { 208 _ensureCreationHelperIsReady(); 209 210 DefaultConfiguration confWithAdditionalParameters = new DefaultConfiguration(parametersConfiguration); 211 212 Configuration[] groups = confWithAdditionalParameters.getChildren("group"); 213 Map<String, ParsedAdditionalParameterConf> parsedAdditionalParameterConfs = new LinkedHashMap<>(); 214 215 // first group 'General' 216 DefaultConfiguration generalGroupConf = (DefaultConfiguration) groups[0]; 217 parsedAdditionalParameterConfs.putAll(_parseAndInjectGeneralAdditionalParameters(generalGroupConf)); 218 219 // last group 'Display' > first fieldset 'Display' 220 DefaultConfiguration displayGroupConf = (DefaultConfiguration) groups[groups.length - 1]; 221 Configuration[] fieldsets = displayGroupConf.getChildren("fieldset"); 222 DefaultConfiguration displayFieldsetConf = (DefaultConfiguration) fieldsets[0]; 223 parsedAdditionalParameterConfs.putAll(_parseAndInjectDisplayAdditionalParameters(displayFieldsetConf)); 224 225 // configure of superclass 226 super.configureParameters(confWithAdditionalParameters); 227 228 // parameters are ready => save the additional ones 229 Collection<AdditionalSearchServiceParameter> additionalParameters = _buildAdditionalSearchServiceParameters(parsedAdditionalParameterConfs); 230 _creationHelper.setAdditionalParameters(additionalParameters); 231 232 // Default value of returnables 233 @SuppressWarnings("unchecked") 234 ServiceParameter<String> paramReturnables = (ServiceParameter<String>) _modelItems.get(PARAM_NAME_RETURNABLES); 235 String[] configuredDefault = paramReturnables.getDefaultValue(); 236 if (configuredDefault == null || configuredDefault.length == 0) 237 { 238 String defaultValue = StringUtils.join(_creationHelper.selectedReturnables(), ','); 239 paramReturnables.setDefaultValue(defaultValue); 240 } 241 242 // Criteria, facets, initial sorts and proposed sorts need widget parameters 243 List<String> fieldsReloadingWidget = _fieldsReloadingWidget(additionalParameters); 244 _setWidgetParameters(PARAM_NAME_CRITERIA, fieldsReloadingWidget); 245 _setWidgetParameters(PARAM_NAME_FACETS, fieldsReloadingWidget); 246 _setWidgetParameters(PARAM_NAME_INITIAL_SORTS, fieldsReloadingWidget); 247 _setWidgetParameters(PARAM_NAME_PROPOSED_SORTS, fieldsReloadingWidget); 248 } 249 250 private void _ensureCreationHelperIsReady() throws ConfigurationException 251 { 252 if (!_creationHelper.isReady()) 253 { 254 try 255 { 256 _creationHelper.initialize(); 257 } 258 catch (Exception e) 259 { 260 throw new ConfigurationException("An error occured when initializing " + SearchServiceCreationHelper.ROLE, e); 261 } 262 } 263 } 264 265 static final class ParsedAdditionalParameterConf 266 { 267 String _name; 268 boolean _reload; 269 270 private ParsedAdditionalParameterConf(String name, boolean reload) 271 { 272 _name = name; 273 _reload = reload; 274 } 275 276 static ParsedAdditionalParameterConf _fromConf(Configuration c) 277 { 278 String name = c.getAttribute("name", null); 279 boolean reload = c.getAttributeAsBoolean("reloadCriteriaOnChange", false); 280 return new ParsedAdditionalParameterConf(name, reload); 281 } 282 283 String name() 284 { 285 return _name; 286 } 287 } 288 289 /** 290 * Retrieve the configurations of additional parameters for 'General' group, inject them into the group configuration and return their parsed representation. 291 * @param containerConf The container configuration for injecting. 292 * @return the {@link ParsedAdditionalParameterConf}s 293 */ 294 protected Map<String, ParsedAdditionalParameterConf> _parseAndInjectGeneralAdditionalParameters(DefaultConfiguration containerConf) 295 { 296 Collection<Configuration> additionalParameterConfigurations = _creationHelper.getAdditionalParameterConfigurationsForGeneral(); 297 return _parseAndInjectAdditionalParameters(additionalParameterConfigurations, containerConf); 298 } 299 300 /** 301 * Retrieve the configurations of additional parameters for 'Display' group, inject them into the group configuration and return their parsed representation. 302 * @param containerConf The container configuration for injecting. 303 * @return the {@link ParsedAdditionalParameterConf}s 304 */ 305 protected Map<String, ParsedAdditionalParameterConf> _parseAndInjectDisplayAdditionalParameters(DefaultConfiguration containerConf) 306 { 307 // The additional parameters have to be inserted at the beginning of the given container 308 309 // Remove the current children 310 Configuration[] currentContainerChildren = containerConf.getChildren(); 311 Arrays.stream(currentContainerChildren) 312 .forEach(containerConf::removeChild); 313 314 // Inject the additional parameters 315 Collection<Configuration> additionalParameterConfigurations = _creationHelper.getAdditionalParameterConfigurationsForDisplay(); 316 Map<String, ParsedAdditionalParameterConf> parsedAdditionalParameters = _parseAndInjectAdditionalParameters(additionalParameterConfigurations, containerConf); 317 318 // Add the removed parameters 319 Arrays.stream(currentContainerChildren) 320 .forEach(containerConf::addChild); 321 322 return parsedAdditionalParameters; 323 } 324 325 private Map<String, ParsedAdditionalParameterConf> _parseAndInjectAdditionalParameters(Collection<Configuration> additionalParameterConfigurations, DefaultConfiguration containerConf) 326 { 327 List<Configuration> orderedAdditionalParameterConfigurations = additionalParameterConfigurations.stream() 328 .sorted(Comparator.comparingLong(c -> c.getAttributeAsLong("order", Long.MAX_VALUE))) 329 .collect(Collectors.toList()); 330 331 // add additional confs to the 'Display' group conf 332 orderedAdditionalParameterConfigurations.forEach(containerConf::addChild); 333 334 return orderedAdditionalParameterConfigurations 335 .stream() 336 .map(ParsedAdditionalParameterConf::_fromConf) 337 .collect(LambdaUtils.Collectors.toLinkedHashMap( 338 ParsedAdditionalParameterConf::name, 339 Function.identity())); 340 } 341 342 /** 343 * Build the {@link AdditionalSearchServiceParameter}s from intermediate {@link ParsedAdditionalParameterConf} objects. 344 * @param parsedAdditionalParameterConfs The parsed configurations for each additional parameter 345 * @return the {@link AdditionalSearchServiceParameter}s 346 */ 347 protected Collection<AdditionalSearchServiceParameter> _buildAdditionalSearchServiceParameters(Map<String, ParsedAdditionalParameterConf> parsedAdditionalParameterConfs) 348 { 349 List<String> additionalParameterNames = parsedAdditionalParameterConfs.keySet() 350 .stream() 351 .collect(Collectors.toList()); 352 353 return _modelItems.entrySet() 354 .stream() 355 .filter(p -> additionalParameterNames.contains(p.getKey())) 356 .map(Map.Entry::getValue) 357 .filter(ServiceParameter.class::isInstance) 358 .map(p -> 359 { 360 @SuppressWarnings("unchecked") 361 ServiceParameter<Object> typedParam = (ServiceParameter<Object>) p; 362 String paramName = typedParam.getName(); 363 ParsedAdditionalParameterConf parsedConf = parsedAdditionalParameterConfs.get(paramName); 364 return new AdditionalSearchServiceParameter<>(typedParam, parsedConf._reload); 365 }) 366 .collect(Collectors.toList()); 367 } 368 369 /** 370 * Set the widget parameters of the given parameter, by adding the list of the fields reloading the widget 371 * @param parameterName The parameter name 372 * @param fieldsReloadingCriteriaWidget the names of the fields which will launch a reload of the widget when their value change 373 */ 374 protected void _setWidgetParameters(String parameterName, List<String> fieldsReloadingCriteriaWidget) 375 { 376 @SuppressWarnings("unchecked") 377 ServiceParameter<String> parameter = (ServiceParameter<String>) _modelItems.get(parameterName); 378 if (parameter != null) 379 { 380 Map<String, I18nizableText> params = new HashMap<>(); 381 Optional.ofNullable(parameter.getWidgetParameters()) 382 .ifPresent(params::putAll); 383 params.put("fieldsReloadingWidget", new I18nizableText(StringUtils.join(fieldsReloadingCriteriaWidget, ","))); 384 parameter.setWidgetParameters(params); 385 } 386 } 387 388 /** 389 * Gets the names of the fields which will launch a reload of this widget when their value change 390 * @param additionalParameters The additional parameters 391 * @return the names of the fields which will launch a reload of this widget when their value change 392 */ 393 protected List<String> _fieldsReloadingWidget(Collection<AdditionalSearchServiceParameter> additionalParameters) 394 { 395 return additionalParameters 396 .stream() 397 .filter(AdditionalSearchServiceParameter::reloadCriteriaOnChange) 398 .map(AdditionalSearchServiceParameter::getParameter) 399 .map(ServiceParameter::getName) 400 .collect(Collectors.toList()); 401 } 402}