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.Map; 023import java.util.Optional; 024import java.util.stream.Collectors; 025 026import org.apache.avalon.framework.activity.Disposable; 027import org.apache.avalon.framework.configuration.Configurable; 028import org.apache.avalon.framework.configuration.Configuration; 029import org.apache.avalon.framework.configuration.ConfigurationException; 030import org.apache.avalon.framework.context.Context; 031import org.apache.avalon.framework.context.ContextException; 032import org.apache.avalon.framework.context.Contextualizable; 033import org.apache.avalon.framework.service.ServiceException; 034import org.apache.avalon.framework.service.ServiceManager; 035import org.apache.avalon.framework.service.Serviceable; 036import org.apache.commons.lang3.StringUtils; 037import org.slf4j.Logger; 038 039import org.ametys.cms.content.ContentHelper; 040import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 041import org.ametys.cms.contenttype.MetadataType; 042import org.ametys.cms.data.type.ModelItemTypeConstants; 043import org.ametys.cms.model.ContentElementDefinition; 044import org.ametys.cms.model.properties.ElementRefProperty; 045import org.ametys.cms.search.SearchField; 046import org.ametys.cms.search.ui.model.SearchUICriterion; 047import org.ametys.cms.search.ui.model.SearchUIModel; 048import org.ametys.plugins.core.ui.util.ConfigurationHelper; 049import org.ametys.runtime.i18n.I18nizableText; 050import org.ametys.runtime.model.ElementDefinition; 051import org.ametys.runtime.model.Enumerator; 052import org.ametys.runtime.model.StaticEnumerator; 053import org.ametys.runtime.parameter.DefaultValidator; 054import org.ametys.runtime.parameter.Parameter; 055import org.ametys.runtime.parameter.Validator; 056import org.ametys.runtime.plugin.component.LogEnabled; 057import org.ametys.runtime.plugin.component.ThreadSafeComponentManager; 058 059/** 060 * This class represents a search criterion of a {@link SearchUIModel}. 061 */ 062public abstract class AbstractSearchUICriterion extends Parameter<MetadataType> implements SearchUICriterion, Contextualizable, Serviceable, Configurable, Disposable, LogEnabled 063{ 064 /** The content type extension point. */ 065 protected ContentTypeExtensionPoint _contentTypeExtensionPoint; 066 067 /** The content type extension point. */ 068 protected ContentHelper _contentHelper; 069 070 /** ComponentManager for {@link Enumerator}s. */ 071 protected ThreadSafeComponentManager<Enumerator> _enumeratorManager; 072 073 /** The service manager */ 074 protected ServiceManager _manager; 075 076 /** the logger */ 077 protected Logger _logger; 078 079 /** The avalon context */ 080 protected Context _context; 081 082 private String _onInitClassName; 083 private String _onSubmitClassName; 084 private String _onChangeClassName; 085 private boolean _hidden; 086 private boolean _multiple; 087 private I18nizableText _group; 088 089 private String _contentTypeId; 090 091 @Override 092 public void contextualize(Context context) throws ContextException 093 { 094 _context = context; 095 } 096 097 @Override 098 public void service(ServiceManager manager) throws ServiceException 099 { 100 _manager = manager; 101 _contentTypeExtensionPoint = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE); 102 _contentHelper = (ContentHelper) manager.lookup(ContentHelper.ROLE); 103 } 104 105 @Override 106 public void configure(Configuration configuration) throws ConfigurationException 107 { 108 configureId(configuration); 109 110 configureLabelsAndGroup(configuration); 111 configureUIProperties(configuration); 112 configureValues(configuration); 113 114 _enumeratorManager = new ThreadSafeComponentManager<>(); 115 _enumeratorManager.setLogger(_logger); 116 _enumeratorManager.contextualize(_context); 117 _enumeratorManager.service(_manager); 118 } 119 120 public void dispose() 121 { 122 _enumeratorManager.dispose(); 123 _enumeratorManager = null; 124 } 125 126 @Override 127 public void setLogger(Logger logger) 128 { 129 _logger = logger; 130 } 131 132 /** 133 * Configure the criterion ID. 134 * @param configuration The search criterion configuration. 135 * @throws ConfigurationException If an error occurs. 136 */ 137 protected void configureId(Configuration configuration) throws ConfigurationException 138 { 139 setId(configuration.getAttribute("id", null)); 140 } 141 142 /** 143 * Configure the labels and group. 144 * @param configuration The search criterion configuration. 145 * @throws ConfigurationException If an error occurs. 146 */ 147 protected void configureLabelsAndGroup(Configuration configuration) throws ConfigurationException 148 { 149 setGroup(_configureI18nizableText(configuration.getChild("group", false), null)); 150 setLabel(_configureI18nizableText(configuration.getChild("label", false), new I18nizableText(""))); 151 setDescription(_configureI18nizableText(configuration.getChild("description", false), new I18nizableText(""))); 152 } 153 154 /** 155 * Configure the default value. 156 * @param configuration The search criterion configuration. 157 * @throws ConfigurationException If an error occurs. 158 */ 159 protected void configureValues(Configuration configuration) throws ConfigurationException 160 { 161 // The default value can be empty. 162 Configuration[] defaultValueConfs = configuration.getChildren("default-value"); 163 if (defaultValueConfs.length == 1) 164 { 165 setDefaultValue(ConfigurationHelper.parseObject(defaultValueConfs[0], "")); 166 } 167 else if (defaultValueConfs.length > 1) 168 { 169 // Make the default value a list 170 List<Object> collection = Arrays.asList(defaultValueConfs).stream() 171 .map(conf -> ConfigurationHelper.parseObject(conf, "")) 172 .collect(Collectors.toList()); 173 setDefaultValue(collection); 174 } 175 } 176 177 /** 178 * Configure the standard UI properties (hidden, init class, change class, submit class). 179 * @param configuration The search criterion configuration. 180 * @throws ConfigurationException If an error occurs. 181 */ 182 protected void configureUIProperties(Configuration configuration) throws ConfigurationException 183 { 184 setHidden(configuration.getAttributeAsBoolean("hidden", false)); 185 setInitClassName(configuration.getChild("oninit").getValue(null)); 186 setChangeClassName(configuration.getChild("onchange").getValue(null)); 187 setSubmitClassName(configuration.getChild("onsubmit").getValue(null)); 188 } 189 190 /** 191 * Configure enumerator of the criterion 192 * @param configuration The search criterion configuration. 193 * @param definition The definition of the criterion 194 * @return the enumerator 195 * @throws ConfigurationException if an error occurs 196 */ 197 protected org.ametys.runtime.parameter.Enumerator configureEnumerator(Configuration configuration, ElementDefinition definition) throws ConfigurationException 198 { 199 return configureEnumerator(configuration, definition, configuration); 200 } 201 202 /** 203 * Configure enumerator of the criterion 204 * @param configuration The search criterion configuration. 205 * @param definition The definition of the criterion 206 * @param defaultEnumeratorConfig The configuration for property's widget params 207 * @return the enumerator 208 * @throws ConfigurationException if an error occurs 209 */ 210 @SuppressWarnings("unchecked") 211 protected org.ametys.runtime.parameter.Enumerator configureEnumerator(Configuration configuration, ElementDefinition definition, Configuration defaultEnumeratorConfig) throws ConfigurationException 212 { 213 Enumerator enumerator = definition.getCriterionEnumerator(defaultEnumeratorConfig, _enumeratorManager); 214 org.ametys.runtime.parameter.Enumerator oldAPIEnumerator = Optional.ofNullable(enumerator) 215 .filter(org.ametys.runtime.parameter.Enumerator.class::isInstance) 216 .map(org.ametys.runtime.parameter.Enumerator.class::cast) 217 .orElse(null); 218 219 if (enumerator instanceof StaticEnumerator) 220 { 221 oldAPIEnumerator = new org.ametys.runtime.parameter.StaticEnumerator(); 222 223 try 224 { 225 for (Map.Entry<? extends Object, I18nizableText> entry : ((Map<Object, I18nizableText>) (Object) enumerator.getTypedEntries()).entrySet()) 226 { 227 ((org.ametys.runtime.parameter.StaticEnumerator) oldAPIEnumerator).add(entry.getValue(), entry.getKey().toString()); 228 } 229 } 230 catch (Exception e) 231 { 232 throw new ConfigurationException("Unable to get entries of the static enumerator of criterion '" + definition.getPath() + "'", configuration, e); 233 } 234 } 235 236 return oldAPIEnumerator; 237 } 238 239 /** 240 * Configure widget of the criterion 241 * @param configuration The search criterion configuration. 242 * @param definition The definition of the criterion 243 * @return the widget 244 */ 245 protected String configureWidget(Configuration configuration, ElementDefinition definition) 246 { 247 String defaultWidget = definition.getCriterionWidget(); 248 return configureWidget(configuration, defaultWidget, MetadataType.fromModelItemType(definition.getType())); 249 } 250 251 /** 252 * Configure widget of the criterion 253 * @param configuration The search criterion configuration. 254 * @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. 255 * @param type the type which must be supported by the widget 256 * @return the widget 257 */ 258 protected String configureWidget(Configuration configuration, String defaultWidget, MetadataType type) 259 { 260 String realDefaultWidget = defaultWidget; 261 if ("edition.textarea".equals(realDefaultWidget)) 262 { 263 realDefaultWidget = null; 264 } 265 else if (type == MetadataType.RICH_TEXT || type == MetadataType.MULTILINGUAL_STRING) 266 { 267 realDefaultWidget = "edition.textfield"; // Force simple text field 268 } 269 else if (realDefaultWidget == null && type == MetadataType.BOOLEAN) 270 { 271 realDefaultWidget = "edition.boolean-combobox"; 272 } 273 274 return configuration.getChild("widget").getValue(realDefaultWidget); 275 } 276 277 /** 278 * Configure widget parameters of the criterion 279 * @param configuration The search criterion configuration. 280 * @param defaultParams The default widget parameters to override with configuration 281 * @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. 282 * @param contentTypeId For {@link MetadataType#CONTENT} or {@link MetadataType#SUB_CONTENT} types only. The id of the content type. 283 * @return the widget parameters 284 * @throws ConfigurationException If an error occurs. 285 */ 286 protected Map<String, I18nizableText> configureWidgetParameters(Configuration configuration, Map<String, I18nizableText> defaultParams, MetadataType type, String contentTypeId) throws ConfigurationException 287 { 288 Configuration widgetParamsConfig = configuration.getChild("widget-params"); 289 Map<String, I18nizableText> widgetParams = new HashMap<>(Optional.ofNullable(defaultParams).orElseGet(Collections::emptyMap)); 290 291 if (widgetParamsConfig != null) 292 { 293 // Overriden params 294 Configuration[] params = widgetParamsConfig.getChildren(); 295 for (Configuration paramConfig : params) 296 { 297 String name = "param".equals(paramConfig.getName()) ? paramConfig.getAttribute("name") : paramConfig.getName(); 298 widgetParams.put(name, I18nizableText.parseI18nizableText(paramConfig, null)); 299 } 300 } 301 302 // Force some param for types CONTENT and SUB_CONTENT 303 if (type == MetadataType.CONTENT || type == MetadataType.SUB_CONTENT) 304 { 305 setContentTypeId(contentTypeId); 306 if (contentTypeId != null) 307 { 308 widgetParams.put("contentType", new I18nizableText(contentTypeId)); 309 } 310 311 // Override the widget parameters to disable search and creation 312 widgetParams.put("allowCreation", new I18nizableText("false")); 313 widgetParams.put("allowSearch", new I18nizableText("false")); 314 } 315 316 return widgetParams; 317 } 318 319 /** 320 * Configure widget parameters of the criterion 321 * @param configuration The search criterion configuration. 322 * @param definition The definition of the criterion's element 323 * @return the widget parameters 324 * @throws ConfigurationException If an error occurs. 325 */ 326 protected Map<String, I18nizableText> configureWidgetParameters(Configuration configuration, ElementDefinition definition) throws ConfigurationException 327 { 328 return configureWidgetParameters(configuration, definition, configuration); 329 } 330 331 /** 332 * Configure widget parameters of the criterion 333 * @param configuration The search criterion configuration. 334 * @param definition The definition of the criterion's element 335 * @param defaultWidgetParamsConfig The configuration for property's widget params 336 * @return the widget parameters 337 * @throws ConfigurationException If an error occurs. 338 */ 339 protected Map<String, I18nizableText> configureWidgetParameters(Configuration configuration, ElementDefinition definition, Configuration defaultWidgetParamsConfig) throws ConfigurationException 340 { 341 Map<String, I18nizableText> widgetParams = new HashMap<>(); 342 Map<String, I18nizableText> criterionWidgetParameters = definition.getCriterionWidgetParameters(defaultWidgetParamsConfig); 343 if (criterionWidgetParameters != null) 344 { 345 widgetParams.putAll(criterionWidgetParameters); 346 } 347 348 // Force some params for type CONTENT 349 if (ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID.equals(definition.getType().getId())) 350 { 351 Optional<ContentElementDefinition> contentElementDefinition = Optional.empty(); 352 if (definition instanceof ContentElementDefinition contentElementDef) 353 { 354 contentElementDefinition = Optional.of(contentElementDef); 355 } 356 else if (definition instanceof ElementRefProperty elementRefProperty) 357 { 358 contentElementDefinition = Optional.of(elementRefProperty.getElementDefinition()) 359 .filter(ContentElementDefinition.class::isInstance) 360 .map(ContentElementDefinition.class::cast); 361 } 362 363 String contentTypeId = contentElementDefinition.map(ContentElementDefinition::getContentTypeId) 364 .filter(StringUtils::isNotEmpty) 365 .orElse(null); 366 if (contentTypeId != null) 367 { 368 setContentTypeId(contentTypeId); 369 widgetParams.put("contentType", new I18nizableText(contentTypeId)); 370 } 371 372 // Override the widget parameters to disable search and creation 373 widgetParams.put("allowCreation", new I18nizableText("false")); 374 widgetParams.put("allowSearch", new I18nizableText("false")); 375 } 376 377 Configuration widgetParamsConfig = configuration.getChild("widget-params"); 378 if (widgetParamsConfig != null) 379 { 380 // Overridden params 381 Configuration[] params = widgetParamsConfig.getChildren(); 382 for (Configuration paramConfig : params) 383 { 384 String name = "param".equals(paramConfig.getName()) ? paramConfig.getAttribute("name") : paramConfig.getName(); 385 widgetParams.put(name, I18nizableText.parseI18nizableText(paramConfig, null)); 386 } 387 } 388 389 return widgetParams; 390 } 391 392 /** 393 * Get the JS class name to execute on 'init' event 394 * @return the JS class name to execute on 'init' event 395 */ 396 public String getInitClassName() 397 { 398 return _onInitClassName; 399 } 400 401 /** 402 * Set the JS class name to execute on 'init' event 403 * @param className the JS class name 404 */ 405 public void setInitClassName(String className) 406 { 407 this._onInitClassName = className; 408 } 409 410 /** 411 * Get the JS class name to execute on 'submit' event 412 * @return the JS class name to execute on 'submit' event 413 */ 414 public String getSubmitClassName() 415 { 416 return _onSubmitClassName; 417 } 418 419 /** 420 * Set the JS class name to execute on 'submit' event 421 * @param className the JS class name 422 */ 423 public void setSubmitClassName(String className) 424 { 425 this._onSubmitClassName = className; 426 } 427 428 /** 429 * Get the JS class name to execute on 'change' event 430 * @return the JS class name to execute on 'change' event 431 */ 432 public String getChangeClassName() 433 { 434 return _onChangeClassName; 435 } 436 437 /** 438 * Set the JS class name to execute on 'change' event 439 * @param className the JS class name 440 */ 441 public void setChangeClassName(String className) 442 { 443 this._onChangeClassName = className; 444 } 445 446 /** 447 * Get the group of the search criteria 448 * @return <code>null</code> if the search criteria does not belong to any group, the name of the group otherwise 449 */ 450 public I18nizableText getGroup() 451 { 452 return _group; 453 } 454 455 /** 456 * Set the group of the search criteria 457 * @param group the group this search criteria will be added to 458 */ 459 public void setGroup(I18nizableText group) 460 { 461 _group = group; 462 } 463 464 /** 465 * Determines if the criteria is hidden 466 * @return <code>true</code> if the criteria is hidden 467 */ 468 public boolean isHidden() 469 { 470 return _hidden; 471 } 472 473 /** 474 * Set the hidden property of the criteria 475 * @param hidden true to hide the search criteria 476 */ 477 public void setHidden (boolean hidden) 478 { 479 this._hidden = hidden; 480 } 481 482 /** 483 * Set the multiple property 484 * @param multiple the multiple property 485 */ 486 public void setMultiple (boolean multiple) 487 { 488 this._multiple = multiple; 489 } 490 491 /** 492 * Determines if the column value is multiple 493 * @return <code>true</code> if the value is multiple 494 */ 495 @Override 496 public boolean isMultiple () 497 { 498 return this._multiple; 499 } 500 501 /** 502 * Get the content type ID (only when the search criteria is of type CONTENT). 503 * @return the content type ID. 504 */ 505 public String getContentTypeId() 506 { 507 return this._contentTypeId; 508 } 509 510 /** 511 * Set the content type ID (only when the search criteria is of type CONTENT). 512 * @param contentTypeId the content type ID. 513 */ 514 public void setContentTypeId(String contentTypeId) 515 { 516 this._contentTypeId = contentTypeId; 517 } 518 519 @Override 520 public SearchField getSearchField() 521 { 522 // Override to provide a specific implementation. 523 return null; 524 } 525 526 /** 527 * Initialize the validator. 528 * @param validatorManager The validator manager. 529 * @param pluginName The plugin name. 530 * @param role The validator role. 531 * @param config The validator configuration. 532 * @return true if the validator was successfully added, false otherwise. 533 * @throws ConfigurationException If an error occurs. 534 */ 535 @SuppressWarnings("unchecked") 536 protected boolean _initializeValidator(ThreadSafeComponentManager<Validator> validatorManager, String pluginName, String role, Configuration config) throws ConfigurationException 537 { 538 Configuration validatorConfig = config.getChild("validation", false); 539 540 if (validatorConfig != null) 541 { 542 String validatorClassName = StringUtils.defaultIfBlank(validatorConfig.getChild("custom-validator").getAttribute("class", ""), DefaultValidator.class.getName()); 543 544 try 545 { 546 Class validatorClass = Class.forName(validatorClassName); 547 validatorManager.addComponent(pluginName, null, role, validatorClass, config); 548 return true; 549 } 550 catch (Exception e) 551 { 552 throw new ConfigurationException("Unable to instantiate validator for class: " + validatorClassName, e); 553 } 554 } 555 556 return false; 557 } 558 559 /** 560 * Configure an i18nizable text 561 * @param config The Configuration. 562 * @param defaultValue The default value as an I18nizableText. 563 * @return The i18nizable text 564 */ 565 protected I18nizableText _configureI18nizableText(Configuration config, I18nizableText defaultValue) 566 { 567 if (config != null) 568 { 569 return I18nizableText.parseI18nizableText(config, null, ""); 570 } 571 else 572 { 573 return defaultValue; 574 } 575 } 576}