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}