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; 026import java.util.stream.Collectors; 027 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.service.ServiceException; 032import org.apache.avalon.framework.service.ServiceManager; 033import org.apache.commons.collections4.CollectionUtils; 034import org.apache.commons.lang3.StringUtils; 035 036import org.ametys.cms.contenttype.ContentType; 037import org.ametys.cms.contenttype.MetadataType; 038import org.ametys.cms.model.ContentElementDefinition; 039import org.ametys.cms.search.SearchField; 040import org.ametys.cms.search.content.ContentSearchHelper; 041import org.ametys.cms.search.model.SystemProperty; 042import org.ametys.cms.search.model.SystemPropertyExtensionPoint; 043import org.ametys.cms.search.model.SystemSearchCriterion; 044import org.ametys.cms.search.query.JoinQuery; 045import org.ametys.cms.search.query.Query; 046import org.ametys.cms.search.query.Query.Operator; 047import org.ametys.cms.search.solr.field.JoinedSystemSearchField; 048import org.ametys.runtime.i18n.I18nizableText; 049import org.ametys.runtime.model.ModelHelper; 050import org.ametys.runtime.model.ModelItem; 051import org.ametys.runtime.model.exception.UndefinedItemPathException; 052import org.ametys.runtime.parameter.Validator; 053import org.ametys.runtime.plugin.component.ThreadSafeComponentManager; 054 055/** 056 * This class is a search criteria on a system property (author, lastModified, with-comments, ...) 057 */ 058public class SystemSearchUICriterion extends AbstractSearchUICriterion implements SystemSearchCriterion 059{ 060 /** Prefix for id of system property search criteria */ 061 public static final String SEARCH_CRITERION_SYSTEM_PREFIX = "property-"; 062 063 /** ComponentManager for {@link Validator}s. */ 064 protected ThreadSafeComponentManager<Validator> _validatorManager; 065 066 /** The system property extension point. */ 067 protected SystemPropertyExtensionPoint _systemPropEP; 068 069 /** The content search helper. */ 070 protected ContentSearchHelper _searchHelper; 071 072 /** The join paths */ 073 protected List<String> _joinPaths; 074 075 private Operator _operator; 076 private SystemProperty _systemProperty; 077 private Set<String> _contentTypes; 078 private String _fullPath; 079 080 @Override 081 public void service(ServiceManager manager) throws ServiceException 082 { 083 super.service(manager); 084 _systemPropEP = (SystemPropertyExtensionPoint) manager.lookup(SystemPropertyExtensionPoint.ROLE); 085 _searchHelper = (ContentSearchHelper) manager.lookup(ContentSearchHelper.ROLE); 086 } 087 088 @Override 089 public void dispose() 090 { 091 super.dispose(); 092 _validatorManager.dispose(); 093 _validatorManager = null; 094 } 095 096 @Override 097 public void configure(Configuration configuration) throws ConfigurationException 098 { 099 try 100 { 101 _validatorManager = new ThreadSafeComponentManager<>(); 102 _validatorManager.setLogger(_logger); 103 _validatorManager.contextualize(_context); 104 _validatorManager.service(_manager); 105 106 _enumeratorManager = new ThreadSafeComponentManager<>(); 107 _enumeratorManager.setLogger(_logger); 108 _enumeratorManager.contextualize(_context); 109 _enumeratorManager.service(_manager); 110 111 _fullPath = configuration.getChild("systemProperty").getAttribute("name"); 112 int pos = _fullPath.lastIndexOf(ModelItem.ITEM_PATH_SEPARATOR); 113 114 String systemPropertyId = pos > -1 ? _fullPath.substring(pos + ModelItem.ITEM_PATH_SEPARATOR.length()) : _fullPath; 115 _operator = Operator.fromName(configuration.getChild("test-operator").getValue("eq")); 116 117 if (!_systemPropEP.isSearchable(systemPropertyId)) 118 { 119 throw new ConfigurationException("The property '" + systemPropertyId + "' doesn't exist or is not searchable."); 120 } 121 122 Set<String> baseContentTypeIds = new HashSet<>(); 123 for (Configuration cTypeConf : configuration.getChild("contentTypes").getChildren("baseType")) 124 { 125 baseContentTypeIds.add(cTypeConf.getAttribute("id")); 126 } 127 128 _contentTypes = new HashSet<>(); 129 String joinPath = pos > -1 ? _fullPath.substring(0, pos) : ""; 130 _joinPaths = _configureJoinPaths(joinPath, baseContentTypeIds, configuration); 131 _systemProperty = _systemPropEP.getExtension(systemPropertyId); 132 133 setId(SEARCH_CRITERION_SYSTEM_PREFIX + _fullPath + "-" + _operator.getName()); 134 setGroup(_configureI18nizableText(configuration.getChild("group", false), null)); 135 136 String validatorRole = "validator"; 137 if (!_initializeValidator(_validatorManager, "cms", validatorRole, configuration)) 138 { 139 validatorRole = null; 140 } 141 142 setLabel(_systemProperty.getLabel()); 143 setDescription(_systemProperty.getDescription()); 144 MetadataType type = MetadataType.fromModelItemType(_systemProperty.getType()); 145 setType(type); 146 // Multiple defaults to false even for a multiple property. 147 setMultiple(configuration.getAttributeAsBoolean("multiple", false)); 148 149 // TODO Add the current criterion configuration. 150 Configuration enumeratorAndWidgetParamConf = _getEnumeratorAndWidgetParamConf(configuration); 151 setEnumerator(configureEnumerator(configuration, _systemProperty, enumeratorAndWidgetParamConf)); 152 setWidget(configureWidget(configuration, _systemProperty)); 153 setWidgetParameters(configureWidgetParameters(configuration, _systemProperty, enumeratorAndWidgetParamConf)); 154 155 // Potentially replace the standard label and description by the custom ones. 156 I18nizableText userLabel = _configureI18nizableText(configuration.getChild("label", false), null); 157 if (userLabel != null) 158 { 159 setLabel(userLabel); 160 } 161 I18nizableText userDescription = _configureI18nizableText(configuration.getChild("description", false), null); 162 if (userDescription != null) 163 { 164 setDescription(userDescription); 165 } 166 167 configureUIProperties(configuration); 168 configureValues(configuration); 169 170 if (validatorRole != null) 171 { 172 _validatorManager.initialize(); 173 setValidator(_validatorManager.lookup(validatorRole)); 174 } 175 } 176 catch (Exception e) 177 { 178 throw new ConfigurationException("Error configuring the system search criterion.", configuration, e); 179 } 180 } 181 182 private Configuration _getEnumeratorAndWidgetParamConf(Configuration critConf) throws ConfigurationException 183 { 184 if (isJoined()) 185 { 186 DefaultConfiguration widgetParamConf = new DefaultConfiguration(critConf); 187 widgetParamConf.removeChild(widgetParamConf.getChild("contentTypes")); 188 if (!_contentTypes.isEmpty()) 189 { 190 DefaultConfiguration cTypesConf = new DefaultConfiguration("contentTypes"); 191 widgetParamConf.addChild(cTypesConf); 192 for (String contentType : _contentTypes) 193 { 194 DefaultConfiguration cTypeConf = new DefaultConfiguration("type"); 195 cTypeConf.setAttribute("id", contentType); 196 cTypesConf.addChild(cTypeConf); 197 } 198 } 199 200 return widgetParamConf; 201 } 202 203 // in case non join, the global conf already contains the real content types 204 return critConf; 205 206 } 207 208 @Override 209 public boolean isJoined() 210 { 211 return CollectionUtils.isNotEmpty(_joinPaths); 212 } 213 214 @Override 215 public boolean isFacetable() 216 { 217 return _systemProperty.isFacetable(); 218 } 219 220 /** 221 * Get the operator. 222 * @return the operator. 223 */ 224 public Operator getOperator() 225 { 226 return _operator; 227 } 228 229 public String getFieldId() 230 { 231 return SEARCH_CRITERION_SYSTEM_PREFIX + _systemProperty.getName(); 232 } 233 234 /** 235 * Get id of this system property 236 * @return The system property's id 237 */ 238 public String getSystemPropertyId() 239 { 240 return _systemProperty.getName(); 241 } 242 243 @Override 244 public Query getQuery(Object value, Operator customOperator, Map<String, Object> allValues, String language, Map<String, Object> contextualParameters) 245 { 246 if (customOperator != Operator.EXISTS 247 && (value == null 248 || value instanceof String && ((String) value).length() == 0 249 || value instanceof List && ((List) value).isEmpty())) 250 { 251 return null; 252 } 253 254 Operator operator = customOperator != null ? customOperator : getOperator(); 255 256 Query query = _systemProperty.getQuery(value, operator, language, contextualParameters); 257 258 if (query != null && !_joinPaths.isEmpty()) 259 { 260 query = new JoinQuery(query, _joinPaths); 261 } 262 263 return query; 264 } 265 266 @Override 267 public SearchField getSearchField() 268 { 269 SearchField sysSearchField = _systemProperty.getSearchField(); 270 if (_joinPaths.isEmpty()) 271 { 272 return sysSearchField; 273 } 274 else 275 { 276 return new JoinedSystemSearchField(_joinPaths, sysSearchField); 277 } 278 } 279 280 /** 281 * Configure the join paths. 282 * @param joinPath The full join path. 283 * @param contentTypeIds The base content type identifiers. 284 * @param configuration The configuration of the criterion 285 * @return The list of join paths. 286 * @throws ConfigurationException If the join paths are missconfigured 287 */ 288 private List<String> _configureJoinPaths(String joinPath, Set<String> contentTypeIds, Configuration configuration) throws ConfigurationException 289 { 290 List<String> joinPaths = new ArrayList<>(); 291 292 if (StringUtils.isNotBlank(joinPath)) 293 { 294 if (contentTypeIds.isEmpty()) 295 { 296 throw new ConfigurationException("System search criterion with path '" + _fullPath + "': impossible to configure a joint system property without base content types."); 297 } 298 299 try 300 { 301 Collection<ContentType> contentTypes = contentTypeIds.stream() 302 .map(_cTypeEP::getExtension) 303 .collect(Collectors.toList()); 304 ModelItem modelItem = ModelHelper.getModelItem(joinPath, contentTypes); 305 // The definition must be a content (to be able to extract a system property from it). 306 if (modelItem instanceof ContentElementDefinition contentElementDefinition) 307 { 308 joinPaths.addAll(_searchHelper.computeJoinPaths(joinPath, contentTypeIds, true)); 309 // Add in _contentTypes the good types 310 Optional.ofNullable(contentElementDefinition.getContentTypeId()) 311 .map(Collections::singletonList) 312 .map(this::_typeAndSubTypes) 313 .ifPresent(_contentTypes::addAll); 314 } 315 else 316 { 317 throw new ConfigurationException("'" + _fullPath + "' is not a valid system search criterion path as '" + joinPath + "' does not represent a content."); 318 } 319 } 320 catch (UndefinedItemPathException e) 321 { 322 throw new ConfigurationException("System search criterion with path '" + _fullPath + "' refers to an unknown model item: " + joinPath, e); 323 } 324 } 325 else 326 { 327 for (Configuration cTypeConf : configuration.getChild("contentTypes").getChildren("type")) 328 { 329 _contentTypes.add(cTypeConf.getAttribute("id")); 330 } 331 } 332 333 return joinPaths; 334 } 335 336 private Collection<String> _typeAndSubTypes(List<String> singletonType) 337 { 338 String typeInSingleton = singletonType.iterator().next(); 339 return CollectionUtils.union(singletonType, _cTypeEP.getSubTypes(typeInSingleton)); 340 } 341}