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