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