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