2 * Copyright (C) 2007-2012 Mathieu Baudier
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.
16 package org
.argeo
.jcr
;
18 import java
.io
.ByteArrayInputStream
;
19 import java
.io
.ByteArrayOutputStream
;
20 import java
.io
.InputStream
;
21 import java
.net
.MalformedURLException
;
23 import java
.security
.Principal
;
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
.security
.AccessControlEntry
;
55 import javax
.jcr
.security
.AccessControlList
;
56 import javax
.jcr
.security
.AccessControlManager
;
57 import javax
.jcr
.security
.AccessControlPolicy
;
58 import javax
.jcr
.security
.AccessControlPolicyIterator
;
59 import javax
.jcr
.security
.Privilege
;
60 import javax
.jcr
.version
.VersionManager
;
62 import org
.apache
.commons
.io
.IOUtils
;
63 import org
.apache
.commons
.logging
.Log
;
64 import org
.apache
.commons
.logging
.LogFactory
;
65 import org
.argeo
.ArgeoException
;
66 import org
.argeo
.util
.security
.SimplePrincipal
;
68 /** Utility methods to simplify common JCR operations. */
69 public class JcrUtils
implements ArgeoJcrConstants
{
71 private final static Log log
= LogFactory
.getLog(JcrUtils
.class);
74 * Not complete yet. See
75 * http://www.day.com/specs/jcr/2.0/3_Repository_Model.html#3.2.2%20Local
78 public final static char[] INVALID_NAME_CHARACTERS
= { '/', ':', '[', ']',
84 /** Prevents instantiation */
89 * Queries one single node.
91 * @return one single node or null if none was found
92 * @throws ArgeoException
93 * if more than one node was found
95 public static Node
querySingleNode(Query query
) {
96 NodeIterator nodeIterator
;
98 QueryResult queryResult
= query
.execute();
99 nodeIterator
= queryResult
.getNodes();
100 } catch (RepositoryException e
) {
101 throw new ArgeoException("Cannot execute query " + query
, e
);
104 if (nodeIterator
.hasNext())
105 node
= nodeIterator
.nextNode();
109 if (nodeIterator
.hasNext())
110 throw new ArgeoException("Query returned more than one node.");
114 /** Retrieves the parent path of the provided path */
115 public static String
parentPath(String path
) {
116 if (path
.equals("/"))
117 throw new ArgeoException("Root path '/' has no parent path");
118 if (path
.charAt(0) != '/')
119 throw new ArgeoException("Path " + path
+ " must start with a '/'");
121 if (pathT
.charAt(pathT
.length() - 1) == '/')
122 pathT
= pathT
.substring(0, pathT
.length() - 2);
124 int index
= pathT
.lastIndexOf('/');
125 return pathT
.substring(0, index
);
128 /** The provided data as a path ('/' at the end, not the beginning) */
129 public static String
dateAsPath(Calendar cal
) {
130 return dateAsPath(cal
, false);
134 * Creates a deep path based on a URL:
135 * http://subdomain.example.com/to/content?args =>
136 * com/example/subdomain/to/content
138 public static String
urlAsPath(String url
) {
140 URL u
= new URL(url
);
141 StringBuffer path
= new StringBuffer(url
.length());
143 path
.append(hostAsPath(u
.getHost()));
144 // we don't put port since it may not always be there and may change
145 path
.append(u
.getPath());
146 return path
.toString();
147 } catch (MalformedURLException e
) {
148 throw new ArgeoException("Cannot generate URL path for " + url
, e
);
152 /** Set the {@link NodeType#NT_ADDRESS} properties based on this URL. */
153 public static void urlToAddressProperties(Node node
, String url
) {
155 URL u
= new URL(url
);
156 node
.setProperty(Property
.JCR_PROTOCOL
, u
.getProtocol());
157 node
.setProperty(Property
.JCR_HOST
, u
.getHost());
158 node
.setProperty(Property
.JCR_PORT
, Integer
.toString(u
.getPort()));
159 node
.setProperty(Property
.JCR_PATH
, normalizePath(u
.getPath()));
160 } catch (Exception e
) {
161 throw new ArgeoException("Cannot set URL " + url
162 + " as nt:address properties", e
);
166 /** Build URL based on the {@link NodeType#NT_ADDRESS} properties. */
167 public static String
urlFromAddressProperties(Node node
) {
170 node
.getProperty(Property
.JCR_PROTOCOL
).getString(), node
171 .getProperty(Property
.JCR_HOST
).getString(),
172 (int) node
.getProperty(Property
.JCR_PORT
).getLong(), node
173 .getProperty(Property
.JCR_PATH
).getString());
175 } catch (Exception e
) {
176 throw new ArgeoException(
177 "Cannot get URL from nt:address properties of " + node
, e
);
181 /** Make sure that: starts with '/', do not end with '/', do not have '//' */
182 public static String
normalizePath(String path
) {
183 List
<String
> tokens
= tokenize(path
);
184 StringBuffer buf
= new StringBuffer(path
.length());
185 for (String token
: tokens
) {
189 return buf
.toString();
193 * Creates a path from a FQDN, inverting the order of the component:
194 * www.argeo.org => org.argeo.www
196 public static String
hostAsPath(String host
) {
197 StringBuffer path
= new StringBuffer(host
.length());
198 String
[] hostTokens
= host
.split("\\.");
199 for (int i
= hostTokens
.length
- 1; i
>= 0; i
--) {
200 path
.append(hostTokens
[i
]);
204 return path
.toString();
208 * The provided data as a path ('/' at the end, not the beginning)
213 * whether to add hour as well
215 public static String
dateAsPath(Calendar cal
, Boolean addHour
) {
216 StringBuffer buf
= new StringBuffer(14);
218 buf
.append(cal
.get(Calendar
.YEAR
));
221 int month
= cal
.get(Calendar
.MONTH
) + 1;
228 int day
= cal
.get(Calendar
.DAY_OF_MONTH
);
236 int hour
= cal
.get(Calendar
.HOUR_OF_DAY
);
243 return buf
.toString();
247 /** Converts in one call a string into a gregorian calendar. */
248 public static Calendar
parseCalendar(DateFormat dateFormat
, String value
) {
250 Date date
= dateFormat
.parse(value
);
251 Calendar calendar
= new GregorianCalendar();
252 calendar
.setTime(date
);
254 } catch (ParseException e
) {
255 throw new ArgeoException("Cannot parse " + value
256 + " with date format " + dateFormat
, e
);
261 /** The last element of a path. */
262 public static String
lastPathElement(String path
) {
263 if (path
.charAt(path
.length() - 1) == '/')
264 throw new ArgeoException("Path " + path
+ " cannot end with '/'");
265 int index
= path
.lastIndexOf('/');
267 throw new ArgeoException("Cannot find last path element for "
269 return path
.substring(index
+ 1);
273 * Routine that get the child with this name, adding id it does not already
276 public static Node
getOrAdd(Node parent
, String childName
,
277 String childPrimaryNodeType
) throws RepositoryException
{
278 return parent
.hasNode(childName
) ? parent
.getNode(childName
) : parent
279 .addNode(childName
, childPrimaryNodeType
);
283 * Routine that get the child with this name, adding id it does not already
286 public static Node
getOrAdd(Node parent
, String childName
)
287 throws RepositoryException
{
288 return parent
.hasNode(childName
) ? parent
.getNode(childName
) : parent
292 /** Creates the nodes making path, if they don't exist. */
293 public static Node
mkdirs(Session session
, String path
) {
294 return mkdirs(session
, path
, null, null, false);
298 * use {@link #mkdirs(Session, String, String, String, Boolean)} instead.
303 public static Node
mkdirs(Session session
, String path
, String type
,
304 Boolean versioning
) {
305 return mkdirs(session
, path
, type
, type
, false);
310 * the type of the leaf node
312 public static Node
mkdirs(Session session
, String path
, String type
) {
313 return mkdirs(session
, path
, type
, null, false);
317 * Synchronized and save is performed, to avoid race conditions in
318 * initializers leading to duplicate nodes.
320 public synchronized static Node
mkdirsSafe(Session session
, String path
,
323 if (session
.hasPendingChanges())
324 throw new ArgeoException(
325 "Session has pending changes, save them first.");
326 Node node
= mkdirs(session
, path
, type
);
329 } catch (RepositoryException e
) {
330 discardQuietly(session
);
331 throw new ArgeoException("Cannot safely make directories", e
);
335 public synchronized static Node
mkdirsSafe(Session session
, String path
) {
336 return mkdirsSafe(session
, path
, null);
340 * Creates the nodes making path, if they don't exist. This is up to the
341 * caller to save the session. Use with caution since it can create
342 * duplicate nodes if used concurrently.
344 public static Node
mkdirs(Session session
, String path
, String type
,
345 String intermediaryNodeType
, Boolean versioning
) {
347 if (path
.equals('/'))
348 return session
.getRootNode();
350 if (session
.itemExists(path
)) {
351 Node node
= session
.getNode(path
);
353 if (type
!= null && !node
.isNodeType(type
))
354 throw new ArgeoException("Node " + node
355 + " exists but is of type "
356 + node
.getPrimaryNodeType().getName()
357 + " not of type " + type
);
358 // TODO: check versioning
362 StringBuffer current
= new StringBuffer("/");
363 Node currentNode
= session
.getRootNode();
364 Iterator
<String
> it
= tokenize(path
).iterator();
365 while (it
.hasNext()) {
366 String part
= it
.next();
367 current
.append(part
).append('/');
368 if (!session
.itemExists(current
.toString())) {
369 if (!it
.hasNext() && type
!= null)
370 currentNode
= currentNode
.addNode(part
, type
);
371 else if (it
.hasNext() && intermediaryNodeType
!= null)
372 currentNode
= currentNode
.addNode(part
,
373 intermediaryNodeType
);
375 currentNode
= currentNode
.addNode(part
);
377 currentNode
.addMixin(NodeType
.MIX_VERSIONABLE
);
378 if (log
.isTraceEnabled())
379 log
.debug("Added folder " + part
+ " as " + current
);
381 currentNode
= (Node
) session
.getItem(current
.toString());
385 } catch (RepositoryException e
) {
386 discardQuietly(session
);
387 throw new ArgeoException("Cannot mkdirs " + path
, e
);
392 /** Convert a path to the list of its tokens */
393 public static List
<String
> tokenize(String path
) {
394 List
<String
> tokens
= new ArrayList
<String
>();
395 boolean optimized
= false;
397 String
[] rawTokens
= path
.split("/");
398 for (String token
: rawTokens
) {
399 if (!token
.equals(""))
403 StringBuffer curr
= new StringBuffer();
404 char[] arr
= path
.toCharArray();
405 chars
: for (int i
= 0; i
< arr
.length
; i
++) {
408 if (i
== 0 || (i
== arr
.length
- 1))
410 if (curr
.length() > 0) {
411 tokens
.add(curr
.toString());
412 curr
= new StringBuffer();
417 if (curr
.length() > 0) {
418 tokens
.add(curr
.toString());
419 curr
= new StringBuffer();
422 return Collections
.unmodifiableList(tokens
);
426 * Safe and repository implementation independent registration of a
429 public static void registerNamespaceSafely(Session session
, String prefix
,
432 registerNamespaceSafely(session
.getWorkspace()
433 .getNamespaceRegistry(), prefix
, uri
);
434 } catch (RepositoryException e
) {
435 throw new ArgeoException("Cannot find namespace registry", e
);
440 * Safe and repository implementation independent registration of a
443 public static void registerNamespaceSafely(NamespaceRegistry nr
,
444 String prefix
, String uri
) {
446 String
[] prefixes
= nr
.getPrefixes();
447 for (String pref
: prefixes
)
448 if (pref
.equals(prefix
)) {
449 String registeredUri
= nr
.getURI(pref
);
450 if (!registeredUri
.equals(uri
))
451 throw new ArgeoException("Prefix " + pref
452 + " already registered for URI "
454 + " which is different from provided URI "
459 nr
.registerNamespace(prefix
, uri
);
460 } catch (RepositoryException e
) {
461 throw new ArgeoException("Cannot register namespace " + uri
462 + " under prefix " + prefix
, e
);
466 /** Recursively outputs the contents of the given node. */
467 public static void debug(Node node
) {
471 /** Recursively outputs the contents of the given node. */
472 public static void debug(Node node
, Log log
) {
474 // First output the node path
475 log
.debug(node
.getPath());
476 // Skip the virtual (and large!) jcr:system subtree
477 if (node
.getName().equals("jcr:system")) {
481 // Then the children nodes (recursive)
482 NodeIterator it
= node
.getNodes();
483 while (it
.hasNext()) {
484 Node childNode
= it
.nextNode();
485 debug(childNode
, log
);
488 // Then output the properties
489 PropertyIterator properties
= node
.getProperties();
490 // log.debug("Property are : ");
492 properties
: while (properties
.hasNext()) {
493 Property property
= properties
.nextProperty();
494 if (property
.getType() == PropertyType
.BINARY
)
495 continue properties
;// skip
496 if (property
.getDefinition().isMultiple()) {
497 // A multi-valued property, print all values
498 Value
[] values
= property
.getValues();
499 for (int i
= 0; i
< values
.length
; i
++) {
500 log
.debug(property
.getPath() + "="
501 + values
[i
].getString());
504 // A single-valued property
505 log
.debug(property
.getPath() + "=" + property
.getString());
508 } catch (Exception e
) {
509 log
.error("Could not debug " + node
, e
);
514 /** Logs the effective access control policies */
515 public static void logEffectiveAccessPolicies(Node node
) {
517 logEffectiveAccessPolicies(node
.getSession(), node
.getPath());
518 } catch (RepositoryException e
) {
519 log
.error("Cannot log effective access policies of " + node
, e
);
523 /** Logs the effective access control policies */
524 public static void logEffectiveAccessPolicies(Session session
, String path
) {
525 if (!log
.isDebugEnabled())
529 AccessControlPolicy
[] effectivePolicies
= session
530 .getAccessControlManager().getEffectivePolicies(path
);
531 if (effectivePolicies
.length
> 0) {
532 for (AccessControlPolicy policy
: effectivePolicies
) {
533 if (policy
instanceof AccessControlList
) {
534 AccessControlList acl
= (AccessControlList
) policy
;
535 log
.debug("Access control list for " + path
+ "\n"
536 + accessControlListSummary(acl
));
540 log
.debug("No effective access control policy for " + path
);
542 } catch (RepositoryException e
) {
543 log
.error("Cannot log effective access policies of " + path
, e
);
547 /** Returns a human-readable summary of this access control list. */
548 public static String
accessControlListSummary(AccessControlList acl
) {
549 StringBuffer buf
= new StringBuffer("");
551 for (AccessControlEntry ace
: acl
.getAccessControlEntries()) {
552 buf
.append('\t').append(ace
.getPrincipal().getName())
554 for (Privilege priv
: ace
.getPrivileges())
555 buf
.append("\t\t").append(priv
.getName()).append('\n');
557 return buf
.toString();
558 } catch (RepositoryException e
) {
559 throw new ArgeoException("Cannot write summary of " + acl
, e
);
564 * Copies recursively the content of a node to another one. Do NOT copy the
565 * property values of {@link NodeType#MIX_CREATED} and
566 * {@link NodeType#MIX_LAST_MODIFIED}, but update the
567 * {@link Property#JCR_LAST_MODIFIED} and
568 * {@link Property#JCR_LAST_MODIFIED_BY} properties if the target node has
569 * the {@link NodeType#MIX_LAST_MODIFIED} mixin.
571 public static void copy(Node fromNode
, Node toNode
) {
573 // process properties
574 PropertyIterator pit
= fromNode
.getProperties();
575 properties
: while (pit
.hasNext()) {
576 Property fromProperty
= pit
.nextProperty();
577 String propertyName
= fromProperty
.getName();
578 if (toNode
.hasProperty(propertyName
)
579 && toNode
.getProperty(propertyName
).getDefinition()
583 if (fromProperty
.getDefinition().isProtected())
586 if (propertyName
.equals("jcr:created")
587 || propertyName
.equals("jcr:createdBy")
588 || propertyName
.equals("jcr:lastModified")
589 || propertyName
.equals("jcr:lastModifiedBy"))
592 if (fromProperty
.isMultiple()) {
593 toNode
.setProperty(propertyName
, fromProperty
.getValues());
595 toNode
.setProperty(propertyName
, fromProperty
.getValue());
599 // update jcr:lastModified and jcr:lastModifiedBy in toNode in case
600 // they existed, before adding the mixins
601 updateLastModified(toNode
);
604 for (NodeType mixinType
: fromNode
.getMixinNodeTypes()) {
605 toNode
.addMixin(mixinType
.getName());
608 // process children nodes
609 NodeIterator nit
= fromNode
.getNodes();
610 while (nit
.hasNext()) {
611 Node fromChild
= nit
.nextNode();
612 Integer index
= fromChild
.getIndex();
613 String nodeRelPath
= fromChild
.getName() + "[" + index
+ "]";
615 if (toNode
.hasNode(nodeRelPath
))
616 toChild
= toNode
.getNode(nodeRelPath
);
618 toChild
= toNode
.addNode(fromChild
.getName(), fromChild
619 .getPrimaryNodeType().getName());
620 copy(fromChild
, toChild
);
622 } catch (RepositoryException e
) {
623 throw new ArgeoException("Cannot copy " + fromNode
+ " to "
629 * Check whether all first-level properties (except jcr:* properties) are
630 * equal. Skip jcr:* properties
632 public static Boolean
allPropertiesEquals(Node reference
, Node observed
,
633 Boolean onlyCommonProperties
) {
635 PropertyIterator pit
= reference
.getProperties();
636 props
: while (pit
.hasNext()) {
637 Property propReference
= pit
.nextProperty();
638 String propName
= propReference
.getName();
639 if (propName
.startsWith("jcr:"))
642 if (!observed
.hasProperty(propName
))
643 if (onlyCommonProperties
)
647 // TODO: deal with multiple property values?
648 if (!observed
.getProperty(propName
).getValue()
649 .equals(propReference
.getValue()))
653 } catch (RepositoryException e
) {
654 throw new ArgeoException("Cannot check all properties equals of "
655 + reference
+ " and " + observed
, e
);
659 public static Map
<String
, PropertyDiff
> diffProperties(Node reference
,
661 Map
<String
, PropertyDiff
> diffs
= new TreeMap
<String
, PropertyDiff
>();
662 diffPropertiesLevel(diffs
, null, reference
, observed
);
667 * Compare the properties of two nodes. Recursivity to child nodes is not
668 * yet supported. Skip jcr:* properties.
670 static void diffPropertiesLevel(Map
<String
, PropertyDiff
> diffs
,
671 String baseRelPath
, Node reference
, Node observed
) {
673 // check removed and modified
674 PropertyIterator pit
= reference
.getProperties();
675 props
: while (pit
.hasNext()) {
676 Property p
= pit
.nextProperty();
677 String name
= p
.getName();
678 if (name
.startsWith("jcr:"))
681 if (!observed
.hasProperty(name
)) {
682 String relPath
= propertyRelPath(baseRelPath
, name
);
683 PropertyDiff pDiff
= new PropertyDiff(PropertyDiff
.REMOVED
,
684 relPath
, p
.getValue(), null);
685 diffs
.put(relPath
, pDiff
);
687 if (p
.isMultiple()) {
688 // FIXME implement multiple
690 Value referenceValue
= p
.getValue();
691 Value newValue
= observed
.getProperty(name
).getValue();
692 if (!referenceValue
.equals(newValue
)) {
693 String relPath
= propertyRelPath(baseRelPath
, name
);
694 PropertyDiff pDiff
= new PropertyDiff(
695 PropertyDiff
.MODIFIED
, relPath
,
696 referenceValue
, newValue
);
697 diffs
.put(relPath
, pDiff
);
703 pit
= observed
.getProperties();
704 props
: while (pit
.hasNext()) {
705 Property p
= pit
.nextProperty();
706 String name
= p
.getName();
707 if (name
.startsWith("jcr:"))
709 if (!reference
.hasProperty(name
)) {
710 if (p
.isMultiple()) {
711 // FIXME implement multiple
713 String relPath
= propertyRelPath(baseRelPath
, name
);
714 PropertyDiff pDiff
= new PropertyDiff(
715 PropertyDiff
.ADDED
, relPath
, null, p
.getValue());
716 diffs
.put(relPath
, pDiff
);
720 } catch (RepositoryException e
) {
721 throw new ArgeoException("Cannot diff " + reference
+ " and "
727 * Compare only a restricted list of properties of two nodes. No
731 public static Map
<String
, PropertyDiff
> diffProperties(Node reference
,
732 Node observed
, List
<String
> properties
) {
733 Map
<String
, PropertyDiff
> diffs
= new TreeMap
<String
, PropertyDiff
>();
735 Iterator
<String
> pit
= properties
.iterator();
737 props
: while (pit
.hasNext()) {
738 String name
= pit
.next();
739 if (!reference
.hasProperty(name
)) {
740 if (!observed
.hasProperty(name
))
742 Value val
= observed
.getProperty(name
).getValue();
744 // empty String but not null
745 if ("".equals(val
.getString()))
747 } catch (Exception e
) {
748 // not parseable as String, silent
750 PropertyDiff pDiff
= new PropertyDiff(PropertyDiff
.ADDED
,
752 diffs
.put(name
, pDiff
);
753 } else if (!observed
.hasProperty(name
)) {
754 PropertyDiff pDiff
= new PropertyDiff(PropertyDiff
.REMOVED
,
755 name
, reference
.getProperty(name
).getValue(), null);
756 diffs
.put(name
, pDiff
);
758 Value referenceValue
= reference
.getProperty(name
)
760 Value newValue
= observed
.getProperty(name
).getValue();
761 if (!referenceValue
.equals(newValue
)) {
762 PropertyDiff pDiff
= new PropertyDiff(
763 PropertyDiff
.MODIFIED
, name
, referenceValue
,
765 diffs
.put(name
, pDiff
);
769 } catch (RepositoryException e
) {
770 throw new ArgeoException("Cannot diff " + reference
+ " and "
776 /** Builds a property relPath to be used in the diff. */
777 private static String
propertyRelPath(String baseRelPath
,
778 String propertyName
) {
779 if (baseRelPath
== null)
782 return baseRelPath
+ '/' + propertyName
;
786 * Normalizes a name so that it can be stored in contexts not supporting
787 * names with ':' (typically databases). Replaces ':' by '_'.
789 public static String
normalize(String name
) {
790 return name
.replace(':', '_');
794 * Replaces characters which are invalid in a JCR name by '_'. Currently not
797 * @see JcrUtils#INVALID_NAME_CHARACTERS
799 public static String
replaceInvalidChars(String name
) {
800 return replaceInvalidChars(name
, '_');
804 * Replaces characters which are invalid in a JCR name. Currently not
807 * @see JcrUtils#INVALID_NAME_CHARACTERS
809 public static String
replaceInvalidChars(String name
, char replacement
) {
810 boolean modified
= false;
811 char[] arr
= name
.toCharArray();
812 for (int i
= 0; i
< arr
.length
; i
++) {
814 invalid
: for (char invalid
: INVALID_NAME_CHARACTERS
) {
816 arr
[i
] = replacement
;
823 return new String(arr
);
825 // do not create new object if unnecessary
830 * Removes forbidden characters from a path, replacing them with '_'
832 * @deprecated use {@link #replaceInvalidChars(String)} instead
834 public static String
removeForbiddenCharacters(String str
) {
835 return str
.replace('[', '_').replace(']', '_').replace('/', '_')
840 /** Cleanly disposes a {@link Binary} even if it is null. */
841 public static void closeQuietly(Binary binary
) {
847 /** Retrieve a {@link Binary} as a byte array */
848 public static byte[] getBinaryAsBytes(Property property
) {
849 ByteArrayOutputStream out
= new ByteArrayOutputStream();
850 InputStream in
= null;
851 Binary binary
= null;
853 binary
= property
.getBinary();
854 in
= binary
.getStream();
855 IOUtils
.copy(in
, out
);
856 return out
.toByteArray();
857 } catch (Exception e
) {
858 throw new ArgeoException("Cannot read binary " + property
861 IOUtils
.closeQuietly(out
);
862 IOUtils
.closeQuietly(in
);
863 closeQuietly(binary
);
867 /** Writes a {@link Binary} from a byte array */
868 public static void setBinaryAsBytes(Node node
, String property
, byte[] bytes
) {
869 InputStream in
= null;
870 Binary binary
= null;
872 in
= new ByteArrayInputStream(bytes
);
873 binary
= node
.getSession().getValueFactory().createBinary(in
);
874 node
.setProperty(property
, binary
);
875 } catch (Exception e
) {
876 throw new ArgeoException("Cannot read binary " + property
879 IOUtils
.closeQuietly(in
);
880 closeQuietly(binary
);
885 * Creates depth from a string (typically a username) by adding levels based
886 * on its first characters: "aBcD",2 => a/aB
888 public static String
firstCharsToPath(String str
, Integer nbrOfChars
) {
889 if (str
.length() < nbrOfChars
)
890 throw new ArgeoException("String " + str
891 + " length must be greater or equal than " + nbrOfChars
);
892 StringBuffer path
= new StringBuffer("");
893 StringBuffer curr
= new StringBuffer("");
894 for (int i
= 0; i
< nbrOfChars
; i
++) {
895 curr
.append(str
.charAt(i
));
897 if (i
< nbrOfChars
- 1)
900 return path
.toString();
904 * Wraps the call to the repository factory based on parameter
905 * {@link ArgeoJcrConstants#JCR_REPOSITORY_ALIAS} in order to simplify it
906 * and protect against future API changes.
908 public static Repository
getRepositoryByAlias(
909 RepositoryFactory repositoryFactory
, String alias
) {
911 Map
<String
, String
> parameters
= new HashMap
<String
, String
>();
912 parameters
.put(JCR_REPOSITORY_ALIAS
, alias
);
913 return repositoryFactory
.getRepository(parameters
);
914 } catch (RepositoryException e
) {
915 throw new ArgeoException(
916 "Unexpected exception when trying to retrieve repository with alias "
922 * Wraps the call to the repository factory based on parameter
923 * {@link ArgeoJcrConstants#JCR_REPOSITORY_URI} in order to simplify it and
924 * protect against future API changes.
926 public static Repository
getRepositoryByUri(
927 RepositoryFactory repositoryFactory
, String uri
) {
929 Map
<String
, String
> parameters
= new HashMap
<String
, String
>();
930 parameters
.put(JCR_REPOSITORY_URI
, uri
);
931 return repositoryFactory
.getRepository(parameters
);
932 } catch (RepositoryException e
) {
933 throw new ArgeoException(
934 "Unexpected exception when trying to retrieve repository with uri "
940 * Discards the current changes in the session attached to this node. To be
941 * used typically in a catch block.
943 * @see #discardQuietly(Session)
945 public static void discardUnderlyingSessionQuietly(Node node
) {
947 discardQuietly(node
.getSession());
948 } catch (RepositoryException e
) {
949 log
.warn("Cannot quietly discard session of node " + node
+ ": "
955 * Discards the current changes in a session by calling
956 * {@link Session#refresh(boolean)} with <code>false</code>, only logging
957 * potential errors when doing so. To be used typically in a catch block.
959 public static void discardQuietly(Session session
) {
962 session
.refresh(false);
963 } catch (RepositoryException e
) {
964 log
.warn("Cannot quietly discard session " + session
+ ": "
969 /** Logs out the session, not throwing any exception, even if it is null. */
970 public static void logoutQuietly(Session session
) {
973 if (session
.isLive())
975 } catch (Exception e
) {
981 * Convenient method to add a listener. uuids passed as null, deep=true,
982 * local=true, only one node type
984 public static void addListener(Session session
, EventListener listener
,
985 int eventTypes
, String basePath
, String nodeType
) {
987 session
.getWorkspace()
988 .getObservationManager()
989 .addEventListener(listener
, eventTypes
, basePath
, true,
990 null, new String
[] { nodeType
}, true);
991 } catch (RepositoryException e
) {
992 throw new ArgeoException("Cannot add JCR listener " + listener
993 + " to session " + session
, e
);
997 /** Removes a listener without throwing exception */
998 public static void removeListenerQuietly(Session session
,
999 EventListener listener
) {
1000 if (session
== null || !session
.isLive())
1003 session
.getWorkspace().getObservationManager()
1004 .removeEventListener(listener
);
1005 } catch (RepositoryException e
) {
1010 /** Returns the home node of the session user or null if none was found. */
1011 public static Node
getUserHome(Session session
) {
1012 String userID
= session
.getUserID();
1013 return getUserHome(session
, userID
);
1016 /** User home path is NOT configurable */
1017 public static String
getUserHomePath(String username
) {
1018 String homeBasePath
= DEFAULT_HOME_BASE_PATH
;
1019 return homeBasePath
+ '/' + firstCharsToPath(username
, 2) + '/'
1024 * Returns the home node of the session user or null if none was found.
1027 * the session to use in order to perform the search, this can be
1028 * a session with a different user ID than the one searched,
1029 * typically when a system or admin session is used.
1031 * the username of the user
1033 public static Node
getUserHome(Session session
, String username
) {
1035 String homePath
= getUserHomePath(username
);
1036 return session
.itemExists(homePath
) ? session
.getNode(homePath
)
1038 // kept for example of QOM queries
1039 // QueryObjectModelFactory qomf = session.getWorkspace()
1040 // .getQueryManager().getQOMFactory();
1041 // Selector userHomeSel = qomf.selector(ArgeoTypes.ARGEO_USER_HOME,
1043 // DynamicOperand userIdDop = qomf.propertyValue("userHome",
1044 // ArgeoNames.ARGEO_USER_ID);
1045 // StaticOperand userIdSop = qomf.literal(session.getValueFactory()
1046 // .createValue(username));
1047 // Constraint constraint = qomf.comparison(userIdDop,
1048 // QueryObjectModelFactory.JCR_OPERATOR_EQUAL_TO, userIdSop);
1049 // Query query = qomf.createQuery(userHomeSel, constraint, null,
1051 // Node userHome = JcrUtils.querySingleNode(query);
1052 } catch (RepositoryException e
) {
1053 throw new ArgeoException("Cannot find home for user " + username
, e
);
1058 * Creates an Argeo user home, does nothing if it already exists. Session is
1061 public static Node
createUserHomeIfNeeded(Session session
, String username
) {
1063 String homePath
= getUserHomePath(username
);
1064 if (session
.itemExists(homePath
))
1065 return session
.getNode(homePath
);
1067 Node userHome
= JcrUtils
.mkdirs(session
, homePath
);
1068 userHome
.addMixin(ArgeoTypes
.ARGEO_USER_HOME
);
1069 userHome
.setProperty(ArgeoNames
.ARGEO_USER_ID
, username
);
1072 } catch (RepositoryException e
) {
1073 discardQuietly(session
);
1074 throw new ArgeoException("Cannot create home for " + username
1075 + " in workspace " + session
.getWorkspace().getName(), e
);
1080 * Creates a user profile in the home of this user. Creates the home if
1081 * needed, but throw an exception if a profile already exists. The session
1082 * is not saved and the node is in a checkedOut state (that is, it requires
1083 * a subsequent checkin after saving the session).
1085 public static Node
createUserProfile(Session session
, String username
) {
1087 Node userHome
= createUserHomeIfNeeded(session
, username
);
1088 if (userHome
.hasNode(ArgeoNames
.ARGEO_PROFILE
))
1089 throw new ArgeoException(
1090 "There is already a user profile under " + userHome
);
1091 Node userProfile
= userHome
.addNode(ArgeoNames
.ARGEO_PROFILE
);
1092 userProfile
.addMixin(ArgeoTypes
.ARGEO_USER_PROFILE
);
1093 userProfile
.setProperty(ArgeoNames
.ARGEO_USER_ID
, username
);
1094 userProfile
.setProperty(ArgeoNames
.ARGEO_ENABLED
, true);
1095 userProfile
.setProperty(ArgeoNames
.ARGEO_ACCOUNT_NON_EXPIRED
, true);
1096 userProfile
.setProperty(ArgeoNames
.ARGEO_ACCOUNT_NON_LOCKED
, true);
1097 userProfile
.setProperty(ArgeoNames
.ARGEO_CREDENTIALS_NON_EXPIRED
,
1100 } catch (RepositoryException e
) {
1101 discardQuietly(session
);
1102 throw new ArgeoException("Cannot create user profile for "
1103 + username
+ " in workspace "
1104 + session
.getWorkspace().getName(), e
);
1109 * Create user profile if needed, the session IS saved.
1111 * @return the user profile
1113 public static Node
createUserProfileIfNeeded(Session securitySession
,
1116 Node userHome
= JcrUtils
.createUserHomeIfNeeded(securitySession
,
1118 Node userProfile
= userHome
.hasNode(ArgeoNames
.ARGEO_PROFILE
) ? userHome
1119 .getNode(ArgeoNames
.ARGEO_PROFILE
) : JcrUtils
1120 .createUserProfile(securitySession
, username
);
1121 if (securitySession
.hasPendingChanges())
1122 securitySession
.save();
1123 VersionManager versionManager
= securitySession
.getWorkspace()
1124 .getVersionManager();
1125 if (versionManager
.isCheckedOut(userProfile
.getPath()))
1126 versionManager
.checkin(userProfile
.getPath());
1128 } catch (RepositoryException e
) {
1129 discardQuietly(securitySession
);
1130 throw new ArgeoException("Cannot create user profile for "
1131 + username
+ " in workspace "
1132 + securitySession
.getWorkspace().getName(), e
);
1136 /** Creates an Argeo user home. */
1137 // public static Node createUserHome(Session session, String homeBasePath,
1138 // String username) {
1140 // if (session == null)
1141 // throw new ArgeoException("Session is null");
1142 // if (session.hasPendingChanges())
1143 // throw new ArgeoException(
1144 // "Session has pending changes, save them first");
1146 // String homePath = getUserHomePath(username);
1148 // if (session.itemExists(homePath)) {
1150 // throw new ArgeoException(
1151 // "Trying to create a user home that already exists");
1152 // } catch (Exception e) {
1153 // // we use this workaround to be sure to get the stack trace
1154 // // to identify the sink of the bug.
1155 // log.warn("trying to create an already existing userHome at path:"
1156 // + homePath + ". Stack trace : ");
1157 // e.printStackTrace();
1161 // Node userHome = JcrUtils.mkdirs(session, homePath);
1162 // Node userProfile;
1163 // if (userHome.hasNode(ArgeoNames.ARGEO_PROFILE)) {
1164 // log.warn("userProfile node already exists for userHome path: "
1165 // + homePath + ". We do not add a new one");
1167 // userProfile = userHome.addNode(ArgeoNames.ARGEO_PROFILE);
1168 // userProfile.addMixin(ArgeoTypes.ARGEO_USER_PROFILE);
1169 // // session.getWorkspace().getVersionManager()
1170 // // .checkout(userProfile.getPath());
1171 // userProfile.setProperty(ArgeoNames.ARGEO_USER_ID, username);
1173 // session.getWorkspace().getVersionManager()
1174 // .checkin(userProfile.getPath());
1175 // // we need to save the profile before adding the user home type
1177 // userHome.addMixin(ArgeoTypes.ARGEO_USER_HOME);
1180 // http://jackrabbit.510166.n4.nabble.com/Jackrabbit-2-0-beta-6-Problem-adding-a-Mixin-type-with-mandatory-properties-after-setting-propertiesn-td1290332.html
1181 // userHome.setProperty(ArgeoNames.ARGEO_USER_ID, username);
1184 // } catch (RepositoryException e) {
1185 // discardQuietly(session);
1186 // throw new ArgeoException("Cannot create home node for user "
1192 * Returns user home has path, embedding exceptions. Contrary to
1193 * {@link #getUserHome(Session)}, it never returns null but throws and
1194 * exception if not found.
1196 * @deprecated use getUserHome() instead, throwing an exception if it
1200 public static String
getUserHomePath(Session session
) {
1201 String userID
= session
.getUserID();
1203 String homePath
= getUserHomePath(userID
);
1204 if (session
.itemExists(homePath
))
1207 throw new ArgeoException("No home registered for " + userID
);
1208 } catch (RepositoryException e
) {
1209 throw new ArgeoException("Cannot find user home path", e
);
1214 * @return null if not found *
1216 public static Node
getUserProfile(Session session
, String username
) {
1218 Node userHome
= getUserHome(session
, username
);
1219 if (userHome
== null)
1221 if (userHome
.hasNode(ArgeoNames
.ARGEO_PROFILE
))
1222 return userHome
.getNode(ArgeoNames
.ARGEO_PROFILE
);
1225 } catch (RepositoryException e
) {
1226 throw new ArgeoException(
1227 "Cannot find profile for user " + username
, e
);
1232 * Get the profile of the user attached to this session.
1234 public static Node
getUserProfile(Session session
) {
1235 String userID
= session
.getUserID();
1236 return getUserProfile(session
, userID
);
1240 * Quietly unregisters an {@link EventListener} from the udnerlying
1241 * workspace of this node.
1243 public static void unregisterQuietly(Node node
, EventListener eventListener
) {
1245 unregisterQuietly(node
.getSession().getWorkspace(), eventListener
);
1246 } catch (RepositoryException e
) {
1248 if (log
.isTraceEnabled())
1249 log
.trace("Could not unregister event listener "
1254 /** Quietly unregisters an {@link EventListener} from this workspace */
1255 public static void unregisterQuietly(Workspace workspace
,
1256 EventListener eventListener
) {
1257 if (eventListener
== null)
1260 workspace
.getObservationManager()
1261 .removeEventListener(eventListener
);
1262 } catch (RepositoryException e
) {
1264 if (log
.isTraceEnabled())
1265 log
.trace("Could not unregister event listener "
1271 * If this node is has the {@link NodeType#MIX_LAST_MODIFIED} mixin, it
1272 * updates the {@link Property#JCR_LAST_MODIFIED} property with the current
1273 * time and the {@link Property#JCR_LAST_MODIFIED_BY} property with the
1274 * underlying session user id. In Jackrabbit 2.x, <a
1275 * href="https://issues.apache.org/jira/browse/JCR-2233">these properties
1276 * are not automatically updated</a>, hence the need for manual update. The
1277 * session is not saved.
1279 public static void updateLastModified(Node node
) {
1281 if (!node
.isNodeType(NodeType
.MIX_LAST_MODIFIED
))
1282 node
.addMixin(NodeType
.MIX_LAST_MODIFIED
);
1283 node
.setProperty(Property
.JCR_LAST_MODIFIED
,
1284 new GregorianCalendar());
1285 node
.setProperty(Property
.JCR_LAST_MODIFIED_BY
, node
.getSession()
1287 } catch (RepositoryException e
) {
1288 throw new ArgeoException("Cannot update last modified on " + node
,
1293 /** Update lastModified recursively until this parent. */
1294 public static void updateLastModifiedAndParents(Node node
, String untilPath
) {
1296 if (!node
.getPath().startsWith(untilPath
))
1297 throw new ArgeoException(node
+ " is not under " + untilPath
);
1298 updateLastModified(node
);
1299 if (!node
.getPath().equals(untilPath
))
1300 updateLastModifiedAndParents(node
.getParent(), untilPath
);
1301 } catch (RepositoryException e
) {
1302 throw new ArgeoException("Cannot update lastModified from " + node
1303 + " until " + untilPath
, e
);
1308 * Returns a String representing the short version (see <a
1309 * href="http://jackrabbit.apache.org/node-type-notation.html"> Node type
1310 * Notation </a> attributes grammar) of the main business attributes of this
1311 * property definition
1315 public static String
getPropertyDefinitionAsString(Property prop
) {
1316 StringBuffer sbuf
= new StringBuffer();
1318 if (prop
.getDefinition().isAutoCreated())
1320 if (prop
.getDefinition().isMandatory())
1322 if (prop
.getDefinition().isProtected())
1324 if (prop
.getDefinition().isMultiple())
1326 } catch (RepositoryException re
) {
1327 throw new ArgeoException(
1328 "unexpected error while getting property definition as String",
1331 return sbuf
.toString();
1335 * Estimate the sub tree size from current node. Computation is based on the
1336 * Jcr {@link Property.getLength()} method. Note : it is not the exact size
1337 * used on the disk by the current part of the JCR Tree.
1340 public static long getNodeApproxSize(Node node
) {
1341 long curNodeSize
= 0;
1343 PropertyIterator pi
= node
.getProperties();
1344 while (pi
.hasNext()) {
1345 Property prop
= pi
.nextProperty();
1346 if (prop
.isMultiple()) {
1347 int nb
= prop
.getLengths().length
;
1348 for (int i
= 0; i
< nb
; i
++) {
1349 curNodeSize
+= (prop
.getLengths()[i
] > 0 ? prop
1350 .getLengths()[i
] : 0);
1353 curNodeSize
+= (prop
.getLength() > 0 ? prop
.getLength() : 0);
1356 NodeIterator ni
= node
.getNodes();
1357 while (ni
.hasNext())
1358 curNodeSize
+= getNodeApproxSize(ni
.nextNode());
1360 } catch (RepositoryException re
) {
1361 throw new ArgeoException(
1362 "Unexpected error while recursively determining node size.",
1372 * Convenience method for adding a single privilege to a principal (user or
1373 * role), typically jcr:all
1375 public static void addPrivilege(Session session
, String path
,
1376 String principal
, String privilege
) throws RepositoryException
{
1377 List
<Privilege
> privileges
= new ArrayList
<Privilege
>();
1378 privileges
.add(session
.getAccessControlManager().privilegeFromName(
1380 addPrivileges(session
, path
, new SimplePrincipal(principal
), privileges
);
1384 * Add privileges on a path to a {@link Principal}. The path must already
1387 public static void addPrivileges(Session session
, String path
,
1388 Principal principal
, List
<Privilege
> privs
)
1389 throws RepositoryException
{
1390 AccessControlManager acm
= session
.getAccessControlManager();
1391 // search for an access control list
1392 AccessControlList acl
= null;
1393 AccessControlPolicyIterator policyIterator
= acm
1394 .getApplicablePolicies(path
);
1395 if (policyIterator
.hasNext()) {
1396 while (policyIterator
.hasNext()) {
1397 AccessControlPolicy acp
= policyIterator
1398 .nextAccessControlPolicy();
1399 if (acp
instanceof AccessControlList
)
1400 acl
= ((AccessControlList
) acp
);
1403 AccessControlPolicy
[] existingPolicies
= acm
.getPolicies(path
);
1404 for (AccessControlPolicy acp
: existingPolicies
) {
1405 if (acp
instanceof AccessControlList
)
1406 acl
= ((AccessControlList
) acp
);
1411 acl
.addAccessControlEntry(principal
,
1412 privs
.toArray(new Privilege
[privs
.size()]));
1413 acm
.setPolicy(path
, acl
);
1414 if (log
.isDebugEnabled())
1415 log
.debug("Added privileges " + privs
+ " to " + principal
1418 throw new ArgeoException("Don't know how to apply privileges "
1419 + privs
+ " to " + principal
+ " on " + path
);