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 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 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 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 302 /** 303 * Set faceted criteria to the given search model from the given reference paths 304 * If there is no reference path, the faceted criteria of the model are not changed 305 * @param model the search model 306 * @param referencePaths the reference paths 307 * @param contextualParameters the contextual parameters 308 */ 309 @SuppressWarnings("unchecked") 310 public void setFacetedCriteria(DefaultSearchModel model, Collection<String> referencePaths, Map<String, Object> contextualParameters) 311 { 312 if (referencePaths != null && !referencePaths.isEmpty()) 313 { 314 ViewItemContainer originalFacetedCriteria = model.getFacetedCriteria(contextualParameters); 315 model.setFacetedCriteria(new View()); 316 317 for (String referencePath : referencePaths) 318 { 319 SearchModelCriterionDefinition referenceCriterion = _findCriterionByReferencePath(originalFacetedCriteria, referencePath); 320 if (referenceCriterion != null) 321 { 322 model.addFacetedCriterion(referenceCriterion, contextualParameters); 323 } 324 else 325 { 326 SearchModelCriterionDefinition criterion = _searchModelCriterionDefinitionHelper.createReferencingCriterionDefinition(model, referencePath, model.getContentTypes(contextualParameters)); 327 if (criterion != null && criterion.getSolrFacetFieldName(contextualParameters) != null) 328 { 329 model.addFacetedCriterion(criterion, contextualParameters); 330 } 331 else 332 { 333 getLogger().warn("The declared facet '{}' is not facetable. Thus, it will not be added to the facets.", referencePath); 334 } 335 } 336 } 337 338 } 339 } 340 341 private SearchModelCriterionDefinition _findCriterionByReferencePath(ViewItemContainer viewItemContainer, String referencePath) 342 { 343 for (ViewItem viewItem : viewItemContainer.getViewItems()) 344 { 345 if (viewItem instanceof ModelViewItem modelViewItem 346 && modelViewItem.getDefinition() instanceof ReferencingSearchModelCriterionDefinition criterion 347 && criterion.getReferencePath().equals(referencePath)) 348 { 349 return criterion; 350 } 351 352 if (viewItem instanceof ViewItemContainer newViewItemContainer) 353 { 354 return _findCriterionByReferencePath(newViewItemContainer, referencePath); 355 } 356 } 357 return null; 358 } 359 360 /** 361 * Get the language. 362 * @param model the search model 363 * @param searchMode The search mode (advanced or simple) 364 * @param values The user values. 365 * @param contextualParameters The search contextual parameters. 366 * @return the query language. 367 */ 368 public String getCriteriaLanguage(SearchModel model, String searchMode, Map<String, Object> values, Map<String, Object> contextualParameters) 369 { 370 ViewItemContainer criteria = "advanced".equals(searchMode) && model instanceof SearchUIModel uiModel 371 ? uiModel.getAdvancedCriteria(contextualParameters) 372 : model.getCriteria(contextualParameters); 373 374 // First search language in criteria 375 String langValue = _getLanguageFromContentLanguageCriterion(criteria, values, contextualParameters); 376 377 if (StringUtils.isEmpty(langValue)) 378 { 379 // If empty, get language from the search contextual parameters (for instance, sent by the tool). 380 langValue = (String) contextualParameters.get("language"); 381 } 382 383 if (StringUtils.isEmpty(langValue)) 384 { 385 // If no language found: fall back to default. 386 langValue = _getDefaultLanguage(); 387 } 388 389 return langValue; 390 } 391 392 private String _getLanguageFromContentLanguageCriterion(ViewItemAccessor criteria, Map<String, Object> values, Map<String, Object> contextualParameters) 393 { 394 for (ViewItem viewItem : criteria.getViewItems()) 395 { 396 if (viewItem instanceof ModelViewItem modelViewItem 397 && modelViewItem.getDefinition() instanceof LanguageAwareCriterionDefinition criterion) 398 { 399 Object value = modelViewItem instanceof SearchModelCriterionViewItem criterionViewItem && criterionViewItem.isHidden() 400 ? criterion.getDefaultValue() 401 : values.get(criterion.getName()); 402 403 return criterion.getLanguage(value, values, contextualParameters); 404 } 405 else if (viewItem instanceof ViewItemContainer itemContainer) 406 { 407 return _getLanguageFromContentLanguageCriterion(itemContainer, values, contextualParameters); 408 } 409 } 410 411 return null; 412 } 413 414 private String _getDefaultLanguage() 415 { 416 Map<String, Language> availableLanguages = _languagesManager.getAvailableLanguages(); 417 if (availableLanguages.containsKey(DEFAULT_LANGUAGE)) 418 { 419 return DEFAULT_LANGUAGE; 420 } 421 422 return availableLanguages.size() > 0 ? availableLanguages.keySet().iterator().next() : DEFAULT_LANGUAGE; 423 } 424 425 /** 426 * Create a content type or mixin query. 427 * @param contentTypes the content types or mixins to search on. 428 * @return the content type {@link Query}. 429 */ 430 public Query createContentTypeOrMixinQuery(Collection<String> contentTypes) 431 { 432 return createContentTypeOrMixinQuery(contentTypes, Operator.EQ); 433 } 434 435 /** 436 * Create a content type or mixin query. 437 * @param contentTypes the content types or mixins to search on. 438 * @param operator The operator to use in created query 439 * @return the content type {@link Query}. 440 */ 441 public Query createContentTypeOrMixinQuery(Collection<String> contentTypes, Operator operator) 442 { 443 if (contentTypes == null || contentTypes.isEmpty()) // empty and non-empty model contentTypes 444 { 445 return null; 446 } 447 448 List<String> onlyMixins = contentTypes.stream() 449 .filter(ct -> _contentTypeExtensionPoint.getExtension(ct).isMixin()) 450 .toList(); 451 List<String> onlyContentTypes = contentTypes.stream() 452 .filter(ct -> !_contentTypeExtensionPoint.getExtension(ct).isMixin()) 453 .toList(); 454 if (onlyMixins.isEmpty()) 455 { 456 return new ContentTypeQuery(operator, contentTypes); 457 } 458 else if (onlyContentTypes.isEmpty()) 459 { 460 return new MixinTypeQuery(operator, contentTypes); 461 } 462 else 463 { 464 return new OrQuery(new ContentTypeQuery(operator, onlyContentTypes), new MixinTypeQuery(operator, onlyMixins)); 465 } 466 } 467 468 /** 469 * Retrieves the criterion with the given name 470 * @param criteria the criteria 471 * @param criterionName the name of the searched criterion 472 * @return the criterion with the given name, or <code>null</code> if no corresponding criterion has been found 473 */ 474 public static ModelViewItem getCriterion(ViewItemContainer criteria, String criterionName) 475 { 476 for (ViewItem viewItem : criteria.getViewItems()) 477 { 478 if (viewItem instanceof ModelViewItem modelViewItem 479 && modelViewItem.getDefinition() instanceof CriterionDefinition criterion 480 && criterionName.equals(criterion.getName())) 481 { 482 return modelViewItem; 483 } 484 485 if (viewItem instanceof ViewItemContainer itemContainer) 486 { 487 ModelViewItem criterion = getCriterion(itemContainer, criterionName); 488 if (criterion != null) 489 { 490 return criterion; 491 } 492 } 493 } 494 495 // No corresponding criterion has been found 496 return null; 497 } 498}