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