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