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.Collections; 020import java.util.List; 021import java.util.Map; 022import java.util.Set; 023 024import org.apache.avalon.framework.component.Component; 025import org.apache.avalon.framework.logger.AbstractLogEnabled; 026import org.apache.avalon.framework.service.ServiceException; 027import org.apache.avalon.framework.service.ServiceManager; 028import org.apache.avalon.framework.service.Serviceable; 029import org.apache.commons.lang3.StringUtils; 030 031import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 032import org.ametys.cms.languages.Language; 033import org.ametys.cms.languages.LanguagesManager; 034import org.ametys.cms.search.model.SearchCriterion; 035import org.ametys.cms.search.model.SystemSearchCriterion; 036import org.ametys.cms.search.query.AndQuery; 037import org.ametys.cms.search.query.ContentLanguageQuery; 038import org.ametys.cms.search.query.ContentTypeOrMixinTypeQuery; 039import org.ametys.cms.search.query.ContentTypeQuery; 040import org.ametys.cms.search.query.MixinTypeQuery; 041import org.ametys.cms.search.query.NotQuery; 042import org.ametys.cms.search.query.OrQuery; 043import org.ametys.cms.search.query.Query; 044import org.ametys.cms.search.query.Query.Operator; 045import org.ametys.cms.search.ui.model.SearchUICriterion; 046import org.ametys.cms.search.ui.model.SearchUIModel; 047import org.ametys.cms.search.ui.model.impl.SystemSearchUICriterion; 048 049/** 050 * Builds a {@link Query} object from a user search. 051 */ 052public class QueryBuilder extends AbstractLogEnabled implements Component, Serviceable 053{ 054 /** The component role. */ 055 public static final String ROLE = QueryBuilder.class.getName(); 056 057 /** Prefix for id of metadata search criteria */ 058 public static final String SEARCH_CRITERIA_METADATA_PREFIX = "metadata-"; 059 /** Prefix for id of system property search criteria */ 060 public static final String SEARCH_CRITERIA_SYSTEM_PREFIX = "property-"; 061 062 /** The query default language. */ 063 public static final String DEFAULT_LANGUAGE = "en"; 064 065 /** The content type extension point. */ 066 protected ContentTypeExtensionPoint _cTypeEP; 067 /** The languages manager */ 068 protected LanguagesManager _languagesManager; 069 070 @Override 071 public void service(ServiceManager serviceManager) throws ServiceException 072 { 073 _cTypeEP = (ContentTypeExtensionPoint) serviceManager.lookup(ContentTypeExtensionPoint.ROLE); 074 _languagesManager = (LanguagesManager) serviceManager.lookup(LanguagesManager.ROLE); 075 } 076 077 /** 078 * Build the {@link Query} object. 079 * @param model the search model. 080 * @param searchMode the search mode. 081 * @param values the user search values. 082 * @param contextualParameters the search contextual parameters. 083 * @return a {@link Query} object representing the search. 084 */ 085 public Query build(SearchUIModel model, String searchMode, Map<String, Object> values, Map<String, Object> contextualParameters) 086 { 087 List<Query> queries = new ArrayList<>(); 088 089 Set<String> contentTypes = model.getContentTypes(contextualParameters); 090 Set<String> excludedCTypes = model.getExcludedContentTypes(contextualParameters); 091 092 // TODO Remove "contentType-eq" and find the criterion by class (like it's done with language?) 093 Query cTypeQuery = createContentTypeQuery(contentTypes, values, contextualParameters); 094 if (cTypeQuery != null) 095 { 096 queries.add(cTypeQuery); 097 } 098 099 if (!excludedCTypes.isEmpty()) 100 { 101 queries.add(new ContentTypeQuery(Operator.NE, excludedCTypes)); 102 } 103 104 String language = getCriteriaLanguage(model, searchMode, values, contextualParameters); 105 106 // TODO Remove "mixin-eq" and find the criterion by class (like it's done with language?) 107 Query mixinQuery = createMixinTypeQuery(contentTypes, values, contextualParameters); 108 if (mixinQuery != null) 109 { 110 queries.add(mixinQuery); 111 } 112 113 if ("simple".equals(searchMode)) 114 { 115 queries.addAll(getCriteriaQueries(model, values, language, contextualParameters)); 116 } 117 else if ("advanced".equals(searchMode)) 118 { 119 Query advQuery = getAdvancedCriteriaQuery(model, values, language, contextualParameters); 120 if (advQuery != null) 121 { 122 queries.add(advQuery); 123 } 124 125 Query contentLanguageQuery = new ContentLanguageQuery(language); 126 queries.add(contentLanguageQuery); 127 } 128 129 return new AndQuery(queries); 130 } 131 132 /** 133 * Get the language. 134 * @param model The search model. 135 * @param searchMode The search mode (advanced or simple) 136 * @param values The user values. 137 * @param contextualParameters The search contextual parameters. 138 * @return the query language. 139 */ 140 protected String getCriteriaLanguage(SearchUIModel model, String searchMode, Map<String, Object> values, Map<String, Object> contextualParameters) 141 { 142 String langValue = null; 143 144 Map<String, SearchUICriterion> criteria = searchMode.equals("advanced") ? model.getAdvancedCriteria(contextualParameters) : model.getCriteria(contextualParameters); 145 // First search language in search criteria 146 for (SearchCriterion criterion : criteria.values()) 147 { 148 if (criterion instanceof SystemSearchCriterion) 149 { 150 SystemSearchUICriterion sysCrit = (SystemSearchUICriterion) criterion; 151 if (sysCrit.getSystemPropertyId().equals("contentLanguage")) 152 { 153 if (sysCrit.isHidden()) 154 { 155 // Use the default value 156 langValue = (String) sysCrit.getDefaultValue(); 157 } 158 else 159 { 160 // Use the user input 161 langValue = (String) values.get(sysCrit.getId()); 162 } 163 break; 164 } 165 } 166 } 167 168 if (StringUtils.isEmpty(langValue) || "CURRENT".equals(langValue)) 169 { 170 // If empty, get language from the search contextual parameters (for instance, sent by the tool). 171 langValue = (String) contextualParameters.get("language"); 172 } 173 174 if (StringUtils.isEmpty(langValue)) 175 { 176 // If no language found: fall back to default. 177 langValue = getDefaultLanguage(); 178 } 179 180 return langValue; 181 } 182 183 /** 184 * Get the default language for search 185 * @return The default language 186 */ 187 protected String getDefaultLanguage() 188 { 189 Map<String, Language> availableLanguages = _languagesManager.getAvailableLanguages(); 190 if (availableLanguages.containsKey(DEFAULT_LANGUAGE)) 191 { 192 return DEFAULT_LANGUAGE; 193 } 194 195 return availableLanguages.size() > 0 ? availableLanguages.keySet().iterator().next() : DEFAULT_LANGUAGE; 196 } 197 198 199 /** 200 * Create a content type query. 201 * @param contentTypes the content types to search on. 202 * @param values the user search values. 203 * @param contextualParameters the search contextual parameters. 204 * @return the content type {@link Query}. 205 */ 206 protected Query createContentTypeQuery(Set<String> contentTypes, Map<String, Object> values, Map<String, Object> contextualParameters) 207 { 208 return createContentTypeQuery(contentTypes, values, contextualParameters, false); 209 } 210 211 /** 212 * Create a content type query. 213 * @param contentTypes the content types to search on. 214 * @param values the user search values. 215 * @param contextualParameters the search contextual parameters. 216 * @param exclude <code>true</code> to create a negative query ("all but the given content types"), <code>false</code> to create a standard "include" query. 217 * @return the content type {@link Query}. 218 */ 219 @SuppressWarnings("unchecked") 220 protected Query createContentTypeQuery(Set<String> contentTypes, Map<String, Object> values, Map<String, Object> contextualParameters, boolean exclude) 221 { 222 Object cTypeParam = values.get(SEARCH_CRITERIA_SYSTEM_PREFIX + "contentType-eq"); 223 224 if (cTypeParam == null) 225 { 226 return createContentTypeOrMixinQuery(contentTypes, values, contextualParameters); 227 } 228 else 229 { 230 Operator op = exclude ? Operator.NE : Operator.EQ; 231 if (cTypeParam instanceof String && StringUtils.isNotEmpty((String) cTypeParam)) 232 { 233 return new ContentTypeQuery(op, StringUtils.split((String) cTypeParam, ',')); 234 } 235 else if (cTypeParam instanceof List<?>) 236 { 237 return new ContentTypeQuery(op, (List<String>) cTypeParam); 238 } 239 else if (contentTypes != null && !contentTypes.isEmpty()) 240 { 241 return new ContentTypeQuery(op, contentTypes); 242 } 243 else 244 { 245 return null; 246 } 247 } 248 } 249 250 /** 251 * Create a mixin type query. 252 * @param mixinTypes the mixin types to search on. 253 * @param values the user search values. 254 * @param contextualParameters the search contextual parameters. 255 * @return the mixin type {@link Query}. 256 */ 257 @SuppressWarnings("unchecked") 258 protected Query createMixinTypeQuery(Set<String> mixinTypes, Map<String, Object> values, Map<String, Object> contextualParameters) 259 { 260 Object mixinParam = values.get(SEARCH_CRITERIA_SYSTEM_PREFIX + "mixin-eq"); 261 262 if (mixinParam instanceof String && StringUtils.isNotEmpty((String) mixinParam)) 263 { 264 return new MixinTypeQuery((String) mixinParam); 265 } 266 else if (mixinParam != null && mixinParam instanceof List<?>) 267 { 268 return new MixinTypeQuery((List<String>) mixinParam); 269 } 270 else 271 { 272 return null; 273 } 274 } 275 276 /** 277 * Create a content type or mixin query. 278 * @param contentTypes the content types or mixins to search on. 279 * @param values the user search values. 280 * @param contextualParameters the search contextual parameters. 281 * @return the content type {@link Query}. 282 */ 283 @SuppressWarnings("unchecked") 284 protected Query createContentTypeOrMixinQuery(Set<String> contentTypes, Map<String, Object> values, Map<String, Object> contextualParameters) 285 { 286 Object cTypeOrMixinParam = values.get(SEARCH_CRITERIA_SYSTEM_PREFIX + "contentTypeOrMixin-eq"); 287 288 if (cTypeOrMixinParam instanceof String && StringUtils.isNotEmpty((String) cTypeOrMixinParam)) 289 { 290 return new ContentTypeOrMixinTypeQuery((String) cTypeOrMixinParam); 291 } 292 else if (cTypeOrMixinParam != null && cTypeOrMixinParam instanceof List<?>) 293 { 294 return new ContentTypeOrMixinTypeQuery((List<String>) cTypeOrMixinParam); 295 } 296 else if (contentTypes != null && !contentTypes.isEmpty()) 297 { 298 return new ContentTypeOrMixinTypeQuery(contentTypes); 299 } 300 else 301 { 302 return null; 303 } 304 } 305 306 /** 307 * Get the list of query on criteria. 308 * @param model the search model. 309 * @param values The submitted values 310 * @param language The query language. 311 * @param contextualParameters The contextual parameters 312 * @return The criteria {@link Query}. 313 */ 314 protected List<Query> getCriteriaQueries(SearchUIModel model, Map<String, Object> values, String language, Map<String, Object> contextualParameters) 315 { 316 List<Query> queries = new ArrayList<>(); 317 Map<String, SearchUICriterion> criteria = model.getCriteria(contextualParameters); 318 319 for (String id : criteria.keySet()) 320 { 321 SearchUICriterion criterion = criteria.get(id); 322 Object submitValue = values.get(id); 323 324 // If the criterion is hidden, take the default value (fixed in the search model). 325 // Otherwise take the standard user value. 326 Object value = criterion.isHidden() ? criterion.getDefaultValue() : submitValue; 327 328 if (value != null || criterion.getOperator() == Operator.EXISTS) 329 { 330 Query query = criterion.getQuery(value, values, language, contextualParameters); 331 if (query != null) 332 { 333 queries.add(query); 334 } 335 } 336 } 337 338 return queries; 339 } 340 341 /** 342 * Get a complex Query from the advanced search values. 343 * @param model the search model. 344 * @param values The submitted values 345 * @param language The query language. 346 * @param contextualParameters The contextual parameters 347 * @return The criteria {@link Query}. 348 */ 349 protected Query getAdvancedCriteriaQuery(SearchUIModel model, Map<String, Object> values, String language, Map<String, Object> contextualParameters) 350 { 351 Query query = null; 352 353 Map<String, ? extends SearchUICriterion> criteria = model.getAdvancedCriteria(contextualParameters); 354 String type = (String) values.get("type"); 355 356 if ("criterion".equals(type)) 357 { 358 // Criterion node: return a simple Query object. 359 String id = (String) values.get("id"); 360 Object value = values.get("value"); 361 String op = (String) values.get("op"); 362 363 SearchUICriterion criterion = criteria.get(id); 364 365 query = getAdvancedCriterionQuery(criterion, op, value, language, contextualParameters); 366 } 367 else if ("and".equalsIgnoreCase(type) || "or".equalsIgnoreCase(type)) 368 { 369 // AND/OR node: recurse on each sub-query and return a AndQuery/OrQuery. 370 List<Query> queries = new ArrayList<>(); 371 372 @SuppressWarnings("unchecked") 373 List<Map<String, Object>> expressions = (List<Map<String, Object>>) values.get("expressions"); 374 for (Map<String, Object> expression : expressions) 375 { 376 Query expQuery = getAdvancedCriteriaQuery(model, expression, language, contextualParameters); 377 if (expQuery != null) 378 { 379 queries.add(expQuery); 380 } 381 } 382 383 query = "and".equalsIgnoreCase(type) ? new AndQuery(queries) : new OrQuery(queries); 384 } 385 386 return query; 387 } 388 389 /** 390 * Build the {@link Query} object corresponding to an advanced criterion value. 391 * @param criterion The search criterion. 392 * @param op The advanced operator (can be a custom one, such as "contain"). 393 * @param value The user value. 394 * @param language The query language. 395 * @param contextualParameters the search contextual parameters. 396 * @return The criterion {@link Query}. 397 */ 398 protected Query getAdvancedCriterionQuery(SearchUICriterion criterion, String op, Object value, String language, Map<String, Object> contextualParameters) 399 { 400 Query query = null; 401 402 // Special wildcard searches. 403 if ("contains".equals(op) || "not-contains".equals(op)) 404 { 405 String wdValue = "*" + filterWildcardChars((String) value) + "*"; 406 query = criterion.getQuery(wdValue, Operator.LIKE, Collections.emptyMap(), language, contextualParameters); 407 if ("not-contains".equals(op)) 408 { 409 query = new NotQuery(query); 410 } 411 } 412 else if ("starts-with".equals(op) || "not-starts-with".equals(op)) 413 { 414 String wdValue = filterWildcardChars((String) value) + "*"; 415 query = criterion.getQuery(wdValue, Operator.LIKE, Collections.emptyMap(), language, contextualParameters); 416 if ("not-starts-with".equals(op)) 417 { 418 query = new NotQuery(query); 419 } 420 } 421 else if ("ends-with".equals(op) || "not-ends-with".equals(op)) 422 { 423 String wdValue = "*" + filterWildcardChars((String) value); 424 query = criterion.getQuery(wdValue, Operator.LIKE, Collections.emptyMap(), language, contextualParameters); 425 if ("not-ends-with".equals(op)) 426 { 427 query = new NotQuery(query); 428 } 429 } 430 else if (StringUtils.isNotBlank(op)) 431 { 432 Operator operator = Operator.fromName(op); 433 query = criterion.getQuery(value, operator, Collections.emptyMap(), language, contextualParameters); 434 } 435 436 return query; 437 } 438 439 /** 440 * Filter wildcard characters '*' and '?' from the input string by replacing them with a space. 441 * @param string The string to filter. 442 * @return The filtered string. 443 */ 444 protected String filterWildcardChars(String string) 445 { 446 return StringUtils.replaceChars(string, "*?", " "); 447 } 448 449}