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}