X-Git-Url: https://git.argeo.org/?a=blobdiff_plain;f=server%2Fruntime%2Forg.argeo.server.jcr%2Fsrc%2Fmain%2Fjava%2Forg%2Fargeo%2Fjcr%2FJcrUtils.java;h=6dfb4a017f57da6870d9ad0a59c81ea11d3aae76;hb=2d4dd736ab07b1ef3aaec0a9e5d29f30c551de9c;hp=6edb854cb6d3f3df5a8458c54f057fbd86f6af53;hpb=da55282938aaebf9fa148454dbc8add9c558501f;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 6edb854cb..6dfb4a017 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 @@ -41,7 +41,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; @@ -58,6 +60,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 +101,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 +164,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(); @@ -213,8 +225,9 @@ public class JcrUtils implements ArgeoJcrConstants { } /** - * @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 +338,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 +383,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 +402,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 +599,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) @@ -623,6 +715,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 +747,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. */ @@ -727,6 +839,7 @@ public class JcrUtils implements ArgeoJcrConstants { } } + /** Creates an Argeo user home. */ public static Node createUserHome(Session session, String homeBasePath, String username) { try { @@ -735,15 +848,35 @@ public class JcrUtils implements ArgeoJcrConstants { if (session.hasPendingChanges()) throw new ArgeoException( "Session has pending changes, save them first"); + String homePath = homeBasePath + '/' + firstCharsToPath(username, 2) + '/' + username; - Node userHome = JcrUtils.mkdirs(session, homePath); - Node 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 + 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 @@ -756,4 +889,117 @@ public class JcrUtils implements ArgeoJcrConstants { + 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); + } + } }