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 _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 firstGroupConf = (DefaultConfiguration) groups[0]; 205 parsedAdditionalParameterConfs.putAll(_parseAndInjectGeneralAdditionalParameters(firstGroupConf)); 206 207 // last group 'Display' 208 DefaultConfiguration lastGroupConf = (DefaultConfiguration) groups[groups.length - 1]; 209 parsedAdditionalParameterConfs.putAll(_parseAndInjectDisplayAdditionalParameters(lastGroupConf)); 210 211 // configure of superclass 212 super.configureParameters(confWithAdditionalParameters); 213 214 // parameters are ready => save the additional ones 215 Collection<AdditionalSearchServiceParameter> additionalParameters = _buildAdditionalSearchServiceParameters(parsedAdditionalParameterConfs); 216 _creationHelper.setAdditionalParameters(additionalParameters); 217 218 // Default value of returnables 219 String defaultValue = StringUtils.join(_creationHelper.selectedReturnables(), ','); 220 @SuppressWarnings("unchecked") 221 ServiceParameter<String> paramReturnables = (ServiceParameter<String>) _modelItems.get(PARAM_NAME_RETURNABLES); 222 paramReturnables.setDefaultValue(defaultValue); 223 224 // Criteria, facets, initial sorts and proposed sorts need widget parameters 225 List<String> fieldsReloadingWidget = _fieldsReloadingWidget(additionalParameters); 226 _setWidgetParameters(PARAM_NAME_CRITERIA, fieldsReloadingWidget); 227 _setWidgetParameters(PARAM_NAME_FACETS, fieldsReloadingWidget); 228 _setWidgetParameters(PARAM_NAME_INITIAL_SORTS, fieldsReloadingWidget); 229 _setWidgetParameters(PARAM_NAME_PROPOSED_SORTS, fieldsReloadingWidget); 230 } 231 232 private void _ensureCreationHelperIsReady() throws ConfigurationException 233 { 234 if (!_creationHelper.isReady()) 235 { 236 try 237 { 238 _creationHelper.initialize(); 239 } 240 catch (Exception e) 241 { 242 throw new ConfigurationException("An error occured when initializing " + SearchServiceCreationHelper.ROLE, e); 243 } 244 } 245 } 246 247 static final class ParsedAdditionalParameterConf 248 { 249 String _name; 250 boolean _reload; 251 252 private ParsedAdditionalParameterConf(String name, boolean reload) 253 { 254 _name = name; 255 _reload = reload; 256 } 257 258 static ParsedAdditionalParameterConf _fromConf(Configuration c) 259 { 260 String name = c.getAttribute("name", null); 261 boolean reload = c.getAttributeAsBoolean("reloadCriteriaOnChange", false); 262 return new ParsedAdditionalParameterConf(name, reload); 263 } 264 265 String name() 266 { 267 return _name; 268 } 269 } 270 271 /** 272 * Retrieve the configurations of additional parameters for 'General' group, inject them into the group configuration and return their parsed representation. 273 * @param firstGroupConf The first group configuration ('General') for injecting. 274 * @return the {@link ParsedAdditionalParameterConf}s 275 */ 276 protected Map<String, ParsedAdditionalParameterConf> _parseAndInjectGeneralAdditionalParameters(DefaultConfiguration firstGroupConf) 277 { 278 List<Configuration> additionalParameterConfigurations = _creationHelper.getAdditionalParameterConfigurationsForGeneral() 279 .stream() 280 .sorted(Comparator.comparingLong(c -> c.getAttributeAsLong("order", Long.MAX_VALUE))) 281 .collect(Collectors.toList()); 282 283 // add additional confs to the 'General' group conf 284 additionalParameterConfigurations.forEach(firstGroupConf::addChild); 285 286 return additionalParameterConfigurations 287 .stream() 288 .map(ParsedAdditionalParameterConf::_fromConf) 289 .collect(LambdaUtils.Collectors.toLinkedHashMap( 290 ParsedAdditionalParameterConf::name, 291 Function.identity())); 292 } 293 294 /** 295 * Retrieve the configurations of additional parameters for 'Display' group, inject them into the group configuration and return their parsed representation. 296 * @param lastGroupConf The last group configuration ('Display') for injecting. 297 * @return the {@link ParsedAdditionalParameterConf}s 298 */ 299 protected Map<String, ParsedAdditionalParameterConf> _parseAndInjectDisplayAdditionalParameters(DefaultConfiguration lastGroupConf) 300 { 301 List<Configuration> additionalParameterConfigurations = _creationHelper.getAdditionalParameterConfigurationsForDisplay() 302 .stream() 303 .sorted(Comparator.comparingLong(c -> c.getAttributeAsLong("order", Long.MAX_VALUE))) 304 .collect(Collectors.toList()); 305 306 // add additional confs to the 'Display' group conf 307 additionalParameterConfigurations.forEach(lastGroupConf::addChild); 308 309 return additionalParameterConfigurations 310 .stream() 311 .map(ParsedAdditionalParameterConf::_fromConf) 312 .collect(LambdaUtils.Collectors.toLinkedHashMap( 313 ParsedAdditionalParameterConf::name, 314 Function.identity())); 315 } 316 317 /** 318 * Build the {@link AdditionalSearchServiceParameter}s from intermediate {@link ParsedAdditionalParameterConf} objects. 319 * @param parsedAdditionalParameterConfs The parsed configurations for each additional parameter 320 * @return the {@link AdditionalSearchServiceParameter}s 321 */ 322 protected Collection<AdditionalSearchServiceParameter> _buildAdditionalSearchServiceParameters(Map<String, ParsedAdditionalParameterConf> parsedAdditionalParameterConfs) 323 { 324 List<String> additionalParameterNames = parsedAdditionalParameterConfs.keySet() 325 .stream() 326 .collect(Collectors.toList()); 327 328 return _modelItems.entrySet() 329 .stream() 330 .filter(p -> additionalParameterNames.contains(p.getKey())) 331 .map(Map.Entry::getValue) 332 .filter(ServiceParameter.class::isInstance) 333 .map(p -> 334 { 335 @SuppressWarnings("unchecked") 336 ServiceParameter<Object> typedParam = (ServiceParameter<Object>) p; 337 String paramName = typedParam.getName(); 338 ParsedAdditionalParameterConf parsedConf = parsedAdditionalParameterConfs.get(paramName); 339 return new AdditionalSearchServiceParameter<>(typedParam, parsedConf._reload); 340 }) 341 .collect(Collectors.toList()); 342 } 343 344 /** 345 * Set the widget parameters of the given parameter, by adding the list of the fields reloading the widget 346 * @param parameterName The parameter name 347 * @param fieldsReloadingCriteriaWidget the names of the fields which will launch a reload of the widget when their value change 348 */ 349 protected void _setWidgetParameters(String parameterName, List<String> fieldsReloadingCriteriaWidget) 350 { 351 @SuppressWarnings("unchecked") 352 ServiceParameter<String> parameter = (ServiceParameter<String>) _modelItems.get(parameterName); 353 Map<String, I18nizableText> params = new HashMap<>(); 354 Optional.ofNullable(parameter.getWidgetParameters()) 355 .ifPresent(params::putAll); 356 params.put("fieldsReloadingWidget", new I18nizableText(StringUtils.join(fieldsReloadingCriteriaWidget, ","))); 357 parameter.setWidgetParameters(params); 358 } 359 360 /** 361 * Gets the names of the fields which will launch a reload of this widget when their value change 362 * @param additionalParameters The additional parameters 363 * @return the names of the fields which will launch a reload of this widget when their value change 364 */ 365 protected List<String> _fieldsReloadingWidget(Collection<AdditionalSearchServiceParameter> additionalParameters) 366 { 367 return additionalParameters 368 .stream() 369 .filter(AdditionalSearchServiceParameter::reloadCriteriaOnChange) 370 .map(AdditionalSearchServiceParameter::getParameter) 371 .map(ServiceParameter::getName) 372 .collect(Collectors.toList()); 373 } 374}