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.ui.model; 017 018import java.util.ArrayList; 019import java.util.Collections; 020import java.util.List; 021import java.util.Map; 022import java.util.Set; 023 024import org.apache.avalon.framework.component.ComponentException; 025import org.apache.avalon.framework.configuration.Configurable; 026import org.apache.avalon.framework.configuration.Configuration; 027import org.apache.avalon.framework.configuration.ConfigurationException; 028import org.apache.avalon.framework.configuration.DefaultConfiguration; 029import org.apache.avalon.framework.service.ServiceException; 030import org.apache.avalon.framework.service.ServiceManager; 031 032import org.ametys.cms.content.referencetable.HierarchicalReferenceTablesHelper; 033import org.ametys.cms.contenttype.ContentType; 034import org.ametys.cms.data.type.ModelItemTypeConstants; 035import org.ametys.cms.repository.Content; 036import org.ametys.cms.search.query.Query.Operator; 037import org.ametys.cms.search.ui.model.impl.IndexingFieldSearchUICriterion; 038import org.ametys.cms.search.ui.model.impl.MetadataSearchUIColumn; 039import org.ametys.cms.search.ui.model.impl.SystemSearchUIColumn; 040import org.ametys.cms.search.ui.model.impl.SystemSearchUICriterion; 041import org.ametys.runtime.model.ModelItem; 042import org.ametys.runtime.model.ModelViewItem; 043import org.ametys.runtime.model.View; 044import org.ametys.runtime.model.ViewItem; 045import org.ametys.runtime.model.ViewItemContainer; 046import org.ametys.runtime.plugin.component.ThreadSafeComponentManager; 047 048/** 049 * Generic implementation of {@link SearchUIModel} for reference tables 050 * The search tool model automatically declares simple first level metadatas as criteria and columns. 051 */ 052public class ReferenceTableSearchUIModel extends AbstractSearchUIModel implements Configurable 053{ 054 055 /** ComponentManager for {@link SearchUICriterion}s. */ 056 protected ThreadSafeComponentManager<SearchUICriterion> _searchCriteriaManager; 057 058 /** ComponentManager for {@link SearchUIColumn}s. */ 059 protected ThreadSafeComponentManager<SearchUIColumn> _searchColumnManager; 060 061 /** The search criteria roles. */ 062 protected List<String> _searchCriteriaRoles; 063 064 /** The search column roles. */ 065 protected List<String> _searchColumnRoles; 066 067 /** The helper component for hierarchical reference tables */ 068 protected HierarchicalReferenceTablesHelper _hierarchicalReferenceTableContentsHelper; 069 070 @Override 071 public void service(ServiceManager smanager) throws ServiceException 072 { 073 super.service(smanager); 074 _hierarchicalReferenceTableContentsHelper = (HierarchicalReferenceTablesHelper) smanager.lookup(HierarchicalReferenceTablesHelper.ROLE); 075 } 076 077 @Override 078 public void configure(Configuration configuration) throws ConfigurationException 079 { 080 try 081 { 082 String cTypeId = configuration.getChild("contentType").getValue(null); 083 if (cTypeId != null) 084 { 085 setContentTypes(Collections.singleton(cTypeId)); 086 } 087 088 _searchCriteriaManager = new ThreadSafeComponentManager<>(); 089 _searchCriteriaManager.setLogger(getLogger()); 090 _searchCriteriaManager.contextualize(_context); 091 _searchCriteriaManager.service(_manager); 092 093 _searchColumnManager = new ThreadSafeComponentManager<>(); 094 _searchColumnManager.setLogger(getLogger()); 095 _searchColumnManager.contextualize(_context); 096 _searchColumnManager.service(_manager); 097 } 098 catch (Exception e) 099 { 100 throw new ConfigurationException("Unable to create local component managers.", configuration, e); 101 } 102 } 103 104 @Override 105 public Set<String> getExcludedContentTypes(Map<String, Object> contextualParameters) 106 { 107 return Collections.emptySet(); 108 } 109 110 @Override 111 public Map<String, SearchUICriterion> getCriteria(Map<String, Object> contextualParameters) 112 { 113 try 114 { 115 if (_searchCriteria == null) 116 { 117 String cTypeId = getContentTypes(contextualParameters).iterator().next(); 118 ContentType cType = _cTypeEP.getExtension(cTypeId); 119 120 _searchCriteriaRoles = new ArrayList<>(); 121 122 addCriteriaComponents(cType); 123 _searchCriteriaManager.initialize(); 124 setCriteria(getSearchUICriteria(cType)); 125 } 126 } 127 catch (Exception e) 128 { 129 throw new RuntimeException("Impossible to initialize criteria components.", e); 130 } 131 132 return _searchCriteria; 133 } 134 135 @Override 136 public Map<String, SearchUICriterion> getFacetedCriteria(Map<String, Object> contextualParameters) 137 { 138 return Collections.emptyMap(); 139 } 140 141 @Override 142 public Map<String, SearchUICriterion> getAdvancedCriteria(Map<String, Object> contextualParameters) 143 { 144 return Collections.emptyMap(); 145 } 146 147 @Override 148 public Map<String, SearchUIColumn> getResultFields(Map<String, Object> contextualParameters) 149 { 150 try 151 { 152 if (_columns == null) 153 { 154 _searchColumnRoles = new ArrayList<>(); 155 156 String cTypeId = getContentTypes(contextualParameters).iterator().next(); 157 ContentType cType = _cTypeEP.getExtension(cTypeId); 158 159 addColumnComponents(cType); 160 _searchColumnManager.initialize(); 161 setResultFields(getColumns()); 162 } 163 } 164 catch (Exception e) 165 { 166 throw new RuntimeException("Impossible to initialize column components.", e); 167 } 168 169 return _columns; 170 } 171 172 /** 173 * Add criteria components to the manager. 174 * @param cType the simple content type. 175 * @throws ConfigurationException if a configuration error occurs. 176 * @throws ComponentException if a component cannot be initialized. 177 */ 178 protected void addCriteriaComponents(ContentType cType) throws ConfigurationException, ComponentException 179 { 180 View view = cType.getView("criteria"); 181 if (view == null) 182 { 183 view = cType.getView("main"); 184 } 185 186 addCriteriaComponents(cType, view); 187 188 if (_hierarchicalReferenceTableContentsHelper.isHierarchical(cType) && _hierarchicalReferenceTableContentsHelper.supportCandidates(cType)) 189 { 190 addExcludeCandidateSystemCriterionComponent(cType); 191 } 192 193 addSystemCriterionComponent(cType, "contributor"); 194 195 if (!cType.isMultilingual()) 196 { 197 addSystemCriterionComponent(cType, "contentLanguage"); 198 } 199 } 200 201 /** 202 * Add criteria components to the manager. 203 * @param cType the simple content type. 204 * @param viewContainer the view item container 205 * @throws ConfigurationException if a configuration error occurs. 206 * @throws ComponentException if a component cannot be initialized. 207 */ 208 protected void addCriteriaComponents(ContentType cType, ViewItemContainer viewContainer) throws ConfigurationException, ComponentException 209 { 210 for (ViewItem viewItem : viewContainer.getViewItems()) 211 { 212 if (viewItem instanceof ViewItemContainer) 213 { 214 addCriteriaComponents(cType, (ViewItemContainer) viewItem); 215 } 216 else if (viewItem instanceof ModelViewItem) 217 { 218 ModelItem modelItem = ((ModelViewItem) viewItem).getDefinition(); 219 if (_filterModelItemForCriteria(modelItem)) 220 { 221 String modelItemPath = modelItem.getPath(); 222 223 Operator operator = org.ametys.runtime.model.type.ModelItemTypeConstants.STRING_TYPE_ID.equals(modelItem.getType().getId()) || ModelItemTypeConstants.MULTILINGUAL_STRING_ELEMENT_TYPE_ID.equals(modelItem.getType().getId()) ? Operator.SEARCH : Operator.EQ; 224 addAttributeCriterionComponent(cType, modelItemPath, operator); 225 226 if (Content.ATTRIBUTE_TITLE.equals(modelItem.getName())) 227 { 228 addLikeTitleCriterionComponent(cType, modelItemPath); 229 } 230 } 231 } 232 } 233 } 234 235 /** Add criteria component to exclude the candidates 236 * @param cType the simple content type 237 */ 238 protected void addExcludeCandidateSystemCriterionComponent(ContentType cType) 239 { 240 DefaultConfiguration conf = (DefaultConfiguration) getSystemCriteriaConfiguration(Set.of(cType.getId()), "mixins", null); 241 242 DefaultConfiguration defaultValueConf = new DefaultConfiguration("default-value"); 243 defaultValueConf.setValue("org.ametys.cms.referencetable.mixin.Candidate"); 244 conf.addChild(defaultValueConf); 245 246 DefaultConfiguration opConf = new DefaultConfiguration("test-operator"); 247 opConf.setValue(Operator.NE.getName()); 248 conf.addChild(opConf); 249 250 DefaultConfiguration widgetConf = new DefaultConfiguration("widget"); 251 widgetConf.setValue("edition.hidden"); 252 conf.addChild(widgetConf); 253 254 _searchCriteriaManager.addComponent("cms", null, "mixins", SystemSearchUICriterion.class, conf); 255 _searchCriteriaRoles.add("mixins"); 256 } 257 258 /** 259 * Returns <code>true</code> if the model item can be used as criteria 260 * @param modelItem the model item 261 * @return <code>true</code> if the model item can be used as criteria 262 */ 263 @SuppressWarnings("static-access") 264 protected boolean _filterModelItemForCriteria(ModelItem modelItem) 265 { 266 String typeId = modelItem.getType().getId(); 267 switch (typeId) 268 { 269 case ModelItemTypeConstants.STRING_TYPE_ID: 270 case ModelItemTypeConstants.MULTILINGUAL_STRING_ELEMENT_TYPE_ID: 271 case ModelItemTypeConstants.DATE_TYPE_ID: 272 case ModelItemTypeConstants.DATETIME_TYPE_ID: 273 case ModelItemTypeConstants.LONG_TYPE_ID: 274 case ModelItemTypeConstants.DOUBLE_TYPE_ID: 275 case ModelItemTypeConstants.BOOLEAN_TYPE_ID: 276 case ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID: 277 case ModelItemTypeConstants.REFERENCE_ELEMENT_TYPE_ID: 278 return true; 279 case ModelItemTypeConstants.BINARY_ELEMENT_TYPE_ID: 280 case ModelItemTypeConstants.RICH_TEXT_ELEMENT_TYPE_ID: 281 case ModelItemTypeConstants.FILE_ELEMENT_TYPE_ID: 282 case ModelItemTypeConstants.GEOCODE_ELEMENT_TYPE_ID: 283 case ModelItemTypeConstants.COMPOSITE_TYPE_ID: 284 case ModelItemTypeConstants.REPEATER_TYPE_ID: 285 default: 286 return false; 287 } 288 } 289 290 /** 291 * Returns <code>true</code> if model item can be used as column search UI 292 * @param modelItem the model item 293 * @return <code>true</code> if model item can be used as column search UI 294 */ 295 @SuppressWarnings("static-access") 296 protected boolean _filterModelItemForColumn(ModelItem modelItem) 297 { 298 String typeId = modelItem.getType().getId(); 299 switch (typeId) 300 { 301 case ModelItemTypeConstants.STRING_TYPE_ID: 302 case ModelItemTypeConstants.MULTILINGUAL_STRING_ELEMENT_TYPE_ID: 303 case ModelItemTypeConstants.DATE_TYPE_ID: 304 case ModelItemTypeConstants.DATETIME_TYPE_ID: 305 case ModelItemTypeConstants.LONG_TYPE_ID: 306 case ModelItemTypeConstants.DOUBLE_TYPE_ID: 307 case ModelItemTypeConstants.BOOLEAN_TYPE_ID: 308 case ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID: 309 case ModelItemTypeConstants.GEOCODE_ELEMENT_TYPE_ID: 310 case ModelItemTypeConstants.REFERENCE_ELEMENT_TYPE_ID: 311 return true; 312 case ModelItemTypeConstants.BINARY_ELEMENT_TYPE_ID: 313 case ModelItemTypeConstants.RICH_TEXT_ELEMENT_TYPE_ID: 314 case ModelItemTypeConstants.FILE_ELEMENT_TYPE_ID: 315 case ModelItemTypeConstants.COMPOSITE_TYPE_ID: 316 case ModelItemTypeConstants.REPEATER_TYPE_ID: 317 default: 318 return false; 319 } 320 } 321 322 /** 323 * Add the title attribute criterion component to the manager, with a 'LIKE' operator and an hidden widget 324 * @param contentType the simple content type. 325 * @param attributePath the attribute path. 326 * @throws ConfigurationException if a configuration error occurs. 327 * @throws ComponentException if a component cannot be initialized. 328 */ 329 protected void addLikeTitleCriterionComponent(ContentType contentType, String attributePath) throws ConfigurationException, ComponentException 330 { 331 DefaultConfiguration originalConf = new DefaultConfiguration("criteria"); 332 DefaultConfiguration widgetConf = new DefaultConfiguration("widget"); 333 widgetConf.setValue("edition.hidden"); 334 originalConf.addChild(widgetConf); 335 Configuration conf = getIndexingFieldCriteriaConfiguration(originalConf, Set.of(contentType.getId()), attributePath, Operator.LIKE, null); 336 337 String role = attributePath + "1"; 338 _searchCriteriaManager.addComponent("cms", null, role, IndexingFieldSearchUICriterion.class, conf); 339 _searchCriteriaRoles.add(role); 340 } 341 342 /** 343 * Add an attribute criterion component to the manager. 344 * @param contentType the simple content type. 345 * @param attributePath the attribute path. 346 * @param operator the criterion operator. 347 * @throws ConfigurationException if a configuration error occurs. 348 * @throws ComponentException if a component cannot be initialized. 349 */ 350 protected void addAttributeCriterionComponent(ContentType contentType, String attributePath, Operator operator) throws ConfigurationException, ComponentException 351 { 352 DefaultConfiguration conf = (DefaultConfiguration) getIndexingFieldCriteriaConfiguration(Set.of(contentType.getId()), attributePath, operator, null); 353 ModelItem metadataDefinition = contentType.getModelItem(attributePath); 354 355 if ( 356 ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID.equals(metadataDefinition.getType().getId()) 357 && contentType.isReferenceTable() 358 && contentType.getParentAttributeDefinition() 359 .map(ModelItem::getPath) 360 .map(parent -> parent.equals(attributePath)) 361 .orElse(false) 362 ) 363 { 364 DefaultConfiguration widgetConfig = (DefaultConfiguration) conf.getChild("widget"); 365 widgetConfig.setValue("edition.select-referencetable-content"); 366 367 DefaultConfiguration widgetParamsConfig = (DefaultConfiguration) conf.getChild("widget-params"); 368 369 DefaultConfiguration allowAutopostingParamsConfig = (DefaultConfiguration) widgetParamsConfig.getChild("param"); 370 allowAutopostingParamsConfig.setAttribute("name", "allowToggleAutoposting"); 371 allowAutopostingParamsConfig.setValue(true); 372 widgetParamsConfig.addChild(allowAutopostingParamsConfig); 373 374 conf.addChild(widgetConfig); 375 conf.addChild(widgetParamsConfig); 376 } 377 _searchCriteriaManager.addComponent("cms", null, attributePath, IndexingFieldSearchUICriterion.class, conf); 378 _searchCriteriaRoles.add(attributePath); 379 } 380 381 /** 382 * Add a system criterion component to the manager. 383 * @param contentType the simple content type. 384 * @param property the system property. 385 * @throws ConfigurationException if a configuration error occurs. 386 * @throws ComponentException if a component cannot be initialized. 387 */ 388 protected void addSystemCriterionComponent(ContentType contentType, String property) throws ConfigurationException, ComponentException 389 { 390 DefaultConfiguration conf = (DefaultConfiguration) getSystemCriteriaConfiguration(Set.of(contentType.getId()), property, null); 391 392 if (property.equals("contentLanguage")) 393 { 394 // FIXME Is this configuration should be provided by Language system property itself ? 395 // FIXME For now the simple contents are only created for language 'fr' 396 DefaultConfiguration widgetConf = new DefaultConfiguration("widget"); 397 widgetConf.setValue("edition.select-language"); 398 conf.addChild(widgetConf); 399 400 DefaultConfiguration defaultConf = new DefaultConfiguration("default-value"); 401 defaultConf.setValue("CURRENT"); 402 conf.addChild(defaultConf); 403 404 DefaultConfiguration validConf = new DefaultConfiguration("validation"); 405 DefaultConfiguration mandatoryConf = new DefaultConfiguration("mandatory"); 406 mandatoryConf.setValue(true); 407 validConf.addChild(mandatoryConf); 408 conf.addChild(validConf); 409 } 410 411 _searchCriteriaManager.addComponent("cms", null, property, SystemSearchUICriterion.class, conf); 412 _searchCriteriaRoles.add(property); 413 } 414 415 /** 416 * Lookup all the criteria. 417 * @param cType the simple content type. 418 * @return the search criteria list. 419 * @throws ComponentException if a component cannot be looked up. 420 */ 421 protected List<SearchUICriterion> getSearchUICriteria(ContentType cType) throws ComponentException 422 { 423 List<SearchUICriterion> criteria = new ArrayList<>(); 424 425 for (String role : _searchCriteriaRoles) 426 { 427 SearchUICriterion criterion = _searchCriteriaManager.lookup(role); 428 criteria.add(criterion); 429 } 430 431 return criteria; 432 } 433 434 /** 435 * Add column components to the manager. 436 * @param cType the simple content type. 437 * @throws ConfigurationException if a configuration error occurs. 438 * @throws ComponentException if a component cannot be initialized. 439 */ 440 protected void addColumnComponents(ContentType cType) throws ConfigurationException, ComponentException 441 { 442 View view = cType.getView("columns"); 443 if (view == null) 444 { 445 view = cType.getView("main"); 446 } 447 448 addColumnComponents(cType, view); 449 450 addSystemColumnComponent(cType, "contributor"); 451 addSystemColumnComponent(cType, "lastModified"); 452 453 if (!cType.isMultilingual()) 454 { 455 addSystemColumnComponent(cType, "contentLanguage"); 456 } 457 } 458 459 /** 460 * Add column components to the manager. 461 * @param cType the simple content type. 462 * @param viewContainer the view item container 463 * @throws ConfigurationException if a configuration error occurs. 464 * @throws ComponentException if a component cannot be initialized. 465 */ 466 protected void addColumnComponents(ContentType cType, ViewItemContainer viewContainer) throws ConfigurationException, ComponentException 467 { 468 for (ViewItem viewItem : viewContainer.getViewItems()) 469 { 470 if (viewItem instanceof ViewItemContainer) 471 { 472 addColumnComponents(cType, (ViewItemContainer) viewItem); 473 } 474 else if (viewItem instanceof ModelViewItem) 475 { 476 ModelItem modelItem = ((ModelViewItem) viewItem).getDefinition(); 477 478 if (_filterModelItemForColumn(modelItem)) 479 { 480 addAttributeColumnComponent(cType, modelItem.getPath()); 481 } 482 } 483 } 484 } 485 486 /** 487 * Add an attribute column component to the manager. 488 * @param contentType the simple content type. 489 * @param attributePath the attribute path. 490 * @throws ConfigurationException if a configuration error occurs. 491 * @throws ComponentException if a component cannot be initialized. 492 */ 493 protected void addAttributeColumnComponent(ContentType contentType, String attributePath) throws ConfigurationException, ComponentException 494 { 495 Configuration columnConf = getMetadataColumnConfiguration(Set.of(contentType.getId()), attributePath); 496 _searchColumnManager.addComponent("cms", null, attributePath, MetadataSearchUIColumn.class, columnConf); 497 _searchColumnRoles.add(attributePath); 498 } 499 500 /** 501 * Add a system column component to the manager. 502 * @param contentType the simple content type. 503 * @param property the system property. 504 * @throws ConfigurationException if a configuration error occurs. 505 * @throws ComponentException if a component cannot be initialized. 506 */ 507 protected void addSystemColumnComponent(ContentType contentType, String property) throws ConfigurationException, ComponentException 508 { 509 Configuration conf = getSystemColumnConfiguration(Set.of(contentType.getId()), property); 510 _searchColumnManager.addComponent("cms", null, property, SystemSearchUIColumn.class, conf); 511 _searchColumnRoles.add(property); 512 } 513 514 /** 515 * Lookup all the columns. 516 * @return the search column list. 517 * @throws ComponentException if a component cannot be looked up. 518 */ 519 protected List<SearchUIColumn> getColumns() throws ComponentException 520 { 521 List<SearchUIColumn> columns = new ArrayList<>(); 522 523 for (String columnRole : _searchColumnRoles) 524 { 525 SearchUIColumn column = _searchColumnManager.lookup(columnRole); 526 columns.add(column); 527 } 528 529 return columns; 530 } 531 532}