]> git.argeo.org Git - lgpl/argeo-commons.git/blobdiff - server/runtime/org.argeo.server.jcr/src/main/java/org/argeo/jcr/JcrUtils.java
Update license headers
[lgpl/argeo-commons.git] / server / runtime / org.argeo.server.jcr / src / main / java / org / argeo / jcr / JcrUtils.java
index d9da84ab3f0a5c8e9631407d7f9130f3e5c30c58..e2f5b54279ad424ce3aec09248f695d2f1c7ab1c 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.
@@ -13,7 +13,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 package org.argeo.jcr;
 
 import java.io.ByteArrayInputStream;
@@ -21,16 +20,18 @@ import java.io.ByteArrayOutputStream;
 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;
@@ -39,6 +40,7 @@ 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;
@@ -49,19 +51,23 @@ 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 javax.jcr.version.VersionManager;
 
 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.util.security.SimplePrincipal;
 
 /** Utility methods to simplify common JCR operations. */
 public class JcrUtils implements ArgeoJcrConstants {
+
        private final static Log log = LogFactory.getLog(JcrUtils.class);
 
        /**
@@ -143,6 +149,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
@@ -267,9 +313,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) {
@@ -280,8 +350,7 @@ 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))
                                        throw new ArgeoException("Node " + node
                                                        + " exists but is of type "
                                                        + node.getPrimaryNodeType().getName()
@@ -290,16 +359,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
@@ -312,11 +381,45 @@ 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);
        }
 
        /**
@@ -379,15 +482,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();
@@ -406,6 +511,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
@@ -530,16 +684,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);
+                                               }
                                        }
                                }
                        }
@@ -551,10 +707,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) {
@@ -817,34 +977,47 @@ 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);
+                       session.getWorkspace()
+                                       .getObservationManager()
+                                       .addEventListener(listener, eventTypes, basePath, true,
+                                                       null, new String[] { nodeType }, true);
                } catch (RepositoryException e) {
-                       throw new ArgeoException("Cannot find user home path", e);
+                       throw new ArgeoException("Cannot add JCR listener " + listener
+                                       + " to session " + session, e);
                }
        }
 
-       /** Get the profile of the user attached to this session. */
-       public static Node getUserProfile(Session session) {
+       /** Removes a listener without throwing exception */
+       public static void removeListenerQuietly(Session session,
+                       EventListener listener) {
+               if (session == null || !session.isLive())
+                       return;
+               try {
+                       session.getWorkspace().getObservationManager()
+                                       .removeEventListener(listener);
+               } catch (RepositoryException e) {
+                       // silent
+               }
+       }
+
+       /** 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 getUserProfile(session, userID);
+               return getUserHome(session, userID);
+       }
+
+       /** User home path is NOT configurable */
+       public static String getUserHomePath(String username) {
+               String homeBasePath = DEFAULT_HOME_BASE_PATH;
+               return homeBasePath + '/' + firstCharsToPath(username, 2) + '/'
+                               + username;
        }
 
        /**
@@ -859,98 +1032,210 @@ public class JcrUtils implements ArgeoJcrConstants {
         */
        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;
+                       String homePath = getUserHomePath(username);
+                       return session.itemExists(homePath) ? session.getNode(homePath)
+                                       : null;
+                       // kept for example of QOM queries
+                       // QueryObjectModelFactory qomf = session.getWorkspace()
+                       // .getQueryManager().getQOMFactory();
+                       // 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);
                } catch (RepositoryException e) {
                        throw new ArgeoException("Cannot find home for user " + username, e);
                }
        }
 
-       public static Node getUserProfile(Session session, String username) {
+       /**
+        * Creates an Argeo user home, does nothing if it already exists. Session is
+        * NOT saved.
+        */
+       public static Node createUserHomeIfNeeded(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;
+                       String homePath = getUserHomePath(username);
+                       if (session.itemExists(homePath))
+                               return session.getNode(homePath);
+                       else {
+                               Node userHome = JcrUtils.mkdirs(session, homePath);
+                               userHome.addMixin(ArgeoTypes.ARGEO_USER_HOME);
+                               userHome.setProperty(ArgeoNames.ARGEO_USER_ID, username);
+                               return userHome;
+                       }
                } catch (RepositoryException e) {
-                       throw new ArgeoException(
-                                       "Cannot find profile for user " + username, e);
+                       discardQuietly(session);
+                       throw new ArgeoException("Cannot create home for " + username
+                                       + " in workspace " + session.getWorkspace().getName(), e);
                }
        }
 
-       /** Creates an Argeo user home. */
-       public static Node createUserHome(Session session, String homeBasePath,
-                       String username) {
+       /**
+        * Creates a user profile in the home of this user. Creates the home if
+        * needed, but throw an exception if a profile already exists. The session
+        * is not saved and the node is in a checkedOut state (that is, it requires
+        * a subsequent checkin after saving the session).
+        */
+       public static Node createUserProfile(Session session, String username) {
                try {
-                       if (session == null)
-                               throw new ArgeoException("Session is null");
-                       if (session.hasPendingChanges())
+                       Node userHome = createUserHomeIfNeeded(session, username);
+                       if (userHome.hasNode(ArgeoNames.ARGEO_PROFILE))
                                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;
+                                               "There is already a user profile under " + userHome);
+                       Node userProfile = userHome.addNode(ArgeoNames.ARGEO_PROFILE);
+                       userProfile.addMixin(ArgeoTypes.ARGEO_USER_PROFILE);
+                       userProfile.setProperty(ArgeoNames.ARGEO_USER_ID, username);
+                       userProfile.setProperty(ArgeoNames.ARGEO_ENABLED, true);
+                       userProfile.setProperty(ArgeoNames.ARGEO_ACCOUNT_NON_EXPIRED, true);
+                       userProfile.setProperty(ArgeoNames.ARGEO_ACCOUNT_NON_LOCKED, true);
+                       userProfile.setProperty(ArgeoNames.ARGEO_CREDENTIALS_NON_EXPIRED,
+                                       true);
+                       return userProfile;
                } catch (RepositoryException e) {
                        discardQuietly(session);
-                       throw new ArgeoException("Cannot create home node for user "
-                                       + username, e);
+                       throw new ArgeoException("Cannot create user profile for "
+                                       + username + " in workspace "
+                                       + session.getWorkspace().getName(), e);
+               }
+       }
+
+       /**
+        * Create user profile if needed, the session IS saved.
+        * 
+        * @return the user profile
+        */
+       public static Node createUserProfileIfNeeded(Session securitySession,
+                       String username) {
+               try {
+                       Node userHome = JcrUtils.createUserHomeIfNeeded(securitySession,
+                                       username);
+                       Node userProfile = userHome.hasNode(ArgeoNames.ARGEO_PROFILE) ? userHome
+                                       .getNode(ArgeoNames.ARGEO_PROFILE) : JcrUtils
+                                       .createUserProfile(securitySession, username);
+                       if (securitySession.hasPendingChanges())
+                               securitySession.save();
+                       VersionManager versionManager = securitySession.getWorkspace()
+                                       .getVersionManager();
+                       if (versionManager.isCheckedOut(userProfile.getPath()))
+                               versionManager.checkin(userProfile.getPath());
+                       return userProfile;
+               } catch (RepositoryException e) {
+                       discardQuietly(securitySession);
+                       throw new ArgeoException("Cannot create user profile for "
+                                       + username + " in workspace "
+                                       + securitySession.getWorkspace().getName(), e);
                }
        }
 
+       /** Creates an Argeo user home. */
+       // public static Node createUserHome(Session session, String homeBasePath,
+       // String username) {
+       // 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 = getUserHomePath(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);
+       // // session.getWorkspace().getVersionManager()
+       // // .checkout(userProfile.getPath());
+       // userProfile.setProperty(ArgeoNames.ARGEO_USER_ID, username);
+       // session.save();
+       // session.getWorkspace().getVersionManager()
+       // .checkin(userProfile.getPath());
+       // // 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;
+       // } catch (RepositoryException e) {
+       // discardQuietly(session);
+       // throw new ArgeoException("Cannot create home node for user "
+       // + username, e);
+       // }
+       // }
+
+       /**
+        * Returns user home has path, embedding exceptions. Contrary to
+        * {@link #getUserHome(Session)}, it never returns null but throws and
+        * exception if not found.
+        * 
+        * @deprecated use getUserHome() instead, throwing an exception if it
+        *             returns null
+        */
+       @Deprecated
+       public static String getUserHomePath(Session session) {
+               String userID = session.getUserID();
+               try {
+                       String homePath = getUserHomePath(userID);
+                       if (session.itemExists(homePath))
+                               return homePath;
+                       else
+                               throw new ArgeoException("No home registered for " + userID);
+               } catch (RepositoryException e) {
+                       throw new ArgeoException("Cannot find user home path", e);
+               }
+       }
+
+       /**
+        * @return null if not found *
+        */
+       public static Node getUserProfile(Session session, String username) {
+               try {
+                       Node userHome = getUserHome(session, username);
+                       if (userHome == null)
+                               return null;
+                       if (userHome.hasNode(ArgeoNames.ARGEO_PROFILE))
+                               return userHome.getNode(ArgeoNames.ARGEO_PROFILE);
+                       else
+                               return null;
+               } catch (RepositoryException e) {
+                       throw new ArgeoException(
+                                       "Cannot find profile for user " + username, 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);
+       }
+
        /**
         * Quietly unregisters an {@link EventListener} from the udnerlying
         * workspace of this node.
@@ -993,14 +1278,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);
                }
        }
 
@@ -1056,7 +1356,6 @@ public class JcrUtils implements ArgeoJcrConstants {
                        NodeIterator ni = node.getNodes();
                        while (ni.hasNext())
                                curNodeSize += getNodeApproxSize(ni.nextNode());
-                       log.debug(node + ": " + curNodeSize);
                        return curNodeSize;
                } catch (RepositoryException re) {
                        throw new ArgeoException(
@@ -1064,4 +1363,61 @@ public class JcrUtils implements ArgeoJcrConstants {
                                        re);
                }
        }
+
+       /*
+        * SECURITY
+        */
+
+       /**
+        * Convenience method for adding a single privilege to a principal (user or
+        * role), typically jcr:all
+        */
+       public 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.
+        */
+       public static void addPrivileges(Session session, String path,
+                       Principal principal, List<Privilege> privs)
+                       throws RepositoryException {
+               AccessControlManager acm = session.getAccessControlManager();
+               // 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) {
+                       acl.addAccessControlEntry(principal,
+                                       privs.toArray(new Privilege[privs.size()]));
+                       acm.setPolicy(path, acl);
+                       if (log.isDebugEnabled())
+                               log.debug("Added privileges " + privs + " to " + principal
+                                               + " on " + path);
+               } else {
+                       throw new ArgeoException("Don't know how to apply  privileges "
+                                       + privs + " to " + principal + " on " + path);
+               }
+       }
+
 }