001/* 002 * Copyright 2013 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.ui.model.impl; 017 018import java.util.ArrayList; 019import java.util.Collection; 020import java.util.Collections; 021import java.util.HashSet; 022import java.util.List; 023import java.util.Map; 024import java.util.Set; 025import java.util.stream.Collectors; 026 027import org.apache.avalon.framework.configuration.Configuration; 028import org.apache.avalon.framework.configuration.ConfigurationException; 029import org.apache.avalon.framework.service.ServiceException; 030import org.apache.avalon.framework.service.ServiceManager; 031import org.apache.commons.lang3.BooleanUtils; 032import org.apache.commons.lang3.StringUtils; 033 034import org.ametys.cms.content.referencetable.HierarchicalReferenceTablesHelper; 035import org.ametys.cms.contenttype.ContentType; 036import org.ametys.cms.contenttype.ContentTypesHelper; 037import org.ametys.cms.contenttype.MetadataType; 038import org.ametys.cms.data.type.ModelItemTypeConstants; 039import org.ametys.cms.model.ContentElementDefinition; 040import org.ametys.cms.model.properties.ReferencingProperty; 041import org.ametys.cms.repository.Content; 042import org.ametys.cms.search.QueryBuilder; 043import org.ametys.cms.search.SearchField; 044import org.ametys.cms.search.content.ContentSearchHelper; 045import org.ametys.cms.search.model.IndexingFieldSearchCriterion; 046import org.ametys.cms.search.query.AndQuery; 047import org.ametys.cms.search.query.BooleanQuery; 048import org.ametys.cms.search.query.ContentQuery; 049import org.ametys.cms.search.query.DateQuery; 050import org.ametys.cms.search.query.DateTimeQuery; 051import org.ametys.cms.search.query.DoubleQuery; 052import org.ametys.cms.search.query.GeocodeQuery; 053import org.ametys.cms.search.query.JoinQuery; 054import org.ametys.cms.search.query.LongQuery; 055import org.ametys.cms.search.query.OrQuery; 056import org.ametys.cms.search.query.Query; 057import org.ametys.cms.search.query.Query.Operator; 058import org.ametys.cms.search.query.RichTextQuery; 059import org.ametys.cms.search.query.StringQuery; 060import org.ametys.cms.search.query.UsersQuery; 061import org.ametys.core.user.UserIdentity; 062import org.ametys.core.util.date.AdaptableDateParser; 063import org.ametys.plugins.core.user.UserHelper; 064import org.ametys.plugins.repository.AmetysObjectResolver; 065import org.ametys.runtime.model.ElementDefinition; 066import org.ametys.runtime.model.ModelHelper; 067import org.ametys.runtime.model.ModelItem; 068import org.ametys.runtime.model.exception.UndefinedItemPathException; 069import org.ametys.runtime.model.type.ModelItemType; 070import org.ametys.runtime.parameter.Validator; 071import org.ametys.runtime.plugin.component.ThreadSafeComponentManager; 072 073/** 074 * This class is a search criteria on a metadata of a content 075 */ 076public class IndexingFieldSearchUICriterion extends AbstractSearchUICriterion implements IndexingFieldSearchCriterion 077{ 078 /** Prefix for id of metadata search criteria */ 079 public static final String SEARCH_CRITERIA_METADATA_PREFIX = "metadata-"; 080 081 /** The content search helper. */ 082 protected ContentSearchHelper _searchHelper; 083 /** The hierarchical reference tables helper. */ 084 protected HierarchicalReferenceTablesHelper _hierarchicalReferenceTablesHelper; 085 /** The user helper */ 086 protected UserHelper _userHelper; 087 /** The helper for convenient methods on content types */ 088 protected ContentTypesHelper _contentTypesHelper; 089 /** The ametys object resolver. */ 090 protected AmetysObjectResolver _resolver; 091 092 /** The criteria operator */ 093 protected Operator _operator; 094 095 /** The definition used for this criterion */ 096 protected ElementDefinition _definition; 097 098 /** The field full path */ 099 protected String _fullPath; 100 101 /** The field path */ 102 protected String _fieldPath; 103 104 /** The join paths */ 105 protected List<String> _joinPaths; 106 107 /** Is it AND or OR for multiple metadata */ 108 protected boolean _isMultipleOperandAnd; 109 110 /** ComponentManager for {@link Validator}s. */ 111 protected ThreadSafeComponentManager<Validator> _validatorManager; 112 113 private boolean _isTypeContentWithMultilingualTitle; 114 115 @Override 116 public void service(ServiceManager manager) throws ServiceException 117 { 118 super.service(manager); 119 _searchHelper = (ContentSearchHelper) manager.lookup(ContentSearchHelper.ROLE); 120 _hierarchicalReferenceTablesHelper = (HierarchicalReferenceTablesHelper) manager.lookup(HierarchicalReferenceTablesHelper.ROLE); 121 _userHelper = (UserHelper) manager.lookup(UserHelper.ROLE); 122 _contentTypesHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE); 123 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 124 } 125 126 @Override 127 public void dispose() 128 { 129 super.dispose(); 130 131 _validatorManager.dispose(); 132 _validatorManager = null; 133 } 134 135 @Override 136 public void configure(Configuration configuration) throws ConfigurationException 137 { 138 try 139 { 140 _validatorManager = new ThreadSafeComponentManager<>(); 141 _validatorManager.setLogger(_logger); 142 _validatorManager.contextualize(_context); 143 _validatorManager.service(_manager); 144 145 _enumeratorManager = new ThreadSafeComponentManager<>(); 146 _enumeratorManager.setLogger(_logger); 147 _enumeratorManager.contextualize(_context); 148 _enumeratorManager.service(_manager); 149 150 _fullPath = configuration.getChild("field").getAttribute("path"); 151 Set<String> baseContentTypeIds = new HashSet<>(); 152 for (Configuration cTypeConf : configuration.getChild("contentTypes").getChildren("baseType")) 153 { 154 baseContentTypeIds.add(cTypeConf.getAttribute("id")); 155 } 156 157 _operator = Operator.fromName(configuration.getChild("test-operator").getValue("eq")); 158 _isMultipleOperandAnd = !StringUtils.equalsIgnoreCase(configuration.getChild("multiple-operand").getValue("or"), "or"); 159 _joinPaths = new ArrayList<>(); 160 161 String id = SEARCH_CRITERIA_METADATA_PREFIX + _fullPath + "-" + _operator.getName(); 162 163 if (!baseContentTypeIds.isEmpty()) 164 { 165 try 166 { 167 Collection<ContentType> baseContentTypes = baseContentTypeIds.stream() 168 .map(_contentTypeExtensionPoint::getExtension) 169 .collect(Collectors.toList()); 170 ModelItem modelItem = ModelHelper.getModelItem(_fullPath, baseContentTypes); 171 if (modelItem instanceof ElementDefinition def) 172 { 173 _definition = def; 174 } 175 else 176 { 177 throw new ConfigurationException("Indexing field search criteria with path '" + _fullPath + "' refers to a group"); 178 } 179 180 _joinPaths = _searchHelper.computeJoinPaths(_fullPath, baseContentTypeIds, false); 181 } 182 catch (UndefinedItemPathException e) 183 { 184 throw new ConfigurationException("Indexing field search criteria with path '" + _fullPath + "' refers to an unknown model item", e); 185 } 186 } 187 else if (Content.ATTRIBUTE_TITLE.equals(_fullPath)) 188 { 189 // Only the title indexing field is allowed if there is no base content types. 190 _definition = _contentTypesHelper.getTitleAttributeDefinition(); 191 } 192 else 193 { 194 throw new ConfigurationException("The indexing field '" + _fullPath + "' is forbidden when no content type is specified: only title can be used."); 195 } 196 197 _fieldPath = _searchHelper.computeFieldPath(_definition); 198 199 setId(id); 200 setGroup(_configureI18nizableText(configuration.getChild("group", false), null)); 201 _configureLabel(configuration, _definition); 202 _configureDescription(configuration, _definition); 203 204 MetadataType type = MetadataType.fromModelItemType(_definition.getType()); 205 setType(type); 206 207// setValidator(finalDefinition.getValidator()); 208 String validatorRole = "validator"; 209 if (!_initializeValidator(_validatorManager, "cms", validatorRole, configuration)) 210 { 211 validatorRole = null; 212 } 213 214 setEnumerator(configureEnumerator(configuration, _definition)); 215 setWidget(configureWidget(configuration, _definition)); 216 setWidgetParameters(configureWidgetParameters(configuration, _definition)); 217 218 setMultiple(_definition.isMultiple()); 219 220 _configureTypeContentWithMultilingualTitle(_definition); 221 222 _configureOperator(configuration, _definition); 223 224 configureUIProperties(configuration); 225 configureValues(configuration); 226 } 227 catch (Exception e) 228 { 229 throw new ConfigurationException("Error configuring the indexing field search criterion.", configuration, e); 230 } 231 } 232 233 /** 234 * Get the operator. 235 * @return the operator. 236 */ 237 public Operator getOperator() 238 { 239 return _operator; 240 } 241 242 public String getFieldId() 243 { 244 return SEARCH_CRITERIA_METADATA_PREFIX + _fullPath; 245 } 246 247 /** 248 * Get the path of field (separated by '/') 249 * @return the path of the field. 250 */ 251 public String getFieldPath() 252 { 253 return _fieldPath; 254 } 255 256 /** 257 * Get the join paths, separated with slashes. 258 * @return the join paths. 259 */ 260 public List<String> getJoinPaths() 261 { 262 return Collections.unmodifiableList(_joinPaths); 263 } 264 265 @Override 266 public Query getQuery(Object value, Operator customOperator, Map<String, Object> allValues, String language, Map<String, Object> contextualParameters) 267 { 268 String fieldPath = getFieldPath(); 269 List<String> joinPaths = getJoinPaths(); 270 Operator operator = customOperator != null ? customOperator : getOperator(); 271 272 if (operator != Operator.EXISTS && isEmpty(value)) 273 { 274 return null; 275 } 276 boolean isValueEscaped = BooleanUtils.isTrue((Boolean) contextualParameters.get(QueryBuilder.VALUE_IS_ESCAPED)); 277 278 Query query = null; 279 switch (getType()) 280 { 281 case DATE: 282 query = getDateQuery(value, fieldPath, operator); 283 break; 284 case DATETIME: 285 query = getDateTimeQuery(value, fieldPath, operator); 286 break; 287 case LONG: 288 query = getLongQuery(value, fieldPath, operator); 289 break; 290 case DOUBLE: 291 query = getDoubleQuery(value, fieldPath, operator); 292 break; 293 case BOOLEAN: 294 query = getBooleanQuery(value, fieldPath, operator); 295 break; 296 case STRING: 297 if (_isPropertyReferencingContents(_definition)) 298 { 299 // FIXME: During criterion review, remove this specific behavior 300 // If the property's type is String, the behavior must correspond to a String behavior 301 // Check MultiStringValuesProperty use cases 302 query = getContentQuery(value, language, fieldPath, operator); 303 } 304 else 305 { 306 if (contextualParameters.containsKey(QueryBuilder.MULTILINGUAL_SEARCH)) 307 { 308 // Force language to null if the search is concern a string field on multilingual contents 309 query = getStringQuery(value, null, fieldPath, operator, isValueEscaped); 310 } 311 else 312 { 313 query = getStringQuery(value, language, fieldPath, operator, isValueEscaped); 314 } 315 } 316 break; 317 case RICH_TEXT: 318 if (contextualParameters.containsKey(QueryBuilder.MULTILINGUAL_SEARCH)) 319 { 320 // Force language to null if the search is concern a string field on multilingual contents 321 query = getRichTextQuery(value, null, fieldPath, operator, isValueEscaped); 322 } 323 else 324 { 325 query = getRichTextQuery(value, language, fieldPath, operator, isValueEscaped); 326 } 327 break; 328 case MULTILINGUAL_STRING: 329 query = getStringQuery(value, language, fieldPath, operator, isValueEscaped); 330 break; 331 case USER: 332 query = getUserQuery(value, fieldPath, operator); 333 break; 334 case CONTENT: 335 query = getContentQuery(value, language, fieldPath, operator); 336 break; 337 case REFERENCE: 338 query = getStringQuery(value, language, fieldPath, operator, isValueEscaped); 339 break; 340 case GEOCODE: 341 query = new GeocodeQuery(fieldPath, operator, (Map<String, Integer>) value); 342 break; 343 default: 344 return null; 345 } 346 347 if (query != null && !joinPaths.isEmpty()) 348 { 349 query = new JoinQuery(query, joinPaths); 350 } 351 352 return query; 353 } 354 355 private static boolean isEmpty(Object value) 356 { 357 return value == null 358 || value instanceof String && ((String) value).length() == 0 359 || value instanceof List && ((List) value).isEmpty(); 360 } 361 362 private boolean _isPropertyReferencingContents(ElementDefinition definition) 363 { 364 if (definition instanceof ReferencingProperty property) 365 { 366 List<String> references = property.getReferences(); 367 return references.stream() 368 .findFirst() // CMS-11919: Keep the behavior that checks only the first reference 369 .map(reference -> property.getModel().getModelItem(reference)) 370 .filter(ContentElementDefinition.class::isInstance) 371 .isPresent(); 372 } 373 374 return false; 375 } 376 377 /** 378 * Get a string query. 379 * @param value The value to use for this criterion. 380 * @param language The search language. 381 * @param fieldPath The field path. 382 * @param operator The query operator to use. 383 * @param isValueEscaped <code>true</code> if the given value is already escaped 384 * @return The query. 385 */ 386 protected Query getStringQuery(Object value, String language, String fieldPath, Operator operator, boolean isValueEscaped) 387 { 388 if (operator == Operator.EXISTS) 389 { 390 return new StringQuery(fieldPath, operator, null, language); 391 } 392 else 393 { 394 return getTextQuery(value, language, fieldPath, operator, isValueEscaped); 395 } 396 } 397 398 /** 399 * Get a boolean query. 400 * @param value The value to use for this criterion. 401 * @param fieldPath The field path. 402 * @param operator The query operator to use. 403 * @return The query. 404 */ 405 protected Query getBooleanQuery(Object value, String fieldPath, Operator operator) 406 { 407 if (operator == Operator.EXISTS) 408 { 409 return new BooleanQuery(fieldPath); 410 } 411 else if (value instanceof Collection<?>) 412 { 413 @SuppressWarnings("unchecked") 414 Collection<Object> values = (Collection<Object>) value; 415 416 List<Query> queries = new ArrayList<>(); 417 for (Object val : values) 418 { 419 queries.add(getBooleanQuery(val, fieldPath, operator)); 420 } 421 422 return _isMultipleOperandAnd ? new AndQuery(queries) : new OrQuery(queries); 423 } 424 else if (value instanceof String) 425 { 426 Boolean boolValue = Boolean.parseBoolean((String) value); 427 return new BooleanQuery(fieldPath, boolValue); 428 } 429 else 430 { 431 return new BooleanQuery(fieldPath, (Boolean) value); 432 } 433 } 434 435 /** 436 * Get a double query. 437 * @param value The value to use for this criterion. 438 * @param fieldPath The field path. 439 * @param operator The query operator to use. 440 * @return The query. 441 */ 442 protected Query getDoubleQuery(Object value, String fieldPath, Operator operator) 443 { 444 if (operator == Operator.EXISTS) 445 { 446 return new DoubleQuery(fieldPath); 447 } 448 else if (value instanceof Collection<?>) 449 { 450 @SuppressWarnings("unchecked") 451 Collection<Object> values = (Collection<Object>) value; 452 453 List<Query> queries = new ArrayList<>(); 454 for (Object val : values) 455 { 456 queries.add(getDoubleQuery(val, fieldPath, operator)); 457 } 458 459 return _isMultipleOperandAnd ? new AndQuery(queries) : new OrQuery(queries); 460 } 461 else if (value instanceof String) 462 { 463 Double doubleValue = Double.parseDouble((String) value); 464 return new DoubleQuery(fieldPath, operator, doubleValue); 465 } 466 else if (value instanceof Integer) 467 { 468 return new DoubleQuery(fieldPath, operator, ((Integer) value).doubleValue()); 469 } 470 else if (value instanceof Number) 471 { 472 Number valueAsNumber = (Number) value; 473 Double valueAsDouble = valueAsNumber.doubleValue(); 474 // We check that the cast to long was safe 475 if (valueAsNumber.toString().equals(valueAsDouble.toString())) 476 { 477 return new DoubleQuery(fieldPath, operator, valueAsDouble); 478 } 479 else 480 { 481 throw new NumberFormatException("Failed to convert the value " + value + " to Double. The value is out of bound."); 482 } 483 } 484 else 485 { 486 throw new NumberFormatException("Failed to convert the value " + value + " to Double."); 487 } 488 } 489 490 /** 491 * Get a long query. 492 * @param value The value to use for this criterion. 493 * @param fieldPath The field path. 494 * @param operator The query operator to use. 495 * @return The query. 496 */ 497 protected Query getLongQuery(Object value, String fieldPath, Operator operator) 498 { 499 if (operator == Operator.EXISTS) 500 { 501 return new LongQuery(fieldPath); 502 } 503 else if (value instanceof Collection<?>) 504 { 505 @SuppressWarnings("unchecked") 506 Collection<Object> values = (Collection<Object>) value; 507 508 List<Query> queries = new ArrayList<>(); 509 for (Object val : values) 510 { 511 queries.add(getLongQuery(val, fieldPath, operator)); 512 } 513 514 return _isMultipleOperandAnd ? new AndQuery(queries) : new OrQuery(queries); 515 } 516 else if (value instanceof String) 517 { 518 return new LongQuery(fieldPath, operator, Long.parseLong((String) value)); 519 } 520 else if (value instanceof Number) 521 { 522 Number valueAsNumber = (Number) value; 523 Long valueAsLong = valueAsNumber.longValue(); 524 // We check that the cast to long was safe 525 if (valueAsNumber.toString().equals(valueAsLong.toString())) 526 { 527 return new LongQuery(fieldPath, operator, valueAsLong); 528 } 529 else 530 { 531 throw new NumberFormatException("Failed to convert the value " + value + " to Long. The value is out of bound."); 532 } 533 } 534 else 535 { 536 throw new NumberFormatException("Failed to convert the value " + value + " to Long."); 537 } 538 } 539 540 /** 541 * Get a date query. 542 * @param value The value to use for this criterion. 543 * @param fieldPath The field path. 544 * @param operator The query operator to use. 545 * @return The query. 546 */ 547 protected Query getDateQuery(Object value, String fieldPath, Operator operator) 548 { 549 if (operator == Operator.EXISTS) 550 { 551 return new DateQuery(fieldPath); 552 } 553 else if (value instanceof Collection<?>) 554 { 555 @SuppressWarnings("unchecked") 556 Collection<Object> values = (Collection<Object>) value; 557 558 List<Query> queries = new ArrayList<>(); 559 for (Object val : values) 560 { 561 queries.add(getDateQuery(val, fieldPath, operator)); 562 } 563 564 return _isMultipleOperandAnd ? new AndQuery(queries) : new OrQuery(queries); 565 } 566 else 567 { 568 return _getSingleDateQuery((String) value, fieldPath, operator); 569 } 570 } 571 572 private Query _getSingleDateQuery(String value, String fieldPath, Operator operator) 573 { 574 return new DateQuery(fieldPath, operator, AdaptableDateParser.parse(value)); 575 } 576 577 /** 578 * Get a dateTime query. 579 * @param value The value to use for this criterion. 580 * @param fieldPath The field path. 581 * @param operator The query operator to use. 582 * @return The query. 583 */ 584 protected Query getDateTimeQuery(Object value, String fieldPath, Operator operator) 585 { 586 if (operator == Operator.EXISTS) 587 { 588 return new DateTimeQuery(fieldPath); 589 } 590 else if (value instanceof Collection<?>) 591 { 592 @SuppressWarnings("unchecked") 593 Collection<Object> values = (Collection<Object>) value; 594 595 List<Query> queries = new ArrayList<>(); 596 for (Object val : values) 597 { 598 queries.add(getDateTimeQuery(val, fieldPath, operator)); 599 } 600 601 return _isMultipleOperandAnd ? new AndQuery(queries) : new OrQuery(queries); 602 } 603 else 604 { 605 return _getSingleDateTimeQuery((String) value, fieldPath, operator); 606 } 607 } 608 609 private Query _getSingleDateTimeQuery(String value, String fieldPath, Operator operator) 610 { 611 return new DateTimeQuery(fieldPath, operator, AdaptableDateParser.parse(value)); 612 } 613 614 /** 615 * Get a content query. 616 * @param value The value to use for this criterion. 617 * @param language The search language. 618 * @param fieldPath The field path. 619 * @param operator The query operator to use. 620 * @return The query. 621 */ 622 protected Query getContentQuery(Object value, String language, String fieldPath, Operator operator) 623 { 624 boolean isMultipleOperandAnd = operator == Operator.NE ? !_isMultipleOperandAnd : _isMultipleOperandAnd; // reverse it if NE 625 return new ContentQuery(fieldPath, operator, value, _resolver, _contentHelper, _hierarchicalReferenceTablesHelper, false, isMultipleOperandAnd); 626 } 627 628 /** 629 * Get a user query 630 * @param value The value to use for this criterion. 631 * @param fieldPath The field path. 632 * @param operator The query operator to use. 633 * @return The query. 634 */ 635 @SuppressWarnings("unchecked") 636 protected Query getUserQuery(Object value, String fieldPath, Operator operator) 637 { 638 List<UserIdentity> users; 639 if (value instanceof Map<?, ?>) 640 { 641 users = List.of(_userHelper.json2userIdentity((Map<String, Object>) value)); 642 } 643 else if (value instanceof Collection< ? >) 644 { 645 users = ((Collection<Map<String, Object>>) value).stream() 646 .map(_userHelper::json2userIdentity).toList(); 647 } 648 else 649 { 650 throw new IllegalArgumentException("The value for the field '" + fieldPath + "' of type USER cannot be cast to a Map or a Collection"); 651 } 652 return new UsersQuery(fieldPath + "_s", operator, users); 653 } 654 655 /** 656 * Get a geocode query. 657 * @param value The value to use for this criterion. 658 * @param fieldPath The field path. 659 * @param operator The query operator to use. 660 * @return The query. 661 */ 662 protected Query getGeocodeQuery(Object value, String fieldPath, Operator operator) 663 { 664 if (operator == Operator.EXISTS) 665 { 666 return new GeocodeQuery(fieldPath); 667 } 668 else if (value instanceof Collection<?>) 669 { 670 @SuppressWarnings("unchecked") 671 Collection<Map<String, Integer>> values = (Collection<Map<String, Integer>>) value; 672 673 List<Query> queries = new ArrayList<>(); 674 for (Object val : values) 675 { 676 queries.add(getGeocodeQuery(val, fieldPath, operator)); 677 } 678 679 return _isMultipleOperandAnd ? new AndQuery(queries) : new OrQuery(queries); 680 } 681 else if (value instanceof Map<?, ?>) 682 { 683 return new GeocodeQuery(fieldPath, operator, (Map<String, Integer>) value); 684 } 685 else 686 { 687 throw new IllegalArgumentException("The value for the field '" + fieldPath + "' of type GEOCODE cannot be cast to a Map or a Collection"); 688 } 689 } 690 691 /** 692 * Get a rich text query. 693 * @param value The value to use for this criterion. 694 * @param language The search language. 695 * @param fieldPath The field path. 696 * @param operator The query operator to use. 697 * @param isValueEscaped <code>true</code> if the given value is already escaped 698 * @return The query. 699 */ 700 protected Query getRichTextQuery(Object value, String language, String fieldPath, Operator operator, boolean isValueEscaped) 701 { 702 if (operator == Operator.EXISTS) 703 { 704 return new RichTextQuery(fieldPath, operator, null, language); 705 } 706 else 707 { 708 return getTextQuery(value, language, fieldPath, operator, isValueEscaped); 709 } 710 } 711 712 /** 713 * Get a text query. 714 * @param value The value to use for this criterion. 715 * @param language The search language. 716 * @param fieldPath The field path. 717 * @param operator The query operator to use. 718 * @param isValueEscaped <code>true</code> if the given value is already escaped 719 * @return The query. 720 */ 721 protected Query getTextQuery(Object value, String language, String fieldPath, Operator operator, boolean isValueEscaped) 722 { 723 if (value instanceof Collection<?>) 724 { 725 @SuppressWarnings("unchecked") 726 Collection<Object> values = (Collection<Object>) value; 727 728 List<Query> queries = new ArrayList<>(); 729 for (Object val : values) 730 { 731 queries.add(getStringQuery(val, language, fieldPath, operator, isValueEscaped)); 732 } 733 734 return _isMultipleOperandAnd ? new AndQuery(queries) : new OrQuery(queries); 735 } 736 else if (operator.equals(Operator.LIKE)) 737 { 738 String stringValue = (String) value; 739 740 if (StringUtils.isNotEmpty(stringValue)) 741 { 742 boolean doNotUseLanguage = getType() != MetadataType.MULTILINGUAL_STRING; 743 return new StringQuery(fieldPath, operator, stringValue, doNotUseLanguage ? null : language, isValueEscaped); 744 } 745 746 return null; 747 } 748 else 749 { 750 return new StringQuery(fieldPath, operator, (String) value, language, isValueEscaped); 751 } 752 } 753 754 @Override 755 public SearchField getSearchField() 756 { 757 return _searchHelper.getMetadataSearchField(getJoinPaths(), getFieldPath(), getType(), _isTypeContentWithMultilingualTitle); 758 } 759 760 private void _configureTypeContentWithMultilingualTitle(ModelItem modelItem) 761 { 762 _isTypeContentWithMultilingualTitle = _searchHelper.isTitleMultilingual(modelItem); 763 } 764 765 /** 766 * Configure the criterion operator. 767 * @param configuration the global criterion configuration. 768 * @param definition the metadata definition. 769 * @throws ConfigurationException if an error occurs. 770 */ 771 @SuppressWarnings("unchecked") 772 private void _configureOperator(Configuration configuration, ElementDefinition definition) throws ConfigurationException 773 { 774 try 775 { 776 String definitionTypeId = definition.getType().getId(); 777 if (definition instanceof ReferencingProperty property) 778 { 779 // FIXME: During criterion review, remove this specific behavior 780 // Check MultiStringValuesProperty use cases 781 List<String> references = property.getReferences(); 782 definitionTypeId = references.stream() 783 .findFirst() 784 .map(reference -> property.getModel().getModelItem(reference)) 785 .map(ModelItem::getType) 786 .map(ModelItemType::getId) 787 .orElse(null); 788 } 789 790 String op = configuration.getChild("test-operator").getValue(""); 791 if (StringUtils.isNotBlank(op)) 792 { 793 _operator = Operator.fromName(op); 794 } 795 else 796 { 797 if (definition.getCriterionEnumerator(configuration, _enumeratorManager) == null 798 && (org.ametys.runtime.model.type.ModelItemTypeConstants.STRING_TYPE_ID.equals(definitionTypeId) 799 || ModelItemTypeConstants.MULTILINGUAL_STRING_ELEMENT_TYPE_ID.equals(definitionTypeId) 800 || ModelItemTypeConstants.RICH_TEXT_ELEMENT_TYPE_ID.equals(definitionTypeId) 801 || ModelItemTypeConstants.REFERENCE_ELEMENT_TYPE_ID.equals(definitionTypeId))) 802 { 803 _operator = Operator.SEARCH; 804 } 805 else 806 { 807 _operator = Operator.EQ; 808 } 809 } 810 } 811 catch (IllegalArgumentException e) 812 { 813 throw new ConfigurationException("Invalid operator", configuration, e); 814 } 815 } 816 817 private void _configureLabel(Configuration configuration, ModelItem modelItem) 818 { 819 setLabel(_configureI18nizableText(configuration.getChild("label", false), modelItem.getLabel())); 820 } 821 822 private void _configureDescription(Configuration configuration, ModelItem modelItem) 823 { 824 setDescription(_configureI18nizableText(configuration.getChild("description", false), modelItem.getDescription())); 825 } 826}