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