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 (customOperator != Operator.EXISTS 289 && (value == null 290 || value instanceof String && ((String) value).length() == 0 291 || value instanceof List && ((List) value).isEmpty())) 292 { 293 return null; 294 } 295 296 Operator operator = customOperator != null ? customOperator : getOperator(); 297 298 Query query = _systemProperty.getQuery(value, operator, language, contextualParameters); 299 300 if (query != null && !_joinPaths.isEmpty()) 301 { 302 query = new JoinQuery(query, _joinPaths); 303 } 304 305 return query; 306 } 307 308 @Override 309 public SearchField getSearchField() 310 { 311 SearchField sysSearchField = _systemProperty.getSearchField(); 312 if (_joinPaths.isEmpty()) 313 { 314 return sysSearchField; 315 } 316 else 317 { 318 return new JoinedSystemSearchField(_joinPaths, sysSearchField); 319 } 320 } 321 322 /** 323 * Configure the join paths. 324 * @param joinPath The full join path. 325 * @param contentTypeIds The base content type identifiers. 326 * @param configuration The configuration of the criterion 327 * @return The list of join paths. 328 * @throws ConfigurationException If the join paths are missconfigured 329 */ 330 private List<String> _configureJoinPaths(String joinPath, Set<String> contentTypeIds, Configuration configuration) throws ConfigurationException 331 { 332 List<String> joinPaths = new ArrayList<>(); 333 334 if (StringUtils.isNotBlank(joinPath)) 335 { 336 if (contentTypeIds.isEmpty()) 337 { 338 throw new ConfigurationException("System search criterion with path '" + _fullPath + "': impossible to configure a joint system property without base content types."); 339 } 340 341 Iterator<IndexingModel> indexingModels = contentTypeIds.stream() 342 .map(_cTypeEP::getExtension) 343 .map(ContentType::getIndexingModel) 344 .iterator(); 345 346 String[] pathSegments = StringUtils.split(joinPath, ModelItem.ITEM_PATH_SEPARATOR); 347 String firstField = pathSegments[0]; 348 349 IndexingField indexingField = null; 350 while (indexingModels.hasNext() && indexingField == null) 351 { 352 indexingField = indexingModels.next().getField(firstField); 353 } 354 355 if (indexingField == null) 356 { 357 throw new ConfigurationException("System search criterion with path '" + _fullPath + "' refers to an unknown indexing field: " + firstField); 358 } 359 360 String[] remainingPathSegments = pathSegments.length > 1 ? (String[]) ArrayUtils.subarray(pathSegments, 1, pathSegments.length) : new String[0]; 361 362 MetadataType type = _computeJoinPaths(indexingField, remainingPathSegments, joinPaths); 363 364 // The final definition must be a content (to be able to extract a system property from it). 365 if (type != MetadataType.CONTENT) 366 { 367 throw new ConfigurationException("'" + _fullPath + "' is not a valid system search criterion path as '" + joinPath + "' does not represent a content."); 368 } 369 } 370 else 371 { 372 for (Configuration cTypeConf : configuration.getChild("contentTypes").getChildren("type")) 373 { 374 _contentTypes.add(cTypeConf.getAttribute("id")); 375 } 376 } 377 378 return joinPaths; 379 } 380 381 /** 382 * Get the indexing field type and compute the join paths. 383 * @param indexingField The initial indexing field 384 * @param remainingPathSegments The path to access the metadata or an another indexing field from the initial indexing field 385 * @param joinPaths The consecutive's path in case of joint to access the field/metadata 386 * @return The metadata definition or null if not found 387 * @throws ConfigurationException If an error occurs. 388 */ 389 private MetadataType _computeJoinPaths(IndexingField indexingField, String[] remainingPathSegments, List<String> joinPaths) throws ConfigurationException 390 { 391 if (indexingField instanceof MetadataIndexingField) 392 { 393 MetadataDefinition definition = getMetadataDefinition((MetadataIndexingField) indexingField, remainingPathSegments, joinPaths, true); 394 // Add in _contentTypes the good types 395 Optional.ofNullable(definition.getContentType()) 396 .map(Collections::singletonList) 397 .map(this::_typeAndSubTypes) 398 .ifPresent(_contentTypes::addAll); 399 400 return definition.getType(); 401 } 402 else if (indexingField instanceof CustomIndexingField) 403 { 404 // Remaining path segments should be exactly 1 (the last path segment being the property). 405 if (remainingPathSegments.length != 1) 406 { 407 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)); 408 } 409 else 410 { 411 // No more recursion 412 return indexingField.getType(); 413 } 414 } 415 else 416 { 417 throw new ConfigurationException("Unsupported class of indexing field:" + indexingField.getName() + " (" + indexingField.getClass().getName() + ")"); 418 } 419 } 420 421 private Collection<String> _typeAndSubTypes(List<String> singletonType) 422 { 423 String typeInSingleton = singletonType.iterator().next(); 424 return CollectionUtils.union(singletonType, _cTypeEP.getSubTypes(typeInSingleton)); 425 } 426 427 private String _initializeEnumerator(EnumeratorDefinition enumDef) 428 { 429 String role = null; 430 431 if (enumDef.isStatic()) 432 { 433 StaticEnumerator enumerator = new StaticEnumerator(); 434 enumDef.getStaticEntries().entrySet().forEach(entry -> enumerator.add(entry.getValue(), entry.getKey())); 435 setEnumerator(enumerator); 436 } 437 else 438 { 439 role = "enumerator"; 440 _enumeratorManager.addComponent("cms", null, role, enumDef.getEnumeratorClass(), enumDef.getConfiguration()); 441 } 442 443 return role; 444 } 445 446}