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