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