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}