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
.security
.Principal
;
25 import java
.text
.DateFormat
;
26 import java
.text
.ParseException
;
27 import java
.util
.ArrayList
;
28 import java
.util
.Calendar
;
29 import java
.util
.Collections
;
30 import java
.util
.Date
;
31 import java
.util
.GregorianCalendar
;
32 import java
.util
.HashMap
;
33 import java
.util
.Iterator
;
34 import java
.util
.List
;
36 import java
.util
.TreeMap
;
38 import javax
.jcr
.Binary
;
39 import javax
.jcr
.NamespaceRegistry
;
40 import javax
.jcr
.Node
;
41 import javax
.jcr
.NodeIterator
;
42 import javax
.jcr
.Property
;
43 import javax
.jcr
.PropertyIterator
;
44 import javax
.jcr
.PropertyType
;
45 import javax
.jcr
.Repository
;
46 import javax
.jcr
.RepositoryException
;
47 import javax
.jcr
.RepositoryFactory
;
48 import javax
.jcr
.Session
;
49 import javax
.jcr
.Value
;
50 import javax
.jcr
.Workspace
;
51 import javax
.jcr
.nodetype
.NodeType
;
52 import javax
.jcr
.observation
.EventListener
;
53 import javax
.jcr
.query
.Query
;
54 import javax
.jcr
.query
.QueryResult
;
55 import javax
.jcr
.security
.AccessControlEntry
;
56 import javax
.jcr
.security
.AccessControlList
;
57 import javax
.jcr
.security
.AccessControlManager
;
58 import javax
.jcr
.security
.AccessControlPolicy
;
59 import javax
.jcr
.security
.AccessControlPolicyIterator
;
60 import javax
.jcr
.security
.Privilege
;
61 import javax
.jcr
.version
.VersionManager
;
63 import org
.apache
.commons
.io
.IOUtils
;
64 import org
.apache
.commons
.logging
.Log
;
65 import org
.apache
.commons
.logging
.LogFactory
;
66 import org
.argeo
.ArgeoException
;
67 import org
.argeo
.util
.security
.SimplePrincipal
;
69 /** Utility methods to simplify common JCR operations. */
70 public class JcrUtils
implements ArgeoJcrConstants
{
72 private final static Log log
= LogFactory
.getLog(JcrUtils
.class);
75 * Not complete yet. See
76 * http://www.day.com/specs/jcr/2.0/3_Repository_Model.html#3.2.2%20Local
79 public final static char[] INVALID_NAME_CHARACTERS
= { '/', ':', '[', ']',
85 /** Prevents instantiation */
90 * Queries one single node.
92 * @return one single node or null if none was found
93 * @throws ArgeoException
94 * if more than one node was found
96 public static Node
querySingleNode(Query query
) {
97 NodeIterator nodeIterator
;
99 QueryResult queryResult
= query
.execute();
100 nodeIterator
= queryResult
.getNodes();
101 } catch (RepositoryException e
) {
102 throw new ArgeoException("Cannot execute query " + query
, e
);
105 if (nodeIterator
.hasNext())
106 node
= nodeIterator
.nextNode();
110 if (nodeIterator
.hasNext())
111 throw new ArgeoException("Query returned more than one node.");
115 /** Retrieves the parent path of the provided path */
116 public static String
parentPath(String path
) {
117 if (path
.equals("/"))
118 throw new ArgeoException("Root path '/' has no parent path");
119 if (path
.charAt(0) != '/')
120 throw new ArgeoException("Path " + path
+ " must start with a '/'");
122 if (pathT
.charAt(pathT
.length() - 1) == '/')
123 pathT
= pathT
.substring(0, pathT
.length() - 2);
125 int index
= pathT
.lastIndexOf('/');
126 return pathT
.substring(0, index
);
129 /** The provided data as a path ('/' at the end, not the beginning) */
130 public static String
dateAsPath(Calendar cal
) {
131 return dateAsPath(cal
, false);
135 * Creates a deep path based on a URL:
136 * http://subdomain.example.com/to/content?args =>
137 * com/example/subdomain/to/content
139 public static String
urlAsPath(String url
) {
141 URL u
= new URL(url
);
142 StringBuffer path
= new StringBuffer(url
.length());
144 path
.append(hostAsPath(u
.getHost()));
145 // we don't put port since it may not always be there and may change
146 path
.append(u
.getPath());
147 return path
.toString();
148 } catch (MalformedURLException e
) {
149 throw new ArgeoException("Cannot generate URL path for " + url
, e
);
153 /** Set the {@link NodeType#NT_ADDRESS} properties based on this URL. */
154 public static void urlToAddressProperties(Node node
, String url
) {
156 URL u
= new URL(url
);
157 node
.setProperty(Property
.JCR_PROTOCOL
, u
.getProtocol());
158 node
.setProperty(Property
.JCR_HOST
, u
.getHost());
159 node
.setProperty(Property
.JCR_PORT
, Integer
.toString(u
.getPort()));
160 node
.setProperty(Property
.JCR_PATH
, normalizePath(u
.getPath()));
161 } catch (Exception e
) {
162 throw new ArgeoException("Cannot set URL " + url
163 + " as nt:address properties", e
);
167 /** Build URL based on the {@link NodeType#NT_ADDRESS} properties. */
168 public static String
urlFromAddressProperties(Node node
) {
171 node
.getProperty(Property
.JCR_PROTOCOL
).getString(), node
172 .getProperty(Property
.JCR_HOST
).getString(),
173 (int) node
.getProperty(Property
.JCR_PORT
).getLong(), node
174 .getProperty(Property
.JCR_PATH
).getString());
176 } catch (Exception e
) {
177 throw new ArgeoException(
178 "Cannot get URL from nt:address properties of " + node
, e
);
182 /** Make sure that: starts with '/', do not end with '/', do not have '//' */
183 public static String
normalizePath(String path
) {
184 List
<String
> tokens
= tokenize(path
);
185 StringBuffer buf
= new StringBuffer(path
.length());
186 for (String token
: tokens
) {
190 return buf
.toString();
194 * Creates a path from a FQDN, inverting the order of the component:
195 * www.argeo.org => org.argeo.www
197 public static String
hostAsPath(String host
) {
198 StringBuffer path
= new StringBuffer(host
.length());
199 String
[] hostTokens
= host
.split("\\.");
200 for (int i
= hostTokens
.length
- 1; i
>= 0; i
--) {
201 path
.append(hostTokens
[i
]);
205 return path
.toString();
209 * The provided data as a path ('/' at the end, not the beginning)
214 * whether to add hour as well
216 public static String
dateAsPath(Calendar cal
, Boolean addHour
) {
217 StringBuffer buf
= new StringBuffer(14);
219 buf
.append(cal
.get(Calendar
.YEAR
));
222 int month
= cal
.get(Calendar
.MONTH
) + 1;
229 int day
= cal
.get(Calendar
.DAY_OF_MONTH
);
237 int hour
= cal
.get(Calendar
.HOUR_OF_DAY
);
244 return buf
.toString();
248 /** Converts in one call a string into a gregorian calendar. */
249 public static Calendar
parseCalendar(DateFormat dateFormat
, String value
) {
251 Date date
= dateFormat
.parse(value
);
252 Calendar calendar
= new GregorianCalendar();
253 calendar
.setTime(date
);
255 } catch (ParseException e
) {
256 throw new ArgeoException("Cannot parse " + value
257 + " with date format " + dateFormat
, e
);
262 /** The last element of a path. */
263 public static String
lastPathElement(String path
) {
264 if (path
.charAt(path
.length() - 1) == '/')
265 throw new ArgeoException("Path " + path
+ " cannot end with '/'");
266 int index
= path
.lastIndexOf('/');
268 throw new ArgeoException("Cannot find last path element for "
270 return path
.substring(index
+ 1);
274 * Routine that get the child with this name, adding id it does not already
277 public static Node
getOrAdd(Node parent
, String childName
,
278 String childPrimaryNodeType
) throws RepositoryException
{
279 return parent
.hasNode(childName
) ? parent
.getNode(childName
) : parent
280 .addNode(childName
, childPrimaryNodeType
);
284 * Routine that get the child with this name, adding id it does not already
287 public static Node
getOrAdd(Node parent
, String childName
)
288 throws RepositoryException
{
289 return parent
.hasNode(childName
) ? parent
.getNode(childName
) : parent
293 /** Creates the nodes making path, if they don't exist. */
294 public static Node
mkdirs(Session session
, String path
) {
295 return mkdirs(session
, path
, null, null, false);
299 * use {@link #mkdirs(Session, String, String, String, Boolean)} instead.
304 public static Node
mkdirs(Session session
, String path
, String type
,
305 Boolean versioning
) {
306 return mkdirs(session
, path
, type
, type
, false);
311 * the type of the leaf node
313 public static Node
mkdirs(Session session
, String path
, String type
) {
314 return mkdirs(session
, path
, type
, null, false);
318 * Synchronized and save is performed, to avoid race conditions in
319 * initializers leading to duplicate nodes.
321 public synchronized static Node
mkdirsSafe(Session session
, String path
,
324 if (session
.hasPendingChanges())
325 throw new ArgeoException(
326 "Session has pending changes, save them first.");
327 Node node
= mkdirs(session
, path
, type
);
330 } catch (RepositoryException e
) {
331 discardQuietly(session
);
332 throw new ArgeoException("Cannot safely make directories", e
);
336 public synchronized static Node
mkdirsSafe(Session session
, String path
) {
337 return mkdirsSafe(session
, path
, null);
341 * Creates the nodes making path, if they don't exist. This is up to the
342 * caller to save the session. Use with caution since it can create
343 * duplicate nodes if used concurrently.
345 public static Node
mkdirs(Session session
, String path
, String type
,
346 String intermediaryNodeType
, Boolean versioning
) {
348 if (path
.equals('/'))
349 return session
.getRootNode();
351 if (session
.itemExists(path
)) {
352 Node node
= session
.getNode(path
);
354 if (type
!= null && !node
.isNodeType(type
))
355 throw new ArgeoException("Node " + node
356 + " exists but is of type "
357 + node
.getPrimaryNodeType().getName()
358 + " not of type " + type
);
359 // TODO: check versioning
363 StringBuffer current
= new StringBuffer("/");
364 Node currentNode
= session
.getRootNode();
365 Iterator
<String
> it
= tokenize(path
).iterator();
366 while (it
.hasNext()) {
367 String part
= it
.next();
368 current
.append(part
).append('/');
369 if (!session
.itemExists(current
.toString())) {
370 if (!it
.hasNext() && type
!= null)
371 currentNode
= currentNode
.addNode(part
, type
);
372 else if (it
.hasNext() && intermediaryNodeType
!= null)
373 currentNode
= currentNode
.addNode(part
,
374 intermediaryNodeType
);
376 currentNode
= currentNode
.addNode(part
);
378 currentNode
.addMixin(NodeType
.MIX_VERSIONABLE
);
379 if (log
.isTraceEnabled())
380 log
.debug("Added folder " + part
+ " as " + current
);
382 currentNode
= (Node
) session
.getItem(current
.toString());
386 } catch (RepositoryException e
) {
387 discardQuietly(session
);
388 throw new ArgeoException("Cannot mkdirs " + path
, e
);
393 /** Convert a path to the list of its tokens */
394 public static List
<String
> tokenize(String path
) {
395 List
<String
> tokens
= new ArrayList
<String
>();
396 boolean optimized
= false;
398 String
[] rawTokens
= path
.split("/");
399 for (String token
: rawTokens
) {
400 if (!token
.equals(""))
404 StringBuffer curr
= new StringBuffer();
405 char[] arr
= path
.toCharArray();
406 chars
: for (int i
= 0; i
< arr
.length
; i
++) {
409 if (i
== 0 || (i
== arr
.length
- 1))
411 if (curr
.length() > 0) {
412 tokens
.add(curr
.toString());
413 curr
= new StringBuffer();
418 if (curr
.length() > 0) {
419 tokens
.add(curr
.toString());
420 curr
= new StringBuffer();
423 return Collections
.unmodifiableList(tokens
);
427 * Safe and repository implementation independent registration of a
430 public static void registerNamespaceSafely(Session session
, String prefix
,
433 registerNamespaceSafely(session
.getWorkspace()
434 .getNamespaceRegistry(), prefix
, uri
);
435 } catch (RepositoryException e
) {
436 throw new ArgeoException("Cannot find namespace registry", e
);
441 * Safe and repository implementation independent registration of a
444 public static void registerNamespaceSafely(NamespaceRegistry nr
,
445 String prefix
, String uri
) {
447 String
[] prefixes
= nr
.getPrefixes();
448 for (String pref
: prefixes
)
449 if (pref
.equals(prefix
)) {
450 String registeredUri
= nr
.getURI(pref
);
451 if (!registeredUri
.equals(uri
))
452 throw new ArgeoException("Prefix " + pref
453 + " already registered for URI "
455 + " which is different from provided URI "
460 nr
.registerNamespace(prefix
, uri
);
461 } catch (RepositoryException e
) {
462 throw new ArgeoException("Cannot register namespace " + uri
463 + " under prefix " + prefix
, e
);
467 /** Recursively outputs the contents of the given node. */
468 public static void debug(Node node
) {
472 /** Recursively outputs the contents of the given node. */
473 public static void debug(Node node
, Log log
) {
475 // First output the node path
476 log
.debug(node
.getPath());
477 // Skip the virtual (and large!) jcr:system subtree
478 if (node
.getName().equals("jcr:system")) {
482 // Then the children nodes (recursive)
483 NodeIterator it
= node
.getNodes();
484 while (it
.hasNext()) {
485 Node childNode
= it
.nextNode();
486 debug(childNode
, log
);
489 // Then output the properties
490 PropertyIterator properties
= node
.getProperties();
491 // log.debug("Property are : ");
493 properties
: while (properties
.hasNext()) {
494 Property property
= properties
.nextProperty();
495 if (property
.getType() == PropertyType
.BINARY
)
496 continue properties
;// skip
497 if (property
.getDefinition().isMultiple()) {
498 // A multi-valued property, print all values
499 Value
[] values
= property
.getValues();
500 for (int i
= 0; i
< values
.length
; i
++) {
501 log
.debug(property
.getPath() + "="
502 + values
[i
].getString());
505 // A single-valued property
506 log
.debug(property
.getPath() + "=" + property
.getString());
509 } catch (Exception e
) {
510 log
.error("Could not debug " + node
, e
);
515 /** Logs the effective access control policies */
516 public static void logEffectiveAccessPolicies(Node node
) {
518 logEffectiveAccessPolicies(node
.getSession(), node
.getPath());
519 } catch (RepositoryException e
) {
520 log
.error("Cannot log effective access policies of " + node
, e
);
524 /** Logs the effective access control policies */
525 public static void logEffectiveAccessPolicies(Session session
, String path
) {
526 if (!log
.isDebugEnabled())
530 AccessControlPolicy
[] effectivePolicies
= session
531 .getAccessControlManager().getEffectivePolicies(path
);
532 if (effectivePolicies
.length
> 0) {
533 for (AccessControlPolicy policy
: effectivePolicies
) {
534 if (policy
instanceof AccessControlList
) {
535 AccessControlList acl
= (AccessControlList
) policy
;
536 log
.debug("Access control list for " + path
+ "\n"
537 + accessControlListSummary(acl
));
541 log
.debug("No effective access control policy for " + path
);
543 } catch (RepositoryException e
) {
544 log
.error("Cannot log effective access policies of " + path
, e
);
548 /** Returns a human-readable summary of this access control list. */
549 public static String
accessControlListSummary(AccessControlList acl
) {
550 StringBuffer buf
= new StringBuffer("");
552 for (AccessControlEntry ace
: acl
.getAccessControlEntries()) {
553 buf
.append('\t').append(ace
.getPrincipal().getName())
555 for (Privilege priv
: ace
.getPrivileges())
556 buf
.append("\t\t").append(priv
.getName()).append('\n');
558 return buf
.toString();
559 } catch (RepositoryException e
) {
560 throw new ArgeoException("Cannot write summary of " + acl
, e
);
565 * Copies recursively the content of a node to another one. Do NOT copy the
566 * property values of {@link NodeType#MIX_CREATED} and
567 * {@link NodeType#MIX_LAST_MODIFIED}, but update the
568 * {@link Property#JCR_LAST_MODIFIED} and
569 * {@link Property#JCR_LAST_MODIFIED_BY} properties if the target node has
570 * the {@link NodeType#MIX_LAST_MODIFIED} mixin.
572 public static void copy(Node fromNode
, Node toNode
) {
574 // process properties
575 PropertyIterator pit
= fromNode
.getProperties();
576 properties
: while (pit
.hasNext()) {
577 Property fromProperty
= pit
.nextProperty();
578 String propertyName
= fromProperty
.getName();
579 if (toNode
.hasProperty(propertyName
)
580 && toNode
.getProperty(propertyName
).getDefinition()
584 if (fromProperty
.getDefinition().isProtected())
587 if (propertyName
.equals("jcr:created")
588 || propertyName
.equals("jcr:createdBy")
589 || propertyName
.equals("jcr:lastModified")
590 || propertyName
.equals("jcr:lastModifiedBy"))
593 if (fromProperty
.isMultiple()) {
594 toNode
.setProperty(propertyName
, fromProperty
.getValues());
596 toNode
.setProperty(propertyName
, fromProperty
.getValue());
600 // update jcr:lastModified and jcr:lastModifiedBy in toNode in case
601 // they existed, before adding the mixins
602 updateLastModified(toNode
);
605 for (NodeType mixinType
: fromNode
.getMixinNodeTypes()) {
606 toNode
.addMixin(mixinType
.getName());
609 // process children nodes
610 NodeIterator nit
= fromNode
.getNodes();
611 while (nit
.hasNext()) {
612 Node fromChild
= nit
.nextNode();
613 Integer index
= fromChild
.getIndex();
614 String nodeRelPath
= fromChild
.getName() + "[" + index
+ "]";
616 if (toNode
.hasNode(nodeRelPath
))
617 toChild
= toNode
.getNode(nodeRelPath
);
619 toChild
= toNode
.addNode(fromChild
.getName(), fromChild
620 .getPrimaryNodeType().getName());
621 copy(fromChild
, toChild
);
623 } catch (RepositoryException e
) {
624 throw new ArgeoException("Cannot copy " + fromNode
+ " to "
630 * Check whether all first-level properties (except jcr:* properties) are
631 * equal. Skip jcr:* properties
633 public static Boolean
allPropertiesEquals(Node reference
, Node observed
,
634 Boolean onlyCommonProperties
) {
636 PropertyIterator pit
= reference
.getProperties();
637 props
: while (pit
.hasNext()) {
638 Property propReference
= pit
.nextProperty();
639 String propName
= propReference
.getName();
640 if (propName
.startsWith("jcr:"))
643 if (!observed
.hasProperty(propName
))
644 if (onlyCommonProperties
)
648 // TODO: deal with multiple property values?
649 if (!observed
.getProperty(propName
).getValue()
650 .equals(propReference
.getValue()))
654 } catch (RepositoryException e
) {
655 throw new ArgeoException("Cannot check all properties equals of "
656 + reference
+ " and " + observed
, e
);
660 public static Map
<String
, PropertyDiff
> diffProperties(Node reference
,
662 Map
<String
, PropertyDiff
> diffs
= new TreeMap
<String
, PropertyDiff
>();
663 diffPropertiesLevel(diffs
, null, reference
, observed
);
668 * Compare the properties of two nodes. Recursivity to child nodes is not
669 * yet supported. Skip jcr:* properties.
671 static void diffPropertiesLevel(Map
<String
, PropertyDiff
> diffs
,
672 String baseRelPath
, Node reference
, Node observed
) {
674 // check removed and modified
675 PropertyIterator pit
= reference
.getProperties();
676 props
: while (pit
.hasNext()) {
677 Property p
= pit
.nextProperty();
678 String name
= p
.getName();
679 if (name
.startsWith("jcr:"))
682 if (!observed
.hasProperty(name
)) {
683 String relPath
= propertyRelPath(baseRelPath
, name
);
684 PropertyDiff pDiff
= new PropertyDiff(PropertyDiff
.REMOVED
,
685 relPath
, p
.getValue(), null);
686 diffs
.put(relPath
, pDiff
);
688 if (p
.isMultiple()) {
689 // FIXME implement multiple
691 Value referenceValue
= p
.getValue();
692 Value newValue
= observed
.getProperty(name
).getValue();
693 if (!referenceValue
.equals(newValue
)) {
694 String relPath
= propertyRelPath(baseRelPath
, name
);
695 PropertyDiff pDiff
= new PropertyDiff(
696 PropertyDiff
.MODIFIED
, relPath
,
697 referenceValue
, newValue
);
698 diffs
.put(relPath
, pDiff
);
704 pit
= observed
.getProperties();
705 props
: while (pit
.hasNext()) {
706 Property p
= pit
.nextProperty();
707 String name
= p
.getName();
708 if (name
.startsWith("jcr:"))
710 if (!reference
.hasProperty(name
)) {
711 if (p
.isMultiple()) {
712 // FIXME implement multiple
714 String relPath
= propertyRelPath(baseRelPath
, name
);
715 PropertyDiff pDiff
= new PropertyDiff(
716 PropertyDiff
.ADDED
, relPath
, null, p
.getValue());
717 diffs
.put(relPath
, pDiff
);
721 } catch (RepositoryException e
) {
722 throw new ArgeoException("Cannot diff " + reference
+ " and "
728 * Compare only a restricted list of properties of two nodes. No
732 public static Map
<String
, PropertyDiff
> diffProperties(Node reference
,
733 Node observed
, List
<String
> properties
) {
734 Map
<String
, PropertyDiff
> diffs
= new TreeMap
<String
, PropertyDiff
>();
736 Iterator
<String
> pit
= properties
.iterator();
738 props
: while (pit
.hasNext()) {
739 String name
= pit
.next();
740 if (!reference
.hasProperty(name
)) {
741 if (!observed
.hasProperty(name
))
743 Value val
= observed
.getProperty(name
).getValue();
745 // empty String but not null
746 if ("".equals(val
.getString()))
748 } catch (Exception e
) {
749 // not parseable as String, silent
751 PropertyDiff pDiff
= new PropertyDiff(PropertyDiff
.ADDED
,
753 diffs
.put(name
, pDiff
);
754 } else if (!observed
.hasProperty(name
)) {
755 PropertyDiff pDiff
= new PropertyDiff(PropertyDiff
.REMOVED
,
756 name
, reference
.getProperty(name
).getValue(), null);
757 diffs
.put(name
, pDiff
);
759 Value referenceValue
= reference
.getProperty(name
)
761 Value newValue
= observed
.getProperty(name
).getValue();
762 if (!referenceValue
.equals(newValue
)) {
763 PropertyDiff pDiff
= new PropertyDiff(
764 PropertyDiff
.MODIFIED
, name
, referenceValue
,
766 diffs
.put(name
, pDiff
);
770 } catch (RepositoryException e
) {
771 throw new ArgeoException("Cannot diff " + reference
+ " and "
777 /** Builds a property relPath to be used in the diff. */
778 private static String
propertyRelPath(String baseRelPath
,
779 String propertyName
) {
780 if (baseRelPath
== null)
783 return baseRelPath
+ '/' + propertyName
;
787 * Normalizes a name so that it can be stored in contexts not supporting
788 * names with ':' (typically databases). Replaces ':' by '_'.
790 public static String
normalize(String name
) {
791 return name
.replace(':', '_');
795 * Replaces characters which are invalid in a JCR name by '_'. Currently not
798 * @see JcrUtils#INVALID_NAME_CHARACTERS
800 public static String
replaceInvalidChars(String name
) {
801 return replaceInvalidChars(name
, '_');
805 * Replaces characters which are invalid in a JCR name. Currently not
808 * @see JcrUtils#INVALID_NAME_CHARACTERS
810 public static String
replaceInvalidChars(String name
, char replacement
) {
811 boolean modified
= false;
812 char[] arr
= name
.toCharArray();
813 for (int i
= 0; i
< arr
.length
; i
++) {
815 invalid
: for (char invalid
: INVALID_NAME_CHARACTERS
) {
817 arr
[i
] = replacement
;
824 return new String(arr
);
826 // do not create new object if unnecessary
831 * Removes forbidden characters from a path, replacing them with '_'
833 * @deprecated use {@link #replaceInvalidChars(String)} instead
835 public static String
removeForbiddenCharacters(String str
) {
836 return str
.replace('[', '_').replace(']', '_').replace('/', '_')
841 /** Cleanly disposes a {@link Binary} even if it is null. */
842 public static void closeQuietly(Binary binary
) {
848 /** Retrieve a {@link Binary} as a byte array */
849 public static byte[] getBinaryAsBytes(Property property
) {
850 ByteArrayOutputStream out
= new ByteArrayOutputStream();
851 InputStream in
= null;
852 Binary binary
= null;
854 binary
= property
.getBinary();
855 in
= binary
.getStream();
856 IOUtils
.copy(in
, out
);
857 return out
.toByteArray();
858 } catch (Exception e
) {
859 throw new ArgeoException("Cannot read binary " + property
862 IOUtils
.closeQuietly(out
);
863 IOUtils
.closeQuietly(in
);
864 closeQuietly(binary
);
868 /** Writes a {@link Binary} from a byte array */
869 public static void setBinaryAsBytes(Node node
, String property
, byte[] bytes
) {
870 InputStream in
= null;
871 Binary binary
= null;
873 in
= new ByteArrayInputStream(bytes
);
874 binary
= node
.getSession().getValueFactory().createBinary(in
);
875 node
.setProperty(property
, binary
);
876 } catch (Exception e
) {
877 throw new ArgeoException("Cannot read binary " + property
880 IOUtils
.closeQuietly(in
);
881 closeQuietly(binary
);
886 * Creates depth from a string (typically a username) by adding levels based
887 * on its first characters: "aBcD",2 => a/aB
889 public static String
firstCharsToPath(String str
, Integer nbrOfChars
) {
890 if (str
.length() < nbrOfChars
)
891 throw new ArgeoException("String " + str
892 + " length must be greater or equal than " + nbrOfChars
);
893 StringBuffer path
= new StringBuffer("");
894 StringBuffer curr
= new StringBuffer("");
895 for (int i
= 0; i
< nbrOfChars
; i
++) {
896 curr
.append(str
.charAt(i
));
898 if (i
< nbrOfChars
- 1)
901 return path
.toString();
905 * Wraps the call to the repository factory based on parameter
906 * {@link ArgeoJcrConstants#JCR_REPOSITORY_ALIAS} in order to simplify it
907 * and protect against future API changes.
909 public static Repository
getRepositoryByAlias(
910 RepositoryFactory repositoryFactory
, String alias
) {
912 Map
<String
, String
> parameters
= new HashMap
<String
, String
>();
913 parameters
.put(JCR_REPOSITORY_ALIAS
, alias
);
914 return repositoryFactory
.getRepository(parameters
);
915 } catch (RepositoryException e
) {
916 throw new ArgeoException(
917 "Unexpected exception when trying to retrieve repository with alias "
923 * Wraps the call to the repository factory based on parameter
924 * {@link ArgeoJcrConstants#JCR_REPOSITORY_URI} in order to simplify it and
925 * protect against future API changes.
927 public static Repository
getRepositoryByUri(
928 RepositoryFactory repositoryFactory
, String uri
) {
930 Map
<String
, String
> parameters
= new HashMap
<String
, String
>();
931 parameters
.put(JCR_REPOSITORY_URI
, uri
);
932 return repositoryFactory
.getRepository(parameters
);
933 } catch (RepositoryException e
) {
934 throw new ArgeoException(
935 "Unexpected exception when trying to retrieve repository with uri "
941 * Discards the current changes in the session attached to this node. To be
942 * used typically in a catch block.
944 * @see #discardQuietly(Session)
946 public static void discardUnderlyingSessionQuietly(Node node
) {
948 discardQuietly(node
.getSession());
949 } catch (RepositoryException e
) {
950 log
.warn("Cannot quietly discard session of node " + node
+ ": "
956 * Discards the current changes in a session by calling
957 * {@link Session#refresh(boolean)} with <code>false</code>, only logging
958 * potential errors when doing so. To be used typically in a catch block.
960 public static void discardQuietly(Session session
) {
963 session
.refresh(false);
964 } catch (RepositoryException e
) {
965 log
.warn("Cannot quietly discard session " + session
+ ": "
970 /** Logs out the session, not throwing any exception, even if it is null. */
971 public static void logoutQuietly(Session session
) {
974 if (session
.isLive())
976 } catch (Exception e
) {
982 * Convenient method to add a listener. uuids passed as null, deep=true,
983 * local=true, only one node type
985 public static void addListener(Session session
, EventListener listener
,
986 int eventTypes
, String basePath
, String nodeType
) {
988 session
.getWorkspace()
989 .getObservationManager()
990 .addEventListener(listener
, eventTypes
, basePath
, true,
991 null, new String
[] { nodeType
}, true);
992 } catch (RepositoryException e
) {
993 throw new ArgeoException("Cannot add JCR listener " + listener
994 + " to session " + session
, e
);
998 /** Removes a listener without throwing exception */
999 public static void removeListenerQuietly(Session session
,
1000 EventListener listener
) {
1001 if (session
== null || !session
.isLive())
1004 session
.getWorkspace().getObservationManager()
1005 .removeEventListener(listener
);
1006 } catch (RepositoryException e
) {
1011 /** Returns the home node of the session user or null if none was found. */
1012 public static Node
getUserHome(Session session
) {
1013 String userID
= session
.getUserID();
1014 return getUserHome(session
, userID
);
1017 /** User home path is NOT configurable */
1018 public static String
getUserHomePath(String username
) {
1019 String homeBasePath
= DEFAULT_HOME_BASE_PATH
;
1020 return homeBasePath
+ '/' + firstCharsToPath(username
, 2) + '/'
1025 * Returns the home node of the session user or null if none was found.
1028 * the session to use in order to perform the search, this can be
1029 * a session with a different user ID than the one searched,
1030 * typically when a system or admin session is used.
1032 * the username of the user
1034 public static Node
getUserHome(Session session
, String username
) {
1036 String homePath
= getUserHomePath(username
);
1037 return session
.itemExists(homePath
) ? session
.getNode(homePath
)
1039 // kept for example of QOM queries
1040 // QueryObjectModelFactory qomf = session.getWorkspace()
1041 // .getQueryManager().getQOMFactory();
1042 // Selector userHomeSel = qomf.selector(ArgeoTypes.ARGEO_USER_HOME,
1044 // DynamicOperand userIdDop = qomf.propertyValue("userHome",
1045 // ArgeoNames.ARGEO_USER_ID);
1046 // StaticOperand userIdSop = qomf.literal(session.getValueFactory()
1047 // .createValue(username));
1048 // Constraint constraint = qomf.comparison(userIdDop,
1049 // QueryObjectModelFactory.JCR_OPERATOR_EQUAL_TO, userIdSop);
1050 // Query query = qomf.createQuery(userHomeSel, constraint, null,
1052 // Node userHome = JcrUtils.querySingleNode(query);
1053 } catch (RepositoryException e
) {
1054 throw new ArgeoException("Cannot find home for user " + username
, e
);
1059 * Creates an Argeo user home, does nothing if it already exists. Session is
1062 public static Node
createUserHomeIfNeeded(Session session
, String username
) {
1064 String homePath
= getUserHomePath(username
);
1065 if (session
.itemExists(homePath
))
1066 return session
.getNode(homePath
);
1068 Node userHome
= JcrUtils
.mkdirs(session
, homePath
);
1069 userHome
.addMixin(ArgeoTypes
.ARGEO_USER_HOME
);
1070 userHome
.setProperty(ArgeoNames
.ARGEO_USER_ID
, username
);
1073 } catch (RepositoryException e
) {
1074 discardQuietly(session
);
1075 throw new ArgeoException("Cannot create home for " + username
1076 + " in workspace " + session
.getWorkspace().getName(), e
);
1081 * Creates a user profile in the home of this user. Creates the home if
1082 * needed, but throw an exception if a profile already exists. The session
1083 * is not saved and the node is in a checkedOut state (that is, it requires
1084 * a subsequent checkin after saving the session).
1086 public static Node
createUserProfile(Session session
, String username
) {
1088 Node userHome
= createUserHomeIfNeeded(session
, username
);
1089 if (userHome
.hasNode(ArgeoNames
.ARGEO_PROFILE
))
1090 throw new ArgeoException(
1091 "There is already a user profile under " + userHome
);
1092 Node userProfile
= userHome
.addNode(ArgeoNames
.ARGEO_PROFILE
);
1093 userProfile
.addMixin(ArgeoTypes
.ARGEO_USER_PROFILE
);
1094 userProfile
.setProperty(ArgeoNames
.ARGEO_USER_ID
, username
);
1095 userProfile
.setProperty(ArgeoNames
.ARGEO_ENABLED
, true);
1096 userProfile
.setProperty(ArgeoNames
.ARGEO_ACCOUNT_NON_EXPIRED
, true);
1097 userProfile
.setProperty(ArgeoNames
.ARGEO_ACCOUNT_NON_LOCKED
, true);
1098 userProfile
.setProperty(ArgeoNames
.ARGEO_CREDENTIALS_NON_EXPIRED
,
1101 } catch (RepositoryException e
) {
1102 discardQuietly(session
);
1103 throw new ArgeoException("Cannot create user profile for "
1104 + username
+ " in workspace "
1105 + session
.getWorkspace().getName(), e
);
1110 * Create user profile if needed, the session IS saved.
1112 * @return the user profile
1114 public static Node
createUserProfileIfNeeded(Session securitySession
,
1117 Node userHome
= JcrUtils
.createUserHomeIfNeeded(securitySession
,
1119 Node userProfile
= userHome
.hasNode(ArgeoNames
.ARGEO_PROFILE
) ? userHome
1120 .getNode(ArgeoNames
.ARGEO_PROFILE
) : JcrUtils
1121 .createUserProfile(securitySession
, username
);
1122 if (securitySession
.hasPendingChanges())
1123 securitySession
.save();
1124 VersionManager versionManager
= securitySession
.getWorkspace()
1125 .getVersionManager();
1126 if (versionManager
.isCheckedOut(userProfile
.getPath()))
1127 versionManager
.checkin(userProfile
.getPath());
1129 } catch (RepositoryException e
) {
1130 discardQuietly(securitySession
);
1131 throw new ArgeoException("Cannot create user profile for "
1132 + username
+ " in workspace "
1133 + securitySession
.getWorkspace().getName(), e
);
1137 /** Creates an Argeo user home. */
1138 // public static Node createUserHome(Session session, String homeBasePath,
1139 // String username) {
1141 // if (session == null)
1142 // throw new ArgeoException("Session is null");
1143 // if (session.hasPendingChanges())
1144 // throw new ArgeoException(
1145 // "Session has pending changes, save them first");
1147 // String homePath = getUserHomePath(username);
1149 // if (session.itemExists(homePath)) {
1151 // throw new ArgeoException(
1152 // "Trying to create a user home that already exists");
1153 // } catch (Exception e) {
1154 // // we use this workaround to be sure to get the stack trace
1155 // // to identify the sink of the bug.
1156 // log.warn("trying to create an already existing userHome at path:"
1157 // + homePath + ". Stack trace : ");
1158 // e.printStackTrace();
1162 // Node userHome = JcrUtils.mkdirs(session, homePath);
1163 // Node userProfile;
1164 // if (userHome.hasNode(ArgeoNames.ARGEO_PROFILE)) {
1165 // log.warn("userProfile node already exists for userHome path: "
1166 // + homePath + ". We do not add a new one");
1168 // userProfile = userHome.addNode(ArgeoNames.ARGEO_PROFILE);
1169 // userProfile.addMixin(ArgeoTypes.ARGEO_USER_PROFILE);
1170 // // session.getWorkspace().getVersionManager()
1171 // // .checkout(userProfile.getPath());
1172 // userProfile.setProperty(ArgeoNames.ARGEO_USER_ID, username);
1174 // session.getWorkspace().getVersionManager()
1175 // .checkin(userProfile.getPath());
1176 // // we need to save the profile before adding the user home type
1178 // userHome.addMixin(ArgeoTypes.ARGEO_USER_HOME);
1181 // http://jackrabbit.510166.n4.nabble.com/Jackrabbit-2-0-beta-6-Problem-adding-a-Mixin-type-with-mandatory-properties-after-setting-propertiesn-td1290332.html
1182 // userHome.setProperty(ArgeoNames.ARGEO_USER_ID, username);
1185 // } catch (RepositoryException e) {
1186 // discardQuietly(session);
1187 // throw new ArgeoException("Cannot create home node for user "
1193 * Returns user home has path, embedding exceptions. Contrary to
1194 * {@link #getUserHome(Session)}, it never returns null but throws and
1195 * exception if not found.
1197 * @deprecated use getUserHome() instead, throwing an exception if it
1201 public static String
getUserHomePath(Session session
) {
1202 String userID
= session
.getUserID();
1204 String homePath
= getUserHomePath(userID
);
1205 if (session
.itemExists(homePath
))
1208 throw new ArgeoException("No home registered for " + userID
);
1209 } catch (RepositoryException e
) {
1210 throw new ArgeoException("Cannot find user home path", e
);
1215 * @return null if not found *
1217 public static Node
getUserProfile(Session session
, String username
) {
1219 Node userHome
= getUserHome(session
, username
);
1220 if (userHome
== null)
1222 if (userHome
.hasNode(ArgeoNames
.ARGEO_PROFILE
))
1223 return userHome
.getNode(ArgeoNames
.ARGEO_PROFILE
);
1226 } catch (RepositoryException e
) {
1227 throw new ArgeoException(
1228 "Cannot find profile for user " + username
, e
);
1233 * Get the profile of the user attached to this session.
1235 public static Node
getUserProfile(Session session
) {
1236 String userID
= session
.getUserID();
1237 return getUserProfile(session
, userID
);
1241 * Quietly unregisters an {@link EventListener} from the udnerlying
1242 * workspace of this node.
1244 public static void unregisterQuietly(Node node
, EventListener eventListener
) {
1246 unregisterQuietly(node
.getSession().getWorkspace(), eventListener
);
1247 } catch (RepositoryException e
) {
1249 if (log
.isTraceEnabled())
1250 log
.trace("Could not unregister event listener "
1255 /** Quietly unregisters an {@link EventListener} from this workspace */
1256 public static void unregisterQuietly(Workspace workspace
,
1257 EventListener eventListener
) {
1258 if (eventListener
== null)
1261 workspace
.getObservationManager()
1262 .removeEventListener(eventListener
);
1263 } catch (RepositoryException e
) {
1265 if (log
.isTraceEnabled())
1266 log
.trace("Could not unregister event listener "
1272 * If this node is has the {@link NodeType#MIX_LAST_MODIFIED} mixin, it
1273 * updates the {@link Property#JCR_LAST_MODIFIED} property with the current
1274 * time and the {@link Property#JCR_LAST_MODIFIED_BY} property with the
1275 * underlying session user id. In Jackrabbit 2.x, <a
1276 * href="https://issues.apache.org/jira/browse/JCR-2233">these properties
1277 * are not automatically updated</a>, hence the need for manual update. The
1278 * session is not saved.
1280 public static void updateLastModified(Node node
) {
1282 if (!node
.isNodeType(NodeType
.MIX_LAST_MODIFIED
))
1283 node
.addMixin(NodeType
.MIX_LAST_MODIFIED
);
1284 node
.setProperty(Property
.JCR_LAST_MODIFIED
,
1285 new GregorianCalendar());
1286 node
.setProperty(Property
.JCR_LAST_MODIFIED_BY
, node
.getSession()
1288 } catch (RepositoryException e
) {
1289 throw new ArgeoException("Cannot update last modified on " + node
,
1294 /** Update lastModified recursively until this parent. */
1295 public static void updateLastModifiedAndParents(Node node
, String untilPath
) {
1297 if (!node
.getPath().startsWith(untilPath
))
1298 throw new ArgeoException(node
+ " is not under " + untilPath
);
1299 updateLastModified(node
);
1300 if (!node
.getPath().equals(untilPath
))
1301 updateLastModifiedAndParents(node
.getParent(), untilPath
);
1302 } catch (RepositoryException e
) {
1303 throw new ArgeoException("Cannot update lastModified from " + node
1304 + " until " + untilPath
, e
);
1309 * Returns a String representing the short version (see <a
1310 * href="http://jackrabbit.apache.org/node-type-notation.html"> Node type
1311 * Notation </a> attributes grammar) of the main business attributes of this
1312 * property definition
1316 public static String
getPropertyDefinitionAsString(Property prop
) {
1317 StringBuffer sbuf
= new StringBuffer();
1319 if (prop
.getDefinition().isAutoCreated())
1321 if (prop
.getDefinition().isMandatory())
1323 if (prop
.getDefinition().isProtected())
1325 if (prop
.getDefinition().isMultiple())
1327 } catch (RepositoryException re
) {
1328 throw new ArgeoException(
1329 "unexpected error while getting property definition as String",
1332 return sbuf
.toString();
1336 * Estimate the sub tree size from current node. Computation is based on the
1337 * Jcr {@link Property.getLength()} method. Note : it is not the exact size
1338 * used on the disk by the current part of the JCR Tree.
1341 public static long getNodeApproxSize(Node node
) {
1342 long curNodeSize
= 0;
1344 PropertyIterator pi
= node
.getProperties();
1345 while (pi
.hasNext()) {
1346 Property prop
= pi
.nextProperty();
1347 if (prop
.isMultiple()) {
1348 int nb
= prop
.getLengths().length
;
1349 for (int i
= 0; i
< nb
; i
++) {
1350 curNodeSize
+= (prop
.getLengths()[i
] > 0 ? prop
1351 .getLengths()[i
] : 0);
1354 curNodeSize
+= (prop
.getLength() > 0 ? prop
.getLength() : 0);
1357 NodeIterator ni
= node
.getNodes();
1358 while (ni
.hasNext())
1359 curNodeSize
+= getNodeApproxSize(ni
.nextNode());
1361 } catch (RepositoryException re
) {
1362 throw new ArgeoException(
1363 "Unexpected error while recursively determining node size.",
1373 * Convenience method for adding a single privilege to a principal (user or
1374 * role), typically jcr:all
1376 public static void addPrivilege(Session session
, String path
,
1377 String principal
, String privilege
) throws RepositoryException
{
1378 List
<Privilege
> privileges
= new ArrayList
<Privilege
>();
1379 privileges
.add(session
.getAccessControlManager().privilegeFromName(
1381 addPrivileges(session
, path
, new SimplePrincipal(principal
), privileges
);
1385 * Add privileges on a path to a {@link Principal}. The path must already
1388 public static void addPrivileges(Session session
, String path
,
1389 Principal principal
, List
<Privilege
> privs
)
1390 throws RepositoryException
{
1391 AccessControlManager acm
= session
.getAccessControlManager();
1392 // search for an access control list
1393 AccessControlList acl
= null;
1394 AccessControlPolicyIterator policyIterator
= acm
1395 .getApplicablePolicies(path
);
1396 if (policyIterator
.hasNext()) {
1397 while (policyIterator
.hasNext()) {
1398 AccessControlPolicy acp
= policyIterator
1399 .nextAccessControlPolicy();
1400 if (acp
instanceof AccessControlList
)
1401 acl
= ((AccessControlList
) acp
);
1404 AccessControlPolicy
[] existingPolicies
= acm
.getPolicies(path
);
1405 for (AccessControlPolicy acp
: existingPolicies
) {
1406 if (acp
instanceof AccessControlList
)
1407 acl
= ((AccessControlList
) acp
);
1412 acl
.addAccessControlEntry(principal
,
1413 privs
.toArray(new Privilege
[privs
.size()]));
1414 acm
.setPolicy(path
, acl
);
1415 if (log
.isDebugEnabled())
1416 log
.debug("Added privileges " + privs
+ " to " + principal
1419 throw new ArgeoException("Don't know how to apply privileges "
1420 + privs
+ " to " + principal
+ " on " + path
);