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