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