+ + reference + " and " + observed, e);
+ }
+ }
+
+ public static Map<String, PropertyDiff> diffProperties(Node reference,
+ Node observed) {
+ Map<String, PropertyDiff> diffs = new TreeMap<String, PropertyDiff>();
+ diffPropertiesLevel(diffs, null, reference, observed);
+ return diffs;
+ }
+
+ /**
+ * Compare the properties of two nodes. Recursivity to child nodes is not
+ * yet supported. Skip jcr:* properties.
+ */
+ static void diffPropertiesLevel(Map<String, PropertyDiff> diffs,
+ String baseRelPath, Node reference, Node observed) {
+ try {
+ // check removed and modified
+ PropertyIterator pit = reference.getProperties();
+ props: while (pit.hasNext()) {
+ Property p = pit.nextProperty();
+ String name = p.getName();
+ if (name.startsWith("jcr:"))
+ continue props;
+
+ if (!observed.hasProperty(name)) {
+ String relPath = propertyRelPath(baseRelPath, name);
+ PropertyDiff pDiff = new PropertyDiff(PropertyDiff.REMOVED,
+ 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);
+ }
+ }
+ }
+ // check added
+ pit = observed.getProperties();
+ props: while (pit.hasNext()) {
+ Property p = pit.nextProperty();
+ String name = p.getName();
+ 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);
+ }
+ }
+ } catch (RepositoryException e) {
+ throw new ArgeoException("Cannot diff " + reference + " and "
+ + observed, e);
+ }
+ }
+
+ /**
+ * Compare only a restricted list of properties of two nodes. No
+ * recursivity.
+ *
+ */
+ public static Map<String, PropertyDiff> diffProperties(Node reference,
+ Node observed, List<String> properties) {
+ Map<String, PropertyDiff> diffs = new TreeMap<String, PropertyDiff>();
+ try {
+ Iterator<String> pit = properties.iterator();
+
+ props: while (pit.hasNext()) {
+ String name = pit.next();
+ 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 {
+ Value referenceValue = reference.getProperty(name)
+ .getValue();
+ Value newValue = observed.getProperty(name).getValue();
+ if (!referenceValue.equals(newValue)) {
+ PropertyDiff pDiff = new PropertyDiff(
+ PropertyDiff.MODIFIED, name, referenceValue,
+ newValue);
+ diffs.put(name, pDiff);
+ }
+ }
+ }
+ } catch (RepositoryException e) {
+ throw new ArgeoException("Cannot diff " + reference + " and "
+ + observed, e);
+ }
+ return diffs;
+ }
+
+ /** Builds a property relPath to be used in the diff. */
+ private static String propertyRelPath(String baseRelPath,
+ String propertyName) {
+ if (baseRelPath == null)
+ return propertyName;
+ else
+ return baseRelPath + '/' + propertyName;
+ }
+
+ /**
+ * Normalize a name so taht it can be stores in contexts not supporting
+ * names with ':' (typically databases). Replaces ':' by '_'.
+ */
+ public static String normalize(String name) {
+ return name.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 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) {
+ if (session != null)
+ session.logout();
+ }
+
+ /** 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);
+ }
+ }
+
+ 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;
+ 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
+ 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);