001/* 002 * Copyright 2015 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.cms.search; 017 018import java.util.ArrayList; 019import java.util.HashMap; 020import java.util.HashSet; 021import java.util.List; 022import java.util.Map; 023import java.util.Set; 024import java.util.function.BiFunction; 025 026import org.apache.avalon.framework.component.Component; 027import org.apache.avalon.framework.logger.AbstractLogEnabled; 028import org.apache.avalon.framework.service.ServiceException; 029import org.apache.avalon.framework.service.ServiceManager; 030import org.apache.avalon.framework.service.Serviceable; 031 032import org.ametys.cms.contenttype.ContentType; 033import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 034import org.ametys.cms.search.advanced.AbstractTreeNode; 035import org.ametys.cms.search.advanced.AdvancedQueryBuilder; 036import org.ametys.cms.search.advanced.TreeMaker; 037import org.ametys.cms.search.advanced.TreeMaker.ClientSideCriterionWrapper; 038import org.ametys.cms.search.advanced.WrappedValue; 039import org.ametys.cms.search.advanced.utils.TreePrinter; 040import org.ametys.cms.search.model.ContentTypesAwareCriterionDefinition; 041import org.ametys.cms.search.model.LanguageAwareCriterionDefinition; 042import org.ametys.cms.search.model.SearchModel; 043import org.ametys.cms.search.model.SearchModelCriterionDefinition; 044import org.ametys.cms.search.model.SearchModelHelper; 045import org.ametys.cms.search.query.AndQuery; 046import org.ametys.cms.search.query.ContentLanguageQuery; 047import org.ametys.cms.search.query.ContentTypeQuery; 048import org.ametys.cms.search.query.Query; 049import org.ametys.cms.search.query.Query.Operator; 050import org.ametys.cms.search.ui.model.SearchModelCriterionViewItem; 051import org.ametys.cms.search.ui.model.SearchUIModel; 052import org.ametys.runtime.model.ModelViewItem; 053import org.ametys.runtime.model.ViewItem; 054import org.ametys.runtime.model.ViewItemAccessor; 055 056/** 057 * Builds a {@link Query} object from a user search. 058 */ 059public class QueryBuilder extends AbstractLogEnabled implements Component, Serviceable 060{ 061 /** The component role. */ 062 public static final String ROLE = QueryBuilder.class.getName(); 063 064 /** Key of flag present in contextual parameters to indicate the current search is multilingual */ 065 public static final String MULTILINGUAL_SEARCH = "multilingualSearch"; 066 067 /** Key of flag present in contextual parameters to indicate the provided value was already escaped */ 068 public static final String VALUE_IS_ESCAPED = "isEscapedValue"; 069 070 /** Key of flag present in contextual parameters to indicate the current search model */ 071 public static final String SEARCH_MODEL = "searchModel"; 072 073 /** The content type extension point. */ 074 protected ContentTypeExtensionPoint _cTypeEP; 075 076 /** The Advanced tree maker */ 077 protected TreeMaker _advancedTreeMaker; 078 079 /** The advanced query builder */ 080 protected AdvancedQueryBuilder _advancedQueryBuilder; 081 082 /** The search model helper */ 083 protected SearchModelHelper _searchModelHelper; 084 085 @Override 086 public void service(ServiceManager serviceManager) throws ServiceException 087 { 088 _cTypeEP = (ContentTypeExtensionPoint) serviceManager.lookup(ContentTypeExtensionPoint.ROLE); 089 _advancedTreeMaker = (TreeMaker) serviceManager.lookup(TreeMaker.ROLE); 090 _advancedQueryBuilder = (AdvancedQueryBuilder) serviceManager.lookup(AdvancedQueryBuilder.ROLE); 091 _searchModelHelper = (SearchModelHelper) serviceManager.lookup(SearchModelHelper.ROLE); 092 } 093 094 /** 095 * Build the {@link Query} object. 096 * @param model the search model. 097 * @param searchMode the search mode. 098 * @param values the user search values. 099 * @param contextualParameters the search contextual parameters. 100 * @return a {@link Query} object representing the search. 101 */ 102 public Query build(SearchModel model, String searchMode, Map<String, Object> values, Map<String, Object> contextualParameters) 103 { 104 Map<String, Object> copiedValues = new HashMap<>(values); 105 List<Query> queries = new ArrayList<>(); 106 107 Map<String, Object> modifiableContextualParameters = new HashMap<>(contextualParameters); 108 modifiableContextualParameters.put(SEARCH_MODEL, model); 109 110 Set<String> modelContentTypes = model.getContentTypes(contextualParameters); 111 if (_isMultilingualSearch(modelContentTypes)) 112 { 113 modifiableContextualParameters.put(MULTILINGUAL_SEARCH, true); 114 } 115 116 Set<String> modelExcludedCTypes = model.getExcludedContentTypes(modifiableContextualParameters); 117 if (!modelExcludedCTypes.isEmpty()) 118 { 119 // query is on `allContentTypes` field, so do not be redundant and only keep the top supertypes for a readable query 120 queries.add(new ContentTypeQuery(Operator.NE, _getOnlySuperTypes(modelExcludedCTypes))); 121 } 122 123 String language = _searchModelHelper.getCriteriaLanguage(model, searchMode, copiedValues, modifiableContextualParameters); 124 CriteriaQueries criteriaQueries = "advanced".equals(searchMode) && model instanceof SearchUIModel uiModel 125 ? getAdvancedCriteriaQuery(uiModel, copiedValues, language, modifiableContextualParameters) 126 : getCriteriaQueries(model, copiedValues, language, modifiableContextualParameters); 127 queries.addAll(criteriaQueries.queries()); 128 129 if ("advanced".equals(searchMode) && !criteriaQueries.isLanguageFound()) 130 { 131 Query contentLanguageQuery = new ContentLanguageQuery(language); 132 queries.add(contentLanguageQuery); 133 } 134 135 if (!criteriaQueries.areContentTypesFound()) 136 { 137 Query modelContentTypesQuery = _searchModelHelper.createContentTypeOrMixinQuery(modelContentTypes); 138 queries.add(modelContentTypesQuery); 139 } 140 141 return new AndQuery(queries); 142 } 143 144 private boolean _isMultilingualSearch(Set<String> cTypeIds) 145 { 146 if (cTypeIds == null) 147 { 148 return false; 149 } 150 151 for (String cTypeId : cTypeIds) 152 { 153 if (!_cTypeEP.getExtension(cTypeId).isMultilingual()) 154 { 155 return false; 156 } 157 } 158 159 // All concerned content types are multilingual 160 return true; 161 } 162 163 private Set<String> _getOnlySuperTypes(Set<String> cTypes) 164 { 165 Set<String> result = new HashSet<>(); 166 167 for (String cTypeId : cTypes) 168 { 169 ContentType cType = _cTypeEP.getExtension(cTypeId); 170 if (!_containsAny(cTypes, cType.getSupertypeIds())) 171 { 172 result.add(cTypeId); 173 } 174 } 175 176 return result; 177 } 178 179 // Returns true if at least one of the supertypeIds is in cTypes 180 private boolean _containsAny(Set<String> cTypes, String[] supertypeIds) 181 { 182 for (String supertypeId : supertypeIds) 183 { 184 if (cTypes.contains(supertypeId)) 185 { 186 return true; 187 } 188 } 189 return false; 190 } 191 192 /** 193 * Get the list of query on given model's criteria. 194 * @param model the model 195 * @param values The submitted values 196 * @param language The query language. 197 * @param contextualParameters The contextual parameters 198 * @return The criteria {@link Query}. 199 */ 200 protected CriteriaQueries getCriteriaQueries(SearchModel model, Map<String, Object> values, String language, Map<String, Object> contextualParameters) 201 { 202 return getCriteriaQueries(model.getCriteria(contextualParameters), values, language, contextualParameters); 203 } 204 205 /** 206 * Get the list of query on criteria. 207 * @param criteria the list of criteria 208 * @param values The submitted values 209 * @param language The query language. 210 * @param contextualParameters The contextual parameters 211 * @return The criteria {@link Query}. 212 */ 213 @SuppressWarnings("unchecked") 214 protected CriteriaQueries getCriteriaQueries(ViewItemAccessor criteria, Map<String, Object> values, String language, Map<String, Object> contextualParameters) 215 { 216 List<Query> queries = new ArrayList<>(); 217 boolean isLanguageFound = false; 218 boolean areContentTypesFound = false; 219 220 for (ViewItem viewItem : criteria.getViewItems()) 221 { 222 if (viewItem instanceof ModelViewItem modelViewItem 223 && modelViewItem.getDefinition() instanceof SearchModelCriterionDefinition criterion) 224 { 225 Object submitValue = values.get(criterion.getName()); 226 227 // If the criterion is hidden, take the default value (fixed in the search model). 228 // Otherwise take the standard user value. 229 Object untypedValue = modelViewItem instanceof SearchModelCriterionViewItem criterionViewItem && criterionViewItem.isHidden() 230 ? criterion.getDefaultValue() 231 : submitValue; 232 233 Object value = criterion.convertQueryValue(untypedValue, contextualParameters); 234 Query query = criterion.getQuery(value, values, language, contextualParameters); 235 if (query != null) 236 { 237 queries.add(query); 238 } 239 240 isLanguageFound = criterion instanceof LanguageAwareCriterionDefinition || isLanguageFound; 241 areContentTypesFound = criterion instanceof ContentTypesAwareCriterionDefinition || areContentTypesFound; 242 } 243 244 if (viewItem instanceof ViewItemAccessor itemAccessor) 245 { 246 CriteriaQueries criteriaQueries = getCriteriaQueries(itemAccessor, values, language, contextualParameters); 247 queries.addAll(criteriaQueries.queries()); 248 isLanguageFound = criteriaQueries.isLanguageFound() || isLanguageFound; 249 areContentTypesFound = criteriaQueries.areContentTypesFound() || areContentTypesFound; 250 } 251 } 252 253 return new CriteriaQueries(queries, isLanguageFound, areContentTypesFound); 254 } 255 256 /** 257 * Get a complex Query from the advanced search values. 258 * @param model the model containing criterion definitions 259 * @param values The submitted values 260 * @param language The query language. 261 * @param contextualParameters The contextual parameters 262 * @return The criteria {@link Query}. 263 */ 264 protected CriteriaQueries getAdvancedCriteriaQuery(SearchUIModel model, Map<String, Object> values, String language, Map<String, Object> contextualParameters) 265 { 266 AbstractTreeNode<ValuedCriterion<Object>> tree = _createTreeNode(model, values, contextualParameters); 267 if (tree == null) 268 { 269 return null; 270 } 271 272 if (getLogger().isDebugEnabled()) 273 { 274 getLogger().debug("\n" + TreePrinter.print(tree, c -> "{" + c._criterionDefinition.getName() + ": Operator=" + c._op + ", Value=" + c._value + "}")); 275 } 276 Query query = _advancedQueryBuilder.build(tree, valuedCrit -> valuedCrit.toQuery(language, contextualParameters)); 277 278 boolean isLanguageFound = AbstractTreeNode.walk(tree, 279 // leaf => check if it is a criterion managing language 280 leaf -> leaf.getValue().isLanguageCriterion(), 281 // check if one of the leaf managed language 282 (booleans, operator) -> booleans.findAny().orElse(false)); 283 284 boolean areContentTypesFound = AbstractTreeNode.walk(tree, 285 // leaf => check if it is a criterion managing content types 286 leaf -> leaf.getValue().isContentTypesCriterion(), 287 // check if one of the leaf managed content types 288 (booleans, operator) -> booleans.findAny().orElse(false)); 289 290 return new CriteriaQueries(query != null ? List.of(query) : List.of(), isLanguageFound, areContentTypesFound); 291 } 292 293 @SuppressWarnings("unchecked") 294 private <T> AbstractTreeNode<ValuedCriterion<T>> _createTreeNode(SearchUIModel model, Map<String, Object> values, Map<String, Object> contextualParameters) 295 { 296 return _advancedTreeMaker.create(values, clientSideCrit -> new ValuedCriterion<>((SearchModelCriterionDefinition<T>) model.getAdvancedCriterion(clientSideCrit.getId(), contextualParameters).getDefinition(), clientSideCrit, _advancedTreeMaker)); 297 } 298 299 private static final class ValuedCriterion<T> 300 { 301 SearchModelCriterionDefinition<T> _criterionDefinition; 302 String _op; 303 Object _value; 304 private TreeMaker _advancedTreeMaker; 305 306 ValuedCriterion(SearchModelCriterionDefinition<T> criterionDefinition, ClientSideCriterionWrapper clientSideCriterion, TreeMaker advancedTreeMaker) 307 { 308 _criterionDefinition = criterionDefinition; 309 _op = clientSideCriterion.getStringOperator(); 310 _value = clientSideCriterion.getValue(); 311 _advancedTreeMaker = advancedTreeMaker; 312 } 313 314 Query toQuery(String language, Map<String, Object> contextualParameters) 315 { 316 BiFunction<WrappedValue, Operator, Query> toQuery = (transformedVal, realOperator) -> _criterionDefinition.getQuery(transformedVal.getValue(), realOperator, language, contextualParameters); 317 Object transformedValue = _criterionDefinition.convertQueryValue(_value, contextualParameters); 318 WrappedValue wrappedValue = new WrappedValue(transformedValue); 319 return _advancedTreeMaker.toQuery(wrappedValue, _op, toQuery, language, contextualParameters); 320 } 321 322 boolean isLanguageCriterion() 323 { 324 return _criterionDefinition instanceof LanguageAwareCriterionDefinition; 325 } 326 327 boolean isContentTypesCriterion() 328 { 329 return _criterionDefinition instanceof ContentTypesAwareCriterionDefinition; 330 } 331 } 332 333 private record CriteriaQueries(List<Query> queries, boolean isLanguageFound, boolean areContentTypesFound) { /* Empty */ } 334}