2 * Copyright (C) 2007-2012 Argeo GmbH
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
.ArgeoMonitor
;
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
{
73 final private 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 ArgeoJcrException
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 ArgeoJcrException("Cannot execute query " + query
, e
);
106 if (nodeIterator
.hasNext())
107 node
= nodeIterator
.nextNode();
111 if (nodeIterator
.hasNext())
112 throw new ArgeoJcrException("Query returned more than one node.");
116 /** Retrieves the node name from the provided path */
117 public static String
nodeNameFromPath(String path
) {
118 if (path
.equals("/"))
120 if (path
.charAt(0) != '/')
121 throw new ArgeoJcrException("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(index
+ 1);
130 /** Retrieves the parent path of the provided path */
131 public static String
parentPath(String path
) {
132 if (path
.equals("/"))
133 throw new ArgeoJcrException("Root path '/' has no parent path");
134 if (path
.charAt(0) != '/')
135 throw new ArgeoJcrException("Path " + path
+ " must start with a '/'");
137 if (pathT
.charAt(pathT
.length() - 1) == '/')
138 pathT
= pathT
.substring(0, pathT
.length() - 2);
140 int index
= pathT
.lastIndexOf('/');
141 return pathT
.substring(0, index
);
144 /** The provided data as a path ('/' at the end, not the beginning) */
145 public static String
dateAsPath(Calendar cal
) {
146 return dateAsPath(cal
, false);
150 * Creates a deep path based on a URL:
151 * http://subdomain.example.com/to/content?args =>
152 * com/example/subdomain/to/content
154 public static String
urlAsPath(String url
) {
156 URL u
= new URL(url
);
157 StringBuffer path
= new StringBuffer(url
.length());
159 path
.append(hostAsPath(u
.getHost()));
160 // we don't put port since it may not always be there and may change
161 path
.append(u
.getPath());
162 return path
.toString();
163 } catch (MalformedURLException e
) {
164 throw new ArgeoJcrException("Cannot generate URL path for " + url
, e
);
168 /** Set the {@link NodeType#NT_ADDRESS} properties based on this URL. */
169 public static void urlToAddressProperties(Node node
, String url
) {
171 URL u
= new URL(url
);
172 node
.setProperty(Property
.JCR_PROTOCOL
, u
.getProtocol());
173 node
.setProperty(Property
.JCR_HOST
, u
.getHost());
174 node
.setProperty(Property
.JCR_PORT
, Integer
.toString(u
.getPort()));
175 node
.setProperty(Property
.JCR_PATH
, normalizePath(u
.getPath()));
176 } catch (Exception e
) {
177 throw new ArgeoJcrException("Cannot set URL " + url
178 + " as nt:address properties", e
);
182 /** Build URL based on the {@link NodeType#NT_ADDRESS} properties. */
183 public static String
urlFromAddressProperties(Node node
) {
186 node
.getProperty(Property
.JCR_PROTOCOL
).getString(), node
187 .getProperty(Property
.JCR_HOST
).getString(),
188 (int) node
.getProperty(Property
.JCR_PORT
).getLong(), node
189 .getProperty(Property
.JCR_PATH
).getString());
191 } catch (Exception e
) {
192 throw new ArgeoJcrException(
193 "Cannot get URL from nt:address properties of " + node
, e
);
201 /** Make sure that: starts with '/', do not end with '/', do not have '//' */
202 public static String
normalizePath(String path
) {
203 List
<String
> tokens
= tokenize(path
);
204 StringBuffer buf
= new StringBuffer(path
.length());
205 for (String token
: tokens
) {
209 return buf
.toString();
213 * Creates a path from a FQDN, inverting the order of the component:
214 * www.argeo.org => org.argeo.www
216 public static String
hostAsPath(String host
) {
217 StringBuffer path
= new StringBuffer(host
.length());
218 String
[] hostTokens
= host
.split("\\.");
219 for (int i
= hostTokens
.length
- 1; i
>= 0; i
--) {
220 path
.append(hostTokens
[i
]);
224 return path
.toString();
228 * Creates a path from a UUID (e.g. 6ebda899-217d-4bf1-abe4-2839085c8f3c =>
229 * 6ebda899-217d/4bf1/abe4/2839085c8f3c/). '/' at the end, not the beginning
231 public static String
uuidAsPath(String uuid
) {
232 StringBuffer path
= new StringBuffer(uuid
.length());
233 String
[] tokens
= uuid
.split("-");
234 for (int i
= 0; i
< tokens
.length
; i
++) {
235 path
.append(tokens
[i
]);
239 return path
.toString();
243 * The provided data as a path ('/' at the end, not the beginning)
248 * whether to add hour as well
250 public static String
dateAsPath(Calendar cal
, Boolean addHour
) {
251 StringBuffer buf
= new StringBuffer(14);
253 buf
.append(cal
.get(Calendar
.YEAR
));
256 int month
= cal
.get(Calendar
.MONTH
) + 1;
263 int day
= cal
.get(Calendar
.DAY_OF_MONTH
);
271 int hour
= cal
.get(Calendar
.HOUR_OF_DAY
);
278 return buf
.toString();
282 /** Converts in one call a string into a gregorian calendar. */
283 public static Calendar
parseCalendar(DateFormat dateFormat
, String value
) {
285 Date date
= dateFormat
.parse(value
);
286 Calendar calendar
= new GregorianCalendar();
287 calendar
.setTime(date
);
289 } catch (ParseException e
) {
290 throw new ArgeoJcrException("Cannot parse " + value
291 + " with date format " + dateFormat
, e
);
296 /** The last element of a path. */
297 public static String
lastPathElement(String path
) {
298 if (path
.charAt(path
.length() - 1) == '/')
299 throw new ArgeoJcrException("Path " + path
+ " cannot end with '/'");
300 int index
= path
.lastIndexOf('/');
303 return path
.substring(index
+ 1);
307 * Call {@link Node#getName()} without exceptions (useful in super
310 public static String
getNameQuietly(Node node
) {
312 return node
.getName();
313 } catch (RepositoryException e
) {
314 throw new ArgeoJcrException("Cannot get name from " + node
, e
);
319 * Call {@link Node#getProperty(String)} without exceptions (useful in super
322 public static String
getStringPropertyQuietly(Node node
, String propertyName
) {
324 return node
.getProperty(propertyName
).getString();
325 } catch (RepositoryException e
) {
326 throw new ArgeoJcrException("Cannot get name from " + node
, e
);
331 * Routine that get the child with this name, adding id it does not already
334 public static Node
getOrAdd(Node parent
, String childName
,
335 String childPrimaryNodeType
) throws RepositoryException
{
336 return parent
.hasNode(childName
) ? parent
.getNode(childName
) : parent
337 .addNode(childName
, childPrimaryNodeType
);
341 * Routine that get the child with this name, adding id it does not already
344 public static Node
getOrAdd(Node parent
, String childName
)
345 throws RepositoryException
{
346 return parent
.hasNode(childName
) ? parent
.getNode(childName
) : parent
350 /** Convert a {@link NodeIterator} to a list of {@link Node} */
351 public static List
<Node
> nodeIteratorToList(NodeIterator nodeIterator
) {
352 List
<Node
> nodes
= new ArrayList
<Node
>();
353 while (nodeIterator
.hasNext()) {
354 nodes
.add(nodeIterator
.nextNode());
364 * Concisely get the string value of a property or null if this node doesn't
367 public static String
get(Node node
, String propertyName
) {
369 if (!node
.hasProperty(propertyName
))
371 return node
.getProperty(propertyName
).getString();
372 } catch (RepositoryException e
) {
373 throw new ArgeoJcrException("Cannot get property " + propertyName
378 /** Concisely get the boolean value of a property */
379 public static Boolean
check(Node node
, String propertyName
) {
381 return node
.getProperty(propertyName
).getBoolean();
382 } catch (RepositoryException e
) {
383 throw new ArgeoJcrException("Cannot get property " + propertyName
388 /** Concisely get the bytes array value of a property */
389 public static byte[] getBytes(Node node
, String propertyName
) {
391 return getBinaryAsBytes(node
.getProperty(propertyName
));
392 } catch (RepositoryException e
) {
393 throw new ArgeoJcrException("Cannot get property " + propertyName
398 /** Creates the nodes making path, if they don't exist. */
399 public static Node
mkdirs(Session session
, String path
) {
400 return mkdirs(session
, path
, null, null, false);
404 * use {@link #mkdirs(Session, String, String, String, Boolean)} instead.
409 public static Node
mkdirs(Session session
, String path
, String type
,
410 Boolean versioning
) {
411 return mkdirs(session
, path
, type
, type
, false);
416 * the type of the leaf node
418 public static Node
mkdirs(Session session
, String path
, String type
) {
419 return mkdirs(session
, path
, type
, null, false);
423 * Create sub nodes relative to a parent node
426 * the type of the leaf node
428 public static Node
mkdirs(Node parentNode
, String relativePath
,
430 return mkdirs(parentNode
, relativePath
, nodeType
, null);
434 * Create sub nodes relative to a parent node
437 * the type of the leaf node
439 public static Node
mkdirs(Node parentNode
, String relativePath
,
440 String nodeType
, String intermediaryNodeType
) {
441 List
<String
> tokens
= tokenize(relativePath
);
442 Node currParent
= parentNode
;
444 for (int i
= 0; i
< tokens
.size(); i
++) {
445 String name
= tokens
.get(i
);
446 if (currParent
.hasNode(name
)) {
447 currParent
= currParent
.getNode(name
);
449 if (i
!= (tokens
.size() - 1)) {// intermediary
450 currParent
= currParent
.addNode(name
,
451 intermediaryNodeType
);
453 currParent
= currParent
.addNode(name
, nodeType
);
458 } catch (RepositoryException e
) {
459 throw new ArgeoJcrException("Cannot mkdirs relative path "
460 + relativePath
+ " from " + parentNode
, e
);
465 * Synchronized and save is performed, to avoid race conditions in
466 * initializers leading to duplicate nodes.
468 public synchronized static Node
mkdirsSafe(Session session
, String path
,
471 if (session
.hasPendingChanges())
472 throw new ArgeoJcrException(
473 "Session has pending changes, save them first.");
474 Node node
= mkdirs(session
, path
, type
);
477 } catch (RepositoryException e
) {
478 discardQuietly(session
);
479 throw new ArgeoJcrException("Cannot safely make directories", e
);
483 public synchronized static Node
mkdirsSafe(Session session
, String path
) {
484 return mkdirsSafe(session
, path
, null);
488 * Creates the nodes making path, if they don't exist. This is up to the
489 * caller to save the session. Use with caution since it can create
490 * duplicate nodes if used concurrently.
492 public static Node
mkdirs(Session session
, String path
, String type
,
493 String intermediaryNodeType
, Boolean versioning
) {
495 if (path
.equals('/'))
496 return session
.getRootNode();
498 if (session
.itemExists(path
)) {
499 Node node
= session
.getNode(path
);
501 if (type
!= null && !node
.isNodeType(type
)
502 && !node
.getPath().equals("/"))
503 throw new ArgeoJcrException("Node " + node
504 + " exists but is of type "
505 + node
.getPrimaryNodeType().getName()
506 + " not of type " + type
);
507 // TODO: check versioning
511 StringBuffer current
= new StringBuffer("/");
512 Node currentNode
= session
.getRootNode();
513 Iterator
<String
> it
= tokenize(path
).iterator();
514 while (it
.hasNext()) {
515 String part
= it
.next();
516 current
.append(part
).append('/');
517 if (!session
.itemExists(current
.toString())) {
518 if (!it
.hasNext() && type
!= null)
519 currentNode
= currentNode
.addNode(part
, type
);
520 else if (it
.hasNext() && intermediaryNodeType
!= null)
521 currentNode
= currentNode
.addNode(part
,
522 intermediaryNodeType
);
524 currentNode
= currentNode
.addNode(part
);
526 currentNode
.addMixin(NodeType
.MIX_VERSIONABLE
);
527 if (log
.isTraceEnabled())
528 log
.debug("Added folder " + part
+ " as " + current
);
530 currentNode
= (Node
) session
.getItem(current
.toString());
534 } catch (RepositoryException e
) {
535 discardQuietly(session
);
536 throw new ArgeoJcrException("Cannot mkdirs " + path
, e
);
541 /** Convert a path to the list of its tokens */
542 public static List
<String
> tokenize(String path
) {
543 List
<String
> tokens
= new ArrayList
<String
>();
544 boolean optimized
= false;
546 String
[] rawTokens
= path
.split("/");
547 for (String token
: rawTokens
) {
548 if (!token
.equals(""))
552 StringBuffer curr
= new StringBuffer();
553 char[] arr
= path
.toCharArray();
554 chars
: for (int i
= 0; i
< arr
.length
; i
++) {
557 if (i
== 0 || (i
== arr
.length
- 1))
559 if (curr
.length() > 0) {
560 tokens
.add(curr
.toString());
561 curr
= new StringBuffer();
566 if (curr
.length() > 0) {
567 tokens
.add(curr
.toString());
568 curr
= new StringBuffer();
571 return Collections
.unmodifiableList(tokens
);
575 * Safe and repository implementation independent registration of a
578 public static void registerNamespaceSafely(Session session
, String prefix
,
581 registerNamespaceSafely(session
.getWorkspace()
582 .getNamespaceRegistry(), prefix
, uri
);
583 } catch (RepositoryException e
) {
584 throw new ArgeoJcrException("Cannot find namespace registry", e
);
589 * Safe and repository implementation independent registration of a
592 public static void registerNamespaceSafely(NamespaceRegistry nr
,
593 String prefix
, String uri
) {
595 String
[] prefixes
= nr
.getPrefixes();
596 for (String pref
: prefixes
)
597 if (pref
.equals(prefix
)) {
598 String registeredUri
= nr
.getURI(pref
);
599 if (!registeredUri
.equals(uri
))
600 throw new ArgeoJcrException("Prefix " + pref
601 + " already registered for URI "
603 + " which is different from provided URI "
608 nr
.registerNamespace(prefix
, uri
);
609 } catch (RepositoryException e
) {
610 throw new ArgeoJcrException("Cannot register namespace " + uri
611 + " under prefix " + prefix
, e
);
615 /** Recursively outputs the contents of the given node. */
616 public static void debug(Node node
) {
620 /** Recursively outputs the contents of the given node. */
621 public static void debug(Node node
, Log log
) {
623 // First output the node path
624 log
.debug(node
.getPath());
625 // Skip the virtual (and large!) jcr:system subtree
626 if (node
.getName().equals("jcr:system")) {
630 // Then the children nodes (recursive)
631 NodeIterator it
= node
.getNodes();
632 while (it
.hasNext()) {
633 Node childNode
= it
.nextNode();
634 debug(childNode
, log
);
637 // Then output the properties
638 PropertyIterator properties
= node
.getProperties();
639 // log.debug("Property are : ");
641 properties
: while (properties
.hasNext()) {
642 Property property
= properties
.nextProperty();
643 if (property
.getType() == PropertyType
.BINARY
)
644 continue properties
;// skip
645 if (property
.getDefinition().isMultiple()) {
646 // A multi-valued property, print all values
647 Value
[] values
= property
.getValues();
648 for (int i
= 0; i
< values
.length
; i
++) {
649 log
.debug(property
.getPath() + "="
650 + values
[i
].getString());
653 // A single-valued property
654 log
.debug(property
.getPath() + "=" + property
.getString());
657 } catch (Exception e
) {
658 log
.error("Could not debug " + node
, e
);
663 /** Logs the effective access control policies */
664 public static void logEffectiveAccessPolicies(Node node
) {
666 logEffectiveAccessPolicies(node
.getSession(), node
.getPath());
667 } catch (RepositoryException e
) {
668 log
.error("Cannot log effective access policies of " + node
, e
);
672 /** Logs the effective access control policies */
673 public static void logEffectiveAccessPolicies(Session session
, String path
) {
674 if (!log
.isDebugEnabled())
678 AccessControlPolicy
[] effectivePolicies
= session
679 .getAccessControlManager().getEffectivePolicies(path
);
680 if (effectivePolicies
.length
> 0) {
681 for (AccessControlPolicy policy
: effectivePolicies
) {
682 if (policy
instanceof AccessControlList
) {
683 AccessControlList acl
= (AccessControlList
) policy
;
684 log
.debug("Access control list for " + path
+ "\n"
685 + accessControlListSummary(acl
));
689 log
.debug("No effective access control policy for " + path
);
691 } catch (RepositoryException e
) {
692 log
.error("Cannot log effective access policies of " + path
, e
);
696 /** Returns a human-readable summary of this access control list. */
697 public static String
accessControlListSummary(AccessControlList acl
) {
698 StringBuffer buf
= new StringBuffer("");
700 for (AccessControlEntry ace
: acl
.getAccessControlEntries()) {
701 buf
.append('\t').append(ace
.getPrincipal().getName())
703 for (Privilege priv
: ace
.getPrivileges())
704 buf
.append("\t\t").append(priv
.getName()).append('\n');
706 return buf
.toString();
707 } catch (RepositoryException e
) {
708 throw new ArgeoJcrException("Cannot write summary of " + acl
, e
);
713 * Copies recursively the content of a node to another one. Do NOT copy the
714 * property values of {@link NodeType#MIX_CREATED} and
715 * {@link NodeType#MIX_LAST_MODIFIED}, but update the
716 * {@link Property#JCR_LAST_MODIFIED} and
717 * {@link Property#JCR_LAST_MODIFIED_BY} properties if the target node has
718 * the {@link NodeType#MIX_LAST_MODIFIED} mixin.
720 public static void copy(Node fromNode
, Node toNode
) {
722 if (toNode
.getDefinition().isProtected())
725 // process properties
726 PropertyIterator pit
= fromNode
.getProperties();
727 properties
: while (pit
.hasNext()) {
728 Property fromProperty
= pit
.nextProperty();
729 String propertyName
= fromProperty
.getName();
730 if (toNode
.hasProperty(propertyName
)
731 && toNode
.getProperty(propertyName
).getDefinition()
735 if (fromProperty
.getDefinition().isProtected())
738 if (propertyName
.equals("jcr:created")
739 || propertyName
.equals("jcr:createdBy")
740 || propertyName
.equals("jcr:lastModified")
741 || propertyName
.equals("jcr:lastModifiedBy"))
744 if (fromProperty
.isMultiple()) {
745 toNode
.setProperty(propertyName
, fromProperty
.getValues());
747 toNode
.setProperty(propertyName
, fromProperty
.getValue());
751 // update jcr:lastModified and jcr:lastModifiedBy in toNode in case
752 // they existed, before adding the mixins
753 updateLastModified(toNode
);
756 for (NodeType mixinType
: fromNode
.getMixinNodeTypes()) {
757 toNode
.addMixin(mixinType
.getName());
760 // process children nodes
761 NodeIterator nit
= fromNode
.getNodes();
762 while (nit
.hasNext()) {
763 Node fromChild
= nit
.nextNode();
764 Integer index
= fromChild
.getIndex();
765 String nodeRelPath
= fromChild
.getName() + "[" + index
+ "]";
767 if (toNode
.hasNode(nodeRelPath
))
768 toChild
= toNode
.getNode(nodeRelPath
);
770 toChild
= toNode
.addNode(fromChild
.getName(), fromChild
771 .getPrimaryNodeType().getName());
772 copy(fromChild
, toChild
);
774 } catch (RepositoryException e
) {
775 throw new ArgeoJcrException("Cannot copy " + fromNode
+ " to "
781 * Check whether all first-level properties (except jcr:* properties) are
782 * equal. Skip jcr:* properties
784 public static Boolean
allPropertiesEquals(Node reference
, Node observed
,
785 Boolean onlyCommonProperties
) {
787 PropertyIterator pit
= reference
.getProperties();
788 props
: while (pit
.hasNext()) {
789 Property propReference
= pit
.nextProperty();
790 String propName
= propReference
.getName();
791 if (propName
.startsWith("jcr:"))
794 if (!observed
.hasProperty(propName
))
795 if (onlyCommonProperties
)
799 // TODO: deal with multiple property values?
800 if (!observed
.getProperty(propName
).getValue()
801 .equals(propReference
.getValue()))
805 } catch (RepositoryException e
) {
806 throw new ArgeoJcrException("Cannot check all properties equals of "
807 + reference
+ " and " + observed
, e
);
811 public static Map
<String
, PropertyDiff
> diffProperties(Node reference
,
813 Map
<String
, PropertyDiff
> diffs
= new TreeMap
<String
, PropertyDiff
>();
814 diffPropertiesLevel(diffs
, null, reference
, observed
);
819 * Compare the properties of two nodes. Recursivity to child nodes is not
820 * yet supported. Skip jcr:* properties.
822 static void diffPropertiesLevel(Map
<String
, PropertyDiff
> diffs
,
823 String baseRelPath
, Node reference
, Node observed
) {
825 // check removed and modified
826 PropertyIterator pit
= reference
.getProperties();
827 props
: while (pit
.hasNext()) {
828 Property p
= pit
.nextProperty();
829 String name
= p
.getName();
830 if (name
.startsWith("jcr:"))
833 if (!observed
.hasProperty(name
)) {
834 String relPath
= propertyRelPath(baseRelPath
, name
);
835 PropertyDiff pDiff
= new PropertyDiff(PropertyDiff
.REMOVED
,
836 relPath
, p
.getValue(), null);
837 diffs
.put(relPath
, pDiff
);
839 if (p
.isMultiple()) {
840 // FIXME implement multiple
842 Value referenceValue
= p
.getValue();
843 Value newValue
= observed
.getProperty(name
).getValue();
844 if (!referenceValue
.equals(newValue
)) {
845 String relPath
= propertyRelPath(baseRelPath
, name
);
846 PropertyDiff pDiff
= new PropertyDiff(
847 PropertyDiff
.MODIFIED
, relPath
,
848 referenceValue
, newValue
);
849 diffs
.put(relPath
, pDiff
);
855 pit
= observed
.getProperties();
856 props
: while (pit
.hasNext()) {
857 Property p
= pit
.nextProperty();
858 String name
= p
.getName();
859 if (name
.startsWith("jcr:"))
861 if (!reference
.hasProperty(name
)) {
862 if (p
.isMultiple()) {
863 // FIXME implement multiple
865 String relPath
= propertyRelPath(baseRelPath
, name
);
866 PropertyDiff pDiff
= new PropertyDiff(
867 PropertyDiff
.ADDED
, relPath
, null, p
.getValue());
868 diffs
.put(relPath
, pDiff
);
872 } catch (RepositoryException e
) {
873 throw new ArgeoJcrException("Cannot diff " + reference
+ " and "
879 * Compare only a restricted list of properties of two nodes. No
883 public static Map
<String
, PropertyDiff
> diffProperties(Node reference
,
884 Node observed
, List
<String
> properties
) {
885 Map
<String
, PropertyDiff
> diffs
= new TreeMap
<String
, PropertyDiff
>();
887 Iterator
<String
> pit
= properties
.iterator();
889 props
: while (pit
.hasNext()) {
890 String name
= pit
.next();
891 if (!reference
.hasProperty(name
)) {
892 if (!observed
.hasProperty(name
))
894 Value val
= observed
.getProperty(name
).getValue();
896 // empty String but not null
897 if ("".equals(val
.getString()))
899 } catch (Exception e
) {
900 // not parseable as String, silent
902 PropertyDiff pDiff
= new PropertyDiff(PropertyDiff
.ADDED
,
904 diffs
.put(name
, pDiff
);
905 } else if (!observed
.hasProperty(name
)) {
906 PropertyDiff pDiff
= new PropertyDiff(PropertyDiff
.REMOVED
,
907 name
, reference
.getProperty(name
).getValue(), null);
908 diffs
.put(name
, pDiff
);
910 Value referenceValue
= reference
.getProperty(name
)
912 Value newValue
= observed
.getProperty(name
).getValue();
913 if (!referenceValue
.equals(newValue
)) {
914 PropertyDiff pDiff
= new PropertyDiff(
915 PropertyDiff
.MODIFIED
, name
, referenceValue
,
917 diffs
.put(name
, pDiff
);
921 } catch (RepositoryException e
) {
922 throw new ArgeoJcrException("Cannot diff " + reference
+ " and "
928 /** Builds a property relPath to be used in the diff. */
929 private static String
propertyRelPath(String baseRelPath
,
930 String propertyName
) {
931 if (baseRelPath
== null)
934 return baseRelPath
+ '/' + propertyName
;
938 * Normalizes a name so that it can be stored in contexts not supporting
939 * names with ':' (typically databases). Replaces ':' by '_'.
941 public static String
normalize(String name
) {
942 return name
.replace(':', '_');
946 * Replaces characters which are invalid in a JCR name by '_'. Currently not
949 * @see JcrUtils#INVALID_NAME_CHARACTERS
951 public static String
replaceInvalidChars(String name
) {
952 return replaceInvalidChars(name
, '_');
956 * Replaces characters which are invalid in a JCR name. Currently not
959 * @see JcrUtils#INVALID_NAME_CHARACTERS
961 public static String
replaceInvalidChars(String name
, char replacement
) {
962 boolean modified
= false;
963 char[] arr
= name
.toCharArray();
964 for (int i
= 0; i
< arr
.length
; i
++) {
966 invalid
: for (char invalid
: INVALID_NAME_CHARACTERS
) {
968 arr
[i
] = replacement
;
975 return new String(arr
);
977 // do not create new object if unnecessary
982 * Removes forbidden characters from a path, replacing them with '_'
984 * @deprecated use {@link #replaceInvalidChars(String)} instead
986 public static String
removeForbiddenCharacters(String str
) {
987 return str
.replace('[', '_').replace(']', '_').replace('/', '_')
992 /** Cleanly disposes a {@link Binary} even if it is null. */
993 public static void closeQuietly(Binary binary
) {
999 /** Retrieve a {@link Binary} as a byte array */
1000 public static byte[] getBinaryAsBytes(Property property
) {
1001 ByteArrayOutputStream out
= new ByteArrayOutputStream();
1002 InputStream in
= null;
1003 Binary binary
= null;
1005 binary
= property
.getBinary();
1006 in
= binary
.getStream();
1007 IOUtils
.copy(in
, out
);
1008 return out
.toByteArray();
1009 } catch (Exception e
) {
1010 throw new ArgeoJcrException("Cannot read binary " + property
1013 IOUtils
.closeQuietly(out
);
1014 IOUtils
.closeQuietly(in
);
1015 closeQuietly(binary
);
1019 /** Writes a {@link Binary} from a byte array */
1020 public static void setBinaryAsBytes(Node node
, String property
, byte[] bytes
) {
1021 InputStream in
= null;
1022 Binary binary
= null;
1024 in
= new ByteArrayInputStream(bytes
);
1025 binary
= node
.getSession().getValueFactory().createBinary(in
);
1026 node
.setProperty(property
, binary
);
1027 } catch (Exception e
) {
1028 throw new ArgeoJcrException("Cannot read binary " + property
1031 IOUtils
.closeQuietly(in
);
1032 closeQuietly(binary
);
1037 * Creates depth from a string (typically a username) by adding levels based
1038 * on its first characters: "aBcD",2 => a/aB
1040 public static String
firstCharsToPath(String str
, Integer nbrOfChars
) {
1041 if (str
.length() < nbrOfChars
)
1042 throw new ArgeoJcrException("String " + str
1043 + " length must be greater or equal than " + nbrOfChars
);
1044 StringBuffer path
= new StringBuffer("");
1045 StringBuffer curr
= new StringBuffer("");
1046 for (int i
= 0; i
< nbrOfChars
; i
++) {
1047 curr
.append(str
.charAt(i
));
1049 if (i
< nbrOfChars
- 1)
1052 return path
.toString();
1056 * Discards the current changes in the session attached to this node. To be
1057 * used typically in a catch block.
1059 * @see #discardQuietly(Session)
1061 public static void discardUnderlyingSessionQuietly(Node node
) {
1063 discardQuietly(node
.getSession());
1064 } catch (RepositoryException e
) {
1065 log
.warn("Cannot quietly discard session of node " + node
+ ": "
1071 * Discards the current changes in a session by calling
1072 * {@link Session#refresh(boolean)} with <code>false</code>, only logging
1073 * potential errors when doing so. To be used typically in a catch block.
1075 public static void discardQuietly(Session session
) {
1077 if (session
!= null)
1078 session
.refresh(false);
1079 } catch (RepositoryException e
) {
1080 log
.warn("Cannot quietly discard session " + session
+ ": "
1086 * Login to a workspace with implicit credentials, creates the workspace
1087 * with these credentials if it does not already exist.
1089 public static Session
loginOrCreateWorkspace(Repository repository
,
1090 String workspaceName
) throws RepositoryException
{
1091 Session workspaceSession
= null;
1092 Session defaultSession
= null;
1095 workspaceSession
= repository
.login(workspaceName
);
1096 } catch (NoSuchWorkspaceException e
) {
1097 // try to create workspace
1098 defaultSession
= repository
.login();
1099 defaultSession
.getWorkspace().createWorkspace(workspaceName
);
1100 workspaceSession
= repository
.login(workspaceName
);
1102 return workspaceSession
;
1104 logoutQuietly(defaultSession
);
1108 /** Logs out the session, not throwing any exception, even if it is null. */
1109 public static void logoutQuietly(Session session
) {
1111 if (session
!= null)
1112 if (session
.isLive())
1114 } catch (Exception e
) {
1120 * Convenient method to add a listener. uuids passed as null, deep=true,
1121 * local=true, only one node type
1123 public static void addListener(Session session
, EventListener listener
,
1124 int eventTypes
, String basePath
, String nodeType
) {
1126 session
.getWorkspace()
1127 .getObservationManager()
1134 nodeType
== null ?
null : new String
[] { nodeType
},
1136 } catch (RepositoryException e
) {
1137 throw new ArgeoJcrException("Cannot add JCR listener " + listener
1138 + " to session " + session
, e
);
1142 /** Removes a listener without throwing exception */
1143 public static void removeListenerQuietly(Session session
,
1144 EventListener listener
) {
1145 if (session
== null || !session
.isLive())
1148 session
.getWorkspace().getObservationManager()
1149 .removeEventListener(listener
);
1150 } catch (RepositoryException e
) {
1156 * Quietly unregisters an {@link EventListener} from the udnerlying
1157 * workspace of this node.
1159 public static void unregisterQuietly(Node node
, EventListener eventListener
) {
1161 unregisterQuietly(node
.getSession().getWorkspace(), eventListener
);
1162 } catch (RepositoryException e
) {
1164 if (log
.isTraceEnabled())
1165 log
.trace("Could not unregister event listener "
1170 /** Quietly unregisters an {@link EventListener} from this workspace */
1171 public static void unregisterQuietly(Workspace workspace
,
1172 EventListener eventListener
) {
1173 if (eventListener
== null)
1176 workspace
.getObservationManager()
1177 .removeEventListener(eventListener
);
1178 } catch (RepositoryException e
) {
1180 if (log
.isTraceEnabled())
1181 log
.trace("Could not unregister event listener "
1187 * If this node is has the {@link NodeType#MIX_LAST_MODIFIED} mixin, it
1188 * updates the {@link Property#JCR_LAST_MODIFIED} property with the current
1189 * time and the {@link Property#JCR_LAST_MODIFIED_BY} property with the
1190 * underlying session user id. In Jackrabbit 2.x, <a
1191 * href="https://issues.apache.org/jira/browse/JCR-2233">these properties
1192 * are not automatically updated</a>, hence the need for manual update. The
1193 * session is not saved.
1195 public static void updateLastModified(Node node
) {
1197 if (!node
.isNodeType(NodeType
.MIX_LAST_MODIFIED
))
1198 node
.addMixin(NodeType
.MIX_LAST_MODIFIED
);
1199 node
.setProperty(Property
.JCR_LAST_MODIFIED
,
1200 new GregorianCalendar());
1201 node
.setProperty(Property
.JCR_LAST_MODIFIED_BY
, node
.getSession()
1203 } catch (RepositoryException e
) {
1204 throw new ArgeoJcrException("Cannot update last modified on " + node
,
1210 * Update lastModified recursively until this parent.
1215 * the base path, null is equivalent to "/"
1217 public static void updateLastModifiedAndParents(Node node
, String untilPath
) {
1219 if (untilPath
!= null && !node
.getPath().startsWith(untilPath
))
1220 throw new ArgeoJcrException(node
+ " is not under " + untilPath
);
1221 updateLastModified(node
);
1222 if (untilPath
== null) {
1223 if (!node
.getPath().equals("/"))
1224 updateLastModifiedAndParents(node
.getParent(), untilPath
);
1226 if (!node
.getPath().equals(untilPath
))
1227 updateLastModifiedAndParents(node
.getParent(), untilPath
);
1229 } catch (RepositoryException e
) {
1230 throw new ArgeoJcrException("Cannot update lastModified from " + node
1231 + " until " + untilPath
, e
);
1236 * Returns a String representing the short version (see <a
1237 * href="http://jackrabbit.apache.org/node-type-notation.html"> Node type
1238 * Notation </a> attributes grammar) of the main business attributes of this
1239 * property definition
1243 public static String
getPropertyDefinitionAsString(Property prop
) {
1244 StringBuffer sbuf
= new StringBuffer();
1246 if (prop
.getDefinition().isAutoCreated())
1248 if (prop
.getDefinition().isMandatory())
1250 if (prop
.getDefinition().isProtected())
1252 if (prop
.getDefinition().isMultiple())
1254 } catch (RepositoryException re
) {
1255 throw new ArgeoJcrException(
1256 "unexpected error while getting property definition as String",
1259 return sbuf
.toString();
1263 * Estimate the sub tree size from current node. Computation is based on the
1264 * Jcr {@link Property.getLength()} method. Note : it is not the exact size
1265 * used on the disk by the current part of the JCR Tree.
1268 public static long getNodeApproxSize(Node node
) {
1269 long curNodeSize
= 0;
1271 PropertyIterator pi
= node
.getProperties();
1272 while (pi
.hasNext()) {
1273 Property prop
= pi
.nextProperty();
1274 if (prop
.isMultiple()) {
1275 int nb
= prop
.getLengths().length
;
1276 for (int i
= 0; i
< nb
; i
++) {
1277 curNodeSize
+= (prop
.getLengths()[i
] > 0 ? prop
1278 .getLengths()[i
] : 0);
1281 curNodeSize
+= (prop
.getLength() > 0 ? prop
.getLength() : 0);
1284 NodeIterator ni
= node
.getNodes();
1285 while (ni
.hasNext())
1286 curNodeSize
+= getNodeApproxSize(ni
.nextNode());
1288 } catch (RepositoryException re
) {
1289 throw new ArgeoJcrException(
1290 "Unexpected error while recursively determining node size.",
1300 * Convenience method for adding a single privilege to a principal (user or
1301 * role), typically jcr:all
1303 public synchronized static void addPrivilege(Session session
, String path
,
1304 String principal
, String privilege
) throws RepositoryException
{
1305 List
<Privilege
> privileges
= new ArrayList
<Privilege
>();
1306 privileges
.add(session
.getAccessControlManager().privilegeFromName(
1308 addPrivileges(session
, path
, new SimplePrincipal(principal
), privileges
);
1312 * Add privileges on a path to a {@link Principal}. The path must already
1313 * exist. Session is saved. Synchronized to prevent concurrent modifications
1316 public synchronized static Boolean
addPrivileges(Session session
,
1317 String path
, Principal principal
, List
<Privilege
> privs
)
1318 throws RepositoryException
{
1319 // make sure the session is in line with the persisted state
1320 session
.refresh(false);
1321 AccessControlManager acm
= session
.getAccessControlManager();
1322 AccessControlList acl
= getAccessControlList(acm
, path
);
1324 accessControlEntries
: for (AccessControlEntry ace
: acl
1325 .getAccessControlEntries()) {
1326 Principal currentPrincipal
= ace
.getPrincipal();
1327 if (currentPrincipal
.getName().equals(principal
.getName())) {
1328 Privilege
[] currentPrivileges
= ace
.getPrivileges();
1329 if (currentPrivileges
.length
!= privs
.size())
1330 break accessControlEntries
;
1331 for (int i
= 0; i
< currentPrivileges
.length
; i
++) {
1332 Privilege currP
= currentPrivileges
[i
];
1333 Privilege p
= privs
.get(i
);
1334 if (!currP
.getName().equals(p
.getName())) {
1335 break accessControlEntries
;
1342 Privilege
[] privileges
= privs
.toArray(new Privilege
[privs
.size()]);
1343 acl
.addAccessControlEntry(principal
, privileges
);
1344 acm
.setPolicy(path
, acl
);
1345 if (log
.isDebugEnabled()) {
1346 StringBuffer privBuf
= new StringBuffer();
1347 for (Privilege priv
: privs
)
1348 privBuf
.append(priv
.getName());
1349 log
.debug("Added privileges " + privBuf
+ " to "
1350 + principal
.getName() + " on " + path
+ " in '"
1351 + session
.getWorkspace().getName() + "'");
1353 session
.refresh(true);
1358 /** Gets access control list for this path, throws exception if not found */
1359 public synchronized static AccessControlList
getAccessControlList(
1360 AccessControlManager acm
, String path
) throws RepositoryException
{
1361 // search for an access control list
1362 AccessControlList acl
= null;
1363 AccessControlPolicyIterator policyIterator
= acm
1364 .getApplicablePolicies(path
);
1365 if (policyIterator
.hasNext()) {
1366 while (policyIterator
.hasNext()) {
1367 AccessControlPolicy acp
= policyIterator
1368 .nextAccessControlPolicy();
1369 if (acp
instanceof AccessControlList
)
1370 acl
= ((AccessControlList
) acp
);
1373 AccessControlPolicy
[] existingPolicies
= acm
.getPolicies(path
);
1374 for (AccessControlPolicy acp
: existingPolicies
) {
1375 if (acp
instanceof AccessControlList
)
1376 acl
= ((AccessControlList
) acp
);
1382 throw new ArgeoJcrException("ACL not found at " + path
);
1385 /** Clear authorizations for a user at this path */
1386 public synchronized static void clearAccessControList(Session session
,
1387 String path
, String username
) throws RepositoryException
{
1388 AccessControlManager acm
= session
.getAccessControlManager();
1389 AccessControlList acl
= getAccessControlList(acm
, path
);
1390 for (AccessControlEntry ace
: acl
.getAccessControlEntries()) {
1391 if (ace
.getPrincipal().getName().equals(username
)) {
1392 acl
.removeAccessControlEntry(ace
);
1395 // the new access control list must be applied otherwise this call:
1396 // acl.removeAccessControlEntry(ace); has no effect
1397 acm
.setPolicy(path
, acl
);
1404 * Creates the nodes making the path as {@link NodeType#NT_FOLDER}
1406 public static Node
mkfolders(Session session
, String path
) {
1407 return mkdirs(session
, path
, NodeType
.NT_FOLDER
, NodeType
.NT_FOLDER
,
1412 * Copy only nt:folder and nt:file, without their additional types and
1416 * if true copies folders as well, otherwise only first level
1418 * @return how many files were copied
1420 @SuppressWarnings("deprecation")
1421 public static Long
copyFiles(Node fromNode
, Node toNode
, Boolean recursive
,
1422 ArgeoMonitor monitor
) {
1425 Binary binary
= null;
1426 InputStream in
= null;
1428 NodeIterator fromChildren
= fromNode
.getNodes();
1429 while (fromChildren
.hasNext()) {
1430 if (monitor
!= null && monitor
.isCanceled())
1431 throw new ArgeoJcrException(
1432 "Copy cancelled before it was completed");
1434 Node fromChild
= fromChildren
.nextNode();
1435 String fileName
= fromChild
.getName();
1436 if (fromChild
.isNodeType(NodeType
.NT_FILE
)) {
1437 if (monitor
!= null)
1438 monitor
.subTask("Copy " + fileName
);
1439 binary
= fromChild
.getNode(Node
.JCR_CONTENT
)
1440 .getProperty(Property
.JCR_DATA
).getBinary();
1441 in
= binary
.getStream();
1442 copyStreamAsFile(toNode
, fileName
, in
);
1443 IOUtils
.closeQuietly(in
);
1444 closeQuietly(binary
);
1447 toNode
.getSession().save();
1450 if (log
.isDebugEnabled())
1451 log
.debug("Copied file " + fromChild
.getPath());
1452 if (monitor
!= null)
1454 } else if (fromChild
.isNodeType(NodeType
.NT_FOLDER
)
1457 if (toNode
.hasNode(fileName
)) {
1458 toChildFolder
= toNode
.getNode(fileName
);
1459 if (!toChildFolder
.isNodeType(NodeType
.NT_FOLDER
))
1460 throw new ArgeoJcrException(toChildFolder
1461 + " is not of type nt:folder");
1463 toChildFolder
= toNode
.addNode(fileName
,
1464 NodeType
.NT_FOLDER
);
1467 toNode
.getSession().save();
1470 + copyFiles(fromChild
, toChildFolder
, recursive
,
1475 } catch (RepositoryException e
) {
1476 throw new ArgeoJcrException("Cannot copy files between " + fromNode
1477 + " and " + toNode
);
1479 // in case there was an exception
1480 IOUtils
.closeQuietly(in
);
1481 closeQuietly(binary
);
1486 * Iteratively count all file nodes in subtree, inefficient but can be
1487 * useful when query are poorly supported, such as in remoting.
1489 public static Long
countFiles(Node node
) {
1490 Long localCount
= 0l;
1492 for (NodeIterator nit
= node
.getNodes(); nit
.hasNext();) {
1493 Node child
= nit
.nextNode();
1494 if (child
.isNodeType(NodeType
.NT_FOLDER
))
1495 localCount
= localCount
+ countFiles(child
);
1496 else if (child
.isNodeType(NodeType
.NT_FILE
))
1497 localCount
= localCount
+ 1;
1499 } catch (RepositoryException e
) {
1500 throw new ArgeoJcrException("Cannot count all children of " + node
);
1506 * Copy a file as an nt:file, assuming an nt:folder hierarchy. The session
1509 * @return the created file node
1511 public static Node
copyFile(Node folderNode
, File file
) {
1512 InputStream in
= null;
1514 in
= new FileInputStream(file
);
1515 return copyStreamAsFile(folderNode
, file
.getName(), in
);
1516 } catch (IOException e
) {
1517 throw new ArgeoJcrException("Cannot copy file " + file
+ " under "
1520 IOUtils
.closeQuietly(in
);
1524 /** Copy bytes as an nt:file */
1525 public static Node
copyBytesAsFile(Node folderNode
, String fileName
,
1527 InputStream in
= null;
1529 in
= new ByteArrayInputStream(bytes
);
1530 return copyStreamAsFile(folderNode
, fileName
, in
);
1531 } catch (Exception e
) {
1532 throw new ArgeoJcrException("Cannot copy file " + fileName
+ " under "
1535 IOUtils
.closeQuietly(in
);
1540 * Copy a stream as an nt:file, assuming an nt:folder hierarchy. The session
1543 * @return the created file node
1545 public static Node
copyStreamAsFile(Node folderNode
, String fileName
,
1547 Binary binary
= null;
1551 if (folderNode
.hasNode(fileName
)) {
1552 fileNode
= folderNode
.getNode(fileName
);
1553 if (!fileNode
.isNodeType(NodeType
.NT_FILE
))
1554 throw new ArgeoJcrException(fileNode
1555 + " is not of type nt:file");
1556 // we assume that the content node is already there
1557 contentNode
= fileNode
.getNode(Node
.JCR_CONTENT
);
1559 fileNode
= folderNode
.addNode(fileName
, NodeType
.NT_FILE
);
1560 contentNode
= fileNode
.addNode(Node
.JCR_CONTENT
,
1561 NodeType
.NT_RESOURCE
);
1563 binary
= contentNode
.getSession().getValueFactory()
1565 contentNode
.setProperty(Property
.JCR_DATA
, binary
);
1567 } catch (Exception e
) {
1568 throw new ArgeoJcrException("Cannot create file node " + fileName
1569 + " under " + folderNode
, e
);
1571 closeQuietly(binary
);
1575 /** Computes the checksum of an nt:file */
1576 public static String
checksumFile(Node fileNode
, String algorithm
) {
1578 InputStream in
= null;
1580 data
= fileNode
.getNode(Node
.JCR_CONTENT
)
1581 .getProperty(Property
.JCR_DATA
).getBinary();
1582 in
= data
.getStream();
1583 return DigestUtils
.digest(algorithm
, in
);
1584 } catch (RepositoryException e
) {
1585 throw new ArgeoJcrException("Cannot checksum file " + fileNode
, e
);
1587 IOUtils
.closeQuietly(in
);