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}