Introduce JCR subtree export to simple xml.
[lgpl/argeo-commons.git] / org.argeo.jcr / src / org / argeo / jcr / JcrUtils.java
index c2450e8c7f8f63bc3f04bdc016742764c1a3cb4a..3be8be184b25f269d581d09f2bf541980883143d 100644 (file)
@@ -1,18 +1,3 @@
-/*
- * Copyright (C) 2007-2012 Argeo GmbH
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *         http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
 package org.argeo.jcr;
 
 import java.io.ByteArrayInputStream;
@@ -21,11 +6,19 @@ import java.io.File;
 import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
 import java.net.MalformedURLException;
 import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
 import java.security.Principal;
 import java.text.DateFormat;
 import java.text.ParseException;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Calendar;
 import java.util.Collections;
@@ -37,6 +30,8 @@ import java.util.Map;
 import java.util.TreeMap;
 
 import javax.jcr.Binary;
+import javax.jcr.Credentials;
+import javax.jcr.ImportUUIDBehavior;
 import javax.jcr.NamespaceRegistry;
 import javax.jcr.NoSuchWorkspaceException;
 import javax.jcr.Node;
@@ -49,6 +44,7 @@ import javax.jcr.RepositoryException;
 import javax.jcr.Session;
 import javax.jcr.Value;
 import javax.jcr.Workspace;
+import javax.jcr.nodetype.NoSuchNodeTypeException;
 import javax.jcr.nodetype.NodeType;
 import javax.jcr.observation.EventListener;
 import javax.jcr.query.Query;
@@ -61,25 +57,19 @@ import javax.jcr.security.AccessControlPolicyIterator;
 import javax.jcr.security.Privilege;
 
 import org.apache.commons.io.IOUtils;
-import org.apache.commons.logging.Log;
-import org.apache.commons.logging.LogFactory;
-import org.argeo.util.DigestUtils;
 
 /** Utility methods to simplify common JCR operations. */
 public class JcrUtils {
 
-       final private static Log log = LogFactory.getLog(JcrUtils.class);
+//     final private static Log log = LogFactory.getLog(JcrUtils.class);
 
        /**
         * Not complete yet. See
         * http://www.day.com/specs/jcr/2.0/3_Repository_Model.html#3.2.2%20Local
         * %20Names
         */
-       public final static char[] INVALID_NAME_CHARACTERS = { '/', ':', '[', ']', '|',
-                       '*', /*
-                                        * invalid XML chars :
-                                        */
-                       '<', '>', '&' };
+       public final static char[] INVALID_NAME_CHARACTERS = { '/', ':', '[', ']', '|', '*', /* invalid for XML: */ '<',
+                       '>', '&' };
 
        /** Prevents instantiation */
        private JcrUtils() {
@@ -89,8 +79,7 @@ public class JcrUtils {
         * Queries one single node.
         * 
         * @return one single node or null if none was found
-        * @throws ArgeoJcrException
-        *             if more than one node was found
+        * @throws JcrException if more than one node was found
         */
        public static Node querySingleNode(Query query) {
                NodeIterator nodeIterator;
@@ -98,7 +87,7 @@ public class JcrUtils {
                        QueryResult queryResult = query.execute();
                        nodeIterator = queryResult.getNodes();
                } catch (RepositoryException e) {
-                       throw new ArgeoJcrException("Cannot execute query " + query, e);
+                       throw new JcrException("Cannot execute query " + query, e);
                }
                Node node;
                if (nodeIterator.hasNext())
@@ -107,7 +96,7 @@ public class JcrUtils {
                        return null;
 
                if (nodeIterator.hasNext())
-                       throw new ArgeoJcrException("Query returned more than one node.");
+                       throw new IllegalArgumentException("Query returned more than one node.");
                return node;
        }
 
@@ -116,7 +105,7 @@ public class JcrUtils {
                if (path.equals("/"))
                        return "";
                if (path.charAt(0) != '/')
-                       throw new ArgeoJcrException("Path " + path + " must start with a '/'");
+                       throw new IllegalArgumentException("Path " + path + " must start with a '/'");
                String pathT = path;
                if (pathT.charAt(pathT.length() - 1) == '/')
                        pathT = pathT.substring(0, pathT.length() - 2);
@@ -128,9 +117,9 @@ public class JcrUtils {
        /** Retrieves the parent path of the provided path */
        public static String parentPath(String path) {
                if (path.equals("/"))
-                       throw new ArgeoJcrException("Root path '/' has no parent path");
+                       throw new IllegalArgumentException("Root path '/' has no parent path");
                if (path.charAt(0) != '/')
-                       throw new ArgeoJcrException("Path " + path + " must start with a '/'");
+                       throw new IllegalArgumentException("Path " + path + " must start with a '/'");
                String pathT = path;
                if (pathT.charAt(pathT.length() - 1) == '/')
                        pathT = pathT.substring(0, pathT.length() - 2);
@@ -159,7 +148,7 @@ public class JcrUtils {
                        path.append(u.getPath());
                        return path.toString();
                } catch (MalformedURLException e) {
-                       throw new ArgeoJcrException("Cannot generate URL path for " + url, e);
+                       throw new IllegalArgumentException("Cannot generate URL path for " + url, e);
                }
        }
 
@@ -171,8 +160,10 @@ public class JcrUtils {
                        node.setProperty(Property.JCR_HOST, u.getHost());
                        node.setProperty(Property.JCR_PORT, Integer.toString(u.getPort()));
                        node.setProperty(Property.JCR_PATH, normalizePath(u.getPath()));
-               } catch (Exception e) {
-                       throw new ArgeoJcrException("Cannot set URL " + url + " as nt:address properties", e);
+               } catch (RepositoryException e) {
+                       throw new JcrException("Cannot set URL " + url + " as nt:address properties", e);
+               } catch (MalformedURLException e) {
+                       throw new IllegalArgumentException("Cannot set URL " + url + " as nt:address properties", e);
                }
        }
 
@@ -184,8 +175,10 @@ public class JcrUtils {
                                        (int) node.getProperty(Property.JCR_PORT).getLong(),
                                        node.getProperty(Property.JCR_PATH).getString());
                        return u.toString();
-               } catch (Exception e) {
-                       throw new ArgeoJcrException("Cannot get URL from nt:address properties of " + node, e);
+               } catch (RepositoryException e) {
+                       throw new JcrException("Cannot get URL from nt:address properties of " + node, e);
+               } catch (MalformedURLException e) {
+                       throw new IllegalArgumentException("Cannot get URL from nt:address properties of " + node, e);
                }
        }
 
@@ -239,10 +232,8 @@ public class JcrUtils {
        /**
         * The provided data as a path ('/' at the end, not the beginning)
         * 
-        * @param cal
-        *            the date
-        * @param addHour
-        *            whether to add hour as well
+        * @param cal     the date
+        * @param addHour whether to add hour as well
         */
        public static String dateAsPath(Calendar cal, Boolean addHour) {
                StringBuffer buf = new StringBuffer(14);
@@ -284,7 +275,7 @@ public class JcrUtils {
                        calendar.setTime(date);
                        return calendar;
                } catch (ParseException e) {
-                       throw new ArgeoJcrException("Cannot parse " + value + " with date format " + dateFormat, e);
+                       throw new IllegalArgumentException("Cannot parse " + value + " with date format " + dateFormat, e);
                }
 
        }
@@ -292,7 +283,7 @@ public class JcrUtils {
        /** The last element of a path. */
        public static String lastPathElement(String path) {
                if (path.charAt(path.length() - 1) == '/')
-                       throw new ArgeoJcrException("Path " + path + " cannot end with '/'");
+                       throw new IllegalArgumentException("Path " + path + " cannot end with '/'");
                int index = path.lastIndexOf('/');
                if (index < 0)
                        return path;
@@ -307,7 +298,7 @@ public class JcrUtils {
                try {
                        return node.getName();
                } catch (RepositoryException e) {
-                       throw new ArgeoJcrException("Cannot get name from " + node, e);
+                       throw new JcrException("Cannot get name from " + node, e);
                }
        }
 
@@ -319,24 +310,50 @@ public class JcrUtils {
                try {
                        return node.getProperty(propertyName).getString();
                } catch (RepositoryException e) {
-                       throw new ArgeoJcrException("Cannot get name from " + node, e);
+                       throw new JcrException("Cannot get name from " + node, e);
                }
        }
 
+//     /**
+//      * Routine that get the child with this name, adding it if it does not already
+//      * exist
+//      */
+//     public static Node getOrAdd(Node parent, String name, String primaryNodeType) throws RepositoryException {
+//             return parent.hasNode(name) ? parent.getNode(name) : parent.addNode(name, primaryNodeType);
+//     }
+
        /**
-        * Routine that get the child with this name, adding id it does not already
+        * Routine that get the child with this name, adding it if it does not already
         * exist
         */
-       public static Node getOrAdd(Node parent, String childName, String childPrimaryNodeType) throws RepositoryException {
-               return parent.hasNode(childName) ? parent.getNode(childName) : parent.addNode(childName, childPrimaryNodeType);
+       public static Node getOrAdd(Node parent, String name, String primaryNodeType, String... mixinNodeTypes)
+                       throws RepositoryException {
+               Node node;
+               if (parent.hasNode(name)) {
+                       node = parent.getNode(name);
+                       if (primaryNodeType != null && !node.isNodeType(primaryNodeType))
+                               throw new IllegalArgumentException("Node " + node + " exists but is of primary node type "
+                                               + node.getPrimaryNodeType().getName() + ", not " + primaryNodeType);
+                       for (String mixin : mixinNodeTypes) {
+                               if (!node.isNodeType(mixin))
+                                       node.addMixin(mixin);
+                       }
+                       return node;
+               } else {
+                       node = primaryNodeType != null ? parent.addNode(name, primaryNodeType) : parent.addNode(name);
+                       for (String mixin : mixinNodeTypes) {
+                               node.addMixin(mixin);
+                       }
+                       return node;
+               }
        }
 
        /**
-        * Routine that get the child with this name, adding id it does not already
+        * Routine that get the child with this name, adding it if it does not already
         * exist
         */
-       public static Node getOrAdd(Node parent, String childName) throws RepositoryException {
-               return parent.hasNode(childName) ? parent.getNode(childName) : parent.addNode(childName);
+       public static Node getOrAdd(Node parent, String name) throws RepositoryException {
+               return parent.hasNode(name) ? parent.getNode(name) : parent.addNode(name);
        }
 
        /** Convert a {@link NodeIterator} to a list of {@link Node} */
@@ -362,7 +379,16 @@ public class JcrUtils {
                                return null;
                        return node.getProperty(propertyName).getString();
                } catch (RepositoryException e) {
-                       throw new ArgeoJcrException("Cannot get property " + propertyName + " of " + node, e);
+                       throw new JcrException("Cannot get property " + propertyName + " of " + node, e);
+               }
+       }
+
+       /** Concisely get the path of the given node. */
+       public static String getPath(Node node) {
+               try {
+                       return node.getPath();
+               } catch (RepositoryException e) {
+                       throw new JcrException("Cannot get path of " + node, e);
                }
        }
 
@@ -371,7 +397,7 @@ public class JcrUtils {
                try {
                        return node.getProperty(propertyName).getBoolean();
                } catch (RepositoryException e) {
-                       throw new ArgeoJcrException("Cannot get property " + propertyName + " of " + node, e);
+                       throw new JcrException("Cannot get property " + propertyName + " of " + node, e);
                }
        }
 
@@ -380,7 +406,7 @@ public class JcrUtils {
                try {
                        return getBinaryAsBytes(node.getProperty(propertyName));
                } catch (RepositoryException e) {
-                       throw new ArgeoJcrException("Cannot get property " + propertyName + " of " + node, e);
+                       throw new JcrException("Cannot get property " + propertyName + " of " + node, e);
                }
        }
 
@@ -398,8 +424,7 @@ public class JcrUtils {
        /**
         * Create sub nodes relative to a parent node
         * 
-        * @param nodeType
-        *            the type of the leaf node
+        * @param nodeType the type of the leaf node
         */
        public static Node mkdirs(Node parentNode, String relativePath, String nodeType) {
                return mkdirs(parentNode, relativePath, nodeType, null);
@@ -408,8 +433,7 @@ public class JcrUtils {
        /**
         * Create sub nodes relative to a parent node
         * 
-        * @param nodeType
-        *            the type of the leaf node
+        * @param nodeType the type of the leaf node
         */
        public static Node mkdirs(Node parentNode, String relativePath, String nodeType, String intermediaryNodeType) {
                List<String> tokens = tokenize(relativePath);
@@ -429,24 +453,24 @@ public class JcrUtils {
                        }
                        return currParent;
                } catch (RepositoryException e) {
-                       throw new ArgeoJcrException("Cannot mkdirs relative path " + relativePath + " from " + parentNode, e);
+                       throw new JcrException("Cannot mkdirs relative path " + relativePath + " from " + parentNode, e);
                }
        }
 
        /**
-        * Synchronized and save is performed, to avoid race conditions in
-        * initializers leading to duplicate nodes.
+        * Synchronized and save is performed, to avoid race conditions in initializers
+        * leading to duplicate nodes.
         */
        public synchronized static Node mkdirsSafe(Session session, String path, String type) {
                try {
                        if (session.hasPendingChanges())
-                               throw new ArgeoJcrException("Session has pending changes, save them first.");
+                               throw new IllegalStateException("Session has pending changes, save them first.");
                        Node node = mkdirs(session, path, type);
                        session.save();
                        return node;
                } catch (RepositoryException e) {
                        discardQuietly(session);
-                       throw new ArgeoJcrException("Cannot safely make directories", e);
+                       throw new JcrException("Cannot safely make directories", e);
                }
        }
 
@@ -460,30 +484,28 @@ public class JcrUtils {
        }
 
        /**
-        * @param type
-        *            the type of the leaf node
+        * @param type the type of the leaf node
         */
        public static Node mkdirs(Session session, String path, String type) {
                return mkdirs(session, path, type, null, false);
        }
 
        /**
-        * Creates the nodes making path, if they don't exist. This is up to the
-        * caller to save the session. Use with caution since it can create
-        * duplicate nodes if used concurrently. Requires read access to the root
-        * node of the workspace.
+        * Creates the nodes making path, if they don't exist. This is up to the caller
+        * to save the session. Use with caution since it can create duplicate nodes if
+        * used concurrently. Requires read access to the root node of the workspace.
         */
        public static Node mkdirs(Session session, String path, String type, String intermediaryNodeType,
                        Boolean versioning) {
                try {
-                       if (path.equals('/'))
+                       if (path.equals("/"))
                                return session.getRootNode();
 
                        if (session.itemExists(path)) {
                                Node node = session.getNode(path);
                                // check type
                                if (type != null && !node.isNodeType(type) && !node.getPath().equals("/"))
-                                       throw new ArgeoJcrException("Node " + node + " exists but is of type "
+                                       throw new IllegalArgumentException("Node " + node + " exists but is of type "
                                                        + node.getPrimaryNodeType().getName() + " not of type " + type);
                                // TODO: check versioning
                                return node;
@@ -510,8 +532,8 @@ public class JcrUtils {
                                                currentNode = currentNode.addNode(part);
                                        if (versioning)
                                                currentNode.addMixin(NodeType.MIX_VERSIONABLE);
-                                       if (log.isTraceEnabled())
-                                               log.debug("Added folder " + part + " as " + current);
+//                                     if (log.isTraceEnabled())
+//                                             log.debug("Added folder " + part + " as " + current);
                                } else {
                                        currentNode = (Node) session.getItem(current.toString());
                                }
@@ -519,7 +541,7 @@ public class JcrUtils {
                        return currentNode;
                } catch (RepositoryException e) {
                        discardQuietly(session);
-                       throw new ArgeoJcrException("Cannot mkdirs " + path, e);
+                       throw new JcrException("Cannot mkdirs " + path, e);
                } finally {
                }
        }
@@ -580,20 +602,18 @@ public class JcrUtils {
        // }
 
        /**
-        * Safe and repository implementation independent registration of a
-        * namespace.
+        * Safe and repository implementation independent registration of a namespace.
         */
        public static void registerNamespaceSafely(Session session, String prefix, String uri) {
                try {
                        registerNamespaceSafely(session.getWorkspace().getNamespaceRegistry(), prefix, uri);
                } catch (RepositoryException e) {
-                       throw new ArgeoJcrException("Cannot find namespace registry", e);
+                       throw new JcrException("Cannot find namespace registry", e);
                }
        }
 
        /**
-        * Safe and repository implementation independent registration of a
-        * namespace.
+        * Safe and repository implementation independent registration of a namespace.
         */
        public static void registerNamespaceSafely(NamespaceRegistry nr, String prefix, String uri) {
                try {
@@ -602,94 +622,94 @@ public class JcrUtils {
                                if (pref.equals(prefix)) {
                                        String registeredUri = nr.getURI(pref);
                                        if (!registeredUri.equals(uri))
-                                               throw new ArgeoJcrException("Prefix " + pref + " already registered for URI " + registeredUri
-                                                               + " which is different from provided URI " + uri);
+                                               throw new IllegalArgumentException("Prefix " + pref + " already registered for URI "
+                                                               + registeredUri + " which is different from provided URI " + uri);
                                        else
                                                return;// skip
                                }
                        nr.registerNamespace(prefix, uri);
                } catch (RepositoryException e) {
-                       throw new ArgeoJcrException("Cannot register namespace " + uri + " under prefix " + prefix, e);
+                       throw new JcrException("Cannot register namespace " + uri + " under prefix " + prefix, e);
                }
        }
 
-       /** Recursively outputs the contents of the given node. */
-       public static void debug(Node node) {
-               debug(node, log);
-       }
-
-       /** Recursively outputs the contents of the given node. */
-       public static void debug(Node node, Log log) {
-               try {
-                       // First output the node path
-                       log.debug(node.getPath());
-                       // Skip the virtual (and large!) jcr:system subtree
-                       if (node.getName().equals("jcr:system")) {
-                               return;
-                       }
-
-                       // Then the children nodes (recursive)
-                       NodeIterator it = node.getNodes();
-                       while (it.hasNext()) {
-                               Node childNode = it.nextNode();
-                               debug(childNode, log);
-                       }
-
-                       // Then output the properties
-                       PropertyIterator properties = node.getProperties();
-                       // log.debug("Property are : ");
-
-                       properties: while (properties.hasNext()) {
-                               Property property = properties.nextProperty();
-                               if (property.getType() == PropertyType.BINARY)
-                                       continue properties;// skip
-                               if (property.getDefinition().isMultiple()) {
-                                       // A multi-valued property, print all values
-                                       Value[] values = property.getValues();
-                                       for (int i = 0; i < values.length; i++) {
-                                               log.debug(property.getPath() + "=" + values[i].getString());
-                                       }
-                               } else {
-                                       // A single-valued property
-                                       log.debug(property.getPath() + "=" + property.getString());
-                               }
-                       }
-               } catch (Exception e) {
-                       log.error("Could not debug " + node, e);
-               }
-
-       }
-
-       /** Logs the effective access control policies */
-       public static void logEffectiveAccessPolicies(Node node) {
-               try {
-                       logEffectiveAccessPolicies(node.getSession(), node.getPath());
-               } catch (RepositoryException e) {
-                       log.error("Cannot log effective access policies of " + node, e);
-               }
-       }
-
-       /** Logs the effective access control policies */
-       public static void logEffectiveAccessPolicies(Session session, String path) {
-               if (!log.isDebugEnabled())
-                       return;
-
-               try {
-                       AccessControlPolicy[] effectivePolicies = session.getAccessControlManager().getEffectivePolicies(path);
-                       if (effectivePolicies.length > 0) {
-                               for (AccessControlPolicy policy : effectivePolicies) {
-                                       if (policy instanceof AccessControlList) {
-                                               AccessControlList acl = (AccessControlList) policy;
-                                               log.debug("Access control list for " + path + "\n" + accessControlListSummary(acl));
-                                       }
-                               }
-                       } else {
-                               log.debug("No effective access control policy for " + path);
-                       }
-               } catch (RepositoryException e) {
-                       log.error("Cannot log effective access policies of " + path, e);
-               }
-       }
+//     /** Recursively outputs the contents of the given node. */
+//     public static void debug(Node node) {
+//             debug(node, log);
+//     }
+//
+//     /** Recursively outputs the contents of the given node. */
+//     public static void debug(Node node, Log log) {
+//             try {
+//                     // First output the node path
+//                     log.debug(node.getPath());
+//                     // Skip the virtual (and large!) jcr:system subtree
+//                     if (node.getName().equals("jcr:system")) {
+//                             return;
+//                     }
+//
+//                     // Then the children nodes (recursive)
+//                     NodeIterator it = node.getNodes();
+//                     while (it.hasNext()) {
+//                             Node childNode = it.nextNode();
+//                             debug(childNode, log);
+//                     }
+//
+//                     // Then output the properties
+//                     PropertyIterator properties = node.getProperties();
+//                     // log.debug("Property are : ");
+//
+//                     properties: while (properties.hasNext()) {
+//                             Property property = properties.nextProperty();
+//                             if (property.getType() == PropertyType.BINARY)
+//                                     continue properties;// skip
+//                             if (property.getDefinition().isMultiple()) {
+//                                     // A multi-valued property, print all values
+//                                     Value[] values = property.getValues();
+//                                     for (int i = 0; i < values.length; i++) {
+//                                             log.debug(property.getPath() + "=" + values[i].getString());
+//                                     }
+//                             } else {
+//                                     // A single-valued property
+//                                     log.debug(property.getPath() + "=" + property.getString());
+//                             }
+//                     }
+//             } catch (Exception e) {
+//                     log.error("Could not debug " + node, e);
+//             }
+//
+//     }
+
+//     /** Logs the effective access control policies */
+//     public static void logEffectiveAccessPolicies(Node node) {
+//             try {
+//                     logEffectiveAccessPolicies(node.getSession(), node.getPath());
+//             } catch (RepositoryException e) {
+//                     log.error("Cannot log effective access policies of " + node, e);
+//             }
+//     }
+//
+//     /** Logs the effective access control policies */
+//     public static void logEffectiveAccessPolicies(Session session, String path) {
+//             if (!log.isDebugEnabled())
+//                     return;
+//
+//             try {
+//                     AccessControlPolicy[] effectivePolicies = session.getAccessControlManager().getEffectivePolicies(path);
+//                     if (effectivePolicies.length > 0) {
+//                             for (AccessControlPolicy policy : effectivePolicies) {
+//                                     if (policy instanceof AccessControlList) {
+//                                             AccessControlList acl = (AccessControlList) policy;
+//                                             log.debug("Access control list for " + path + "\n" + accessControlListSummary(acl));
+//                                     }
+//                             }
+//                     } else {
+//                             log.debug("No effective access control policy for " + path);
+//                     }
+//             } catch (RepositoryException e) {
+//                     log.error("Cannot log effective access policies of " + path, e);
+//             }
+//     }
 
        /** Returns a human-readable summary of this access control list. */
        public static String accessControlListSummary(AccessControlList acl) {
@@ -702,7 +722,34 @@ public class JcrUtils {
                        }
                        return buf.toString();
                } catch (RepositoryException e) {
-                       throw new ArgeoJcrException("Cannot write summary of " + acl, e);
+                       throw new JcrException("Cannot write summary of " + acl, e);
+               }
+       }
+
+       /** Copy the whole workspace via a system view XML. */
+       public static void copyWorkspaceXml(Session fromSession, Session toSession) {
+               Workspace fromWorkspace = fromSession.getWorkspace();
+               Workspace toWorkspace = toSession.getWorkspace();
+               String errorMsg = "Cannot copy workspace " + fromWorkspace + " to " + toWorkspace + " via XML.";
+
+               try (PipedInputStream in = new PipedInputStream(1024 * 1024);) {
+                       new Thread(() -> {
+                               try (PipedOutputStream out = new PipedOutputStream(in)) {
+                                       fromSession.exportSystemView("/", out, false, false);
+                                       out.flush();
+                               } catch (IOException e) {
+                                       throw new RuntimeException(errorMsg, e);
+                               } catch (RepositoryException e) {
+                                       throw new JcrException(errorMsg, e);
+                               }
+                       }, "Copy workspace" + fromWorkspace + " to " + toWorkspace).start();
+
+                       toSession.importXML("/", in, ImportUUIDBehavior.IMPORT_UUID_COLLISION_REPLACE_EXISTING);
+                       toSession.save();
+               } catch (IOException e) {
+                       throw new RuntimeException(errorMsg, e);
+               } catch (RepositoryException e) {
+                       throw new JcrException(errorMsg, e);
                }
        }
 
@@ -710,15 +757,25 @@ public class JcrUtils {
         * Copies recursively the content of a node to another one. Do NOT copy the
         * property values of {@link NodeType#MIX_CREATED} and
         * {@link NodeType#MIX_LAST_MODIFIED}, but update the
-        * {@link Property#JCR_LAST_MODIFIED} and
-        * {@link Property#JCR_LAST_MODIFIED_BY} properties if the target node has
-        * the {@link NodeType#MIX_LAST_MODIFIED} mixin.
+        * {@link Property#JCR_LAST_MODIFIED} and {@link Property#JCR_LAST_MODIFIED_BY}
+        * properties if the target node has the {@link NodeType#MIX_LAST_MODIFIED}
+        * mixin.
         */
        public static void copy(Node fromNode, Node toNode) {
                try {
                        if (toNode.getDefinition().isProtected())
                                return;
 
+                       // add mixins
+                       for (NodeType mixinType : fromNode.getMixinNodeTypes()) {
+                               try {
+                                       toNode.addMixin(mixinType.getName());
+                               } catch (NoSuchNodeTypeException e) {
+                                       // ignore unknown mixins
+                                       // TODO log it
+                               }
+                       }
+
                        // process properties
                        PropertyIterator pit = fromNode.getProperties();
                        properties: while (pit.hasNext()) {
@@ -743,12 +800,7 @@ public class JcrUtils {
 
                        // update jcr:lastModified and jcr:lastModifiedBy in toNode in case
                        // they existed, before adding the mixins
-                       updateLastModified(toNode);
-
-                       // add mixins
-                       for (NodeType mixinType : fromNode.getMixinNodeTypes()) {
-                               toNode.addMixin(mixinType.getName());
-                       }
+                       updateLastModified(toNode, true);
 
                        // process children nodes
                        NodeIterator nit = fromNode.getNodes();
@@ -759,18 +811,25 @@ public class JcrUtils {
                                Node toChild;
                                if (toNode.hasNode(nodeRelPath))
                                        toChild = toNode.getNode(nodeRelPath);
-                               else
-                                       toChild = toNode.addNode(fromChild.getName(), fromChild.getPrimaryNodeType().getName());
+                               else {
+                                       try {
+                                               toChild = toNode.addNode(fromChild.getName(), fromChild.getPrimaryNodeType().getName());
+                                       } catch (NoSuchNodeTypeException e) {
+                                               // ignore unknown primary types
+                                               // TODO log it
+                                               return;
+                                       }
+                               }
                                copy(fromChild, toChild);
                        }
                } catch (RepositoryException e) {
-                       throw new ArgeoJcrException("Cannot copy " + fromNode + " to " + toNode, e);
+                       throw new JcrException("Cannot copy " + fromNode + " to " + toNode, e);
                }
        }
 
        /**
-        * Check whether all first-level properties (except jcr:* properties) are
-        * equal. Skip jcr:* properties
+        * Check whether all first-level properties (except jcr:* properties) are equal.
+        * Skip jcr:* properties
         */
        public static Boolean allPropertiesEquals(Node reference, Node observed, Boolean onlyCommonProperties) {
                try {
@@ -792,7 +851,7 @@ public class JcrUtils {
                        }
                        return true;
                } catch (RepositoryException e) {
-                       throw new ArgeoJcrException("Cannot check all properties equals of " + reference + " and " + observed, e);
+                       throw new JcrException("Cannot check all properties equals of " + reference + " and " + observed, e);
                }
        }
 
@@ -803,8 +862,8 @@ public class JcrUtils {
        }
 
        /**
-        * Compare the properties of two nodes. Recursivity to child nodes is not
-        * yet supported. Skip jcr:* properties.
+        * Compare the properties of two nodes. Recursivity to child nodes is not yet
+        * supported. Skip jcr:* properties.
         */
        static void diffPropertiesLevel(Map<String, PropertyDiff> diffs, String baseRelPath, Node reference,
                        Node observed) {
@@ -854,13 +913,12 @@ public class JcrUtils {
                                }
                        }
                } catch (RepositoryException e) {
-                       throw new ArgeoJcrException("Cannot diff " + reference + " and " + observed, e);
+                       throw new JcrException("Cannot diff " + reference + " and " + observed, e);
                }
        }
 
        /**
-        * Compare only a restricted list of properties of two nodes. No
-        * recursivity.
+        * Compare only a restricted list of properties of two nodes. No recursivity.
         * 
         */
        public static Map<String, PropertyDiff> diffProperties(Node reference, Node observed, List<String> properties) {
@@ -897,7 +955,7 @@ public class JcrUtils {
                                }
                        }
                } catch (RepositoryException e) {
-                       throw new ArgeoJcrException("Cannot diff " + reference + " and " + observed, e);
+                       throw new JcrException("Cannot diff " + reference + " and " + observed, e);
                }
                return diffs;
        }
@@ -911,8 +969,8 @@ public class JcrUtils {
        }
 
        /**
-        * Normalizes a name so that it can be stored in contexts not supporting
-        * names with ':' (typically databases). Replaces ':' by '_'.
+        * Normalizes a name so that it can be stored in contexts not supporting names
+        * with ':' (typically databases). Replaces ':' by '_'.
         */
        public static String normalize(String name) {
                return name.replace(':', '_');
@@ -954,15 +1012,16 @@ public class JcrUtils {
                        return name;
        }
 
-       /**
-        * Removes forbidden characters from a path, replacing them with '_'
-        * 
-        * @deprecated use {@link #replaceInvalidChars(String)} instead
-        */
-       public static String removeForbiddenCharacters(String str) {
-               return str.replace('[', '_').replace(']', '_').replace('/', '_').replace('*', '_');
-
-       }
+       // /**
+       // * Removes forbidden characters from a path, replacing them with '_'
+       // *
+       // * @deprecated use {@link #replaceInvalidChars(String)} instead
+       // */
+       // public static String removeForbiddenCharacters(String str) {
+       // return str.replace('[', '_').replace(']', '_').replace('/', '_').replace('*',
+       // '_');
+       //
+       // }
 
        /** Cleanly disposes a {@link Binary} even if it is null. */
        public static void closeQuietly(Binary binary) {
@@ -973,46 +1032,55 @@ public class JcrUtils {
 
        /** Retrieve a {@link Binary} as a byte array */
        public static byte[] getBinaryAsBytes(Property property) {
-               ByteArrayOutputStream out = new ByteArrayOutputStream();
-               InputStream in = null;
-               Binary binary = null;
-               try {
-                       binary = property.getBinary();
-                       in = binary.getStream();
+               try (ByteArrayOutputStream out = new ByteArrayOutputStream();
+                               Bin binary = new Bin(property);
+                               InputStream in = binary.getStream()) {
                        IOUtils.copy(in, out);
                        return out.toByteArray();
-               } catch (Exception e) {
-                       throw new ArgeoJcrException("Cannot read binary " + property + " as bytes", e);
-               } finally {
-                       IOUtils.closeQuietly(out);
-                       IOUtils.closeQuietly(in);
-                       closeQuietly(binary);
+               } catch (RepositoryException e) {
+                       throw new JcrException("Cannot read binary " + property + " as bytes", e);
+               } catch (IOException e) {
+                       throw new RuntimeException("Cannot read binary " + property + " as bytes", e);
                }
        }
 
        /** Writes a {@link Binary} from a byte array */
        public static void setBinaryAsBytes(Node node, String property, byte[] bytes) {
-               InputStream in = null;
                Binary binary = null;
-               try {
-                       in = new ByteArrayInputStream(bytes);
+               try (InputStream in = new ByteArrayInputStream(bytes)) {
                        binary = node.getSession().getValueFactory().createBinary(in);
                        node.setProperty(property, binary);
-               } catch (Exception e) {
-                       throw new ArgeoJcrException("Cannot read binary " + property + " as bytes", e);
+               } catch (RepositoryException e) {
+                       throw new JcrException("Cannot set binary " + property + " as bytes", e);
+               } catch (IOException e) {
+                       throw new RuntimeException("Cannot set binary " + property + " as bytes", e);
+               } finally {
+                       closeQuietly(binary);
+               }
+       }
+
+       /** Writes a {@link Binary} from a byte array */
+       public static void setBinaryAsBytes(Property prop, byte[] bytes) {
+               Binary binary = null;
+               try (InputStream in = new ByteArrayInputStream(bytes)) {
+                       binary = prop.getSession().getValueFactory().createBinary(in);
+                       prop.setValue(binary);
+               } catch (RepositoryException e) {
+                       throw new JcrException("Cannot set binary " + prop + " as bytes", e);
+               } catch (IOException e) {
+                       throw new RuntimeException("Cannot set binary " + prop + " as bytes", e);
                } finally {
-                       IOUtils.closeQuietly(in);
                        closeQuietly(binary);
                }
        }
 
        /**
-        * Creates depth from a string (typically a username) by adding levels based
-        * on its first characters: "aBcD",2 becomes a/aB
+        * Creates depth from a string (typically a username) by adding levels based on
+        * its first characters: "aBcD",2 becomes a/aB
         */
        public static String firstCharsToPath(String str, Integer nbrOfChars) {
                if (str.length() < nbrOfChars)
-                       throw new ArgeoJcrException("String " + str + " length must be greater or equal than " + nbrOfChars);
+                       throw new IllegalArgumentException("String " + str + " length must be greater or equal than " + nbrOfChars);
                StringBuffer path = new StringBuffer("");
                StringBuffer curr = new StringBuffer("");
                for (int i = 0; i < nbrOfChars; i++) {
@@ -1025,8 +1093,8 @@ public class JcrUtils {
        }
 
        /**
-        * Discards the current changes in the session attached to this node. To be
-        * used typically in a catch block.
+        * Discards the current changes in the session attached to this node. To be used
+        * typically in a catch block.
         * 
         * @see #discardQuietly(Session)
         */
@@ -1034,7 +1102,7 @@ public class JcrUtils {
                try {
                        discardQuietly(node.getSession());
                } catch (RepositoryException e) {
-                       log.warn("Cannot quietly discard session of node " + node + ": " + e.getMessage());
+                       // silent
                }
        }
 
@@ -1048,26 +1116,35 @@ public class JcrUtils {
                        if (session != null)
                                session.refresh(false);
                } catch (RepositoryException e) {
-                       log.warn("Cannot quietly discard session " + session + ": " + e.getMessage());
+                       // silent
                }
        }
 
        /**
-        * Login to a workspace with implicit credentials, creates the workspace
-        * with these credentials if it does not already exist.
+        * Login to a workspace with implicit credentials, creates the workspace with
+        * these credentials if it does not already exist.
         */
        public static Session loginOrCreateWorkspace(Repository repository, String workspaceName)
                        throws RepositoryException {
+               return loginOrCreateWorkspace(repository, workspaceName, null);
+       }
+
+       /**
+        * Login to a workspace with implicit credentials, creates the workspace with
+        * these credentials if it does not already exist.
+        */
+       public static Session loginOrCreateWorkspace(Repository repository, String workspaceName, Credentials credentials)
+                       throws RepositoryException {
                Session workspaceSession = null;
                Session defaultSession = null;
                try {
                        try {
-                               workspaceSession = repository.login(workspaceName);
+                               workspaceSession = repository.login(credentials, workspaceName);
                        } catch (NoSuchWorkspaceException e) {
                                // try to create workspace
-                               defaultSession = repository.login();
+                               defaultSession = repository.login(credentials);
                                defaultSession.getWorkspace().createWorkspace(workspaceName);
-                               workspaceSession = repository.login(workspaceName);
+                               workspaceSession = repository.login(credentials, workspaceName);
                        }
                        return workspaceSession;
                } finally {
@@ -1075,15 +1152,19 @@ public class JcrUtils {
                }
        }
 
-       /** Logs out the session, not throwing any exception, even if it is null. */
+       /**
+        * Logs out the session, not throwing any exception, even if it is null.
+        * {@link Jcr#logout(Session)} should rather be used.
+        */
        public static void logoutQuietly(Session session) {
-               try {
-                       if (session != null)
-                               if (session.isLive())
-                                       session.logout();
-               } catch (Exception e) {
-                       // silent
-               }
+               Jcr.logout(session);
+//             try {
+//                     if (session != null)
+//                             if (session.isLive())
+//                                     session.logout();
+//             } catch (Exception e) {
+//                     // silent
+//             }
        }
 
        /**
@@ -1096,7 +1177,7 @@ public class JcrUtils {
                        session.getWorkspace().getObservationManager().addEventListener(listener, eventTypes, basePath, true, null,
                                        nodeType == null ? null : new String[] { nodeType }, true);
                } catch (RepositoryException e) {
-                       throw new ArgeoJcrException("Cannot add JCR listener " + listener + " to session " + session, e);
+                       throw new JcrException("Cannot add JCR listener " + listener + " to session " + session, e);
                }
        }
 
@@ -1112,16 +1193,14 @@ public class JcrUtils {
        }
 
        /**
-        * Quietly unregisters an {@link EventListener} from the udnerlying
-        * workspace of this node.
+        * Quietly unregisters an {@link EventListener} from the udnerlying workspace of
+        * this node.
         */
        public static void unregisterQuietly(Node node, EventListener eventListener) {
                try {
                        unregisterQuietly(node.getSession().getWorkspace(), eventListener);
                } catch (RepositoryException e) {
                        // silent
-                       if (log.isTraceEnabled())
-                               log.trace("Could not unregister event listener " + eventListener);
                }
        }
 
@@ -1133,53 +1212,104 @@ public class JcrUtils {
                        workspace.getObservationManager().removeEventListener(eventListener);
                } catch (RepositoryException e) {
                        // silent
-                       if (log.isTraceEnabled())
-                               log.trace("Could not unregister event listener " + eventListener);
                }
        }
 
        /**
-        * If this node is has the {@link NodeType#MIX_LAST_MODIFIED} mixin, it
-        * updates the {@link Property#JCR_LAST_MODIFIED} property with the current
-        * time and the {@link Property#JCR_LAST_MODIFIED_BY} property with the
-        * underlying session user id. In Jackrabbit 2.x,
-        * <a href="https://issues.apache.org/jira/browse/JCR-2233">these properties
-        * are not automatically updated</a>, hence the need for manual update. The
-        * session is not saved.
+        * Checks whether {@link Property#JCR_LAST_MODIFIED} or (afterwards)
+        * {@link Property#JCR_CREATED} are set and returns it as an {@link Instant}.
+        */
+       public static Instant getModified(Node node) {
+               Calendar calendar = null;
+               try {
+                       if (node.hasProperty(Property.JCR_LAST_MODIFIED))
+                               calendar = node.getProperty(Property.JCR_LAST_MODIFIED).getDate();
+                       else if (node.hasProperty(Property.JCR_CREATED))
+                               calendar = node.getProperty(Property.JCR_CREATED).getDate();
+                       else
+                               throw new IllegalArgumentException("No modification time found in " + node);
+                       return calendar.toInstant();
+               } catch (RepositoryException e) {
+                       throw new JcrException("Cannot get modification time for " + node, e);
+               }
+
+       }
+
+       /**
+        * Get {@link Property#JCR_CREATED} as an {@link Instant}, if it is set.
+        */
+       public static Instant getCreated(Node node) {
+               Calendar calendar = null;
+               try {
+                       if (node.hasProperty(Property.JCR_CREATED))
+                               calendar = node.getProperty(Property.JCR_CREATED).getDate();
+                       else
+                               throw new IllegalArgumentException("No created time found in " + node);
+                       return calendar.toInstant();
+               } catch (RepositoryException e) {
+                       throw new JcrException("Cannot get created time for " + node, e);
+               }
+
+       }
+
+       /**
+        * Updates the {@link Property#JCR_LAST_MODIFIED} property with the current time
+        * and the {@link Property#JCR_LAST_MODIFIED_BY} property with the underlying
+        * session user id.
         */
        public static void updateLastModified(Node node) {
+               updateLastModified(node, false);
+       }
+
+       /**
+        * Updates the {@link Property#JCR_LAST_MODIFIED} property with the current time
+        * and the {@link Property#JCR_LAST_MODIFIED_BY} property with the underlying
+        * session user id. In Jackrabbit 2.x,
+        * <a href="https://issues.apache.org/jira/browse/JCR-2233">these properties are
+        * not automatically updated</a>, hence the need for manual update. The session
+        * is not saved.
+        */
+       public static void updateLastModified(Node node, boolean addMixin) {
                try {
-                       if (!node.isNodeType(NodeType.MIX_LAST_MODIFIED))
+                       if (addMixin && !node.isNodeType(NodeType.MIX_LAST_MODIFIED))
                                node.addMixin(NodeType.MIX_LAST_MODIFIED);
                        node.setProperty(Property.JCR_LAST_MODIFIED, new GregorianCalendar());
                        node.setProperty(Property.JCR_LAST_MODIFIED_BY, node.getSession().getUserID());
                } catch (RepositoryException e) {
-                       throw new ArgeoJcrException("Cannot update last modified on " + node, e);
+                       throw new JcrException("Cannot update last modified on " + node, e);
                }
        }
 
        /**
         * Update lastModified recursively until this parent.
         * 
-        * @param node
-        *            the node
-        * @param untilPath
-        *            the base path, null is equivalent to "/"
+        * @param node      the node
+        * @param untilPath the base path, null is equivalent to "/"
         */
        public static void updateLastModifiedAndParents(Node node, String untilPath) {
+               updateLastModifiedAndParents(node, untilPath, true);
+       }
+
+       /**
+        * Update lastModified recursively until this parent.
+        * 
+        * @param node      the node
+        * @param untilPath the base path, null is equivalent to "/"
+        */
+       public static void updateLastModifiedAndParents(Node node, String untilPath, boolean addMixin) {
                try {
                        if (untilPath != null && !node.getPath().startsWith(untilPath))
-                               throw new ArgeoJcrException(node + " is not under " + untilPath);
-                       updateLastModified(node);
+                               throw new IllegalArgumentException(node + " is not under " + untilPath);
+                       updateLastModified(node, addMixin);
                        if (untilPath == null) {
                                if (!node.getPath().equals("/"))
-                                       updateLastModifiedAndParents(node.getParent(), untilPath);
+                                       updateLastModifiedAndParents(node.getParent(), untilPath, addMixin);
                        } else {
                                if (!node.getPath().equals(untilPath))
-                                       updateLastModifiedAndParents(node.getParent(), untilPath);
+                                       updateLastModifiedAndParents(node.getParent(), untilPath, addMixin);
                        }
                } catch (RepositoryException e) {
-                       throw new ArgeoJcrException("Cannot update lastModified from " + node + " until " + untilPath, e);
+                       throw new JcrException("Cannot update lastModified from " + node + " until " + untilPath, e);
                }
        }
 
@@ -1203,15 +1333,15 @@ public class JcrUtils {
                        if (prop.getDefinition().isMultiple())
                                sbuf.append("*");
                } catch (RepositoryException re) {
-                       throw new ArgeoJcrException("unexpected error while getting property definition as String", re);
+                       throw new JcrException("unexpected error while getting property definition as String", re);
                }
                return sbuf.toString();
        }
 
        /**
-        * Estimate the sub tree size from current node. Computation is based on the
-        * Jcr {@link Property#getLength()} method. Note : it is not the exact size
-        * used on the disk by the current part of the JCR Tree.
+        * Estimate the sub tree size from current node. Computation is based on the Jcr
+        * {@link Property#getLength()} method. Note : it is not the exact size used on
+        * the disk by the current part of the JCR Tree.
         */
 
        public static long getNodeApproxSize(Node node) {
@@ -1234,7 +1364,7 @@ public class JcrUtils {
                                curNodeSize += getNodeApproxSize(ni.nextNode());
                        return curNodeSize;
                } catch (RepositoryException re) {
-                       throw new ArgeoJcrException("Unexpected error while recursively determining node size.", re);
+                       throw new JcrException("Unexpected error while recursively determining node size.", re);
                }
        }
 
@@ -1254,9 +1384,9 @@ public class JcrUtils {
        }
 
        /**
-        * Add privileges on a path to a {@link Principal}. The path must already
-        * exist. Session is saved. Synchronized to prevent concurrent modifications
-        * of the same node.
+        * Add privileges on a path to a {@link Principal}. The path must already exist.
+        * Session is saved. Synchronized to prevent concurrent modifications of the
+        * same node.
         */
        public synchronized static Boolean addPrivileges(Session session, String path, Principal principal,
                        List<Privilege> privs) throws RepositoryException {
@@ -1285,41 +1415,48 @@ public class JcrUtils {
                Privilege[] privileges = privs.toArray(new Privilege[privs.size()]);
                acl.addAccessControlEntry(principal, privileges);
                acm.setPolicy(path, acl);
-               if (log.isDebugEnabled()) {
-                       StringBuffer privBuf = new StringBuffer();
-                       for (Privilege priv : privs)
-                               privBuf.append(priv.getName());
-                       log.debug("Added privileges " + privBuf + " to " + principal.getName() + " on " + path + " in '"
-                                       + session.getWorkspace().getName() + "'");
-               }
+//             if (log.isDebugEnabled()) {
+//                     StringBuffer privBuf = new StringBuffer();
+//                     for (Privilege priv : privs)
+//                             privBuf.append(priv.getName());
+//                     log.debug("Added privileges " + privBuf + " to " + principal.getName() + " on " + path + " in '"
+//                                     + session.getWorkspace().getName() + "'");
+//             }
                session.refresh(true);
                session.save();
                return true;
        }
 
-       /** Gets access control list for this path, throws exception if not found */
+       /**
+        * Gets the first available access control list for this path, throws exception
+        * if not found
+        */
        public synchronized static AccessControlList getAccessControlList(AccessControlManager acm, String path)
                        throws RepositoryException {
                // search for an access control list
                AccessControlList acl = null;
                AccessControlPolicyIterator policyIterator = acm.getApplicablePolicies(path);
-               if (policyIterator.hasNext()) {
+               applicablePolicies: if (policyIterator.hasNext()) {
                        while (policyIterator.hasNext()) {
                                AccessControlPolicy acp = policyIterator.nextAccessControlPolicy();
-                               if (acp instanceof AccessControlList)
+                               if (acp instanceof AccessControlList) {
                                        acl = ((AccessControlList) acp);
+                                       break applicablePolicies;
+                               }
                        }
                } else {
                        AccessControlPolicy[] existingPolicies = acm.getPolicies(path);
-                       for (AccessControlPolicy acp : existingPolicies) {
-                               if (acp instanceof AccessControlList)
+                       existingPolicies: for (AccessControlPolicy acp : existingPolicies) {
+                               if (acp instanceof AccessControlList) {
                                        acl = ((AccessControlList) acp);
+                                       break existingPolicies;
+                               }
                        }
                }
                if (acl != null)
                        return acl;
                else
-                       throw new ArgeoJcrException("ACL not found at " + path);
+                       throw new IllegalArgumentException("ACL not found at " + path);
        }
 
        /** Clear authorizations for a user at this path */
@@ -1335,6 +1472,8 @@ public class JcrUtils {
                // the new access control list must be applied otherwise this call:
                // acl.removeAccessControlEntry(ace); has no effect
                acm.setPolicy(path, acl);
+               session.refresh(true);
+               session.save();
        }
 
        /*
@@ -1351,39 +1490,44 @@ public class JcrUtils {
         * Copy only nt:folder and nt:file, without their additional types and
         * properties.
         * 
-        * @param recursive
-        *            if true copies folders as well, otherwise only first level
-        *            files
+        * @param recursive if true copies folders as well, otherwise only first level
+        *                  files
         * @return how many files were copied
         */
-       public static Long copyFiles(Node fromNode, Node toNode, Boolean recursive, JcrMonitor monitor) {
+       public static Long copyFiles(Node fromNode, Node toNode, Boolean recursive, JcrMonitor monitor, boolean onlyAdd) {
                long count = 0l;
 
-               Binary binary = null;
-               InputStream in = null;
+               // Binary binary = null;
+               // InputStream in = null;
                try {
                        NodeIterator fromChildren = fromNode.getNodes();
-                       while (fromChildren.hasNext()) {
+                       children: while (fromChildren.hasNext()) {
                                if (monitor != null && monitor.isCanceled())
-                                       throw new ArgeoJcrException("Copy cancelled before it was completed");
+                                       throw new IllegalStateException("Copy cancelled before it was completed");
 
                                Node fromChild = fromChildren.nextNode();
                                String fileName = fromChild.getName();
                                if (fromChild.isNodeType(NodeType.NT_FILE)) {
+                                       if (onlyAdd && toNode.hasNode(fileName)) {
+                                               monitor.subTask("Skip existing " + fileName);
+                                               continue children;
+                                       }
+
                                        if (monitor != null)
                                                monitor.subTask("Copy " + fileName);
-                                       binary = fromChild.getNode(Node.JCR_CONTENT).getProperty(Property.JCR_DATA).getBinary();
-                                       in = binary.getStream();
-                                       copyStreamAsFile(toNode, fileName, in);
-                                       IOUtils.closeQuietly(in);
-                                       closeQuietly(binary);
+                                       try (Bin binary = new Bin(fromChild.getNode(Node.JCR_CONTENT).getProperty(Property.JCR_DATA));
+                                                       InputStream in = binary.getStream();) {
+                                               copyStreamAsFile(toNode, fileName, in);
+                                       } catch (IOException e) {
+                                               throw new RuntimeException("Cannot copy " + fileName + " to " + toNode, e);
+                                       }
 
                                        // save session
                                        toNode.getSession().save();
                                        count++;
 
-                                       if (log.isDebugEnabled())
-                                               log.debug("Copied file " + fromChild.getPath());
+//                                     if (log.isDebugEnabled())
+//                                             log.debug("Copied file " + fromChild.getPath());
                                        if (monitor != null)
                                                monitor.worked(1);
                                } else if (fromChild.isNodeType(NodeType.NT_FOLDER) && recursive) {
@@ -1391,29 +1535,29 @@ public class JcrUtils {
                                        if (toNode.hasNode(fileName)) {
                                                toChildFolder = toNode.getNode(fileName);
                                                if (!toChildFolder.isNodeType(NodeType.NT_FOLDER))
-                                                       throw new ArgeoJcrException(toChildFolder + " is not of type nt:folder");
+                                                       throw new IllegalArgumentException(toChildFolder + " is not of type nt:folder");
                                        } else {
                                                toChildFolder = toNode.addNode(fileName, NodeType.NT_FOLDER);
 
                                                // save session
                                                toNode.getSession().save();
                                        }
-                                       count = count + copyFiles(fromChild, toChildFolder, recursive, monitor);
+                                       count = count + copyFiles(fromChild, toChildFolder, recursive, monitor, onlyAdd);
                                }
                        }
                        return count;
                } catch (RepositoryException e) {
-                       throw new ArgeoJcrException("Cannot copy files between " + fromNode + " and " + toNode);
+                       throw new JcrException("Cannot copy files between " + fromNode + " and " + toNode, e);
                } finally {
                        // in case there was an exception
-                       IOUtils.closeQuietly(in);
-                       closeQuietly(binary);
+                       // IOUtils.closeQuietly(in);
+                       // closeQuietly(binary);
                }
        }
 
        /**
-        * Iteratively count all file nodes in subtree, inefficient but can be
-        * useful when query are poorly supported, such as in remoting.
+        * Iteratively count all file nodes in subtree, inefficient but can be useful
+        * when query are poorly supported, such as in remoting.
         */
        public static Long countFiles(Node node) {
                Long localCount = 0l;
@@ -1426,45 +1570,42 @@ public class JcrUtils {
                                        localCount = localCount + 1;
                        }
                } catch (RepositoryException e) {
-                       throw new ArgeoJcrException("Cannot count all children of " + node);
+                       throw new JcrException("Cannot count all children of " + node, e);
                }
                return localCount;
        }
 
        /**
-        * Copy a file as an nt:file, assuming an nt:folder hierarchy. The session
-        * is NOT saved.
+        * Copy a file as an nt:file, assuming an nt:folder hierarchy. The session is
+        * NOT saved.
         * 
         * @return the created file node
         */
+       @Deprecated
        public static Node copyFile(Node folderNode, File file) {
-               InputStream in = null;
-               try {
-                       in = new FileInputStream(file);
+               try (InputStream in = new FileInputStream(file)) {
                        return copyStreamAsFile(folderNode, file.getName(), in);
                } catch (IOException e) {
-                       throw new ArgeoJcrException("Cannot copy file " + file + " under " + folderNode, e);
-               } finally {
-                       IOUtils.closeQuietly(in);
+                       throw new RuntimeException("Cannot copy file " + file + " under " + folderNode, e);
                }
        }
 
        /** Copy bytes as an nt:file */
        public static Node copyBytesAsFile(Node folderNode, String fileName, byte[] bytes) {
-               InputStream in = null;
-               try {
-                       in = new ByteArrayInputStream(bytes);
+               // InputStream in = null;
+               try (InputStream in = new ByteArrayInputStream(bytes)) {
+                       // in = new ByteArrayInputStream(bytes);
                        return copyStreamAsFile(folderNode, fileName, in);
-               } catch (Exception e) {
-                       throw new ArgeoJcrException("Cannot copy file " + fileName + " under " + folderNode, e);
-               } finally {
-                       IOUtils.closeQuietly(in);
+               } catch (IOException e) {
+                       throw new RuntimeException("Cannot copy file " + fileName + " under " + folderNode, e);
+                       // } finally {
+                       // IOUtils.closeQuietly(in);
                }
        }
 
        /**
-        * Copy a stream as an nt:file, assuming an nt:folder hierarchy. The session
-        * is NOT saved.
+        * Copy a stream as an nt:file, assuming an nt:folder hierarchy. The session is
+        * NOT saved.
         * 
         * @return the created file node
         */
@@ -1476,36 +1617,161 @@ public class JcrUtils {
                        if (folderNode.hasNode(fileName)) {
                                fileNode = folderNode.getNode(fileName);
                                if (!fileNode.isNodeType(NodeType.NT_FILE))
-                                       throw new ArgeoJcrException(fileNode + " is not of type nt:file");
+                                       throw new IllegalArgumentException(fileNode + " is not of type nt:file");
                                // we assume that the content node is already there
                                contentNode = fileNode.getNode(Node.JCR_CONTENT);
                        } else {
                                fileNode = folderNode.addNode(fileName, NodeType.NT_FILE);
-                               contentNode = fileNode.addNode(Node.JCR_CONTENT, NodeType.NT_RESOURCE);
+                               contentNode = fileNode.addNode(Node.JCR_CONTENT, NodeType.NT_UNSTRUCTURED);
                        }
                        binary = contentNode.getSession().getValueFactory().createBinary(in);
                        contentNode.setProperty(Property.JCR_DATA, binary);
+                       updateLastModified(contentNode);
                        return fileNode;
-               } catch (Exception e) {
-                       throw new ArgeoJcrException("Cannot create file node " + fileName + " under " + folderNode, e);
+               } catch (RepositoryException e) {
+                       throw new JcrException("Cannot create file node " + fileName + " under " + folderNode, e);
                } finally {
                        closeQuietly(binary);
                }
        }
 
-       /** Computes the checksum of an nt:file */
-       public static String checksumFile(Node fileNode, String algorithm) {
-               Binary data = null;
-               InputStream in = null;
+       /** Read an an nt:file as an {@link InputStream}. */
+       public static InputStream getFileAsStream(Node fileNode) throws RepositoryException {
+               return fileNode.getNode(Node.JCR_CONTENT).getProperty(Property.JCR_DATA).getBinary().getStream();
+       }
+
+       /**
+        * Set the properties of {@link NodeType#MIX_MIMETYPE} on the content of this
+        * file node.
+        */
+       public static void setFileMimeType(Node fileNode, String mimeType, String encoding) throws RepositoryException {
+               Node contentNode = fileNode.getNode(Node.JCR_CONTENT);
+               if (mimeType != null)
+                       contentNode.setProperty(Property.JCR_MIMETYPE, mimeType);
+               if (encoding != null)
+                       contentNode.setProperty(Property.JCR_ENCODING, encoding);
+               // TODO remove properties if args are null?
+       }
+
+       public static void copyFilesToFs(Node baseNode, Path targetDir, boolean recursive) {
                try {
-                       data = fileNode.getNode(Node.JCR_CONTENT).getProperty(Property.JCR_DATA).getBinary();
-                       in = data.getStream();
-                       return DigestUtils.digest(algorithm, in);
+                       Files.createDirectories(targetDir);
+                       for (NodeIterator nit = baseNode.getNodes(); nit.hasNext();) {
+                               Node node = nit.nextNode();
+                               if (node.isNodeType(NodeType.NT_FILE)) {
+                                       Path filePath = targetDir.resolve(node.getName());
+                                       try (OutputStream out = Files.newOutputStream(filePath); InputStream in = getFileAsStream(node)) {
+                                               IOUtils.copy(in, out);
+                                       }
+                               } else if (recursive && node.isNodeType(NodeType.NT_FOLDER)) {
+                                       Path dirPath = targetDir.resolve(node.getName());
+                                       copyFilesToFs(node, dirPath, true);
+                               }
+                       }
                } catch (RepositoryException e) {
-                       throw new ArgeoJcrException("Cannot checksum file " + fileNode, e);
-               } finally {
-                       IOUtils.closeQuietly(in);
-                       closeQuietly(data);
+                       throw new JcrException("Cannot copy " + baseNode + " to " + targetDir, e);
+               } catch (IOException e) {
+                       throw new RuntimeException("Cannot copy " + baseNode + " to " + targetDir, e);
+               }
+       }
+
+       /**
+        * Computes the checksum of an nt:file.
+        * 
+        * @deprecated use separate digest utilities
+        */
+       @Deprecated
+       public static String checksumFile(Node fileNode, String algorithm) {
+               try (InputStream in = fileNode.getNode(Node.JCR_CONTENT).getProperty(Property.JCR_DATA).getBinary()
+                               .getStream()) {
+                       return digest(algorithm, in);
+               } catch (IOException e) {
+                       throw new RuntimeException("Cannot checksum file " + fileNode + " with algorithm " + algorithm, e);
+               } catch (RepositoryException e) {
+                       throw new JcrException("Cannot checksum file " + fileNode + " with algorithm " + algorithm, e);
+               }
+       }
+
+       @Deprecated
+       private static String digest(String algorithm, InputStream in) {
+               final Integer byteBufferCapacity = 100 * 1024;// 100 KB
+               try {
+                       MessageDigest digest = MessageDigest.getInstance(algorithm);
+                       byte[] buffer = new byte[byteBufferCapacity];
+                       int read = 0;
+                       while ((read = in.read(buffer)) > 0) {
+                               digest.update(buffer, 0, read);
+                       }
+
+                       byte[] checksum = digest.digest();
+                       String res = encodeHexString(checksum);
+                       return res;
+               } catch (IOException e) {
+                       throw new RuntimeException("Cannot digest with algorithm " + algorithm, e);
+               } catch (NoSuchAlgorithmException e) {
+                       throw new IllegalArgumentException("Cannot digest with algorithm " + algorithm, e);
+               }
+       }
+
+       /**
+        * From
+        * http://stackoverflow.com/questions/9655181/how-to-convert-a-byte-array-to
+        * -a-hex-string-in-java
+        */
+       @Deprecated
+       private static String encodeHexString(byte[] bytes) {
+               final char[] hexArray = "0123456789abcdef".toCharArray();
+               char[] hexChars = new char[bytes.length * 2];
+               for (int j = 0; j < bytes.length; j++) {
+                       int v = bytes[j] & 0xFF;
+                       hexChars[j * 2] = hexArray[v >>> 4];
+                       hexChars[j * 2 + 1] = hexArray[v & 0x0F];
+               }
+               return new String(hexChars);
+       }
+
+       /** Export a subtree as a compact XML without namespaces. */
+       public static void toSimpleXml(Node node, StringBuilder sb) throws RepositoryException {
+               sb.append('<');
+               String nodeName = node.getName();
+               int colIndex = nodeName.indexOf(':');
+               if (colIndex > 0) {
+                       nodeName = nodeName.substring(colIndex + 1);
+               }
+               sb.append(nodeName);
+               PropertyIterator pit = node.getProperties();
+               properties: while (pit.hasNext()) {
+                       Property p = pit.nextProperty();
+                       // skip multiple properties
+                       if (p.isMultiple())
+                               continue properties;
+                       String propertyName = p.getName();
+                       int pcolIndex = propertyName.indexOf(':');
+                       // skip properties with namespaces
+                       if (pcolIndex > 0)
+                               continue properties;
+                       // skip binaries
+                       if (p.getType() == PropertyType.BINARY) {
+                               continue properties;
+                               // TODO retrieve identifier?
+                       }
+                       sb.append(' ');
+                       sb.append(propertyName);
+                       sb.append('=');
+                       sb.append('\"').append(p.getString()).append('\"');
+               }
+
+               if (node.hasNodes()) {
+                       sb.append('>');
+                       NodeIterator children = node.getNodes();
+                       while (children.hasNext()) {
+                               toSimpleXml(children.nextNode(), sb);
+                       }
+                       sb.append("</");
+                       sb.append(nodeName);
+                       sb.append('>');
+               } else {
+                       sb.append("/>");
                }
        }