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