001/* 002 * Copyright 2025 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.trash.model; 017 018import java.util.ArrayList; 019import java.util.HashMap; 020import java.util.LinkedHashMap; 021import java.util.List; 022import java.util.Map; 023import java.util.Optional; 024import java.util.Set; 025import java.util.stream.Collectors; 026 027import org.apache.avalon.framework.component.Component; 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.StringUtils; 032 033import org.ametys.cms.content.indexing.solr.SolrFieldNames; 034import org.ametys.cms.data.type.indexing.IndexableElementType; 035import org.ametys.cms.data.type.indexing.SortableIndexableElementType; 036import org.ametys.cms.search.SortOrder; 037import org.ametys.cms.search.model.impl.AbstractCriterionDefinition; 038import org.ametys.cms.search.query.AndQuery; 039import org.ametys.cms.search.query.BooleanQuery; 040import org.ametys.cms.search.query.DocumentTypeQuery; 041import org.ametys.cms.search.query.Query; 042import org.ametys.cms.search.query.Query.Operator; 043import org.ametys.cms.search.solr.SearcherFactory.FacetDefinition; 044import org.ametys.cms.search.solr.SearcherFactory.SortDefinition; 045import org.ametys.cms.search.ui.model.SearchUIColumn; 046import org.ametys.cms.search.ui.model.SearchUIColumnHelper; 047import org.ametys.cms.trash.ContentTrashElementType; 048import org.ametys.cms.trash.TrashManager; 049import org.ametys.core.util.JSONUtils; 050import org.ametys.runtime.i18n.I18nizableText; 051import org.ametys.runtime.model.DefinitionContext; 052import org.ametys.runtime.model.ElementDefinition; 053import org.ametys.runtime.model.Model; 054import org.ametys.runtime.model.ModelItem; 055import org.ametys.runtime.model.type.DataContext; 056 057/** 058 * This class represent the search model for trash elements. 059 */ 060public class TrashSearchModel implements Component, Serviceable 061{ 062 /** The avalon role */ 063 public static final String ROLE = TrashSearchModel.class.getName(); 064 065 private static final Set<String> __HIDDEN_COLUMNS = Set.of(TrashElementModel.PARENT_PATH, TrashElementModel.DELETED_OBJECT, TrashElementModel.LINKED_OBJECTS); 066 067 private JSONUtils _jsonUtils; 068 private Model _model; 069 070 private Map<String, TrashElementCriterionDefinition> _criteria; 071 private Map<String, TrashElementFacetDefinition> _facets; 072 private Map<String, Object> _json; 073 074 075 public void service(ServiceManager serviceManager) throws ServiceException 076 { 077 _jsonUtils = (JSONUtils) serviceManager.lookup(JSONUtils.ROLE); 078 _model = (TrashElementModel) serviceManager.lookup(TrashElementModel.ROLE); 079 } 080 081 /** 082 * Get the JSON representation of the {@link TrashSearchModel}. 083 * @return the search model as JSON 084 */ 085 public Map<String, Object> toJSON() 086 { 087 if (_json == null) 088 { 089 _json = new HashMap<>(); 090 _json.put("searchRole", TrashManager.ROLE); 091 _json.put("searchMethodName", "search"); 092 _json.put("criteria", _getCriteria().values().stream().collect(Collectors.toMap(TrashElementCriterionDefinition::getCriterionName, c -> c.toJSON(DefinitionContext.newInstance()), (a, b) -> b, LinkedHashMap::new))); 093 _json.put("columns", _model.getModelItems().stream().filter(item -> !TrashElementModel.HIDDEN.equals(item.getName())).map(this::modelItem2Column).map(col -> col.toJSON(DefinitionContext.newInstance())).toList()); 094 } 095 return _json; 096 } 097 098 /** 099 * The filter queries to retrieve the trash element 100 * @return a list of queries to apply 101 */ 102 public List<Query> getFilterQueries() 103 { 104 List<Query> queries = new ArrayList<>(); 105 106 queries.add(new DocumentTypeQuery(SolrFieldNames.TYPE_TRASH_ELEMENT)); 107 queries.add(new BooleanQuery(TrashElementModel.HIDDEN, false)); 108 109 return queries; 110 } 111 112 /** 113 * Get the query from search criteria. 114 * @param criteria the search criteria 115 * @return a Solr query 116 */ 117 @SuppressWarnings("unchecked") 118 public Query getQuery(Map<String, Object> criteria) 119 { 120 List<Query> queries = new ArrayList<>(); 121 122 for (Map.Entry<String, Object> criterionValue : criteria.entrySet()) 123 { 124 TrashElementCriterionDefinition criterion = _getCriteria().get(criterionValue.getKey()); 125 126 Optional.ofNullable(criterionValue.getValue()) 127 .map(v -> criterion.convertQueryValue(v, Map.of())) 128 .map(v -> criterion.getQuery(v, (Operator) null, null, Map.of())) 129 .ifPresent(queries::add); 130 } 131 132 return new AndQuery(queries); 133 } 134 135 /** 136 * Get the facet definitions as list. 137 * @return a list of facet definitions 138 */ 139 public List<FacetDefinition> getFacetDefinitions() 140 { 141 return _getFacets().values().stream().map(TrashElementFacetDefinition::getFacetDefinition).toList(); 142 } 143 144 /** 145 * Get the facets values for the search tool. 146 * @param facetResults the facets results from the search 147 * @return the facets values 148 */ 149 public List<Map<String, Object>> getFacetsValues(Map<String, Map<String, Integer>> facetResults) 150 { 151 return facetResults.entrySet() 152 .stream() 153 .filter(entry -> _getFacets().containsKey(entry.getKey())) 154 .map(entry -> _getFacets().get(entry.getKey()).toJSON(entry.getValue())) 155 .toList(); 156 } 157 158 /** 159 * Get the sort definitions depending on search sorts and groups. 160 * @param sortString the sorts 161 * @param groupString the groups 162 * @return the list of sort definitions. 163 */ 164 @SuppressWarnings("unchecked") 165 public List<SortDefinition> getSortDefinitions(String sortString, String groupString) 166 { 167 List<Object> sortList = new ArrayList<>(_jsonUtils.convertJsonToList(sortString)); 168 if (StringUtils.isNotEmpty(groupString)) 169 { 170 // Grouping will be treated server side as a sort. It just needs to be before all the sorters 171 sortList.add(0, _jsonUtils.convertJsonToMap(groupString)); 172 } 173 174 return sortList.stream() 175 .map(obj -> (Map<String, String>) obj) 176 .map(this::getSortDefinition) 177 .toList(); 178 } 179 180 private SortDefinition getSortDefinition(Map<String, String> map) 181 { 182 ModelItem modelItem = _model.getModelItem(map.get("property")); 183 String sortFieldName = modelItem.getName() + ((SortableIndexableElementType) modelItem.getType()).getSortFieldSuffix(DataContext.newInstance()); 184 SortOrder sortOrder = "ASC".equals(map.get("direction")) ? SortOrder.ASC : SortOrder.DESC; 185 return new SortDefinition(sortFieldName, sortOrder); 186 } 187 188 private Map<String, TrashElementCriterionDefinition> _getCriteria() 189 { 190 if (_criteria == null) 191 { 192 _criteria = new LinkedHashMap<>(); 193 _addCriterion(TrashElementModel.TITLE, Operator.SEARCH, null); 194 _addCriterion(TrashElementModel.TRASH_TYPE, Operator.EQ, null); 195 // add a criteria for the content type to make it user friendly 196 _addCriterion(ContentTrashElementType.CONTENT_TYPES, Operator.EQ, null); 197 _addCriterion(TrashElementModel.DATE, Operator.GT, new I18nizableText("plugin.cms", "UITOOL_TRASH_CRITERION_DELETION_DATE_AFTER")); 198 _addCriterion(TrashElementModel.DATE, Operator.LT, new I18nizableText("plugin.cms", "UITOOL_TRASH_CRITERION_DELETION_DATE_BEFORE")); 199 _addCriterion(TrashElementModel.AUTHOR, Operator.EQ, null); 200 } 201 return _criteria; 202 } 203 204 private void _addCriterion(String name, Operator operator, I18nizableText label) 205 { 206 TrashElementCriterionDefinition criterion = new TrashElementCriterionDefinition((ElementDefinition) _model.getModelItem(name), name, operator); 207 if (label != null) 208 { 209 criterion.setLabel(label); 210 } 211 _criteria.put(criterion.getCriterionName(), criterion); 212 } 213 214 private Map<String, TrashElementFacetDefinition> _getFacets() 215 { 216 if (_facets == null) 217 { 218 _facets = new LinkedHashMap<>(); 219 _addFacet(TrashElementModel.TRASH_TYPE); 220 _addFacet(TrashElementModel.AUTHOR); 221 } 222 return _facets; 223 } 224 225 private void _addFacet(String name) 226 { 227 TrashElementFacetDefinition facet = new TrashElementFacetDefinition(_model.getModelItem(name)); 228 _facets.put(name, facet); 229 } 230 231 /** 232 * Convert the model item to search ui column 233 * @param modelItem the model item 234 * @return the column 235 */ 236 protected SearchUIColumn modelItem2Column(ModelItem modelItem) 237 { 238 SearchUIColumn<?> column = SearchUIColumnHelper.createModelItemColumn(modelItem); 239 column.setHidden(__HIDDEN_COLUMNS.contains(modelItem.getName())); 240 241 if (TrashElementModel.TITLE.equals(modelItem.getPath())) 242 { 243 column.setRenderer(Optional.of("Ametys.grid.GridColumnHelper.renderTextWithIcon")); 244 } 245 return column; 246 } 247 248 private static class TrashElementFacetDefinition 249 { 250 private ModelItem _modelItem; 251 private FacetDefinition _facetDefinition; 252 253 public TrashElementFacetDefinition(ModelItem item) 254 { 255 _modelItem = item; 256 _facetDefinition = new FacetDefinition(_modelItem.getName(), _modelItem.getName() + ((IndexableElementType) _modelItem.getType()).getFacetFieldSuffix(DataContext.newInstance())); 257 } 258 259 public Map<String, Object> toJSON(Map<String, Integer> facetValues) 260 { 261 return Map.of( 262 "name", _modelItem.getName(), 263 "label", _modelItem.getLabel(), 264 "type", "criterion", 265 "children", _getValues(facetValues) 266 ); 267 } 268 269 private List<Map<String, Object>> _getValues(Map<String, Integer> facetValues) 270 { 271 IndexableElementType type = (IndexableElementType) _modelItem.getType(); 272 273 DataContext context = DataContext.newInstance().withModelItem(_modelItem); 274 275 return facetValues 276 .entrySet() 277 .stream() 278 .map( 279 facetValue -> 280 Map.of( 281 "value", facetValue.getKey(), 282 "count", facetValue.getValue(), 283 "label", type.getFacetLabel(facetValue.getKey(), context), 284 "type", "facet" 285 ) 286 ) 287 .toList(); 288 } 289 290 public FacetDefinition getFacetDefinition() 291 { 292 return _facetDefinition; 293 } 294 } 295 296 private static class TrashElementCriterionDefinition<T> extends AbstractCriterionDefinition<T> 297 { 298 private String _criterionName; 299 private Operator _operator; 300 301 public TrashElementCriterionDefinition(ElementDefinition reference, String referencePath, Operator operator) 302 { 303 this.setName(reference.getName()); 304 this.setLabel(reference.getLabel()); 305 this.setDescription(reference.getDescription()); 306 this.setEnumerator(reference.getEnumerator()); 307 this.setModel(reference.getModel()); 308 this.setSolrFacetFieldName(reference.getName() + ((IndexableElementType) reference.getType()).getIndexingFieldSuffix(DataContext.newInstance())); 309 this.setType(reference.getType()); 310 this.setMultiple(reference.isMultiple()); 311 _operator = operator; 312 313 _criterionName = referencePath + "-" + operator.toString().toLowerCase(); 314 } 315 316 @Override 317 public Query getQuery(Object value, Operator operator, Map<String, Object> allValues, String language, Map<String, Object> contextualParameters) 318 { 319 return super.getQuery(value, operator != null ? operator : _operator, allValues, language, contextualParameters); 320 } 321 322 public String getCriterionName() 323 { 324 return _criterionName; 325 } 326 } 327}