001/* 002 * Copyright 2025 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.impl; 017 018import java.util.HashMap; 019import java.util.HashSet; 020import java.util.List; 021import java.util.Map; 022import java.util.Optional; 023import java.util.Set; 024import java.util.stream.Collectors; 025 026import org.apache.avalon.framework.configuration.Configuration; 027import org.apache.avalon.framework.configuration.ConfigurationException; 028import org.apache.avalon.framework.configuration.DefaultConfiguration; 029import org.apache.avalon.framework.service.ServiceException; 030import org.apache.commons.lang3.StringUtils; 031 032import org.ametys.cms.contenttype.ContentType; 033import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 034import org.ametys.cms.data.type.ModelItemTypeExtensionPoint; 035import org.ametys.cms.data.type.indexing.IndexableElementType; 036import org.ametys.cms.model.CMSDataContext; 037import org.ametys.cms.model.ContentElementDefinition; 038import org.ametys.cms.model.properties.Property; 039import org.ametys.cms.search.content.ContentSearchHelper; 040import org.ametys.cms.search.content.ContentSearchHelper.JoinedPaths; 041import org.ametys.cms.search.model.CriterionDefinition; 042import org.ametys.cms.search.model.CriterionDefinitionAwareElementDefinition; 043import org.ametys.cms.search.model.IndexationAwareElementDefinition; 044import org.ametys.cms.search.query.JoinQuery; 045import org.ametys.cms.search.query.NotQuery; 046import org.ametys.cms.search.query.Query; 047import org.ametys.cms.search.query.Query.LogicalOperator; 048import org.ametys.cms.search.query.Query.Operator; 049import org.ametys.runtime.i18n.I18nizableText; 050import org.ametys.runtime.model.ElementDefinition; 051import org.ametys.runtime.model.Enumerator; 052import org.ametys.runtime.model.ModelHelper; 053import org.ametys.runtime.model.ModelItem; 054import org.ametys.runtime.model.exception.UnknownTypeException; 055import org.ametys.runtime.model.type.DataContext; 056import org.ametys.runtime.plugin.component.ThreadSafeComponentManager; 057 058/** 059 * Default implementation for {@link CriterionDefinition} searching on a model item. 060 * @param <T> Type of the criterion value 061 */ 062public class ReferencingCriterionDefinition<T> extends AbstractCriterionDefinition<T> 063{ 064 /** None value to search for reference where the attribute does not exist */ 065 public static final String NONE_VALUE = "__ametys_none"; 066 /** The content type extension point */ 067 protected ContentTypeExtensionPoint _contentTypeExtensionPoint; 068 069 /** The extension point containing all available criterion types */ 070 protected ModelItemTypeExtensionPoint _criterionTypeExtensionPoint; 071 072 /** The content search helper */ 073 protected ContentSearchHelper _contentSearchHelper; 074 075 /** The referenced definition used for this criterion */ 076 protected ElementDefinition _reference; 077 078 /** The path of the field from the containing search model */ 079 protected String _referencePath; 080 081 /** The identifiers of the content types defining the reference */ 082 protected Set<String> _contentTypeIds = new HashSet<>(); 083 084 /** 085 * Default constructor. 086 */ 087 public ReferencingCriterionDefinition() 088 { 089 // Nothing to do 090 } 091 092 /** 093 * Constructor used to create a BO criterion definition on a referenced item 094 * @param reference the item referenced by this criterion 095 * @param referencePath the path of the criterion's reference 096 */ 097 public ReferencingCriterionDefinition(ElementDefinition reference, String referencePath) 098 { 099 _reference = reference; 100 _referencePath = referencePath; 101 } 102 103 /** 104 * Get the path in the criterion's reference. 105 * @return the path in the criterion's reference. 106 */ 107 public String getReferencePath() 108 { 109 return _referencePath; 110 } 111 112 /** 113 * Set the path of the criterion's reference 114 * @param referencePath the reference path to set 115 */ 116 protected void setReferencePath(String referencePath) 117 { 118 _referencePath = referencePath; 119 } 120 121 /** 122 * Retrieves the referenced element definition 123 * @return the referenced element definition 124 */ 125 public ElementDefinition<T> getReference() 126 { 127 return _reference; 128 } 129 130 /** 131 * Set the referenced element definition 132 * @param reference the referenced element definition to set 133 */ 134 protected void setReference(ElementDefinition<T> reference) 135 { 136 _reference = reference; 137 } 138 139 /** 140 * Retrieves the identifiers of the content types defining the reference 141 * @param contextualParameters the contextual parameters 142 * @return the identifiers of the content types defining the reference 143 */ 144 public Set<String> getContentTypeIds(Map<String, Object> contextualParameters) 145 { 146 return _contentTypeIds; 147 } 148 149 /** 150 * Set the identifiers of the content types defining the reference 151 * @param contentTypeIds the content type identifiers to set 152 */ 153 public void setContentTypeIds(Set<String> contentTypeIds) 154 { 155 _contentTypeIds = contentTypeIds; 156 } 157 158 @Override 159 public I18nizableText getLabel() 160 { 161 I18nizableText label = super.getLabel(); 162 return label != null ? label : getReference().getLabel(); 163 } 164 165 @Override 166 public I18nizableText getDescription() 167 { 168 I18nizableText description = super.getDescription(); 169 return description != null ? description : getReference().getDescription(); 170 } 171 172 @SuppressWarnings("unchecked") 173 @Override 174 public IndexableElementType<T> getType() 175 { 176 if (getReference() instanceof CriterionDefinitionAwareElementDefinition criterionDefinitionAwareReference) 177 { 178 return criterionDefinitionAwareReference.getCriterionType(); 179 } 180 else 181 { 182 IndexableElementType<T> referenceType = (IndexableElementType<T>) getReference().getType(); 183 String criterionTypeId = referenceType.getCriterionTypeId(CMSDataContext.newInstance() 184 .withModelItem(this)); 185 186 if (_getCriterionTypeExtensionPoint().hasExtension(criterionTypeId)) 187 { 188 return (IndexableElementType<T>) _getCriterionTypeExtensionPoint().getExtension(criterionTypeId); 189 } 190 else 191 { 192 throw new UnknownTypeException("Unable to retrieve type of the criterion definition referencing '" + getReferencePath() + "'. The type '" + criterionTypeId + "' is not available for criteria"); 193 } 194 } 195 } 196 197 @Override 198 protected String _getDefaultWidget() 199 { 200 ElementDefinition<T> reference = getReference(); 201 if (reference instanceof CriterionDefinitionAwareElementDefinition criterionDefinitionAwareReference) 202 { 203 String defaultWidget = criterionDefinitionAwareReference.getDefaultCriterionWidget(); 204 return "edition.textarea".equals(defaultWidget) ? null : defaultWidget; 205 } 206 else 207 { 208 String defaultWidget = _getCriterionDefinitionHelper().getCriterionDefinitionDefaultWidget(this); 209 if (StringUtils.isEmpty(defaultWidget) && reference.isEditable()) 210 { 211 defaultWidget = reference.getWidget(); 212 } 213 214 return defaultWidget; 215 } 216 } 217 218 @Override 219 protected Map<String, I18nizableText> _getDefaultWidgetParameters() 220 { 221 ElementDefinition<T> reference = getReference(); 222 if (reference instanceof CriterionDefinitionAwareElementDefinition criterionDefinitionAwareReference) 223 { 224 try 225 { 226 Configuration configuration = _getEnumeratorAndWidgetParamConf(); 227 return criterionDefinitionAwareReference.getDefaultCriterionWidgetParameters(configuration); 228 } 229 catch (ConfigurationException e) 230 { 231 // An error occurs while trying to configure the criterion widget parameters of the system property => use the classic widget parameters 232 _logger.error("Unable to use widget parameters for criterion of the system property '" + criterionDefinitionAwareReference.getName() + "'", e); 233 return _getCriterionDefinitionHelper().getCriterionDefinitionDefaultWidgetParameters(this); 234 } 235 } 236 else 237 { 238 Map<String, I18nizableText> defaultWidgetParameters = new HashMap<>(); 239 if (reference.isEditable()) 240 { 241 defaultWidgetParameters.putAll(reference.getWidgetParameters()); 242 } 243 244 defaultWidgetParameters.putAll(_getCriterionDefinitionHelper().getCriterionDefinitionDefaultWidgetParameters(this)); 245 246 return defaultWidgetParameters; 247 } 248 } 249 250 @Override 251 public Enumerator<T> getEnumerator() 252 { 253 return Optional.ofNullable(super.getEnumerator()) 254 .orElseGet(() -> _getDefaultEnumerator()); 255 } 256 257 /** 258 * Retrieves the default {@link Enumerator} for the current criterion definition 259 * @return the default {@link Enumerator} for the current criterion definition 260 */ 261 @SuppressWarnings("unchecked") 262 protected Enumerator<T> _getDefaultEnumerator() 263 { 264 ElementDefinition<T> reference = getReference(); 265 if (reference instanceof CriterionDefinitionAwareElementDefinition criterionDefinitionAwareReference) 266 { 267 ThreadSafeComponentManager<Enumerator> enumeratorManager = new ThreadSafeComponentManager<>(); 268 try 269 { 270 enumeratorManager.setLogger(_logger); 271 enumeratorManager.contextualize(__context); 272 enumeratorManager.service(__serviceManager); 273 274 Configuration configuration = _getEnumeratorAndWidgetParamConf(); 275 return criterionDefinitionAwareReference.getDefaultCriterionEnumerator(configuration, enumeratorManager); 276 } 277 catch (ConfigurationException e) 278 { 279 // An error occurs while trying to configure the criterion enumerator of the reference => use the classic enumerator if exists 280 _logger.error("Unable to use enumerator for criterion of the reference '" + criterionDefinitionAwareReference.getName() + "'", e); 281 return reference.getEnumerator(); 282 } 283 finally 284 { 285 enumeratorManager.dispose(); 286 enumeratorManager = null; 287 } 288 } 289 else 290 { 291 return reference.getEnumerator(); 292 } 293 } 294 295 private Configuration _getEnumeratorAndWidgetParamConf() throws ConfigurationException 296 { 297 Set<String> contentTypeIds = new HashSet<>(); 298 for (String contentTypeId : getContentTypeIds(new HashMap<>())) 299 { 300 contentTypeIds.add(contentTypeId); 301 contentTypeIds.addAll(_getContentTypeExtensionPoint().getSubTypes(contentTypeId)); 302 } 303 Set<ContentType> contentTypes = contentTypeIds.stream() 304 .map(_getContentTypeExtensionPoint()::getExtension) 305 .collect(Collectors.toSet()); 306 307 String referencePath = getReferencePath(); 308 if (referencePath.contains(ModelItem.ITEM_PATH_SEPARATOR)) 309 { 310 String referencePathPrefix = StringUtils.substringBeforeLast(referencePath, ModelItem.ITEM_PATH_SEPARATOR); 311 ContentElementDefinition contentElementDefinition = (ContentElementDefinition) ModelHelper.getModelItem(referencePathPrefix, contentTypes); 312 String contentTypeId = contentElementDefinition.getContentTypeId(); 313 contentTypes = Optional.ofNullable(contentTypeId) 314 .filter(StringUtils::isNotEmpty) 315 .map(id -> _getContentTypeExtensionPoint().getExtension(contentTypeId)) 316 .map(contentType -> Set.of(contentType)) 317 .orElseGet(() -> Set.of()); 318 } 319 320 Configuration wrap = _getCriterionDefinitionHelper().wrapCriterionConfiguration(_getRootCriterionConfiguration(), contentTypes); 321 322 return wrap; 323 } 324 325 /** 326 * Retrieves the root criterion configuration 327 * Used to wrap it for enumerator and widget parameters 328 * @return the root criterion configuration 329 */ 330 protected Configuration _getRootCriterionConfiguration() 331 { 332 return new DefaultConfiguration("criterion"); 333 } 334 335 @SuppressWarnings("unchecked") 336 @Override 337 public Object convertQueryValue(Object value, Map<String, Object> contextualParameters) 338 { 339 return getReference() instanceof CriterionDefinitionAwareElementDefinition criterionDefinitionAwareReference 340 ? criterionDefinitionAwareReference.convertQueryValue(value, getReferencePath(), contextualParameters) 341 : super.convertQueryValue(value, contextualParameters); 342 } 343 344 @SuppressWarnings("unchecked") 345 @Override 346 public Query getQuery(Object value, Operator operator, Map<String, Object> allValues, String language, Map<String, Object> contextualParameters) 347 { 348 if (operator != Operator.EXISTS && _getCriterionDefinitionHelper().isQueryValueEmpty(value)) 349 { 350 return null; 351 } 352 353 ElementDefinition<T> reference = getReference(); 354 Query query = null; 355 if (reference instanceof CriterionDefinitionAwareElementDefinition criterionDefinitionAwareReference) 356 { 357 // If value wanted is none, create query that search for contents with no value for the attribute 358 if (NONE_VALUE.equals(value)) 359 { 360 query = new NotQuery(criterionDefinitionAwareReference.getQuery(value, Operator.EXISTS, language, contextualParameters)); 361 } 362 else 363 { 364 query = criterionDefinitionAwareReference.getQuery(value, operator, language, contextualParameters); 365 } 366 } 367 else 368 { 369 boolean isMultipleOperandAnd = LogicalOperator.AND.equals(getMultipleOperandOperator()); 370 CMSDataContext context = getQueryContext(language, contextualParameters); 371 372 IndexableElementType referenceType = (IndexableElementType) reference.getType(); 373 String queryFieldPath = _getContentSearchHelper().computeQueryFieldPath(reference); 374 375 // If value wanted is none, create query that search for contents with no value for the attribute 376 if (NONE_VALUE.equals(value)) 377 { 378 query = new NotQuery(referenceType.getDefaultQuery(value, queryFieldPath, Operator.EXISTS, isMultipleOperandAnd, context)); 379 } 380 else 381 { 382 query = referenceType.getDefaultQuery(value, queryFieldPath, operator, isMultipleOperandAnd, context); 383 } 384 } 385 386 List<String> queryJoinPaths = getJoinedPaths(contextualParameters); 387 if (query != null && !queryJoinPaths.isEmpty()) 388 { 389 query = new JoinQuery(query, queryJoinPaths); 390 } 391 392 return query; 393 } 394 395 @Override 396 public String getSolrFacetFieldName(Map<String, Object> contextualParameters) 397 { 398 String solrFacetFieldName = super.getSolrFacetFieldName(contextualParameters); 399 400 if (solrFacetFieldName == null && _isFacetable()) 401 { 402 Set<String> contentTypeIds = getContentTypeIds(contextualParameters); 403 JoinedPaths joinedPaths = _getContentSearchHelper().computeJoinedPaths(getReferencePath(), contentTypeIds); 404 String fieldNameSuffix = _getFacetFieldNameSuffix(); 405 solrFacetFieldName = joinedPaths.lastSegmentPrefix() + fieldNameSuffix; 406 setSolrFacetFieldName(solrFacetFieldName); 407 } 408 409 return solrFacetFieldName; 410 } 411 412 /** 413 * Determine if the current criterion definition can be used as facet 414 * @return <code>true</code> if the current criterion definition can be used as facet, <code>false</code> otherwise 415 */ 416 protected boolean _isFacetable() 417 { 418 ElementDefinition<T> reference = getReference(); 419 return reference instanceof IndexationAwareElementDefinition indexationAwareElementDefinition 420 ? indexationAwareElementDefinition.isFacetable() 421 : reference instanceof Property 422 ? false 423 : ((IndexableElementType) reference.getType()).isFacetable(DataContext.newInstance() 424 .withModelItem(this)); 425 } 426 427 /** 428 * Retrieves the suffix of the facet field name 429 * @return the suffix of the facet field name 430 */ 431 protected String _getFacetFieldNameSuffix() 432 { 433 ElementDefinition<T> reference = getReference(); 434 return reference instanceof IndexationAwareElementDefinition indexationAwareElementDefinition 435 ? indexationAwareElementDefinition.getSolrFacetFieldName() 436 : reference instanceof Property 437 ? null 438 : reference.getName() + ((IndexableElementType) reference.getType()).getFacetFieldSuffix(DataContext.newInstance() 439 .withModelItem(reference)); 440 } 441 442 @Override 443 public List<String> getJoinedPaths(Map<String, Object> contextualParameters) 444 { 445 List<String> joinedPaths = super.getJoinedPaths(contextualParameters); 446 447 if (joinedPaths.isEmpty()) 448 { 449 Set<String> contentTypeIds = getContentTypeIds(contextualParameters); 450 JoinedPaths computedJoinedPaths = _getContentSearchHelper().computeJoinedPaths(getReferencePath(), contentTypeIds); 451 joinedPaths = computedJoinedPaths.joinedPaths(); 452 setJoinedPaths(joinedPaths); 453 } 454 455 return joinedPaths; 456 } 457 458 /** 459 * Retrieves The content type extension point 460 * @return The content type extension point 461 */ 462 protected ContentTypeExtensionPoint _getContentTypeExtensionPoint() 463 { 464 if (_contentTypeExtensionPoint == null) 465 { 466 try 467 { 468 _contentTypeExtensionPoint = (ContentTypeExtensionPoint) __serviceManager.lookup(ContentTypeExtensionPoint.ROLE); 469 } 470 catch (ServiceException e) 471 { 472 throw new RuntimeException("Unable to lookup after the content type extension point", e); 473 } 474 } 475 476 return _contentTypeExtensionPoint; 477 } 478 479 /** 480 * Retrieves The extension point containing all available criterion types 481 * @return The extension point containing all available criterion types 482 */ 483 protected ModelItemTypeExtensionPoint _getCriterionTypeExtensionPoint() 484 { 485 if (_criterionTypeExtensionPoint == null) 486 { 487 try 488 { 489 _criterionTypeExtensionPoint = (ModelItemTypeExtensionPoint) __serviceManager.lookup(ModelItemTypeExtensionPoint.ROLE_CRITERION_DEFINITION); 490 } 491 catch (ServiceException e) 492 { 493 throw new RuntimeException("Unable to lookup after the extension point containing all available criterion types", e); 494 } 495 } 496 497 return _criterionTypeExtensionPoint; 498 } 499 500 /** 501 * Retrieves the content search helper 502 * @return the content search helper 503 */ 504 protected ContentSearchHelper _getContentSearchHelper() 505 { 506 if (_contentSearchHelper == null) 507 { 508 try 509 { 510 _contentSearchHelper = (ContentSearchHelper) __serviceManager.lookup(ContentSearchHelper.ROLE); 511 } 512 catch (ServiceException e) 513 { 514 throw new RuntimeException("Unable to lookup after the content search helper", e); 515 } 516 } 517 518 return _contentSearchHelper; 519 } 520}