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