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
;
21 import java
.io
.FileInputStream
;
22 import java
.io
.IOException
;
23 import java
.io
.InputStream
;
24 import java
.net
.MalformedURLException
;
26 import java
.security
.Principal
;
27 import java
.text
.DateFormat
;
28 import java
.text
.ParseException
;
29 import java
.util
.ArrayList
;
30 import java
.util
.Calendar
;
31 import java
.util
.Collections
;
32 import java
.util
.Date
;
33 import java
.util
.GregorianCalendar
;
34 import java
.util
.Iterator
;
35 import java
.util
.List
;
37 import java
.util
.TreeMap
;
39 import javax
.jcr
.Binary
;
40 import javax
.jcr
.NamespaceRegistry
;
41 import javax
.jcr
.NoSuchWorkspaceException
;
42 import javax
.jcr
.Node
;
43 import javax
.jcr
.NodeIterator
;
44 import javax
.jcr
.Property
;
45 import javax
.jcr
.PropertyIterator
;
46 import javax
.jcr
.PropertyType
;
47 import javax
.jcr
.Repository
;
48 import javax
.jcr
.RepositoryException
;
49 import javax
.jcr
.Session
;
50 import javax
.jcr
.Value
;
51 import javax
.jcr
.Workspace
;
52 import javax
.jcr
.nodetype
.NodeType
;
53 import javax
.jcr
.observation
.EventListener
;
54 import javax
.jcr
.query
.Query
;
55 import javax
.jcr
.query
.QueryResult
;
56 import javax
.jcr
.security
.AccessControlEntry
;
57 import javax
.jcr
.security
.AccessControlList
;
58 import javax
.jcr
.security
.AccessControlManager
;
59 import javax
.jcr
.security
.AccessControlPolicy
;
60 import javax
.jcr
.security
.AccessControlPolicyIterator
;
61 import javax
.jcr
.security
.Privilege
;
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
.DigestUtils
;
68 import org
.argeo
.util
.security
.SimplePrincipal
;
70 /** Utility methods to simplify common JCR operations. */
71 public class JcrUtils
implements ArgeoJcrConstants
{
73 private final static Log log
= LogFactory
.getLog(JcrUtils
.class);
76 * Not complete yet. See
77 * http://www.day.com/specs/jcr/2.0/3_Repository_Model.html#3.2.2%20Local
80 public final static char[] INVALID_NAME_CHARACTERS
= { '/', ':', '[', ']',
86 /** Prevents instantiation */
91 * Queries one single node.
93 * @return one single node or null if none was found
94 * @throws ArgeoException
95 * if more than one node was found
97 public static Node
querySingleNode(Query query
) {
98 NodeIterator nodeIterator
;
100 QueryResult queryResult
= query
.execute();
101 nodeIterator
= queryResult
.getNodes();
102 } catch (RepositoryException e
) {
103 throw new ArgeoException("Cannot execute query " + query
, e
);
106 if (nodeIterator
.hasNext())
107 node
= nodeIterator
.nextNode();
111 if (nodeIterator
.hasNext())
112 throw new ArgeoException("Query returned more than one node.");
116 /** Retrieves the parent path of the provided path */
117 public static String
parentPath(String path
) {
118 if (path
.equals("/"))
119 throw new ArgeoException("Root path '/' has no parent path");
120 if (path
.charAt(0) != '/')
121 throw new ArgeoException("Path " + path
+ " must start with a '/'");
123 if (pathT
.charAt(pathT
.length() - 1) == '/')
124 pathT
= pathT
.substring(0, pathT
.length() - 2);
126 int index
= pathT
.lastIndexOf('/');
127 return pathT
.substring(0, index
);
130 /** The provided data as a path ('/' at the end, not the beginning) */
131 public static String
dateAsPath(Calendar cal
) {
132 return dateAsPath(cal
, false);
136 * Creates a deep path based on a URL:
137 * http://subdomain.example.com/to/content?args =>
138 * com/example/subdomain/to/content
140 public static String
urlAsPath(String url
) {
142 URL u
= new URL(url
);
143 StringBuffer path
= new StringBuffer(url
.length());
145 path
.append(hostAsPath(u
.getHost()));
146 // we don't put port since it may not always be there and may change
147 path
.append(u
.getPath());
148 return path
.toString();
149 } catch (MalformedURLException e
) {
150 throw new ArgeoException("Cannot generate URL path for " + url
, e
);
154 /** Set the {@link NodeType#NT_ADDRESS} properties based on this URL. */
155 public static void urlToAddressProperties(Node node
, String url
) {
157 URL u
= new URL(url
);
158 node
.setProperty(Property
.JCR_PROTOCOL
, u
.getProtocol());
159 node
.setProperty(Property
.JCR_HOST
, u
.getHost());
160 node
.setProperty(Property
.JCR_PORT
, Integer
.toString(u
.getPort()));
161 node
.setProperty(Property
.JCR_PATH
, normalizePath(u
.getPath()));
162 } catch (Exception e
) {
163 throw new ArgeoException("Cannot set URL " + url
164 + " as nt:address properties", e
);
168 /** Build URL based on the {@link NodeType#NT_ADDRESS} properties. */
169 public static String
urlFromAddressProperties(Node node
) {
172 node
.getProperty(Property
.JCR_PROTOCOL
).getString(), node
173 .getProperty(Property
.JCR_HOST
).getString(),
174 (int) node
.getProperty(Property
.JCR_PORT
).getLong(), node
175 .getProperty(Property
.JCR_PATH
).getString());
177 } catch (Exception e
) {
178 throw new ArgeoException(
179 "Cannot get URL from nt:address properties of " + node
, e
);
183 /** Make sure that: starts with '/', do not end with '/', do not have '//' */
184 public static String
normalizePath(String path
) {
185 List
<String
> tokens
= tokenize(path
);
186 StringBuffer buf
= new StringBuffer(path
.length());
187 for (String token
: tokens
) {
191 return buf
.toString();
195 * Creates a path from a FQDN, inverting the order of the component:
196 * www.argeo.org => org.argeo.www
198 public static String
hostAsPath(String host
) {
199 StringBuffer path
= new StringBuffer(host
.length());
200 String
[] hostTokens
= host
.split("\\.");
201 for (int i
= hostTokens
.length
- 1; i
>= 0; i
--) {
202 path
.append(hostTokens
[i
]);
206 return path
.toString();
210 * The provided data as a path ('/' at the end, not the beginning)
215 * whether to add hour as well
217 public static String
dateAsPath(Calendar cal
, Boolean addHour
) {
218 StringBuffer buf
= new StringBuffer(14);
220 buf
.append(cal
.get(Calendar
.YEAR
));
223 int month
= cal
.get(Calendar
.MONTH
) + 1;
230 int day
= cal
.get(Calendar
.DAY_OF_MONTH
);
238 int hour
= cal
.get(Calendar
.HOUR_OF_DAY
);
245 return buf
.toString();
249 /** Converts in one call a string into a gregorian calendar. */
250 public static Calendar
parseCalendar(DateFormat dateFormat
, String value
) {
252 Date date
= dateFormat
.parse(value
);
253 Calendar calendar
= new GregorianCalendar();
254 calendar
.setTime(date
);
256 } catch (ParseException e
) {
257 throw new ArgeoException("Cannot parse " + value
258 + " with date format " + dateFormat
, e
);
263 /** The last element of a path. */
264 public static String
lastPathElement(String path
) {
265 if (path
.charAt(path
.length() - 1) == '/')
266 throw new ArgeoException("Path " + path
+ " cannot end with '/'");
267 int index
= path
.lastIndexOf('/');
269 throw new ArgeoException("Cannot find last path element for "
271 return path
.substring(index
+ 1);
275 * Routine that get the child with this name, adding id it does not already
278 public static Node
getOrAdd(Node parent
, String childName
,
279 String childPrimaryNodeType
) throws RepositoryException
{
280 return parent
.hasNode(childName
) ? parent
.getNode(childName
) : parent
281 .addNode(childName
, childPrimaryNodeType
);
285 * Routine that get the child with this name, adding id it does not already
288 public static Node
getOrAdd(Node parent
, String childName
)
289 throws RepositoryException
{
290 return parent
.hasNode(childName
) ? parent
.getNode(childName
) : parent
294 /** Convert a {@link NodeIterator} to a list of {@link Node} */
295 public static List
<Node
> nodeIteratorToList(NodeIterator nodeIterator
) {
296 List
<Node
> nodes
= new ArrayList
<Node
>();
297 while (nodeIterator
.hasNext()) {
298 nodes
.add(nodeIterator
.nextNode());
308 * Concisely get the string value of a property or null if this node doesn't
311 public static String
get(Node node
, String propertyName
) {
313 if (!node
.hasProperty(propertyName
))
315 return node
.getProperty(propertyName
).getString();
316 } catch (RepositoryException e
) {
317 throw new ArgeoException("Cannot get property " + propertyName
322 /** Concisely get the boolean value of a property */
323 public static Boolean
check(Node node
, String propertyName
) {
325 return node
.getProperty(propertyName
).getBoolean();
326 } catch (RepositoryException e
) {
327 throw new ArgeoException("Cannot get property " + propertyName
332 /** Concisely get the bytes array value of a property */
333 public static byte[] getBytes(Node node
, String propertyName
) {
335 return getBinaryAsBytes(node
.getProperty(propertyName
));
336 } catch (RepositoryException e
) {
337 throw new ArgeoException("Cannot get property " + propertyName
342 /** Creates the nodes making path, if they don't exist. */
343 public static Node
mkdirs(Session session
, String path
) {
344 return mkdirs(session
, path
, null, null, false);
348 * use {@link #mkdirs(Session, String, String, String, Boolean)} instead.
353 public static Node
mkdirs(Session session
, String path
, String type
,
354 Boolean versioning
) {
355 return mkdirs(session
, path
, type
, type
, false);
360 * the type of the leaf node
362 public static Node
mkdirs(Session session
, String path
, String type
) {
363 return mkdirs(session
, path
, type
, null, false);
367 * Synchronized and save is performed, to avoid race conditions in
368 * initializers leading to duplicate nodes.
370 public synchronized static Node
mkdirsSafe(Session session
, String path
,
373 if (session
.hasPendingChanges())
374 throw new ArgeoException(
375 "Session has pending changes, save them first.");
376 Node node
= mkdirs(session
, path
, type
);
379 } catch (RepositoryException e
) {
380 discardQuietly(session
);
381 throw new ArgeoException("Cannot safely make directories", e
);
385 public synchronized static Node
mkdirsSafe(Session session
, String path
) {
386 return mkdirsSafe(session
, path
, null);
390 * Creates the nodes making the path as {@link NodeType#NT_FOLDER}
392 public static Node
mkfolders(Session session
, String path
) {
393 return mkdirs(session
, path
, NodeType
.NT_FOLDER
, NodeType
.NT_FOLDER
,
398 * Creates the nodes making path, if they don't exist. This is up to the
399 * caller to save the session. Use with caution since it can create
400 * duplicate nodes if used concurrently.
402 public static Node
mkdirs(Session session
, String path
, String type
,
403 String intermediaryNodeType
, Boolean versioning
) {
405 if (path
.equals('/'))
406 return session
.getRootNode();
408 if (session
.itemExists(path
)) {
409 Node node
= session
.getNode(path
);
411 if (type
!= null && !node
.isNodeType(type
)
412 && !node
.getPath().equals("/"))
413 throw new ArgeoException("Node " + node
414 + " exists but is of type "
415 + node
.getPrimaryNodeType().getName()
416 + " not of type " + type
);
417 // TODO: check versioning
421 StringBuffer current
= new StringBuffer("/");
422 Node currentNode
= session
.getRootNode();
423 Iterator
<String
> it
= tokenize(path
).iterator();
424 while (it
.hasNext()) {
425 String part
= it
.next();
426 current
.append(part
).append('/');
427 if (!session
.itemExists(current
.toString())) {
428 if (!it
.hasNext() && type
!= null)
429 currentNode
= currentNode
.addNode(part
, type
);
430 else if (it
.hasNext() && intermediaryNodeType
!= null)
431 currentNode
= currentNode
.addNode(part
,
432 intermediaryNodeType
);
434 currentNode
= currentNode
.addNode(part
);
436 currentNode
.addMixin(NodeType
.MIX_VERSIONABLE
);
437 if (log
.isTraceEnabled())
438 log
.debug("Added folder " + part
+ " as " + current
);
440 currentNode
= (Node
) session
.getItem(current
.toString());
444 } catch (RepositoryException e
) {
445 discardQuietly(session
);
446 throw new ArgeoException("Cannot mkdirs " + path
, e
);
451 /** Convert a path to the list of its tokens */
452 public static List
<String
> tokenize(String path
) {
453 List
<String
> tokens
= new ArrayList
<String
>();
454 boolean optimized
= false;
456 String
[] rawTokens
= path
.split("/");
457 for (String token
: rawTokens
) {
458 if (!token
.equals(""))
462 StringBuffer curr
= new StringBuffer();
463 char[] arr
= path
.toCharArray();
464 chars
: for (int i
= 0; i
< arr
.length
; i
++) {
467 if (i
== 0 || (i
== arr
.length
- 1))
469 if (curr
.length() > 0) {
470 tokens
.add(curr
.toString());
471 curr
= new StringBuffer();
476 if (curr
.length() > 0) {
477 tokens
.add(curr
.toString());
478 curr
= new StringBuffer();
481 return Collections
.unmodifiableList(tokens
);
485 * Safe and repository implementation independent registration of a
488 public static void registerNamespaceSafely(Session session
, String prefix
,
491 registerNamespaceSafely(session
.getWorkspace()
492 .getNamespaceRegistry(), prefix
, uri
);
493 } catch (RepositoryException e
) {
494 throw new ArgeoException("Cannot find namespace registry", e
);
499 * Safe and repository implementation independent registration of a
502 public static void registerNamespaceSafely(NamespaceRegistry nr
,
503 String prefix
, String uri
) {
505 String
[] prefixes
= nr
.getPrefixes();
506 for (String pref
: prefixes
)
507 if (pref
.equals(prefix
)) {
508 String registeredUri
= nr
.getURI(pref
);
509 if (!registeredUri
.equals(uri
))
510 throw new ArgeoException("Prefix " + pref
511 + " already registered for URI "
513 + " which is different from provided URI "
518 nr
.registerNamespace(prefix
, uri
);
519 } catch (RepositoryException e
) {
520 throw new ArgeoException("Cannot register namespace " + uri
521 + " under prefix " + prefix
, e
);
525 /** Recursively outputs the contents of the given node. */
526 public static void debug(Node node
) {
530 /** Recursively outputs the contents of the given node. */
531 public static void debug(Node node
, Log log
) {
533 // First output the node path
534 log
.debug(node
.getPath());
535 // Skip the virtual (and large!) jcr:system subtree
536 if (node
.getName().equals("jcr:system")) {
540 // Then the children nodes (recursive)
541 NodeIterator it
= node
.getNodes();
542 while (it
.hasNext()) {
543 Node childNode
= it
.nextNode();
544 debug(childNode
, log
);
547 // Then output the properties
548 PropertyIterator properties
= node
.getProperties();
549 // log.debug("Property are : ");
551 properties
: while (properties
.hasNext()) {
552 Property property
= properties
.nextProperty();
553 if (property
.getType() == PropertyType
.BINARY
)
554 continue properties
;// skip
555 if (property
.getDefinition().isMultiple()) {
556 // A multi-valued property, print all values
557 Value
[] values
= property
.getValues();
558 for (int i
= 0; i
< values
.length
; i
++) {
559 log
.debug(property
.getPath() + "="
560 + values
[i
].getString());
563 // A single-valued property
564 log
.debug(property
.getPath() + "=" + property
.getString());
567 } catch (Exception e
) {
568 log
.error("Could not debug " + node
, e
);
573 /** Logs the effective access control policies */
574 public static void logEffectiveAccessPolicies(Node node
) {
576 logEffectiveAccessPolicies(node
.getSession(), node
.getPath());
577 } catch (RepositoryException e
) {
578 log
.error("Cannot log effective access policies of " + node
, e
);
582 /** Logs the effective access control policies */
583 public static void logEffectiveAccessPolicies(Session session
, String path
) {
584 if (!log
.isDebugEnabled())
588 AccessControlPolicy
[] effectivePolicies
= session
589 .getAccessControlManager().getEffectivePolicies(path
);
590 if (effectivePolicies
.length
> 0) {
591 for (AccessControlPolicy policy
: effectivePolicies
) {
592 if (policy
instanceof AccessControlList
) {
593 AccessControlList acl
= (AccessControlList
) policy
;
594 log
.debug("Access control list for " + path
+ "\n"
595 + accessControlListSummary(acl
));
599 log
.debug("No effective access control policy for " + path
);
601 } catch (RepositoryException e
) {
602 log
.error("Cannot log effective access policies of " + path
, e
);
606 /** Returns a human-readable summary of this access control list. */
607 public static String
accessControlListSummary(AccessControlList acl
) {
608 StringBuffer buf
= new StringBuffer("");
610 for (AccessControlEntry ace
: acl
.getAccessControlEntries()) {
611 buf
.append('\t').append(ace
.getPrincipal().getName())
613 for (Privilege priv
: ace
.getPrivileges())
614 buf
.append("\t\t").append(priv
.getName()).append('\n');
616 return buf
.toString();
617 } catch (RepositoryException e
) {
618 throw new ArgeoException("Cannot write summary of " + acl
, e
);
623 * Copies recursively the content of a node to another one. Do NOT copy the
624 * property values of {@link NodeType#MIX_CREATED} and
625 * {@link NodeType#MIX_LAST_MODIFIED}, but update the
626 * {@link Property#JCR_LAST_MODIFIED} and
627 * {@link Property#JCR_LAST_MODIFIED_BY} properties if the target node has
628 * the {@link NodeType#MIX_LAST_MODIFIED} mixin.
630 public static void copy(Node fromNode
, Node toNode
) {
632 // process properties
633 PropertyIterator pit
= fromNode
.getProperties();
634 properties
: while (pit
.hasNext()) {
635 Property fromProperty
= pit
.nextProperty();
636 String propertyName
= fromProperty
.getName();
637 if (toNode
.hasProperty(propertyName
)
638 && toNode
.getProperty(propertyName
).getDefinition()
642 if (fromProperty
.getDefinition().isProtected())
645 if (propertyName
.equals("jcr:created")
646 || propertyName
.equals("jcr:createdBy")
647 || propertyName
.equals("jcr:lastModified")
648 || propertyName
.equals("jcr:lastModifiedBy"))
651 if (fromProperty
.isMultiple()) {
652 toNode
.setProperty(propertyName
, fromProperty
.getValues());
654 toNode
.setProperty(propertyName
, fromProperty
.getValue());
658 // update jcr:lastModified and jcr:lastModifiedBy in toNode in case
659 // they existed, before adding the mixins
660 updateLastModified(toNode
);
663 for (NodeType mixinType
: fromNode
.getMixinNodeTypes()) {
664 toNode
.addMixin(mixinType
.getName());
667 // process children nodes
668 NodeIterator nit
= fromNode
.getNodes();
669 while (nit
.hasNext()) {
670 Node fromChild
= nit
.nextNode();
671 Integer index
= fromChild
.getIndex();
672 String nodeRelPath
= fromChild
.getName() + "[" + index
+ "]";
674 if (toNode
.hasNode(nodeRelPath
))
675 toChild
= toNode
.getNode(nodeRelPath
);
677 toChild
= toNode
.addNode(fromChild
.getName(), fromChild
678 .getPrimaryNodeType().getName());
679 copy(fromChild
, toChild
);
681 } catch (RepositoryException e
) {
682 throw new ArgeoException("Cannot copy " + fromNode
+ " to "
688 * Check whether all first-level properties (except jcr:* properties) are
689 * equal. Skip jcr:* properties
691 public static Boolean
allPropertiesEquals(Node reference
, Node observed
,
692 Boolean onlyCommonProperties
) {
694 PropertyIterator pit
= reference
.getProperties();
695 props
: while (pit
.hasNext()) {
696 Property propReference
= pit
.nextProperty();
697 String propName
= propReference
.getName();
698 if (propName
.startsWith("jcr:"))
701 if (!observed
.hasProperty(propName
))
702 if (onlyCommonProperties
)
706 // TODO: deal with multiple property values?
707 if (!observed
.getProperty(propName
).getValue()
708 .equals(propReference
.getValue()))
712 } catch (RepositoryException e
) {
713 throw new ArgeoException("Cannot check all properties equals of "
714 + reference
+ " and " + observed
, e
);
718 public static Map
<String
, PropertyDiff
> diffProperties(Node reference
,
720 Map
<String
, PropertyDiff
> diffs
= new TreeMap
<String
, PropertyDiff
>();
721 diffPropertiesLevel(diffs
, null, reference
, observed
);
726 * Compare the properties of two nodes. Recursivity to child nodes is not
727 * yet supported. Skip jcr:* properties.
729 static void diffPropertiesLevel(Map
<String
, PropertyDiff
> diffs
,
730 String baseRelPath
, Node reference
, Node observed
) {
732 // check removed and modified
733 PropertyIterator pit
= reference
.getProperties();
734 props
: while (pit
.hasNext()) {
735 Property p
= pit
.nextProperty();
736 String name
= p
.getName();
737 if (name
.startsWith("jcr:"))
740 if (!observed
.hasProperty(name
)) {
741 String relPath
= propertyRelPath(baseRelPath
, name
);
742 PropertyDiff pDiff
= new PropertyDiff(PropertyDiff
.REMOVED
,
743 relPath
, p
.getValue(), null);
744 diffs
.put(relPath
, pDiff
);
746 if (p
.isMultiple()) {
747 // FIXME implement multiple
749 Value referenceValue
= p
.getValue();
750 Value newValue
= observed
.getProperty(name
).getValue();
751 if (!referenceValue
.equals(newValue
)) {
752 String relPath
= propertyRelPath(baseRelPath
, name
);
753 PropertyDiff pDiff
= new PropertyDiff(
754 PropertyDiff
.MODIFIED
, relPath
,
755 referenceValue
, newValue
);
756 diffs
.put(relPath
, pDiff
);
762 pit
= observed
.getProperties();
763 props
: while (pit
.hasNext()) {
764 Property p
= pit
.nextProperty();
765 String name
= p
.getName();
766 if (name
.startsWith("jcr:"))
768 if (!reference
.hasProperty(name
)) {
769 if (p
.isMultiple()) {
770 // FIXME implement multiple
772 String relPath
= propertyRelPath(baseRelPath
, name
);
773 PropertyDiff pDiff
= new PropertyDiff(
774 PropertyDiff
.ADDED
, relPath
, null, p
.getValue());
775 diffs
.put(relPath
, pDiff
);
779 } catch (RepositoryException e
) {
780 throw new ArgeoException("Cannot diff " + reference
+ " and "
786 * Compare only a restricted list of properties of two nodes. No
790 public static Map
<String
, PropertyDiff
> diffProperties(Node reference
,
791 Node observed
, List
<String
> properties
) {
792 Map
<String
, PropertyDiff
> diffs
= new TreeMap
<String
, PropertyDiff
>();
794 Iterator
<String
> pit
= properties
.iterator();
796 props
: while (pit
.hasNext()) {
797 String name
= pit
.next();
798 if (!reference
.hasProperty(name
)) {
799 if (!observed
.hasProperty(name
))
801 Value val
= observed
.getProperty(name
).getValue();
803 // empty String but not null
804 if ("".equals(val
.getString()))
806 } catch (Exception e
) {
807 // not parseable as String, silent
809 PropertyDiff pDiff
= new PropertyDiff(PropertyDiff
.ADDED
,
811 diffs
.put(name
, pDiff
);
812 } else if (!observed
.hasProperty(name
)) {
813 PropertyDiff pDiff
= new PropertyDiff(PropertyDiff
.REMOVED
,
814 name
, reference
.getProperty(name
).getValue(), null);
815 diffs
.put(name
, pDiff
);
817 Value referenceValue
= reference
.getProperty(name
)
819 Value newValue
= observed
.getProperty(name
).getValue();
820 if (!referenceValue
.equals(newValue
)) {
821 PropertyDiff pDiff
= new PropertyDiff(
822 PropertyDiff
.MODIFIED
, name
, referenceValue
,
824 diffs
.put(name
, pDiff
);
828 } catch (RepositoryException e
) {
829 throw new ArgeoException("Cannot diff " + reference
+ " and "
835 /** Builds a property relPath to be used in the diff. */
836 private static String
propertyRelPath(String baseRelPath
,
837 String propertyName
) {
838 if (baseRelPath
== null)
841 return baseRelPath
+ '/' + propertyName
;
845 * Normalizes a name so that it can be stored in contexts not supporting
846 * names with ':' (typically databases). Replaces ':' by '_'.
848 public static String
normalize(String name
) {
849 return name
.replace(':', '_');
853 * Replaces characters which are invalid in a JCR name by '_'. Currently not
856 * @see JcrUtils#INVALID_NAME_CHARACTERS
858 public static String
replaceInvalidChars(String name
) {
859 return replaceInvalidChars(name
, '_');
863 * Replaces characters which are invalid in a JCR name. Currently not
866 * @see JcrUtils#INVALID_NAME_CHARACTERS
868 public static String
replaceInvalidChars(String name
, char replacement
) {
869 boolean modified
= false;
870 char[] arr
= name
.toCharArray();
871 for (int i
= 0; i
< arr
.length
; i
++) {
873 invalid
: for (char invalid
: INVALID_NAME_CHARACTERS
) {
875 arr
[i
] = replacement
;
882 return new String(arr
);
884 // do not create new object if unnecessary
889 * Removes forbidden characters from a path, replacing them with '_'
891 * @deprecated use {@link #replaceInvalidChars(String)} instead
893 public static String
removeForbiddenCharacters(String str
) {
894 return str
.replace('[', '_').replace(']', '_').replace('/', '_')
899 /** Cleanly disposes a {@link Binary} even if it is null. */
900 public static void closeQuietly(Binary binary
) {
906 /** Retrieve a {@link Binary} as a byte array */
907 public static byte[] getBinaryAsBytes(Property property
) {
908 ByteArrayOutputStream out
= new ByteArrayOutputStream();
909 InputStream in
= null;
910 Binary binary
= null;
912 binary
= property
.getBinary();
913 in
= binary
.getStream();
914 IOUtils
.copy(in
, out
);
915 return out
.toByteArray();
916 } catch (Exception e
) {
917 throw new ArgeoException("Cannot read binary " + property
920 IOUtils
.closeQuietly(out
);
921 IOUtils
.closeQuietly(in
);
922 closeQuietly(binary
);
926 /** Writes a {@link Binary} from a byte array */
927 public static void setBinaryAsBytes(Node node
, String property
, byte[] bytes
) {
928 InputStream in
= null;
929 Binary binary
= null;
931 in
= new ByteArrayInputStream(bytes
);
932 binary
= node
.getSession().getValueFactory().createBinary(in
);
933 node
.setProperty(property
, binary
);
934 } catch (Exception e
) {
935 throw new ArgeoException("Cannot read binary " + property
938 IOUtils
.closeQuietly(in
);
939 closeQuietly(binary
);
944 * Copy a file as an nt:file, assuming an nt:folder hierarchy. The session
947 * @return the created file node
949 public static Node
copyFile(Node folderNode
, File file
) {
950 InputStream in
= null;
952 in
= new FileInputStream(file
);
953 return copyStreamAsFile(folderNode
, file
.getName(), in
);
954 } catch (IOException e
) {
955 throw new ArgeoException("Cannot copy file " + file
+ " under "
958 IOUtils
.closeQuietly(in
);
962 /** Copy bytes as an nt:file */
963 public static Node
copyBytesAsFile(Node folderNode
, String fileName
,
965 InputStream in
= null;
967 in
= new ByteArrayInputStream(bytes
);
968 return copyStreamAsFile(folderNode
, fileName
, in
);
969 } catch (Exception e
) {
970 throw new ArgeoException("Cannot copy file " + fileName
+ " under "
973 IOUtils
.closeQuietly(in
);
978 * Copy a stream as an nt:file, assuming an nt:folder hierarchy. The session
981 * @return the created file node
983 public static Node
copyStreamAsFile(Node folderNode
, String fileName
,
985 Binary binary
= null;
989 if (folderNode
.hasNode(fileName
)) {
990 fileNode
= folderNode
.getNode(fileName
);
991 // we assume that the content node is already there
992 contentNode
= fileNode
.getNode(Node
.JCR_CONTENT
);
994 fileNode
= folderNode
.addNode(fileName
, NodeType
.NT_FILE
);
995 contentNode
= fileNode
.addNode(Node
.JCR_CONTENT
,
996 NodeType
.NT_RESOURCE
);
998 binary
= contentNode
.getSession().getValueFactory()
1000 contentNode
.setProperty(Property
.JCR_DATA
, binary
);
1002 } catch (Exception e
) {
1003 throw new ArgeoException("Cannot create file node " + fileName
1004 + " under " + folderNode
, e
);
1006 closeQuietly(binary
);
1010 /** Computes the checksum of an nt:file */
1011 public static String
checksumFile(Node fileNode
, String algorithm
) {
1013 InputStream in
= null;
1015 data
= fileNode
.getNode(Node
.JCR_CONTENT
)
1016 .getProperty(Property
.JCR_DATA
).getBinary();
1017 in
= data
.getStream();
1018 return DigestUtils
.digest(algorithm
, in
);
1019 } catch (RepositoryException e
) {
1020 throw new ArgeoException("Cannot checksum file " + fileNode
, e
);
1022 IOUtils
.closeQuietly(in
);
1028 * Creates depth from a string (typically a username) by adding levels based
1029 * on its first characters: "aBcD",2 => a/aB
1031 public static String
firstCharsToPath(String str
, Integer nbrOfChars
) {
1032 if (str
.length() < nbrOfChars
)
1033 throw new ArgeoException("String " + str
1034 + " length must be greater or equal than " + nbrOfChars
);
1035 StringBuffer path
= new StringBuffer("");
1036 StringBuffer curr
= new StringBuffer("");
1037 for (int i
= 0; i
< nbrOfChars
; i
++) {
1038 curr
.append(str
.charAt(i
));
1040 if (i
< nbrOfChars
- 1)
1043 return path
.toString();
1047 * Discards the current changes in the session attached to this node. To be
1048 * used typically in a catch block.
1050 * @see #discardQuietly(Session)
1052 public static void discardUnderlyingSessionQuietly(Node node
) {
1054 discardQuietly(node
.getSession());
1055 } catch (RepositoryException e
) {
1056 log
.warn("Cannot quietly discard session of node " + node
+ ": "
1062 * Discards the current changes in a session by calling
1063 * {@link Session#refresh(boolean)} with <code>false</code>, only logging
1064 * potential errors when doing so. To be used typically in a catch block.
1066 public static void discardQuietly(Session session
) {
1068 if (session
!= null)
1069 session
.refresh(false);
1070 } catch (RepositoryException e
) {
1071 log
.warn("Cannot quietly discard session " + session
+ ": "
1077 * Login to a workspace with implicit credentials, creates the workspace
1078 * with these credentials if it does not already exist.
1080 public static Session
loginOrCreateWorkspace(Repository repository
,
1081 String workspaceName
) throws RepositoryException
{
1082 Session workspaceSession
= null;
1083 Session defaultSession
= null;
1086 workspaceSession
= repository
.login(workspaceName
);
1087 } catch (NoSuchWorkspaceException e
) {
1088 // try to create workspace
1089 defaultSession
= repository
.login();
1090 defaultSession
.getWorkspace().createWorkspace(workspaceName
);
1091 workspaceSession
= repository
.login(workspaceName
);
1093 return workspaceSession
;
1095 logoutQuietly(defaultSession
);
1099 /** Logs out the session, not throwing any exception, even if it is null. */
1100 public static void logoutQuietly(Session session
) {
1102 if (session
!= null)
1103 if (session
.isLive())
1105 } catch (Exception e
) {
1111 * Convenient method to add a listener. uuids passed as null, deep=true,
1112 * local=true, only one node type
1114 public static void addListener(Session session
, EventListener listener
,
1115 int eventTypes
, String basePath
, String nodeType
) {
1117 session
.getWorkspace()
1118 .getObservationManager()
1119 .addEventListener(listener
, eventTypes
, basePath
, true,
1120 null, new String
[] { nodeType
}, true);
1121 } catch (RepositoryException e
) {
1122 throw new ArgeoException("Cannot add JCR listener " + listener
1123 + " to session " + session
, e
);
1127 /** Removes a listener without throwing exception */
1128 public static void removeListenerQuietly(Session session
,
1129 EventListener listener
) {
1130 if (session
== null || !session
.isLive())
1133 session
.getWorkspace().getObservationManager()
1134 .removeEventListener(listener
);
1135 } catch (RepositoryException e
) {
1141 * Quietly unregisters an {@link EventListener} from the udnerlying
1142 * workspace of this node.
1144 public static void unregisterQuietly(Node node
, EventListener eventListener
) {
1146 unregisterQuietly(node
.getSession().getWorkspace(), eventListener
);
1147 } catch (RepositoryException e
) {
1149 if (log
.isTraceEnabled())
1150 log
.trace("Could not unregister event listener "
1155 /** Quietly unregisters an {@link EventListener} from this workspace */
1156 public static void unregisterQuietly(Workspace workspace
,
1157 EventListener eventListener
) {
1158 if (eventListener
== null)
1161 workspace
.getObservationManager()
1162 .removeEventListener(eventListener
);
1163 } catch (RepositoryException e
) {
1165 if (log
.isTraceEnabled())
1166 log
.trace("Could not unregister event listener "
1172 * If this node is has the {@link NodeType#MIX_LAST_MODIFIED} mixin, it
1173 * updates the {@link Property#JCR_LAST_MODIFIED} property with the current
1174 * time and the {@link Property#JCR_LAST_MODIFIED_BY} property with the
1175 * underlying session user id. In Jackrabbit 2.x, <a
1176 * href="https://issues.apache.org/jira/browse/JCR-2233">these properties
1177 * are not automatically updated</a>, hence the need for manual update. The
1178 * session is not saved.
1180 public static void updateLastModified(Node node
) {
1182 if (!node
.isNodeType(NodeType
.MIX_LAST_MODIFIED
))
1183 node
.addMixin(NodeType
.MIX_LAST_MODIFIED
);
1184 node
.setProperty(Property
.JCR_LAST_MODIFIED
,
1185 new GregorianCalendar());
1186 node
.setProperty(Property
.JCR_LAST_MODIFIED_BY
, node
.getSession()
1188 } catch (RepositoryException e
) {
1189 throw new ArgeoException("Cannot update last modified on " + node
,
1194 /** Update lastModified recursively until this parent. */
1195 public static void updateLastModifiedAndParents(Node node
, String untilPath
) {
1197 if (!node
.getPath().startsWith(untilPath
))
1198 throw new ArgeoException(node
+ " is not under " + untilPath
);
1199 updateLastModified(node
);
1200 if (!node
.getPath().equals(untilPath
))
1201 updateLastModifiedAndParents(node
.getParent(), untilPath
);
1202 } catch (RepositoryException e
) {
1203 throw new ArgeoException("Cannot update lastModified from " + node
1204 + " until " + untilPath
, e
);
1209 * Returns a String representing the short version (see <a
1210 * href="http://jackrabbit.apache.org/node-type-notation.html"> Node type
1211 * Notation </a> attributes grammar) of the main business attributes of this
1212 * property definition
1216 public static String
getPropertyDefinitionAsString(Property prop
) {
1217 StringBuffer sbuf
= new StringBuffer();
1219 if (prop
.getDefinition().isAutoCreated())
1221 if (prop
.getDefinition().isMandatory())
1223 if (prop
.getDefinition().isProtected())
1225 if (prop
.getDefinition().isMultiple())
1227 } catch (RepositoryException re
) {
1228 throw new ArgeoException(
1229 "unexpected error while getting property definition as String",
1232 return sbuf
.toString();
1236 * Estimate the sub tree size from current node. Computation is based on the
1237 * Jcr {@link Property.getLength()} method. Note : it is not the exact size
1238 * used on the disk by the current part of the JCR Tree.
1241 public static long getNodeApproxSize(Node node
) {
1242 long curNodeSize
= 0;
1244 PropertyIterator pi
= node
.getProperties();
1245 while (pi
.hasNext()) {
1246 Property prop
= pi
.nextProperty();
1247 if (prop
.isMultiple()) {
1248 int nb
= prop
.getLengths().length
;
1249 for (int i
= 0; i
< nb
; i
++) {
1250 curNodeSize
+= (prop
.getLengths()[i
] > 0 ? prop
1251 .getLengths()[i
] : 0);
1254 curNodeSize
+= (prop
.getLength() > 0 ? prop
.getLength() : 0);
1257 NodeIterator ni
= node
.getNodes();
1258 while (ni
.hasNext())
1259 curNodeSize
+= getNodeApproxSize(ni
.nextNode());
1261 } catch (RepositoryException re
) {
1262 throw new ArgeoException(
1263 "Unexpected error while recursively determining node size.",
1273 * Convenience method for adding a single privilege to a principal (user or
1274 * role), typically jcr:all
1276 public static void addPrivilege(Session session
, String path
,
1277 String principal
, String privilege
) throws RepositoryException
{
1278 List
<Privilege
> privileges
= new ArrayList
<Privilege
>();
1279 privileges
.add(session
.getAccessControlManager().privilegeFromName(
1281 addPrivileges(session
, path
, new SimplePrincipal(principal
), privileges
);
1285 * Add privileges on a path to a {@link Principal}. The path must already
1286 * exist. Session is saved.
1288 public static void addPrivileges(Session session
, String path
,
1289 Principal principal
, List
<Privilege
> privs
)
1290 throws RepositoryException
{
1291 AccessControlManager acm
= session
.getAccessControlManager();
1292 AccessControlList acl
= getAccessControlList(acm
, path
);
1293 acl
.addAccessControlEntry(principal
,
1294 privs
.toArray(new Privilege
[privs
.size()]));
1295 acm
.setPolicy(path
, acl
);
1296 if (log
.isDebugEnabled()) {
1297 StringBuffer privBuf
= new StringBuffer();
1298 for (Privilege priv
: privs
)
1299 privBuf
.append(priv
.getName());
1300 log
.debug("Added privileges " + privBuf
+ " to " + principal
1306 /** Gets access control list for this path, throws exception if not found */
1307 public static AccessControlList
getAccessControlList(
1308 AccessControlManager acm
, String path
) throws RepositoryException
{
1309 // search for an access control list
1310 AccessControlList acl
= null;
1311 AccessControlPolicyIterator policyIterator
= acm
1312 .getApplicablePolicies(path
);
1313 if (policyIterator
.hasNext()) {
1314 while (policyIterator
.hasNext()) {
1315 AccessControlPolicy acp
= policyIterator
1316 .nextAccessControlPolicy();
1317 if (acp
instanceof AccessControlList
)
1318 acl
= ((AccessControlList
) acp
);
1321 AccessControlPolicy
[] existingPolicies
= acm
.getPolicies(path
);
1322 for (AccessControlPolicy acp
: existingPolicies
) {
1323 if (acp
instanceof AccessControlList
)
1324 acl
= ((AccessControlList
) acp
);
1330 throw new ArgeoException("ACL not found at " + path
);
1333 /** Clear authorizations for a user at this path */
1334 public static void clearAccesControList(Session session
, String path
,
1335 String username
) throws RepositoryException
{
1336 AccessControlManager acm
= session
.getAccessControlManager();
1337 AccessControlList acl
= getAccessControlList(acm
, path
);
1338 for (AccessControlEntry ace
: acl
.getAccessControlEntries()) {
1339 if (ace
.getPrincipal().getName().equals(username
)) {
1340 acl
.removeAccessControlEntry(ace
);