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
.ArrayList
;
27 import java
.util
.Calendar
;
28 import java
.util
.Collections
;
29 import java
.util
.Date
;
30 import java
.util
.GregorianCalendar
;
31 import java
.util
.HashMap
;
32 import java
.util
.Iterator
;
33 import java
.util
.List
;
35 import java
.util
.TreeMap
;
37 import javax
.jcr
.Binary
;
38 import javax
.jcr
.NamespaceRegistry
;
39 import javax
.jcr
.Node
;
40 import javax
.jcr
.NodeIterator
;
41 import javax
.jcr
.Property
;
42 import javax
.jcr
.PropertyIterator
;
43 import javax
.jcr
.PropertyType
;
44 import javax
.jcr
.Repository
;
45 import javax
.jcr
.RepositoryException
;
46 import javax
.jcr
.RepositoryFactory
;
47 import javax
.jcr
.Session
;
48 import javax
.jcr
.Value
;
49 import javax
.jcr
.Workspace
;
50 import javax
.jcr
.nodetype
.NodeType
;
51 import javax
.jcr
.observation
.EventListener
;
52 import javax
.jcr
.query
.Query
;
53 import javax
.jcr
.query
.QueryResult
;
54 import javax
.jcr
.version
.VersionManager
;
56 import org
.apache
.commons
.io
.IOUtils
;
57 import org
.apache
.commons
.logging
.Log
;
58 import org
.apache
.commons
.logging
.LogFactory
;
59 import org
.argeo
.ArgeoException
;
61 /** Utility methods to simplify common JCR operations. */
62 public class JcrUtils
implements ArgeoJcrConstants
{
64 private final static Log log
= LogFactory
.getLog(JcrUtils
.class);
67 * Not complete yet. See
68 * http://www.day.com/specs/jcr/2.0/3_Repository_Model.html#3.2.2%20Local
71 public final static char[] INVALID_NAME_CHARACTERS
= { '/', ':', '[', ']',
77 /** Prevents instantiation */
82 * Queries one single node.
84 * @return one single node or null if none was found
85 * @throws ArgeoException
86 * if more than one node was found
88 public static Node
querySingleNode(Query query
) {
89 NodeIterator nodeIterator
;
91 QueryResult queryResult
= query
.execute();
92 nodeIterator
= queryResult
.getNodes();
93 } catch (RepositoryException e
) {
94 throw new ArgeoException("Cannot execute query " + query
, e
);
97 if (nodeIterator
.hasNext())
98 node
= nodeIterator
.nextNode();
102 if (nodeIterator
.hasNext())
103 throw new ArgeoException("Query returned more than one node.");
107 /** Retrieves the parent path of the provided path */
108 public static String
parentPath(String path
) {
109 if (path
.equals("/"))
110 throw new ArgeoException("Root path '/' has no parent path");
111 if (path
.charAt(0) != '/')
112 throw new ArgeoException("Path " + path
+ " must start with a '/'");
114 if (pathT
.charAt(pathT
.length() - 1) == '/')
115 pathT
= pathT
.substring(0, pathT
.length() - 2);
117 int index
= pathT
.lastIndexOf('/');
118 return pathT
.substring(0, index
);
121 /** The provided data as a path ('/' at the end, not the beginning) */
122 public static String
dateAsPath(Calendar cal
) {
123 return dateAsPath(cal
, false);
127 * Creates a deep path based on a URL:
128 * http://subdomain.example.com/to/content?args =>
129 * com/example/subdomain/to/content
131 public static String
urlAsPath(String url
) {
133 URL u
= new URL(url
);
134 StringBuffer path
= new StringBuffer(url
.length());
136 path
.append(hostAsPath(u
.getHost()));
137 // we don't put port since it may not always be there and may change
138 path
.append(u
.getPath());
139 return path
.toString();
140 } catch (MalformedURLException e
) {
141 throw new ArgeoException("Cannot generate URL path for " + url
, e
);
145 /** Set the {@link NodeType#NT_ADDRESS} properties based on this URL. */
146 public static void urlToAddressProperties(Node node
, String url
) {
148 URL u
= new URL(url
);
149 node
.setProperty(Property
.JCR_PROTOCOL
, u
.getProtocol());
150 node
.setProperty(Property
.JCR_HOST
, u
.getHost());
151 node
.setProperty(Property
.JCR_PORT
, Integer
.toString(u
.getPort()));
152 node
.setProperty(Property
.JCR_PATH
, normalizePath(u
.getPath()));
153 } catch (Exception e
) {
154 throw new ArgeoException("Cannot set URL " + url
155 + " as nt:address properties", e
);
159 /** Build URL based on the {@link NodeType#NT_ADDRESS} properties. */
160 public static String
urlFromAddressProperties(Node node
) {
163 node
.getProperty(Property
.JCR_PROTOCOL
).getString(), node
164 .getProperty(Property
.JCR_HOST
).getString(),
165 (int) node
.getProperty(Property
.JCR_PORT
).getLong(), node
166 .getProperty(Property
.JCR_PATH
).getString());
168 } catch (Exception e
) {
169 throw new ArgeoException(
170 "Cannot get URL from nt:address properties of " + node
, e
);
174 /** Make sure that: starts with '/', do not end with '/', do not have '//' */
175 public static String
normalizePath(String path
) {
176 List
<String
> tokens
= tokenize(path
);
177 StringBuffer buf
= new StringBuffer(path
.length());
178 for (String token
: tokens
) {
182 return buf
.toString();
186 * Creates a path from a FQDN, inverting the order of the component:
187 * www.argeo.org => org.argeo.www
189 public static String
hostAsPath(String host
) {
190 StringBuffer path
= new StringBuffer(host
.length());
191 String
[] hostTokens
= host
.split("\\.");
192 for (int i
= hostTokens
.length
- 1; i
>= 0; i
--) {
193 path
.append(hostTokens
[i
]);
197 return path
.toString();
201 * The provided data as a path ('/' at the end, not the beginning)
206 * whether to add hour as well
208 public static String
dateAsPath(Calendar cal
, Boolean addHour
) {
209 StringBuffer buf
= new StringBuffer(14);
211 buf
.append(cal
.get(Calendar
.YEAR
));
214 int month
= cal
.get(Calendar
.MONTH
) + 1;
221 int day
= cal
.get(Calendar
.DAY_OF_MONTH
);
229 int hour
= cal
.get(Calendar
.HOUR_OF_DAY
);
236 return buf
.toString();
240 /** Converts in one call a string into a gregorian calendar. */
241 public static Calendar
parseCalendar(DateFormat dateFormat
, String value
) {
243 Date date
= dateFormat
.parse(value
);
244 Calendar calendar
= new GregorianCalendar();
245 calendar
.setTime(date
);
247 } catch (ParseException e
) {
248 throw new ArgeoException("Cannot parse " + value
249 + " with date format " + dateFormat
, e
);
254 /** The last element of a path. */
255 public static String
lastPathElement(String path
) {
256 if (path
.charAt(path
.length() - 1) == '/')
257 throw new ArgeoException("Path " + path
+ " cannot end with '/'");
258 int index
= path
.lastIndexOf('/');
260 throw new ArgeoException("Cannot find last path element for "
262 return path
.substring(index
+ 1);
266 * Routine that get the child with this name, adding id it does not already
269 public static Node
getOrAdd(Node parent
, String childName
,
270 String childPrimaryNodeType
) throws RepositoryException
{
271 return parent
.hasNode(childName
) ? parent
.getNode(childName
) : parent
272 .addNode(childName
, childPrimaryNodeType
);
276 * Routine that get the child with this name, adding id it does not already
279 public static Node
getOrAdd(Node parent
, String childName
)
280 throws RepositoryException
{
281 return parent
.hasNode(childName
) ? parent
.getNode(childName
) : parent
285 /** Creates the nodes making path, if they don't exist. */
286 public static Node
mkdirs(Session session
, String path
) {
287 return mkdirs(session
, path
, null, null, false);
291 * use {@link #mkdirs(Session, String, String, String, Boolean)} instead.
296 public static Node
mkdirs(Session session
, String path
, String type
,
297 Boolean versioning
) {
298 return mkdirs(session
, path
, type
, type
, false);
303 * the type of the leaf node
305 public static Node
mkdirs(Session session
, String path
, String type
) {
306 return mkdirs(session
, path
, type
, null, false);
310 * Synchronized and save is performed, to avoid race conditions in
311 * initializers leading to duplicate nodes.
313 public synchronized static Node
mkdirsSafe(Session session
, String path
,
316 if (session
.hasPendingChanges())
317 throw new ArgeoException(
318 "Session has pending changes, save them first.");
319 Node node
= mkdirs(session
, path
, type
);
322 } catch (RepositoryException e
) {
323 discardQuietly(session
);
324 throw new ArgeoException("Cannot safely make directories", e
);
328 public synchronized static Node
mkdirsSafe(Session session
, String path
) {
329 return mkdirsSafe(session
, path
, null);
333 * Creates the nodes making path, if they don't exist. This is up to the
334 * caller to save the session. Use with caution since it can create
335 * duplicate nodes if used concurrently.
337 public static Node
mkdirs(Session session
, String path
, String type
,
338 String intermediaryNodeType
, Boolean versioning
) {
340 if (path
.equals('/'))
341 return session
.getRootNode();
343 if (session
.itemExists(path
)) {
344 Node node
= session
.getNode(path
);
347 && !type
.equals(node
.getPrimaryNodeType().getName()))
348 throw new ArgeoException("Node " + node
349 + " exists but is of type "
350 + node
.getPrimaryNodeType().getName()
351 + " not of type " + type
);
352 // TODO: check versioning
356 StringBuffer current
= new StringBuffer("/");
357 Node currentNode
= session
.getRootNode();
358 Iterator
<String
> it
= tokenize(path
).iterator();
359 while (it
.hasNext()) {
360 String part
= it
.next();
361 current
.append(part
).append('/');
362 if (!session
.itemExists(current
.toString())) {
363 if (!it
.hasNext() && type
!= null)
364 currentNode
= currentNode
.addNode(part
, type
);
365 else if (it
.hasNext() && intermediaryNodeType
!= null)
366 currentNode
= currentNode
.addNode(part
,
367 intermediaryNodeType
);
369 currentNode
= currentNode
.addNode(part
);
371 currentNode
.addMixin(NodeType
.MIX_VERSIONABLE
);
372 if (log
.isTraceEnabled())
373 log
.debug("Added folder " + part
+ " as " + current
);
375 currentNode
= (Node
) session
.getItem(current
.toString());
379 } catch (RepositoryException e
) {
380 discardQuietly(session
);
381 throw new ArgeoException("Cannot mkdirs " + path
, e
);
386 /** Convert a path to the list of its tokens */
387 public static List
<String
> tokenize(String path
) {
388 List
<String
> tokens
= new ArrayList
<String
>();
389 boolean optimized
= false;
391 String
[] rawTokens
= path
.split("/");
392 for (String token
: rawTokens
) {
393 if (!token
.equals(""))
397 StringBuffer curr
= new StringBuffer();
398 char[] arr
= path
.toCharArray();
399 chars
: for (int i
= 0; i
< arr
.length
; i
++) {
402 if (i
== 0 || (i
== arr
.length
- 1))
404 if (curr
.length() > 0) {
405 tokens
.add(curr
.toString());
406 curr
= new StringBuffer();
411 if (curr
.length() > 0) {
412 tokens
.add(curr
.toString());
413 curr
= new StringBuffer();
416 return Collections
.unmodifiableList(tokens
);
420 * Safe and repository implementation independent registration of a
423 public static void registerNamespaceSafely(Session session
, String prefix
,
426 registerNamespaceSafely(session
.getWorkspace()
427 .getNamespaceRegistry(), prefix
, uri
);
428 } catch (RepositoryException e
) {
429 throw new ArgeoException("Cannot find namespace registry", e
);
434 * Safe and repository implementation independent registration of a
437 public static void registerNamespaceSafely(NamespaceRegistry nr
,
438 String prefix
, String uri
) {
440 String
[] prefixes
= nr
.getPrefixes();
441 for (String pref
: prefixes
)
442 if (pref
.equals(prefix
)) {
443 String registeredUri
= nr
.getURI(pref
);
444 if (!registeredUri
.equals(uri
))
445 throw new ArgeoException("Prefix " + pref
446 + " already registered for URI "
448 + " which is different from provided URI "
453 nr
.registerNamespace(prefix
, uri
);
454 } catch (RepositoryException e
) {
455 throw new ArgeoException("Cannot register namespace " + uri
456 + " under prefix " + prefix
, e
);
460 /** Recursively outputs the contents of the given node. */
461 public static void debug(Node node
) {
465 /** Recursively outputs the contents of the given node. */
466 public static void debug(Node node
, Log log
) {
468 // First output the node path
469 log
.debug(node
.getPath());
470 // Skip the virtual (and large!) jcr:system subtree
471 if (node
.getName().equals("jcr:system")) {
475 // Then the children nodes (recursive)
476 NodeIterator it
= node
.getNodes();
477 while (it
.hasNext()) {
478 Node childNode
= it
.nextNode();
479 debug(childNode
, log
);
482 // Then output the properties
483 PropertyIterator properties
= node
.getProperties();
484 // log.debug("Property are : ");
486 properties
: while (properties
.hasNext()) {
487 Property property
= properties
.nextProperty();
488 if (property
.getType() == PropertyType
.BINARY
)
489 continue properties
;// skip
490 if (property
.getDefinition().isMultiple()) {
491 // A multi-valued property, print all values
492 Value
[] values
= property
.getValues();
493 for (int i
= 0; i
< values
.length
; i
++) {
494 log
.debug(property
.getPath() + "="
495 + values
[i
].getString());
498 // A single-valued property
499 log
.debug(property
.getPath() + "=" + property
.getString());
502 } catch (Exception e
) {
503 log
.error("Could not debug " + node
, e
);
509 * Copies recursively the content of a node to another one. Do NOT copy the
510 * property values of {@link NodeType#MIX_CREATED} and
511 * {@link NodeType#MIX_LAST_MODIFIED}, but update the
512 * {@link Property#JCR_LAST_MODIFIED} and
513 * {@link Property#JCR_LAST_MODIFIED_BY} properties if the target node has
514 * the {@link NodeType#MIX_LAST_MODIFIED} mixin.
516 public static void copy(Node fromNode
, Node toNode
) {
518 // process properties
519 PropertyIterator pit
= fromNode
.getProperties();
520 properties
: while (pit
.hasNext()) {
521 Property fromProperty
= pit
.nextProperty();
522 String propertyName
= fromProperty
.getName();
523 if (toNode
.hasProperty(propertyName
)
524 && toNode
.getProperty(propertyName
).getDefinition()
528 if (fromProperty
.getDefinition().isProtected())
531 if (propertyName
.equals("jcr:created")
532 || propertyName
.equals("jcr:createdBy")
533 || propertyName
.equals("jcr:lastModified")
534 || propertyName
.equals("jcr:lastModifiedBy"))
537 if (fromProperty
.isMultiple()) {
538 toNode
.setProperty(propertyName
, fromProperty
.getValues());
540 toNode
.setProperty(propertyName
, fromProperty
.getValue());
544 // update jcr:lastModified and jcr:lastModifiedBy in toNode in case
545 // they existed, before adding the mixins
546 updateLastModified(toNode
);
549 for (NodeType mixinType
: fromNode
.getMixinNodeTypes()) {
550 toNode
.addMixin(mixinType
.getName());
553 // process children nodes
554 NodeIterator nit
= fromNode
.getNodes();
555 while (nit
.hasNext()) {
556 Node fromChild
= nit
.nextNode();
557 Integer index
= fromChild
.getIndex();
558 String nodeRelPath
= fromChild
.getName() + "[" + index
+ "]";
560 if (toNode
.hasNode(nodeRelPath
))
561 toChild
= toNode
.getNode(nodeRelPath
);
563 toChild
= toNode
.addNode(fromChild
.getName(), fromChild
564 .getPrimaryNodeType().getName());
565 copy(fromChild
, toChild
);
567 } catch (RepositoryException e
) {
568 throw new ArgeoException("Cannot copy " + fromNode
+ " to "
574 * Check whether all first-level properties (except jcr:* properties) are
575 * equal. Skip jcr:* properties
577 public static Boolean
allPropertiesEquals(Node reference
, Node observed
,
578 Boolean onlyCommonProperties
) {
580 PropertyIterator pit
= reference
.getProperties();
581 props
: while (pit
.hasNext()) {
582 Property propReference
= pit
.nextProperty();
583 String propName
= propReference
.getName();
584 if (propName
.startsWith("jcr:"))
587 if (!observed
.hasProperty(propName
))
588 if (onlyCommonProperties
)
592 // TODO: deal with multiple property values?
593 if (!observed
.getProperty(propName
).getValue()
594 .equals(propReference
.getValue()))
598 } catch (RepositoryException e
) {
599 throw new ArgeoException("Cannot check all properties equals of "
600 + reference
+ " and " + observed
, e
);
604 public static Map
<String
, PropertyDiff
> diffProperties(Node reference
,
606 Map
<String
, PropertyDiff
> diffs
= new TreeMap
<String
, PropertyDiff
>();
607 diffPropertiesLevel(diffs
, null, reference
, observed
);
612 * Compare the properties of two nodes. Recursivity to child nodes is not
613 * yet supported. Skip jcr:* properties.
615 static void diffPropertiesLevel(Map
<String
, PropertyDiff
> diffs
,
616 String baseRelPath
, Node reference
, Node observed
) {
618 // check removed and modified
619 PropertyIterator pit
= reference
.getProperties();
620 props
: while (pit
.hasNext()) {
621 Property p
= pit
.nextProperty();
622 String name
= p
.getName();
623 if (name
.startsWith("jcr:"))
626 if (!observed
.hasProperty(name
)) {
627 String relPath
= propertyRelPath(baseRelPath
, name
);
628 PropertyDiff pDiff
= new PropertyDiff(PropertyDiff
.REMOVED
,
629 relPath
, p
.getValue(), null);
630 diffs
.put(relPath
, pDiff
);
632 if (p
.isMultiple()) {
633 // FIXME implement multiple
635 Value referenceValue
= p
.getValue();
636 Value newValue
= observed
.getProperty(name
).getValue();
637 if (!referenceValue
.equals(newValue
)) {
638 String relPath
= propertyRelPath(baseRelPath
, name
);
639 PropertyDiff pDiff
= new PropertyDiff(
640 PropertyDiff
.MODIFIED
, relPath
,
641 referenceValue
, newValue
);
642 diffs
.put(relPath
, pDiff
);
648 pit
= observed
.getProperties();
649 props
: while (pit
.hasNext()) {
650 Property p
= pit
.nextProperty();
651 String name
= p
.getName();
652 if (name
.startsWith("jcr:"))
654 if (!reference
.hasProperty(name
)) {
655 if (p
.isMultiple()) {
656 // FIXME implement multiple
658 String relPath
= propertyRelPath(baseRelPath
, name
);
659 PropertyDiff pDiff
= new PropertyDiff(
660 PropertyDiff
.ADDED
, relPath
, null, p
.getValue());
661 diffs
.put(relPath
, pDiff
);
665 } catch (RepositoryException e
) {
666 throw new ArgeoException("Cannot diff " + reference
+ " and "
672 * Compare only a restricted list of properties of two nodes. No
676 public static Map
<String
, PropertyDiff
> diffProperties(Node reference
,
677 Node observed
, List
<String
> properties
) {
678 Map
<String
, PropertyDiff
> diffs
= new TreeMap
<String
, PropertyDiff
>();
680 Iterator
<String
> pit
= properties
.iterator();
682 props
: while (pit
.hasNext()) {
683 String name
= pit
.next();
684 if (!reference
.hasProperty(name
)) {
685 if (!observed
.hasProperty(name
))
687 Value val
= observed
.getProperty(name
).getValue();
689 // empty String but not null
690 if ("".equals(val
.getString()))
692 } catch (Exception e
) {
693 // not parseable as String, silent
695 PropertyDiff pDiff
= new PropertyDiff(PropertyDiff
.ADDED
,
697 diffs
.put(name
, pDiff
);
698 } else if (!observed
.hasProperty(name
)) {
699 PropertyDiff pDiff
= new PropertyDiff(PropertyDiff
.REMOVED
,
700 name
, reference
.getProperty(name
).getValue(), null);
701 diffs
.put(name
, pDiff
);
703 Value referenceValue
= reference
.getProperty(name
)
705 Value newValue
= observed
.getProperty(name
).getValue();
706 if (!referenceValue
.equals(newValue
)) {
707 PropertyDiff pDiff
= new PropertyDiff(
708 PropertyDiff
.MODIFIED
, name
, referenceValue
,
710 diffs
.put(name
, pDiff
);
714 } catch (RepositoryException e
) {
715 throw new ArgeoException("Cannot diff " + reference
+ " and "
721 /** Builds a property relPath to be used in the diff. */
722 private static String
propertyRelPath(String baseRelPath
,
723 String propertyName
) {
724 if (baseRelPath
== null)
727 return baseRelPath
+ '/' + propertyName
;
731 * Normalizes a name so that it can be stored in contexts not supporting
732 * names with ':' (typically databases). Replaces ':' by '_'.
734 public static String
normalize(String name
) {
735 return name
.replace(':', '_');
739 * Replaces characters which are invalid in a JCR name by '_'. Currently not
742 * @see JcrUtils#INVALID_NAME_CHARACTERS
744 public static String
replaceInvalidChars(String name
) {
745 return replaceInvalidChars(name
, '_');
749 * Replaces characters which are invalid in a JCR name. Currently not
752 * @see JcrUtils#INVALID_NAME_CHARACTERS
754 public static String
replaceInvalidChars(String name
, char replacement
) {
755 boolean modified
= false;
756 char[] arr
= name
.toCharArray();
757 for (int i
= 0; i
< arr
.length
; i
++) {
759 invalid
: for (char invalid
: INVALID_NAME_CHARACTERS
) {
761 arr
[i
] = replacement
;
768 return new String(arr
);
770 // do not create new object if unnecessary
775 * Removes forbidden characters from a path, replacing them with '_'
777 * @deprecated use {@link #replaceInvalidChars(String)} instead
779 public static String
removeForbiddenCharacters(String str
) {
780 return str
.replace('[', '_').replace(']', '_').replace('/', '_')
785 /** Cleanly disposes a {@link Binary} even if it is null. */
786 public static void closeQuietly(Binary binary
) {
792 /** Retrieve a {@link Binary} as a byte array */
793 public static byte[] getBinaryAsBytes(Property property
) {
794 ByteArrayOutputStream out
= new ByteArrayOutputStream();
795 InputStream in
= null;
796 Binary binary
= null;
798 binary
= property
.getBinary();
799 in
= binary
.getStream();
800 IOUtils
.copy(in
, out
);
801 return out
.toByteArray();
802 } catch (Exception e
) {
803 throw new ArgeoException("Cannot read binary " + property
806 IOUtils
.closeQuietly(out
);
807 IOUtils
.closeQuietly(in
);
808 closeQuietly(binary
);
812 /** Writes a {@link Binary} from a byte array */
813 public static void setBinaryAsBytes(Node node
, String property
, byte[] bytes
) {
814 InputStream in
= null;
815 Binary binary
= null;
817 in
= new ByteArrayInputStream(bytes
);
818 binary
= node
.getSession().getValueFactory().createBinary(in
);
819 node
.setProperty(property
, binary
);
820 } catch (Exception e
) {
821 throw new ArgeoException("Cannot read binary " + property
824 IOUtils
.closeQuietly(in
);
825 closeQuietly(binary
);
830 * Creates depth from a string (typically a username) by adding levels based
831 * on its first characters: "aBcD",2 => a/aB
833 public static String
firstCharsToPath(String str
, Integer nbrOfChars
) {
834 if (str
.length() < nbrOfChars
)
835 throw new ArgeoException("String " + str
836 + " length must be greater or equal than " + nbrOfChars
);
837 StringBuffer path
= new StringBuffer("");
838 StringBuffer curr
= new StringBuffer("");
839 for (int i
= 0; i
< nbrOfChars
; i
++) {
840 curr
.append(str
.charAt(i
));
842 if (i
< nbrOfChars
- 1)
845 return path
.toString();
849 * Wraps the call to the repository factory based on parameter
850 * {@link ArgeoJcrConstants#JCR_REPOSITORY_ALIAS} in order to simplify it
851 * and protect against future API changes.
853 public static Repository
getRepositoryByAlias(
854 RepositoryFactory repositoryFactory
, String alias
) {
856 Map
<String
, String
> parameters
= new HashMap
<String
, String
>();
857 parameters
.put(JCR_REPOSITORY_ALIAS
, alias
);
858 return repositoryFactory
.getRepository(parameters
);
859 } catch (RepositoryException e
) {
860 throw new ArgeoException(
861 "Unexpected exception when trying to retrieve repository with alias "
867 * Wraps the call to the repository factory based on parameter
868 * {@link ArgeoJcrConstants#JCR_REPOSITORY_URI} in order to simplify it and
869 * protect against future API changes.
871 public static Repository
getRepositoryByUri(
872 RepositoryFactory repositoryFactory
, String uri
) {
874 Map
<String
, String
> parameters
= new HashMap
<String
, String
>();
875 parameters
.put(JCR_REPOSITORY_URI
, uri
);
876 return repositoryFactory
.getRepository(parameters
);
877 } catch (RepositoryException e
) {
878 throw new ArgeoException(
879 "Unexpected exception when trying to retrieve repository with uri "
885 * Discards the current changes in the session attached to this node. To be
886 * used typically in a catch block.
888 * @see #discardQuietly(Session)
890 public static void discardUnderlyingSessionQuietly(Node node
) {
892 discardQuietly(node
.getSession());
893 } catch (RepositoryException e
) {
894 log
.warn("Cannot quietly discard session of node " + node
+ ": "
900 * Discards the current changes in a session by calling
901 * {@link Session#refresh(boolean)} with <code>false</code>, only logging
902 * potential errors when doing so. To be used typically in a catch block.
904 public static void discardQuietly(Session session
) {
907 session
.refresh(false);
908 } catch (RepositoryException e
) {
909 log
.warn("Cannot quietly discard session " + session
+ ": "
914 /** Logs out the session, not throwing any exception, even if it is null. */
915 public static void logoutQuietly(Session session
) {
918 if (session
.isLive())
920 } catch (Exception e
) {
926 * Convenient method to add a listener. uuids passed as null, deep=true,
927 * local=true, only one node type
929 public static void addListener(Session session
, EventListener listener
,
930 int eventTypes
, String basePath
, String nodeType
) {
932 session
.getWorkspace()
933 .getObservationManager()
934 .addEventListener(listener
, eventTypes
, basePath
, true,
935 null, new String
[] { nodeType
}, true);
936 } catch (RepositoryException e
) {
937 throw new ArgeoException("Cannot add JCR listener " + listener
938 + " to session " + session
, e
);
942 /** Removes a listener without throwing exception */
943 public static void removeListenerQuietly(Session session
,
944 EventListener listener
) {
945 if (session
== null || !session
.isLive())
948 session
.getWorkspace().getObservationManager()
949 .removeEventListener(listener
);
950 } catch (RepositoryException e
) {
955 /** Returns the home node of the session user or null if none was found. */
956 public static Node
getUserHome(Session session
) {
957 String userID
= session
.getUserID();
958 return getUserHome(session
, userID
);
961 /** User home path is NOT configurable */
962 public static String
getUserHomePath(String username
) {
963 String homeBasePath
= DEFAULT_HOME_BASE_PATH
;
964 return homeBasePath
+ '/' + firstCharsToPath(username
, 2) + '/'
969 * Returns the home node of the session user or null if none was found.
972 * the session to use in order to perform the search, this can be
973 * a session with a different user ID than the one searched,
974 * typically when a system or admin session is used.
976 * the username of the user
978 public static Node
getUserHome(Session session
, String username
) {
980 String homePath
= getUserHomePath(username
);
981 return session
.itemExists(homePath
) ? session
.getNode(homePath
)
983 // kept for example of QOM queries
984 // QueryObjectModelFactory qomf = session.getWorkspace()
985 // .getQueryManager().getQOMFactory();
986 // Selector userHomeSel = qomf.selector(ArgeoTypes.ARGEO_USER_HOME,
988 // DynamicOperand userIdDop = qomf.propertyValue("userHome",
989 // ArgeoNames.ARGEO_USER_ID);
990 // StaticOperand userIdSop = qomf.literal(session.getValueFactory()
991 // .createValue(username));
992 // Constraint constraint = qomf.comparison(userIdDop,
993 // QueryObjectModelFactory.JCR_OPERATOR_EQUAL_TO, userIdSop);
994 // Query query = qomf.createQuery(userHomeSel, constraint, null,
996 // Node userHome = JcrUtils.querySingleNode(query);
997 } catch (RepositoryException e
) {
998 throw new ArgeoException("Cannot find home for user " + username
, e
);
1003 * Creates an Argeo user home, does nothing if it already exists. Session is
1006 public static Node
createUserHomeIfNeeded(Session session
, String username
) {
1008 String homePath
= getUserHomePath(username
);
1009 if (session
.itemExists(homePath
))
1010 return session
.getNode(homePath
);
1012 Node userHome
= JcrUtils
.mkdirs(session
, homePath
);
1013 userHome
.addMixin(ArgeoTypes
.ARGEO_USER_HOME
);
1014 userHome
.setProperty(ArgeoNames
.ARGEO_USER_ID
, username
);
1017 } catch (RepositoryException e
) {
1018 discardQuietly(session
);
1019 throw new ArgeoException("Cannot create home for " + username
1020 + " in workspace " + session
.getWorkspace().getName(), e
);
1025 * Creates a user profile in the home of this user. Creates the home if
1026 * needed, but throw an exception if a profile already exists. The session
1027 * is not saved and the node is in a checkedOut state (that is, it requires
1028 * a subsequent checkin after saving the session).
1030 public static Node
createUserProfile(Session session
, String username
) {
1032 Node userHome
= createUserHomeIfNeeded(session
, username
);
1033 if (userHome
.hasNode(ArgeoNames
.ARGEO_PROFILE
))
1034 throw new ArgeoException(
1035 "There is already a user profile under " + userHome
);
1036 Node userProfile
= userHome
.addNode(ArgeoNames
.ARGEO_PROFILE
);
1037 userProfile
.addMixin(ArgeoTypes
.ARGEO_USER_PROFILE
);
1038 userProfile
.setProperty(ArgeoNames
.ARGEO_USER_ID
, username
);
1039 userProfile
.setProperty(ArgeoNames
.ARGEO_ENABLED
, true);
1040 userProfile
.setProperty(ArgeoNames
.ARGEO_ACCOUNT_NON_EXPIRED
, true);
1041 userProfile
.setProperty(ArgeoNames
.ARGEO_ACCOUNT_NON_LOCKED
, true);
1042 userProfile
.setProperty(ArgeoNames
.ARGEO_CREDENTIALS_NON_EXPIRED
,
1045 } catch (RepositoryException e
) {
1046 discardQuietly(session
);
1047 throw new ArgeoException("Cannot create user profile for "
1048 + username
+ " in workspace "
1049 + session
.getWorkspace().getName(), e
);
1054 * Create user profile if needed, the session IS saved.
1056 * @return the user profile
1058 public static Node
createUserProfileIfNeeded(Session securitySession
,
1061 Node userHome
= JcrUtils
.createUserHomeIfNeeded(securitySession
,
1063 Node userProfile
= userHome
.hasNode(ArgeoNames
.ARGEO_PROFILE
) ? userHome
1064 .getNode(ArgeoNames
.ARGEO_PROFILE
) : JcrUtils
1065 .createUserProfile(securitySession
, username
);
1066 if (securitySession
.hasPendingChanges())
1067 securitySession
.save();
1068 VersionManager versionManager
= securitySession
.getWorkspace()
1069 .getVersionManager();
1070 if (versionManager
.isCheckedOut(userProfile
.getPath()))
1071 versionManager
.checkin(userProfile
.getPath());
1073 } catch (RepositoryException e
) {
1074 discardQuietly(securitySession
);
1075 throw new ArgeoException("Cannot create user profile for "
1076 + username
+ " in workspace "
1077 + securitySession
.getWorkspace().getName(), e
);
1081 /** Creates an Argeo user home. */
1082 // public static Node createUserHome(Session session, String homeBasePath,
1083 // String username) {
1085 // if (session == null)
1086 // throw new ArgeoException("Session is null");
1087 // if (session.hasPendingChanges())
1088 // throw new ArgeoException(
1089 // "Session has pending changes, save them first");
1091 // String homePath = getUserHomePath(username);
1093 // if (session.itemExists(homePath)) {
1095 // throw new ArgeoException(
1096 // "Trying to create a user home that already exists");
1097 // } catch (Exception e) {
1098 // // we use this workaround to be sure to get the stack trace
1099 // // to identify the sink of the bug.
1100 // log.warn("trying to create an already existing userHome at path:"
1101 // + homePath + ". Stack trace : ");
1102 // e.printStackTrace();
1106 // Node userHome = JcrUtils.mkdirs(session, homePath);
1107 // Node userProfile;
1108 // if (userHome.hasNode(ArgeoNames.ARGEO_PROFILE)) {
1109 // log.warn("userProfile node already exists for userHome path: "
1110 // + homePath + ". We do not add a new one");
1112 // userProfile = userHome.addNode(ArgeoNames.ARGEO_PROFILE);
1113 // userProfile.addMixin(ArgeoTypes.ARGEO_USER_PROFILE);
1114 // // session.getWorkspace().getVersionManager()
1115 // // .checkout(userProfile.getPath());
1116 // userProfile.setProperty(ArgeoNames.ARGEO_USER_ID, username);
1118 // session.getWorkspace().getVersionManager()
1119 // .checkin(userProfile.getPath());
1120 // // we need to save the profile before adding the user home type
1122 // userHome.addMixin(ArgeoTypes.ARGEO_USER_HOME);
1125 // http://jackrabbit.510166.n4.nabble.com/Jackrabbit-2-0-beta-6-Problem-adding-a-Mixin-type-with-mandatory-properties-after-setting-propertiesn-td1290332.html
1126 // userHome.setProperty(ArgeoNames.ARGEO_USER_ID, username);
1129 // } catch (RepositoryException e) {
1130 // discardQuietly(session);
1131 // throw new ArgeoException("Cannot create home node for user "
1137 * Returns user home has path, embedding exceptions. Contrary to
1138 * {@link #getUserHome(Session)}, it never returns null but throws and
1139 * exception if not found.
1141 * @deprecated use getUserHome() instead, throwing an exception if it
1145 public static String
getUserHomePath(Session session
) {
1146 String userID
= session
.getUserID();
1148 String homePath
= getUserHomePath(userID
);
1149 if (session
.itemExists(homePath
))
1152 throw new ArgeoException("No home registered for " + userID
);
1153 } catch (RepositoryException e
) {
1154 throw new ArgeoException("Cannot find user home path", e
);
1159 * @return null if not found *
1161 public static Node
getUserProfile(Session session
, String username
) {
1163 Node userHome
= getUserHome(session
, username
);
1164 if (userHome
== null)
1166 if (userHome
.hasNode(ArgeoNames
.ARGEO_PROFILE
))
1167 return userHome
.getNode(ArgeoNames
.ARGEO_PROFILE
);
1170 } catch (RepositoryException e
) {
1171 throw new ArgeoException(
1172 "Cannot find profile for user " + username
, e
);
1177 * Get the profile of the user attached to this session.
1179 public static Node
getUserProfile(Session session
) {
1180 String userID
= session
.getUserID();
1181 return getUserProfile(session
, userID
);
1185 * Quietly unregisters an {@link EventListener} from the udnerlying
1186 * workspace of this node.
1188 public static void unregisterQuietly(Node node
, EventListener eventListener
) {
1190 unregisterQuietly(node
.getSession().getWorkspace(), eventListener
);
1191 } catch (RepositoryException e
) {
1193 if (log
.isTraceEnabled())
1194 log
.trace("Could not unregister event listener "
1199 /** Quietly unregisters an {@link EventListener} from this workspace */
1200 public static void unregisterQuietly(Workspace workspace
,
1201 EventListener eventListener
) {
1202 if (eventListener
== null)
1205 workspace
.getObservationManager()
1206 .removeEventListener(eventListener
);
1207 } catch (RepositoryException e
) {
1209 if (log
.isTraceEnabled())
1210 log
.trace("Could not unregister event listener "
1216 * If this node is has the {@link NodeType#MIX_LAST_MODIFIED} mixin, it
1217 * updates the {@link Property#JCR_LAST_MODIFIED} property with the current
1218 * time and the {@link Property#JCR_LAST_MODIFIED_BY} property with the
1219 * underlying session user id. In Jackrabbit 2.x, <a
1220 * href="https://issues.apache.org/jira/browse/JCR-2233">these properties
1221 * are not automatically updated</a>, hence the need for manual update. The
1222 * session is not saved.
1224 public static void updateLastModified(Node node
) {
1226 if (!node
.isNodeType(NodeType
.MIX_LAST_MODIFIED
))
1227 node
.addMixin(NodeType
.MIX_LAST_MODIFIED
);
1228 node
.setProperty(Property
.JCR_LAST_MODIFIED
,
1229 new GregorianCalendar());
1230 node
.setProperty(Property
.JCR_LAST_MODIFIED_BY
, node
.getSession()
1232 } catch (RepositoryException e
) {
1233 throw new ArgeoException("Cannot update last modified on " + node
,
1238 /** Update lastModified recursively until this parent. */
1239 public static void updateLastModifiedAndParents(Node node
, String untilPath
) {
1241 if (!node
.getPath().startsWith(untilPath
))
1242 throw new ArgeoException(node
+ " is not under " + untilPath
);
1243 updateLastModified(node
);
1244 if (!node
.getPath().equals(untilPath
))
1245 updateLastModifiedAndParents(node
.getParent(), untilPath
);
1246 } catch (RepositoryException e
) {
1247 throw new ArgeoException("Cannot update lastModified from " + node
1248 + " until " + untilPath
, e
);
1253 * Returns a String representing the short version (see <a
1254 * href="http://jackrabbit.apache.org/node-type-notation.html"> Node type
1255 * Notation </a> attributes grammar) of the main business attributes of this
1256 * property definition
1260 public static String
getPropertyDefinitionAsString(Property prop
) {
1261 StringBuffer sbuf
= new StringBuffer();
1263 if (prop
.getDefinition().isAutoCreated())
1265 if (prop
.getDefinition().isMandatory())
1267 if (prop
.getDefinition().isProtected())
1269 if (prop
.getDefinition().isMultiple())
1271 } catch (RepositoryException re
) {
1272 throw new ArgeoException(
1273 "unexpected error while getting property definition as String",
1276 return sbuf
.toString();
1280 * Estimate the sub tree size from current node. Computation is based on the
1281 * Jcr {@link Property.getLength()} method. Note : it is not the exact size
1282 * used on the disk by the current part of the JCR Tree.
1285 public static long getNodeApproxSize(Node node
) {
1286 long curNodeSize
= 0;
1288 PropertyIterator pi
= node
.getProperties();
1289 while (pi
.hasNext()) {
1290 Property prop
= pi
.nextProperty();
1291 if (prop
.isMultiple()) {
1292 int nb
= prop
.getLengths().length
;
1293 for (int i
= 0; i
< nb
; i
++) {
1294 curNodeSize
+= (prop
.getLengths()[i
] > 0 ? prop
1295 .getLengths()[i
] : 0);
1298 curNodeSize
+= (prop
.getLength() > 0 ? prop
.getLength() : 0);
1301 NodeIterator ni
= node
.getNodes();
1302 while (ni
.hasNext())
1303 curNodeSize
+= getNodeApproxSize(ni
.nextNode());
1305 } catch (RepositoryException re
) {
1306 throw new ArgeoException(
1307 "Unexpected error while recursively determining node size.",