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.configuration.Configurable; 026import org.apache.avalon.framework.configuration.Configuration; 027import org.apache.avalon.framework.configuration.ConfigurationException; 028import org.apache.avalon.framework.service.ServiceException; 029import org.apache.avalon.framework.service.ServiceManager; 030import org.apache.avalon.framework.service.Serviceable; 031import org.apache.commons.lang3.tuple.ImmutablePair; 032 033import org.ametys.cms.content.referencetable.HierarchicalReferenceTablesHelper; 034import org.ametys.cms.contenttype.ContentType; 035import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 036import org.ametys.cms.data.type.ModelItemTypeConstants; 037import org.ametys.cms.repository.Content; 038import org.ametys.cms.search.model.SearchModelCriterionDefinition; 039import org.ametys.cms.search.model.SystemPropertyExtensionPoint; 040import org.ametys.cms.search.query.Query.Operator; 041import org.ametys.cms.search.ui.model.impl.DefaultSearchUIModel; 042import org.ametys.runtime.i18n.I18nizableText; 043import org.ametys.runtime.model.ElementDefinition; 044import org.ametys.runtime.model.ModelItem; 045import org.ametys.runtime.model.ModelViewItem; 046import org.ametys.runtime.model.SimpleViewItemGroup; 047import org.ametys.runtime.model.View; 048import org.ametys.runtime.model.ViewElement; 049import org.ametys.runtime.model.ViewItem; 050import org.ametys.runtime.model.ViewItemAccessor; 051import org.ametys.runtime.model.ViewItemContainer; 052import org.ametys.runtime.model.ViewItemGroup; 053import org.ametys.runtime.parameter.DefaultValidator; 054 055/** 056 * Generic implementation of {@link SearchUIModel} for reference tables 057 * The search tool model automatically declares simple first level attributes as criteria and columns. 058 */ 059public class ReferenceTableSearchUIModel extends DefaultSearchUIModel implements Serviceable, Configurable 060{ 061 /** The helper component for hierarchical reference tables */ 062 protected HierarchicalReferenceTablesHelper _hierarchicalReferenceTableContentsHelper; 063 064 /** The content type extension point */ 065 protected ContentTypeExtensionPoint _contentTypeExtensionPoint; 066 067 /** The systemPropertyExtension point */ 068 protected SystemPropertyExtensionPoint _systemPropertyExtensionPoint; 069 070 /** The helper for {@link SearchUIModel} criterion definition */ 071 protected SearchModelCriterionViewItemHelper _searchModelCriterionViewItemHelper; 072 073 public void service(ServiceManager manager) throws ServiceException 074 { 075 _hierarchicalReferenceTableContentsHelper = (HierarchicalReferenceTablesHelper) manager.lookup(HierarchicalReferenceTablesHelper.ROLE); 076 _contentTypeExtensionPoint = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE); 077 _systemPropertyExtensionPoint = (SystemPropertyExtensionPoint) manager.lookup(SystemPropertyExtensionPoint.ROLE); 078 _searchModelCriterionViewItemHelper = (SearchModelCriterionViewItemHelper) manager.lookup(SearchModelCriterionViewItemHelper.ROLE); 079 } 080 081 @Override 082 public void configure(Configuration configuration) throws ConfigurationException 083 { 084 try 085 { 086 String cTypeId = configuration.getChild("contentType").getValue(null); 087 if (cTypeId != null) 088 { 089 setContentTypes(Collections.singleton(cTypeId)); 090 } 091 } 092 catch (Exception e) 093 { 094 throw new ConfigurationException("Unable to create local component managers.", configuration, e); 095 } 096 } 097 098 @Override 099 public Set<String> getExcludedContentTypes(Map<String, Object> contextualParameters) 100 { 101 return Collections.emptySet(); 102 } 103 104 @Override 105 public ViewItemContainer getCriteria(Map<String, Object> contextualParameters) 106 { 107 if (super.getCriteria(contextualParameters).getViewItems().isEmpty()) 108 { 109 String contentTypeId = getContentTypes(contextualParameters).iterator().next(); 110 ContentType contentType = _contentTypeExtensionPoint.getExtension(contentTypeId); 111 112 ViewItemContainer criteria = _getCriteria(contentType); 113 setCriteria(criteria); 114 } 115 116 return super.getCriteria(contextualParameters); 117 } 118 119 @Override 120 public ViewItemContainer getFacetedCriteria(Map<String, Object> contextualParameters) 121 { 122 if (super.getFacetedCriteria(contextualParameters).getViewItems().isEmpty()) 123 { 124 setFacetedCriteria(new View()); 125 } 126 127 return super.getFacetedCriteria(contextualParameters); 128 } 129 130 @Override 131 public ViewItemContainer getAdvancedCriteria(Map<String, Object> contextualParameters) 132 { 133 if (super.getAdvancedCriteria(contextualParameters).getViewItems().isEmpty()) 134 { 135 setAdvancedCriteria(new View()); 136 } 137 138 return super.getAdvancedCriteria(contextualParameters); 139 } 140 141 @Override 142 public ViewItemContainer getResultItems(Map<String, Object> contextualParameters) 143 { 144 if (super.getResultItems(contextualParameters).getViewItems().isEmpty()) 145 { 146 String contentTypeId = getContentTypes(contextualParameters).iterator().next(); 147 ContentType contentType = _contentTypeExtensionPoint.getExtension(contentTypeId); 148 149 ViewItemContainer resultItems = _getResultItems(contentType); 150 setResultItems(resultItems); 151 } 152 153 return super.getResultItems(contextualParameters); 154 } 155 156 /** 157 * Retrieves the criteria for the given content type 158 * @param contentType The content type 159 * @return the criteria 160 */ 161 protected ViewItemContainer _getCriteria(ContentType contentType) 162 { 163 View view = Optional.ofNullable(contentType.getView("criteria")) 164 .orElse(contentType.getView("main")); 165 166 View criteria = new View(); 167 SimpleViewItemGroup group = new SimpleViewItemGroup(); 168 group.setRole(ViewItemGroup.FIELDSET_ROLE); 169 criteria.addViewItem(group); 170 171 group.addViewItems(_createReferencingCriteria(contentType, view.getViewItems())); 172 173 if (_hierarchicalReferenceTableContentsHelper.isHierarchical(contentType) && _hierarchicalReferenceTableContentsHelper.supportCandidates(contentType)) 174 { 175 SearchModelCriterionViewItem excludeCandidateCriterion = createExcludeCandidateCriterion(); 176 group.addViewItem(excludeCandidateCriterion); 177 } 178 179 SearchModelCriterionViewItem contributorCriterion = _searchModelCriterionViewItemHelper.createReferencingCriterionViewItem(this, "contributor"); 180 group.addViewItem(contributorCriterion); 181 182 if (!contentType.isMultilingual()) 183 { 184 SearchModelCriterionViewItem contentLanguageCriterion = createContentLanguageCriterion(); 185 group.addViewItem(contentLanguageCriterion); 186 } 187 188 return criteria; 189 } 190 191 /** 192 * Copy the given view items and filter to keep only items that can be used in {@link SearchUIColumn}s. 193 * Also copy the children of view item accessors 194 * @param contentType the content type 195 * @param viewItems the view items to copy 196 * @return the view items copies 197 */ 198 protected List<ViewItem> _createReferencingCriteria(ContentType contentType, List<ViewItem> viewItems) 199 { 200 List<ViewItem> criteria = new ArrayList<>(); 201 202 for (ViewItem viewItem : viewItems) 203 { 204 if (viewItem instanceof ViewItemContainer viewItemContainer) 205 { 206 criteria.addAll(_createReferencingCriteria(contentType, viewItemContainer.getViewItems())); 207 } 208 else if (viewItem instanceof ViewElement viewElement && _filterModelItemForCriteria(viewElement.getDefinition())) 209 { 210 ElementDefinition definition = viewElement.getDefinition(); 211 ViewItem criterion = createReferencingCriterionViewItem(contentType, definition); 212 if (criterion != null) 213 { 214 criteria.add(criterion); 215 } 216 217 if (Content.ATTRIBUTE_TITLE.equals(definition.getName())) 218 { 219 ViewItem likeTitleCriterion = createLikeTitleCriterion(definition); 220 criteria.add(likeTitleCriterion); 221 } 222 } 223 } 224 225 return criteria; 226 } 227 228 /** 229 * Retrieves the criteria used to exclude candidates 230 * @return the criteria used to exclude candidates 231 */ 232 protected SearchModelCriterionViewItem createExcludeCandidateCriterion() 233 { 234 SearchModelCriterionViewItem criterionViewItem = _searchModelCriterionViewItemHelper.createReferencingCriterionViewItem(this, "mixins"); 235 236 @SuppressWarnings("unchecked") 237 SearchModelCriterionDefinition<String> criterion = (SearchModelCriterionDefinition<String>) criterionViewItem.getDefinition(); 238 criterion.setOperator(Operator.NE); 239 criterion.setWidget("edition.hidden"); 240 criterion.setParsedDefaultValues(List.of(new ImmutablePair<>(null, "org.ametys.cms.referencetable.mixin.Candidate"))); 241 242 return criterionViewItem; 243 } 244 245 /** 246 * Returns <code>true</code> if the model item can be used as criteria 247 * @param modelItem the model item 248 * @return <code>true</code> if the model item can be used as criteria 249 */ 250 @SuppressWarnings("static-access") 251 protected boolean _filterModelItemForCriteria(ModelItem modelItem) 252 { 253 String typeId = modelItem.getType().getId(); 254 switch (typeId) 255 { 256 case ModelItemTypeConstants.STRING_TYPE_ID: 257 case ModelItemTypeConstants.MULTILINGUAL_STRING_ELEMENT_TYPE_ID: 258 case ModelItemTypeConstants.DATE_TYPE_ID: 259 case ModelItemTypeConstants.DATETIME_TYPE_ID: 260 case ModelItemTypeConstants.LONG_TYPE_ID: 261 case ModelItemTypeConstants.DOUBLE_TYPE_ID: 262 case ModelItemTypeConstants.BOOLEAN_TYPE_ID: 263 case ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID: 264 case ModelItemTypeConstants.REFERENCE_ELEMENT_TYPE_ID: 265 case ModelItemTypeConstants.USER_ELEMENT_TYPE_ID: 266 return true; 267 case ModelItemTypeConstants.BINARY_ELEMENT_TYPE_ID: 268 case ModelItemTypeConstants.RICH_TEXT_ELEMENT_TYPE_ID: 269 case ModelItemTypeConstants.FILE_ELEMENT_TYPE_ID: 270 case ModelItemTypeConstants.GEOCODE_ELEMENT_TYPE_ID: 271 case ModelItemTypeConstants.COMPOSITE_TYPE_ID: 272 case ModelItemTypeConstants.REPEATER_TYPE_ID: 273 default: 274 return false; 275 } 276 } 277 278 /** 279 * Retrieves the title attribute criterion, with a 'LIKE' operator and an hidden widget 280 * @param titleReference the title reference 281 * @return the like title criterion 282 */ 283 protected SearchModelCriterionViewItem createLikeTitleCriterion(ElementDefinition titleReference) 284 { 285 SearchModelCriterionViewItem criterionViewItem = _searchModelCriterionViewItemHelper.createReferencingCriterionViewItem(this, titleReference, Content.ATTRIBUTE_TITLE); 286 287 SearchModelCriterionDefinition criterion = (SearchModelCriterionDefinition) criterionViewItem.getDefinition(); 288 criterion.setOperator(Operator.LIKE); 289 criterion.setWidget("edition.hidden"); 290 291 return criterionViewItem; 292 } 293 294 /** 295 * Retrieves a {@link SearchModelCriterionViewItem} for referencing the given definition 296 * @param contentType the simple content type. 297 * @param reference the referenced definition. 298 * @return the criterion 299 */ 300 protected SearchModelCriterionViewItem createReferencingCriterionViewItem(ContentType contentType, ElementDefinition reference) 301 { 302 SearchModelCriterionViewItem criterionViewItem = _searchModelCriterionViewItemHelper.createReferencingCriterionViewItem(this, reference, reference.getPath()); 303 SearchModelCriterionDefinition criterion = (SearchModelCriterionDefinition) criterionViewItem.getDefinition(); 304 305 if ( 306 ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID.equals(reference.getType().getId()) 307 && contentType.isReferenceTable() 308 && contentType.getParentAttributeDefinition() 309 .map(ModelItem::getPath) 310 .map(parent -> parent.equals(reference.getPath())) 311 .orElse(false) 312 ) 313 { 314 criterion.setWidget("edition.select-referencetable-content"); 315 316 Map<String, I18nizableText> widgetParameters = criterion.getWidgetParameters(); 317 widgetParameters.put("allowToggleAutoposting", new I18nizableText(String.valueOf(true))); 318 } 319 320 return criterionViewItem; 321 } 322 323 /** 324 * Retrieves the criteria for content language 325 * @return the criteria for content language 326 */ 327 protected SearchModelCriterionViewItem createContentLanguageCriterion() 328 { 329 SearchModelCriterionViewItem criterionViewItem = _searchModelCriterionViewItemHelper.createReferencingCriterionViewItem(this, "contentLanguage"); 330 331 // FIXME For now the simple contents are only created for language 'fr' 332 @SuppressWarnings("unchecked") 333 ElementDefinition<String> criterion = (ElementDefinition<String>) criterionViewItem.getDefinition(); 334 criterion.setWidget("edition.select-language"); 335 criterion.setParsedDefaultValues(List.of(new ImmutablePair<>(null, "CURRENT"))); 336 criterion.setValidator(new DefaultValidator(null, true)); 337 338 return criterionViewItem; 339 } 340 341 /** 342 * Retrieves the result items for the given content type 343 * @param contentType The content type 344 * @return the result items 345 */ 346 protected ViewItemContainer _getResultItems(ContentType contentType) 347 { 348 View view = Optional.ofNullable(contentType.getView("columns")) 349 .orElse(contentType.getView("main")); 350 351 View resultItems = new View(); 352 resultItems.addViewItems(_copyAndFilterViewItemsForColumns(view.getViewItems())); 353 354 resultItems.addViewItem(SearchUIColumnHelper.createModelItemColumn(_systemPropertyExtensionPoint.getExtension("contributor"))); 355 resultItems.addViewItem(SearchUIColumnHelper.createModelItemColumn(_systemPropertyExtensionPoint.getExtension("lastModified"))); 356 357 if (!contentType.isMultilingual()) 358 { 359 resultItems.addViewItem(SearchUIColumnHelper.createModelItemColumn(_systemPropertyExtensionPoint.getExtension("contentLanguage"))); 360 } 361 362 return resultItems; 363 } 364 365 /** 366 * Copy the given view items and filter to keep only items that can be used in {@link SearchUIColumn}s. 367 * Also copy the children of view item accessors 368 * @param viewItems the view items to copy 369 * @return the view items copies 370 */ 371 protected List<ViewItem> _copyAndFilterViewItemsForColumns(List<ViewItem> viewItems) 372 { 373 List<ViewItem> copies = new ArrayList<>(); 374 375 for (ViewItem viewItem : viewItems) 376 { 377 if (!(viewItem instanceof ViewElement) || _filterModelItemForColumn(((ViewElement) viewItem).getDefinition())) 378 { 379 ViewItem copy = viewItem.createInstance(); 380 if (viewItem instanceof ViewItemAccessor viewItemAccessor && !viewItemAccessor.getViewItems().isEmpty()) 381 { 382 assert copy instanceof ViewItemAccessor; 383 ((ViewItemAccessor) copy).addViewItems(_copyAndFilterViewItemsForColumns(viewItemAccessor.getViewItems())); 384 } 385 else if (viewItem instanceof ModelViewItem modelViewItem) 386 { 387 // If the view item is a leaf, create a column 388 ModelItem modelItem = modelViewItem.getDefinition(); 389 copy = SearchUIColumnHelper.createModelItemColumn(modelItem); 390 } 391 392 viewItem.copyTo(copy); 393 copies.add(copy); 394 } 395 } 396 397 return copies; 398 } 399 400 /** 401 * Returns <code>true</code> if model item can be used as column search UI 402 * @param modelItem the model item 403 * @return <code>true</code> if model item can be used as column search UI 404 */ 405 @SuppressWarnings("static-access") 406 protected boolean _filterModelItemForColumn(ModelItem modelItem) 407 { 408 String typeId = modelItem.getType().getId(); 409 switch (typeId) 410 { 411 case ModelItemTypeConstants.STRING_TYPE_ID: 412 case ModelItemTypeConstants.MULTILINGUAL_STRING_ELEMENT_TYPE_ID: 413 case ModelItemTypeConstants.DATE_TYPE_ID: 414 case ModelItemTypeConstants.DATETIME_TYPE_ID: 415 case ModelItemTypeConstants.LONG_TYPE_ID: 416 case ModelItemTypeConstants.DOUBLE_TYPE_ID: 417 case ModelItemTypeConstants.BOOLEAN_TYPE_ID: 418 case ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID: 419 case ModelItemTypeConstants.GEOCODE_ELEMENT_TYPE_ID: 420 case ModelItemTypeConstants.REFERENCE_ELEMENT_TYPE_ID: 421 case ModelItemTypeConstants.USER_ELEMENT_TYPE_ID: 422 return true; 423 case ModelItemTypeConstants.BINARY_ELEMENT_TYPE_ID: 424 case ModelItemTypeConstants.RICH_TEXT_ELEMENT_TYPE_ID: 425 case ModelItemTypeConstants.FILE_ELEMENT_TYPE_ID: 426 default: 427 return false; 428 } 429 } 430}