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