001/* 002 * Copyright 2023 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.model; 017 018import java.util.Collections; 019import java.util.HashMap; 020import java.util.HashSet; 021import java.util.Locale; 022import java.util.Map; 023import java.util.Optional; 024import java.util.Set; 025 026import org.apache.avalon.framework.component.Component; 027import org.apache.avalon.framework.configuration.Configuration; 028import org.apache.avalon.framework.configuration.ConfigurationException; 029import org.apache.avalon.framework.configuration.DefaultConfiguration; 030import org.apache.avalon.framework.service.ServiceException; 031import org.apache.avalon.framework.service.ServiceManager; 032import org.apache.avalon.framework.service.Serviceable; 033 034import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 035import org.ametys.cms.contenttype.MetadataType; 036import org.ametys.cms.repository.Content; 037import org.ametys.cms.search.query.Query.Operator; 038import org.ametys.core.user.User; 039import org.ametys.core.user.UserIdentity; 040import org.ametys.core.user.UserManager; 041import org.ametys.core.user.population.UserPopulationDAO; 042import org.ametys.plugins.repository.AmetysObjectResolver; 043import org.ametys.runtime.i18n.I18nizableText; 044import org.ametys.runtime.i18n.I18nizableTextParameter; 045import org.ametys.runtime.parameter.Enumerator; 046import org.ametys.runtime.plugin.component.AbstractLogEnabled; 047 048/** 049 * Helper for {@link SearchCriterion} 050 */ 051public class SearchCriterionHelper extends AbstractLogEnabled implements Component, Serviceable 052{ 053 /** The component role. */ 054 public static final String ROLE = SearchCriterionHelper.class.getName(); 055 056 /** The ametys object resolver. */ 057 protected AmetysObjectResolver _resolver; 058 /** The content type extension point */ 059 protected ContentTypeExtensionPoint _contentTypeExtensionPoint; 060 /** The searchModelHelper */ 061 protected SearchModelHelper _searchModelHelper; 062 /** The user manager. */ 063 protected UserManager _userManager; 064 /** The user population DAO */ 065 protected UserPopulationDAO _userPopulationDAO; 066 067 public void service(ServiceManager manager) throws ServiceException 068 { 069 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 070 _contentTypeExtensionPoint = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE); 071 _searchModelHelper = (SearchModelHelper) manager.lookup(SearchModelHelper.ROLE); 072 _userManager = (UserManager) manager.lookup(UserManager.ROLE); 073 _userPopulationDAO = (UserPopulationDAO) manager.lookup(UserPopulationDAO.ROLE); 074 } 075 076 /** 077 * Get the configuration of a criteria component. 078 * @param originalConf the optional original criteria configuration. 079 * @param baseContentTypeIds the "base" content type identifiers. 080 * @param path the field path. 081 * @param operator the optional criteria operator 082 * @param group the optional group 083 * @return the configuration to provide to the criteria component. 084 * @throws ConfigurationException if an error occurs. 085 */ 086 public Configuration getIndexingFieldCriteriaConfiguration(Optional<Configuration> originalConf, Set<String> baseContentTypeIds, String path, Optional<Operator> operator, Optional<I18nizableText> group) throws ConfigurationException 087 { 088 return _getIndexingFieldCriteriaConfiguration(Optional.empty(), originalConf, baseContentTypeIds, path, operator, group); 089 } 090 091 /** 092 * Get the configuration of a criteria component. 093 * @param searchModel the search model. 094 * @param originalConf the optional original criteria configuration. 095 * @param baseContentTypeIds the "base" content type identifiers. 096 * @param path the field path. 097 * @param operator the optional criteria operator 098 * @param group the optional group 099 * @return the configuration to provide to the criteria component. 100 * @throws ConfigurationException if an error occurs. 101 */ 102 public Configuration getIndexingFieldCriteriaConfiguration(SearchModel searchModel, Optional<Configuration> originalConf, Set<String> baseContentTypeIds, String path, Optional<Operator> operator, Optional<I18nizableText> group) throws ConfigurationException 103 { 104 return _getIndexingFieldCriteriaConfiguration(Optional.ofNullable(searchModel), originalConf, baseContentTypeIds, path, operator, group); 105 } 106 107 private Configuration _getIndexingFieldCriteriaConfiguration(Optional<SearchModel> searchModel, Optional<Configuration> originalConf, Set<String> baseContentTypeIds, String path, Optional<Operator> operator, Optional<I18nizableText> group) throws ConfigurationException 108 { 109 DefaultConfiguration conf = originalConf.isPresent() 110 ? new DefaultConfiguration(originalConf.get()) 111 : new DefaultConfiguration("criteria"); 112 113 DefaultConfiguration metaConf = new DefaultConfiguration("field"); 114 conf.addChild(metaConf); 115 metaConf.setAttribute("path", path); 116 117 if (operator.isPresent()) 118 { 119 DefaultConfiguration operatorConf = new DefaultConfiguration("test-operator"); 120 conf.addChild(operatorConf); 121 operatorConf.setValue(operator.get().getName()); 122 } 123 124 Configuration contentTypesConfiguration = _getContentTypesConfiguration(searchModel, baseContentTypeIds); 125 conf.addChild(contentTypesConfiguration); 126 127 if (group.isPresent()) 128 { 129 Configuration groupConfiguration = _getGroupConfiguration(group.get()); 130 conf.addChild(groupConfiguration); 131 } 132 133 return conf; 134 } 135 136 /** 137 * Get the configuration of a system criteria component. 138 * @param originalConf the optional original criteria configuration. 139 * @param baseContentTypeIds the "base" content type identifiers. 140 * @param propertyId the system property identifier. 141 * @param group The optional group. 142 * @return the configuration to provide to the system criterion component. 143 * @throws ConfigurationException if an error occurs. 144 */ 145 public Configuration getSystemCriteriaConfiguration(Optional<Configuration> originalConf, Set<String> baseContentTypeIds, String propertyId, Optional<I18nizableText> group) throws ConfigurationException 146 { 147 return _getSystemCriteriaConfiguration(Optional.empty(), originalConf, baseContentTypeIds, propertyId, group); 148 } 149 150 /** 151 * Get the configuration of a system criteria component. 152 * @param searchModel the search model. 153 * @param originalConf the optional original criteria configuration. 154 * @param baseContentTypeIds the "base" content type identifiers. 155 * @param propertyId the system property identifier. 156 * @param group The optional group. 157 * @return the configuration to provide to the system criterion component. 158 * @throws ConfigurationException if an error occurs. 159 */ 160 public Configuration getSystemCriteriaConfiguration(SearchModel searchModel, Optional<Configuration> originalConf, Set<String> baseContentTypeIds, String propertyId, Optional<I18nizableText> group) throws ConfigurationException 161 { 162 return _getSystemCriteriaConfiguration(Optional.ofNullable(searchModel), originalConf, baseContentTypeIds, propertyId, group); 163 } 164 165 private Configuration _getSystemCriteriaConfiguration(Optional<SearchModel> searchModel, Optional<Configuration> originalConf, Set<String> baseContentTypeIds, String propertyId, Optional<I18nizableText> group) throws ConfigurationException 166 { 167 DefaultConfiguration conf = originalConf.isPresent() 168 ? new DefaultConfiguration(originalConf.get()) 169 : new DefaultConfiguration("criteria"); 170 171 DefaultConfiguration propConf = new DefaultConfiguration("systemProperty"); 172 propConf.setAttribute("name", propertyId); 173 conf.addChild(propConf); 174 175 Configuration contentTypesConfiguration = _getContentTypesConfiguration(searchModel, baseContentTypeIds); 176 conf.addChild(contentTypesConfiguration); 177 178 if (group.isPresent()) 179 { 180 Configuration groupConfiguration = _getGroupConfiguration(group.get()); 181 conf.addChild(groupConfiguration); 182 } 183 184 return conf; 185 } 186 187 /** 188 * Get the configuration of a custom criteria component. 189 * @param searchModel the search model. 190 * @param originalConf the original criteria configuration. 191 * @param baseContentTypeIds the "base" content type identifiers. 192 * @param customCriterionId the custom criterion id 193 * @param group The group. Can be null. 194 * @return the configuration to provide to the custom criterion component. 195 * @throws ConfigurationException if an error occurs. 196 */ 197 public Configuration getCustomCriteriaConfiguration(SearchModel searchModel, Optional<Configuration> originalConf, Set<String> baseContentTypeIds, String customCriterionId, Optional<I18nizableText> group) throws ConfigurationException 198 { 199 DefaultConfiguration conf = originalConf.isPresent() 200 ? new DefaultConfiguration(originalConf.get()) 201 : new DefaultConfiguration("criteria"); 202 203 DefaultConfiguration customCriterionConf = new DefaultConfiguration("customCriterion"); 204 customCriterionConf.setAttribute("id", customCriterionId); 205 conf.addChild(customCriterionConf); 206 207 Configuration contentTypesConfiguration = _getContentTypesConfiguration(Optional.ofNullable(searchModel), baseContentTypeIds); 208 conf.addChild(contentTypesConfiguration); 209 210 if (group.isPresent()) 211 { 212 Configuration groupConfiguration = _getGroupConfiguration(group.get()); 213 conf.addChild(groupConfiguration); 214 } 215 216 return conf; 217 } 218 219 private Configuration _getContentTypesConfiguration(Optional<SearchModel> searchModel, Set<String> baseContentTypeIds) 220 { 221 DefaultConfiguration contentTypesConf = new DefaultConfiguration("contentTypes"); 222 223 if (searchModel.isPresent()) 224 { 225 Set<String> contentTypes = getAllContentTypes(searchModel.get(), Collections.emptyMap()); 226 for (String contentType : contentTypes) 227 { 228 DefaultConfiguration cTypeConf = new DefaultConfiguration("type"); 229 cTypeConf.setAttribute("id", contentType); 230 contentTypesConf.addChild(cTypeConf); 231 } 232 } 233 234 if (baseContentTypeIds != null) 235 { 236 for (String baseContentTypeId : baseContentTypeIds) 237 { 238 DefaultConfiguration cTypeConf = new DefaultConfiguration("baseType"); 239 cTypeConf.setAttribute("id", baseContentTypeId); 240 contentTypesConf.addChild(cTypeConf); 241 } 242 } 243 244 return contentTypesConf; 245 } 246 247 private Configuration _getGroupConfiguration(I18nizableText group) 248 { 249 DefaultConfiguration groupConf = new DefaultConfiguration("group"); 250 groupConf.setAttribute("i18n", group.isI18n()); 251 groupConf.setValue(group.isI18n() ? group.getCatalogue() + ":" + group.getKey() : group.getLabel()); 252 return groupConf; 253 } 254 255 256 257 /** 258 * Get all the real content types that a model works on (the included content types, minus the excluded types). 259 * @param model the search model. 260 * @param contextualParameters the contextual parameters. 261 * @return a Set of the content type IDs. 262 */ 263 public Set<String> getAllContentTypes(SearchModel model, Map<String, Object> contextualParameters) 264 { 265 Set<String> allContentTypes = new HashSet<>(); 266 267 Set<String> modelCTypes = model.getContentTypes(contextualParameters); 268 Set<String> modelExcludedCTypes = model.getExcludedContentTypes(contextualParameters); 269 270 if (modelCTypes.isEmpty()) 271 { 272 // Empty "declared" content types: the model works on all content types. 273 allContentTypes.addAll(_contentTypeExtensionPoint.getExtensionsIds()); 274 } 275 else 276 { 277 // Otherwise, add all declared content types and their sub-types. 278 for (String cTypeId : modelCTypes) 279 { 280 allContentTypes.add(cTypeId); 281 allContentTypes.addAll(_contentTypeExtensionPoint.getSubTypes(cTypeId)); 282 } 283 } 284 285 // Remove all excluded types. 286 allContentTypes.removeAll(modelExcludedCTypes); 287 288 return allContentTypes; 289 } 290 291 /** 292 * Get the label of a facet value for the given criterion. 293 * @param criterion the criterion 294 * @param value the facet value. 295 * @param currentLocale the current locale 296 * @return the label, or null if the value does not exist. 297 */ 298 public I18nizableText getFacetLabel(SearchCriterion criterion, String value, Locale currentLocale) 299 { 300 I18nizableText label = null; 301 302 try 303 { 304 MetadataType type = criterion.getType(); 305 Enumerator enumerator = criterion.getEnumerator(); 306 307 if (type == MetadataType.CONTENT) 308 { 309 Content content = _resolver.resolveById(value); 310 label = new I18nizableText(content.getTitle(currentLocale)); 311 } 312 else if (type == MetadataType.USER) 313 { 314 UserIdentity userIdentity = UserIdentity.stringToUserIdentity(value); 315 String login = userIdentity.getLogin(); 316 String populationId = userIdentity.getPopulationId(); 317 User user = _userManager.getUser(populationId, login); 318 if (user != null) 319 { 320 // Default i18n key only use login, sortablename and population. 321 // But we provide more parameters if the user want to override it. 322 Map<String, I18nizableTextParameter> i18nParams = new HashMap<>(); 323 i18nParams.put("login", new I18nizableText(login)); 324 i18nParams.put("firstname", new I18nizableText(user.getFirstName())); 325 i18nParams.put("lastname", new I18nizableText(user.getLastName())); 326 i18nParams.put("fullname", new I18nizableText(user.getFullName())); 327 i18nParams.put("sortablename", new I18nizableText(user.getSortableName())); 328 i18nParams.put("population", _userPopulationDAO.getUserPopulation(populationId).getLabel()); 329 330 label = new I18nizableText("plugin.cms", "PLUGINS_CMS_UITOOL_SEARCH_FACET_USER_LABEL", i18nParams); 331 } 332 else 333 { 334 Map<String, I18nizableTextParameter> i18nParams = new HashMap<>(); 335 i18nParams.put("login", new I18nizableText(login)); 336 i18nParams.put("population", new I18nizableText(populationId)); 337 338 label = new I18nizableText("plugin.cms", "PLUGINS_CMS_UITOOL_SEARCH_FACET_UNKNOWN_USER_LABEL", i18nParams); 339 } 340 } 341 else if (enumerator != null) 342 { 343 label = enumerator.getEntry(value); 344 } 345 else if (type == MetadataType.BOOLEAN) 346 { 347 boolean boolValue = "T".equals(value) /* if joined facet, value will be "F" or "T" */ || Boolean.valueOf(value); 348 label = new I18nizableText("plugin.cms", boolValue ? "PLUGINS_CMS_UITOOL_SEARCH_FACET_BOOLEAN_TRUE_LABEL" : "PLUGINS_CMS_UITOOL_SEARCH_FACET_BOOLEAN_FALSE_LABEL"); 349 } 350 } 351 catch (Exception e) 352 { 353 // Ignore, just return null. 354 getLogger().warn("Unable to get facel label for value '{}'", value, e); 355 } 356 357 return label; 358 } 359}