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 227 // Override the widget parameters to disable search and creation 228 widgetParams.put("allowCreation", new I18nizableText("false")); 229 widgetParams.put("allowSearch", new I18nizableText("false")); 230 } 231 232 return widgetParams; 233 } 234 235 /** 236 * Get the JS class name to execute on 'init' event 237 * @return the JS class name to execute on 'init' event 238 */ 239 public String getInitClassName() 240 { 241 return _onInitClassName; 242 } 243 244 /** 245 * Set the JS class name to execute on 'init' event 246 * @param className the JS class name 247 */ 248 public void setInitClassName(String className) 249 { 250 this._onInitClassName = className; 251 } 252 253 /** 254 * Get the JS class name to execute on 'submit' event 255 * @return the JS class name to execute on 'submit' event 256 */ 257 public String getSubmitClassName() 258 { 259 return _onSubmitClassName; 260 } 261 262 /** 263 * Set the JS class name to execute on 'submit' event 264 * @param className the JS class name 265 */ 266 public void setSubmitClassName(String className) 267 { 268 this._onSubmitClassName = className; 269 } 270 271 /** 272 * Get the JS class name to execute on 'change' event 273 * @return the JS class name to execute on 'change' event 274 */ 275 public String getChangeClassName() 276 { 277 return _onChangeClassName; 278 } 279 280 /** 281 * Set the JS class name to execute on 'change' event 282 * @param className the JS class name 283 */ 284 public void setChangeClassName(String className) 285 { 286 this._onChangeClassName = className; 287 } 288 289 /** 290 * Get the group of the search criteria 291 * @return <code>null</code> if the search criteria does not belong to any group, the name of the group otherwise 292 */ 293 public I18nizableText getGroup() 294 { 295 return _group; 296 } 297 298 /** 299 * Set the group of the search criteria 300 * @param group the group this search criteria will be added to 301 */ 302 public void setGroup(I18nizableText group) 303 { 304 _group = group; 305 } 306 307 /** 308 * Determines if the criteria is hidden 309 * @return <code>true</code> if the criteria is hidden 310 */ 311 public boolean isHidden() 312 { 313 return _hidden; 314 } 315 316 /** 317 * Set the hidden property of the criteria 318 * @param hidden true to hide the search criteria 319 */ 320 public void setHidden (boolean hidden) 321 { 322 this._hidden = hidden; 323 } 324 325 /** 326 * Set the multiple property 327 * @param multiple the multiple property 328 */ 329 public void setMultiple (boolean multiple) 330 { 331 this._multiple = multiple; 332 } 333 334 /** 335 * Determines if the column value is multiple 336 * @return <code>true</code> if the value is multiple 337 */ 338 @Override 339 public boolean isMultiple () 340 { 341 return this._multiple; 342 } 343 344 /** 345 * Get the content type ID (only when the search criteria is of type CONTENT). 346 * @return the content type ID. 347 */ 348 public String getContentTypeId() 349 { 350 return this._contentTypeId; 351 } 352 353 /** 354 * Set the content type ID (only when the search criteria is of type CONTENT). 355 * @param contentTypeId the content type ID. 356 */ 357 public void setContentTypeId(String contentTypeId) 358 { 359 this._contentTypeId = contentTypeId; 360 } 361 362 @Override 363 public I18nizableText getFacetLabel(String value, Locale currentLocale) 364 { 365 I18nizableText label = null; 366 367 try 368 { 369 MetadataType type = getType(); 370 Enumerator enumerator = getEnumerator(); 371 372 if (type == MetadataType.CONTENT || type == MetadataType.SUB_CONTENT) 373 { 374 Content content = _resolver.resolveById(value); 375 label = new I18nizableText(content.getTitle(currentLocale)); 376 } 377 else if (type == MetadataType.USER) 378 { 379 UserIdentity userIdentity = UserIdentity.stringToUserIdentity(value); 380 String login = userIdentity.getLogin(); 381 String populationId = userIdentity.getPopulationId(); 382 User user = _userManager.getUser(populationId, login); 383 if (user != null) 384 { 385 Map<String, I18nizableText> i18nParams = new HashMap<>(); 386 i18nParams.put("login", new I18nizableText(login)); 387 i18nParams.put("fullname", new I18nizableText(user.getFullName())); 388 i18nParams.put("population", _userPopulationDAO.getUserPopulation(populationId).getLabel()); 389 390 label = new I18nizableText("plugin.cms", "PLUGINS_CMS_UITOOL_SEARCH_FACET_USER_LABEL", i18nParams); 391 } 392 } 393 else if (enumerator != null) 394 { 395 label = enumerator.getEntry(value); 396 } 397 else if (type == MetadataType.BOOLEAN) 398 { 399 boolean boolValue = "1".equals(value) /* if joined facet, value will be "0" or "1" */ || Boolean.valueOf(value); 400 label = new I18nizableText("plugin.cms", boolValue ? "PLUGINS_CMS_UITOOL_SEARCH_FACET_BOOLEAN_TRUE_LABEL" : "PLUGINS_CMS_UITOOL_SEARCH_FACET_BOOLEAN_FALSE_LABEL"); 401 } 402 } 403 catch (Exception e) 404 { 405 // Ignore, just return null. 406 } 407 408 return label; 409 } 410 411 @Override 412 public SearchField getSearchField() 413 { 414 // Override to provide a specific implementation. 415 return null; 416 } 417 418 /** 419 * Initialize the validator. 420 * @param validatorManager The validator manager. 421 * @param pluginName The plugin name. 422 * @param role The validator role. 423 * @param config The validator configuration. 424 * @return true if the validator was successfully added, false otherwise. 425 * @throws ConfigurationException If an error occurs. 426 */ 427 @SuppressWarnings("unchecked") 428 protected boolean _initializeValidator(ThreadSafeComponentManager<Validator> validatorManager, String pluginName, String role, Configuration config) throws ConfigurationException 429 { 430 Configuration validatorConfig = config.getChild("validation", false); 431 432 if (validatorConfig != null) 433 { 434 String validatorClassName = StringUtils.defaultIfBlank(validatorConfig.getChild("custom-validator").getAttribute("class", ""), DefaultValidator.class.getName()); 435 436 try 437 { 438 Class validatorClass = Class.forName(validatorClassName); 439 validatorManager.addComponent(pluginName, null, role, validatorClass, config); 440 return true; 441 } 442 catch (Exception e) 443 { 444 throw new ConfigurationException("Unable to instantiate validator for class: " + validatorClassName, e); 445 } 446 } 447 448 return false; 449 } 450 451 /** 452 * Configure an i18nizable text 453 * @param config The Configuration. 454 * @param defaultValue The default value as an I18nizableText. 455 * @return The i18nizable text 456 */ 457 protected I18nizableText _configureI18nizableText(Configuration config, I18nizableText defaultValue) 458 { 459 if (config != null) 460 { 461 return I18nizableText.parseI18nizableText(config, null, ""); 462 } 463 else 464 { 465 return defaultValue; 466 } 467 } 468 469 /** 470 * 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. 471 * @param indexingField The initial indexing field 472 * @param remainingPathSegments The path to access the metadata or an another indexing field from the initial indexing field 473 * @param joinPaths The consecutive's path in case of joint to access the field/metadata 474 * @param addLast <code>true</code> to add the last join path element to the list, <code>false</code> otherwise. 475 * @return The metadata definition or null if not found 476 * @throws ConfigurationException If an error occurs. 477 */ 478 protected MetadataDefinition getMetadataDefinition(MetadataIndexingField indexingField, String[] remainingPathSegments, List<String> joinPaths, boolean addLast) throws ConfigurationException 479 { 480 StringBuilder currentMetaPath = new StringBuilder(); 481 currentMetaPath.append(indexingField.getName()); 482 483 MetadataDefinition definition = indexingField.getMetadataDefinition(); 484 485 for (int i = 0; i < remainingPathSegments.length && definition != null; i++) 486 { 487 if (definition.getType() == MetadataType.CONTENT || definition.getType() == MetadataType.SUB_CONTENT) 488 { 489 // Add path to content from current content type to join paths. 490 // Join paths are the consecutive metadata paths (separated with '/') to access 491 // the searched content, for instance [address/city, links/department]. 492 joinPaths.add(currentMetaPath.toString()); 493 494 String refCTypeId = definition.getContentType(); 495 if (refCTypeId != null) 496 { 497 if (!_cTypeEP.hasExtension(refCTypeId)) 498 { 499 throw new ConfigurationException("Search criterion with path '" + StringUtils.join(remainingPathSegments, ContentConstants.METADATA_PATH_SEPARATOR) + "' references an unknown content type:" + refCTypeId); 500 } 501 502 ContentType refCType = _cTypeEP.getExtension(refCTypeId); 503 IndexingModel refIndexingModel = refCType.getIndexingModel(); 504 505 IndexingField refIndexingField = refIndexingModel.getField(remainingPathSegments[i]); 506 if (refIndexingField == null) 507 { 508 throw new ConfigurationException("Search criterion with path '" + StringUtils.join(remainingPathSegments, ContentConstants.METADATA_PATH_SEPARATOR) + "' refers to an unknown indexing field: " + remainingPathSegments[i]); 509 } 510 if (!(refIndexingField instanceof MetadataIndexingField)) 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 515 return getMetadataDefinition((MetadataIndexingField) refIndexingField, ArrayUtils.subarray(remainingPathSegments, i + 1, remainingPathSegments.length), joinPaths, addLast); 516 } 517 else if ("title".equals(remainingPathSegments[i])) 518 { 519 // No specific content type: allow only title. 520 return ContentTypesHelper.getTitleMetadataDefinition(); 521 } 522 } 523 else 524 { 525 if (definition instanceof RepeaterDefinition) 526 { 527 // Add path to repeater from current content type or last repeater to join paths 528 joinPaths.add(currentMetaPath.toString()); 529 currentMetaPath = new StringBuilder(); 530 currentMetaPath.append(remainingPathSegments[i]); 531 } 532 else 533 { 534 currentMetaPath.append(ContentConstants.METADATA_PATH_SEPARATOR).append(remainingPathSegments[i]); 535 } 536 definition = definition.getMetadataDefinition(remainingPathSegments[i]); 537 } 538 } 539 540 if (addLast) 541 { 542 joinPaths.add(currentMetaPath.toString()); 543 } 544 545 return definition; 546 } 547 548}