Introduce Jcr singleton, making using Jcr less verbose.
[lgpl/argeo-commons.git] / org.argeo.jcr / src / org / argeo / jcr / JcrUtils.java
index c2450e8c7f8f63bc3f04bdc016742764c1a3cb4a..331164cf2b442dec485bb7a71ebb0d7a0c50dd65 100644 (file)
@@ -23,6 +23,7 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.net.MalformedURLException;
 import java.net.URL;
+import java.security.MessageDigest;
 import java.security.Principal;
 import java.text.DateFormat;
 import java.text.ParseException;
@@ -63,7 +64,6 @@ 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 {
@@ -75,11 +75,8 @@ public class JcrUtils {
         * 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 +86,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 ArgeoJcrException if more than one node was found
         */
        public static Node querySingleNode(Query query) {
                NodeIterator nodeIterator;
@@ -239,10 +235,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);
@@ -366,6 +360,15 @@ public class JcrUtils {
                }
        }
 
+       /** Concisely get the path of the given node. */
+       public static String getPath(Node node) {
+               try {
+                       return node.getPath();
+               } catch (RepositoryException e) {
+                       throw new ArgeoJcrException("Cannot get path of " + node, e);
+               }
+       }
+
        /** Concisely get the boolean value of a property */
        public static Boolean check(Node node, String propertyName) {
                try {
@@ -398,8 +401,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 +410,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);
@@ -434,8 +435,8 @@ public class JcrUtils {
        }
 
        /**
-        * 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 {
@@ -460,23 +461,21 @@ 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)) {
@@ -580,8 +579,7 @@ 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 {
@@ -592,8 +590,7 @@ 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(NamespaceRegistry nr, String prefix, String uri) {
                try {
@@ -710,9 +707,9 @@ 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 {
@@ -769,8 +766,8 @@ public class JcrUtils {
        }
 
        /**
-        * 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 {
@@ -803,8 +800,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) {
@@ -859,8 +856,7 @@ public class JcrUtils {
        }
 
        /**
-        * 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) {
@@ -911,8 +907,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 +950,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,42 +970,44 @@ 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();
+               // ByteArrayOutputStream out = new ByteArrayOutputStream();
+               // InputStream in = null;
+               // Binary binary = null;
+               try (ByteArrayOutputStream out = new ByteArrayOutputStream();
+                               Bin binary = new Bin(property);
+                               InputStream in = binary.getStream()) {
+                       // binary = property.getBinary();
+                       // 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);
+                       // IOUtils.closeQuietly(out);
+                       // IOUtils.closeQuietly(in);
+                       // closeQuietly(binary);
                }
        }
 
        /** Writes a {@link Binary} from a byte array */
        public static void setBinaryAsBytes(Node node, String property, byte[] bytes) {
-               InputStream in = null;
+               // InputStream in = null;
                Binary binary = null;
-               try {
-                       in = new ByteArrayInputStream(bytes);
+               try (InputStream in = new ByteArrayInputStream(bytes)) {
+                       // 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);
                } finally {
-                       IOUtils.closeQuietly(in);
+                       // 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)
@@ -1025,8 +1024,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)
         */
@@ -1053,8 +1052,8 @@ public class JcrUtils {
        }
 
        /**
-        * 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 {
@@ -1075,15 +1074,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
+//             }
        }
 
        /**
@@ -1112,8 +1115,8 @@ 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 {
@@ -1139,13 +1142,13 @@ public class JcrUtils {
        }
 
        /**
-        * 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.
+        * 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.
         */
        public static void updateLastModified(Node node) {
                try {
@@ -1161,10 +1164,8 @@ public class JcrUtils {
        /**
         * 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) {
                try {
@@ -1209,9 +1210,9 @@ public class JcrUtils {
        }
 
        /**
-        * 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) {
@@ -1254,9 +1255,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 {
@@ -1351,32 +1352,37 @@ 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");
 
                                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);
+                                       }
+                                       // IOUtils.closeQuietly(in);
+                                       // closeQuietly(binary);
 
                                        // save session
                                        toNode.getSession().save();
@@ -1398,22 +1404,22 @@ public class JcrUtils {
                                                // 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) {
+               } catch (RepositoryException | IOException e) {
                        throw new ArgeoJcrException("Cannot copy files between " + fromNode + " and " + toNode);
                } 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;
@@ -1432,39 +1438,39 @@ public class JcrUtils {
        }
 
        /**
-        * 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
         */
        public static Node copyFile(Node folderNode, File file) {
-               InputStream in = null;
-               try {
-                       in = new FileInputStream(file);
+               // InputStream in = null;
+               try (InputStream in = new FileInputStream(file)) {
+                       // 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);
+                       // } finally {
+                       // IOUtils.closeQuietly(in);
                }
        }
 
        /** 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);
+                       // } 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
         */
@@ -1493,20 +1499,58 @@ public class JcrUtils {
                }
        }
 
-       /** Computes the checksum of an nt:file */
+       /**
+        * Computes the checksum of an nt:file.
+        * 
+        * @deprecated use separate digest utilities
+        */
+       @Deprecated
        public static String checksumFile(Node fileNode, String algorithm) {
                Binary data = null;
-               InputStream in = null;
-               try {
-                       data = fileNode.getNode(Node.JCR_CONTENT).getProperty(Property.JCR_DATA).getBinary();
-                       in = data.getStream();
-                       return DigestUtils.digest(algorithm, in);
-               } catch (RepositoryException e) {
+               try (InputStream in = fileNode.getNode(Node.JCR_CONTENT).getProperty(Property.JCR_DATA).getBinary()
+                               .getStream()) {
+                       return digest(algorithm, in);
+               } catch (RepositoryException | IOException e) {
                        throw new ArgeoJcrException("Cannot checksum file " + fileNode, e);
                } finally {
-                       IOUtils.closeQuietly(in);
                        closeQuietly(data);
                }
        }
 
+       @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 (Exception e) {
+                       throw new ArgeoJcrException("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);
+       }
+
 }