2 * Copyright (C) 2010 Mathieu Baudier <mbaudier@argeo.org>
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 package org
.argeo
.jcr
;
19 import java
.io
.ByteArrayInputStream
;
20 import java
.io
.ByteArrayOutputStream
;
21 import java
.io
.InputStream
;
22 import java
.net
.MalformedURLException
;
24 import java
.text
.DateFormat
;
25 import java
.text
.ParseException
;
26 import java
.util
.Calendar
;
27 import java
.util
.Date
;
28 import java
.util
.GregorianCalendar
;
29 import java
.util
.HashMap
;
30 import java
.util
.Iterator
;
31 import java
.util
.List
;
33 import java
.util
.StringTokenizer
;
34 import java
.util
.TreeMap
;
36 import javax
.jcr
.Binary
;
37 import javax
.jcr
.NamespaceRegistry
;
38 import javax
.jcr
.Node
;
39 import javax
.jcr
.NodeIterator
;
40 import javax
.jcr
.Property
;
41 import javax
.jcr
.PropertyIterator
;
42 import javax
.jcr
.Repository
;
43 import javax
.jcr
.RepositoryException
;
44 import javax
.jcr
.RepositoryFactory
;
45 import javax
.jcr
.Session
;
46 import javax
.jcr
.Value
;
47 import javax
.jcr
.Workspace
;
48 import javax
.jcr
.nodetype
.NodeType
;
49 import javax
.jcr
.observation
.EventListener
;
50 import javax
.jcr
.query
.Query
;
51 import javax
.jcr
.query
.QueryResult
;
52 import javax
.jcr
.query
.qom
.Constraint
;
53 import javax
.jcr
.query
.qom
.DynamicOperand
;
54 import javax
.jcr
.query
.qom
.QueryObjectModelFactory
;
55 import javax
.jcr
.query
.qom
.Selector
;
56 import javax
.jcr
.query
.qom
.StaticOperand
;
58 import org
.apache
.commons
.io
.IOUtils
;
59 import org
.apache
.commons
.logging
.Log
;
60 import org
.apache
.commons
.logging
.LogFactory
;
61 import org
.argeo
.ArgeoException
;
63 /** Utility methods to simplify common JCR operations. */
64 public class JcrUtils
implements ArgeoJcrConstants
{
65 private final static Log log
= LogFactory
.getLog(JcrUtils
.class);
68 * Not complete yet. See
69 * http://www.day.com/specs/jcr/2.0/3_Repository_Model.html#3.2.2%20Local
72 public final static char[] INVALID_NAME_CHARACTERS
= { '/', ':', '[', ']',
78 /** Prevents instantiation */
83 * Queries one single node.
85 * @return one single node or null if none was found
86 * @throws ArgeoException
87 * if more than one node was found
89 public static Node
querySingleNode(Query query
) {
90 NodeIterator nodeIterator
;
92 QueryResult queryResult
= query
.execute();
93 nodeIterator
= queryResult
.getNodes();
94 } catch (RepositoryException e
) {
95 throw new ArgeoException("Cannot execute query " + query
, e
);
98 if (nodeIterator
.hasNext())
99 node
= nodeIterator
.nextNode();
103 if (nodeIterator
.hasNext())
104 throw new ArgeoException("Query returned more than one node.");
108 /** Retrieves the parent path of the provided path */
109 public static String
parentPath(String path
) {
110 if (path
.equals("/"))
111 throw new ArgeoException("Root path '/' has no parent path");
112 if (path
.charAt(0) != '/')
113 throw new ArgeoException("Path " + path
+ " must start with a '/'");
115 if (pathT
.charAt(pathT
.length() - 1) == '/')
116 pathT
= pathT
.substring(0, pathT
.length() - 2);
118 int index
= pathT
.lastIndexOf('/');
119 return pathT
.substring(0, index
);
122 /** The provided data as a path ('/' at the end, not the beginning) */
123 public static String
dateAsPath(Calendar cal
) {
124 return dateAsPath(cal
, false);
128 * Creates a deep path based on a URL:
129 * http://subdomain.example.com/to/content?args =>
130 * com/example/subdomain/to/content
132 public static String
urlAsPath(String url
) {
134 URL u
= new URL(url
);
135 StringBuffer path
= new StringBuffer(url
.length());
137 path
.append(hostAsPath(u
.getHost()));
138 // we don't put port since it may not always be there and may change
139 path
.append(u
.getPath());
140 return path
.toString();
141 } catch (MalformedURLException e
) {
142 throw new ArgeoException("Cannot generate URL path for " + url
, e
);
147 * Creates a path from a FQDN, inverting the order of the component:
148 * www.argeo.org => org.argeo.www
150 public static String
hostAsPath(String host
) {
151 StringBuffer path
= new StringBuffer(host
.length());
152 String
[] hostTokens
= host
.split("\\.");
153 for (int i
= hostTokens
.length
- 1; i
>= 0; i
--) {
154 path
.append(hostTokens
[i
]);
158 return path
.toString();
162 * The provided data as a path ('/' at the end, not the beginning)
167 * whether to add hour as well
169 public static String
dateAsPath(Calendar cal
, Boolean addHour
) {
170 StringBuffer buf
= new StringBuffer(14);
172 buf
.append(cal
.get(Calendar
.YEAR
));
175 int month
= cal
.get(Calendar
.MONTH
) + 1;
182 int day
= cal
.get(Calendar
.DAY_OF_MONTH
);
190 int hour
= cal
.get(Calendar
.HOUR_OF_DAY
);
197 return buf
.toString();
201 /** Converts in one call a string into a gregorian calendar. */
202 public static Calendar
parseCalendar(DateFormat dateFormat
, String value
) {
204 Date date
= dateFormat
.parse(value
);
205 Calendar calendar
= new GregorianCalendar();
206 calendar
.setTime(date
);
208 } catch (ParseException e
) {
209 throw new ArgeoException("Cannot parse " + value
210 + " with date format " + dateFormat
, e
);
215 /** The last element of a path. */
216 public static String
lastPathElement(String path
) {
217 if (path
.charAt(path
.length() - 1) == '/')
218 throw new ArgeoException("Path " + path
+ " cannot end with '/'");
219 int index
= path
.lastIndexOf('/');
221 throw new ArgeoException("Cannot find last path element for "
223 return path
.substring(index
+ 1);
227 * Routine that get the child with this name, adding id it does not already
230 public static Node
getOrAdd(Node parent
, String childName
,
231 String childPrimaryNodeType
) throws RepositoryException
{
232 return parent
.hasNode(childName
) ? parent
.getNode(childName
) : parent
233 .addNode(childName
, childPrimaryNodeType
);
237 * Routine that get the child with this name, adding id it does not already
240 public static Node
getOrAdd(Node parent
, String childName
)
241 throws RepositoryException
{
242 return parent
.hasNode(childName
) ? parent
.getNode(childName
) : parent
246 /** Creates the nodes making path, if they don't exist. */
247 public static Node
mkdirs(Session session
, String path
) {
248 return mkdirs(session
, path
, null, null, false);
252 * use {@link #mkdirs(Session, String, String, String, Boolean)} instead.
257 public static Node
mkdirs(Session session
, String path
, String type
,
258 Boolean versioning
) {
259 return mkdirs(session
, path
, type
, type
, false);
264 * the type of the leaf node
266 public static Node
mkdirs(Session session
, String path
, String type
) {
267 return mkdirs(session
, path
, type
, null, false);
271 * Creates the nodes making path, if they don't exist. This is up to the
272 * caller to save the session.
274 public static Node
mkdirs(Session session
, String path
, String type
,
275 String intermediaryNodeType
, Boolean versioning
) {
277 if (path
.equals('/'))
278 return session
.getRootNode();
280 if (session
.itemExists(path
)) {
281 Node node
= session
.getNode(path
);
284 && !type
.equals(node
.getPrimaryNodeType().getName()))
285 throw new ArgeoException("Node " + node
286 + " exists but is of type "
287 + node
.getPrimaryNodeType().getName()
288 + " not of type " + type
);
289 // TODO: check versioning
293 StringTokenizer st
= new StringTokenizer(path
, "/");
294 StringBuffer current
= new StringBuffer("/");
295 Node currentNode
= session
.getRootNode();
296 while (st
.hasMoreTokens()) {
297 String part
= st
.nextToken();
298 current
.append(part
).append('/');
299 if (!session
.itemExists(current
.toString())) {
300 if (!st
.hasMoreTokens() && type
!= null)
301 currentNode
= currentNode
.addNode(part
, type
);
302 else if (st
.hasMoreTokens() && intermediaryNodeType
!= null)
303 currentNode
= currentNode
.addNode(part
,
304 intermediaryNodeType
);
306 currentNode
= currentNode
.addNode(part
);
308 currentNode
.addMixin(NodeType
.MIX_VERSIONABLE
);
309 if (log
.isTraceEnabled())
310 log
.debug("Added folder " + part
+ " as " + current
);
312 currentNode
= (Node
) session
.getItem(current
.toString());
317 } catch (RepositoryException e
) {
318 throw new ArgeoException("Cannot mkdirs " + path
, e
);
323 * Safe and repository implementation independent registration of a
326 public static void registerNamespaceSafely(Session session
, String prefix
,
329 registerNamespaceSafely(session
.getWorkspace()
330 .getNamespaceRegistry(), prefix
, uri
);
331 } catch (RepositoryException e
) {
332 throw new ArgeoException("Cannot find namespace registry", e
);
337 * Safe and repository implementation independent registration of a
340 public static void registerNamespaceSafely(NamespaceRegistry nr
,
341 String prefix
, String uri
) {
343 String
[] prefixes
= nr
.getPrefixes();
344 for (String pref
: prefixes
)
345 if (pref
.equals(prefix
)) {
346 String registeredUri
= nr
.getURI(pref
);
347 if (!registeredUri
.equals(uri
))
348 throw new ArgeoException("Prefix " + pref
349 + " already registered for URI "
351 + " which is different from provided URI "
356 nr
.registerNamespace(prefix
, uri
);
357 } catch (RepositoryException e
) {
358 throw new ArgeoException("Cannot register namespace " + uri
359 + " under prefix " + prefix
, e
);
363 /** Recursively outputs the contents of the given node. */
364 public static void debug(Node node
) {
368 /** Recursively outputs the contents of the given node. */
369 public static void debug(Node node
, Log log
) {
371 // First output the node path
372 log
.debug(node
.getPath());
373 // Skip the virtual (and large!) jcr:system subtree
374 if (node
.getName().equals("jcr:system")) {
378 // Then the children nodes (recursive)
379 NodeIterator it
= node
.getNodes();
380 while (it
.hasNext()) {
381 Node childNode
= it
.nextNode();
385 // Then output the properties
386 PropertyIterator properties
= node
.getProperties();
387 // log.debug("Property are : ");
389 while (properties
.hasNext()) {
390 Property property
= properties
.nextProperty();
391 if (property
.getDefinition().isMultiple()) {
392 // A multi-valued property, print all values
393 Value
[] values
= property
.getValues();
394 for (int i
= 0; i
< values
.length
; i
++) {
395 log
.debug(property
.getPath() + "="
396 + values
[i
].getString());
399 // A single-valued property
400 log
.debug(property
.getPath() + "=" + property
.getString());
403 } catch (Exception e
) {
404 log
.error("Could not debug " + node
, e
);
410 * Copies recursively the content of a node to another one. Do NOT copy the
411 * property values of {@link NodeType#MIX_CREATED} and
412 * {@link NodeType#MIX_LAST_MODIFIED}, but update the
413 * {@link Property#JCR_LAST_MODIFIED} and
414 * {@link Property#JCR_LAST_MODIFIED_BY} properties if the target node has
415 * the {@link NodeType#MIX_LAST_MODIFIED} mixin.
417 public static void copy(Node fromNode
, Node toNode
) {
419 // process properties
420 PropertyIterator pit
= fromNode
.getProperties();
421 properties
: while (pit
.hasNext()) {
422 Property fromProperty
= pit
.nextProperty();
423 String propertyName
= fromProperty
.getName();
424 if (toNode
.hasProperty(propertyName
)
425 && toNode
.getProperty(propertyName
).getDefinition()
429 if (fromProperty
.getDefinition().isProtected())
432 if (propertyName
.equals("jcr:created")
433 || propertyName
.equals("jcr:createdBy")
434 || propertyName
.equals("jcr:lastModified")
435 || propertyName
.equals("jcr:lastModifiedBy"))
438 if (fromProperty
.isMultiple()) {
439 toNode
.setProperty(propertyName
, fromProperty
.getValues());
441 toNode
.setProperty(propertyName
, fromProperty
.getValue());
445 // update jcr:lastModified and jcr:lastModifiedBy in toNode in case
446 // they existed, before adding the mixins
447 updateLastModified(toNode
);
450 for (NodeType mixinType
: fromNode
.getMixinNodeTypes()) {
451 toNode
.addMixin(mixinType
.getName());
454 // process children nodes
455 NodeIterator nit
= fromNode
.getNodes();
456 while (nit
.hasNext()) {
457 Node fromChild
= nit
.nextNode();
458 Integer index
= fromChild
.getIndex();
459 String nodeRelPath
= fromChild
.getName() + "[" + index
+ "]";
461 if (toNode
.hasNode(nodeRelPath
))
462 toChild
= toNode
.getNode(nodeRelPath
);
464 toChild
= toNode
.addNode(fromChild
.getName(), fromChild
465 .getPrimaryNodeType().getName());
466 copy(fromChild
, toChild
);
468 } catch (RepositoryException e
) {
469 throw new ArgeoException("Cannot copy " + fromNode
+ " to "
475 * Check whether all first-level properties (except jcr:* properties) are
476 * equal. Skip jcr:* properties
478 public static Boolean
allPropertiesEquals(Node reference
, Node observed
,
479 Boolean onlyCommonProperties
) {
481 PropertyIterator pit
= reference
.getProperties();
482 props
: while (pit
.hasNext()) {
483 Property propReference
= pit
.nextProperty();
484 String propName
= propReference
.getName();
485 if (propName
.startsWith("jcr:"))
488 if (!observed
.hasProperty(propName
))
489 if (onlyCommonProperties
)
493 // TODO: deal with multiple property values?
494 if (!observed
.getProperty(propName
).getValue()
495 .equals(propReference
.getValue()))
499 } catch (RepositoryException e
) {
500 throw new ArgeoException("Cannot check all properties equals of "
501 + reference
+ " and " + observed
, e
);
505 public static Map
<String
, PropertyDiff
> diffProperties(Node reference
,
507 Map
<String
, PropertyDiff
> diffs
= new TreeMap
<String
, PropertyDiff
>();
508 diffPropertiesLevel(diffs
, null, reference
, observed
);
513 * Compare the properties of two nodes. Recursivity to child nodes is not
514 * yet supported. Skip jcr:* properties.
516 static void diffPropertiesLevel(Map
<String
, PropertyDiff
> diffs
,
517 String baseRelPath
, Node reference
, Node observed
) {
519 // check removed and modified
520 PropertyIterator pit
= reference
.getProperties();
521 props
: while (pit
.hasNext()) {
522 Property p
= pit
.nextProperty();
523 String name
= p
.getName();
524 if (name
.startsWith("jcr:"))
527 if (!observed
.hasProperty(name
)) {
528 String relPath
= propertyRelPath(baseRelPath
, name
);
529 PropertyDiff pDiff
= new PropertyDiff(PropertyDiff
.REMOVED
,
530 relPath
, p
.getValue(), null);
531 diffs
.put(relPath
, pDiff
);
535 Value referenceValue
= p
.getValue();
536 Value newValue
= observed
.getProperty(name
).getValue();
537 if (!referenceValue
.equals(newValue
)) {
538 String relPath
= propertyRelPath(baseRelPath
, name
);
539 PropertyDiff pDiff
= new PropertyDiff(
540 PropertyDiff
.MODIFIED
, relPath
, referenceValue
,
542 diffs
.put(relPath
, pDiff
);
547 pit
= observed
.getProperties();
548 props
: while (pit
.hasNext()) {
549 Property p
= pit
.nextProperty();
550 String name
= p
.getName();
551 if (name
.startsWith("jcr:"))
553 if (!reference
.hasProperty(name
)) {
554 String relPath
= propertyRelPath(baseRelPath
, name
);
555 PropertyDiff pDiff
= new PropertyDiff(PropertyDiff
.ADDED
,
556 relPath
, null, p
.getValue());
557 diffs
.put(relPath
, pDiff
);
560 } catch (RepositoryException e
) {
561 throw new ArgeoException("Cannot diff " + reference
+ " and "
567 * Compare only a restricted list of properties of two nodes. No
571 public static Map
<String
, PropertyDiff
> diffProperties(Node reference
,
572 Node observed
, List
<String
> properties
) {
573 Map
<String
, PropertyDiff
> diffs
= new TreeMap
<String
, PropertyDiff
>();
575 Iterator
<String
> pit
= properties
.iterator();
577 props
: while (pit
.hasNext()) {
578 String name
= pit
.next();
579 if (!reference
.hasProperty(name
)) {
580 if (!observed
.hasProperty(name
))
582 Value val
= observed
.getProperty(name
).getValue();
584 // empty String but not null
585 if ("".equals(val
.getString()))
587 } catch (Exception e
) {
588 // not parseable as String, silent
590 PropertyDiff pDiff
= new PropertyDiff(PropertyDiff
.ADDED
,
592 diffs
.put(name
, pDiff
);
593 } else if (!observed
.hasProperty(name
)) {
594 PropertyDiff pDiff
= new PropertyDiff(PropertyDiff
.REMOVED
,
595 name
, reference
.getProperty(name
).getValue(), null);
596 diffs
.put(name
, pDiff
);
598 Value referenceValue
= reference
.getProperty(name
)
600 Value newValue
= observed
.getProperty(name
).getValue();
601 if (!referenceValue
.equals(newValue
)) {
602 PropertyDiff pDiff
= new PropertyDiff(
603 PropertyDiff
.MODIFIED
, name
, referenceValue
,
605 diffs
.put(name
, pDiff
);
609 } catch (RepositoryException e
) {
610 throw new ArgeoException("Cannot diff " + reference
+ " and "
616 /** Builds a property relPath to be used in the diff. */
617 private static String
propertyRelPath(String baseRelPath
,
618 String propertyName
) {
619 if (baseRelPath
== null)
622 return baseRelPath
+ '/' + propertyName
;
626 * Normalizes a name so that it can be stored in contexts not supporting
627 * names with ':' (typically databases). Replaces ':' by '_'.
629 public static String
normalize(String name
) {
630 return name
.replace(':', '_');
634 * Replaces characters which are invalid in a JCR name by '_'. Currently not
637 * @see JcrUtils#INVALID_NAME_CHARACTERS
639 public static String
replaceInvalidChars(String name
) {
640 return replaceInvalidChars(name
, '_');
644 * Replaces characters which are invalid in a JCR name. Currently not
647 * @see JcrUtils#INVALID_NAME_CHARACTERS
649 public static String
replaceInvalidChars(String name
, char replacement
) {
650 boolean modified
= false;
651 char[] arr
= name
.toCharArray();
652 for (int i
= 0; i
< arr
.length
; i
++) {
654 invalid
: for (char invalid
: INVALID_NAME_CHARACTERS
) {
656 arr
[i
] = replacement
;
663 return new String(arr
);
665 // do not create new object if unnecessary
670 * Removes forbidden characters from a path, replacing them with '_'
672 * @deprecated use {@link #replaceInvalidChars(String)} instead
674 public static String
removeForbiddenCharacters(String str
) {
675 return str
.replace('[', '_').replace(']', '_').replace('/', '_')
680 /** Cleanly disposes a {@link Binary} even if it is null. */
681 public static void closeQuietly(Binary binary
) {
687 /** Retrieve a {@link Binary} as a byte array */
688 public static byte[] getBinaryAsBytes(Property property
) {
689 ByteArrayOutputStream out
= new ByteArrayOutputStream();
690 InputStream in
= null;
691 Binary binary
= null;
693 binary
= property
.getBinary();
694 in
= binary
.getStream();
695 IOUtils
.copy(in
, out
);
696 return out
.toByteArray();
697 } catch (Exception e
) {
698 throw new ArgeoException("Cannot read binary " + property
701 IOUtils
.closeQuietly(out
);
702 IOUtils
.closeQuietly(in
);
703 closeQuietly(binary
);
707 /** Writes a {@link Binary} from a byte array */
708 public static void setBinaryAsBytes(Node node
, String property
, byte[] bytes
) {
709 InputStream in
= null;
710 Binary binary
= null;
712 in
= new ByteArrayInputStream(bytes
);
713 binary
= node
.getSession().getValueFactory().createBinary(in
);
714 node
.setProperty(property
, binary
);
715 } catch (Exception e
) {
716 throw new ArgeoException("Cannot read binary " + property
719 IOUtils
.closeQuietly(in
);
720 closeQuietly(binary
);
725 * Creates depth from a string (typically a username) by adding levels based
726 * on its first characters: "aBcD",2 => a/aB
728 public static String
firstCharsToPath(String str
, Integer nbrOfChars
) {
729 if (str
.length() < nbrOfChars
)
730 throw new ArgeoException("String " + str
731 + " length must be greater or equal than " + nbrOfChars
);
732 StringBuffer path
= new StringBuffer("");
733 StringBuffer curr
= new StringBuffer("");
734 for (int i
= 0; i
< nbrOfChars
; i
++) {
735 curr
.append(str
.charAt(i
));
737 if (i
< nbrOfChars
- 1)
740 return path
.toString();
744 * Wraps the call to the repository factory based on parameter
745 * {@link ArgeoJcrConstants#JCR_REPOSITORY_ALIAS} in order to simplify it
746 * and protect against future API changes.
748 public static Repository
getRepositoryByAlias(
749 RepositoryFactory repositoryFactory
, String alias
) {
751 Map
<String
, String
> parameters
= new HashMap
<String
, String
>();
752 parameters
.put(JCR_REPOSITORY_ALIAS
, alias
);
753 return repositoryFactory
.getRepository(parameters
);
754 } catch (RepositoryException e
) {
755 throw new ArgeoException(
756 "Unexpected exception when trying to retrieve repository with alias "
762 * Wraps the call to the repository factory based on parameter
763 * {@link ArgeoJcrConstants#JCR_REPOSITORY_URI} in order to simplify it and
764 * protect against future API changes.
766 public static Repository
getRepositoryByUri(
767 RepositoryFactory repositoryFactory
, String uri
) {
769 Map
<String
, String
> parameters
= new HashMap
<String
, String
>();
770 parameters
.put(JCR_REPOSITORY_URI
, uri
);
771 return repositoryFactory
.getRepository(parameters
);
772 } catch (RepositoryException e
) {
773 throw new ArgeoException(
774 "Unexpected exception when trying to retrieve repository with uri "
780 * Discards the current changes in the session attached to this node. To be
781 * used typically in a catch block.
783 * @see #discardQuietly(Session)
785 public static void discardUnderlyingSessionQuietly(Node node
) {
787 discardQuietly(node
.getSession());
788 } catch (RepositoryException e
) {
789 log
.warn("Cannot quietly discard session of node " + node
+ ": "
795 * Discards the current changes in a session by calling
796 * {@link Session#refresh(boolean)} with <code>false</code>, only logging
797 * potential errors when doing so. To be used typically in a catch block.
799 public static void discardQuietly(Session session
) {
802 session
.refresh(false);
803 } catch (RepositoryException e
) {
804 log
.warn("Cannot quietly discard session " + session
+ ": "
809 /** Logs out the session, not throwing any exception, even if it is null. */
810 public static void logoutQuietly(Session session
) {
813 if (session
.isLive())
815 } catch (Exception e
) {
820 /** Returns the home node of the session user or null if none was found. */
821 public static Node
getUserHome(Session session
) {
822 String userID
= session
.getUserID();
823 return getUserHome(session
, userID
);
827 * Returns user home has path, embedding exceptions. Contrary to
828 * {@link #getUserHome(Session)}, it never returns null but throws and
829 * exception if not found.
831 public static String
getUserHomePath(Session session
) {
832 String userID
= session
.getUserID();
834 Node userHome
= getUserHome(session
, userID
);
835 if (userHome
!= null)
836 return userHome
.getPath();
838 throw new ArgeoException("No home registered for " + userID
);
839 } catch (RepositoryException e
) {
840 throw new ArgeoException("Cannot find user home path", e
);
844 /** Get the profile of the user attached to this session. */
845 public static Node
getUserProfile(Session session
) {
846 String userID
= session
.getUserID();
847 return getUserProfile(session
, userID
);
851 * Returns the home node of the session user or null if none was found.
854 * the session to use in order to perform the search, this can be
855 * a session with a different user ID than the one searched,
856 * typically when a system or admin session is used.
858 * the username of the user
860 public static Node
getUserHome(Session session
, String username
) {
862 QueryObjectModelFactory qomf
= session
.getWorkspace()
863 .getQueryManager().getQOMFactory();
865 // query the user home for this user id
866 Selector userHomeSel
= qomf
.selector(ArgeoTypes
.ARGEO_USER_HOME
,
868 DynamicOperand userIdDop
= qomf
.propertyValue("userHome",
869 ArgeoNames
.ARGEO_USER_ID
);
870 StaticOperand userIdSop
= qomf
.literal(session
.getValueFactory()
871 .createValue(username
));
872 Constraint constraint
= qomf
.comparison(userIdDop
,
873 QueryObjectModelFactory
.JCR_OPERATOR_EQUAL_TO
, userIdSop
);
874 Query query
= qomf
.createQuery(userHomeSel
, constraint
, null, null);
875 Node userHome
= JcrUtils
.querySingleNode(query
);
877 } catch (RepositoryException e
) {
878 throw new ArgeoException("Cannot find home for user " + username
, e
);
882 public static Node
getUserProfile(Session session
, String username
) {
884 QueryObjectModelFactory qomf
= session
.getWorkspace()
885 .getQueryManager().getQOMFactory();
886 Selector sel
= qomf
.selector(ArgeoTypes
.ARGEO_USER_PROFILE
,
888 DynamicOperand userIdDop
= qomf
.propertyValue("userProfile",
889 ArgeoNames
.ARGEO_USER_ID
);
890 StaticOperand userIdSop
= qomf
.literal(session
.getValueFactory()
891 .createValue(username
));
892 Constraint constraint
= qomf
.comparison(userIdDop
,
893 QueryObjectModelFactory
.JCR_OPERATOR_EQUAL_TO
, userIdSop
);
894 Query query
= qomf
.createQuery(sel
, constraint
, null, null);
895 Node userHome
= JcrUtils
.querySingleNode(query
);
897 } catch (RepositoryException e
) {
898 throw new ArgeoException(
899 "Cannot find profile for user " + username
, e
);
903 /** Creates an Argeo user home. */
904 public static Node
createUserHome(Session session
, String homeBasePath
,
908 throw new ArgeoException("Session is null");
909 if (session
.hasPendingChanges())
910 throw new ArgeoException(
911 "Session has pending changes, save them first");
913 String homePath
= homeBasePath
+ '/'
914 + firstCharsToPath(username
, 2) + '/' + username
;
916 if (session
.itemExists(homePath
)) {
918 throw new ArgeoException(
919 "Trying to create a user home that already exists");
920 } catch (Exception e
) {
921 // we use this workaround to be sure to get the stack trace
922 // to identify the sink of the bug.
923 log
.warn("trying to create an already existing userHome at path:"
924 + homePath
+ ". Stack trace : ");
929 Node userHome
= JcrUtils
.mkdirs(session
, homePath
);
931 if (userHome
.hasNode(ArgeoNames
.ARGEO_PROFILE
)) {
932 log
.warn("userProfile node already exists for userHome path: "
933 + homePath
+ ". We do not add a new one");
935 userProfile
= userHome
.addNode(ArgeoNames
.ARGEO_PROFILE
);
936 userProfile
.addMixin(ArgeoTypes
.ARGEO_USER_PROFILE
);
937 userProfile
.setProperty(ArgeoNames
.ARGEO_USER_ID
, username
);
939 // we need to save the profile before adding the user home type
941 userHome
.addMixin(ArgeoTypes
.ARGEO_USER_HOME
);
943 // http://jackrabbit.510166.n4.nabble.com/Jackrabbit-2-0-beta-6-Problem-adding-a-Mixin-type-with-mandatory-properties-after-setting-propertiesn-td1290332.html
944 userHome
.setProperty(ArgeoNames
.ARGEO_USER_ID
, username
);
947 } catch (RepositoryException e
) {
948 discardQuietly(session
);
949 throw new ArgeoException("Cannot create home node for user "
955 * Quietly unregisters an {@link EventListener} from the udnerlying
956 * workspace of this node.
958 public static void unregisterQuietly(Node node
, EventListener eventListener
) {
960 unregisterQuietly(node
.getSession().getWorkspace(), eventListener
);
961 } catch (RepositoryException e
) {
963 if (log
.isTraceEnabled())
964 log
.trace("Could not unregister event listener "
969 /** Quietly unregisters an {@link EventListener} from this workspace */
970 public static void unregisterQuietly(Workspace workspace
,
971 EventListener eventListener
) {
972 if (eventListener
== null)
975 workspace
.getObservationManager()
976 .removeEventListener(eventListener
);
977 } catch (RepositoryException e
) {
979 if (log
.isTraceEnabled())
980 log
.trace("Could not unregister event listener "
986 * If this node is has the {@link NodeType#MIX_LAST_MODIFIED} mixin, it
987 * updates the {@link Property#JCR_LAST_MODIFIED} property with the current
988 * time and the {@link Property#JCR_LAST_MODIFIED_BY} property with the
989 * underlying session user id. In Jackrabbit 2.x, <a
990 * href="https://issues.apache.org/jira/browse/JCR-2233">these properties
991 * are not automatically updated</a>, hence the need for manual update. The
992 * session is not saved.
994 public static void updateLastModified(Node node
) {
996 if (node
.isNodeType(NodeType
.MIX_LAST_MODIFIED
)) {
997 node
.setProperty(Property
.JCR_LAST_MODIFIED
,
998 new GregorianCalendar());
999 node
.setProperty(Property
.JCR_LAST_MODIFIED_BY
, node
1000 .getSession().getUserID());
1002 } catch (RepositoryException e
) {
1003 throw new ArgeoException("Cannot update last modified", e
);
1008 * Returns a String representing the short version (see <a
1009 * href="http://jackrabbit.apache.org/node-type-notation.html"> Node type
1010 * Notation </a> attributes grammar) of the main business attributes of this
1011 * property definition
1015 public static String
getPropertyDefinitionAsString(Property prop
) {
1016 StringBuffer sbuf
= new StringBuffer();
1018 if (prop
.getDefinition().isAutoCreated())
1020 if (prop
.getDefinition().isMandatory())
1022 if (prop
.getDefinition().isProtected())
1024 if (prop
.getDefinition().isMultiple())
1026 } catch (RepositoryException re
) {
1027 throw new ArgeoException(
1028 "unexpected error while getting property definition as String",
1031 return sbuf
.toString();
1035 * Estimate the sub tree size from current node. Computation is based on the
1036 * Jcr {@link Property.getLength()} method. Note : it is not the exact size
1037 * used on the disk by the current part of the JCR Tree.
1040 public static long getNodeApproxSize(Node node
) {
1041 long curNodeSize
= 0;
1043 PropertyIterator pi
= node
.getProperties();
1044 while (pi
.hasNext()) {
1045 Property prop
= pi
.nextProperty();
1046 if (prop
.isMultiple()) {
1047 int nb
= prop
.getLengths().length
;
1048 for (int i
= 0; i
< nb
; i
++) {
1049 curNodeSize
+= (prop
.getLengths()[i
] > 0 ? prop
1050 .getLengths()[i
] : 0);
1053 curNodeSize
+= (prop
.getLength() > 0 ? prop
.getLength() : 0);
1056 NodeIterator ni
= node
.getNodes();
1057 while (ni
.hasNext())
1058 curNodeSize
+= getNodeApproxSize(ni
.nextNode());
1059 log
.debug(node
+ ": " + curNodeSize
);
1061 } catch (RepositoryException re
) {
1062 throw new ArgeoException(
1063 "Unexpected error while recursively determining node size.",