001/* 002 * Copyright 2016 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.plugins.explorer.resources; 017 018import java.io.IOException; 019import java.io.InputStream; 020 021import org.apache.commons.lang3.StringUtils; 022import org.slf4j.Logger; 023import org.slf4j.LoggerFactory; 024 025import org.ametys.plugins.explorer.cmis.CMISRootResourcesCollection; 026import org.ametys.plugins.explorer.resources.actions.ExplorerResourcesDAO; 027import org.ametys.plugins.explorer.resources.jcr.JCRResource; 028import org.ametys.plugins.explorer.resources.jcr.JCRResourcesCollection; 029import org.ametys.plugins.repository.AmetysObject; 030import org.ametys.plugins.repository.AmetysObjectIterable; 031import org.ametys.plugins.repository.AmetysObjectResolver; 032import org.ametys.plugins.repository.AmetysRepositoryException; 033import org.ametys.plugins.repository.TraversableAmetysObject; 034import org.ametys.plugins.repository.UnknownAmetysObjectException; 035import org.ametys.plugins.repository.jcr.NameHelper; 036import org.ametys.plugins.repository.jcr.NameHelper.NameComputationMode; 037import org.ametys.plugins.repository.jcr.NodeHelper; 038import org.ametys.plugins.repository.trash.ConflictingNameException; 039import org.ametys.plugins.repository.trash.TrashableAmetysObject.MergeComputationMode; 040 041/** 042 * Helper class that provides utility methods to work with resources. 043 */ 044 045public final class ResourceHelper 046{ 047 private static final Logger __LOGGER = LoggerFactory.getLogger(ResourceHelper.class); 048 049 private ResourceHelper() 050 { 051 // Hide the default constructor. 052 } 053 054 /** 055 * Get the resource root of a resource. 056 * @param resource The resource. 057 * @return The resource root. 058 */ 059 public static TraversableAmetysObject getResourceRoot(Resource resource) 060 { 061 return _getResourceRoot(resource); 062 } 063 064 /** 065 * Get the resource root of a resource collection. 066 * @param collection The resource collection. 067 * @return The resource root. 068 */ 069 public static TraversableAmetysObject getResourceRoot(ResourceCollection collection) 070 { 071 return _getResourceRoot(collection); 072 } 073 074 /** 075 * Get the resource root of an ametys object. 076 * @param object The object. 077 * @return The resource root of the ametys object. 078 */ 079 private static TraversableAmetysObject _getResourceRoot(AmetysObject object) 080 { 081 AmetysObject current = object; 082 TraversableAmetysObject parent = object.getParent(); 083 084 while (parent != null && parent instanceof ResourceCollection) 085 { 086 current = parent; 087 parent = current.getParent(); 088 } 089 090 return current instanceof TraversableAmetysObject ? (TraversableAmetysObject) current : null; 091 } 092 093 094 /** 095 * Get or create resources collections by path. 096 * Try to retrieve a resources collection with his path. If resources collections are missing, create them. 097 * @param ametysObjectResolver the ametys object resolver 098 * @param path the parent path 099 * @return the resources collection related to the path or null if the path cannot be created 100 */ 101 public static JCRResourcesCollection getOrCreateResourcesCollection(AmetysObjectResolver ametysObjectResolver, String path) 102 { 103 try 104 { 105 AmetysObject object = ametysObjectResolver.resolveByPath(path); 106 if (object instanceof JCRResourcesCollection resourcesCollection) 107 { 108 return resourcesCollection; 109 } 110 // If the parent is not a JCRResourcesCollection, it means that the path corresponds to an object of another type or that we are no longer under root, so we cannot create the path 111 else 112 { 113 return null; 114 } 115 } 116 // The path does not exists, we try to check if the parent exists 117 catch (UnknownAmetysObjectException e) 118 { 119 // Try to create path by creating missing resourcesCollections 120 String parentPath = StringUtils.substringBeforeLast(path, "/"); 121 String resourcesCollectionName = StringUtils.substringAfterLast(path, "/"); 122 123 // Recursively get or create the parent resourcesCollection 124 JCRResourcesCollection parentOfParent = ResourceHelper.getOrCreateResourcesCollection(ametysObjectResolver, parentPath); 125 if (parentOfParent == null) 126 { 127 // The parent path cannot be created, so we cannot create the path 128 return null; 129 } 130 131 // Create the missing resourcesCollection 132 JCRResourcesCollection newResourcesCollection = (JCRResourcesCollection) parentOfParent.createChild(resourcesCollectionName, parentOfParent.getCollectionType()); 133 parentOfParent.saveChanges(); 134 return newResourcesCollection; 135 } 136 // The path is not a TraversableObject, so we are under a resource, abort resources collection creation 137 catch (AmetysRepositoryException e) 138 { 139 return null; 140 } 141 } 142 143 /** 144 * Clone or merge the resource collection to the given parent node keeping the identifiers. 145 * To be able to do that, parent resource collection should be in another workspace than the resource collection to restore. 146 * This method does not save the parent node. 147 * @param resourcesCollectionToRestore The resource collection to restore 148 * @param parentResourceCollection The parent resource collection where the element will be restored 149 * @param mergeComputationMode The merge computation mode (ERROR, RENAME or UPDATE) 150 * @param resourcesDAO The resources DAO 151 * @return the created or merged resource collection 152 * @throws ConflictingNameException if a conflicting name is found and mode is ERROR or null 153 * @throws AmetysRepositoryException if a repository error occurs 154 */ 155 public static JCRResourcesCollection restoreResourcesCollection(JCRResourcesCollection resourcesCollectionToRestore, JCRResourcesCollection parentResourceCollection, MergeComputationMode mergeComputationMode, ExplorerResourcesDAO resourcesDAO) throws ConflictingNameException, AmetysRepositoryException 156 { 157 if (!parentResourceCollection.hasChild(resourcesCollectionToRestore.getName())) 158 { 159 // Simple case: clone the node 160 NodeHelper.cloneNode(resourcesCollectionToRestore.getNode(), parentResourceCollection.getNode(), resourcesCollectionToRestore.getName()); 161 return (JCRResourcesCollection) parentResourceCollection.getChild(resourcesCollectionToRestore.getName()); 162 } 163 164 // Merge case: the node already exists 165 AmetysObject aoChild = parentResourceCollection.getChild(resourcesCollectionToRestore.getName()); 166 if (!(aoChild instanceof JCRResourcesCollection existingCollection)) 167 { 168 throw new ConflictingNameException("The object at path " + aoChild.getPath() + " already exists."); 169 } 170 171 // Iterate over all children of trashElementToCopy and merge them 172 try (AmetysObjectIterable<AmetysObject> children = resourcesCollectionToRestore.getChildren()) 173 { 174 for (AmetysObject child : children) 175 { 176 if (child instanceof JCRResourcesCollection collection) 177 { 178 restoreResourcesCollection(collection, existingCollection, mergeComputationMode, resourcesDAO); 179 } 180 else if (child instanceof JCRResource resource) 181 { 182 // We do not want to call to directly call resource.restoreFromTrash(mergeComputationMode) as we do not want to do any save until all operation is done 183 restoreResource(resource, existingCollection, mergeComputationMode, resourcesDAO); 184 } 185 else if (child instanceof CMISRootResourcesCollection collection) 186 { 187 restoreCMISCollection(collection, existingCollection, mergeComputationMode); 188 } 189 else 190 { 191 // For other types of AmetysObject, we can not/do not want to merge them, but we can at least log some basic info about it 192 __LOGGER.info("Cannot restore child with name {} and type {} under path {} because this type is not supported for merging. It will be ignored during the restore process.", child.getName(), child.getClass().getName(), existingCollection.getPath()); 193 } 194 } 195 } 196 197 return existingCollection; 198 } 199 200 /** 201 * Restore a CMIS root resources collection. 202 * @param cmisResourcesCollectionToRestore The CMIS resources collection to restore 203 * @param parentResourcesCollection The parent resource collection where the element will be restored 204 * @param mergeComputationMode The merge computation mode (ERROR, RENAME or UPDATE) 205 * @return the restored CMIS resource collection 206 * @throws ConflictingNameException if a conflicting name is found and mode is ERROR or null 207 * @throws AmetysRepositoryException if a repository error occurs 208 */ 209 public static CMISRootResourcesCollection restoreCMISCollection(CMISRootResourcesCollection cmisResourcesCollectionToRestore, JCRResourcesCollection parentResourcesCollection, MergeComputationMode mergeComputationMode) throws ConflictingNameException, AmetysRepositoryException 210 { 211 if (!parentResourcesCollection.hasChild(cmisResourcesCollectionToRestore.getName())) 212 { 213 NodeHelper.cloneNode(cmisResourcesCollectionToRestore.getNode(), parentResourcesCollection.getNode(), cmisResourcesCollectionToRestore.getName()); 214 return (CMISRootResourcesCollection) parentResourcesCollection.getChild(cmisResourcesCollectionToRestore.getName()); 215 } 216 217 if (MergeComputationMode.RENAME.equals(mergeComputationMode)) 218 { 219 String resourceName = NameHelper.getUniqueAmetysObjectName(parentResourcesCollection, cmisResourcesCollectionToRestore.getName(), NameComputationMode.USER_FRIENDLY, true); 220 NodeHelper.cloneNode(cmisResourcesCollectionToRestore.getNode(), parentResourcesCollection.getNode(), resourceName); 221 return (CMISRootResourcesCollection) parentResourcesCollection.getChild(resourceName); 222 } 223 else if (MergeComputationMode.UPDATE.equals(mergeComputationMode)) 224 { 225 // Having an existing object with the same name is not a problem for RENAME mode as the new resource will be renamed, 226 // but for UPDATE mode, it means that we want to update the existing object with the content of the object to restore, so we need to check that they share the same type 227 if (!(parentResourcesCollection.getChild(cmisResourcesCollectionToRestore.getName()) instanceof CMISRootResourcesCollection existingCollection)) 228 { 229 throw new ConflictingNameException("The object at path " + cmisResourcesCollectionToRestore.getPath() + " is not a CMISRootResourcesCollection."); 230 } 231 existingCollection.setMountPoint(cmisResourcesCollectionToRestore.getMountPoint()); 232 existingCollection.setPassword(cmisResourcesCollectionToRestore.getPassword()); 233 existingCollection.setRepositoryId(cmisResourcesCollectionToRestore.getRepositoryId()); 234 existingCollection.setRepositoryUrl(cmisResourcesCollectionToRestore.getRepositoryUrl()); 235 existingCollection.setUser(cmisResourcesCollectionToRestore.getUser()); 236 return existingCollection; 237 } 238 // If mode is ERROR or null, we throw an exception as we cannot restore the CMIS collection because a resource collection with the same name already exists 239 else 240 { 241 throw new ConflictingNameException("The object at path " + cmisResourcesCollectionToRestore.getPath() + " already exists."); 242 } 243 244 } 245 246 /** 247 * Restore a resource from the trash into a resource collection. 248 * @param resourceToRestore The resource from the trash to restore 249 * @param parentResourcesCollection The target resource collection to restore into 250 * @param mergeComputationMode The merge computation mode (ERROR, RENAME or UPDATE) 251 * @param resourcesDAO The resources DAO 252 * @return the restored resource 253 * @throws ConflictingNameException if a conflicting name is found and mode is ERROR or null 254 * @throws AmetysRepositoryException if a repository error occurs 255 */ 256 public static JCRResource restoreResource(JCRResource resourceToRestore, JCRResourcesCollection parentResourcesCollection, MergeComputationMode mergeComputationMode, ExplorerResourcesDAO resourcesDAO) throws ConflictingNameException, AmetysRepositoryException 257 { 258 String resourceName = resourceToRestore.getName(); 259 if (parentResourcesCollection.hasChild(resourceName)) 260 { 261 if (MergeComputationMode.RENAME.equals(mergeComputationMode)) 262 { 263 String nameWithoutExtension = StringUtils.substringBeforeLast(resourceName, "."); 264 String extension = StringUtils.substringAfterLast(resourceName, "."); 265 // Get the new node name 266 resourceName = NameHelper.getUniqueAmetysObjectName(parentResourcesCollection, nameWithoutExtension, NameComputationMode.USER_FRIENDLY, true, StringUtils.isEmpty(extension) ? StringUtils.EMPTY : "." + extension); 267 NodeHelper.cloneNode(resourceToRestore.getNode(), parentResourcesCollection.getNode(), resourceName); 268 return (JCRResource) parentResourcesCollection.getChild(resourceName); 269 } 270 else if (MergeComputationMode.UPDATE.equals(mergeComputationMode)) 271 { 272 // Having an existing object with the same name is not a problem for RENAME mode as the new resource will be renamed, 273 // but for UPDATE mode, it means that we want to update the existing object with the content of the object to restore, so we need to check that they share the same type 274 if (!(parentResourcesCollection.getChild(resourceName) instanceof JCRResource existingResource)) 275 { 276 // If existing object is not also a resource, we cannot merge them so we throw an exception 277 throw new ConflictingNameException("The object at path " + resourceToRestore.getPath() + " is not a JCRResource."); 278 } 279 // use try to open/close is 280 try (InputStream is = resourceToRestore.getInputStream()) 281 { 282 resourcesDAO.updateResource(existingResource, is, resourceName); 283 } 284 catch (IOException e) 285 { 286 __LOGGER.warn("An error occurred while closing the ressource " + resourceToRestore.getId(), e); 287 } 288 return existingResource; 289 } 290 // If mode is ERROR or null, we throw an exception as we cannot restore the resource because a resource with the same name already exists 291 else 292 { 293 throw new ConflictingNameException("The object at path " + resourceToRestore.getPath() + " already exists."); 294 } 295 } 296 else 297 { 298 // Clone the ametys object from the trash session to default session 299 NodeHelper.cloneNode(resourceToRestore.getNode(), parentResourcesCollection.getNode(), resourceName); 300 return (JCRResource) parentResourcesCollection.getChild(resourceName); 301 } 302 } 303 304}