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