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