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