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=fdc69214942fde0a57b9ffd679b789097fc78ca9;hb=7c4ea2803d8dd912e7751b85be4eeb1861905bde;hp=6afd8e13c3467175d2f84716714d1a5ed7fb188f;hpb=477f38e7085ce32c6f09e062cac90f7ba8769698;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 6afd8e13c..fdc692149 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 @@ -1,5 +1,5 @@ /* - * Copyright (C) 2010 Mathieu Baudier + * 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,41 +13,82 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.argeo.jcr; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.security.Principal; import java.text.DateFormat; import java.text.ParseException; +import java.util.ArrayList; import java.util.Calendar; +import java.util.Collections; import java.util.Date; import java.util.GregorianCalendar; +import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.StringTokenizer; import java.util.TreeMap; import javax.jcr.Binary; import javax.jcr.NamespaceRegistry; +import javax.jcr.NoSuchWorkspaceException; import javax.jcr.Node; import javax.jcr.NodeIterator; import javax.jcr.Property; import javax.jcr.PropertyIterator; +import javax.jcr.PropertyType; +import javax.jcr.Repository; import javax.jcr.RepositoryException; +import javax.jcr.RepositoryFactory; import javax.jcr.Session; import javax.jcr.Value; +import javax.jcr.Workspace; import javax.jcr.nodetype.NodeType; +import javax.jcr.observation.EventListener; import javax.jcr.query.Query; import javax.jcr.query.QueryResult; +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 { +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. * @@ -74,13 +115,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("/")) @@ -100,6 +134,80 @@ 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); + } + } + + /** 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 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 + */ + 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) * @@ -110,25 +218,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(); @@ -148,13 +262,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,14 +273,80 @@ public class JcrUtils { 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, 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); + } + + /** + * 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. 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) { try { if (path.equals('/')) return session.getRootNode(); @@ -181,8 +354,7 @@ public class JcrUtils { 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() @@ -191,15 +363,18 @@ public class JcrUtils { return node; } - StringTokenizer st = new StringTokenizer(path, "/"); StringBuffer current = new StringBuffer("/"); Node currentNode = session.getRootNode(); - while (st.hasMoreTokens()) { - String part = st.nextToken(); + Iterator it = tokenize(path).iterator(); + while (it.hasNext()) { + String part = it.next(); current.append(part).append('/'); if (!session.itemExists(current.toString())) { - if (type != null) + if (!it.hasNext() && type != null) currentNode = currentNode.addNode(part, type); + else if (it.hasNext() && intermediaryNodeType != null) + currentNode = currentNode.addNode(part, + intermediaryNodeType); else currentNode = currentNode.addNode(part); if (versioning) @@ -210,11 +385,45 @@ public class JcrUtils { 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 tokenize(String path) { + List tokens = new ArrayList(); + 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); } /** @@ -260,6 +469,11 @@ 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()); @@ -272,15 +486,17 @@ public class JcrUtils { 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(); @@ -299,12 +515,66 @@ public class JcrUtils { } + /** 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. 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(); @@ -314,10 +584,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(); @@ -396,16 +688,18 @@ public class JcrUtils { 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); + } } } } @@ -417,10 +711,14 @@ public class JcrUtils { 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) { @@ -489,16 +787,731 @@ public class JcrUtils { } /** - * 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(':', '_'); } - - public static void closeQuietly(Binary binary){ - if(binary==null) + + /** + * 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(); } + + /** 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); + } + } + + /** + * Copy a file as an nt:file, assuming an nt:folder hierarchy. The session + * is NOT saved. + * + * @return the created file node + */ + public static Node copyFile(Node folderNode, File file) { + InputStream in = null; + try { + in = new FileInputStream(file); + return copyStreamAsFile(folderNode, file.getName(), in); + } catch (IOException e) { + throw new ArgeoException("Cannot copy file " + file + " under " + + folderNode, e); + } finally { + IOUtils.closeQuietly(in); + } + } + + /** Copy bytes as an nt:file */ + public static Node copyBytesAsFile(Node folderNode, String fileName, + byte[] bytes) { + InputStream in = null; + try { + in = new ByteArrayInputStream(bytes); + return copyStreamAsFile(folderNode, fileName, in); + } catch (Exception e) { + throw new ArgeoException("Cannot copy file " + fileName + " under " + + folderNode, e); + } finally { + IOUtils.closeQuietly(in); + } + } + + /** + * Copy a stream as an nt:file, assuming an nt:folder hierarchy. The session + * is NOT saved. + * + * @return the created file node + */ + public static Node copyStreamAsFile(Node folderNode, String fileName, + InputStream in) { + Binary binary = null; + try { + Node fileNode; + Node contentNode; + if (folderNode.hasNode(fileName)) { + fileNode = folderNode.getNode(fileName); + // we assume that the content node is already there + contentNode = fileNode.getNode(Node.JCR_CONTENT); + } else { + fileNode = folderNode.addNode(fileName, NodeType.NT_FILE); + contentNode = fileNode.addNode(Node.JCR_CONTENT, + NodeType.NT_RESOURCE); + } + binary = contentNode.getSession().getValueFactory() + .createBinary(in); + contentNode.setProperty(Property.JCR_DATA, binary); + return fileNode; + } catch (Exception e) { + throw new ArgeoException("Cannot create file node " + fileName + + " under " + folderNode, e); + } finally { + closeQuietly(binary); + } + } + + /** + * 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 parameters = new HashMap(); + 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 parameters = new HashMap(); + 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 false, 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()); + } + } + + /** + * Login to a workspace with implicit credentials, creates the workspace + * with these credentials if it does not already exist. + */ + public static Session loginOrCreateWorkspace(Repository repository, + String workspaceName) throws RepositoryException { + Session workspaceSession = null; + Session defaultSession = null; + try { + try { + workspaceSession = repository.login(workspaceName); + } catch (NoSuchWorkspaceException e) { + // try to create workspace + defaultSession = repository.login(); + defaultSession.getWorkspace().createWorkspace(workspaceName); + workspaceSession = repository.login(workspaceName); + } + return workspaceSession; + } finally { + logoutQuietly(defaultSession); + } + } + + /** Logs out the session, not throwing any exception, even if it is null. */ + public static void logoutQuietly(Session session) { + try { + if (session != null) + if (session.isLive()) + session.logout(); + } catch (Exception e) { + // silent + } + } + + /** + * Convenient method to add a listener. uuids passed as null, deep=true, + * local=true, only one node type + */ + public static void addListener(Session session, EventListener listener, + int eventTypes, String basePath, String nodeType) { + try { + session.getWorkspace() + .getObservationManager() + .addEventListener(listener, eventTypes, basePath, true, + null, new String[] { nodeType }, true); + } catch (RepositoryException e) { + throw new ArgeoException("Cannot add JCR listener " + listener + + " to session " + session, e); + } + } + + /** 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 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; + } + + /** + * 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 { + 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); + } + } + + /** + * Creates an Argeo user home, does nothing if it already exists. Session is + * NOT saved. + */ + public static Node createUserHomeIfNeeded(Session session, String username) { + try { + 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) { + discardQuietly(session); + throw new ArgeoException("Cannot create home for " + username + + " in workspace " + session.getWorkspace().getName(), e); + } + } + + /** + * 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 { + Node userHome = createUserHomeIfNeeded(session, username); + if (userHome.hasNode(ArgeoNames.ARGEO_PROFILE)) + throw new ArgeoException( + "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 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. + */ + 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.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 lastModified from " + node + + " until " + untilPath, 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); + } + } + + /* + * 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 privileges = new ArrayList(); + 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 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); + } + } + }