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