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 314 contentTypes = new HashSet<>(); 315 if (contentTypeId != null) 316 { 317 contentTypes.add(_getContentTypeExtensionPoint().getExtension(contentTypeId)); 318 _getContentTypeExtensionPoint().getSubTypes(contentTypeId) 319 .stream() 320 .map(_getContentTypeExtensionPoint()::getExtension) 321 .forEach(contentTypes::add); 322 } 323 } 324 325 Configuration wrap = _getCriterionDefinitionHelper().wrapCriterionConfiguration(_getRootCriterionConfiguration(), contentTypes); 326 327 return wrap; 328 } 329 330 /** 331 * Retrieves the root criterion configuration 332 * Used to wrap it for enumerator and widget parameters 333 * @return the root criterion configuration 334 */ 335 protected Configuration _getRootCriterionConfiguration() 336 { 337 return new DefaultConfiguration("criterion"); 338 } 339 340 @SuppressWarnings("unchecked") 341 @Override 342 public Object convertQueryValue(Object value, Map<String, Object> contextualParameters) 343 { 344 return getReference() instanceof CriterionDefinitionAwareElementDefinition criterionDefinitionAwareReference 345 ? criterionDefinitionAwareReference.convertQueryValue(value, getReferencePath(), contextualParameters) 346 : super.convertQueryValue(value, contextualParameters); 347 } 348 349 @SuppressWarnings("unchecked") 350 @Override 351 public Query getQuery(Object value, Operator operator, Map<String, Object> allValues, String language, Map<String, Object> contextualParameters) 352 { 353 if (operator != Operator.EXISTS && _getCriterionDefinitionHelper().isQueryValueEmpty(value)) 354 { 355 return null; 356 } 357 358 ElementDefinition<T> reference = getReference(); 359 Query query = null; 360 if (reference instanceof CriterionDefinitionAwareElementDefinition criterionDefinitionAwareReference) 361 { 362 // If value wanted is none, create query that search for contents with no value for the attribute 363 if (NONE_VALUE.equals(value)) 364 { 365 query = new NotQuery(criterionDefinitionAwareReference.getQuery(value, Operator.EXISTS, language, contextualParameters)); 366 } 367 else 368 { 369 query = criterionDefinitionAwareReference.getQuery(value, operator, language, contextualParameters); 370 } 371 } 372 else 373 { 374 boolean isMultipleOperandAnd = LogicalOperator.AND.equals(getMultipleOperandOperator()); 375 CMSDataContext context = getQueryContext(language, contextualParameters); 376 377 IndexableElementType referenceType = (IndexableElementType) reference.getType(); 378 String queryFieldPath = _getContentSearchHelper().computeQueryFieldPath(reference); 379 380 // If value wanted is none, create query that search for contents with no value for the attribute 381 if (NONE_VALUE.equals(value)) 382 { 383 query = new NotQuery(referenceType.getDefaultQuery(value, queryFieldPath, Operator.EXISTS, isMultipleOperandAnd, context)); 384 } 385 else 386 { 387 query = referenceType.getDefaultQuery(value, queryFieldPath, operator, isMultipleOperandAnd, context); 388 } 389 } 390 391 List<String> queryJoinPaths = getJoinedPaths(contextualParameters); 392 if (query != null && !queryJoinPaths.isEmpty()) 393 { 394 query = new JoinQuery(query, queryJoinPaths); 395 } 396 397 return query; 398 } 399 400 @Override 401 public String getSolrFacetFieldName(Map<String, Object> contextualParameters) 402 { 403 String solrFacetFieldName = super.getSolrFacetFieldName(contextualParameters); 404 405 if (solrFacetFieldName == null && _isFacetable()) 406 { 407 Set<String> contentTypeIds = getContentTypeIds(contextualParameters); 408 JoinedPaths joinedPaths = _getContentSearchHelper().computeJoinedPaths(getReferencePath(), contentTypeIds); 409 String fieldNameSuffix = _getFacetFieldNameSuffix(); 410 solrFacetFieldName = joinedPaths.lastSegmentPrefix() + fieldNameSuffix; 411 setSolrFacetFieldName(solrFacetFieldName); 412 } 413 414 return solrFacetFieldName; 415 } 416 417 /** 418 * Determine if the current criterion definition can be used as facet 419 * @return <code>true</code> if the current criterion definition can be used as facet, <code>false</code> otherwise 420 */ 421 protected boolean _isFacetable() 422 { 423 ElementDefinition<T> reference = getReference(); 424 return reference instanceof IndexationAwareElementDefinition indexationAwareElementDefinition 425 ? indexationAwareElementDefinition.isFacetable() 426 : reference instanceof Property 427 ? false 428 : ((IndexableElementType) reference.getType()).isFacetable(DataContext.newInstance() 429 .withModelItem(this)); 430 } 431 432 /** 433 * Retrieves the suffix of the facet field name 434 * @return the suffix of the facet field name 435 */ 436 protected String _getFacetFieldNameSuffix() 437 { 438 ElementDefinition<T> reference = getReference(); 439 return reference instanceof IndexationAwareElementDefinition indexationAwareElementDefinition 440 ? indexationAwareElementDefinition.getSolrFacetFieldName() 441 : reference instanceof Property 442 ? null 443 : reference.getName() + ((IndexableElementType) reference.getType()).getFacetFieldSuffix(DataContext.newInstance() 444 .withModelItem(reference)); 445 } 446 447 @Override 448 public List<String> getJoinedPaths(Map<String, Object> contextualParameters) 449 { 450 List<String> joinedPaths = super.getJoinedPaths(contextualParameters); 451 452 if (joinedPaths.isEmpty()) 453 { 454 Set<String> contentTypeIds = getContentTypeIds(contextualParameters); 455 JoinedPaths computedJoinedPaths = _getContentSearchHelper().computeJoinedPaths(getReferencePath(), contentTypeIds); 456 joinedPaths = computedJoinedPaths.joinedPaths(); 457 setJoinedPaths(joinedPaths); 458 } 459 460 return joinedPaths; 461 } 462 463 /** 464 * Retrieves The content type extension point 465 * @return The content type extension point 466 */ 467 protected ContentTypeExtensionPoint _getContentTypeExtensionPoint() 468 { 469 if (_contentTypeExtensionPoint == null) 470 { 471 try 472 { 473 _contentTypeExtensionPoint = (ContentTypeExtensionPoint) __serviceManager.lookup(ContentTypeExtensionPoint.ROLE); 474 } 475 catch (ServiceException e) 476 { 477 throw new RuntimeException("Unable to lookup after the content type extension point", e); 478 } 479 } 480 481 return _contentTypeExtensionPoint; 482 } 483 484 /** 485 * Retrieves The extension point containing all available criterion types 486 * @return The extension point containing all available criterion types 487 */ 488 protected ModelItemTypeExtensionPoint _getCriterionTypeExtensionPoint() 489 { 490 if (_criterionTypeExtensionPoint == null) 491 { 492 try 493 { 494 _criterionTypeExtensionPoint = (ModelItemTypeExtensionPoint) __serviceManager.lookup(ModelItemTypeExtensionPoint.ROLE_CRITERION_DEFINITION); 495 } 496 catch (ServiceException e) 497 { 498 throw new RuntimeException("Unable to lookup after the extension point containing all available criterion types", e); 499 } 500 } 501 502 return _criterionTypeExtensionPoint; 503 } 504 505 /** 506 * Retrieves the content search helper 507 * @return the content search helper 508 */ 509 protected ContentSearchHelper _getContentSearchHelper() 510 { 511 if (_contentSearchHelper == null) 512 { 513 try 514 { 515 _contentSearchHelper = (ContentSearchHelper) __serviceManager.lookup(ContentSearchHelper.ROLE); 516 } 517 catch (ServiceException e) 518 { 519 throw new RuntimeException("Unable to lookup after the content search helper", e); 520 } 521 } 522 523 return _contentSearchHelper; 524 } 525}