001/* 002 * Copyright 2013 Anyware Services 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.ametys.cms.search.ui.model.impl; 017 018import java.util.Arrays; 019import java.util.Collections; 020import java.util.HashMap; 021import java.util.List; 022import java.util.Locale; 023import java.util.Map; 024import java.util.Optional; 025import java.util.stream.Collectors; 026 027import org.apache.avalon.framework.activity.Disposable; 028import org.apache.avalon.framework.configuration.Configurable; 029import org.apache.avalon.framework.configuration.Configuration; 030import org.apache.avalon.framework.configuration.ConfigurationException; 031import org.apache.avalon.framework.context.Context; 032import org.apache.avalon.framework.context.ContextException; 033import org.apache.avalon.framework.context.Contextualizable; 034import org.apache.avalon.framework.service.ServiceException; 035import org.apache.avalon.framework.service.ServiceManager; 036import org.apache.avalon.framework.service.Serviceable; 037import org.apache.commons.lang3.StringUtils; 038import org.slf4j.Logger; 039import org.slf4j.LoggerFactory; 040 041import org.ametys.cms.content.ContentHelper; 042import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 043import org.ametys.cms.contenttype.MetadataType; 044import org.ametys.cms.data.type.ModelItemTypeConstants; 045import org.ametys.cms.model.ContentElementDefinition; 046import org.ametys.cms.model.properties.ElementRefProperty; 047import org.ametys.cms.repository.Content; 048import org.ametys.cms.search.SearchField; 049import org.ametys.cms.search.ui.model.SearchUICriterion; 050import org.ametys.cms.search.ui.model.SearchUIModel; 051import org.ametys.core.user.User; 052import org.ametys.core.user.UserIdentity; 053import org.ametys.core.user.UserManager; 054import org.ametys.core.user.population.UserPopulationDAO; 055import org.ametys.plugins.core.ui.util.ConfigurationHelper; 056import org.ametys.plugins.repository.AmetysObjectResolver; 057import org.ametys.runtime.i18n.I18nizableText; 058import org.ametys.runtime.i18n.I18nizableTextParameter; 059import org.ametys.runtime.model.ElementDefinition; 060import org.ametys.runtime.model.Enumerator; 061import org.ametys.runtime.model.StaticEnumerator; 062import org.ametys.runtime.parameter.DefaultValidator; 063import org.ametys.runtime.parameter.Parameter; 064import org.ametys.runtime.parameter.Validator; 065import org.ametys.runtime.plugin.component.LogEnabled; 066import org.ametys.runtime.plugin.component.ThreadSafeComponentManager; 067 068/** 069 * This class represents a search criterion of a {@link SearchUIModel}. 070 */ 071public abstract class AbstractSearchUICriterion extends Parameter<MetadataType> implements SearchUICriterion, Contextualizable, Serviceable, Configurable, Disposable, LogEnabled 072{ 073 private static final Logger __LOGGER = LoggerFactory.getLogger(AbstractSearchUICriterion.class); 074 075 /** The ametys object resolver. */ 076 protected AmetysObjectResolver _resolver; 077 078 /** The content type extension point. */ 079 protected ContentTypeExtensionPoint _cTypeEP; 080 081 /** The content type extension point. */ 082 protected ContentHelper _contentHelper; 083 084 /** The user manager. */ 085 protected UserManager _userManager; 086 087 /** The user population DAO */ 088 protected UserPopulationDAO _userPopulationDAO; 089 090 /** ComponentManager for {@link Enumerator}s. */ 091 protected ThreadSafeComponentManager<Enumerator> _enumeratorManager; 092 093 /** The service manager */ 094 protected ServiceManager _manager; 095 096 /** the logger */ 097 protected Logger _logger; 098 099 /** The avalon context */ 100 protected Context _context; 101 102 private String _onInitClassName; 103 private String _onSubmitClassName; 104 private String _onChangeClassName; 105 private boolean _hidden; 106 private boolean _multiple; 107 private I18nizableText _group; 108 109 private String _contentTypeId; 110 111 @Override 112 public void contextualize(Context context) throws ContextException 113 { 114 _context = context; 115 } 116 117 @Override 118 public void service(ServiceManager manager) throws ServiceException 119 { 120 _manager = manager; 121 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 122 _cTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE); 123 _contentHelper = (ContentHelper) manager.lookup(ContentHelper.ROLE); 124 _userManager = (UserManager) manager.lookup(UserManager.ROLE); 125 _userPopulationDAO = (UserPopulationDAO) manager.lookup(UserPopulationDAO.ROLE); 126 } 127 128 @Override 129 public void configure(Configuration configuration) throws ConfigurationException 130 { 131 configureId(configuration); 132 133 configureLabelsAndGroup(configuration); 134 configureUIProperties(configuration); 135 configureValues(configuration); 136 137 _enumeratorManager = new ThreadSafeComponentManager<>(); 138 _enumeratorManager.setLogger(_logger); 139 _enumeratorManager.contextualize(_context); 140 _enumeratorManager.service(_manager); 141 } 142 143 public void dispose() 144 { 145 _enumeratorManager.dispose(); 146 _enumeratorManager = null; 147 } 148 149 @Override 150 public void setLogger(Logger logger) 151 { 152 _logger = logger; 153 } 154 155 /** 156 * Configure the criterion ID. 157 * @param configuration The search criterion configuration. 158 * @throws ConfigurationException If an error occurs. 159 */ 160 protected void configureId(Configuration configuration) throws ConfigurationException 161 { 162 setId(configuration.getAttribute("id", null)); 163 } 164 165 /** 166 * Configure the labels and group. 167 * @param configuration The search criterion configuration. 168 * @throws ConfigurationException If an error occurs. 169 */ 170 protected void configureLabelsAndGroup(Configuration configuration) throws ConfigurationException 171 { 172 setGroup(_configureI18nizableText(configuration.getChild("group", false), null)); 173 setLabel(_configureI18nizableText(configuration.getChild("label", false), new I18nizableText(""))); 174 setDescription(_configureI18nizableText(configuration.getChild("description", false), new I18nizableText(""))); 175 } 176 177 /** 178 * Configure the default value. 179 * @param configuration The search criterion configuration. 180 * @throws ConfigurationException If an error occurs. 181 */ 182 protected void configureValues(Configuration configuration) throws ConfigurationException 183 { 184 // The default value can be empty. 185 Configuration[] defaultValueConfs = configuration.getChildren("default-value"); 186 if (defaultValueConfs.length == 1) 187 { 188 setDefaultValue(ConfigurationHelper.parseObject(defaultValueConfs[0], "")); 189 } 190 else if (defaultValueConfs.length > 1) 191 { 192 // Make the default value a list 193 List<Object> collection = Arrays.asList(defaultValueConfs).stream() 194 .map(conf -> ConfigurationHelper.parseObject(conf, "")) 195 .collect(Collectors.toList()); 196 setDefaultValue(collection); 197 } 198 } 199 200 /** 201 * Configure the standard UI properties (hidden, init class, change class, submit class). 202 * @param configuration The search criterion configuration. 203 * @throws ConfigurationException If an error occurs. 204 */ 205 protected void configureUIProperties(Configuration configuration) throws ConfigurationException 206 { 207 setHidden(configuration.getAttributeAsBoolean("hidden", false)); 208 setInitClassName(configuration.getChild("oninit").getValue(null)); 209 setChangeClassName(configuration.getChild("onchange").getValue(null)); 210 setSubmitClassName(configuration.getChild("onsubmit").getValue(null)); 211 } 212 213 /** 214 * Configure enumerator of the criterion 215 * @param configuration The search criterion configuration. 216 * @param definition The definition of the criterion 217 * @return the enumerator 218 * @throws ConfigurationException if an error occurs 219 */ 220 protected org.ametys.runtime.parameter.Enumerator configureEnumerator(Configuration configuration, ElementDefinition definition) throws ConfigurationException 221 { 222 return configureEnumerator(configuration, definition, configuration); 223 } 224 225 /** 226 * Configure enumerator of the criterion 227 * @param configuration The search criterion configuration. 228 * @param definition The definition of the criterion 229 * @param defaultEnumeratorConfig The configuration for property's widget params 230 * @return the enumerator 231 * @throws ConfigurationException if an error occurs 232 */ 233 @SuppressWarnings("unchecked") 234 protected org.ametys.runtime.parameter.Enumerator configureEnumerator(Configuration configuration, ElementDefinition definition, Configuration defaultEnumeratorConfig) throws ConfigurationException 235 { 236 Enumerator enumerator = definition.getCriterionEnumerator(defaultEnumeratorConfig, _enumeratorManager); 237 org.ametys.runtime.parameter.Enumerator oldAPIEnumerator = Optional.ofNullable(enumerator) 238 .filter(org.ametys.runtime.parameter.Enumerator.class::isInstance) 239 .map(org.ametys.runtime.parameter.Enumerator.class::cast) 240 .orElse(null); 241 242 if (enumerator instanceof StaticEnumerator) 243 { 244 oldAPIEnumerator = new org.ametys.runtime.parameter.StaticEnumerator(); 245 246 try 247 { 248 for (Map.Entry<? extends Object, I18nizableText> entry : ((Map<Object, I18nizableText>) (Object) enumerator.getTypedEntries()).entrySet()) 249 { 250 ((org.ametys.runtime.parameter.StaticEnumerator) oldAPIEnumerator).add(entry.getValue(), entry.getKey().toString()); 251 } 252 } 253 catch (Exception e) 254 { 255 throw new ConfigurationException("Unable to get entries of the static enumerator of criterion '" + definition.getPath() + "'", configuration, e); 256 } 257 } 258 259 return oldAPIEnumerator; 260 } 261 262 /** 263 * Configure widget of the criterion 264 * @param configuration The search criterion configuration. 265 * @param definition The definition of the criterion 266 * @return the widget 267 */ 268 protected String configureWidget(Configuration configuration, ElementDefinition definition) 269 { 270 String defaultWidget = definition.getCriterionWidget(); 271 return configureWidget(configuration, defaultWidget, MetadataType.fromModelItemType(definition.getType())); 272 } 273 274 /** 275 * Configure widget of the criterion 276 * @param configuration The search criterion configuration. 277 * @param defaultWidget The default widget to use if not present in configuration. If {@link MetadataType#RICH_TEXT} or {@link MetadataType#MULTILINGUAL_STRING}, it will be forced. 278 * @param type the type which must be supported by the widget 279 * @return the widget 280 */ 281 protected String configureWidget(Configuration configuration, String defaultWidget, MetadataType type) 282 { 283 String realDefaultWidget = defaultWidget; 284 if ("edition.textarea".equals(realDefaultWidget)) 285 { 286 realDefaultWidget = null; 287 } 288 else if (type == MetadataType.RICH_TEXT || type == MetadataType.MULTILINGUAL_STRING) 289 { 290 realDefaultWidget = "edition.textfield"; // Force simple text field 291 } 292 else if (realDefaultWidget == null && type == MetadataType.BOOLEAN) 293 { 294 realDefaultWidget = "edition.boolean-combobox"; 295 } 296 297 return configuration.getChild("widget").getValue(realDefaultWidget); 298 } 299 300 /** 301 * Configure widget parameters of the criterion 302 * @param configuration The search criterion configuration. 303 * @param defaultParams The default widget parameters to override with configuration 304 * @param type the type which must be supported by the widget. If {@link MetadataType#CONTENT} or {@link MetadataType#SUB_CONTENT}, some parameters will be forced. 305 * @param contentTypeId For {@link MetadataType#CONTENT} or {@link MetadataType#SUB_CONTENT} types only. The id of the content type. 306 * @return the widget parameters 307 * @throws ConfigurationException If an error occurs. 308 */ 309 protected Map<String, I18nizableText> configureWidgetParameters(Configuration configuration, Map<String, I18nizableText> defaultParams, MetadataType type, String contentTypeId) throws ConfigurationException 310 { 311 Configuration widgetParamsConfig = configuration.getChild("widget-params"); 312 Map<String, I18nizableText> widgetParams = new HashMap<>(Optional.ofNullable(defaultParams).orElseGet(Collections::emptyMap)); 313 314 if (widgetParamsConfig != null) 315 { 316 // Overriden params 317 Configuration[] params = widgetParamsConfig.getChildren(); 318 for (Configuration paramConfig : params) 319 { 320 String name = "param".equals(paramConfig.getName()) ? paramConfig.getAttribute("name") : paramConfig.getName(); 321 widgetParams.put(name, I18nizableText.parseI18nizableText(paramConfig, null)); 322 } 323 } 324 325 // Force some param for types CONTENT and SUB_CONTENT 326 if (type == MetadataType.CONTENT || type == MetadataType.SUB_CONTENT) 327 { 328 setContentTypeId(contentTypeId); 329 if (contentTypeId != null) 330 { 331 widgetParams.put("contentType", new I18nizableText(contentTypeId)); 332 } 333 334 // Override the widget parameters to disable search and creation 335 widgetParams.put("allowCreation", new I18nizableText("false")); 336 widgetParams.put("allowSearch", new I18nizableText("false")); 337 } 338 339 return widgetParams; 340 } 341 342 /** 343 * Configure widget parameters of the criterion 344 * @param configuration The search criterion configuration. 345 * @param definition The definition of the criterion's element 346 * @return the widget parameters 347 * @throws ConfigurationException If an error occurs. 348 */ 349 protected Map<String, I18nizableText> configureWidgetParameters(Configuration configuration, ElementDefinition definition) throws ConfigurationException 350 { 351 return configureWidgetParameters(configuration, definition, configuration); 352 } 353 354 /** 355 * Configure widget parameters of the criterion 356 * @param configuration The search criterion configuration. 357 * @param definition The definition of the criterion's element 358 * @param defaultWidgetParamsConfig The configuration for property's widget params 359 * @return the widget parameters 360 * @throws ConfigurationException If an error occurs. 361 */ 362 protected Map<String, I18nizableText> configureWidgetParameters(Configuration configuration, ElementDefinition definition, Configuration defaultWidgetParamsConfig) throws ConfigurationException 363 { 364 Map<String, I18nizableText> widgetParams = new HashMap<>(); 365 Map<String, I18nizableText> criterionWidgetParameters = definition.getCriterionWidgetParameters(defaultWidgetParamsConfig); 366 if (criterionWidgetParameters != null) 367 { 368 widgetParams.putAll(criterionWidgetParameters); 369 } 370 371 Configuration widgetParamsConfig = configuration.getChild("widget-params"); 372 if (widgetParamsConfig != null) 373 { 374 // Overriden params 375 Configuration[] params = widgetParamsConfig.getChildren(); 376 for (Configuration paramConfig : params) 377 { 378 String name = "param".equals(paramConfig.getName()) ? paramConfig.getAttribute("name") : paramConfig.getName(); 379 widgetParams.put(name, I18nizableText.parseI18nizableText(paramConfig, null)); 380 } 381 } 382 383 // Force some params for type CONTENT 384 if (ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID.equals(definition.getType().getId())) 385 { 386 Optional<ContentElementDefinition> contentElementDefinition = Optional.empty(); 387 if (definition instanceof ContentElementDefinition contentElementDef) 388 { 389 contentElementDefinition = Optional.of(contentElementDef); 390 } 391 else if (definition instanceof ElementRefProperty elementRefProperty) 392 { 393 contentElementDefinition = Optional.of(elementRefProperty.getElementDefinition()) 394 .filter(ContentElementDefinition.class::isInstance) 395 .map(ContentElementDefinition.class::cast); 396 } 397 398 String contentTypeId = contentElementDefinition.map(ContentElementDefinition::getContentTypeId) 399 .filter(StringUtils::isNotEmpty) 400 .orElse(null); 401 if (contentTypeId != null) 402 { 403 setContentTypeId(contentTypeId); 404 widgetParams.put("contentType", new I18nizableText(contentTypeId)); 405 } 406 407 // Override the widget parameters to disable search and creation 408 widgetParams.put("allowCreation", new I18nizableText("false")); 409 widgetParams.put("allowSearch", new I18nizableText("false")); 410 } 411 412 return widgetParams; 413 } 414 415 /** 416 * Get the JS class name to execute on 'init' event 417 * @return the JS class name to execute on 'init' event 418 */ 419 public String getInitClassName() 420 { 421 return _onInitClassName; 422 } 423 424 /** 425 * Set the JS class name to execute on 'init' event 426 * @param className the JS class name 427 */ 428 public void setInitClassName(String className) 429 { 430 this._onInitClassName = className; 431 } 432 433 /** 434 * Get the JS class name to execute on 'submit' event 435 * @return the JS class name to execute on 'submit' event 436 */ 437 public String getSubmitClassName() 438 { 439 return _onSubmitClassName; 440 } 441 442 /** 443 * Set the JS class name to execute on 'submit' event 444 * @param className the JS class name 445 */ 446 public void setSubmitClassName(String className) 447 { 448 this._onSubmitClassName = className; 449 } 450 451 /** 452 * Get the JS class name to execute on 'change' event 453 * @return the JS class name to execute on 'change' event 454 */ 455 public String getChangeClassName() 456 { 457 return _onChangeClassName; 458 } 459 460 /** 461 * Set the JS class name to execute on 'change' event 462 * @param className the JS class name 463 */ 464 public void setChangeClassName(String className) 465 { 466 this._onChangeClassName = className; 467 } 468 469 /** 470 * Get the group of the search criteria 471 * @return <code>null</code> if the search criteria does not belong to any group, the name of the group otherwise 472 */ 473 public I18nizableText getGroup() 474 { 475 return _group; 476 } 477 478 /** 479 * Set the group of the search criteria 480 * @param group the group this search criteria will be added to 481 */ 482 public void setGroup(I18nizableText group) 483 { 484 _group = group; 485 } 486 487 /** 488 * Determines if the criteria is hidden 489 * @return <code>true</code> if the criteria is hidden 490 */ 491 public boolean isHidden() 492 { 493 return _hidden; 494 } 495 496 /** 497 * Set the hidden property of the criteria 498 * @param hidden true to hide the search criteria 499 */ 500 public void setHidden (boolean hidden) 501 { 502 this._hidden = hidden; 503 } 504 505 /** 506 * Set the multiple property 507 * @param multiple the multiple property 508 */ 509 public void setMultiple (boolean multiple) 510 { 511 this._multiple = multiple; 512 } 513 514 /** 515 * Determines if the column value is multiple 516 * @return <code>true</code> if the value is multiple 517 */ 518 @Override 519 public boolean isMultiple () 520 { 521 return this._multiple; 522 } 523 524 /** 525 * Get the content type ID (only when the search criteria is of type CONTENT). 526 * @return the content type ID. 527 */ 528 public String getContentTypeId() 529 { 530 return this._contentTypeId; 531 } 532 533 /** 534 * Set the content type ID (only when the search criteria is of type CONTENT). 535 * @param contentTypeId the content type ID. 536 */ 537 public void setContentTypeId(String contentTypeId) 538 { 539 this._contentTypeId = contentTypeId; 540 } 541 542 @Override 543 public I18nizableText getFacetLabel(String value, Locale currentLocale) 544 { 545 I18nizableText label = null; 546 547 try 548 { 549 MetadataType type = getType(); 550 org.ametys.runtime.parameter.Enumerator enumerator = getEnumerator(); 551 552 if (type == MetadataType.CONTENT || type == MetadataType.SUB_CONTENT) 553 { 554 Content content = _resolver.resolveById(value); 555 label = new I18nizableText(content.getTitle(currentLocale)); 556 } 557 else if (type == MetadataType.USER) 558 { 559 UserIdentity userIdentity = UserIdentity.stringToUserIdentity(value); 560 String login = userIdentity.getLogin(); 561 String populationId = userIdentity.getPopulationId(); 562 User user = _userManager.getUser(populationId, login); 563 if (user != null) 564 { 565 // Default i18n key only use login, sortablename and population. 566 // But we provide more parameters if the user want to override it. 567 Map<String, I18nizableTextParameter> i18nParams = new HashMap<>(); 568 i18nParams.put("login", new I18nizableText(login)); 569 i18nParams.put("firstname", new I18nizableText(user.getFirstName())); 570 i18nParams.put("lastname", new I18nizableText(user.getLastName())); 571 i18nParams.put("fullname", new I18nizableText(user.getFullName())); 572 i18nParams.put("sortablename", new I18nizableText(user.getSortableName())); 573 i18nParams.put("population", _userPopulationDAO.getUserPopulation(populationId).getLabel()); 574 575 label = new I18nizableText("plugin.cms", "PLUGINS_CMS_UITOOL_SEARCH_FACET_USER_LABEL", i18nParams); 576 } 577 else 578 { 579 Map<String, I18nizableTextParameter> i18nParams = new HashMap<>(); 580 i18nParams.put("login", new I18nizableText(login)); 581 i18nParams.put("population", new I18nizableText(populationId)); 582 583 label = new I18nizableText("plugin.cms", "PLUGINS_CMS_UITOOL_SEARCH_FACET_UNKNOWN_USER_LABEL", i18nParams); 584 } 585 } 586 else if (enumerator != null) 587 { 588 label = enumerator.getEntry(value); 589 } 590 else if (type == MetadataType.BOOLEAN) 591 { 592 boolean boolValue = "1".equals(value) /* if joined facet, value will be "0" or "1" */ || Boolean.valueOf(value); 593 label = new I18nizableText("plugin.cms", boolValue ? "PLUGINS_CMS_UITOOL_SEARCH_FACET_BOOLEAN_TRUE_LABEL" : "PLUGINS_CMS_UITOOL_SEARCH_FACET_BOOLEAN_FALSE_LABEL"); 594 } 595 } 596 catch (Exception e) 597 { 598 // Ignore, just return null. 599 __LOGGER.warn("Unable to get facel label for value '{}'", value, e); 600 } 601 602 return label; 603 } 604 605 @Override 606 public SearchField getSearchField() 607 { 608 // Override to provide a specific implementation. 609 return null; 610 } 611 612 /** 613 * Initialize the validator. 614 * @param validatorManager The validator manager. 615 * @param pluginName The plugin name. 616 * @param role The validator role. 617 * @param config The validator configuration. 618 * @return true if the validator was successfully added, false otherwise. 619 * @throws ConfigurationException If an error occurs. 620 */ 621 @SuppressWarnings("unchecked") 622 protected boolean _initializeValidator(ThreadSafeComponentManager<Validator> validatorManager, String pluginName, String role, Configuration config) throws ConfigurationException 623 { 624 Configuration validatorConfig = config.getChild("validation", false); 625 626 if (validatorConfig != null) 627 { 628 String validatorClassName = StringUtils.defaultIfBlank(validatorConfig.getChild("custom-validator").getAttribute("class", ""), DefaultValidator.class.getName()); 629 630 try 631 { 632 Class validatorClass = Class.forName(validatorClassName); 633 validatorManager.addComponent(pluginName, null, role, validatorClass, config); 634 return true; 635 } 636 catch (Exception e) 637 { 638 throw new ConfigurationException("Unable to instantiate validator for class: " + validatorClassName, e); 639 } 640 } 641 642 return false; 643 } 644 645 /** 646 * Configure an i18nizable text 647 * @param config The Configuration. 648 * @param defaultValue The default value as an I18nizableText. 649 * @return The i18nizable text 650 */ 651 protected I18nizableText _configureI18nizableText(Configuration config, I18nizableText defaultValue) 652 { 653 if (config != null) 654 { 655 return I18nizableText.parseI18nizableText(config, null, ""); 656 } 657 else 658 { 659 return defaultValue; 660 } 661 } 662}