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}