]> git.argeo.org Git - lgpl/argeo-commons.git/blobdiff - server/runtime/org.argeo.server.jcr/src/main/java/org/argeo/jcr/JcrUtils.java
Better protect access to Jackrabbit user manager
[lgpl/argeo-commons.git] / server / runtime / org.argeo.server.jcr / src / main / java / org / argeo / jcr / JcrUtils.java
index f62196cafe3663de56e1e2ce544832c02ad40fce..9f3d761cafd79e8bdb51a7ae481fc9cfb32c847e 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2010 Mathieu Baudier <mbaudier@argeo.org>
+ * Copyright (C) 2007-2012 Mathieu Baudier
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 package org.argeo.jcr;
 
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
 import java.net.MalformedURLException;
 import java.net.URL;
+import java.security.Principal;
 import java.text.DateFormat;
 import java.text.ParseException;
+import java.util.ArrayList;
 import java.util.Calendar;
+import java.util.Collections;
 import java.util.Date;
 import java.util.GregorianCalendar;
-import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
-import java.util.StringTokenizer;
 import java.util.TreeMap;
 
 import javax.jcr.Binary;
 import javax.jcr.NamespaceRegistry;
+import javax.jcr.NoSuchWorkspaceException;
 import javax.jcr.Node;
 import javax.jcr.NodeIterator;
 import javax.jcr.Property;
 import javax.jcr.PropertyIterator;
+import javax.jcr.PropertyType;
 import javax.jcr.Repository;
 import javax.jcr.RepositoryException;
-import javax.jcr.RepositoryFactory;
 import javax.jcr.Session;
 import javax.jcr.Value;
 import javax.jcr.Workspace;
@@ -46,19 +53,25 @@ import javax.jcr.nodetype.NodeType;
 import javax.jcr.observation.EventListener;
 import javax.jcr.query.Query;
 import javax.jcr.query.QueryResult;
-import javax.jcr.query.qom.Constraint;
-import javax.jcr.query.qom.DynamicOperand;
-import javax.jcr.query.qom.QueryObjectModelFactory;
-import javax.jcr.query.qom.Selector;
-import javax.jcr.query.qom.StaticOperand;
-
+import javax.jcr.security.AccessControlEntry;
+import javax.jcr.security.AccessControlList;
+import javax.jcr.security.AccessControlManager;
+import javax.jcr.security.AccessControlPolicy;
+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.ArgeoException;
+import org.argeo.ArgeoMonitor;
+import org.argeo.util.security.DigestUtils;
+import org.argeo.util.security.SimplePrincipal;
 
 /** Utility methods to simplify common JCR operations. */
 public class JcrUtils implements ArgeoJcrConstants {
-       private final static Log log = LogFactory.getLog(JcrUtils.class);
+
+       final private static Log log = LogFactory.getLog(JcrUtils.class);
 
        /**
         * Not complete yet. See
@@ -101,6 +114,20 @@ public class JcrUtils implements ArgeoJcrConstants {
                return node;
        }
 
+       /** Retrieves the node name from the provided path */
+       public static String nodeNameFromPath(String path) {
+               if (path.equals("/"))
+                       return "";
+               if (path.charAt(0) != '/')
+                       throw new ArgeoException("Path " + path + " must start with a '/'");
+               String pathT = path;
+               if (pathT.charAt(pathT.length() - 1) == '/')
+                       pathT = pathT.substring(0, pathT.length() - 2);
+
+               int index = pathT.lastIndexOf('/');
+               return pathT.substring(index + 1);
+       }
+
        /** Retrieves the parent path of the provided path */
        public static String parentPath(String path) {
                if (path.equals("/"))
@@ -139,6 +166,46 @@ public class JcrUtils implements ArgeoJcrConstants {
                }
        }
 
+       /** Set the {@link NodeType#NT_ADDRESS} properties based on this URL. */
+       public static void urlToAddressProperties(Node node, String url) {
+               try {
+                       URL u = new URL(url);
+                       node.setProperty(Property.JCR_PROTOCOL, u.getProtocol());
+                       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 ArgeoException("Cannot set URL " + url
+                                       + " as nt:address properties", e);
+               }
+       }
+
+       /** Build URL based on the {@link NodeType#NT_ADDRESS} properties. */
+       public static String urlFromAddressProperties(Node node) {
+               try {
+                       URL u = new URL(
+                                       node.getProperty(Property.JCR_PROTOCOL).getString(), node
+                                                       .getProperty(Property.JCR_HOST).getString(),
+                                       (int) node.getProperty(Property.JCR_PORT).getLong(), node
+                                                       .getProperty(Property.JCR_PATH).getString());
+                       return u.toString();
+               } catch (Exception e) {
+                       throw new ArgeoException(
+                                       "Cannot get URL from nt:address properties of " + node, e);
+               }
+       }
+
+       /** Make sure that: starts with '/', do not end with '/', do not have '//' */
+       public static String normalizePath(String path) {
+               List<String> tokens = tokenize(path);
+               StringBuffer buf = new StringBuffer(path.length());
+               for (String token : tokens) {
+                       buf.append('/');
+                       buf.append(token);
+               }
+               return buf.toString();
+       }
+
        /**
         * Creates a path from a FQDN, inverting the order of the component:
         * www.argeo.org => org.argeo.www
@@ -219,14 +286,107 @@ public class JcrUtils implements ArgeoJcrConstants {
                return path.substring(index + 1);
        }
 
+       /**
+        * Call {@link Node#getName()} without exceptions (useful in super
+        * constructors).
+        */
+       public static String getNameQuietly(Node node) {
+               try {
+                       return node.getName();
+               } catch (RepositoryException e) {
+                       throw new ArgeoException("Cannot get name from " + node, e);
+               }
+       }
+
+       /**
+        * Call {@link Node#getProperty(String)} without exceptions (useful in super
+        * constructors).
+        */
+       public static String getStringPropertyQuietly(Node node, String propertyName) {
+               try {
+                       return node.getProperty(propertyName).getString();
+               } catch (RepositoryException e) {
+                       throw new ArgeoException("Cannot get name from " + node, e);
+               }
+       }
+
+       /**
+        * Routine that get the child with this name, adding id 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);
+       }
+
+       /**
+        * Routine that get the child with this name, adding id 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);
+       }
+
+       /** Convert a {@link NodeIterator} to a list of {@link Node} */
+       public static List<Node> nodeIteratorToList(NodeIterator nodeIterator) {
+               List<Node> nodes = new ArrayList<Node>();
+               while (nodeIterator.hasNext()) {
+                       nodes.add(nodeIterator.nextNode());
+               }
+               return nodes;
+       }
+
+       /*
+        * PROPERTIES
+        */
+
+       /**
+        * Concisely get the string value of a property or null if this node doesn't
+        * have this property
+        */
+       public static String get(Node node, String propertyName) {
+               try {
+                       if (!node.hasProperty(propertyName))
+                               return null;
+                       return node.getProperty(propertyName).getString();
+               } catch (RepositoryException e) {
+                       throw new ArgeoException("Cannot get property " + propertyName
+                                       + " of " + node, e);
+               }
+       }
+
+       /** Concisely get the boolean value of a property */
+       public static Boolean check(Node node, String propertyName) {
+               try {
+                       return node.getProperty(propertyName).getBoolean();
+               } catch (RepositoryException e) {
+                       throw new ArgeoException("Cannot get property " + propertyName
+                                       + " of " + node, e);
+               }
+       }
+
+       /** Concisely get the bytes array value of a property */
+       public static byte[] getBytes(Node node, String propertyName) {
+               try {
+                       return getBinaryAsBytes(node.getProperty(propertyName));
+               } catch (RepositoryException e) {
+                       throw new ArgeoException("Cannot get property " + propertyName
+                                       + " of " + node, e);
+               }
+       }
+
        /** Creates the nodes making path, if they don't exist. */
        public static Node mkdirs(Session session, String path) {
                return mkdirs(session, path, null, null, false);
        }
 
        /**
-        * @deprecated use {@link #mkdirs(Session, String, String, String, Boolean)}
-        *             instead.
+        * use {@link #mkdirs(Session, String, String, String, Boolean)} instead.
+        * 
+        * @deprecated
         */
        @Deprecated
        public static Node mkdirs(Session session, String path, String type,
@@ -242,9 +402,33 @@ public class JcrUtils implements ArgeoJcrConstants {
                return mkdirs(session, path, type, null, false);
        }
 
+       /**
+        * 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 ArgeoException(
+                                               "Session has pending changes, save them first.");
+                       Node node = mkdirs(session, path, type);
+                       session.save();
+                       return node;
+               } catch (RepositoryException e) {
+                       discardQuietly(session);
+                       throw new ArgeoException("Cannot safely make directories", e);
+               }
+       }
+
+       public synchronized static Node mkdirsSafe(Session session, String path) {
+               return mkdirsSafe(session, path, null);
+       }
+
        /**
         * Creates the nodes making path, if they don't exist. This is up to the
-        * caller to save the session.
+        * caller to save the session. Use with caution since it can create
+        * duplicate nodes if used concurrently.
         */
        public static Node mkdirs(Session session, String path, String type,
                        String intermediaryNodeType, Boolean versioning) {
@@ -255,8 +439,8 @@ public class JcrUtils implements ArgeoJcrConstants {
                        if (session.itemExists(path)) {
                                Node node = session.getNode(path);
                                // check type
-                               if (type != null
-                                               && !type.equals(node.getPrimaryNodeType().getName()))
+                               if (type != null && !node.isNodeType(type)
+                                               && !node.getPath().equals("/"))
                                        throw new ArgeoException("Node " + node
                                                        + " exists but is of type "
                                                        + node.getPrimaryNodeType().getName()
@@ -265,16 +449,16 @@ public class JcrUtils implements ArgeoJcrConstants {
                                return node;
                        }
 
-                       StringTokenizer st = new StringTokenizer(path, "/");
                        StringBuffer current = new StringBuffer("/");
                        Node currentNode = session.getRootNode();
-                       while (st.hasMoreTokens()) {
-                               String part = st.nextToken();
+                       Iterator<String> it = tokenize(path).iterator();
+                       while (it.hasNext()) {
+                               String part = it.next();
                                current.append(part).append('/');
                                if (!session.itemExists(current.toString())) {
-                                       if (!st.hasMoreTokens() && type != null)
+                                       if (!it.hasNext() && type != null)
                                                currentNode = currentNode.addNode(part, type);
-                                       else if (st.hasMoreTokens() && intermediaryNodeType != null)
+                                       else if (it.hasNext() && intermediaryNodeType != null)
                                                currentNode = currentNode.addNode(part,
                                                                intermediaryNodeType);
                                        else
@@ -287,13 +471,47 @@ public class JcrUtils implements ArgeoJcrConstants {
                                        currentNode = (Node) session.getItem(current.toString());
                                }
                        }
-                       // session.save();
                        return currentNode;
                } catch (RepositoryException e) {
+                       discardQuietly(session);
                        throw new ArgeoException("Cannot mkdirs " + path, e);
+               } finally {
                }
        }
 
+       /** Convert a path to the list of its tokens */
+       public static List<String> tokenize(String path) {
+               List<String> tokens = new ArrayList<String>();
+               boolean optimized = false;
+               if (!optimized) {
+                       String[] rawTokens = path.split("/");
+                       for (String token : rawTokens) {
+                               if (!token.equals(""))
+                                       tokens.add(token);
+                       }
+               } else {
+                       StringBuffer curr = new StringBuffer();
+                       char[] arr = path.toCharArray();
+                       chars: for (int i = 0; i < arr.length; i++) {
+                               char c = arr[i];
+                               if (c == '/') {
+                                       if (i == 0 || (i == arr.length - 1))
+                                               continue chars;
+                                       if (curr.length() > 0) {
+                                               tokens.add(curr.toString());
+                                               curr = new StringBuffer();
+                                       }
+                               } else
+                                       curr.append(c);
+                       }
+                       if (curr.length() > 0) {
+                               tokens.add(curr.toString());
+                               curr = new StringBuffer();
+                       }
+               }
+               return Collections.unmodifiableList(tokens);
+       }
+
        /**
         * Safe and repository implementation independent registration of a
         * namespace.
@@ -354,15 +572,17 @@ public class JcrUtils implements ArgeoJcrConstants {
                        NodeIterator it = node.getNodes();
                        while (it.hasNext()) {
                                Node childNode = it.nextNode();
-                               debug(childNode);
+                               debug(childNode, log);
                        }
 
                        // Then output the properties
                        PropertyIterator properties = node.getProperties();
                        // log.debug("Property are : ");
 
-                       while (properties.hasNext()) {
+                       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();
@@ -381,6 +601,55 @@ public class JcrUtils implements ArgeoJcrConstants {
 
        }
 
+       /** 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) {
+               StringBuffer buf = new StringBuffer("");
+               try {
+                       for (AccessControlEntry ace : acl.getAccessControlEntries()) {
+                               buf.append('\t').append(ace.getPrincipal().getName())
+                                               .append('\n');
+                               for (Privilege priv : ace.getPrivileges())
+                                       buf.append("\t\t").append(priv.getName()).append('\n');
+                       }
+                       return buf.toString();
+               } catch (RepositoryException e) {
+                       throw new ArgeoException("Cannot write summary of " + acl, e);
+               }
+       }
+
        /**
         * Copies recursively the content of a node to another one. Do NOT copy the
         * property values of {@link NodeType#MIX_CREATED} and
@@ -391,6 +660,9 @@ public class JcrUtils implements ArgeoJcrConstants {
         */
        public static void copy(Node fromNode, Node toNode) {
                try {
+                       if (toNode.getDefinition().isProtected())
+                               return;
+
                        // process properties
                        PropertyIterator pit = fromNode.getProperties();
                        properties: while (pit.hasNext()) {
@@ -505,16 +777,18 @@ public class JcrUtils implements ArgeoJcrConstants {
                                                        relPath, p.getValue(), null);
                                        diffs.put(relPath, pDiff);
                                } else {
-                                       if (p.isMultiple())
-                                               continue props;
-                                       Value referenceValue = p.getValue();
-                                       Value newValue = observed.getProperty(name).getValue();
-                                       if (!referenceValue.equals(newValue)) {
-                                               String relPath = propertyRelPath(baseRelPath, name);
-                                               PropertyDiff pDiff = new PropertyDiff(
-                                                               PropertyDiff.MODIFIED, relPath, referenceValue,
-                                                               newValue);
-                                               diffs.put(relPath, pDiff);
+                                       if (p.isMultiple()) {
+                                               // FIXME implement multiple
+                                       } else {
+                                               Value referenceValue = p.getValue();
+                                               Value newValue = observed.getProperty(name).getValue();
+                                               if (!referenceValue.equals(newValue)) {
+                                                       String relPath = propertyRelPath(baseRelPath, name);
+                                                       PropertyDiff pDiff = new PropertyDiff(
+                                                                       PropertyDiff.MODIFIED, relPath,
+                                                                       referenceValue, newValue);
+                                                       diffs.put(relPath, pDiff);
+                                               }
                                        }
                                }
                        }
@@ -526,10 +800,14 @@ public class JcrUtils implements ArgeoJcrConstants {
                                if (name.startsWith("jcr:"))
                                        continue props;
                                if (!reference.hasProperty(name)) {
-                                       String relPath = propertyRelPath(baseRelPath, name);
-                                       PropertyDiff pDiff = new PropertyDiff(PropertyDiff.ADDED,
-                                                       relPath, null, p.getValue());
-                                       diffs.put(relPath, pDiff);
+                                       if (p.isMultiple()) {
+                                               // FIXME implement multiple
+                                       } else {
+                                               String relPath = propertyRelPath(baseRelPath, name);
+                                               PropertyDiff pDiff = new PropertyDiff(
+                                                               PropertyDiff.ADDED, relPath, null, p.getValue());
+                                               diffs.put(relPath, pDiff);
+                                       }
                                }
                        }
                } catch (RepositoryException e) {
@@ -659,6 +937,43 @@ public class JcrUtils implements ArgeoJcrConstants {
                binary.dispose();
        }
 
+       /** 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();
+                       IOUtils.copy(in, out);
+                       return out.toByteArray();
+               } catch (Exception e) {
+                       throw new ArgeoException("Cannot read binary " + property
+                                       + " as bytes", e);
+               } finally {
+                       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;
+               Binary binary = null;
+               try {
+                       in = new ByteArrayInputStream(bytes);
+                       binary = node.getSession().getValueFactory().createBinary(in);
+                       node.setProperty(property, binary);
+               } catch (Exception e) {
+                       throw new ArgeoException("Cannot read binary " + property
+                                       + " 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 => a/aB
@@ -678,42 +993,6 @@ public class JcrUtils implements ArgeoJcrConstants {
                return path.toString();
        }
 
-       /**
-        * Wraps the call to the repository factory based on parameter
-        * {@link ArgeoJcrConstants#JCR_REPOSITORY_ALIAS} in order to simplify it
-        * and protect against future API changes.
-        */
-       public static Repository getRepositoryByAlias(
-                       RepositoryFactory repositoryFactory, String alias) {
-               try {
-                       Map<String, String> parameters = new HashMap<String, String>();
-                       parameters.put(JCR_REPOSITORY_ALIAS, alias);
-                       return repositoryFactory.getRepository(parameters);
-               } catch (RepositoryException e) {
-                       throw new ArgeoException(
-                                       "Unexpected exception when trying to retrieve repository with alias "
-                                                       + alias, e);
-               }
-       }
-
-       /**
-        * Wraps the call to the repository factory based on parameter
-        * {@link ArgeoJcrConstants#JCR_REPOSITORY_URI} in order to simplify it and
-        * protect against future API changes.
-        */
-       public static Repository getRepositoryByUri(
-                       RepositoryFactory repositoryFactory, String uri) {
-               try {
-                       Map<String, String> parameters = new HashMap<String, String>();
-                       parameters.put(JCR_REPOSITORY_URI, uri);
-                       return repositoryFactory.getRepository(parameters);
-               } catch (RepositoryException e) {
-                       throw new ArgeoException(
-                                       "Unexpected exception when trying to retrieve repository with uri "
-                                                       + uri, e);
-               }
-       }
-
        /**
         * Discards the current changes in the session attached to this node. To be
         * used typically in a catch block.
@@ -744,6 +1023,29 @@ public class JcrUtils implements ArgeoJcrConstants {
                }
        }
 
+       /**
+        * 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 {
+               Session workspaceSession = null;
+               Session defaultSession = null;
+               try {
+                       try {
+                               workspaceSession = repository.login(workspaceName);
+                       } catch (NoSuchWorkspaceException e) {
+                               // try to create workspace
+                               defaultSession = repository.login();
+                               defaultSession.getWorkspace().createWorkspace(workspaceName);
+                               workspaceSession = repository.login(workspaceName);
+                       }
+                       return workspaceSession;
+               } finally {
+                       logoutQuietly(defaultSession);
+               }
+       }
+
        /** Logs out the session, not throwing any exception, even if it is null. */
        public static void logoutQuietly(Session session) {
                try {
@@ -755,137 +1057,39 @@ public class JcrUtils implements ArgeoJcrConstants {
                }
        }
 
-       /** Returns the home node of the session user or null if none was found. */
-       public static Node getUserHome(Session session) {
-               String userID = session.getUserID();
-               return getUserHome(session, userID);
-       }
-
        /**
-        * Returns user home has path, embedding exceptions. Contrary to
-        * {@link #getUserHome(Session)}, it never returns null but throws and
-        * exception if not found.
+        * Convenient method to add a listener. uuids passed as null, deep=true,
+        * local=true, only one node type
         */
-       public static String getUserHomePath(Session session) {
-               String userID = session.getUserID();
+       public static void addListener(Session session, EventListener listener,
+                       int eventTypes, String basePath, String nodeType) {
                try {
-                       Node userHome = getUserHome(session, userID);
-                       if (userHome != null)
-                               return userHome.getPath();
-                       else
-                               throw new ArgeoException("No home registered for " + userID);
-               } catch (RepositoryException e) {
-                       throw new ArgeoException("Cannot find user home path", e);
-               }
-       }
-
-       /** Get the profile of the user attached to this session. */
-       public static Node getUserProfile(Session session) {
-               String userID = session.getUserID();
-               return getUserProfile(session, userID);
-       }
-
-       /**
-        * Returns the home node of the session user or null if none was found.
-        * 
-        * @param session
-        *            the session to use in order to perform the search, this can be
-        *            a session with a different user ID than the one searched,
-        *            typically when a system or admin session is used.
-        * @param username
-        *            the username of the user
-        */
-       public static Node getUserHome(Session session, String username) {
-               try {
-                       QueryObjectModelFactory qomf = session.getWorkspace()
-                                       .getQueryManager().getQOMFactory();
-
-                       // query the user home for this user id
-                       Selector userHomeSel = qomf.selector(ArgeoTypes.ARGEO_USER_HOME,
-                                       "userHome");
-                       DynamicOperand userIdDop = qomf.propertyValue("userHome",
-                                       ArgeoNames.ARGEO_USER_ID);
-                       StaticOperand userIdSop = qomf.literal(session.getValueFactory()
-                                       .createValue(username));
-                       Constraint constraint = qomf.comparison(userIdDop,
-                                       QueryObjectModelFactory.JCR_OPERATOR_EQUAL_TO, userIdSop);
-                       Query query = qomf.createQuery(userHomeSel, constraint, null, null);
-                       Node userHome = JcrUtils.querySingleNode(query);
-                       return userHome;
+                       session.getWorkspace()
+                                       .getObservationManager()
+                                       .addEventListener(
+                                                       listener,
+                                                       eventTypes,
+                                                       basePath,
+                                                       true,
+                                                       null,
+                                                       nodeType == null ? null : new String[] { nodeType },
+                                                       true);
                } catch (RepositoryException e) {
-                       throw new ArgeoException("Cannot find home for user " + username, e);
+                       throw new ArgeoException("Cannot add JCR listener " + listener
+                                       + " to session " + session, e);
                }
        }
 
-       public static Node getUserProfile(Session session, String username) {
-               try {
-                       QueryObjectModelFactory qomf = session.getWorkspace()
-                                       .getQueryManager().getQOMFactory();
-                       Selector sel = qomf.selector(ArgeoTypes.ARGEO_USER_PROFILE,
-                                       "userProfile");
-                       DynamicOperand userIdDop = qomf.propertyValue("userProfile",
-                                       ArgeoNames.ARGEO_USER_ID);
-                       StaticOperand userIdSop = qomf.literal(session.getValueFactory()
-                                       .createValue(username));
-                       Constraint constraint = qomf.comparison(userIdDop,
-                                       QueryObjectModelFactory.JCR_OPERATOR_EQUAL_TO, userIdSop);
-                       Query query = qomf.createQuery(sel, constraint, null, null);
-                       Node userHome = JcrUtils.querySingleNode(query);
-                       return userHome;
-               } catch (RepositoryException e) {
-                       throw new ArgeoException(
-                                       "Cannot find profile for user " + username, e);
-               }
-       }
-
-       /** Creates an Argeo user home. */
-       public static Node createUserHome(Session session, String homeBasePath,
-                       String username) {
+       /** Removes a listener without throwing exception */
+       public static void removeListenerQuietly(Session session,
+                       EventListener listener) {
+               if (session == null || !session.isLive())
+                       return;
                try {
-                       if (session == null)
-                               throw new ArgeoException("Session is null");
-                       if (session.hasPendingChanges())
-                               throw new ArgeoException(
-                                               "Session has pending changes, save them first");
-
-                       String homePath = homeBasePath + '/'
-                                       + firstCharsToPath(username, 2) + '/' + username;
-
-                       if (session.itemExists(homePath)) {
-                               try {
-                                       throw new ArgeoException(
-                                                       "Trying to create a user home that already exists");
-                               } catch (Exception e) {
-                                       // we use this workaround to be sure to get the stack trace
-                                       // to identify the sink of the bug.
-                                       log.warn("trying to create an already existing userHome at path:"
-                                                       + homePath + ". Stack trace : ");
-                                       e.printStackTrace();
-                               }
-                       }
-
-                       Node userHome = JcrUtils.mkdirs(session, homePath);
-                       Node userProfile;
-                       if (userHome.hasNode(ArgeoNames.ARGEO_PROFILE)) {
-                               log.warn("userProfile node already exists for userHome path: "
-                                               + homePath + ". We do not add a new one");
-                       } else {
-                               userProfile = userHome.addNode(ArgeoNames.ARGEO_PROFILE);
-                               userProfile.addMixin(ArgeoTypes.ARGEO_USER_PROFILE);
-                               userProfile.setProperty(ArgeoNames.ARGEO_USER_ID, username);
-                               session.save();
-                               // we need to save the profile before adding the user home type
-                       }
-                       userHome.addMixin(ArgeoTypes.ARGEO_USER_HOME);
-                       // see
-                       // http://jackrabbit.510166.n4.nabble.com/Jackrabbit-2-0-beta-6-Problem-adding-a-Mixin-type-with-mandatory-properties-after-setting-propertiesn-td1290332.html
-                       userHome.setProperty(ArgeoNames.ARGEO_USER_ID, username);
-                       session.save();
-                       return userHome;
+                       session.getWorkspace().getObservationManager()
+                                       .removeEventListener(listener);
                } catch (RepositoryException e) {
-                       discardQuietly(session);
-                       throw new ArgeoException("Cannot create home node for user "
-                                       + username, e);
+                       // silent
                }
        }
 
@@ -931,14 +1135,29 @@ public class JcrUtils implements ArgeoJcrConstants {
         */
        public static void updateLastModified(Node node) {
                try {
-                       if (node.isNodeType(NodeType.MIX_LAST_MODIFIED)) {
-                               node.setProperty(Property.JCR_LAST_MODIFIED,
-                                               new GregorianCalendar());
-                               node.setProperty(Property.JCR_LAST_MODIFIED_BY, node
-                                               .getSession().getUserID());
-                       }
+                       if (!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 ArgeoException("Cannot update last modified on " + node,
+                                       e);
+               }
+       }
+
+       /** Update lastModified recursively until this parent. */
+       public static void updateLastModifiedAndParents(Node node, String untilPath) {
+               try {
+                       if (!node.getPath().startsWith(untilPath))
+                               throw new ArgeoException(node + " is not under " + untilPath);
+                       updateLastModified(node);
+                       if (!node.getPath().equals(untilPath))
+                               updateLastModifiedAndParents(node.getParent(), untilPath);
                } catch (RepositoryException e) {
-                       throw new ArgeoException("Cannot update last modified", e);
+                       throw new ArgeoException("Cannot update lastModified from " + node
+                                       + " until " + untilPath, e);
                }
        }
 
@@ -968,4 +1187,310 @@ public class JcrUtils implements ArgeoJcrConstants {
                }
                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.
+        */
+
+       public static long getNodeApproxSize(Node node) {
+               long curNodeSize = 0;
+               try {
+                       PropertyIterator pi = node.getProperties();
+                       while (pi.hasNext()) {
+                               Property prop = pi.nextProperty();
+                               if (prop.isMultiple()) {
+                                       int nb = prop.getLengths().length;
+                                       for (int i = 0; i < nb; i++) {
+                                               curNodeSize += (prop.getLengths()[i] > 0 ? prop
+                                                               .getLengths()[i] : 0);
+                                       }
+                               } else
+                                       curNodeSize += (prop.getLength() > 0 ? prop.getLength() : 0);
+                       }
+
+                       NodeIterator ni = node.getNodes();
+                       while (ni.hasNext())
+                               curNodeSize += getNodeApproxSize(ni.nextNode());
+                       return curNodeSize;
+               } catch (RepositoryException re) {
+                       throw new ArgeoException(
+                                       "Unexpected error while recursively determining node size.",
+                                       re);
+               }
+       }
+
+       /*
+        * SECURITY
+        */
+
+       /**
+        * Convenience method for adding a single privilege to a principal (user or
+        * role), typically jcr:all
+        */
+       public synchronized static void addPrivilege(Session session, String path,
+                       String principal, String privilege) throws RepositoryException {
+               List<Privilege> privileges = new ArrayList<Privilege>();
+               privileges.add(session.getAccessControlManager().privilegeFromName(
+                               privilege));
+               addPrivileges(session, path, new SimplePrincipal(principal), privileges);
+       }
+
+       /**
+        * 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 void addPrivileges(Session session, String path,
+                       Principal principal, List<Privilege> privs)
+                       throws RepositoryException {
+               // make sure the session is in line with the persisted state
+               session.refresh(false);
+               AccessControlManager acm = session.getAccessControlManager();
+               AccessControlList acl = getAccessControlList(acm, path);
+               acl.addAccessControlEntry(principal,
+                               privs.toArray(new Privilege[privs.size()]));
+               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
+                                       + " on " + path);
+               }
+               session.refresh(true);
+               session.save();
+       }
+
+       /** Gets 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()) {
+                       while (policyIterator.hasNext()) {
+                               AccessControlPolicy acp = policyIterator
+                                               .nextAccessControlPolicy();
+                               if (acp instanceof AccessControlList)
+                                       acl = ((AccessControlList) acp);
+                       }
+               } else {
+                       AccessControlPolicy[] existingPolicies = acm.getPolicies(path);
+                       for (AccessControlPolicy acp : existingPolicies) {
+                               if (acp instanceof AccessControlList)
+                                       acl = ((AccessControlList) acp);
+                       }
+               }
+               if (acl != null)
+                       return acl;
+               else
+                       throw new ArgeoException("ACL not found at " + path);
+       }
+
+       /** Clear authorizations for a user at this path */
+       public synchronized static void clearAccessControList(Session session,
+                       String path, String username) throws RepositoryException {
+               AccessControlManager acm = session.getAccessControlManager();
+               AccessControlList acl = getAccessControlList(acm, path);
+               for (AccessControlEntry ace : acl.getAccessControlEntries()) {
+                       if (ace.getPrincipal().getName().equals(username)) {
+                               acl.removeAccessControlEntry(ace);
+                       }
+               }
+       }
+
+       /*
+        * FILES UTILITIES
+        */
+       /**
+        * Creates the nodes making the path as {@link NodeType#NT_FOLDER}
+        */
+       public static Node mkfolders(Session session, String path) {
+               return mkdirs(session, path, NodeType.NT_FOLDER, NodeType.NT_FOLDER,
+                               false);
+       }
+
+       /**
+        * 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
+        * @return how many files were copied
+        */
+       public static Long copyFiles(Node fromNode, Node toNode, Boolean recursive,
+                       ArgeoMonitor monitor) {
+               long count = 0l;
+
+               Binary binary = null;
+               InputStream in = null;
+               try {
+                       NodeIterator fromChildren = fromNode.getNodes();
+                       while (fromChildren.hasNext()) {
+                               if (monitor != null && monitor.isCanceled())
+                                       throw new ArgeoException(
+                                                       "Copy cancelled before it was completed");
+
+                               Node fromChild = fromChildren.nextNode();
+                               String fileName = fromChild.getName();
+                               if (fromChild.isNodeType(NodeType.NT_FILE)) {
+                                       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);
+
+                                       // save session
+                                       toNode.getSession().save();
+                                       count++;
+
+                                       if (log.isDebugEnabled())
+                                               log.debug("Copied file " + fromChild.getPath());
+                                       if (monitor != null)
+                                               monitor.worked(1);
+                               } else if (fromChild.isNodeType(NodeType.NT_FOLDER)
+                                               && recursive) {
+                                       Node toChildFolder;
+                                       if (toNode.hasNode(fileName)) {
+                                               toChildFolder = toNode.getNode(fileName);
+                                               if (!toChildFolder.isNodeType(NodeType.NT_FOLDER))
+                                                       throw new ArgeoException(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);
+                               }
+                       }
+                       return count;
+               } catch (RepositoryException e) {
+                       throw new ArgeoException("Cannot copy files between " + fromNode
+                                       + " and " + toNode);
+               } finally {
+                       // in case there was an exception
+                       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.
+        */
+       public static Long countFiles(Node node) {
+               Long localCount = 0l;
+               try {
+                       for (NodeIterator nit = node.getNodes(); nit.hasNext();) {
+                               Node child = nit.nextNode();
+                               if (child.isNodeType(NodeType.NT_FOLDER))
+                                       localCount = localCount + countFiles(child);
+                               else if (child.isNodeType(NodeType.NT_FILE))
+                                       localCount = localCount + 1;
+                       }
+               } catch (RepositoryException e) {
+                       throw new ArgeoException("Cannot count all children of " + node);
+               }
+               return localCount;
+       }
+
+       /**
+        * 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);
+                       return copyStreamAsFile(folderNode, file.getName(), in);
+               } catch (IOException e) {
+                       throw new ArgeoException("Cannot copy file " + file + " under "
+                                       + folderNode, e);
+               } 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);
+                       return copyStreamAsFile(folderNode, fileName, in);
+               } catch (Exception e) {
+                       throw new ArgeoException("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.
+        * 
+        * @return the created file node
+        */
+       public static Node copyStreamAsFile(Node folderNode, String fileName,
+                       InputStream in) {
+               Binary binary = null;
+               try {
+                       Node fileNode;
+                       Node contentNode;
+                       if (folderNode.hasNode(fileName)) {
+                               fileNode = folderNode.getNode(fileName);
+                               if (!fileNode.isNodeType(NodeType.NT_FILE))
+                                       throw new ArgeoException(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);
+                       }
+                       binary = contentNode.getSession().getValueFactory()
+                                       .createBinary(in);
+                       contentNode.setProperty(Property.JCR_DATA, binary);
+                       return fileNode;
+               } catch (Exception e) {
+                       throw new ArgeoException("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;
+               try {
+                       data = fileNode.getNode(Node.JCR_CONTENT)
+                                       .getProperty(Property.JCR_DATA).getBinary();
+                       in = data.getStream();
+                       return DigestUtils.digest(algorithm, in);
+               } catch (RepositoryException e) {
+                       throw new ArgeoException("Cannot checksum file " + fileNode, e);
+               } finally {
+                       IOUtils.closeQuietly(in);
+                       closeQuietly(data);
+               }
+       }
+
 }