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