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.ArrayList; 019import java.util.HashMap; 020import java.util.HashSet; 021import java.util.List; 022import java.util.Map; 023import java.util.Set; 024 025import org.apache.avalon.framework.activity.Disposable; 026import org.apache.avalon.framework.configuration.Configuration; 027import org.apache.avalon.framework.configuration.ConfigurationException; 028import org.apache.avalon.framework.context.Context; 029import org.apache.avalon.framework.context.ContextException; 030import org.apache.avalon.framework.context.Contextualizable; 031import org.apache.avalon.framework.service.ServiceException; 032import org.apache.avalon.framework.service.ServiceManager; 033import org.apache.commons.lang3.ArrayUtils; 034import org.apache.commons.lang3.StringUtils; 035import org.slf4j.Logger; 036 037import org.ametys.cms.contenttype.ContentConstants; 038import org.ametys.cms.contenttype.ContentType; 039import org.ametys.cms.contenttype.MetadataDefinition; 040import org.ametys.cms.contenttype.MetadataType; 041import org.ametys.cms.contenttype.indexing.CustomIndexingField; 042import org.ametys.cms.contenttype.indexing.IndexingField; 043import org.ametys.cms.contenttype.indexing.IndexingModel; 044import org.ametys.cms.contenttype.indexing.MetadataIndexingField; 045import org.ametys.cms.search.SearchField; 046import org.ametys.cms.search.model.SystemProperty; 047import org.ametys.cms.search.model.SystemProperty.EnumeratorDefinition; 048import org.ametys.cms.search.model.SystemPropertyExtensionPoint; 049import org.ametys.cms.search.model.SystemSearchCriterion; 050import org.ametys.cms.search.query.JoinQuery; 051import org.ametys.cms.search.query.Query; 052import org.ametys.cms.search.query.Query.Operator; 053import org.ametys.runtime.i18n.I18nizableText; 054import org.ametys.runtime.parameter.Enumerator; 055import org.ametys.runtime.parameter.StaticEnumerator; 056import org.ametys.runtime.parameter.Validator; 057import org.ametys.runtime.plugin.component.LogEnabled; 058import org.ametys.runtime.plugin.component.ThreadSafeComponentManager; 059 060/** 061 * This class is a search criteria on a system property (author, lastModified, with-comments, ...) 062 */ 063public class SystemSearchUICriterion extends AbstractSearchUICriterion implements SystemSearchCriterion, Contextualizable, LogEnabled, Disposable 064{ 065 066 /** Prefix for id of system property search criteria */ 067 public static final String SEARCH_CRITERION_SYSTEM_PREFIX = "property-"; 068 069 /** ComponentManager for {@link Validator}s. */ 070 protected ThreadSafeComponentManager<Validator> _validatorManager; 071 /** ComponentManager for {@link Enumerator}s. */ 072 protected ThreadSafeComponentManager<Enumerator> _enumeratorManager; 073 074 /** The system property extension point. */ 075 protected SystemPropertyExtensionPoint _systemPropEP; 076 077 /** The join paths */ 078 protected List<String> _joinPaths; 079 080 private Operator _operator; 081 private SystemProperty _systemProperty; 082 private Set<String> _contentTypes; 083 private String _fullPath; 084 085 private ServiceManager _manager; 086 private Logger _logger; 087 private Context _context; 088 089 @Override 090 public void contextualize(Context context) throws ContextException 091 { 092 _context = context; 093 } 094 095 @Override 096 public void setLogger(Logger logger) 097 { 098 _logger = logger; 099 } 100 101 @Override 102 public void service(ServiceManager manager) throws ServiceException 103 { 104 super.service(manager); 105 _manager = manager; 106 _systemPropEP = (SystemPropertyExtensionPoint) manager.lookup(SystemPropertyExtensionPoint.ROLE); 107 } 108 109 @Override 110 public void dispose() 111 { 112 _validatorManager.dispose(); 113 _validatorManager = null; 114 _enumeratorManager.dispose(); 115 _enumeratorManager = null; 116 } 117 118 @Override 119 public void configure(Configuration configuration) throws ConfigurationException 120 { 121 try 122 { 123 _validatorManager = new ThreadSafeComponentManager<>(); 124 _validatorManager.setLogger(_logger); 125 _validatorManager.contextualize(_context); 126 _validatorManager.service(_manager); 127 128 _enumeratorManager = new ThreadSafeComponentManager<>(); 129 _enumeratorManager.setLogger(_logger); 130 _enumeratorManager.contextualize(_context); 131 _enumeratorManager.service(_manager); 132 133 _fullPath = configuration.getChild("systemProperty").getAttribute("name"); 134 int pos = _fullPath.lastIndexOf(ContentConstants.METADATA_PATH_SEPARATOR); 135 136 String systemPropertyId = pos > -1 ? _fullPath.substring(pos + ContentConstants.METADATA_PATH_SEPARATOR.length()) : _fullPath; 137 _operator = Operator.fromName(configuration.getChild("test-operator").getValue("eq")); 138 139 if (!_systemPropEP.isSearchable(systemPropertyId)) 140 { 141 throw new ConfigurationException("The property '" + systemPropertyId + "' doesn't exist or is not searchable."); 142 } 143 144 String contentTypeId = configuration.getChild("contentTypes").getAttribute("baseId", null); 145 146 String joinPath = pos > -1 ? _fullPath.substring(0, pos) : ""; 147 _joinPaths = _configureJoinPaths(joinPath, contentTypeId); 148 _systemProperty = _systemPropEP.getExtension(systemPropertyId); 149 150 setId(SEARCH_CRITERION_SYSTEM_PREFIX + _fullPath + "-" + _operator.getName()); 151 setGroup(_configureI18nizableText(configuration.getChild("group", false), null)); 152 153 String validatorRole = "validator"; 154 if (!_initializeValidator(_validatorManager, "cms", validatorRole, configuration)) 155 { 156 validatorRole = null; 157 } 158 159 _contentTypes = new HashSet<>(); 160 for (Configuration cTypeConf : configuration.getChild("contentTypes").getChildren("type")) 161 { 162 _contentTypes.add(cTypeConf.getAttribute("id")); 163 } 164 165 setLabel(_systemProperty.getLabel()); 166 setDescription(_systemProperty.getDescription()); 167 setType(_systemProperty.getType()); 168 // Multiple defaults to false even for a multiple property. 169 setMultiple(configuration.getAttributeAsBoolean("multiple", false)); 170 171 // TODO Add the current criterion configuration. 172 String enumeratorRole = null; 173 EnumeratorDefinition enumDef = _systemProperty.getEnumeratorDefinition(_contentTypes, configuration); 174 if (enumDef != null) 175 { 176 enumeratorRole = _initializeEnumerator(enumDef); 177 } 178 179 String defaultWidget = _systemProperty.getWidget(); 180 if ("edition.textarea".equals(defaultWidget)) 181 { 182 defaultWidget = null; 183 } 184 else if (defaultWidget == null && _systemProperty.getType() == MetadataType.BOOLEAN) 185 { 186 defaultWidget = "edition.boolean-combobox"; 187 } 188 189 setWidget(configuration.getChild("widget").getValue(defaultWidget)); 190 setWidgetParameters(_configureWidgetParameters(configuration.getChild("widget-params", false), _systemProperty.getWidgetParameters())); 191 192 // Potentially replace the standard label and description by the custom ones. 193 I18nizableText userLabel = _configureI18nizableText(configuration.getChild("label", false), null); 194 if (userLabel != null) 195 { 196 setLabel(userLabel); 197 } 198 I18nizableText userDescription = _configureI18nizableText(configuration.getChild("description", false), null); 199 if (userDescription != null) 200 { 201 setDescription(userDescription); 202 } 203 204 configureUIProperties(configuration); 205 configureValues(configuration); 206 207 if (enumeratorRole != null) 208 { 209 _enumeratorManager.initialize(); 210 setEnumerator(_enumeratorManager.lookup(enumeratorRole)); 211 } 212 213 if (validatorRole != null) 214 { 215 _validatorManager.initialize(); 216 setValidator(_validatorManager.lookup(validatorRole)); 217 } 218 } 219 catch (Exception e) 220 { 221 throw new ConfigurationException("Error configuring the system search criterion.", configuration, e); 222 } 223 } 224 225 /** 226 * Get the operator. 227 * @return the operator. 228 */ 229 public Operator getOperator() 230 { 231 return _operator; 232 } 233 234 public String getFieldId() 235 { 236 return SEARCH_CRITERION_SYSTEM_PREFIX + _systemProperty.getId(); 237 } 238 239 /** 240 * Get id of this system property 241 * @return The system property's id 242 */ 243 public String getSystemPropertyId() 244 { 245 return _systemProperty.getId(); 246 } 247 248 @Override 249 public Query getQuery(Object value, Operator customOperator, Map<String, Object> allValues, String language, Map<String, Object> contextualParameters) 250 { 251 if (value == null || (value instanceof String && ((String) value).length() == 0) || (value instanceof List && ((List) value).isEmpty())) 252 { 253 return null; 254 } 255 256 Operator operator = customOperator != null ? customOperator : getOperator(); 257 258 Query query = _systemProperty.getQuery(value, operator, language, contextualParameters); 259 260 if (query != null && !_joinPaths.isEmpty()) 261 { 262 query = new JoinQuery(query, _joinPaths); 263 } 264 265 return query; 266 } 267 268 @Override 269 public SearchField getSearchField() 270 { 271 // No search field for a joined field. 272 if (_joinPaths.isEmpty()) 273 { 274 return _systemProperty.getSearchField(); 275 } 276 277 return null; 278 } 279 280 private Map<String, I18nizableText> _configureWidgetParameters(Configuration config, Map<String, I18nizableText> defaultParams) throws ConfigurationException 281 { 282 if (config != null) 283 { 284 Map<String, I18nizableText> widgetParams = new HashMap<>(); 285 286 Configuration[] params = config.getChildren("param"); 287 for (Configuration paramConfig : params) 288 { 289 boolean i18nSupported = paramConfig.getAttributeAsBoolean("i18n", false); 290 if (i18nSupported) 291 { 292 String catalogue = paramConfig.getAttribute("catalogue", null); 293 widgetParams.put(paramConfig.getAttribute("name"), new I18nizableText(catalogue, paramConfig.getValue())); 294 } 295 else 296 { 297 widgetParams.put(paramConfig.getAttribute("name"), new I18nizableText(paramConfig.getValue(""))); 298 } 299 } 300 301 return widgetParams; 302 } 303 else 304 { 305 return defaultParams; 306 } 307 } 308 309 /** 310 * Configure the join paths. 311 * @param joinPath The full join path. 312 * @param contentTypeId The base content type ID, can be null. 313 * @return The list of join paths. 314 * @throws ConfigurationException If the join paths are missconfigured 315 */ 316 private List<String> _configureJoinPaths(String joinPath, String contentTypeId) throws ConfigurationException 317 { 318 List<String> joinPaths = new ArrayList<>(); 319 320 if (StringUtils.isNotBlank(joinPath)) 321 { 322 if (StringUtils.isBlank(contentTypeId)) 323 { 324 throw new ConfigurationException("System search criterion with path '" + _fullPath + "': impossible to configure a joint system property without a base content type."); 325 } 326 327 ContentType cType = _cTypeEP.getExtension(contentTypeId); 328 IndexingModel indexingModel = cType.getIndexingModel(); 329 330 String[] pathSegments = StringUtils.split(joinPath, ContentConstants.METADATA_PATH_SEPARATOR); 331 String firstField = pathSegments[0]; 332 IndexingField indexingField = indexingModel.getField(firstField); 333 334 if (indexingField == null) 335 { 336 throw new ConfigurationException("System search criterion with path '" + _fullPath + "' refers to an unknown indexing field: " + firstField); 337 } 338 339 String[] remainingPathSegments = pathSegments.length > 1 ? (String[]) ArrayUtils.subarray(pathSegments, 1, pathSegments.length) : new String[0]; 340 341 MetadataType type = _computeJoinPaths(indexingField, remainingPathSegments, joinPaths); 342 343 // The final definition must be a content (to be able to extract a system property from it). 344 if (type != MetadataType.CONTENT) 345 { 346 throw new ConfigurationException("'" + _fullPath + "' is not a valid system search criterion path as '" + joinPath + "' does not represent a content."); 347 } 348 } 349 350 return joinPaths; 351 } 352 353 /** 354 * Get the indexing field type and compute the join paths. 355 * @param indexingField The initial indexing field 356 * @param remainingPathSegments The path to access the metadata or an another indexing field from the initial indexing field 357 * @param joinPaths The consecutive's path in case of joint to access the field/metadata 358 * @return The metadata definition or null if not found 359 * @throws ConfigurationException If an error occurs. 360 */ 361 private MetadataType _computeJoinPaths(IndexingField indexingField, String[] remainingPathSegments, List<String> joinPaths) throws ConfigurationException 362 { 363 if (indexingField instanceof MetadataIndexingField) 364 { 365 MetadataDefinition definition = getMetadataDefinition((MetadataIndexingField) indexingField, remainingPathSegments, joinPaths, true); 366 367 return definition.getType(); 368 } 369 else if (indexingField instanceof CustomIndexingField) 370 { 371 // Remaining path segments should be exactly 1 (the last path segment being the property). 372 if (remainingPathSegments.length != 1) 373 { 374 throw new ConfigurationException("The remaining path of the custom indexing field '" + indexingField.getName() + "' must represent a system property: " + StringUtils.join(remainingPathSegments, ContentConstants.METADATA_PATH_SEPARATOR)); 375 } 376 else 377 { 378 // No more recursion 379 return indexingField.getType(); 380 } 381 } 382 else 383 { 384 throw new ConfigurationException("Unsupported class of indexing field:" + indexingField.getName() + " (" + indexingField.getClass().getName() + ")"); 385 } 386 } 387 388 private String _initializeEnumerator(EnumeratorDefinition enumDef) 389 { 390 String role = null; 391 392 if (enumDef.isStatic()) 393 { 394 StaticEnumerator enumerator = new StaticEnumerator(); 395 enumDef.getStaticEntries().entrySet().forEach(entry -> enumerator.add(entry.getValue(), entry.getKey())); 396 setEnumerator(enumerator); 397 } 398 else 399 { 400 role = "enumerator"; 401 _enumeratorManager.addComponent("cms", null, role, enumDef.getEnumeratorClass(), enumDef.getConfiguration()); 402 } 403 404 return role; 405 } 406 407}