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.List; 022import java.util.Map; 023import java.util.Objects; 024import java.util.Optional; 025import java.util.function.Function; 026import java.util.stream.Collectors; 027import java.util.stream.Stream; 028 029import org.apache.avalon.framework.component.Component; 030import org.apache.avalon.framework.service.ServiceException; 031import org.apache.avalon.framework.service.ServiceManager; 032import org.apache.avalon.framework.service.Serviceable; 033import org.apache.commons.lang3.StringUtils; 034import org.apache.commons.lang3.tuple.Pair; 035import org.apache.commons.math3.util.IntegerSequence.Incrementor; 036 037import org.ametys.cms.search.Sort; 038import org.ametys.cms.search.advanced.AbstractTreeNode; 039import org.ametys.cms.search.advanced.TreeMaker; 040import org.ametys.cms.search.advanced.TreeMaker.ClientSideCriterionWrapper; 041import org.ametys.core.util.JSONUtils; 042import org.ametys.plugins.repository.AmetysObjectResolver; 043import org.ametys.plugins.repository.data.holder.ModelAwareDataHolder; 044import org.ametys.web.frontoffice.search.SearchService; 045import org.ametys.web.frontoffice.search.instance.model.ContextLang; 046import org.ametys.web.frontoffice.search.instance.model.FOSearchCriterion; 047import org.ametys.web.frontoffice.search.instance.model.FOSearchCriterionMode; 048import org.ametys.web.frontoffice.search.instance.model.Link; 049import org.ametys.web.frontoffice.search.instance.model.ResultDisplay; 050import org.ametys.web.frontoffice.search.instance.model.ResultDisplayType; 051import org.ametys.web.frontoffice.search.instance.model.RightCheckingMode; 052import org.ametys.web.frontoffice.search.instance.model.SearchContext; 053import org.ametys.web.frontoffice.search.instance.model.SiteContext; 054import org.ametys.web.frontoffice.search.instance.model.SiteContextType; 055import org.ametys.web.frontoffice.search.instance.model.SitemapContext; 056import org.ametys.web.frontoffice.search.instance.model.SitemapContextType; 057import org.ametys.web.frontoffice.search.metamodel.AdditionalParameterValueMap; 058import org.ametys.web.frontoffice.search.metamodel.AdditionalSearchServiceParameter; 059import org.ametys.web.frontoffice.search.metamodel.EnumeratedValues.RestrictedValues; 060import org.ametys.web.frontoffice.search.metamodel.FacetDefinition; 061import org.ametys.web.frontoffice.search.metamodel.Returnable; 062import org.ametys.web.frontoffice.search.metamodel.SearchCriterionDefinition; 063import org.ametys.web.frontoffice.search.metamodel.SearchServiceCreationHelper; 064import org.ametys.web.frontoffice.search.metamodel.Searchable; 065import org.ametys.web.frontoffice.search.metamodel.SortDefinition; 066import org.ametys.web.repository.page.ZoneItem; 067import org.ametys.web.repository.site.Site; 068import org.ametys.web.repository.site.SiteManager; 069 070import com.google.common.collect.ImmutableMap; 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, SearchCriterionDefinition> searchCriterionDefinitions = _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().getPage().getSite(); 123 Map<String, Object> contextualParameters = _contextualParameters(site); 124 AbstractTreeNode<FOSearchCriterion> criterionTree = _createCriterionTree(_json.convertJsonToMap(treeStr), searchCriterionDefinitions, incrementor, contextualParameters); 125 126 Map<String, FacetDefinition> availableFacets = _serviceCreationHelper.getFacetDefinitions(returnables, additionalParameterValues); 127 Collection<FacetDefinition> facets = _createFacetDefinitions(serviceParameters, availableFacets); 128 129 Map<String, SortDefinition> availableSorts = _serviceCreationHelper.getSortDefinitions(returnables, additionalParameterValues); 130 List<Pair<SortDefinition, Sort.Order>> initialSorts = _createInitialSorts(serviceParameters, availableSorts); 131 Collection<SortDefinition> 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 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 return new SearchContext( 173 _createSiteContext(config.get("sites")), 174 _createSitemapContext(config.get("search-sitemap-context")), 175 _getContextLang(config.get("context-lang")), 176 tags.getLeft(), 177 tags.getRight() 178 ); 179 } 180 181 @SuppressWarnings("unchecked") 182 private SiteContext _createSiteContext(Object sitesObj) 183 { 184 Map<String, Object> sitesAsMap = _json.convertJsonToMap((String) sitesObj); 185 SiteContextType siteContextType = SiteContextType.fromClientSideName((String) sitesAsMap.get("context")); 186 List<String> sites = null; 187 if (siteContextType == SiteContextType.AMONG) 188 { 189 sites = (List<String>) sitesAsMap.get("sites"); 190 } 191 192 return new SiteContext(siteContextType, sites, _siteManager); 193 } 194 195 @SuppressWarnings("unchecked") 196 private SitemapContext _createSitemapContext(Object sitemapObj) 197 { 198 Map<String, Object> sitemapAsMap = _json.convertJsonToMap((String) sitemapObj); 199 String sitemapContextTypeStr = (String) sitemapAsMap.get("context"); 200 SitemapContextType sitemapContextType = sitemapContextTypeStr == null ? SitemapContextType.CURRENT_SITE : SitemapContextType.valueOf(sitemapContextTypeStr); 201 List<String> pageList = null; 202 if (sitemapContextType == SitemapContextType.CHILD_PAGES_OF || sitemapContextType == SitemapContextType.DIRECT_CHILD_PAGES_OF) 203 { 204 Object pagesObj = sitemapAsMap.get("page"); 205 if (pagesObj instanceof String) 206 { 207 pageList = Collections.singletonList((String) pagesObj); 208 } 209 else 210 { 211 pageList = (List<String>) pagesObj; 212 } 213 } 214 215 return new SitemapContext(sitemapContextType, pageList, _resolver); 216 } 217 218 private ContextLang _getContextLang(Object langObj) 219 { 220 return ContextLang.valueOf((String) langObj); 221 } 222 223 @SuppressWarnings("unchecked") 224 private Pair<List<String>, Boolean> _getTags(Object tagsObj) 225 { 226 List<String> tagIds; 227 boolean autoposting = false; 228 if (tagsObj instanceof Map<?, ?>) 229 { 230 Map<String, Object> tagsAsMap = (Map<String, Object>) tagsObj; 231 tagIds = (List<String>) tagsAsMap.get("value"); 232 autoposting = (Boolean) tagsAsMap.get("autoposting"); 233 } 234 else 235 { 236 tagIds = (List<String>) tagsObj; 237 } 238 239 return Pair.of(tagIds, autoposting); 240 } 241 242 private AbstractTreeNode<FOSearchCriterion> _createCriterionTree(Map<String, Object> criteriaValues, Map<String, SearchCriterionDefinition> searchCriterionDefinitions, Incrementor incrementor, Map<String, Object> contextualParameters) 243 { 244 Function<ClientSideCriterionWrapper, FOSearchCriterion> leafValueMaker = crit -> _createSearchCriterion(crit, searchCriterionDefinitions, incrementor, contextualParameters); 245 return _treeMaker.create(criteriaValues, leafValueMaker); 246 } 247 248 private FOSearchCriterion _createSearchCriterion(ClientSideCriterionWrapper critWrapper, Map<String, SearchCriterionDefinition> searchCriterionDefinitions, Incrementor incrementor, Map<String, Object> contextualParameters) 249 { 250 String criterionDefId = critWrapper.getId(); 251 SearchCriterionDefinition criterionDefinition = searchCriterionDefinitions.get(criterionDefId); 252 Objects.requireNonNull(criterionDefinition, String.format("The SearchCriterionDefinition for id '%s' must be non null", criterionDefId)); 253 254 // Generate an id 255 incrementor.increment(); 256 String id = criterionDefId + "$" + incrementor.getCount(); 257 258 Map<String, Object> otherProperties = critWrapper.getOtherProperties(); 259 String mode = (String) otherProperties.get("mode"); 260 261 @SuppressWarnings("unchecked") 262 RestrictedValues restrictedValues = "RESTRICTED_USER_INPUT".equals(mode) ? _restrictedValues((List<Object>) otherProperties.get("restrictedValues"), criterionDefinition, contextualParameters) : null; 263 Object staticValue = "STATIC".equals(mode) ? critWrapper.getValue() : null; 264 265 return new FOSearchCriterion( 266 id, 267 criterionDefinition, 268 critWrapper.getStringOperator(), 269 FOSearchCriterionMode.valueOf(mode), 270 restrictedValues, 271 staticValue 272 ); 273 } 274 275 private RestrictedValues _restrictedValues(List<Object> values, SearchCriterionDefinition criterionDefinition, Map<String, Object> contextualParameters) 276 { 277 return criterionDefinition.getEnumeratedValues(contextualParameters) 278 .map(enumeratedValues -> enumeratedValues.getRestrictedValuesFor(values)) 279 .orElseThrow(() -> new IllegalStateException("An unexpected error occured. There must be restricted values at this point.")); 280 } 281 282 private Map<String, Object> _contextualParameters(Site currentSite) 283 { 284 return ImmutableMap.of("siteName", currentSite.getName()); 285 } 286 287 private Collection<FacetDefinition> _createFacetDefinitions(ModelAwareDataHolder serviceParameters, Map<String, FacetDefinition> availableFacets) 288 { 289 String[] facetStrs = serviceParameters.getValue(SearchService.PARAM_NAME_FACETS, false, new String[0]); 290 return Stream.of(facetStrs) 291 .map(availableFacets::get) 292 .filter(Objects::nonNull) 293 .collect(Collectors.toList()); 294 } 295 296 private List<Pair<SortDefinition, Sort.Order>> _createInitialSorts(ModelAwareDataHolder serviceParameters, Map<String, SortDefinition> availableSorts) 297 { 298 String[] initialSortIds = serviceParameters.getValue(SearchService.PARAM_NAME_INITIAL_SORTS, false, new String[0]); 299 return Stream.of(initialSortIds) 300 .map(_json::convertJsonToMap) 301 .map(json -> _initialSort(json, availableSorts)) 302 .collect(Collectors.toList()); 303 } 304 305 private Pair<SortDefinition, Sort.Order> _initialSort(Map<String, Object> json, Map<String, SortDefinition> availableSorts) 306 { 307 String sortDefId = (String) json.get("name"); 308 Sort.Order direction = Sort.Order.valueOf((String) json.get("sort")); 309 SortDefinition sortDef = availableSorts.get(sortDefId); 310 Objects.requireNonNull(sortDef); 311 return Pair.of(sortDef, direction); 312 } 313 314 private Collection<SortDefinition> _createProposedSortDefinitions(ModelAwareDataHolder serviceParameters, Map<String, SortDefinition> availableSorts) 315 { 316 String[] proposedSortStrs = serviceParameters.getValue(SearchService.PARAM_NAME_PROPOSED_SORTS, false, new String[0]); 317 return Stream.of(proposedSortStrs) 318 .map(availableSorts::get) 319 .filter(Objects::nonNull) 320 .collect(Collectors.toList()); 321 } 322 323 private RightCheckingMode _rightCheckingMode(ModelAwareDataHolder serviceParameters, boolean hasUserInput) 324 { 325 if (hasUserInput) 326 { 327 // Force to exact as it cannot be cached 328 return RightCheckingMode.EXACT; 329 } 330 else 331 { 332 String rightCheckingModeStr = serviceParameters.getValue(SearchService.PARAM_NAME_RIGHT_CHECKING_MODE); 333 return RightCheckingMode.valueOf(rightCheckingModeStr.toUpperCase()); 334 } 335 } 336 337 private ResultDisplay _createResultDisplay(ModelAwareDataHolder serviceParameters, boolean hasUserCriteria) 338 { 339 ResultDisplayType type; 340 if (hasUserCriteria) 341 { 342 type = ResultDisplayType.valueOf(serviceParameters.getValue(SearchService.PARAM_NAME_RESULT_PLACE)); 343 } 344 else 345 { 346 type = ResultDisplayType.ABOVE_CRITERIA; 347 } 348 349 String pageId = null; 350 if (type == ResultDisplayType.ON_PAGE) 351 { 352 pageId = serviceParameters.getValue(SearchService.PARAM_NAME_RESULT_PAGE); 353 } 354 355 Boolean launchSearchAtStartup = null; 356 if (!hasUserCriteria) 357 { 358 // Force to true 359 launchSearchAtStartup = true; 360 } 361 else if (type != ResultDisplayType.ON_PAGE) 362 { 363 launchSearchAtStartup = serviceParameters.getValue(SearchService.PARAM_NAME_LAUNCH_SEARCH_AT_STARTUP); 364 } 365 366 String serviceGroupId = ""; 367 if (hasUserCriteria && serviceParameters.hasValue(SearchService.PARAM_NAME_SERVICE_GROUP_ID)) 368 { 369 serviceGroupId = serviceParameters.getValue(SearchService.PARAM_NAME_SERVICE_GROUP_ID); 370 } 371 372 return new ResultDisplay(type, pageId, launchSearchAtStartup, serviceGroupId, _resolver); 373 } 374 375 private Link _createLink(ModelAwareDataHolder serviceParameters) 376 { 377 String targetPageId = serviceParameters.getValue(SearchService.PARAM_NAME_LINK_PAGE); 378 String targetPage = StringUtils.isNotEmpty(targetPageId) ? targetPageId : null; 379 String title = serviceParameters.getValue(SearchService.PARAM_NAME_LINK_TITLE, false, ""); 380 return new Link(targetPage, title, _resolver); 381 } 382} 383