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.model; 017 018import java.util.Collection; 019import java.util.HashSet; 020import java.util.List; 021import java.util.Map; 022import java.util.Optional; 023import java.util.Set; 024 025import org.apache.avalon.framework.component.Component; 026import org.apache.avalon.framework.configuration.Configuration; 027import org.apache.avalon.framework.context.Context; 028import org.apache.avalon.framework.context.ContextException; 029import org.apache.avalon.framework.context.Contextualizable; 030import org.apache.avalon.framework.service.ServiceException; 031import org.apache.avalon.framework.service.ServiceManager; 032import org.apache.avalon.framework.service.Serviceable; 033import org.apache.cocoon.ProcessingException; 034import org.apache.commons.lang3.StringUtils; 035 036import org.ametys.cms.contenttype.ContentTypeEnumerator; 037import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 038import org.ametys.cms.contenttype.ContentTypesHelper; 039import org.ametys.cms.languages.Language; 040import org.ametys.cms.languages.LanguagesManager; 041import org.ametys.cms.search.model.impl.ContentTypesAwareReferencingCriterionDefinition; 042import org.ametys.cms.search.model.impl.ReferencingSearchModelCriterionDefinition; 043import org.ametys.cms.search.model.impl.SolrFilterCriterionDefinition; 044import org.ametys.cms.search.query.ContentTypeQuery; 045import org.ametys.cms.search.query.MixinTypeQuery; 046import org.ametys.cms.search.query.OrQuery; 047import org.ametys.cms.search.query.Query; 048import org.ametys.cms.search.query.Query.Operator; 049import org.ametys.cms.search.ui.model.SearchModelCriterionViewItem; 050import org.ametys.cms.search.ui.model.SearchUIModel; 051import org.ametys.cms.search.ui.model.SearchUIModelExtensionPoint; 052import org.ametys.cms.search.ui.model.impl.DefaultSearchUIModel; 053import org.ametys.core.ui.Callable; 054import org.ametys.runtime.model.Enumerator; 055import org.ametys.runtime.model.ModelViewItem; 056import org.ametys.runtime.model.View; 057import org.ametys.runtime.model.ViewItem; 058import org.ametys.runtime.model.ViewItemAccessor; 059import org.ametys.runtime.model.ViewItemContainer; 060import org.ametys.runtime.plugin.component.AbstractLogEnabled; 061import org.ametys.runtime.plugin.component.ThreadSafeComponentManager; 062 063/** 064 * Helper for {@link SearchModel}. 065 */ 066public class SearchModelHelper extends AbstractLogEnabled implements Component, Contextualizable, Serviceable 067{ 068 /** The component role. */ 069 public static final String ROLE = SearchModelHelper.class.getName(); 070 071 /** The query default language. */ 072 public static final String DEFAULT_LANGUAGE = "en"; 073 074 private static final String __SOLR_REQUEST_PARAMETER_NAME = "solrRequest"; 075 private static final String __RESTRICTED_CONTENT_TYPE_PARAMETER_NAME = "restrictedContentType"; 076 private static final String __SOLR_REQUEST_CRITERION_ID = "solr-filter-criterion"; 077 078 private Context _context; 079 private ServiceManager _manager; 080 081 private SearchUIModelExtensionPoint _searchUIModelExtensionPoint; 082 private ContentTypeExtensionPoint _contentTypeExtensionPoint; 083 private ContentTypesHelper _contentTypesHelper; 084 private SearchModelCriterionDefinitionHelper _searchModelCriterionDefinitionHelper; 085 private LanguagesManager _languagesManager; 086 087 public void contextualize(Context context) throws ContextException 088 { 089 _context = context; 090 } 091 092 @Override 093 public void service(ServiceManager manager) throws ServiceException 094 { 095 _manager = manager; 096 097 _searchUIModelExtensionPoint = (SearchUIModelExtensionPoint) manager.lookup(SearchUIModelExtensionPoint.ROLE); 098 _contentTypeExtensionPoint = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE); 099 _contentTypesHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE); 100 _searchModelCriterionDefinitionHelper = (SearchModelCriterionDefinitionHelper) manager.lookup(SearchModelCriterionDefinitionHelper.ROLE); 101 _languagesManager = (LanguagesManager) manager.lookup(LanguagesManager.ROLE); 102 } 103 104 /** 105 * Get the search model configuration as JSON object 106 * @param modelId The id of search model 107 * @param contextualParameters the contextual parameters 108 * @return The search model configuration in a Map 109 * @throws ProcessingException if an error occurred 110 */ 111 @Callable(rights = Callable.NO_CHECK_REQUIRED) // Search model definition are public 112 public Map<String, Object> getSearchModelConfiguration(String modelId, Map<String, Object> contextualParameters) throws ProcessingException 113 { 114 SearchUIModel model = getSearchUIModel(modelId, contextualParameters); 115 return model.toJSON(contextualParameters); 116 } 117 118 /** 119 * Get the search model configuration as JSON object. 120 * Add some restrictions on the search model, due to the given contextual parameters (content types / solr request) 121 * @param modelId The id of search model 122 * @param contextualParameters the contextual parameters 123 * @return The search model configuration in a Map 124 * @throws ProcessingException if an error occurred while converting the model to JSON 125 */ 126 @Callable(rights = Callable.NO_CHECK_REQUIRED) // Search model definition are public 127 public Map<String, Object> getRestrictedSearchModelConfiguration(String modelId, Map<String, Object> contextualParameters) throws ProcessingException 128 { 129 SearchModel model = getRestrictedSearchModel(modelId, contextualParameters); 130 return model.toJSON(contextualParameters); 131 } 132 133 /** 134 * Get the search model with the given identifier 135 * Add some restrictions on the search model, due to the given contextual parameters (content types / solr request) 136 * @param modelId The id of search model 137 * @param contextualParameters the contextual parameters 138 * @return The search model 139 */ 140 public SearchModel getRestrictedSearchModel(String modelId, Map<String, Object> contextualParameters) 141 { 142 SearchUIModel model = getSearchUIModel(modelId, contextualParameters); 143 144 // Copy is needed because some modifications will be made on the search model 145 DefaultSearchModel copy = copySearchModel(model, contextualParameters); 146 147 String restrictedContentTypeId = (String) contextualParameters.get(__RESTRICTED_CONTENT_TYPE_PARAMETER_NAME); 148 if (StringUtils.isNotEmpty(restrictedContentTypeId)) 149 { 150 addContentTypeRestrictions(copy, restrictedContentTypeId, contextualParameters); 151 } 152 153 String solrRequest = (String) contextualParameters.get(__SOLR_REQUEST_PARAMETER_NAME); 154 if (StringUtils.isNotEmpty(solrRequest)) 155 { 156 addSolrFilterCriterion(copy, solrRequest, contextualParameters); 157 } 158 159 return copy; 160 } 161 162 /** 163 * Get the column configurations of search model as JSON object 164 * @param modelId The id of search model 165 * @param contextualParameters the contextual parameters 166 * @return The column configurations in a List 167 * @throws ProcessingException if an error occurred 168 */ 169 @Callable(rights = Callable.NO_CHECK_REQUIRED) // Search model definition are public 170 public List<Object> getColumnConfigurations(String modelId, Map<String, Object> contextualParameters) throws ProcessingException 171 { 172 SearchUIModel model = getSearchUIModel(modelId, contextualParameters); 173 return model.resultItemsToJSON(contextualParameters); 174 } 175 176 /** 177 * Retrieves the {@link SearchUIModel} with the given model identifier, with restrictions on content types 178 * @param modelId the model identifier 179 * @param contextualParameters the contextual parameters 180 * @return the {@link SearchUIModel} 181 */ 182 public SearchUIModel getSearchUIModel(String modelId, Map<String, Object> contextualParameters) 183 { 184 return Optional.ofNullable(_searchUIModelExtensionPoint.getExtension(modelId)) 185 .orElseThrow(() -> new IllegalArgumentException("The search model '" + modelId + "' does not exist")); 186 } 187 188 /** 189 * Copy the given search model 190 * @param model the model to copy 191 * @param contextualParameters the contextual parameters 192 * @return the copy of the model 193 */ 194 public DefaultSearchModel copySearchModel(SearchModel model, Map<String, Object> contextualParameters) 195 { 196 return model instanceof SearchUIModel uiModel 197 ? new DefaultSearchUIModel(uiModel, contextualParameters) 198 : new DefaultSearchModel(model, contextualParameters); 199 } 200 201 /** 202 * Add content type restrictions on the given search model 203 * Add content types to the model and restrict enumerator values of its content types criteria 204 * @param model the model 205 * @param restrictedContentTypeId the selected content type identifier 206 * @param contextualParameters the contextual parameters 207 */ 208 public void addContentTypeRestrictions(DefaultSearchModel model, String restrictedContentTypeId, Map<String, Object> contextualParameters) 209 { 210 model.setContentTypes(Set.of(restrictedContentTypeId)); 211 212 ViewItemContainer criteria = model.getCriteria(contextualParameters); 213 Set<String> restrictedContentTypeIds = _getRestrictedContentTypeIds(restrictedContentTypeId); 214 _addContentTypeRestrictions(criteria, restrictedContentTypeIds, contextualParameters); 215 } 216 217 private Set<String> _getRestrictedContentTypeIds(String restrictedContentTypeId) 218 { 219 Set<String> restrictedContentTypeIds = new HashSet<>(); 220 221 restrictedContentTypeIds.add(restrictedContentTypeId); 222 for (String subContentTypeId : _contentTypeExtensionPoint.getSubTypes(restrictedContentTypeId)) 223 { 224 restrictedContentTypeIds.addAll(_getRestrictedContentTypeIds(subContentTypeId)); 225 } 226 227 return restrictedContentTypeIds; 228 } 229 230 @SuppressWarnings("unchecked") 231 private void _addContentTypeRestrictions(ViewItemContainer criteria, Set<String> restrictedContentTypeIds, Map<String, Object> contextualParameters) 232 { 233 for (ViewItem viewItem : criteria.getViewItems()) 234 { 235 if (viewItem instanceof ModelViewItem modelViewItem 236 && modelViewItem.getDefinition() instanceof ContentTypesAwareCriterionDefinition criterion) 237 { 238 Enumerator<String> enumerator = _getContentTypesEnumerator(restrictedContentTypeIds.toArray(String[]::new), criterion.includeContentTypes(), criterion.includeMixins()); 239 if (enumerator != null) 240 { 241 SearchModelCriterionDefinition<String> criterionCopy = new ContentTypesAwareReferencingCriterionDefinition(); 242 criterion.copyTo(criterionCopy, contextualParameters); 243 244 criterionCopy.setEnumerator(enumerator); 245 modelViewItem.setDefinition(criterionCopy); 246 } 247 } 248 249 if (viewItem instanceof ViewItemContainer viewItemContainer) 250 { 251 _addContentTypeRestrictions(viewItemContainer, restrictedContentTypeIds, contextualParameters); 252 } 253 } 254 } 255 256 private Enumerator<String> _getContentTypesEnumerator(String[] contentTypeIds, boolean includeContentTypes, boolean incudeMixins) 257 { 258 ThreadSafeComponentManager<Enumerator> enumeratorManager = new ThreadSafeComponentManager<>(); 259 try 260 { 261 enumeratorManager.setLogger(getLogger()); 262 enumeratorManager.contextualize(_context); 263 enumeratorManager.service(_manager); 264 265 String role = "enumerator"; 266 Configuration configuration = _contentTypesHelper.getContentTypesEnumeratorConfiguration(contentTypeIds, includeContentTypes, incudeMixins); 267 enumeratorManager.addComponent("cms", null, role, ContentTypeEnumerator.class, configuration); 268 269 enumeratorManager.initialize(); 270 return enumeratorManager.lookup(role); 271 } 272 catch (Exception e) 273 { 274 getLogger().error("Unable to create a content types enumerator on types '" + StringUtils.join(contentTypeIds, ",") + "'", e); 275 return null; 276 } 277 finally 278 { 279 enumeratorManager.dispose(); 280 enumeratorManager = null; 281 } 282 } 283 284 /** 285 * Add a solr filter criterion to the given model 286 * @param model the model 287 * @param solrRequest the solr request 288 * @param contextualParameters the contextual parameters 289 */ 290 public void addSolrFilterCriterion(SearchModel model, String solrRequest, Map<String, Object> contextualParameters) 291 { 292 // Create a criterion with solr request 293 SolrFilterCriterionDefinition criterion = new SolrFilterCriterionDefinition(); 294 criterion.setName(__SOLR_REQUEST_CRITERION_ID); 295 criterion.setQuery(solrRequest); 296 criterion.setModel(model); 297 298 // Add the criterion to the search model 299 model.addCriterion(criterion, contextualParameters); 300 301 // Hide the criterion 302 ModelViewItem viewItem = model.getCriterion(__SOLR_REQUEST_CRITERION_ID, contextualParameters); 303 if (viewItem instanceof SearchModelCriterionViewItem criterionViewItem) 304 { 305 criterionViewItem.setHidden(true); 306 } 307 } 308 309 /** 310 * Set faceted criteria to the given search model from the given reference paths 311 * If there is no reference path, the faceted criteria of the model are not changed 312 * @param model the search model 313 * @param referencePaths the reference paths 314 * @param contextualParameters the contextual parameters 315 */ 316 @SuppressWarnings("unchecked") 317 public void setFacetedCriteria(DefaultSearchModel model, Collection<String> referencePaths, Map<String, Object> contextualParameters) 318 { 319 if (referencePaths != null && !referencePaths.isEmpty()) 320 { 321 ViewItemContainer originalFacetedCriteria = model.getFacetedCriteria(contextualParameters); 322 model.setFacetedCriteria(new View()); 323 324 for (String referencePath : referencePaths) 325 { 326 SearchModelCriterionDefinition referenceCriterion = _findCriterionByReferencePath(originalFacetedCriteria, referencePath); 327 if (referenceCriterion != null) 328 { 329 model.addFacetedCriterion(referenceCriterion, contextualParameters); 330 } 331 else 332 { 333 SearchModelCriterionDefinition criterion = _searchModelCriterionDefinitionHelper.createReferencingCriterionDefinition(model, referencePath, model.getContentTypes(contextualParameters)); 334 if (criterion != null && criterion.getSolrFacetFieldName(contextualParameters) != null) 335 { 336 model.addFacetedCriterion(criterion, contextualParameters); 337 } 338 else 339 { 340 getLogger().warn("The declared facet '{}' is not facetable. Thus, it will not be added to the facets.", referencePath); 341 } 342 } 343 } 344 345 } 346 } 347 348 private SearchModelCriterionDefinition _findCriterionByReferencePath(ViewItemContainer viewItemContainer, String referencePath) 349 { 350 for (ViewItem viewItem : viewItemContainer.getViewItems()) 351 { 352 if (viewItem instanceof ModelViewItem modelViewItem 353 && modelViewItem.getDefinition() instanceof ReferencingSearchModelCriterionDefinition criterion 354 && criterion.getReferencePath().equals(referencePath)) 355 { 356 return criterion; 357 } 358 359 if (viewItem instanceof ViewItemContainer newViewItemContainer) 360 { 361 return _findCriterionByReferencePath(newViewItemContainer, referencePath); 362 } 363 } 364 return null; 365 } 366 367 /** 368 * Get the language. 369 * @param model the search model 370 * @param searchMode The search mode (advanced or simple) 371 * @param values The user values. 372 * @param contextualParameters The search contextual parameters. 373 * @return the query language. 374 */ 375 public String getCriteriaLanguage(SearchModel model, String searchMode, Map<String, Object> values, Map<String, Object> contextualParameters) 376 { 377 ViewItemContainer criteria = "advanced".equals(searchMode) && model instanceof SearchUIModel uiModel 378 ? uiModel.getAdvancedCriteria(contextualParameters) 379 : model.getCriteria(contextualParameters); 380 381 // First search language in criteria 382 String langValue = _getLanguageFromContentLanguageCriterion(criteria, values, contextualParameters); 383 384 if (StringUtils.isEmpty(langValue)) 385 { 386 // If empty, get language from the search contextual parameters (for instance, sent by the tool). 387 langValue = (String) contextualParameters.get("language"); 388 } 389 390 if (StringUtils.isEmpty(langValue)) 391 { 392 // If no language found: fall back to default. 393 langValue = _getDefaultLanguage(); 394 } 395 396 return langValue; 397 } 398 399 private String _getLanguageFromContentLanguageCriterion(ViewItemAccessor criteria, Map<String, Object> values, Map<String, Object> contextualParameters) 400 { 401 if (criteria != null) 402 { 403 for (ViewItem viewItem : criteria.getViewItems()) 404 { 405 if (viewItem instanceof ModelViewItem modelViewItem 406 && modelViewItem.getDefinition() instanceof LanguageAwareCriterionDefinition criterion) 407 { 408 Object value = modelViewItem instanceof SearchModelCriterionViewItem criterionViewItem && criterionViewItem.isHidden() 409 ? criterion.getDefaultValue() 410 : values.get(criterion.getName()); 411 412 return criterion.getLanguage(value, values, contextualParameters); 413 } 414 else if (viewItem instanceof ViewItemContainer itemContainer) 415 { 416 return _getLanguageFromContentLanguageCriterion(itemContainer, values, contextualParameters); 417 } 418 } 419 } 420 421 return null; 422 } 423 424 private String _getDefaultLanguage() 425 { 426 Map<String, Language> availableLanguages = _languagesManager.getAvailableLanguages(); 427 if (availableLanguages.containsKey(DEFAULT_LANGUAGE)) 428 { 429 return DEFAULT_LANGUAGE; 430 } 431 432 return availableLanguages.size() > 0 ? availableLanguages.keySet().iterator().next() : DEFAULT_LANGUAGE; 433 } 434 435 /** 436 * Create a content type or mixin query. 437 * @param contentTypes the content types or mixins to search on. 438 * @return the content type {@link Query}. 439 */ 440 public Query createContentTypeOrMixinQuery(Collection<String> contentTypes) 441 { 442 return createContentTypeOrMixinQuery(contentTypes, Operator.EQ); 443 } 444 445 /** 446 * Create a content type or mixin query. 447 * @param contentTypes the content types or mixins to search on. 448 * @param operator The operator to use in created query 449 * @return the content type {@link Query}. 450 */ 451 public Query createContentTypeOrMixinQuery(Collection<String> contentTypes, Operator operator) 452 { 453 if (contentTypes == null || contentTypes.isEmpty()) // empty and non-empty model contentTypes 454 { 455 return null; 456 } 457 458 List<String> onlyMixins = contentTypes.stream() 459 .filter(ct -> _contentTypeExtensionPoint.getExtension(ct).isMixin()) 460 .toList(); 461 List<String> onlyContentTypes = contentTypes.stream() 462 .filter(ct -> !_contentTypeExtensionPoint.getExtension(ct).isMixin()) 463 .toList(); 464 if (onlyMixins.isEmpty()) 465 { 466 return new ContentTypeQuery(operator, contentTypes); 467 } 468 else if (onlyContentTypes.isEmpty()) 469 { 470 return new MixinTypeQuery(operator, contentTypes); 471 } 472 else 473 { 474 return new OrQuery(new ContentTypeQuery(operator, onlyContentTypes), new MixinTypeQuery(operator, onlyMixins)); 475 } 476 } 477 478 /** 479 * Retrieves the criterion with the given name 480 * @param criteria the criteria 481 * @param criterionName the name of the searched criterion 482 * @return the criterion with the given name, or <code>null</code> if no corresponding criterion has been found 483 */ 484 public static ModelViewItem getCriterion(ViewItemContainer criteria, String criterionName) 485 { 486 if (criteria != null) 487 { 488 for (ViewItem viewItem : criteria.getViewItems()) 489 { 490 if (viewItem instanceof ModelViewItem modelViewItem 491 && modelViewItem.getDefinition() instanceof CriterionDefinition criterion 492 && criterionName.equals(criterion.getName())) 493 { 494 return modelViewItem; 495 } 496 497 if (viewItem instanceof ViewItemContainer itemContainer) 498 { 499 ModelViewItem criterion = getCriterion(itemContainer, criterionName); 500 if (criterion != null) 501 { 502 return criterion; 503 } 504 } 505 } 506 } 507 508 // No corresponding criterion has been found 509 return null; 510 } 511}