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