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;
017
018import java.util.ArrayList;
019import java.util.HashMap;
020import java.util.List;
021import java.util.Map;
022
023import javax.jcr.RepositoryException;
024
025import org.apache.avalon.framework.component.Component;
026import org.apache.avalon.framework.context.Context;
027import org.apache.avalon.framework.context.ContextException;
028import org.apache.avalon.framework.context.Contextualizable;
029import org.apache.avalon.framework.service.ServiceException;
030import org.apache.avalon.framework.service.ServiceManager;
031import org.apache.avalon.framework.service.Serviceable;
032import org.apache.cocoon.ProcessingException;
033import org.apache.cocoon.components.ContextHelper;
034import org.apache.cocoon.environment.Request;
035
036import org.ametys.cms.search.SearchResults;
037import org.ametys.cms.search.solr.SearcherFactory;
038import org.ametys.cms.trash.element.TrashElementDAO;
039import org.ametys.cms.trash.element.TrashElementDAO.TrashElementFilter;
040import org.ametys.cms.trash.element.TrashElementDAO.RestorationReport;
041import org.ametys.cms.trash.model.TrashElementModel;
042import org.ametys.cms.trash.model.TrashSearchModel;
043import org.ametys.core.ui.Callable;
044import org.ametys.plugins.repository.AmetysObject;
045import org.ametys.plugins.repository.AmetysObjectIterable;
046import org.ametys.plugins.repository.provider.RequestAttributeWorkspaceSelector;
047import org.ametys.plugins.repository.trash.TrashElement;
048import org.ametys.plugins.repository.trash.TrashElementType;
049import org.ametys.plugins.repository.trash.TrashElementTypeExtensionPoint;
050import org.ametys.plugins.repository.trash.TrashableAmetysObject;
051import org.ametys.plugins.repository.trash.UnknownParentException;
052import org.ametys.runtime.plugin.component.AbstractLogEnabled;
053
054/**
055 * Trash manager to search in the trash, empty the trash, restore or delete objects from the trash.
056 */
057public class TrashManager extends AbstractLogEnabled implements Component, Serviceable, Contextualizable
058{
059    /** The avalon role */
060    public static final String ROLE = TrashManager.class.getName();
061
062    /** The avalon context */
063    protected Context _context;
064    /** The trash element type extension point */
065    protected TrashElementTypeExtensionPoint _trashElementTypeEP;
066    /** The trash element DAO */
067    protected TrashElementDAO _trashElementDAO;
068    private TrashSearchModel _searchModel;
069    private SearcherFactory _searcherFactory;
070    
071    public void service(ServiceManager serviceManager) throws ServiceException
072    {
073        _trashElementTypeEP = (TrashElementTypeExtensionPoint) serviceManager.lookup(TrashElementTypeExtensionPoint.ROLE);
074        _trashElementDAO = (TrashElementDAO) serviceManager.lookup(org.ametys.plugins.repository.trash.TrashElementDAO.ROLE);
075        _searchModel = (TrashSearchModel) serviceManager.lookup(TrashSearchModel.ROLE);
076        _searcherFactory = (SearcherFactory) serviceManager.lookup(SearcherFactory.ROLE);
077    }
078    
079    public void contextualize(Context context) throws ContextException
080    {
081        _context = context;
082    }
083    
084    /**
085     * Get the search model for trash tool.
086     * @return The search model as JSON
087     */
088    @Callable(rights = "CMS_Rights_Trash")
089    public Map<String, Object> getSearchModel()
090    {
091        return _searchModel.toJSON();
092    }
093    
094    /**
095     * Search trash elements from criteria and facets, and taking account of sorting, grouping and pagination.
096     * @param jsonParams The JSON parameters of the search
097     * @return The search results as JSON
098     * @throws Exception if an exception occurs
099     */
100    @SuppressWarnings("unchecked")
101    @Callable(rights = "CMS_Rights_Trash")
102    public Map<String, Object> search(Map<String, Object> jsonParams) throws Exception
103    {
104        Request request = ContextHelper.getRequest(_context);
105        String originalWorkspace = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
106        
107        try
108        {
109            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, TrashConstants.TRASH_WORKSPACE);
110            
111            SearchResults<TrashElement> results = _searcherFactory.create()
112                .withFilterQueries(_searchModel.getFilterQueries())
113                .withQuery(_searchModel.getQuery((Map<String, Object>) jsonParams.getOrDefault("values", Map.of())))
114                .withFacets(_searchModel.getFacetDefinitions())
115                .withFacetValues((Map<String, List<String>>) jsonParams.getOrDefault("facetValues", Map.of()))
116                .withSort(_searchModel.getSortDefinitions((String) jsonParams.get("sort"), (String) jsonParams.get("group")))
117                .withLimits((int) jsonParams.getOrDefault("start", 0), (int) jsonParams.getOrDefault("limit", Integer.MAX_VALUE))
118                .setCheckRights(false)
119                .searchWithFacets();
120            
121            try (AmetysObjectIterable<TrashElement> objects = results.getObjects())
122            {
123                List<Map<String, Object>> items = new ArrayList<>();
124                for (TrashElement trashElement : objects)
125                {
126                    Map<String, Object> json = trashElement.dataToJSON();
127                    
128                    String typeId = trashElement.getValue(TrashElementModel.TRASH_TYPE);
129                    TrashElementType type = _trashElementTypeEP.getExtension(typeId);
130                    if (type != null)
131                    {
132                        json.putAll(type.getIcon(trashElement));
133                    }
134                    
135                    items.add(json);
136                }
137                
138                return Map.of(
139                        "total", results.getTotalCount(),
140                        "facets", _searchModel.getFacetsValues(results.getFacetResults()),
141                        "items", items
142                        );
143            }
144        }
145        catch (Exception e)
146        {
147            throw new ProcessingException("Cannot search for trash elements: " + e.getMessage(), e);
148        }
149        finally
150        {
151            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, originalWorkspace);
152        }
153    }
154    
155    /**
156     * Empty the trash.
157     * @throws RepositoryException if an error occurs
158     */
159    @Callable(rights = "CMS_Rights_Trash")
160    public void empty() throws RepositoryException
161    {
162        _trashElementDAO.empty(new TrashElementFilter(0));
163    }
164    
165    /**
166     * Restore an {@link AmetysObject} from the trash
167     * @param trashElementId The trash element identifier
168     * @return <code>true</code> if it has been restored
169     */
170    @Callable(rights = "CMS_Rights_Trash")
171    public Map<String, Object> restore(String trashElementId)
172    {
173        try
174        {
175            RestorationReport report = _trashElementDAO.restore(trashElementId);
176            
177            TrashableAmetysObject restoredObject = report.restoredObject();
178            List<Map<String, Object>> linkedObjects = new ArrayList<>();
179            for (TrashableAmetysObject linkedObject : report.restoredLinkedObject())
180            {
181                _trashElementTypeEP.getFirstSupportingExtension(linkedObject)
182                    .ifPresent(type ->
183                        linkedObjects.add(Map.of(
184                                "id", linkedObject.getId(),
185                                "type", type.getMessageTargetType(linkedObject)
186                            ))
187                    );
188            }
189            
190            Map<String, Object> result = new HashMap<>();
191            result.put("success", true);
192            TrashElementType type = _trashElementTypeEP.getFirstSupportingExtension(restoredObject).get();
193            result.put("restorationDescription", type.getRestorationDescription(restoredObject));
194            result.put("notificationOpenToolAction", type.getNotificationOpenToolAction(restoredObject));
195            // separate the restored object from the linked in case the client needs to differentiate them
196            result.put("restoredObject", Map.of(
197                    "id", restoredObject.getId(),
198                    "type", type.getMessageTargetType(restoredObject)
199                    ));
200            result.put("restoredLinkedObjects", linkedObjects);
201            
202            return result;
203        }
204        catch (UnknownParentException e)
205        {
206            getLogger().warn("Failed to restore trash element '{}'", trashElementId, e);
207            return Map.of("success", false, "reason", "unknown-parent");
208        }
209    }
210    
211    /**
212     * Delete definitively an object from the trash.
213     * @param trashElementId The trash element identifier
214     * @throws Exception if an error occurs
215     */
216    @Callable(rights = "CMS_Rights_Trash")
217    public void delete(String trashElementId) throws Exception
218    {
219        _trashElementDAO.remove(trashElementId);
220    }
221}