001/* 002 * Copyright 2016 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.solr; 017 018import java.util.ArrayList; 019import java.util.Collection; 020import java.util.Collections; 021import java.util.LinkedHashMap; 022import java.util.List; 023import java.util.Map; 024import java.util.Optional; 025import java.util.Set; 026import java.util.stream.Stream; 027 028import org.apache.avalon.framework.configuration.Configuration; 029import org.apache.avalon.framework.configuration.ConfigurationException; 030import org.apache.avalon.framework.configuration.DefaultConfiguration; 031import org.apache.avalon.framework.context.Context; 032import org.apache.avalon.framework.service.ServiceException; 033import org.apache.avalon.framework.service.ServiceManager; 034import org.apache.commons.lang3.StringUtils; 035import org.slf4j.Logger; 036 037import org.ametys.cms.repository.Content; 038import org.ametys.cms.search.model.SystemProperty; 039import org.ametys.cms.search.model.SystemPropertyExtensionPoint; 040import org.ametys.cms.search.query.Query.Operator; 041import org.ametys.cms.search.ui.model.AbstractSearchUIModel; 042import org.ametys.cms.search.ui.model.ColumnHelper.Column; 043import org.ametys.cms.search.ui.model.SearchUIColumn; 044import org.ametys.cms.search.ui.model.SearchUICriterion; 045import org.ametys.cms.search.ui.model.SearchUIModel; 046import org.ametys.cms.search.ui.model.SearchUIModelExtensionPoint; 047import org.ametys.cms.search.ui.model.impl.IndexingFieldSearchUICriterion; 048import org.ametys.cms.search.ui.model.impl.MetadataSearchUIColumn; 049import org.ametys.cms.search.ui.model.impl.SystemSearchUIColumn; 050import org.ametys.cms.search.ui.model.impl.SystemSearchUICriterion; 051import org.ametys.runtime.plugin.component.ThreadSafeComponentManager; 052 053/** 054 * Search model wrapper which handles custom on-the-fly columns and facets. 055 */ 056public class CriteriaSearchUIModelWrapper extends AbstractSearchUIModel 057{ 058 059 /** ComponentManager for {@link SearchUICriterion}s. */ 060 protected ThreadSafeComponentManager<SearchUICriterion> _searchUICriterionManager; 061 062 /** ComponentManager for {@link SearchUIColumn}s. */ 063 protected ThreadSafeComponentManager<SearchUIColumn> _searchUIColumnManager; 064 065 private SearchUIModelExtensionPoint _searchModelEP; 066 private SystemPropertyExtensionPoint _sysPropEP; 067 068 private SearchUIModel _wrappedModel; 069 070 private int _criteriaIndex; 071 072 /** 073 * Build a model wrapper. 074 * @param model the search model to wrap. 075 * @param manager the service manager. 076 * @param context the component context. 077 * @param logger the logger. 078 */ 079 public CriteriaSearchUIModelWrapper(SearchUIModel model, ServiceManager manager, Context context, Logger logger) 080 { 081 _wrappedModel = model; 082 083 _logger = logger; 084 085 try 086 { 087 _searchUICriterionManager = new ThreadSafeComponentManager<>(); 088 _searchUICriterionManager.setLogger(logger); 089 _searchUICriterionManager.contextualize(context); 090 _searchUICriterionManager.service(manager); 091 092 _searchUIColumnManager = new ThreadSafeComponentManager<>(); 093 _searchUIColumnManager.setLogger(logger); 094 _searchUIColumnManager.contextualize(context); 095 _searchUIColumnManager.service(manager); 096 } 097 catch (Exception e) 098 { 099 _logger.error("Error initializing the SearchModel", e); 100 } 101 } 102 103 @Override 104 public void service(ServiceManager manager) throws ServiceException 105 { 106 super.service(manager); 107 _searchModelEP = (SearchUIModelExtensionPoint) manager.lookup(SearchUIModelExtensionPoint.ROLE); 108 _sysPropEP = (SystemPropertyExtensionPoint) manager.lookup(SystemPropertyExtensionPoint.ROLE); 109 } 110 111 /** 112 * Set the custom faceted criteria. 113 * @param baseContentTypeIds the reference content type identifiers. 114 * @param criteriaIds the criteria IDs 115 * @param contextualParameters the contextual parameters 116 * @return the valid criteria IDs 117 * @throws Exception if an error occurs initializing criteria. 118 */ 119 public Collection<String> setFacetedCriteria(Set<String> baseContentTypeIds, Collection<String> criteriaIds, Map<String, Object> contextualParameters) throws Exception 120 { 121 Collection<String> resultCriteriaIds = null; 122 123 if (criteriaIds != null) 124 { 125 _facetedCriteria = new LinkedHashMap<>(criteriaIds.size()); 126 127 List<Object> searchToolCriterionRoles = new ArrayList<>(); 128 129 resultCriteriaIds = configureFacets(searchToolCriterionRoles, baseContentTypeIds, criteriaIds, _wrappedModel, contextualParameters); 130 131 _searchUICriterionManager.initialize(); 132 133 for (Object critObj : searchToolCriterionRoles) 134 { 135 SearchUICriterion criterion = null; 136 if (critObj instanceof SearchUICriterion) 137 { 138 // Already existing SearchUICriterion object (taken from the wrapped model). 139 criterion = (SearchUICriterion) critObj; 140 } 141 else if (critObj instanceof String) 142 { 143 // Criterion just added in the local component manager, we have to look it up. 144 criterion = _searchUICriterionManager.lookup((String) critObj); 145 } 146 147 if (criterion != null && criterion.isFacetable()) 148 { 149 _facetedCriteria.put(criterion.getId(), criterion); 150 } 151 } 152 } 153 154 return resultCriteriaIds != null ? resultCriteriaIds : Collections.emptySet(); 155 } 156 157 /** 158 * Set the custom columns. 159 * @param baseContentTypeIds the reference content type identifiers. 160 * @param columns The columns 161 * @param contextualParameters the contextual parameters 162 * @throws Exception if an error occurs initializing columns. 163 */ 164 public void setResultColumns(Set<String> baseContentTypeIds, Collection<Column> columns, Map<String, Object> contextualParameters) throws Exception 165 { 166 String wrappedModelId = (String) contextualParameters.get("wrappedModelId"); 167 if (StringUtils.isNotEmpty(wrappedModelId)) 168 { 169 // Dashboard 170 SearchUIModel model = _searchModelEP.getExtension(wrappedModelId); 171 _columns = model.getResultFields(contextualParameters); 172 } 173 else if (columns != null) 174 { 175 _columns = new LinkedHashMap<>(columns.size()); 176 177 List<Object> columnRoles = new ArrayList<>(); 178 179 configureColumns(columnRoles, baseContentTypeIds, columns, _wrappedModel, contextualParameters); 180 181 _searchUIColumnManager.initialize(); 182 183 for (Object col : columnRoles) 184 { 185 SearchUIColumn column = null; 186 if (col instanceof SearchUIColumn) 187 { 188 // Already existing SearchUIColumn object (taken from the wrapped model). 189 column = (SearchUIColumn) col; 190 } 191 else if (col instanceof String) 192 { 193 // Column just added in the local component manager, we have to look it up. 194 column = _searchUIColumnManager.lookup((String) col); 195 } 196 197 if (column != null) 198 { 199 _columns.put(column.getId(), column); 200 } 201 } 202 } 203 } 204 205 /** 206 * Configure the list of faceted criteria. 207 * @param criteriaRoles the roles of criteria to lookup (or the already existing SearchUICriterion objects). 208 * @param baseContentTypeIds The reference content type identifiers. 209 * @param criteriaIds the criteria IDs. 210 * @param referenceModel the reference model. 211 * @param contextualParameters the contextual parameters 212 * @return the valid criteria IDs 213 * @throws ConfigurationException if an error occurs creating a component configuration. 214 */ 215 protected Collection<String> configureFacets(List<Object> criteriaRoles, Set<String> baseContentTypeIds, Collection<String> criteriaIds, SearchUIModel referenceModel, Map<String, Object> contextualParameters) throws ConfigurationException 216 { 217 Collection<String> resultCriteriaIds = new ArrayList<>(); 218 219 for (String criterionId : criteriaIds) 220 { 221 String[] facetPathSegments = StringUtils.split(criterionId, '/'); 222 String lastSegmentOfFacetPath = facetPathSegments[facetPathSegments.length - 1]; 223 224 SearchUICriterion referenceCriterion = null; 225 if (referenceModel != null) 226 { 227 referenceCriterion = getCriterion(referenceModel, criterionId, contextualParameters); 228 } 229 230 if (referenceCriterion != null) 231 { 232 criteriaRoles.add(referenceCriterion); 233 } 234 else if (_sysPropEP.hasExtension(lastSegmentOfFacetPath)) 235 { 236 SystemProperty systemProperty = _sysPropEP.getExtension(lastSegmentOfFacetPath); 237 if (systemProperty.isFacetable()) 238 { 239 addSystemCriteriaComponents(criteriaRoles, baseContentTypeIds, criterionId); 240 } 241 else 242 { 243 getLogger().warn("The declared facet '{}' is a system property but is not facetable. Thus, it will not be added to the facets.", criterionId); 244 break; 245 } 246 } 247 else 248 { 249 if (!baseContentTypeIds.isEmpty()) 250 { 251 // Metadata property. 252 addIndexingFieldCriteriaComponents(criteriaRoles, baseContentTypeIds, criterionId); 253 } 254 else if (Content.ATTRIBUTE_TITLE.equals(criterionId)) 255 { 256 // title property of a random ContentType. 257 String firstCTypeId = _cTypeEP.getExtensionsIds().iterator().next(); 258 addIndexingFieldCriteriaComponents(criteriaRoles, Set.of(firstCTypeId), criterionId); 259 } 260 else 261 { 262 break; 263 } 264 } 265 266 resultCriteriaIds.add(criterionId); 267 } 268 269 return resultCriteriaIds; 270 } 271 272 /** 273 * Search a criterion in the reference model from its criterion identifier. 274 * @param searchModel the reference search model. 275 * @param criterionId the criterion identifier. 276 * @param contextualParameters the contextual parameters 277 * @return the criterion if found, null otherwise. 278 */ 279 protected SearchUICriterion getCriterion(SearchUIModel searchModel, String criterionId, Map<String, Object> contextualParameters) 280 { 281 Map<String, SearchUICriterion> criteria = searchModel.getFacetedCriteria(contextualParameters); 282 283 for (SearchUICriterion criterion : criteria.values()) 284 { 285 if (criterion instanceof IndexingFieldSearchUICriterion && ((IndexingFieldSearchUICriterion) criterion).getFieldPath().equals(criterionId)) 286 { 287 return criterion; 288 } 289 else if (criterion instanceof SystemSearchUICriterion && ((SystemSearchUICriterion) criterion).getSystemPropertyId().equals(criterionId)) 290 { 291 return criterion; 292 } 293 else if (criterion.getId().equals(criterionId)) 294 { 295 return criterion; 296 } 297 } 298 299 return null; 300 } 301 302 /** 303 * Configure the list of search columns. 304 * @param columnRoles the roles of columns to lookup (or the already existing SearchUIColumn objects). 305 * @param baseContentTypeIds The reference content type identifiers. 306 * @param columnIds The columns 307 * @param referenceModel the reference model. 308 * @param contextualParameters the contextual parameters 309 * @throws ConfigurationException if an error occurs creating a component configuration. 310 */ 311 protected void configureColumns(List<Object> columnRoles, Set<String> baseContentTypeIds, Collection<Column> columnIds, SearchUIModel referenceModel, Map<String, Object> contextualParameters) throws ConfigurationException 312 { 313 for (Column column : columnIds) 314 { 315 String columnId = column.getId(); 316 Optional<String> columnLabel = column.getLabel(); 317 String[] columnPathSegments = StringUtils.split(columnId, '/'); 318 String lastSegmentOfColumnPath = columnPathSegments[columnPathSegments.length - 1]; 319 320 SearchUIColumn referenceColumn = null; 321 if (referenceModel != null && !columnLabel.isPresent() /* cannot temporary change the label of a reference column on the fly => re-create a component */) 322 { 323 referenceColumn = getColumn(referenceModel, columnId, contextualParameters); 324 } 325 326 if (referenceColumn != null) 327 { 328 columnRoles.add(referenceColumn); 329 } 330 else if (_sysPropEP.isDisplayable(lastSegmentOfColumnPath)) 331 { 332 // System property. 333 addSystemColumnComponent(columnRoles, baseContentTypeIds, columnId, columnLabel); 334 } 335 else if (_sysPropEP.hasExtension(lastSegmentOfColumnPath)) 336 { 337 getLogger().warn("The column '{}' is a system property but is not displayable. Thus, it will not be displayed.", columnId); 338 } 339 else 340 { 341 // Metadata property. 342 if (!baseContentTypeIds.isEmpty()) 343 { 344 addMetadataColumnComponents(columnRoles, baseContentTypeIds, columnId, columnLabel); 345 } 346 else if (Content.ATTRIBUTE_TITLE.equals(columnId)) 347 { 348 // Get the title property of a random ContentType. 349 String firstCTypeId = _cTypeEP.getExtensionsIds().iterator().next(); 350 addMetadataColumnComponents(columnRoles, Set.of(firstCTypeId), columnId, columnLabel); 351 } 352 } 353 } 354 } 355 356 /** 357 * Search a column in the reference model from its column identifier. 358 * @param searchModel the reference search model. 359 * @param columnId the column identifier. 360 * @param contextualParameters the contextual parameters 361 * @return the column if found, null otherwise. 362 */ 363 protected SearchUIColumn getColumn(SearchUIModel searchModel, String columnId, Map<String, Object> contextualParameters) 364 { 365 Map<String, SearchUIColumn> columns = searchModel.getResultFields(contextualParameters); 366 367 for (SearchUIColumn column : columns.values()) 368 { 369 if (column instanceof MetadataSearchUIColumn && ((MetadataSearchUIColumn) column).getFieldPath().equals(columnId)) 370 { 371 return column; 372 } 373 else if (column instanceof SystemSearchUIColumn && ((SystemSearchUIColumn) column).getSystemPropertyId().equals(columnId)) 374 { 375 return column; 376 } 377// else if (column instanceof CustomSearchToolColumn && column.getId().equals(columnId)) 378// { 379// return column; 380// } 381 } 382 383 return null; 384 } 385 386 /** 387 * Add a indexing field criteria component to the manager. 388 * @param searchToolCriterionRoles the criteria role list to fill. 389 * @param baseContentTypeIds the reference content type identifiers. 390 * @param fieldRef the field path. 391 * @throws ConfigurationException if an error occurs. 392 */ 393 protected void addIndexingFieldCriteriaComponents(List<Object> searchToolCriterionRoles, Set<String> baseContentTypeIds, String fieldRef) throws ConfigurationException 394 { 395 try 396 { 397 String slashPath = fieldRef.replace('.', '/'); 398 399 String role = fieldRef + _criteriaIndex; 400 _criteriaIndex++; 401 Configuration criteriaConf = getIndexingFieldCriteriaConfiguration(baseContentTypeIds, slashPath, Operator.EQ); 402 403 _searchUICriterionManager.addComponent("search", null, role, IndexingFieldSearchUICriterion.class, criteriaConf); 404 405 searchToolCriterionRoles.add(role); 406 } 407 catch (Exception e) 408 { 409 throw new ConfigurationException("Unable to instanciate IndexingFieldSearchUICriterion for field " + fieldRef, e); 410 } 411 } 412 413 /** 414 * Add a system criteria component to the manager. 415 * @param searchToolCriterionRoles the criteria role list to fill. 416 * @param baseContentTypeIds the reference content type identifiers. 417 * @param property the system property id. 418 * @throws ConfigurationException if an error occurs. 419 */ 420 protected void addSystemCriteriaComponents(List<Object> searchToolCriterionRoles, Set<String> baseContentTypeIds, String property) throws ConfigurationException 421 { 422 try 423 { 424 String role = property + _criteriaIndex; 425 _criteriaIndex++; 426 427 Configuration criteriaConf = getSystemCriteriaConfiguration(baseContentTypeIds, property); 428 _searchUICriterionManager.addComponent("search", null, role, SystemSearchUICriterion.class, criteriaConf); 429 430 searchToolCriterionRoles.add(role); 431 } 432 catch (Exception e) 433 { 434 throw new ConfigurationException("Unable to instanciate SystemSearchUICriterion for property " + property, e); 435 } 436 } 437 438 private Configuration addLabelInColumnConf(Configuration originalConf, String label) throws ConfigurationException 439 { 440 DefaultConfiguration confWithLabel = new DefaultConfiguration(originalConf); 441 Stream.of(confWithLabel.getChildren("label")).forEach(confWithLabel::removeChild); 442 DefaultConfiguration labelConf = new DefaultConfiguration("label"); 443 labelConf.setValue(label); 444 confWithLabel.addChild(labelConf); 445 return confWithLabel; 446 } 447 448 /** 449 * Add a metadata column component to the manager. 450 * @param columnsRolesToLookup the columns roles 451 * @param baseContentTypeIds the reference content type identifiers. 452 * @param metadataPath the metadata path. 453 * @param columnLabel The (optional) label of the column. If not present, the column label will be the metadata one. 454 * @throws ConfigurationException if an error occurs. 455 */ 456 protected void addMetadataColumnComponents(List<Object> columnsRolesToLookup, Set<String> baseContentTypeIds, String metadataPath, Optional<String> columnLabel) throws ConfigurationException 457 { 458 try 459 { 460 Configuration columnConf = getMetadataColumnConfiguration(baseContentTypeIds, metadataPath); 461 if (columnLabel.isPresent()) 462 { 463 columnConf = addLabelInColumnConf(columnConf, columnLabel.get()); 464 } 465 466 _searchUIColumnManager.addComponent("search", null, metadataPath, MetadataSearchUIColumn.class, columnConf); 467 columnsRolesToLookup.add(metadataPath); 468 } 469 catch (Exception e) 470 { 471 throw new ConfigurationException("Unable to instanciate MetadataSearchUIColumn for metadata " + metadataPath, e); 472 } 473 } 474 475 /** 476 * Add a system column component to the manager. 477 * @param columnsRolesToLookup the columns roles 478 * @param baseContentTypeIds the reference content type identifiers. 479 * @param property the system property. 480 * @param columnLabel The (optional) label of the column. If not present, the column label will be the metadata one. 481 * @throws ConfigurationException if an error occurs. 482 */ 483 protected void addSystemColumnComponent(List<Object> columnsRolesToLookup, Set<String> baseContentTypeIds, String property, Optional<String> columnLabel) throws ConfigurationException 484 { 485 try 486 { 487 Configuration conf = getSystemColumnConfiguration(baseContentTypeIds, property); 488 if (columnLabel.isPresent()) 489 { 490 conf = addLabelInColumnConf(conf, columnLabel.get()); 491 } 492 493 _searchUIColumnManager.addComponent("cms", null, property, SystemSearchUIColumn.class, conf); 494 columnsRolesToLookup.add(property); 495 } 496 catch (Exception e) 497 { 498 throw new ConfigurationException("Unable to instanciate SystemSearchUIColumn for property " + property, e); 499 } 500 } 501 502 @Override 503 public Map<String, SearchUICriterion> getFacetedCriteria(Map<String, Object> contextualParameters) 504 { 505 if (_facetedCriteria != null && !_facetedCriteria.isEmpty()) 506 { 507 return Collections.unmodifiableMap(_facetedCriteria); 508 } 509 else 510 { 511 return _wrappedModel.getFacetedCriteria(contextualParameters); 512 } 513 } 514 515 @Override 516 public Map<String, SearchUIColumn> getResultFields(Map<String, Object> contextualParameters) 517 { 518 if (_columns != null && !_columns.isEmpty()) 519 { 520 return Collections.unmodifiableMap(_columns); 521 } 522 else 523 { 524 return _wrappedModel.getResultFields(contextualParameters); 525 } 526 } 527 528 @Override 529 public SearchUIColumn getResultField(String id, Map<String, Object> contextualParameters) 530 { 531 if (_columns != null && !_columns.isEmpty()) 532 { 533 return getResultFields(contextualParameters).get(id); 534 } 535 else 536 { 537 return _wrappedModel.getResultField(id, contextualParameters); 538 } 539 } 540 541 //// PROXY METHODS //// 542 543 @Override 544 public Set<String> getContentTypes(Map<String, Object> contextualParameters) 545 { 546 return _wrappedModel.getContentTypes(contextualParameters); 547 } 548 549 @Override 550 public Set<String> getExcludedContentTypes(Map<String, Object> contextualParameters) 551 { 552 return _wrappedModel.getExcludedContentTypes(contextualParameters); 553 } 554 555 @Override 556 public String getSearchUrl(Map<String, Object> contextualParameters) 557 { 558 return _wrappedModel.getSearchUrl(contextualParameters); 559 } 560 561 @Override 562 public String getSearchUrlPlugin(Map<String, Object> contextualParameters) 563 { 564 return _wrappedModel.getSearchUrlPlugin(contextualParameters); 565 } 566 567 @Override 568 public String getExportCSVUrl(Map<String, Object> contextualParameters) 569 { 570 return _wrappedModel.getExportCSVUrl(contextualParameters); 571 } 572 573 @Override 574 public String getExportCSVUrlPlugin(Map<String, Object> contextualParameters) 575 { 576 return _wrappedModel.getExportCSVUrlPlugin(contextualParameters); 577 } 578 579 @Override 580 public String getExportDOCUrl(Map<String, Object> contextualParameters) 581 { 582 return _wrappedModel.getExportDOCUrl(contextualParameters); 583 } 584 585 @Override 586 public String getExportDOCUrlPlugin(Map<String, Object> contextualParameters) 587 { 588 return _wrappedModel.getExportDOCUrlPlugin(contextualParameters); 589 } 590 591 @Override 592 public String getExportXMLUrl(Map<String, Object> contextualParameters) 593 { 594 return _wrappedModel.getExportXMLUrl(contextualParameters); 595 } 596 597 @Override 598 public String getExportXMLUrlPlugin(Map<String, Object> contextualParameters) 599 { 600 return _wrappedModel.getExportXMLUrlPlugin(contextualParameters); 601 } 602 603 @Override 604 public String getPrintUrl(Map<String, Object> contextualParameters) 605 { 606 return _wrappedModel.getPrintUrl(contextualParameters); 607 } 608 609 @Override 610 public String getPrintUrlPlugin(Map<String, Object> contextualParameters) 611 { 612 return _wrappedModel.getPrintUrlPlugin(contextualParameters); 613 } 614 615 @Override 616 public boolean allowSortOnMultipleJoin() 617 { 618 return _wrappedModel.allowSortOnMultipleJoin(); 619 } 620 621 @Override 622 public Map<String, SearchUICriterion> getCriteria(Map<String, Object> contextualParameters) 623 { 624 return _wrappedModel.getCriteria(contextualParameters); 625 } 626 627 @Override 628 public SearchUICriterion getCriterion(String id, Map<String, Object> contextualParameters) 629 { 630 return _wrappedModel.getCriterion(id, contextualParameters); 631 } 632 633 @Override 634 public Map<String, SearchUICriterion> getAdvancedCriteria(Map<String, Object> contextualParameters) 635 { 636 return _wrappedModel.getAdvancedCriteria(contextualParameters); 637 } 638 639 640}