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 setCriteria(criteria); 254 setAdvancedCriteria(advancedCriteria); 255 setFacetedCriteria(facetedCriteria); 256 257 Configuration columnConfs = searchConfig.getChild("columns").getChild("default"); 258 ViewParser parser = new StaticSearchUIModelColumnsParser(commonContentTypes); 259 try 260 { 261 LifecycleHelper.setupComponent(parser, new SLF4JLoggerAdapter(getLogger()), _context, _manager, null); 262 setResultItems(parser.parseView(new ConfigurationAndPluginName(columnConfs, ""))); 263 } 264 catch (Exception e) 265 { 266 throw new ConfigurationException("Unable to parse columns of search model", columnConfs, e); 267 } 268 finally 269 { 270 LifecycleHelper.dispose(parser); 271 } 272 } 273 catch (Exception e) 274 { 275 throw new ConfigurationException("Unable to create local component managers.", configuration, e); 276 } 277 } 278 279 @Override 280 public void dispose() 281 { 282 _criterionDefinitionManager.dispose(); 283 _criterionDefinitionManager = null; 284 } 285 286 /** 287 * Configure the content type ids 288 * @param configuration The content types configuration 289 * @throws ConfigurationException If an error occurs 290 */ 291 protected void _configureContentTypes(Configuration configuration) throws ConfigurationException 292 { 293 Set<String> contentTypes = new HashSet<>(); 294 Set<String> excludedContentTypes = new HashSet<>(); 295 296 if (configuration != null) 297 { 298 Configuration excludeConf = configuration.getChild("exclude"); 299 300 List<String> excludedTags = new ArrayList<>(); 301 for (Configuration tagCong : excludeConf.getChildren("tag")) 302 { 303 excludedTags.add(tagCong.getValue()); 304 } 305 306 List<String> excludedCTypes = new ArrayList<>(); 307 for (Configuration cType : excludeConf.getChildren("content-type")) 308 { 309 excludedCTypes.add(cType.getValue()); 310 } 311 312 Configuration[] cTypesConfiguration = configuration.getChildren("content-type"); 313 if (cTypesConfiguration.length == 0) 314 { 315 // Keep "content types" empty. 316 for (String id : _contentTypeExtensionPoint.getExtensionsIds()) 317 { 318 if (!_isValidContentType(id, excludedTags, excludedCTypes)) 319 { 320 excludedContentTypes.add(id); 321 } 322 } 323 } 324 else 325 { 326 for (Configuration conf : configuration.getChildren("content-type")) 327 { 328 String id = conf.getAttribute("id"); 329 contentTypes.add(id); 330 if (!_isValidContentType(id, excludedTags, excludedCTypes)) 331 { 332 excludedContentTypes.add(id); 333 } 334 335 for (String subTypeId : _contentTypeExtensionPoint.getSubTypes(id)) 336 { 337 if (!_isValidContentType(subTypeId, excludedTags, excludedCTypes)) 338 { 339 excludedContentTypes.add(subTypeId); 340 } 341 } 342 } 343 } 344 } 345 346 setContentTypes(contentTypes); 347 setExcludedContentTypes(excludedContentTypes); 348 } 349 350 /** 351 * Configure the base content type ids. 352 * @param configuration The content types configuration 353 * @return The set of base content type ids 354 * @throws ConfigurationException If an error occurs 355 */ 356 protected Set<String> _configureBaseContentTypes(Configuration configuration) throws ConfigurationException 357 { 358 Set<String> cTypes = new HashSet<>(); 359 360 Configuration[] cTypesConfiguration = configuration.getChildren("content-type"); 361 if (cTypesConfiguration.length == 0) 362 { 363 cTypes.addAll(_contentTypeExtensionPoint.getExtensionsIds()); 364 } 365 else 366 { 367 for (Configuration conf : cTypesConfiguration) 368 { 369 cTypes.add(conf.getAttribute("id")); 370 } 371 } 372 373 return cTypes; 374 } 375 376 /** 377 * Determines if the content type is a valid content type in current configuration 378 * @param id The content type id 379 * @param excludedTags The tags to exclude 380 * @param excludedContentTypes The content types to exclude 381 * @return <code>true</code> if the content type is a valid content type 382 */ 383 protected boolean _isValidContentType (String id, List<String> excludedTags, List<String> excludedContentTypes) 384 { 385 if (excludedContentTypes.contains(id)) 386 { 387 return false; 388 } 389 390 ContentType cType = _contentTypeExtensionPoint.getExtension(id); 391 for (String tag : excludedTags) 392 { 393 if (cType.hasTag(tag)) 394 { 395 return false; 396 } 397 } 398 399 return true; 400 } 401 402 private void _configureSearchUrl(Configuration configuration) 403 { 404 setSearchUrlPlugin(configuration.getChild("search-url").getAttribute("plugin", __DEFAULT_URL_PLUGIN)); 405 setSearchUrl(configuration.getChild("search-url").getValue(__DEFAULT_SEARCH_URL)); 406 } 407 408 private void _configureExportCSVUrl(Configuration configuration) 409 { 410 setExportCSVUrlPlugin(configuration.getChild("export-csv-url").getAttribute("plugin", __DEFAULT_URL_PLUGIN)); 411 setExportCSVUrl(configuration.getChild("export-csv-url").getValue(__DEFAULT_EXPORT_CSV_URL)); 412 } 413 414 private void _configureExportDOCUrl(Configuration configuration) 415 { 416 setExportDOCUrlPlugin(configuration.getChild("export-doc-url").getAttribute("plugin", __DEFAULT_URL_PLUGIN)); 417 setExportDOCUrl(configuration.getChild("export-doc-url").getValue(__DEFAULT_EXPORT_DOC_URL)); 418 } 419 420 private void _configureExportXMLUrl(Configuration configuration) 421 { 422 setExportXMLUrlPlugin(configuration.getChild("export-xml-url").getAttribute("plugin", __DEFAULT_URL_PLUGIN)); 423 setExportXMLUrl(configuration.getChild("export-xml-url").getValue(__DEFAULT_EXPORT_XML_URL)); 424 } 425 426 private void _configureExportPDFUrl(Configuration configuration) 427 { 428 setExportPDFUrlPlugin(configuration.getChild("export-pdf-url").getAttribute("plugin", __DEFAULT_URL_PLUGIN)); 429 setExportPDFUrl(configuration.getChild("export-pdf-url").getValue(__DEFAULT_EXPORT_PDF_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("cms", 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("cms", 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}