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 private static boolean _hasUserInput(SearchServiceInstance serviceInstance) 159 { 160 return hasUserInput(serviceInstance.getCriterionTree(), serviceInstance.getFacets(), serviceInstance.getProposedSorts()); 161 } 162 163 @Override 164 public boolean isCacheable(Page currentPage, ZoneItem zoneItem) 165 { 166 boolean isDebug = _isDebug(ContextHelper.getRequest(_context), _renderingContextHandler); 167 168 if (isDebug) 169 { 170 return false; 171 } 172 173 SearchServiceInstance serviceInstance = _searchServiceInstanceManager.get(zoneItem.getId()); 174 ResultDisplay resultDisplay = serviceInstance.getResultDisplay(); 175 if (resultDisplay.getType() == ResultDisplayType.ON_PAGE) 176 { 177 String resultPageId = resultDisplay 178 .resultPage() 179 .map(Page::getId) 180 .orElseThrow(() -> new IllegalArgumentException("Error. resultPage() cannot return empty at this time.")); 181 // Cacheable if results are on ANOTHER PAGE 182 return !resultPageId.equals(currentPage.getId()); 183 } 184 185 // Cacheable if no user input and right checking mode is not 'EXACT' 186 return serviceInstance.getRightCheckingMode() != RightCheckingMode.EXACT && !_hasUserInput(serviceInstance); 187 } 188 189 private boolean _isDebug(Request request, RenderingContextHandler renderingContextHandler) 190 { 191 return SearchServiceDebugModeHelper.debugMode(request, renderingContextHandler) != null; 192 } 193 194 @Override 195 protected void configureParameters(Configuration parametersConfiguration) throws ConfigurationException 196 { 197 _ensureCreationHelperIsReady(); 198 199 DefaultConfiguration confWithAdditionalParameters = new DefaultConfiguration(parametersConfiguration); 200 201 Configuration[] groups = confWithAdditionalParameters.getChildren("group"); 202 Map<String, ParsedAdditionalParameterConf> parsedAdditionalParameterConfs = new LinkedHashMap<>(); 203 204 // first group 'General' 205 DefaultConfiguration generalGroupConf = (DefaultConfiguration) groups[0]; 206 parsedAdditionalParameterConfs.putAll(_parseAndInjectGeneralAdditionalParameters(generalGroupConf)); 207 208 // last group 'Display' > first fieldset 'Display' 209 DefaultConfiguration displayGroupConf = (DefaultConfiguration) groups[groups.length - 1]; 210 Configuration[] fieldsets = displayGroupConf.getChildren("fieldset"); 211 DefaultConfiguration displayFieldsetConf = (DefaultConfiguration) fieldsets[0]; 212 parsedAdditionalParameterConfs.putAll(_parseAndInjectDisplayAdditionalParameters(displayFieldsetConf)); 213 214 // configure of superclass 215 super.configureParameters(confWithAdditionalParameters); 216 217 // parameters are ready => save the additional ones 218 Collection<AdditionalSearchServiceParameter> additionalParameters = _buildAdditionalSearchServiceParameters(parsedAdditionalParameterConfs); 219 _creationHelper.setAdditionalParameters(additionalParameters); 220 221 // Default value of returnables 222 String defaultValue = StringUtils.join(_creationHelper.selectedReturnables(), ','); 223 @SuppressWarnings("unchecked") 224 ServiceParameter<String> paramReturnables = (ServiceParameter<String>) _modelItems.get(PARAM_NAME_RETURNABLES); 225 paramReturnables.setDefaultValue(defaultValue); 226 227 // Criteria, facets, initial sorts and proposed sorts need widget parameters 228 List<String> fieldsReloadingWidget = _fieldsReloadingWidget(additionalParameters); 229 _setWidgetParameters(PARAM_NAME_CRITERIA, fieldsReloadingWidget); 230 _setWidgetParameters(PARAM_NAME_FACETS, fieldsReloadingWidget); 231 _setWidgetParameters(PARAM_NAME_INITIAL_SORTS, fieldsReloadingWidget); 232 _setWidgetParameters(PARAM_NAME_PROPOSED_SORTS, fieldsReloadingWidget); 233 } 234 235 private void _ensureCreationHelperIsReady() throws ConfigurationException 236 { 237 if (!_creationHelper.isReady()) 238 { 239 try 240 { 241 _creationHelper.initialize(); 242 } 243 catch (Exception e) 244 { 245 throw new ConfigurationException("An error occured when initializing " + SearchServiceCreationHelper.ROLE, e); 246 } 247 } 248 } 249 250 static final class ParsedAdditionalParameterConf 251 { 252 String _name; 253 boolean _reload; 254 255 private ParsedAdditionalParameterConf(String name, boolean reload) 256 { 257 _name = name; 258 _reload = reload; 259 } 260 261 static ParsedAdditionalParameterConf _fromConf(Configuration c) 262 { 263 String name = c.getAttribute("name", null); 264 boolean reload = c.getAttributeAsBoolean("reloadCriteriaOnChange", false); 265 return new ParsedAdditionalParameterConf(name, reload); 266 } 267 268 String name() 269 { 270 return _name; 271 } 272 } 273 274 /** 275 * Retrieve the configurations of additional parameters for 'General' group, inject them into the group configuration and return their parsed representation. 276 * @param containerConf The container configuration for injecting. 277 * @return the {@link ParsedAdditionalParameterConf}s 278 */ 279 protected Map<String, ParsedAdditionalParameterConf> _parseAndInjectGeneralAdditionalParameters(DefaultConfiguration containerConf) 280 { 281 Collection<Configuration> additionalParameterConfigurations = _creationHelper.getAdditionalParameterConfigurationsForGeneral(); 282 return _parseAndInjectAdditionalParameters(additionalParameterConfigurations, containerConf); 283 } 284 285 /** 286 * Retrieve the configurations of additional parameters for 'Display' group, inject them into the group configuration and return their parsed representation. 287 * @param containerConf The container configuration for injecting. 288 * @return the {@link ParsedAdditionalParameterConf}s 289 */ 290 protected Map<String, ParsedAdditionalParameterConf> _parseAndInjectDisplayAdditionalParameters(DefaultConfiguration containerConf) 291 { 292 // The additional parameters have to be inserted at the beginning of the given container 293 294 // Remove the current children 295 Configuration[] currentContainerChildren = containerConf.getChildren(); 296 Arrays.stream(currentContainerChildren) 297 .forEach(containerConf::removeChild); 298 299 // Inject the additional parameters 300 Collection<Configuration> additionalParameterConfigurations = _creationHelper.getAdditionalParameterConfigurationsForDisplay(); 301 Map<String, ParsedAdditionalParameterConf> parsedAdditionalParameters = _parseAndInjectAdditionalParameters(additionalParameterConfigurations, containerConf); 302 303 // Add the removed parameters 304 Arrays.stream(currentContainerChildren) 305 .forEach(containerConf::addChild); 306 307 return parsedAdditionalParameters; 308 } 309 310 private Map<String, ParsedAdditionalParameterConf> _parseAndInjectAdditionalParameters(Collection<Configuration> additionalParameterConfigurations, DefaultConfiguration containerConf) 311 { 312 List<Configuration> orderedAdditionalParameterConfigurations = additionalParameterConfigurations.stream() 313 .sorted(Comparator.comparingLong(c -> c.getAttributeAsLong("order", Long.MAX_VALUE))) 314 .collect(Collectors.toList()); 315 316 // add additional confs to the 'Display' group conf 317 orderedAdditionalParameterConfigurations.forEach(containerConf::addChild); 318 319 return orderedAdditionalParameterConfigurations 320 .stream() 321 .map(ParsedAdditionalParameterConf::_fromConf) 322 .collect(LambdaUtils.Collectors.toLinkedHashMap( 323 ParsedAdditionalParameterConf::name, 324 Function.identity())); 325 } 326 327 /** 328 * Build the {@link AdditionalSearchServiceParameter}s from intermediate {@link ParsedAdditionalParameterConf} objects. 329 * @param parsedAdditionalParameterConfs The parsed configurations for each additional parameter 330 * @return the {@link AdditionalSearchServiceParameter}s 331 */ 332 protected Collection<AdditionalSearchServiceParameter> _buildAdditionalSearchServiceParameters(Map<String, ParsedAdditionalParameterConf> parsedAdditionalParameterConfs) 333 { 334 List<String> additionalParameterNames = parsedAdditionalParameterConfs.keySet() 335 .stream() 336 .collect(Collectors.toList()); 337 338 return _modelItems.entrySet() 339 .stream() 340 .filter(p -> additionalParameterNames.contains(p.getKey())) 341 .map(Map.Entry::getValue) 342 .filter(ServiceParameter.class::isInstance) 343 .map(p -> 344 { 345 @SuppressWarnings("unchecked") 346 ServiceParameter<Object> typedParam = (ServiceParameter<Object>) p; 347 String paramName = typedParam.getName(); 348 ParsedAdditionalParameterConf parsedConf = parsedAdditionalParameterConfs.get(paramName); 349 return new AdditionalSearchServiceParameter<>(typedParam, parsedConf._reload); 350 }) 351 .collect(Collectors.toList()); 352 } 353 354 /** 355 * Set the widget parameters of the given parameter, by adding the list of the fields reloading the widget 356 * @param parameterName The parameter name 357 * @param fieldsReloadingCriteriaWidget the names of the fields which will launch a reload of the widget when their value change 358 */ 359 protected void _setWidgetParameters(String parameterName, List<String> fieldsReloadingCriteriaWidget) 360 { 361 @SuppressWarnings("unchecked") 362 ServiceParameter<String> parameter = (ServiceParameter<String>) _modelItems.get(parameterName); 363 Map<String, I18nizableText> params = new HashMap<>(); 364 Optional.ofNullable(parameter.getWidgetParameters()) 365 .ifPresent(params::putAll); 366 params.put("fieldsReloadingWidget", new I18nizableText(StringUtils.join(fieldsReloadingCriteriaWidget, ","))); 367 parameter.setWidgetParameters(params); 368 } 369 370 /** 371 * Gets the names of the fields which will launch a reload of this widget when their value change 372 * @param additionalParameters The additional parameters 373 * @return the names of the fields which will launch a reload of this widget when their value change 374 */ 375 protected List<String> _fieldsReloadingWidget(Collection<AdditionalSearchServiceParameter> additionalParameters) 376 { 377 return additionalParameters 378 .stream() 379 .filter(AdditionalSearchServiceParameter::reloadCriteriaOnChange) 380 .map(AdditionalSearchServiceParameter::getParameter) 381 .map(ServiceParameter::getName) 382 .collect(Collectors.toList()); 383 } 384}