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