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}