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; 017 018import java.util.ArrayList; 019import java.util.HashMap; 020import java.util.HashSet; 021import java.util.List; 022import java.util.Map; 023import java.util.Optional; 024import java.util.Set; 025import java.util.function.Predicate; 026import java.util.stream.Collectors; 027 028import org.apache.avalon.framework.activity.Disposable; 029import org.apache.avalon.framework.component.ComponentException; 030import org.apache.avalon.framework.configuration.Configurable; 031import org.apache.avalon.framework.configuration.Configuration; 032import org.apache.avalon.framework.configuration.ConfigurationException; 033import org.apache.avalon.framework.context.Context; 034import org.apache.avalon.framework.context.ContextException; 035import org.apache.avalon.framework.context.Contextualizable; 036import org.apache.avalon.framework.service.ServiceException; 037import org.apache.avalon.framework.service.ServiceManager; 038import org.apache.avalon.framework.service.Serviceable; 039import org.apache.cocoon.components.LifecycleHelper; 040import org.apache.cocoon.util.log.SLF4JLoggerAdapter; 041import org.apache.commons.lang3.StringUtils; 042import org.slf4j.Logger; 043 044import org.ametys.cms.contenttype.ContentType; 045import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 046import org.ametys.cms.contenttype.ContentTypesHelper; 047import org.ametys.cms.data.type.ModelItemTypeConstants; 048import org.ametys.cms.model.properties.Property; 049import org.ametys.cms.repository.Content; 050import org.ametys.cms.search.model.CriterionDefinitionAwareElementDefinition; 051import org.ametys.cms.search.model.CriterionDefinitionHelper; 052import org.ametys.cms.search.model.IndexationAwareElementDefinition; 053import org.ametys.cms.search.model.SearchModelCriterionDefinition; 054import org.ametys.cms.search.model.SearchModelCriterionDefinitionHelper; 055import org.ametys.cms.search.model.SystemProperty; 056import org.ametys.cms.search.model.SystemPropertyExtensionPoint; 057import org.ametys.cms.search.model.impl.ReferencingSearchModelCriterionDefinition; 058import org.ametys.cms.search.ui.model.impl.DefaultSearchModelCriterionViewItem; 059import org.ametys.cms.search.ui.model.impl.DefaultSearchUIModel; 060import org.ametys.runtime.i18n.I18nizableText; 061import org.ametys.runtime.model.ElementDefinition; 062import org.ametys.runtime.model.ItemParserHelper; 063import org.ametys.runtime.model.ItemParserHelper.ConfigurationAndPluginName; 064import org.ametys.runtime.model.ModelItem; 065import org.ametys.runtime.model.ModelViewItem; 066import org.ametys.runtime.model.SimpleViewItemGroup; 067import org.ametys.runtime.model.View; 068import org.ametys.runtime.model.ViewItem; 069import org.ametys.runtime.model.ViewItemAccessor; 070import org.ametys.runtime.model.ViewItemContainer; 071import org.ametys.runtime.model.ViewItemGroup; 072import org.ametys.runtime.model.ViewParser; 073import org.ametys.runtime.model.ViewParserContext; 074import org.ametys.runtime.model.type.DataContext; 075import org.ametys.runtime.plugin.component.LogEnabled; 076import org.ametys.runtime.plugin.component.PluginAware; 077import org.ametys.runtime.plugin.component.ThreadSafeComponentManager; 078 079/** 080 * Static implementation of a {@link SearchUIModel} 081 */ 082public class StaticSearchUIModel extends DefaultSearchUIModel implements Serviceable, Contextualizable, Configurable, Disposable, LogEnabled, PluginAware 083{ 084 /** ComponentManager for {@link SearchModelCriterionDefinition}s. */ 085 protected ThreadSafeComponentManager<SearchModelCriterionDefinition> _criterionDefinitionManager; 086 087 /** The context. */ 088 protected Context _context; 089 090 /** The service manager */ 091 protected ServiceManager _manager; 092 093 /** The plugin name */ 094 protected String _pluginName; 095 096 /** The content type helper. */ 097 protected ContentTypesHelper _contentTypesHelper; 098 099 /** The helper for columns */ 100 protected ColumnHelper _columnHelper; 101 102 /** The content type extension point */ 103 protected ContentTypeExtensionPoint _contentTypeExtensionPoint; 104 105 /** The system property extension point. */ 106 protected SystemPropertyExtensionPoint _systemPropertyExtensionPoint; 107 108 /** The criterion definition helper */ 109 protected CriterionDefinitionHelper _criterionDefinitionHelper; 110 111 /** The helper for search model criterion definition */ 112 protected SearchModelCriterionDefinitionHelper _searchModelCriterionDefinitionHelper; 113 114 /** The helper for search model criterion definition */ 115 protected SearchModelCriterionViewItemHelper _searchUIModelCriterionDefinitionHelper; 116 117 /** The logger. */ 118 protected Logger _logger; 119 120 /** The search model identifier */ 121 protected String _id; 122 123 private int _criteriaIndex; 124 125 public void setLogger(final Logger logger) 126 { 127 _logger = logger; 128 } 129 130 /** 131 * Get the logger. 132 * @return the logger. 133 */ 134 protected final Logger getLogger() 135 { 136 return _logger; 137 } 138 139 @Override 140 public void contextualize(Context context) throws ContextException 141 { 142 _context = context; 143 } 144 145 public void service(ServiceManager manager) throws ServiceException 146 { 147 _manager = manager; 148 149 _contentTypesHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE); 150 _columnHelper = (ColumnHelper) manager.lookup(ColumnHelper.ROLE); 151 _contentTypeExtensionPoint = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE); 152 _systemPropertyExtensionPoint = (SystemPropertyExtensionPoint) manager.lookup(SystemPropertyExtensionPoint.ROLE); 153 _criterionDefinitionHelper = (CriterionDefinitionHelper) manager.lookup(CriterionDefinitionHelper.ROLE); 154 _searchModelCriterionDefinitionHelper = (SearchModelCriterionDefinitionHelper) manager.lookup(SearchModelCriterionDefinitionHelper.ROLE); 155 _searchUIModelCriterionDefinitionHelper = (SearchModelCriterionViewItemHelper) manager.lookup(SearchModelCriterionViewItemHelper.ROLE); 156 } 157 158 public void setPluginInfo(String pluginName, String featureName, String id) 159 { 160 _pluginName = pluginName; 161 _id = id; 162 } 163 164 @Override 165 public void configure(Configuration configuration) throws ConfigurationException 166 { 167 try 168 { 169 _criterionDefinitionManager = new ThreadSafeComponentManager<>(); 170 _criterionDefinitionManager.setLogger(getLogger()); 171 _criterionDefinitionManager.contextualize(_context); 172 _criterionDefinitionManager.service(_manager); 173 174 Configuration searchConfig = configuration.getChild("SearchModel"); 175 setLabel(I18nizableText.parseI18nizableText(searchConfig.getChild("label", false), "plugin." + _pluginName)); 176 setDescription(I18nizableText.parseI18nizableText(searchConfig.getChild("description", false), "plugin." + _pluginName)); 177 178 _configureTags(searchConfig.getChild("tags")); 179 180 _configureContentTypes(searchConfig.getChild("content-types", false)); 181 182 setSummaryView(searchConfig.getChild("summary-view").getValue(null)); 183 setAllowSortOnMultipleJoin(searchConfig.getChild("allow-sort-on-multiple-join").getValueAsBoolean(super.allowSortOnMultipleJoin())); 184 185 // Get the base content types and try to find common ancestors. 186 Set<String> baseCTypeIds = _configureBaseContentTypes(searchConfig.getChild("content-types")); 187 Set<ContentType> commonContentTypes = _contentTypesHelper.getCommonAncestors(baseCTypeIds) 188 .stream() 189 .map(_contentTypeExtensionPoint::getExtension) 190 .collect(Collectors.toSet()); 191 192 int pageSize = searchConfig.getChild("page-size").getValueAsInteger(50); 193 if (pageSize < 0) 194 { 195 pageSize = 50; 196 } 197 setPageSize(pageSize); 198 setWorkspace(searchConfig.getChild("workspace").getValue(null)); 199 200 _configureSearchUrl(searchConfig); 201 _configureExportCSVUrl(searchConfig); 202 _configurePrintUrl(searchConfig); 203 204 _criteriaIndex = 0; 205 Map<String, ModelViewItem> criteriaRoles = new HashMap<>(); 206 Map<String, ModelViewItem> advancedCriteriaRoles = new HashMap<>(); 207 Map<String, ModelViewItem> facetedCriteriaRoles = new HashMap<>(); 208 209 Configuration criteriaConf = searchConfig.getChild("simple-search-criteria"); 210 ViewItemContainer criteria = _parseCriteria(commonContentTypes, criteriaConf, criteriaRoles, true); 211 212 Configuration advancedCriteriaConf = searchConfig.getChild("advanced-search-criteria", false); 213 ViewItemContainer advancedCriteria = new View(); 214 if (advancedCriteriaConf != null) 215 { 216 advancedCriteria = _parseCriteria(commonContentTypes, advancedCriteriaConf, advancedCriteriaRoles, false); 217 } 218 219 Configuration facetsConf = searchConfig.getChild("facets", false); 220 ViewItemContainer facetedCriteria = new View(); 221 if (facetsConf != null) 222 { 223 facetedCriteria = _parseCriteria(commonContentTypes, facetsConf, facetedCriteriaRoles, false); 224 } 225 226 _criterionDefinitionManager.initialize(); 227 228 _lookupCriteriaComponents(criteriaRoles, false); 229 if (advancedCriteriaConf != null) 230 { 231 if (advancedCriteria.getViewItems().isEmpty()) 232 { 233 // The advanced criteria root configuration is present but has no criteria: 234 // copy the simple criteria. 235 advancedCriteria = _copyCriteria(criteria, this::_isAdvanced); 236 } 237 else 238 { 239 _lookupCriteriaComponents(advancedCriteriaRoles, false); 240 } 241 } 242 243 if (facetsConf != null) 244 { 245 if (facetedCriteria.getViewItems().isEmpty()) 246 { 247 // The facet root configuration is present but has no criteria: 248 // copy the simple criteria which are facetable. 249 facetedCriteria = _copyCriteria(criteria, this::_isFacetable); 250 } 251 else 252 { 253 _lookupCriteriaComponents(facetedCriteriaRoles, true); 254 } 255 } 256 257 setCriteria(criteria); 258 setAdvancedCriteria(advancedCriteria); 259 setFacetedCriteria(facetedCriteria); 260 261 Configuration columnConfs = searchConfig.getChild("columns").getChild("default"); 262 ViewParser parser = new StaticSearchUIModelColumnsParser(commonContentTypes); 263 try 264 { 265 LifecycleHelper.setupComponent(parser, new SLF4JLoggerAdapter(getLogger()), _context, _manager, null); 266 setResultItems(parser.parseView(new ConfigurationAndPluginName(columnConfs, ""), ViewParserContext.newInstance())); 267 } 268 catch (Exception e) 269 { 270 throw new ConfigurationException("Unable to parse columns of search model", columnConfs, e); 271 } 272 finally 273 { 274 LifecycleHelper.dispose(parser); 275 } 276 } 277 catch (Exception e) 278 { 279 throw new ConfigurationException("Unable to create local component managers.", configuration, e); 280 } 281 } 282 283 private void _configureTags(Configuration tagsConfiguration) 284 { 285 Set<String> tags = new HashSet<>(); 286 for (Configuration tagCfg : tagsConfiguration.getChildren("tag")) 287 { 288 String tag = tagCfg.getValue(null); 289 if (StringUtils.isNotBlank(tag)) 290 { 291 tags.add(tag); 292 } 293 } 294 setTags(tags); 295 } 296 297 @Override 298 public void dispose() 299 { 300 _criterionDefinitionManager.dispose(); 301 _criterionDefinitionManager = null; 302 } 303 304 /** 305 * Configure the content type ids 306 * @param configuration The content types configuration 307 * @throws ConfigurationException If an error occurs 308 */ 309 protected void _configureContentTypes(Configuration configuration) throws ConfigurationException 310 { 311 Set<String> contentTypes = new HashSet<>(); 312 Set<String> excludedContentTypes = new HashSet<>(); 313 314 if (configuration != null) 315 { 316 Configuration excludeConf = configuration.getChild("exclude"); 317 318 List<String> excludedTags = new ArrayList<>(); 319 for (Configuration tagCong : excludeConf.getChildren("tag")) 320 { 321 excludedTags.add(tagCong.getValue()); 322 } 323 324 List<String> excludedCTypes = new ArrayList<>(); 325 for (Configuration cType : excludeConf.getChildren("content-type")) 326 { 327 excludedCTypes.add(cType.getValue()); 328 } 329 330 Configuration[] cTypesConfiguration = configuration.getChildren("content-type"); 331 if (cTypesConfiguration.length == 0) 332 { 333 // Keep "content types" empty. 334 for (String id : _contentTypeExtensionPoint.getExtensionsIds()) 335 { 336 if (!_isValidContentType(id, excludedTags, excludedCTypes)) 337 { 338 excludedContentTypes.add(id); 339 } 340 } 341 } 342 else 343 { 344 for (Configuration conf : configuration.getChildren("content-type")) 345 { 346 String id = conf.getAttribute("id"); 347 contentTypes.add(id); 348 if (!_isValidContentType(id, excludedTags, excludedCTypes)) 349 { 350 excludedContentTypes.add(id); 351 } 352 353 for (String subTypeId : _contentTypeExtensionPoint.getSubTypes(id)) 354 { 355 if (!_isValidContentType(subTypeId, excludedTags, excludedCTypes)) 356 { 357 excludedContentTypes.add(subTypeId); 358 } 359 } 360 } 361 } 362 } 363 364 setContentTypes(contentTypes); 365 setExcludedContentTypes(excludedContentTypes); 366 } 367 368 /** 369 * Configure the base content type ids. 370 * @param configuration The content types configuration 371 * @return The set of base content type ids 372 * @throws ConfigurationException If an error occurs 373 */ 374 protected Set<String> _configureBaseContentTypes(Configuration configuration) throws ConfigurationException 375 { 376 Set<String> cTypes = new HashSet<>(); 377 378 Configuration[] cTypesConfiguration = configuration.getChildren("content-type"); 379 if (cTypesConfiguration.length == 0) 380 { 381 cTypes.addAll(_contentTypeExtensionPoint.getExtensionsIds()); 382 } 383 else 384 { 385 for (Configuration conf : cTypesConfiguration) 386 { 387 cTypes.add(conf.getAttribute("id")); 388 } 389 } 390 391 return cTypes; 392 } 393 394 /** 395 * Determines if the content type is a valid content type in current configuration 396 * @param id The content type id 397 * @param excludedTags The tags to exclude 398 * @param excludedContentTypes The content types to exclude 399 * @return <code>true</code> if the content type is a valid content type 400 */ 401 protected boolean _isValidContentType (String id, List<String> excludedTags, List<String> excludedContentTypes) 402 { 403 if (excludedContentTypes.contains(id)) 404 { 405 return false; 406 } 407 408 ContentType cType = _contentTypeExtensionPoint.getExtension(id); 409 for (String tag : excludedTags) 410 { 411 if (cType.hasTag(tag)) 412 { 413 return false; 414 } 415 } 416 417 return true; 418 } 419 420 private void _configureSearchUrl(Configuration configuration) 421 { 422 setSearchUrlPlugin(configuration.getChild("search-url").getAttribute("plugin", __DEFAULT_URL_PLUGIN)); 423 setSearchUrl(configuration.getChild("search-url").getValue(__DEFAULT_SEARCH_URL)); 424 } 425 426 private void _configureExportCSVUrl(Configuration configuration) 427 { 428 setExportCSVUrlPlugin(configuration.getChild("export-csv-url").getAttribute("plugin", __DEFAULT_URL_PLUGIN)); 429 setExportCSVUrl(configuration.getChild("export-csv-url").getValue(__DEFAULT_EXPORT_CSV_URL)); 430 } 431 432 private void _configurePrintUrl(Configuration configuration) 433 { 434 setPrintUrlPlugin(configuration.getChild("print-url").getAttribute("plugin", __DEFAULT_URL_PLUGIN)); 435 setPrintUrl(configuration.getChild("print-url").getValue(__DEFAULT_PRINT_URL)); 436 } 437 438 /** 439 * Parses criteria in the given configuration. 440 * Add components in Search criteria manager and fill the Map with roles and {@link SearchModelCriterionViewItem} 441 * @param contentTypes the model's content types 442 * @param configuration the model's configuration. 443 * @param rolesByUICriteria the map to fill with the {@link SearchModelCriterionViewItem} and the role of the referenced {@link SearchModelCriterionDefinition} 444 * @param acceptGroups <code>true</code> if the parsed criteria is able to have groups, <code>false</code> otherwise 445 * @return the parsed criteria 446 * @throws ConfigurationException if an error occurs. 447 */ 448 protected ViewItemContainer _parseCriteria(Set<ContentType> contentTypes, Configuration configuration, Map<String, ModelViewItem> rolesByUICriteria, boolean acceptGroups) throws ConfigurationException 449 { 450 ViewItemContainer criteria = new View(); 451 ViewItemAccessor criteriaWithoutGroupAccessor = criteria; 452 453 if (acceptGroups) 454 { 455 for (Configuration groupConf : configuration.getChildren("group")) 456 { 457 SimpleViewItemGroup group = new SimpleViewItemGroup(); 458 group.setRole(groupConf.getAttribute("role", ViewItemGroup.FIELDSET_ROLE)); 459 group.setName(groupConf.getAttribute("name", null)); 460 461 ConfigurationAndPluginName groupConfAndPluginName = new ConfigurationAndPluginName(groupConf, _pluginName); 462 group.setLabel(ItemParserHelper.parseI18nizableText(groupConfAndPluginName, "label")); 463 group.setDescription(ItemParserHelper.parseI18nizableText(groupConfAndPluginName, "description")); 464 465 criteria.addViewItem(group); 466 467 List<SearchModelCriterionViewItemWithRole> uiCriteriaWithRoles = _parseCriteria(contentTypes, groupConf); 468 group.addViewItems(uiCriteriaWithRoles.stream().map(SearchModelCriterionViewItemWithRole::criterionViewItem).toList()); 469 uiCriteriaWithRoles.stream() 470 .filter(criterionViewItemWithRole -> criterionViewItemWithRole.role().isPresent()) 471 .forEachOrdered(criterionViewItemWithRole -> rolesByUICriteria.put(criterionViewItemWithRole.role().get(), criterionViewItemWithRole.criterionViewItem())); 472 } 473 474 // Group for criteria with no group 475 SimpleViewItemGroup group = new SimpleViewItemGroup(); 476 group.setRole(ViewItemGroup.FIELDSET_ROLE); 477 criteria.addViewItem(group); 478 criteriaWithoutGroupAccessor = group; 479 } 480 481 // Criteria with no group 482 List<SearchModelCriterionViewItemWithRole> uiCriteriaWithRoles = _parseCriteria(contentTypes, configuration); 483 criteriaWithoutGroupAccessor.addViewItems(uiCriteriaWithRoles.stream().map(SearchModelCriterionViewItemWithRole::criterionViewItem).toList()); 484 uiCriteriaWithRoles.stream() 485 .filter(criterionViewItemWithRole -> criterionViewItemWithRole.role().isPresent()) 486 .forEachOrdered(criterionViewItemWithRole -> rolesByUICriteria.put(criterionViewItemWithRole.role().get(), criterionViewItemWithRole.criterionViewItem())); 487 488 return criteria; 489 } 490 491 /** 492 * Parses criteria in the given configuration. 493 * Add components in Search criteria manager 494 * @param contentTypes the model's content types 495 * @param configuration the model's configuration. 496 * @return the Map with roles and {@link SearchModelCriterionViewItem} 497 * @throws ConfigurationException if an error occurs. 498 */ 499 protected List<SearchModelCriterionViewItemWithRole> _parseCriteria(Set<ContentType> contentTypes, Configuration configuration) throws ConfigurationException 500 { 501 List<SearchModelCriterionViewItemWithRole> uiCriteriaWithRoles = new ArrayList<>(); 502 503 for (Configuration conf : configuration.getChildren("item")) 504 { 505 String reference = conf.getAttribute("ref", null); 506 507 if (StringUtils.isNotEmpty(reference)) 508 { 509 uiCriteriaWithRoles.addAll(_parseReferencingCriteria(contentTypes, conf, reference)); 510 } 511 else 512 { 513 uiCriteriaWithRoles.addAll(_parseStaticCriteria(contentTypes, conf)); 514 } 515 } 516 517 return uiCriteriaWithRoles; 518 } 519 520 /** 521 * Parses a criteria that references a model item of model's content types, or a system property 522 * Add a referencing criteria component to the manager. 523 * @param contentTypes the model's content types. 524 * @param conf the criteria configuration. 525 * @param reference the path of the referenced item 526 * @return A Map with the created UI criteria and potential role of the corresponding criterion 527 * @throws ConfigurationException if an error occurs. 528 */ 529 protected List<SearchModelCriterionViewItemWithRole> _parseReferencingCriteria(Set<ContentType> contentTypes, Configuration conf, String reference) throws ConfigurationException 530 { 531 try 532 { 533 if (ViewParser.ALL_ITEMS_REFERENCE.equals(reference)) 534 { 535 List<SearchModelCriterionViewItemWithRole> viewItemsWithRoles = new ArrayList<>(); 536 537 if (contentTypes.isEmpty()) 538 { 539 // There is no given content types, only add the title attribute 540 SearchModelCriterionViewItem titleCriterionViewItem = _searchUIModelCriterionDefinitionHelper.createReferencingCriterionViewItem(this, _contentTypesHelper.getTitleAttributeDefinition(), Content.ATTRIBUTE_TITLE); 541 viewItemsWithRoles.add(new SearchModelCriterionViewItemWithRole(titleCriterionViewItem, Optional.empty())); 542 } 543 else 544 { 545 // Add criteria for all model items of the given content types 546 for (ContentType contentType : contentTypes) 547 { 548 for (ModelItem modelItem : contentType.getModelItems()) 549 { 550 _getCriterionViewItemWithRole(modelItem).ifPresent(viewItemsWithRoles::add); 551 } 552 } 553 } 554 555 // Add criteria for all system properties 556 for (String systemPropertyId : _systemPropertyExtensionPoint.getExtensionsIds()) 557 { 558 SystemProperty systemProperty = _systemPropertyExtensionPoint.getExtension(systemPropertyId); 559 _getCriterionViewItemWithRole(systemProperty).ifPresent(viewItemsWithRoles::add); 560 } 561 562 return viewItemsWithRoles; 563 } 564 else 565 { 566 if (contentTypes.isEmpty() && !Content.ATTRIBUTE_TITLE.equals(reference) && !_systemPropertyExtensionPoint.hasExtension(reference)) 567 { 568 throw new ConfigurationException("The criteria '" + reference + "' is forbidden when no content type is specified: only title or system properties can be used."); 569 } 570 571 // The criteria references a specific item 572 String role = reference + _criteriaIndex; 573 _criteriaIndex++; 574 SearchModelCriterionViewItem criterionViewItem = _parseCriterionViewItem(conf); 575 576 Configuration wrapConf = _criterionDefinitionHelper.wrapCriterionConfiguration(conf, contentTypes); 577 Class<? extends SearchModelCriterionDefinition> criterionDefinitionClass = _searchModelCriterionDefinitionHelper.getStaticCriterionDefinitionClass(contentTypes, reference); 578 if (criterionDefinitionClass != null) 579 { 580 _criterionDefinitionManager.addComponent(_pluginName, null, role, _searchModelCriterionDefinitionHelper.getStaticCriterionDefinitionClass(contentTypes, reference), wrapConf); 581 return List.of(new SearchModelCriterionViewItemWithRole(criterionViewItem, Optional.of(role))); 582 } 583 else 584 { 585 return List.of(); 586 } 587 } 588 } 589 catch (Exception e) 590 { 591 throw new ConfigurationException("Unable to instanciate criterion definition referencing " + reference, conf, e); 592 } 593 } 594 595 private Optional<SearchModelCriterionViewItemWithRole> _getCriterionViewItemWithRole(ModelItem modelItem) 596 { 597 if (modelItem instanceof ElementDefinition definition // Get only first-level field (ignore composites and repeaters) 598 && (!(definition instanceof Property) || definition instanceof CriterionDefinitionAwareElementDefinition)) // Exclude properties that are not criterion aware 599 { 600 SearchModelCriterionViewItem criterionViewItem = _searchUIModelCriterionDefinitionHelper.createReferencingCriterionViewItem(this, definition, definition.getName()); 601 if (criterionViewItem != null) 602 { 603 return Optional.of(new SearchModelCriterionViewItemWithRole(criterionViewItem, Optional.empty())); 604 } 605 } 606 607 return Optional.empty(); 608 } 609 610 /** 611 * Parses a static criteria 612 * Add a referencing criteria component to the manager. 613 * @param contentTypes the model's content types. 614 * @param conf the criteria configuration. 615 * @return A Map with the created UI criteria and potential role of the corresponding criterion 616 * @throws ConfigurationException if an error occurs. 617 */ 618 protected List<SearchModelCriterionViewItemWithRole> _parseStaticCriteria(Set<ContentType> contentTypes, Configuration conf) throws ConfigurationException 619 { 620 String criteriaName = conf.getAttribute("name", null); 621 String className = conf.getAttribute("class", null); 622 623 if (criteriaName == null || className == null) 624 { 625 throw new ConfigurationException("The custom criterion definition '" + criteriaName + "' does not specifiy a class or a name.", conf); 626 } 627 628 try 629 { 630 String role = criteriaName + _criteriaIndex; 631 _criteriaIndex++; 632 633 SearchModelCriterionViewItem criterionViewItem = _parseCriterionViewItem(conf); 634 635 @SuppressWarnings("unchecked") 636 Class<SearchModelCriterionDefinition> criterionDefinitionClass = (Class<SearchModelCriterionDefinition>) Class.forName(className); 637 Configuration wrapConf = _criterionDefinitionHelper.wrapCriterionConfiguration(conf, contentTypes); 638 _criterionDefinitionManager.addComponent(_pluginName, null, role, criterionDefinitionClass, wrapConf); 639 640 return List.of(new SearchModelCriterionViewItemWithRole(criterionViewItem, Optional.of(role))); 641 } 642 catch (Exception e) 643 { 644 throw new ConfigurationException("Unable to instanciate custom criterion view item for class: " + className, conf, e); 645 } 646 } 647 648 /** 649 * Parses the {@link SearchModelCriterionViewItem} from the given configuration 650 * @param configuration the configuration 651 * @return the parsed UI criterion 652 * @throws ConfigurationException if an error occurs 653 */ 654 protected SearchModelCriterionViewItem _parseCriterionViewItem(Configuration configuration) throws ConfigurationException 655 { 656 SearchModelCriterionViewItem criterionViewItem = new DefaultSearchModelCriterionViewItem(); 657 criterionViewItem.setHidden(configuration.getAttributeAsBoolean("hidden", false)); 658 return criterionViewItem; 659 } 660 661 /** 662 * Lookup the previously initialized criteria components and add the criteria to their pending {@link SearchModelCriterionViewItem} 663 * @param criteriaRoles the criteria map to fill. 664 * @param checkFacetable true to check if the criteria are facetable. 665 * @throws ConfigurationException if an error occurs. 666 */ 667 protected void _lookupCriteriaComponents(Map<String, ModelViewItem> criteriaRoles, boolean checkFacetable) throws ConfigurationException 668 { 669 for (Map.Entry<String, ModelViewItem> criteriaRole : criteriaRoles.entrySet()) 670 { 671 String role = criteriaRole.getKey(); 672 ModelViewItem<SearchModelCriterionDefinition> criterionViewItem = criteriaRole.getValue(); 673 try 674 { 675 SearchModelCriterionDefinition criterion = _criterionDefinitionManager.lookup(role); 676 criterion.setModel(this); 677 criterionViewItem.setDefinition(criterion); 678 679 if (checkFacetable && !_isFacetable(criterion)) 680 { 681 throw new ConfigurationException("The criterion definition of id '" + criterion.getName() + "' is not facetable."); 682 } 683 } 684 catch (ComponentException e) 685 { 686 throw new ConfigurationException("Impossible to lookup the criterion definition of role: " + role, e); 687 } 688 } 689 } 690 691 /** 692 * Copy the given criteria 693 * Groups are ignored and criteria are filtered by the given {@link Predicate} 694 * @param criteria the criteria to copy 695 * @param filter the filter to apply on copied criteria 696 * @return the copied criteria 697 * @throws ConfigurationException if an error occurs. 698 */ 699 protected ViewItemContainer _copyCriteria(ViewItemContainer criteria, Predicate<ModelItem> filter) throws ConfigurationException 700 { 701 ViewItemContainer copy = new View(); 702 copy.addViewItems(_copyCriteriaViewItems(criteria, filter)); 703 704 return copy; 705 } 706 707 /** 708 * Retrieves the copies of the given criteria 709 * @param criteria the criteria to copy 710 * @param filter the filter to apply on copied criteria 711 * @return the copied criteria 712 * @throws ConfigurationException if an error occurs. 713 */ 714 protected List<ViewItem> _copyCriteriaViewItems(ViewItemContainer criteria, Predicate<ModelItem> filter) throws ConfigurationException 715 { 716 List<ViewItem> viewItems = new ArrayList<>(); 717 718 for (ViewItem viewItem : criteria.getViewItems()) 719 { 720 if (viewItem instanceof ViewItemContainer group) 721 { 722 viewItems.addAll(_copyCriteriaViewItems(group, filter)); 723 } 724 725 if (viewItem instanceof SearchModelCriterionViewItem criterionViewItem && filter.test(criterionViewItem.getDefinition())) 726 { 727 ViewItem advancedCriterionViewItem = criterionViewItem.createInstance(); 728 criterionViewItem.copyTo(advancedCriterionViewItem); 729 viewItems.add(advancedCriterionViewItem); 730 } 731 } 732 733 return viewItems; 734 } 735 736 /** 737 * Test if a criterion definition can be used in advanced search mode. 738 * For instance: geocode, rich-text, file-typed criterion are not allowed. 739 * @param modelItem the criterion definition to test. 740 * @return <code>true</code> if the criterion can be used in advanced search mode, <code>false</code> otherwise. 741 */ 742 @SuppressWarnings("static-access") 743 protected boolean _isAdvanced(ModelItem modelItem) 744 { 745 String typeId = modelItem.getType().getId(); 746 switch (typeId) 747 { 748 case ModelItemTypeConstants.STRING_TYPE_ID: 749 case ModelItemTypeConstants.MULTILINGUAL_STRING_ELEMENT_TYPE_ID: 750 case ModelItemTypeConstants.LONG_TYPE_ID: 751 case ModelItemTypeConstants.DOUBLE_TYPE_ID: 752 case ModelItemTypeConstants.DATE_TYPE_ID: 753 case ModelItemTypeConstants.DATETIME_TYPE_ID: 754 case ModelItemTypeConstants.BOOLEAN_TYPE_ID: 755 case ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID: 756 case ModelItemTypeConstants.USER_ELEMENT_TYPE_ID: 757 case ModelItemTypeConstants.RICH_TEXT_ELEMENT_TYPE_ID: 758 return true; 759 case ModelItemTypeConstants.FILE_ELEMENT_TYPE_ID: 760 case ModelItemTypeConstants.BINARY_ELEMENT_TYPE_ID: 761 case ModelItemTypeConstants.REFERENCE_ELEMENT_TYPE_ID: 762 case ModelItemTypeConstants.GEOCODE_ELEMENT_TYPE_ID: 763 default: 764 return false; 765 } 766 } 767 768 /** 769 * Test if a criterion definition can be used as a facet 770 * @param modelItem the criterion definition to test. 771 * @return <code>true</code> if the criterion can be used as a facet, <code>false</code> otherwise. 772 */ 773 protected boolean _isFacetable(ModelItem modelItem) 774 { 775 if (modelItem instanceof ReferencingSearchModelCriterionDefinition criterionDefinition) 776 { 777 ElementDefinition reference = criterionDefinition.getReference(); 778 return reference instanceof IndexationAwareElementDefinition indexationAwareElementDefinition 779 ? indexationAwareElementDefinition.isFacetable() 780 : modelItem instanceof Property 781 ? false 782 : criterionDefinition.getType().isFacetable(DataContext.newInstance() 783 .withModelItem(criterionDefinition)); 784 } 785 else 786 { 787 return false; 788 } 789 } 790 791 public String getId() 792 { 793 return _id; 794 } 795 796 private record SearchModelCriterionViewItemWithRole(ModelViewItem criterionViewItem, Optional<String> role) { /* empty */ } 797}