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