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().getPage().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 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<FOSearchCriterion> _createCriterionTree(Map<String, Object> criteriaValues, Map<String, SearchCriterionDefinition> searchCriterionDefinitions, Incrementor incrementor, Map<String, Object> contextualParameters) 252 { 253 Function<ClientSideCriterionWrapper, FOSearchCriterion> leafValueMaker = crit -> _createSearchCriterion(crit, searchCriterionDefinitions, incrementor, contextualParameters); 254 return _treeMaker.create(criteriaValues, leafValueMaker); 255 } 256 257 private FOSearchCriterion _createSearchCriterion(ClientSideCriterionWrapper critWrapper, Map<String, SearchCriterionDefinition> searchCriterionDefinitions, Incrementor incrementor, Map<String, Object> contextualParameters) 258 { 259 String criterionDefId = critWrapper.getId(); 260 SearchCriterionDefinition criterionDefinition = searchCriterionDefinitions.get(criterionDefId); 261 Objects.requireNonNull(criterionDefinition, String.format("The SearchCriterionDefinition for id '%s' must be non null", criterionDefId)); 262 263 // Generate an id 264 incrementor.increment(); 265 String id = criterionDefId + "$" + incrementor.getCount(); 266 267 Map<String, Object> otherProperties = critWrapper.getOtherProperties(); 268 String mode = (String) otherProperties.get("mode"); 269 270 @SuppressWarnings("unchecked") 271 RestrictedValues restrictedValues = "RESTRICTED_USER_INPUT".equals(mode) ? _restrictedValues((List<Object>) otherProperties.get("restrictedValues"), criterionDefinition, contextualParameters) : null; 272 Object staticValue = "STATIC".equals(mode) ? critWrapper.getValue() : null; 273 274 return new FOSearchCriterion( 275 id, 276 criterionDefinition, 277 critWrapper.getStringOperator(), 278 FOSearchCriterionMode.valueOf(mode), 279 restrictedValues, 280 staticValue 281 ); 282 } 283 284 private RestrictedValues _restrictedValues(List<Object> values, SearchCriterionDefinition criterionDefinition, Map<String, Object> contextualParameters) 285 { 286 return criterionDefinition.getEnumeratedValues(contextualParameters) 287 .map(enumeratedValues -> enumeratedValues.getRestrictedValuesFor(values)) 288 .orElseThrow(() -> new IllegalStateException("An unexpected error occured. There must be restricted values at this point.")); 289 } 290 291 private Map<String, Object> _contextualParameters(Site currentSite) 292 { 293 return new HashMap<>(ImmutableMap.of("siteName", currentSite.getName())); 294 } 295 296 private Collection<FacetDefinition> _createFacetDefinitions(ModelAwareDataHolder serviceParameters, Map<String, FacetDefinition> availableFacets) 297 { 298 String[] facetStrs = serviceParameters.getValue(SearchService.PARAM_NAME_FACETS, false, new String[0]); 299 return Stream.of(facetStrs) 300 .map(availableFacets::get) 301 .filter(Objects::nonNull) 302 .collect(Collectors.toList()); 303 } 304 305 private List<Pair<SortDefinition, Sort.Order>> _createInitialSorts(ModelAwareDataHolder serviceParameters, Map<String, SortDefinition> availableSorts) 306 { 307 String[] initialSortIds = serviceParameters.getValue(SearchService.PARAM_NAME_INITIAL_SORTS, false, new String[0]); 308 return Stream.of(initialSortIds) 309 .map(_json::convertJsonToMap) 310 .map(json -> _initialSort(json, availableSorts)) 311 .collect(Collectors.toList()); 312 } 313 314 private Pair<SortDefinition, Sort.Order> _initialSort(Map<String, Object> json, Map<String, SortDefinition> availableSorts) 315 { 316 String sortDefId = (String) json.get("name"); 317 Sort.Order direction = Sort.Order.valueOf((String) json.get("sort")); 318 SortDefinition sortDef = availableSorts.get(sortDefId); 319 Objects.requireNonNull(sortDef); 320 return Pair.of(sortDef, direction); 321 } 322 323 private Collection<SortDefinition> _createProposedSortDefinitions(ModelAwareDataHolder serviceParameters, Map<String, SortDefinition> availableSorts) 324 { 325 String[] proposedSortStrs = serviceParameters.getValue(SearchService.PARAM_NAME_PROPOSED_SORTS, false, new String[0]); 326 return Stream.of(proposedSortStrs) 327 .map(availableSorts::get) 328 .filter(Objects::nonNull) 329 .collect(Collectors.toList()); 330 } 331 332 private RightCheckingMode _rightCheckingMode(ModelAwareDataHolder serviceParameters, boolean hasUserInput) 333 { 334 String rightCheckingModeStr = serviceParameters.getValue(SearchService.PARAM_NAME_RIGHT_CHECKING_MODE); 335 RightCheckingMode rightCheckingMode = RightCheckingMode.valueOf(rightCheckingModeStr.toUpperCase()); 336 if (hasUserInput && rightCheckingMode == RightCheckingMode.FAST) 337 { 338 // It cannot be cached with FAST => force to EXACT 339 return RightCheckingMode.EXACT; 340 } 341 else 342 { 343 return rightCheckingMode; 344 } 345 } 346 347 private ResultDisplay _createResultDisplay(ModelAwareDataHolder serviceParameters, boolean hasUserCriteria) 348 { 349 ResultDisplayType type; 350 if (hasUserCriteria) 351 { 352 type = ResultDisplayType.valueOf(serviceParameters.getValue(SearchService.PARAM_NAME_RESULT_PLACE)); 353 } 354 else 355 { 356 type = ResultDisplayType.ABOVE_CRITERIA; 357 } 358 359 String pageId = null; 360 if (type == ResultDisplayType.ON_PAGE) 361 { 362 pageId = serviceParameters.getValue(SearchService.PARAM_NAME_RESULT_PAGE); 363 } 364 365 Boolean launchSearchAtStartup = null; 366 if (!hasUserCriteria) 367 { 368 // Force to true 369 launchSearchAtStartup = true; 370 } 371 else if (type != ResultDisplayType.ON_PAGE) 372 { 373 launchSearchAtStartup = serviceParameters.getValue(SearchService.PARAM_NAME_LAUNCH_SEARCH_AT_STARTUP); 374 } 375 376 String serviceGroupId = ""; 377 if (hasUserCriteria && serviceParameters.hasValue(SearchService.PARAM_NAME_SERVICE_GROUP_ID)) 378 { 379 serviceGroupId = serviceParameters.getValue(SearchService.PARAM_NAME_SERVICE_GROUP_ID); 380 } 381 382 return new ResultDisplay(type, pageId, launchSearchAtStartup, serviceGroupId, _resolver); 383 } 384 385 private Link _createLink(ModelAwareDataHolder serviceParameters) 386 { 387 String targetPageId = serviceParameters.getValue(SearchService.PARAM_NAME_LINK_PAGE); 388 String targetPage = StringUtils.isNotEmpty(targetPageId) ? targetPageId : null; 389 String title = serviceParameters.getValue(SearchService.PARAM_NAME_LINK_TITLE, false, ""); 390 return new Link(targetPage, title, _resolver); 391 } 392} 393