]> git.argeo.org Git - lgpl/argeo-commons.git/blobdiff - server/runtime/org.argeo.server.jcr/src/main/java/org/argeo/jcr/JcrUtils.java
Introduce JCR proxys
[lgpl/argeo-commons.git] / server / runtime / org.argeo.server.jcr / src / main / java / org / argeo / jcr / JcrUtils.java
index a579392dd50e97c7d13d06382f0c3904daf36978..6dfb4a017f57da6870d9ad0a59c81ea11d3aae76 100644 (file)
 
 package org.argeo.jcr;
 
+import java.net.MalformedURLException;
+import java.net.URL;
 import java.text.DateFormat;
 import java.text.ParseException;
 import java.util.Calendar;
 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.Node;
 import javax.jcr.NodeIterator;
 import javax.jcr.Property;
 import javax.jcr.PropertyIterator;
+import javax.jcr.Repository;
 import javax.jcr.RepositoryException;
+import javax.jcr.RepositoryFactory;
 import javax.jcr.Session;
 import javax.jcr.Value;
+import javax.jcr.Workspace;
+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 org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 import org.argeo.ArgeoException;
 
 /** Utility methods to simplify common JCR operations. */
-public class JcrUtils {
+public class JcrUtils implements ArgeoJcrConstants {
        private final static Log log = LogFactory.getLog(JcrUtils.class);
 
+       /**
+        * Not complete yet. See
+        * http://www.day.com/specs/jcr/2.0/3_Repository_Model.html#3.2.2%20Local
+        * %20Names
+        */
+       public final static char[] INVALID_NAME_CHARACTERS = { '/', ':', '[', ']',
+                       '|', '*', /*
+                                        * invalid XML chars :
+                                        */
+                       '<', '>', '&' };
+
+       /** Prevents instantiation */
+       private JcrUtils() {
+       }
+
        /**
         * Queries one single node.
         * 
@@ -72,13 +101,6 @@ public class JcrUtils {
                return node;
        }
 
-       /** Removes forbidden characters from a path, replacing them with '_' */
-       public static String removeForbiddenCharacters(String str) {
-               return str.replace('[', '_').replace(']', '_').replace('/', '_')
-                               .replace('*', '_');
-
-       }
-
        /** Retrieves the parent path of the provided path */
        public static String parentPath(String path) {
                if (path.equals("/"))
@@ -98,6 +120,40 @@ public class JcrUtils {
                return dateAsPath(cal, false);
        }
 
+       /**
+        * Creates a deep path based on a URL:
+        * http://subdomain.example.com/to/content?args =>
+        * com/example/subdomain/to/content
+        */
+       public static String urlAsPath(String url) {
+               try {
+                       URL u = new URL(url);
+                       StringBuffer path = new StringBuffer(url.length());
+                       // invert host
+                       path.append(hostAsPath(u.getHost()));
+                       // we don't put port since it may not always be there and may change
+                       path.append(u.getPath());
+                       return path.toString();
+               } catch (MalformedURLException e) {
+                       throw new ArgeoException("Cannot generate URL path for " + url, e);
+               }
+       }
+
+       /**
+        * Creates a path from a FQDN, inverting the order of the component:
+        * www.argeo.org => org.argeo.www
+        */
+       public static String hostAsPath(String host) {
+               StringBuffer path = new StringBuffer(host.length());
+               String[] hostTokens = host.split("\\.");
+               for (int i = hostTokens.length - 1; i >= 0; i--) {
+                       path.append(hostTokens[i]);
+                       if (i != 0)
+                               path.append('/');
+               }
+               return path.toString();
+       }
+
        /**
         * The provided data as a path ('/' at the end, not the beginning)
         * 
@@ -108,25 +164,31 @@ public class JcrUtils {
         */
        public static String dateAsPath(Calendar cal, Boolean addHour) {
                StringBuffer buf = new StringBuffer(14);
-               buf.append('Y').append(cal.get(Calendar.YEAR));// 5
-               buf.append('/');// 1
+               buf.append('Y');
+               buf.append(cal.get(Calendar.YEAR));
+               buf.append('/');
+
                int month = cal.get(Calendar.MONTH) + 1;
                buf.append('M');
                if (month < 10)
                        buf.append(0);
-               buf.append(month);// 3
-               buf.append('/');// 1
+               buf.append(month);
+               buf.append('/');
+
                int day = cal.get(Calendar.DAY_OF_MONTH);
+               buf.append('D');
                if (day < 10)
                        buf.append(0);
-               buf.append('D').append(day);// 3
-               buf.append('/');// 1
+               buf.append(day);
+               buf.append('/');
+
                if (addHour) {
                        int hour = cal.get(Calendar.HOUR_OF_DAY);
+                       buf.append('H');
                        if (hour < 10)
                                buf.append(0);
-                       buf.append('H').append(hour);// 3
-                       buf.append('/');// 1
+                       buf.append(hour);
+                       buf.append('/');
                }
                return buf.toString();
 
@@ -146,13 +208,6 @@ public class JcrUtils {
 
        }
 
-       /** Converts the FQDN of an host into a path (converts '.' into '/'). */
-       public static String hostAsPath(String host) {
-               // TODO : inverse order of the elements (to have org/argeo/test IO
-               // test/argeo/org
-               return host.replace('.', '/');
-       }
-
        /** The last element of a path. */
        public static String lastPathElement(String path) {
                if (path.charAt(path.length() - 1) == '/')
@@ -166,12 +221,34 @@ public class JcrUtils {
 
        /** Creates the nodes making path, if they don't exist. */
        public static Node mkdirs(Session session, String path) {
-               return mkdirs(session, path, null, false);
+               return mkdirs(session, path, null, null, false);
        }
 
-       /** Creates the nodes making path, if they don't exist. */
+       /**
+        * use {@link #mkdirs(Session, String, String, String, Boolean)} instead.
+        * 
+        * @deprecated
+        */
+       @Deprecated
        public static Node mkdirs(Session session, String path, String type,
                        Boolean versioning) {
+               return mkdirs(session, path, type, type, false);
+       }
+
+       /**
+        * @param type
+        *            the type of the leaf node
+        */
+       public static Node mkdirs(Session session, String path, String type) {
+               return mkdirs(session, path, type, null, false);
+       }
+
+       /**
+        * Creates the nodes making path, if they don't exist. This is up to the
+        * caller to save the session.
+        */
+       public static Node mkdirs(Session session, String path, String type,
+                       String intermediaryNodeType, Boolean versioning) {
                try {
                        if (path.equals('/'))
                                return session.getRootNode();
@@ -196,19 +273,22 @@ public class JcrUtils {
                                String part = st.nextToken();
                                current.append(part).append('/');
                                if (!session.itemExists(current.toString())) {
-                                       if (type != null)
+                                       if (!st.hasMoreTokens() && type != null)
                                                currentNode = currentNode.addNode(part, type);
+                                       else if (st.hasMoreTokens() && intermediaryNodeType != null)
+                                               currentNode = currentNode.addNode(part,
+                                                               intermediaryNodeType);
                                        else
                                                currentNode = currentNode.addNode(part);
                                        if (versioning)
-                                               currentNode.addMixin(ArgeoJcrConstants.MIX_VERSIONABLE);
+                                               currentNode.addMixin(NodeType.MIX_VERSIONABLE);
                                        if (log.isTraceEnabled())
                                                log.debug("Added folder " + part + " as " + current);
                                } else {
                                        currentNode = (Node) session.getItem(current.toString());
                                }
                        }
-                       session.save();
+                       // session.save();
                        return currentNode;
                } catch (RepositoryException e) {
                        throw new ArgeoException("Cannot mkdirs " + path, e);
@@ -258,11 +338,16 @@ public class JcrUtils {
 
        /** Recursively outputs the contents of the given node. */
        public static void debug(Node node) {
+               debug(node, log);
+       }
+
+       /** Recursively outputs the contents of the given node. */
+       public static void debug(Node node, Log log) {
                try {
                        // First output the node path
                        log.debug(node.getPath());
                        // Skip the virtual (and large!) jcr:system subtree
-                       if (node.getName().equals(ArgeoJcrConstants.JCR_SYSTEM)) {
+                       if (node.getName().equals("jcr:system")) {
                                return;
                        }
 
@@ -298,11 +383,16 @@ public class JcrUtils {
        }
 
        /**
-        * Copies recursively the content of a node to another one. Mixin are NOT
-        * copied.
+        * Copies recursively the content of a node to another one. Do NOT copy the
+        * property values of {@link NodeType#MIX_CREATED} and
+        * {@link NodeType#MIX_LAST_MODIFIED}, but update the
+        * {@link Property#JCR_LAST_MODIFIED} and
+        * {@link Property#JCR_LAST_MODIFIED_BY} properties if the target node has
+        * the {@link NodeType#MIX_LAST_MODIFIED} mixin.
         */
        public static void copy(Node fromNode, Node toNode) {
                try {
+                       // process properties
                        PropertyIterator pit = fromNode.getProperties();
                        properties: while (pit.hasNext()) {
                                Property fromProperty = pit.nextProperty();
@@ -312,10 +402,32 @@ public class JcrUtils {
                                                                .isProtected())
                                        continue properties;
 
-                               toNode.setProperty(fromProperty.getName(),
-                                               fromProperty.getValue());
+                               if (fromProperty.getDefinition().isProtected())
+                                       continue properties;
+
+                               if (propertyName.equals("jcr:created")
+                                               || propertyName.equals("jcr:createdBy")
+                                               || propertyName.equals("jcr:lastModified")
+                                               || propertyName.equals("jcr:lastModifiedBy"))
+                                       continue properties;
+
+                               if (fromProperty.isMultiple()) {
+                                       toNode.setProperty(propertyName, fromProperty.getValues());
+                               } else {
+                                       toNode.setProperty(propertyName, fromProperty.getValue());
+                               }
                        }
 
+                       // update jcr:lastModified and jcr:lastModifiedBy in toNode in case
+                       // they existed, before adding the mixins
+                       updateLastModified(toNode);
+
+                       // add mixins
+                       for (NodeType mixinType : fromNode.getMixinNodeTypes()) {
+                               toNode.addMixin(mixinType.getName());
+                       }
+
+                       // process children nodes
                        NodeIterator nit = fromNode.getNodes();
                        while (nit.hasNext()) {
                                Node fromChild = nit.nextNode();
@@ -438,16 +550,26 @@ public class JcrUtils {
                try {
                        Iterator<String> pit = properties.iterator();
 
-                       while (pit.hasNext()) {
+                       props: while (pit.hasNext()) {
                                String name = pit.next();
-                               if (!observed.hasProperty(name)) {
+                               if (!reference.hasProperty(name)) {
+                                       if (!observed.hasProperty(name))
+                                               continue props;
+                                       Value val = observed.getProperty(name).getValue();
+                                       try {
+                                               // empty String but not null
+                                               if ("".equals(val.getString()))
+                                                       continue props;
+                                       } catch (Exception e) {
+                                               // not parseable as String, silent
+                                       }
+                                       PropertyDiff pDiff = new PropertyDiff(PropertyDiff.ADDED,
+                                                       name, null, val);
+                                       diffs.put(name, pDiff);
+                               } else if (!observed.hasProperty(name)) {
                                        PropertyDiff pDiff = new PropertyDiff(PropertyDiff.REMOVED,
                                                        name, reference.getProperty(name).getValue(), null);
                                        diffs.put(name, pDiff);
-                               } else if (!reference.hasProperty(name)) {
-                                       PropertyDiff pDiff = new PropertyDiff(PropertyDiff.ADDED,
-                                                       name, null, observed.getProperty(name).getValue());
-                                       diffs.put(name, pDiff);
                                } else {
                                        Value referenceValue = reference.getProperty(name)
                                                        .getValue();
@@ -475,4 +597,409 @@ public class JcrUtils {
                else
                        return baseRelPath + '/' + propertyName;
        }
+
+       /**
+        * Normalizes a name so that it can be stored in contexts not supporting
+        * names with ':' (typically databases). Replaces ':' by '_'.
+        */
+       public static String normalize(String name) {
+               return name.replace(':', '_');
+       }
+
+       /**
+        * Replaces characters which are invalid in a JCR name by '_'. Currently not
+        * exhaustive.
+        * 
+        * @see JcrUtils#INVALID_NAME_CHARACTERS
+        */
+       public static String replaceInvalidChars(String name) {
+               return replaceInvalidChars(name, '_');
+       }
+
+       /**
+        * Replaces characters which are invalid in a JCR name. Currently not
+        * exhaustive.
+        * 
+        * @see JcrUtils#INVALID_NAME_CHARACTERS
+        */
+       public static String replaceInvalidChars(String name, char replacement) {
+               boolean modified = false;
+               char[] arr = name.toCharArray();
+               for (int i = 0; i < arr.length; i++) {
+                       char c = arr[i];
+                       invalid: for (char invalid : INVALID_NAME_CHARACTERS) {
+                               if (c == invalid) {
+                                       arr[i] = replacement;
+                                       modified = true;
+                                       break invalid;
+                               }
+                       }
+               }
+               if (modified)
+                       return new String(arr);
+               else
+                       // do not create new object if unnecessary
+                       return name;
+       }
+
+       /**
+        * Removes forbidden characters from a path, replacing them with '_'
+        * 
+        * @deprecated use {@link #replaceInvalidChars(String)} instead
+        */
+       public static String removeForbiddenCharacters(String str) {
+               return str.replace('[', '_').replace(']', '_').replace('/', '_')
+                               .replace('*', '_');
+
+       }
+
+       /** Cleanly disposes a {@link Binary} even if it is null. */
+       public static void closeQuietly(Binary binary) {
+               if (binary == null)
+                       return;
+               binary.dispose();
+       }
+
+       /**
+        * Creates depth from a string (typically a username) by adding levels based
+        * on its first characters: "aBcD",2 => a/aB
+        */
+       public static String firstCharsToPath(String str, Integer nbrOfChars) {
+               if (str.length() < nbrOfChars)
+                       throw new ArgeoException("String " + str
+                                       + " length must be greater or equal than " + nbrOfChars);
+               StringBuffer path = new StringBuffer("");
+               StringBuffer curr = new StringBuffer("");
+               for (int i = 0; i < nbrOfChars; i++) {
+                       curr.append(str.charAt(i));
+                       path.append(curr);
+                       if (i < nbrOfChars - 1)
+                               path.append('/');
+               }
+               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.
+        * 
+        * @see #discardQuietly(Session)
+        */
+       public static void discardUnderlyingSessionQuietly(Node node) {
+               try {
+                       discardQuietly(node.getSession());
+               } catch (RepositoryException e) {
+                       log.warn("Cannot quietly discard session of node " + node + ": "
+                                       + e.getMessage());
+               }
+       }
+
+       /**
+        * Discards the current changes in a session by calling
+        * {@link Session#refresh(boolean)} with <code>false</code>, only logging
+        * potential errors when doing so. To be used typically in a catch block.
+        */
+       public static void discardQuietly(Session session) {
+               try {
+                       if (session != null)
+                               session.refresh(false);
+               } catch (RepositoryException e) {
+                       log.warn("Cannot quietly discard session " + session + ": "
+                                       + e.getMessage());
+               }
+       }
+
+       /** Logs out the session, not throwing any exception, even if it is null. */
+       public static void logoutQuietly(Session session) {
+               try {
+                       if (session != null)
+                               if (session.isLive())
+                                       session.logout();
+               } catch (Exception 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 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.
+        */
+       public static String getUserHomePath(Session session) {
+               String userID = session.getUserID();
+               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;
+               } catch (RepositoryException e) {
+                       throw new ArgeoException("Cannot find home for user " + username, 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) {
+               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;
+               } catch (RepositoryException e) {
+                       discardQuietly(session);
+                       throw new ArgeoException("Cannot create home node for user "
+                                       + username, e);
+               }
+       }
+
+       /**
+        * Quietly unregisters an {@link EventListener} from the udnerlying
+        * workspace of this node.
+        */
+       public static void unregisterQuietly(Node node, EventListener eventListener) {
+               try {
+                       unregisterQuietly(node.getSession().getWorkspace(), eventListener);
+               } catch (RepositoryException e) {
+                       // silent
+                       if (log.isTraceEnabled())
+                               log.trace("Could not unregister event listener "
+                                               + eventListener);
+               }
+       }
+
+       /** Quietly unregisters an {@link EventListener} from this workspace */
+       public static void unregisterQuietly(Workspace workspace,
+                       EventListener eventListener) {
+               if (eventListener == null)
+                       return;
+               try {
+                       workspace.getObservationManager()
+                                       .removeEventListener(eventListener);
+               } catch (RepositoryException e) {
+                       // silent
+                       if (log.isTraceEnabled())
+                               log.trace("Could not unregister event listener "
+                                               + eventListener);
+               }
+       }
+
+       /**
+        * If this node is has the {@link NodeType#MIX_LAST_MODIFIED} mixin, it
+        * updates the {@link Property#JCR_LAST_MODIFIED} property with the current
+        * time and the {@link Property#JCR_LAST_MODIFIED_BY} property with the
+        * underlying session user id. In Jackrabbit 2.x, <a
+        * href="https://issues.apache.org/jira/browse/JCR-2233">these properties
+        * are not automatically updated</a>, hence the need for manual update. The
+        * session is not saved.
+        */
+       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());
+                       }
+               } catch (RepositoryException e) {
+                       throw new ArgeoException("Cannot update last modified", e);
+               }
+       }
+
+       /**
+        * Returns a String representing the short version (see <a
+        * href="http://jackrabbit.apache.org/node-type-notation.html"> Node type
+        * Notation </a> attributes grammar) of the main business attributes of this
+        * property definition
+        * 
+        * @param prop
+        */
+       public static String getPropertyDefinitionAsString(Property prop) {
+               StringBuffer sbuf = new StringBuffer();
+               try {
+                       if (prop.getDefinition().isAutoCreated())
+                               sbuf.append("a");
+                       if (prop.getDefinition().isMandatory())
+                               sbuf.append("m");
+                       if (prop.getDefinition().isProtected())
+                               sbuf.append("p");
+                       if (prop.getDefinition().isMultiple())
+                               sbuf.append("*");
+               } catch (RepositoryException re) {
+                       throw new ArgeoException(
+                                       "unexpected error while getting property definition as String",
+                                       re);
+               }
+               return sbuf.toString();
+       }
+
+       /**
+        * Estimate the sub tree size from current node. Computation is based on the
+        * Jcr {@link Property.getLength()} method. Note : it is not the exact size
+        * used on the disk by the current part of the JCR Tree.
+        */
+
+       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);
+               }
+       }
 }