001/* 002 * Copyright 2019 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.instance; 017 018import java.util.Arrays; 019import java.util.Collection; 020import java.util.Collections; 021import java.util.HashMap; 022import java.util.List; 023import java.util.Map; 024import java.util.Objects; 025import java.util.Optional; 026import java.util.function.Function; 027import java.util.stream.Collectors; 028import java.util.stream.Stream; 029 030import org.apache.avalon.framework.component.Component; 031import org.apache.avalon.framework.service.ServiceException; 032import org.apache.avalon.framework.service.ServiceManager; 033import org.apache.avalon.framework.service.Serviceable; 034import org.apache.commons.lang3.StringUtils; 035import org.apache.commons.lang3.tuple.Pair; 036import org.apache.commons.math3.util.IntegerSequence.Incrementor; 037 038import org.ametys.cms.search.Sort; 039import org.ametys.cms.search.advanced.AbstractTreeNode; 040import org.ametys.cms.search.advanced.TreeMaker; 041import org.ametys.cms.search.advanced.TreeMaker.ClientSideCriterionWrapper; 042import org.ametys.core.util.JSONUtils; 043import org.ametys.plugins.repository.AmetysObjectResolver; 044import org.ametys.plugins.repository.data.holder.ModelAwareDataHolder; 045import org.ametys.web.frontoffice.search.SearchService; 046import org.ametys.web.frontoffice.search.instance.model.ContextLang; 047import org.ametys.web.frontoffice.search.instance.model.FOSearchCriterion; 048import org.ametys.web.frontoffice.search.instance.model.FOSearchCriterionMode; 049import org.ametys.web.frontoffice.search.instance.model.Link; 050import org.ametys.web.frontoffice.search.instance.model.ResultDisplay; 051import org.ametys.web.frontoffice.search.instance.model.ResultDisplayType; 052import org.ametys.web.frontoffice.search.instance.model.RightCheckingMode; 053import org.ametys.web.frontoffice.search.instance.model.SearchContext; 054import org.ametys.web.frontoffice.search.instance.model.SiteContext; 055import org.ametys.web.frontoffice.search.instance.model.SiteContextType; 056import org.ametys.web.frontoffice.search.instance.model.SitemapContext; 057import org.ametys.web.frontoffice.search.instance.model.SitemapContextType; 058import org.ametys.web.frontoffice.search.metamodel.AdditionalParameterValueMap; 059import org.ametys.web.frontoffice.search.metamodel.AdditionalSearchServiceParameter; 060import org.ametys.web.frontoffice.search.metamodel.EnumeratedValues.RestrictedValues; 061import org.ametys.web.frontoffice.search.metamodel.FacetDefinition; 062import org.ametys.web.frontoffice.search.metamodel.Returnable; 063import org.ametys.web.frontoffice.search.metamodel.SearchCriterionDefinition; 064import org.ametys.web.frontoffice.search.metamodel.SearchServiceCreationHelper; 065import org.ametys.web.frontoffice.search.metamodel.Searchable; 066import org.ametys.web.frontoffice.search.metamodel.SortDefinition; 067import org.ametys.web.repository.page.ZoneItem; 068import org.ametys.web.repository.site.Site; 069import org.ametys.web.repository.site.SiteManager; 070 071/** 072 * The component able to {@link #createSearchServiceInstance create} some {@link SearchServiceInstance}s. 073 */ 074public class SearchServiceInstanceFactory implements Component, Serviceable 075{ 076 /** Avalon Role */ 077 public static final String ROLE = SearchServiceInstanceFactory.class.getName(); 078 079 private SearchServiceCreationHelper _serviceCreationHelper; 080 private AmetysObjectResolver _resolver; 081 private JSONUtils _json; 082 private SiteManager _siteManager; 083 private TreeMaker _treeMaker; 084 085 @Override 086 public void service(ServiceManager manager) throws ServiceException 087 { 088 _serviceCreationHelper = (SearchServiceCreationHelper) manager.lookup(SearchServiceCreationHelper.ROLE); 089 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 090 _json = (JSONUtils) manager.lookup(JSONUtils.ROLE); 091 _siteManager = (SiteManager) manager.lookup(SiteManager.ROLE); 092 _treeMaker = (TreeMaker) manager.lookup(TreeMaker.ROLE); 093 } 094 095 /** 096 * Creates a new {@link SearchServiceInstance} 097 * @param zoneItemId the id of the {@link ZoneItem} 098 * @return the created {@link SearchServiceInstance} which is placed at the given {@link ZoneItem} 099 */ 100 public SearchServiceInstance createSearchServiceInstance(String zoneItemId) 101 { 102 ZoneItem zoneItem = _resolver.resolveById(zoneItemId); 103 ModelAwareDataHolder serviceParameters = zoneItem.getServiceParameters(); 104 105 List<Returnable> returnables = _serviceCreationHelper.getReturnables(Arrays.asList(serviceParameters.getValue(SearchService.PARAM_NAME_RETURNABLES))); 106 Collection<AdditionalSearchServiceParameter> additionalParameters = _serviceCreationHelper.getAdditionalParameters(); 107 AdditionalParameterValueMap additionalParameterValues = _serviceCreationHelper.getAdditionalParameterValues(additionalParameters, serviceParameters); 108 109 String[] contextIds = serviceParameters.getValue(SearchService.PARAM_NAME_CONTEXTS, false, new String[0]); 110 Collection<SearchContext> contexts = Stream.of(contextIds) 111 .map(_json::convertJsonToMap) 112 .map(this::_createSearchContext) 113 .collect(Collectors.toList()); 114 115 Collection<Searchable> searchables = _serviceCreationHelper.getSearchables(returnables); 116 Map<String, SearchCriterionDefinition> searchCriterionDefinitions = _serviceCreationHelper.getCriterionDefinitions(searchables, additionalParameterValues); 117 Incrementor incrementor = Incrementor.create() 118 .withStart(0) 119 .withMaximalCount(Integer.MAX_VALUE); 120 String treeStr = serviceParameters.getValue(SearchService.PARAM_NAME_CRITERIA); 121 Site site = zoneItem.getZone().getSitemapElement().getSite(); 122 Map<String, Object> contextualParameters = _contextualParameters(contexts, site); 123 AbstractTreeNode<FOSearchCriterion> criterionTree = _createCriterionTree(_json.convertJsonToMap(treeStr), searchCriterionDefinitions, incrementor, contextualParameters); 124 125 Map<String, FacetDefinition> availableFacets = _serviceCreationHelper.getFacetDefinitions(returnables, additionalParameterValues); 126 Collection<FacetDefinition> facets = _createFacetDefinitions(serviceParameters, availableFacets); 127 128 Map<String, SortDefinition> availableSorts = _serviceCreationHelper.getSortDefinitions(returnables, additionalParameterValues); 129 List<Pair<SortDefinition, Sort.Order>> initialSorts = _createInitialSorts(serviceParameters, availableSorts); 130 Collection<SortDefinition> proposedSorts = _createProposedSortDefinitions(serviceParameters, availableSorts); 131 132 Long resultsPerPage = serviceParameters.getValue(SearchService.PARAM_NAME_RESULTS_PER_PAGE); 133 Long maxResults = serviceParameters.getValue(SearchService.PARAM_NAME_MAX_RESULTS); 134 135 boolean hasUserCriteria = SearchService.hasUserCriteria(Optional.ofNullable(criterionTree)); 136 boolean hasUserInput = SearchService.hasUserInput(Optional.ofNullable(criterionTree), facets, proposedSorts); 137 138 RightCheckingMode rightCheckingMode = _rightCheckingMode(serviceParameters, hasUserInput); 139 140 ResultDisplay resultDisplay = _createResultDisplay(serviceParameters, hasUserCriteria); 141 Link link = _createLink(serviceParameters); 142 143 SearchServiceInstance instance = new SearchServiceInstance( 144 zoneItemId, 145 serviceParameters.getValue(SearchService.PARAM_NAME_HEADER, false, ""), 146 returnables, 147 searchables, 148 additionalParameters, 149 additionalParameterValues, 150 contexts, 151 criterionTree, 152 serviceParameters.getValue(SearchService.PARAM_NAME_COMPUTE_COUNTS, false, false), 153 facets, 154 initialSorts, 155 proposedSorts, 156 resultsPerPage == null ? null : resultsPerPage.intValue(), 157 maxResults == null ? null : maxResults.intValue(), 158 rightCheckingMode, 159 serviceParameters.getValue(SearchService.PARAM_NAME_XSLT), 160 resultDisplay, 161 link, 162 serviceParameters.getValue(SearchService.PARAM_NAME_RSS), 163 serviceParameters.getValue(SearchService.PARAM_NAME_SAVE_USER_PREFS, true, false) 164 ); 165 return instance; 166 } 167 168 private SearchContext _createSearchContext(Map<String, Object> config) 169 { 170 Pair<List<String>, Boolean> tags = _getTags(config.get("tags")); 171 172 SitemapContext sitemapContext = _createSitemapContext(config.get("search-sitemap-context")); 173 return new SearchContext( 174 _createSiteContext(config.get("sites")), 175 sitemapContext, 176 _getContextLang(config.get("context-lang"), sitemapContext), 177 tags.getLeft(), 178 tags.getRight() 179 ); 180 } 181 182 @SuppressWarnings("unchecked") 183 private SiteContext _createSiteContext(Object sitesObj) 184 { 185 Map<String, Object> sitesAsMap = _json.convertJsonToMap((String) sitesObj); 186 SiteContextType siteContextType = SiteContextType.fromClientSideName((String) sitesAsMap.get("context")); 187 List<String> sites = null; 188 if (siteContextType == SiteContextType.AMONG) 189 { 190 sites = (List<String>) sitesAsMap.get("sites"); 191 } 192 193 return new SiteContext(siteContextType, sites, _siteManager); 194 } 195 196 @SuppressWarnings("unchecked") 197 private SitemapContext _createSitemapContext(Object sitemapObj) 198 { 199 Map<String, Object> sitemapAsMap = _json.convertJsonToMap((String) sitemapObj); 200 String sitemapContextTypeStr = (String) sitemapAsMap.get("context"); 201 SitemapContextType sitemapContextType = sitemapContextTypeStr == null ? SitemapContextType.CURRENT_SITE : SitemapContextType.valueOf(sitemapContextTypeStr); 202 List<String> pageList = null; 203 if (sitemapContextType == SitemapContextType.CHILD_PAGES_OF || sitemapContextType == SitemapContextType.DIRECT_CHILD_PAGES_OF) 204 { 205 Object pagesObj = sitemapAsMap.get("page"); 206 if (pagesObj instanceof String) 207 { 208 pageList = Collections.singletonList((String) pagesObj); 209 } 210 else 211 { 212 pageList = (List<String>) pagesObj; 213 } 214 } 215 216 return new SitemapContext(sitemapContextType, pageList, _resolver); 217 } 218 219 private ContextLang _getContextLang(Object langObj, SitemapContext sitemapContext) 220 { 221 if (sitemapContext.getType() == SitemapContextType.CURRENT_SITE) 222 { 223 return ContextLang.valueOf((String) langObj); 224 } 225 else 226 { 227 return ContextLang.ALL; 228 } 229 } 230 231 @SuppressWarnings("unchecked") 232 private Pair<List<String>, Boolean> _getTags(Object tagsObj) 233 { 234 List<String> tagIds; 235 boolean autoposting = false; 236 if (tagsObj instanceof Map<?, ?>) 237 { 238 Map<String, Object> tagsAsMap = (Map<String, Object>) tagsObj; 239 tagIds = (List<String>) tagsAsMap.get("value"); 240 autoposting = (Boolean) tagsAsMap.get("autoposting"); 241 } 242 else 243 { 244 tagIds = (List<String>) tagsObj; 245 } 246 247 return Pair.of(tagIds, autoposting); 248 } 249 250 private AbstractTreeNode<FOSearchCriterion> _createCriterionTree(Map<String, Object> criteriaValues, Map<String, SearchCriterionDefinition> searchCriterionDefinitions, Incrementor incrementor, Map<String, Object> contextualParameters) 251 { 252 Function<ClientSideCriterionWrapper, FOSearchCriterion> leafValueMaker = crit -> _createSearchCriterion(crit, searchCriterionDefinitions, incrementor, contextualParameters); 253 return _treeMaker.create(criteriaValues, leafValueMaker); 254 } 255 256 private FOSearchCriterion _createSearchCriterion(ClientSideCriterionWrapper critWrapper, Map<String, SearchCriterionDefinition> searchCriterionDefinitions, Incrementor incrementor, Map<String, Object> contextualParameters) 257 { 258 String criterionDefId = critWrapper.getId(); 259 SearchCriterionDefinition criterionDefinition = searchCriterionDefinitions.get(criterionDefId); 260 Objects.requireNonNull(criterionDefinition, String.format("The SearchCriterionDefinition for id '%s' must be non null", criterionDefId)); 261 262 // Generate an id 263 incrementor.increment(); 264 String id = criterionDefId + "$" + incrementor.getCount(); 265 266 Map<String, Object> otherProperties = critWrapper.getOtherProperties(); 267 String mode = (String) otherProperties.get("mode"); 268 269 @SuppressWarnings("unchecked") 270 RestrictedValues restrictedValues = "RESTRICTED_USER_INPUT".equals(mode) ? _restrictedValues((List<Object>) otherProperties.get("restrictedValues"), criterionDefinition, contextualParameters) : null; 271 Object staticValue = "STATIC".equals(mode) ? critWrapper.getValue() : null; 272 273 return new FOSearchCriterion( 274 id, 275 criterionDefinition, 276 critWrapper.getStringOperator(), 277 FOSearchCriterionMode.valueOf(mode), 278 restrictedValues, 279 staticValue 280 ); 281 } 282 283 private RestrictedValues _restrictedValues(List<Object> values, SearchCriterionDefinition criterionDefinition, Map<String, Object> contextualParameters) 284 { 285 return criterionDefinition.getEnumeratedValues(contextualParameters) 286 .map(enumeratedValues -> enumeratedValues.getRestrictedValuesFor(values)) 287 .orElseThrow(() -> new IllegalStateException("An unexpected error occured. There must be restricted values at this point.")); 288 } 289 290 private Map<String, Object> _contextualParameters(Collection<SearchContext> searchContexts, Site currentSite) 291 { 292 Map<String, Object> contextualParameters = new HashMap<>(); 293 294 contextualParameters.put("searchContexts", searchContexts); 295 contextualParameters.put("siteName", currentSite.getName()); 296 297 return contextualParameters; 298 } 299 300 private Collection<FacetDefinition> _createFacetDefinitions(ModelAwareDataHolder serviceParameters, Map<String, FacetDefinition> availableFacets) 301 { 302 if (serviceParameters.hasDefinition(SearchService.PARAM_NAME_FACETS)) 303 { 304 String[] facetStrs = serviceParameters.getValue(SearchService.PARAM_NAME_FACETS, false, new String[0]); 305 return Stream.of(facetStrs) 306 .map(availableFacets::get) 307 .filter(Objects::nonNull) 308 .collect(Collectors.toList()); 309 } 310 return Collections.EMPTY_LIST; 311 312 } 313 314 private List<Pair<SortDefinition, Sort.Order>> _createInitialSorts(ModelAwareDataHolder serviceParameters, Map<String, SortDefinition> availableSorts) 315 { 316 if (serviceParameters.hasDefinition(SearchService.PARAM_NAME_INITIAL_SORTS)) 317 { 318 String[] initialSortIds = serviceParameters.getValue(SearchService.PARAM_NAME_INITIAL_SORTS, false, new String[0]); 319 return Stream.of(initialSortIds) 320 .map(_json::convertJsonToMap) 321 .map(json -> _initialSort(json, availableSorts)) 322 .collect(Collectors.toList()); 323 } 324 return Collections.EMPTY_LIST; 325 } 326 327 private Pair<SortDefinition, Sort.Order> _initialSort(Map<String, Object> json, Map<String, SortDefinition> availableSorts) 328 { 329 String sortDefId = (String) json.get("name"); 330 Sort.Order direction = Sort.Order.valueOf((String) json.get("sort")); 331 SortDefinition sortDef = availableSorts.get(sortDefId); 332 Objects.requireNonNull(sortDef); 333 return Pair.of(sortDef, direction); 334 } 335 336 private Collection<SortDefinition> _createProposedSortDefinitions(ModelAwareDataHolder serviceParameters, Map<String, SortDefinition> availableSorts) 337 { 338 if (serviceParameters.hasDefinition(SearchService.PARAM_NAME_PROPOSED_SORTS)) 339 { 340 String[] proposedSortStrs = serviceParameters.getValue(SearchService.PARAM_NAME_PROPOSED_SORTS, false, new String[0]); 341 return Stream.of(proposedSortStrs) 342 .map(availableSorts::get) 343 .filter(Objects::nonNull) 344 .collect(Collectors.toList()); 345 } 346 return Collections.EMPTY_LIST; 347 } 348 349 private RightCheckingMode _rightCheckingMode(ModelAwareDataHolder serviceParameters, boolean hasUserInput) 350 { 351 String rightCheckingModeStr = serviceParameters.getValue(SearchService.PARAM_NAME_RIGHT_CHECKING_MODE); 352 RightCheckingMode rightCheckingMode = RightCheckingMode.valueOf(rightCheckingModeStr.toUpperCase()); 353 if (hasUserInput && rightCheckingMode == RightCheckingMode.FAST) 354 { 355 // It cannot be cached with FAST => force to EXACT 356 return RightCheckingMode.EXACT; 357 } 358 else 359 { 360 return rightCheckingMode; 361 } 362 } 363 364 private ResultDisplay _createResultDisplay(ModelAwareDataHolder serviceParameters, boolean hasUserCriteria) 365 { 366 ResultDisplayType type; 367 if (hasUserCriteria) 368 { 369 type = ResultDisplayType.valueOf(serviceParameters.getValue(SearchService.PARAM_NAME_RESULT_PLACE)); 370 } 371 else 372 { 373 type = ResultDisplayType.ABOVE_CRITERIA; 374 } 375 376 String pageId = null; 377 if (type == ResultDisplayType.ON_PAGE) 378 { 379 pageId = serviceParameters.getValue(SearchService.PARAM_NAME_RESULT_PAGE); 380 } 381 382 Boolean launchSearchAtStartup = null; 383 if (!hasUserCriteria) 384 { 385 // Force to true 386 launchSearchAtStartup = true; 387 } 388 else if (type != ResultDisplayType.ON_PAGE) 389 { 390 launchSearchAtStartup = serviceParameters.getValue(SearchService.PARAM_NAME_LAUNCH_SEARCH_AT_STARTUP); 391 } 392 393 String serviceGroupId = ""; 394 if (hasUserCriteria && serviceParameters.hasValue(SearchService.PARAM_NAME_SERVICE_GROUP_ID)) 395 { 396 serviceGroupId = serviceParameters.getValue(SearchService.PARAM_NAME_SERVICE_GROUP_ID); 397 } 398 399 return new ResultDisplay(type, pageId, launchSearchAtStartup, serviceGroupId, _resolver); 400 } 401 402 private Link _createLink(ModelAwareDataHolder serviceParameters) 403 { 404 String targetPageId = serviceParameters.getValue(SearchService.PARAM_NAME_LINK_PAGE); 405 String targetPage = StringUtils.isNotEmpty(targetPageId) ? targetPageId : null; 406 String title = serviceParameters.getValue(SearchService.PARAM_NAME_LINK_TITLE, false, ""); 407 return new Link(targetPage, title, _resolver); 408 } 409} 410