X-Git-Url: http://git.argeo.org/?a=blobdiff_plain;f=server%2Fruntime%2Forg.argeo.server.jcr%2Fsrc%2Fmain%2Fjava%2Forg%2Fargeo%2Fjcr%2FJcrUtils.java;h=08f8d55c393727aecb32259c6b6f45df97db931c;hb=8b12104293bbdb0127bc46aeea7aa2b776171b39;hp=72b94fbcd0590baef6c4b30049282dd61cdc0ae0;hpb=03db65bd74ce09b696a4c5af15a58df988e5368d;p=lgpl%2Fargeo-commons.git
diff --git a/server/runtime/org.argeo.server.jcr/src/main/java/org/argeo/jcr/JcrUtils.java b/server/runtime/org.argeo.server.jcr/src/main/java/org/argeo/jcr/JcrUtils.java
index 72b94fbcd..08f8d55c3 100644
--- a/server/runtime/org.argeo.server.jcr/src/main/java/org/argeo/jcr/JcrUtils.java
+++ b/server/runtime/org.argeo.server.jcr/src/main/java/org/argeo/jcr/JcrUtils.java
@@ -16,6 +16,9 @@
package org.argeo.jcr;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.DateFormat;
@@ -41,7 +44,9 @@ 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;
@@ -50,6 +55,7 @@ import javax.jcr.query.qom.QueryObjectModelFactory;
import javax.jcr.query.qom.Selector;
import javax.jcr.query.qom.StaticOperand;
+import org.apache.commons.io.IOUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.argeo.ArgeoException;
@@ -58,6 +64,17 @@ import org.argeo.ArgeoException;
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() {
}
@@ -88,13 +105,6 @@ public class JcrUtils implements ArgeoJcrConstants {
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("/"))
@@ -158,25 +168,31 @@ public class JcrUtils implements ArgeoJcrConstants {
*/
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();
@@ -207,14 +223,35 @@ public class JcrUtils implements ArgeoJcrConstants {
return path.substring(index + 1);
}
+ /**
+ * 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);
+ }
+
/** 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,
@@ -325,6 +362,11 @@ public class JcrUtils implements ArgeoJcrConstants {
/** 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());
@@ -365,11 +407,16 @@ public class JcrUtils implements ArgeoJcrConstants {
}
/**
- * 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();
@@ -379,10 +426,32 @@ public class JcrUtils implements ArgeoJcrConstants {
.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();
@@ -554,13 +623,60 @@ public class JcrUtils implements ArgeoJcrConstants {
}
/**
- * Normalize a name so taht it can be stores in contexts not supporting
+ * 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)
@@ -568,6 +684,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
@@ -623,6 +776,21 @@ public class JcrUtils implements ArgeoJcrConstants {
}
}
+ /**
+ * 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 false
, only logging
@@ -640,8 +808,13 @@ public class JcrUtils implements ArgeoJcrConstants {
/** Logs out the session, not throwing any exception, even if it is null. */
public static void logoutQuietly(Session session) {
- if (session != null)
- session.logout();
+ 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. */
@@ -650,6 +823,30 @@ public class JcrUtils implements ArgeoJcrConstants {
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.
*
@@ -657,10 +854,10 @@ public class JcrUtils implements ArgeoJcrConstants {
* 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 userID
- * the id of the user
+ * @param username
+ * the username of the user
*/
- public static Node getUserHome(Session session, String userID) {
+ public static Node getUserHome(Session session, String username) {
try {
QueryObjectModelFactory qomf = session.getWorkspace()
.getQueryManager().getQOMFactory();
@@ -671,14 +868,199 @@ public class JcrUtils implements ArgeoJcrConstants {
DynamicOperand userIdDop = qomf.propertyValue("userHome",
ArgeoNames.ARGEO_USER_ID);
StaticOperand userIdSop = qomf.literal(session.getValueFactory()
- .createValue(userID));
+ .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 " + userID, 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, these properties
+ * are not automatically updated, 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 Node type
+ * Notation 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);
}
}
}