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(cType)); 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("main"); 181 182 addCriteriaComponents(cType, view); 183 184 if (_hierarchicalReferenceTableContentsHelper.isHierarchical(cType) && _hierarchicalReferenceTableContentsHelper.supportCandidates(cType)) 185 { 186 addExcludeCandidateSystemCriterionComponent(cType); 187 } 188 189 addSystemCriterionComponent(cType, "contributor"); 190 191 if (!cType.isMultilingual()) 192 { 193 addSystemCriterionComponent(cType, "contentLanguage"); 194 } 195 196 } 197 198 /** 199 * Add criteria components to the manager. 200 * @param cType the simple content type. 201 * @param viewContainer the view item container 202 * @throws ConfigurationException if a configuration error occurs. 203 * @throws ComponentException if a component cannot be initialized. 204 */ 205 protected void addCriteriaComponents(ContentType cType, ViewItemContainer viewContainer) throws ConfigurationException, ComponentException 206 { 207 for (ViewItem viewItem : viewContainer.getViewItems()) 208 { 209 if (viewItem instanceof ViewItemContainer) 210 { 211 addCriteriaComponents(cType, (ViewItemContainer) viewItem); 212 } 213 else if (viewItem instanceof ModelViewItem) 214 { 215 ModelItem modelItem = ((ModelViewItem) viewItem).getDefinition(); 216 if (_filterModelItemForCriteria(modelItem)) 217 { 218 String modelItemPath = modelItem.getPath(); 219 220 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; 221 addAttributeCriterionComponent(cType, modelItemPath, operator); 222 223 if (Content.ATTRIBUTE_TITLE.equals(modelItem.getName())) 224 { 225 addLikeTitleCriterionComponent(cType, modelItemPath); 226 } 227 } 228 } 229 } 230 } 231 232 /** Add criteria component to exclude the candidates 233 * @param cType the simple content type 234 */ 235 protected void addExcludeCandidateSystemCriterionComponent(ContentType cType) 236 { 237 DefaultConfiguration conf = (DefaultConfiguration) getSystemCriteriaConfiguration(Set.of(cType.getId()), "mixins", null); 238 239 DefaultConfiguration defaultValueConf = new DefaultConfiguration("default-value"); 240 defaultValueConf.setValue("org.ametys.cms.referencetable.mixin.Candidate"); 241 conf.addChild(defaultValueConf); 242 243 DefaultConfiguration opConf = new DefaultConfiguration("test-operator"); 244 opConf.setValue(Operator.NE.getName()); 245 conf.addChild(opConf); 246 247 DefaultConfiguration widgetConf = new DefaultConfiguration("widget"); 248 widgetConf.setValue("edition.hidden"); 249 conf.addChild(widgetConf); 250 251 _searchCriteriaManager.addComponent("cms", null, "mixins", SystemSearchUICriterion.class, conf); 252 _searchCriteriaRoles.add("mixins"); 253 } 254 255 /** 256 * Returns <code>true</code> if the model item can be used as criteria 257 * @param modelItem the model item 258 * @return <code>true</code> if the model item can be used as criteria 259 */ 260 @SuppressWarnings("static-access") 261 protected boolean _filterModelItemForCriteria(ModelItem modelItem) 262 { 263 String typeId = modelItem.getType().getId(); 264 switch (typeId) 265 { 266 case ModelItemTypeConstants.STRING_TYPE_ID: 267 case ModelItemTypeConstants.MULTILINGUAL_STRING_ELEMENT_TYPE_ID: 268 case ModelItemTypeConstants.DATE_TYPE_ID: 269 case ModelItemTypeConstants.DATETIME_TYPE_ID: 270 case ModelItemTypeConstants.LONG_TYPE_ID: 271 case ModelItemTypeConstants.DOUBLE_TYPE_ID: 272 case ModelItemTypeConstants.BOOLEAN_TYPE_ID: 273 case ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID: 274 case ModelItemTypeConstants.REFERENCE_ELEMENT_TYPE_ID: 275 return true; 276 case ModelItemTypeConstants.BINARY_ELEMENT_TYPE_ID: 277 case ModelItemTypeConstants.RICH_TEXT_ELEMENT_TYPE_ID: 278 case ModelItemTypeConstants.FILE_ELEMENT_TYPE_ID: 279 case ModelItemTypeConstants.GEOCODE_ELEMENT_TYPE_ID: 280 case ModelItemTypeConstants.COMPOSITE_TYPE_ID: 281 case ModelItemTypeConstants.REPEATER_TYPE_ID: 282 default: 283 return false; 284 } 285 } 286 287 /** 288 * Returns <code>true</code> if model item can be used as column search UI 289 * @param modelItem the model item 290 * @return <code>true</code> if model item can be used as column search UI 291 */ 292 @SuppressWarnings("static-access") 293 protected boolean _filterModelItemForColumn(ModelItem modelItem) 294 { 295 String typeId = modelItem.getType().getId(); 296 switch (typeId) 297 { 298 case ModelItemTypeConstants.STRING_TYPE_ID: 299 case ModelItemTypeConstants.MULTILINGUAL_STRING_ELEMENT_TYPE_ID: 300 case ModelItemTypeConstants.DATE_TYPE_ID: 301 case ModelItemTypeConstants.DATETIME_TYPE_ID: 302 case ModelItemTypeConstants.LONG_TYPE_ID: 303 case ModelItemTypeConstants.DOUBLE_TYPE_ID: 304 case ModelItemTypeConstants.BOOLEAN_TYPE_ID: 305 case ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID: 306 case ModelItemTypeConstants.GEOCODE_ELEMENT_TYPE_ID: 307 case ModelItemTypeConstants.REFERENCE_ELEMENT_TYPE_ID: 308 return true; 309 case ModelItemTypeConstants.BINARY_ELEMENT_TYPE_ID: 310 case ModelItemTypeConstants.RICH_TEXT_ELEMENT_TYPE_ID: 311 case ModelItemTypeConstants.FILE_ELEMENT_TYPE_ID: 312 case ModelItemTypeConstants.COMPOSITE_TYPE_ID: 313 case ModelItemTypeConstants.REPEATER_TYPE_ID: 314 default: 315 return false; 316 } 317 } 318 319 /** 320 * Add the title attribute criterion component to the manager, with a 'LIKE' operator and an hidden widget 321 * @param contentType the simple content type. 322 * @param attributePath the attribute path. 323 * @throws ConfigurationException if a configuration error occurs. 324 * @throws ComponentException if a component cannot be initialized. 325 */ 326 protected void addLikeTitleCriterionComponent(ContentType contentType, String attributePath) throws ConfigurationException, ComponentException 327 { 328 DefaultConfiguration originalConf = new DefaultConfiguration("criteria"); 329 DefaultConfiguration widgetConf = new DefaultConfiguration("widget"); 330 widgetConf.setValue("edition.hidden"); 331 originalConf.addChild(widgetConf); 332 Configuration conf = getIndexingFieldCriteriaConfiguration(originalConf, Set.of(contentType.getId()), attributePath, Operator.LIKE, null); 333 334 String role = attributePath + "1"; 335 _searchCriteriaManager.addComponent("cms", null, role, IndexingFieldSearchUICriterion.class, conf); 336 _searchCriteriaRoles.add(role); 337 } 338 339 /** 340 * Add an attribute criterion component to the manager. 341 * @param contentType the simple content type. 342 * @param attributePath the attribute path. 343 * @param operator the criterion operator. 344 * @throws ConfigurationException if a configuration error occurs. 345 * @throws ComponentException if a component cannot be initialized. 346 */ 347 protected void addAttributeCriterionComponent(ContentType contentType, String attributePath, Operator operator) throws ConfigurationException, ComponentException 348 { 349 DefaultConfiguration conf = (DefaultConfiguration) getIndexingFieldCriteriaConfiguration(Set.of(contentType.getId()), attributePath, operator, null); 350 ModelItem metadataDefinition = contentType.getModelItem(attributePath); 351 352 if ( 353 ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID.equals(metadataDefinition.getType().getId()) 354 && contentType.isReferenceTable() 355 && contentType.getParentAttributeDefinition() 356 .map(ModelItem::getPath) 357 .map(parent -> parent.equals(attributePath)) 358 .orElse(false) 359 ) 360 { 361 DefaultConfiguration widgetConfig = (DefaultConfiguration) conf.getChild("widget"); 362 widgetConfig.setValue("edition.select-referencetable-content"); 363 364 DefaultConfiguration widgetParamsConfig = (DefaultConfiguration) conf.getChild("widget-params"); 365 366 DefaultConfiguration allowAutopostingParamsConfig = (DefaultConfiguration) widgetParamsConfig.getChild("param"); 367 allowAutopostingParamsConfig.setAttribute("name", "allowToggleAutoposting"); 368 allowAutopostingParamsConfig.setValue(true); 369 widgetParamsConfig.addChild(allowAutopostingParamsConfig); 370 371 conf.addChild(widgetConfig); 372 conf.addChild(widgetParamsConfig); 373 } 374 _searchCriteriaManager.addComponent("cms", null, attributePath, IndexingFieldSearchUICriterion.class, conf); 375 _searchCriteriaRoles.add(attributePath); 376 } 377 378 /** 379 * Add a system criterion component to the manager. 380 * @param contentType the simple content type. 381 * @param property the system property. 382 * @throws ConfigurationException if a configuration error occurs. 383 * @throws ComponentException if a component cannot be initialized. 384 */ 385 protected void addSystemCriterionComponent(ContentType contentType, String property) throws ConfigurationException, ComponentException 386 { 387 DefaultConfiguration conf = (DefaultConfiguration) getSystemCriteriaConfiguration(Set.of(contentType.getId()), property, null); 388 389 if (property.equals("contentLanguage")) 390 { 391 // FIXME Is this configuration should be provided by Language system property itself ? 392 // FIXME For now the simple contents are only created for language 'fr' 393 DefaultConfiguration widgetConf = new DefaultConfiguration("widget"); 394 widgetConf.setValue("edition.select-language"); 395 conf.addChild(widgetConf); 396 397 DefaultConfiguration defaultConf = new DefaultConfiguration("default-value"); 398 defaultConf.setValue("CURRENT"); 399 conf.addChild(defaultConf); 400 401 DefaultConfiguration validConf = new DefaultConfiguration("validation"); 402 DefaultConfiguration mandatoryConf = new DefaultConfiguration("mandatory"); 403 mandatoryConf.setValue(true); 404 validConf.addChild(mandatoryConf); 405 conf.addChild(validConf); 406 } 407 408 _searchCriteriaManager.addComponent("cms", null, property, SystemSearchUICriterion.class, conf); 409 _searchCriteriaRoles.add(property); 410 } 411 412 /** 413 * Lookup all the criteria. 414 * @param cType the simple content type. 415 * @return the search criteria list. 416 * @throws ComponentException if a component cannot be looked up. 417 */ 418 protected List<SearchUICriterion> getSearchUICriteria(ContentType cType) throws ComponentException 419 { 420 List<SearchUICriterion> criteria = new ArrayList<>(); 421 422 for (String role : _searchCriteriaRoles) 423 { 424 SearchUICriterion criterion = _searchCriteriaManager.lookup(role); 425 criteria.add(criterion); 426 } 427 428 return criteria; 429 } 430 431 /** 432 * Add column components to the manager. 433 * @param cType the simple content type. 434 * @throws ConfigurationException if a configuration error occurs. 435 * @throws ComponentException if a component cannot be initialized. 436 */ 437 protected void addColumnComponents(ContentType cType) throws ConfigurationException, ComponentException 438 { 439 View view = cType.getView("main"); 440 441 addColumnComponents(cType, view); 442 443 addSystemColumnComponent(cType, "contributor"); 444 addSystemColumnComponent(cType, "lastModified"); 445 446 if (!cType.isMultilingual()) 447 { 448 addSystemColumnComponent(cType, "contentLanguage"); 449 } 450 } 451 452 /** 453 * Add column components to the manager. 454 * @param cType the simple content type. 455 * @param viewContainer the view item container 456 * @throws ConfigurationException if a configuration error occurs. 457 * @throws ComponentException if a component cannot be initialized. 458 */ 459 protected void addColumnComponents(ContentType cType, ViewItemContainer viewContainer) throws ConfigurationException, ComponentException 460 { 461 for (ViewItem viewItem : viewContainer.getViewItems()) 462 { 463 if (viewItem instanceof ViewItemContainer) 464 { 465 addColumnComponents(cType, (ViewItemContainer) viewItem); 466 } 467 else if (viewItem instanceof ModelViewItem) 468 { 469 ModelItem modelItem = ((ModelViewItem) viewItem).getDefinition(); 470 471 if (_filterModelItemForColumn(modelItem)) 472 { 473 addAttributeColumnComponent(cType, modelItem.getPath()); 474 } 475 } 476 } 477 } 478 479 /** 480 * Add an attribute column component to the manager. 481 * @param contentType the simple content type. 482 * @param attributePath the attribute path. 483 * @throws ConfigurationException if a configuration error occurs. 484 * @throws ComponentException if a component cannot be initialized. 485 */ 486 protected void addAttributeColumnComponent(ContentType contentType, String attributePath) throws ConfigurationException, ComponentException 487 { 488 Configuration columnConf = getMetadataColumnConfiguration(Set.of(contentType.getId()), attributePath); 489 _searchColumnManager.addComponent("cms", null, attributePath, MetadataSearchUIColumn.class, columnConf); 490 _searchColumnRoles.add(attributePath); 491 } 492 493 /** 494 * Add a system column component to the manager. 495 * @param contentType the simple content type. 496 * @param property the system property. 497 * @throws ConfigurationException if a configuration error occurs. 498 * @throws ComponentException if a component cannot be initialized. 499 */ 500 protected void addSystemColumnComponent(ContentType contentType, String property) throws ConfigurationException, ComponentException 501 { 502 Configuration conf = getSystemColumnConfiguration(Set.of(contentType.getId()), property); 503 _searchColumnManager.addComponent("cms", null, property, SystemSearchUIColumn.class, conf); 504 _searchColumnRoles.add(property); 505 } 506 507 /** 508 * Lookup all the columns. 509 * @param cType the simple content type. 510 * @return the search column list. 511 * @throws ComponentException if a component cannot be looked up. 512 */ 513 protected List<SearchUIColumn> getColumns(ContentType cType) throws ComponentException 514 { 515 List<SearchUIColumn> columns = new ArrayList<>(); 516 517 for (String columnRole : _searchColumnRoles) 518 { 519 SearchUIColumn column = _searchColumnManager.lookup(columnRole); 520 columns.add(column); 521 } 522 523 return columns; 524 } 525 526}