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.Collection; 019import java.util.Collections; 020import java.util.Comparator; 021import java.util.HashMap; 022import java.util.LinkedHashMap; 023import java.util.List; 024import java.util.Map; 025import java.util.Optional; 026import java.util.function.Function; 027import java.util.stream.Collectors; 028 029import org.apache.avalon.framework.configuration.Configuration; 030import org.apache.avalon.framework.configuration.ConfigurationException; 031import org.apache.avalon.framework.configuration.DefaultConfiguration; 032import org.apache.avalon.framework.service.ServiceException; 033import org.apache.avalon.framework.service.ServiceManager; 034import org.apache.cocoon.components.ContextHelper; 035import org.apache.cocoon.environment.Request; 036import org.apache.commons.lang3.StringUtils; 037 038import org.ametys.cms.search.advanced.AbstractTreeNode; 039import org.ametys.cms.search.advanced.TreeLeaf; 040import org.ametys.core.util.LambdaUtils; 041import org.ametys.runtime.i18n.I18nizableText; 042import org.ametys.web.frontoffice.search.instance.SearchServiceInstance; 043import org.ametys.web.frontoffice.search.instance.SearchServiceInstanceManager; 044import org.ametys.web.frontoffice.search.instance.model.FOSearchCriterion; 045import org.ametys.web.frontoffice.search.instance.model.FOSearchCriterionMode; 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() != FOSearchCriterionMode.STATIC); 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 DefaultConfiguration confWithAdditionalParameters = new DefaultConfiguration(parametersConfiguration); 197 198 Configuration[] groups = confWithAdditionalParameters.getChildren("group"); 199 Map<String, ParsedAdditionalParameterConf> parsedAdditionalParameterConfs = new LinkedHashMap<>(); 200 201 // first group 'General' 202 DefaultConfiguration firstGroupConf = (DefaultConfiguration) groups[0]; 203 parsedAdditionalParameterConfs.putAll(_parseAndInjectGeneralAdditionalParameters(firstGroupConf)); 204 205 // last group 'Display' 206 DefaultConfiguration lastGroupConf = (DefaultConfiguration) groups[groups.length - 1]; 207 parsedAdditionalParameterConfs.putAll(_parseAndInjectDisplayAdditionalParameters(lastGroupConf)); 208 209 // configure of superclass 210 super.configureParameters(confWithAdditionalParameters); 211 212 // parameters are ready => save the additional ones 213 Collection<AdditionalSearchServiceParameter> additionalParameters = _buildAdditionalSearchServiceParameters(parsedAdditionalParameterConfs); 214 _creationHelper.setAdditionalParameters(additionalParameters); 215 216 // Default value of returnables 217 String defaultValue = StringUtils.join(_creationHelper.selectedReturnables(), ','); 218 @SuppressWarnings("unchecked") 219 ServiceParameter<String> paramReturnables = (ServiceParameter<String>) _modelItems.get(PARAM_NAME_RETURNABLES); 220 paramReturnables.setDefaultValue(defaultValue); 221 222 // Criteria, facets, initial sorts and proposed sorts need widget parameters 223 List<String> fieldsReloadingWidget = _fieldsReloadingWidget(additionalParameters); 224 _setWidgetParameters(PARAM_NAME_CRITERIA, fieldsReloadingWidget); 225 _setWidgetParameters(PARAM_NAME_FACETS, fieldsReloadingWidget); 226 _setWidgetParameters(PARAM_NAME_INITIAL_SORTS, fieldsReloadingWidget); 227 _setWidgetParameters(PARAM_NAME_PROPOSED_SORTS, fieldsReloadingWidget); 228 } 229 230 static final class ParsedAdditionalParameterConf 231 { 232 String _name; 233 boolean _reload; 234 235 private ParsedAdditionalParameterConf(String name, boolean reload) 236 { 237 _name = name; 238 _reload = reload; 239 } 240 241 static ParsedAdditionalParameterConf _fromConf(Configuration c) 242 { 243 String name = c.getAttribute("name", null); 244 boolean reload = c.getAttributeAsBoolean("reloadCriteriaOnChange", false); 245 return new ParsedAdditionalParameterConf(name, reload); 246 } 247 248 String name() 249 { 250 return _name; 251 } 252 } 253 254 /** 255 * Retrieve the configurations of additional parameters for 'General' group, inject them into the group configuration and return their parsed representation. 256 * @param firstGroupConf The first group configuration ('General') for injecting. 257 * @return the {@link ParsedAdditionalParameterConf}s 258 */ 259 protected Map<String, ParsedAdditionalParameterConf> _parseAndInjectGeneralAdditionalParameters(DefaultConfiguration firstGroupConf) 260 { 261 List<Configuration> additionalParameterConfigurations = _creationHelper.getAdditionalParameterConfigurationsForGeneral() 262 .stream() 263 .sorted(Comparator.comparingLong(c -> c.getAttributeAsLong("order", Long.MAX_VALUE))) 264 .collect(Collectors.toList()); 265 266 // add additional confs to the 'General' group conf 267 additionalParameterConfigurations.forEach(firstGroupConf::addChild); 268 269 return additionalParameterConfigurations 270 .stream() 271 .map(ParsedAdditionalParameterConf::_fromConf) 272 .collect(LambdaUtils.Collectors.toLinkedHashMap( 273 ParsedAdditionalParameterConf::name, 274 Function.identity())); 275 } 276 277 /** 278 * Retrieve the configurations of additional parameters for 'Display' group, inject them into the group configuration and return their parsed representation. 279 * @param lastGroupConf The last group configuration ('Display') for injecting. 280 * @return the {@link ParsedAdditionalParameterConf}s 281 */ 282 protected Map<String, ParsedAdditionalParameterConf> _parseAndInjectDisplayAdditionalParameters(DefaultConfiguration lastGroupConf) 283 { 284 List<Configuration> additionalParameterConfigurations = _creationHelper.getAdditionalParameterConfigurationsForDisplay() 285 .stream() 286 .sorted(Comparator.comparingLong(c -> c.getAttributeAsLong("order", Long.MAX_VALUE))) 287 .collect(Collectors.toList()); 288 289 // add additional confs to the 'Display' group conf 290 additionalParameterConfigurations.forEach(lastGroupConf::addChild); 291 292 return additionalParameterConfigurations 293 .stream() 294 .map(ParsedAdditionalParameterConf::_fromConf) 295 .collect(LambdaUtils.Collectors.toLinkedHashMap( 296 ParsedAdditionalParameterConf::name, 297 Function.identity())); 298 } 299 300 /** 301 * Build the {@link AdditionalSearchServiceParameter}s from intermediate {@link ParsedAdditionalParameterConf} objects. 302 * @param parsedAdditionalParameterConfs The parsed configurations for each additional parameter 303 * @return the {@link AdditionalSearchServiceParameter}s 304 */ 305 protected Collection<AdditionalSearchServiceParameter> _buildAdditionalSearchServiceParameters(Map<String, ParsedAdditionalParameterConf> parsedAdditionalParameterConfs) 306 { 307 List<String> additionalParameterNames = parsedAdditionalParameterConfs.keySet() 308 .stream() 309 .collect(Collectors.toList()); 310 311 return _modelItems.entrySet() 312 .stream() 313 .filter(p -> additionalParameterNames.contains(p.getKey())) 314 .map(Map.Entry::getValue) 315 .filter(ServiceParameter.class::isInstance) 316 .map(p -> 317 { 318 @SuppressWarnings("unchecked") 319 ServiceParameter<Object> typedParam = (ServiceParameter<Object>) p; 320 String paramName = typedParam.getName(); 321 ParsedAdditionalParameterConf parsedConf = parsedAdditionalParameterConfs.get(paramName); 322 return new AdditionalSearchServiceParameter<>(typedParam, parsedConf._reload); 323 }) 324 .collect(Collectors.toList()); 325 } 326 327 /** 328 * Set the widget parameters of the given parameter, by adding the list of the fields reloading the widget 329 * @param parameterName The parameter name 330 * @param fieldsReloadingCriteriaWidget the names of the fields which will launch a reload of the widget when their value change 331 */ 332 protected void _setWidgetParameters(String parameterName, List<String> fieldsReloadingCriteriaWidget) 333 { 334 @SuppressWarnings("unchecked") 335 ServiceParameter<String> parameter = (ServiceParameter<String>) _modelItems.get(parameterName); 336 Map<String, I18nizableText> params = new HashMap<>(); 337 Optional.ofNullable(parameter.getWidgetParameters()) 338 .ifPresent(params::putAll); 339 params.put("fieldsReloadingWidget", new I18nizableText(StringUtils.join(fieldsReloadingCriteriaWidget, ","))); 340 parameter.setWidgetParameters(params); 341 } 342 343 /** 344 * Gets the names of the fields which will launch a reload of this widget when their value change 345 * @param additionalParameters The additional parameters 346 * @return the names of the fields which will launch a reload of this widget when their value change 347 */ 348 protected List<String> _fieldsReloadingWidget(Collection<AdditionalSearchServiceParameter> additionalParameters) 349 { 350 return additionalParameters 351 .stream() 352 .filter(AdditionalSearchServiceParameter::reloadCriteriaOnChange) 353 .map(AdditionalSearchServiceParameter::getParameter) 354 .map(ServiceParameter::getName) 355 .collect(Collectors.toList()); 356 } 357}