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
.util
.DigestUtils
;
68 /** Utility methods to simplify common JCR operations. */
69 public class JcrUtils
{
71 final private static Log log
= LogFactory
.getLog(JcrUtils
.class);
74 * Not complete yet. See
75 * http://www.day.com/specs/jcr/2.0/3_Repository_Model.html#3.2.2%20Local
78 public final static char[] INVALID_NAME_CHARACTERS
= { '/', ':', '[', ']', '|', '*', /* invalid for XML: */ '<',
81 /** Prevents instantiation */
86 * Queries one single node.
88 * @return one single node or null if none was found
89 * @throws ArgeoJcrException
90 * if more than one node was found
92 public static Node
querySingleNode(Query query
) {
93 NodeIterator nodeIterator
;
95 QueryResult queryResult
= query
.execute();
96 nodeIterator
= queryResult
.getNodes();
97 } catch (RepositoryException e
) {
98 throw new ArgeoJcrException("Cannot execute query " + query
, e
);
101 if (nodeIterator
.hasNext())
102 node
= nodeIterator
.nextNode();
106 if (nodeIterator
.hasNext())
107 throw new ArgeoJcrException("Query returned more than one node.");
111 /** Retrieves the node name from the provided path */
112 public static String
nodeNameFromPath(String path
) {
113 if (path
.equals("/"))
115 if (path
.charAt(0) != '/')
116 throw new ArgeoJcrException("Path " + path
+ " must start with a '/'");
118 if (pathT
.charAt(pathT
.length() - 1) == '/')
119 pathT
= pathT
.substring(0, pathT
.length() - 2);
121 int index
= pathT
.lastIndexOf('/');
122 return pathT
.substring(index
+ 1);
125 /** Retrieves the parent path of the provided path */
126 public static String
parentPath(String path
) {
127 if (path
.equals("/"))
128 throw new ArgeoJcrException("Root path '/' has no parent path");
129 if (path
.charAt(0) != '/')
130 throw new ArgeoJcrException("Path " + path
+ " must start with a '/'");
132 if (pathT
.charAt(pathT
.length() - 1) == '/')
133 pathT
= pathT
.substring(0, pathT
.length() - 2);
135 int index
= pathT
.lastIndexOf('/');
136 return pathT
.substring(0, index
);
139 /** The provided data as a path ('/' at the end, not the beginning) */
140 public static String
dateAsPath(Calendar cal
) {
141 return dateAsPath(cal
, false);
145 * Creates a deep path based on a URL:
146 * http://subdomain.example.com/to/content?args becomes
147 * com/example/subdomain/to/content
149 public static String
urlAsPath(String url
) {
151 URL u
= new URL(url
);
152 StringBuffer path
= new StringBuffer(url
.length());
154 path
.append(hostAsPath(u
.getHost()));
155 // we don't put port since it may not always be there and may change
156 path
.append(u
.getPath());
157 return path
.toString();
158 } catch (MalformedURLException e
) {
159 throw new ArgeoJcrException("Cannot generate URL path for " + url
, e
);
163 /** Set the {@link NodeType#NT_ADDRESS} properties based on this URL. */
164 public static void urlToAddressProperties(Node node
, String url
) {
166 URL u
= new URL(url
);
167 node
.setProperty(Property
.JCR_PROTOCOL
, u
.getProtocol());
168 node
.setProperty(Property
.JCR_HOST
, u
.getHost());
169 node
.setProperty(Property
.JCR_PORT
, Integer
.toString(u
.getPort()));
170 node
.setProperty(Property
.JCR_PATH
, normalizePath(u
.getPath()));
171 } catch (Exception e
) {
172 throw new ArgeoJcrException("Cannot set URL " + url
+ " as nt:address properties", e
);
176 /** Build URL based on the {@link NodeType#NT_ADDRESS} properties. */
177 public static String
urlFromAddressProperties(Node node
) {
179 URL u
= new URL(node
.getProperty(Property
.JCR_PROTOCOL
).getString(),
180 node
.getProperty(Property
.JCR_HOST
).getString(),
181 (int) node
.getProperty(Property
.JCR_PORT
).getLong(),
182 node
.getProperty(Property
.JCR_PATH
).getString());
184 } catch (Exception e
) {
185 throw new ArgeoJcrException("Cannot get URL from nt:address properties of " + node
, e
);
194 * Make sure that: starts with '/', do not end with '/', do not have '//'
196 public static String
normalizePath(String path
) {
197 List
<String
> tokens
= tokenize(path
);
198 StringBuffer buf
= new StringBuffer(path
.length());
199 for (String token
: tokens
) {
203 return buf
.toString();
207 * Creates a path from a FQDN, inverting the order of the component:
208 * www.argeo.org becomes org.argeo.www
210 public static String
hostAsPath(String host
) {
211 StringBuffer path
= new StringBuffer(host
.length());
212 String
[] hostTokens
= host
.split("\\.");
213 for (int i
= hostTokens
.length
- 1; i
>= 0; i
--) {
214 path
.append(hostTokens
[i
]);
218 return path
.toString();
222 * Creates a path from a UUID (e.g. 6ebda899-217d-4bf1-abe4-2839085c8f3c becomes
223 * 6ebda899-217d/4bf1/abe4/2839085c8f3c/). '/' at the end, not the beginning
225 public static String
uuidAsPath(String uuid
) {
226 StringBuffer path
= new StringBuffer(uuid
.length());
227 String
[] tokens
= uuid
.split("-");
228 for (int i
= 0; i
< tokens
.length
; i
++) {
229 path
.append(tokens
[i
]);
233 return path
.toString();
237 * The provided data as a path ('/' at the end, not the beginning)
242 * whether to add hour as well
244 public static String
dateAsPath(Calendar cal
, Boolean addHour
) {
245 StringBuffer buf
= new StringBuffer(14);
247 buf
.append(cal
.get(Calendar
.YEAR
));
250 int month
= cal
.get(Calendar
.MONTH
) + 1;
257 int day
= cal
.get(Calendar
.DAY_OF_MONTH
);
265 int hour
= cal
.get(Calendar
.HOUR_OF_DAY
);
272 return buf
.toString();
276 /** Converts in one call a string into a gregorian calendar. */
277 public static Calendar
parseCalendar(DateFormat dateFormat
, String value
) {
279 Date date
= dateFormat
.parse(value
);
280 Calendar calendar
= new GregorianCalendar();
281 calendar
.setTime(date
);
283 } catch (ParseException e
) {
284 throw new ArgeoJcrException("Cannot parse " + value
+ " with date format " + dateFormat
, e
);
289 /** The last element of a path. */
290 public static String
lastPathElement(String path
) {
291 if (path
.charAt(path
.length() - 1) == '/')
292 throw new ArgeoJcrException("Path " + path
+ " cannot end with '/'");
293 int index
= path
.lastIndexOf('/');
296 return path
.substring(index
+ 1);
300 * Call {@link Node#getName()} without exceptions (useful in super
303 public static String
getNameQuietly(Node node
) {
305 return node
.getName();
306 } catch (RepositoryException e
) {
307 throw new ArgeoJcrException("Cannot get name from " + node
, e
);
312 * Call {@link Node#getProperty(String)} without exceptions (useful in super
315 public static String
getStringPropertyQuietly(Node node
, String propertyName
) {
317 return node
.getProperty(propertyName
).getString();
318 } catch (RepositoryException e
) {
319 throw new ArgeoJcrException("Cannot get name from " + node
, e
);
324 * Routine that get the child with this name, adding id it does not already
327 public static Node
getOrAdd(Node parent
, String childName
, String childPrimaryNodeType
) throws RepositoryException
{
328 return parent
.hasNode(childName
) ? parent
.getNode(childName
) : parent
.addNode(childName
, childPrimaryNodeType
);
332 * Routine that get the child with this name, adding id it does not already
335 public static Node
getOrAdd(Node parent
, String childName
) throws RepositoryException
{
336 return parent
.hasNode(childName
) ? parent
.getNode(childName
) : parent
.addNode(childName
);
339 /** Convert a {@link NodeIterator} to a list of {@link Node} */
340 public static List
<Node
> nodeIteratorToList(NodeIterator nodeIterator
) {
341 List
<Node
> nodes
= new ArrayList
<Node
>();
342 while (nodeIterator
.hasNext()) {
343 nodes
.add(nodeIterator
.nextNode());
353 * Concisely get the string value of a property or null if this node doesn't
356 public static String
get(Node node
, String propertyName
) {
358 if (!node
.hasProperty(propertyName
))
360 return node
.getProperty(propertyName
).getString();
361 } catch (RepositoryException e
) {
362 throw new ArgeoJcrException("Cannot get property " + propertyName
+ " of " + node
, e
);
366 /** Concisely get the boolean value of a property */
367 public static Boolean
check(Node node
, String propertyName
) {
369 return node
.getProperty(propertyName
).getBoolean();
370 } catch (RepositoryException e
) {
371 throw new ArgeoJcrException("Cannot get property " + propertyName
+ " of " + node
, e
);
375 /** Concisely get the bytes array value of a property */
376 public static byte[] getBytes(Node node
, String propertyName
) {
378 return getBinaryAsBytes(node
.getProperty(propertyName
));
379 } catch (RepositoryException e
) {
380 throw new ArgeoJcrException("Cannot get property " + propertyName
+ " of " + node
, e
);
389 * Create sub nodes relative to a parent node
391 public static Node
mkdirs(Node parentNode
, String relativePath
) {
392 return mkdirs(parentNode
, relativePath
, null, null);
396 * Create sub nodes relative to a parent node
399 * the type of the leaf node
401 public static Node
mkdirs(Node parentNode
, String relativePath
, String nodeType
) {
402 return mkdirs(parentNode
, relativePath
, nodeType
, null);
406 * Create sub nodes relative to a parent node
409 * the type of the leaf node
411 public static Node
mkdirs(Node parentNode
, String relativePath
, String nodeType
, String intermediaryNodeType
) {
412 List
<String
> tokens
= tokenize(relativePath
);
413 Node currParent
= parentNode
;
415 for (int i
= 0; i
< tokens
.size(); i
++) {
416 String name
= tokens
.get(i
);
417 if (currParent
.hasNode(name
)) {
418 currParent
= currParent
.getNode(name
);
420 if (i
!= (tokens
.size() - 1)) {// intermediary
421 currParent
= currParent
.addNode(name
, intermediaryNodeType
);
423 currParent
= currParent
.addNode(name
, nodeType
);
428 } catch (RepositoryException e
) {
429 throw new ArgeoJcrException("Cannot mkdirs relative path " + relativePath
+ " from " + parentNode
, e
);
434 * Synchronized and save is performed, to avoid race conditions in initializers
435 * leading to duplicate nodes.
437 public synchronized static Node
mkdirsSafe(Session session
, String path
, String type
) {
439 if (session
.hasPendingChanges())
440 throw new ArgeoJcrException("Session has pending changes, save them first.");
441 Node node
= mkdirs(session
, path
, type
);
444 } catch (RepositoryException e
) {
445 discardQuietly(session
);
446 throw new ArgeoJcrException("Cannot safely make directories", e
);
450 public synchronized static Node
mkdirsSafe(Session session
, String path
) {
451 return mkdirsSafe(session
, path
, null);
454 /** Creates the nodes making path, if they don't exist. */
455 public static Node
mkdirs(Session session
, String path
) {
456 return mkdirs(session
, path
, null, null, false);
461 * the type of the leaf node
463 public static Node
mkdirs(Session session
, String path
, String type
) {
464 return mkdirs(session
, path
, type
, null, false);
468 * Creates the nodes making path, if they don't exist. This is up to the caller
469 * to save the session. Use with caution since it can create duplicate nodes if
470 * used concurrently. Requires read access to the root node of the workspace.
472 public static Node
mkdirs(Session session
, String path
, String type
, String intermediaryNodeType
,
473 Boolean versioning
) {
475 if (path
.equals('/'))
476 return session
.getRootNode();
478 if (session
.itemExists(path
)) {
479 Node node
= session
.getNode(path
);
481 if (type
!= null && !node
.isNodeType(type
) && !node
.getPath().equals("/"))
482 throw new ArgeoJcrException("Node " + node
+ " exists but is of type "
483 + node
.getPrimaryNodeType().getName() + " not of type " + type
);
484 // TODO: check versioning
488 // StringBuffer current = new StringBuffer("/");
489 // Node currentNode = session.getRootNode();
491 Node currentNode
= findClosestExistingParent(session
, path
);
492 String closestExistingParentPath
= currentNode
.getPath();
493 StringBuffer current
= new StringBuffer(closestExistingParentPath
);
494 if (!closestExistingParentPath
.endsWith("/"))
496 Iterator
<String
> it
= tokenize(path
.substring(closestExistingParentPath
.length())).iterator();
497 while (it
.hasNext()) {
498 String part
= it
.next();
499 current
.append(part
).append('/');
500 if (!session
.itemExists(current
.toString())) {
501 if (!it
.hasNext() && type
!= null)
502 currentNode
= currentNode
.addNode(part
, type
);
503 else if (it
.hasNext() && intermediaryNodeType
!= null)
504 currentNode
= currentNode
.addNode(part
, intermediaryNodeType
);
506 currentNode
= currentNode
.addNode(part
);
508 currentNode
.addMixin(NodeType
.MIX_VERSIONABLE
);
509 if (log
.isTraceEnabled())
510 log
.debug("Added folder " + part
+ " as " + current
);
512 currentNode
= (Node
) session
.getItem(current
.toString());
516 } catch (RepositoryException e
) {
517 discardQuietly(session
);
518 throw new ArgeoJcrException("Cannot mkdirs " + path
, e
);
523 private static Node
findClosestExistingParent(Session session
, String path
) throws RepositoryException
{
524 int idx
= path
.lastIndexOf('/');
526 return session
.getRootNode();
527 String parentPath
= path
.substring(0, idx
);
528 if (session
.itemExists(parentPath
))
529 return session
.getNode(parentPath
);
531 return findClosestExistingParent(session
, parentPath
);
534 /** Convert a path to the list of its tokens */
535 public static List
<String
> tokenize(String path
) {
536 List
<String
> tokens
= new ArrayList
<String
>();
537 boolean optimized
= false;
539 String
[] rawTokens
= path
.split("/");
540 for (String token
: rawTokens
) {
541 if (!token
.equals(""))
545 StringBuffer curr
= new StringBuffer();
546 char[] arr
= path
.toCharArray();
547 chars
: for (int i
= 0; i
< arr
.length
; i
++) {
550 if (i
== 0 || (i
== arr
.length
- 1))
552 if (curr
.length() > 0) {
553 tokens
.add(curr
.toString());
554 curr
= new StringBuffer();
559 if (curr
.length() > 0) {
560 tokens
.add(curr
.toString());
561 curr
= new StringBuffer();
564 return Collections
.unmodifiableList(tokens
);
568 // * use {@link #mkdirs(Session, String, String, String, Boolean)} instead.
573 // public static Node mkdirs(Session session, String path, String type,
574 // Boolean versioning) {
575 // return mkdirs(session, path, type, type, false);
579 * Safe and repository implementation independent registration of a namespace.
581 public static void registerNamespaceSafely(Session session
, String prefix
, String uri
) {
583 registerNamespaceSafely(session
.getWorkspace().getNamespaceRegistry(), prefix
, uri
);
584 } catch (RepositoryException e
) {
585 throw new ArgeoJcrException("Cannot find namespace registry", e
);
590 * Safe and repository implementation independent registration of a namespace.
592 public static void registerNamespaceSafely(NamespaceRegistry nr
, String prefix
, String uri
) {
594 String
[] prefixes
= nr
.getPrefixes();
595 for (String pref
: prefixes
)
596 if (pref
.equals(prefix
)) {
597 String registeredUri
= nr
.getURI(pref
);
598 if (!registeredUri
.equals(uri
))
599 throw new ArgeoJcrException("Prefix " + pref
+ " already registered for URI " + registeredUri
600 + " which is different from provided URI " + uri
);
604 nr
.registerNamespace(prefix
, uri
);
605 } catch (RepositoryException e
) {
606 throw new ArgeoJcrException("Cannot register namespace " + uri
+ " under prefix " + prefix
, e
);
610 /** Recursively outputs the contents of the given node. */
611 public static void debug(Node node
) {
615 /** Recursively outputs the contents of the given node. */
616 public static void debug(Node node
, Log log
) {
618 // First output the node path
619 log
.debug(node
.getPath());
620 // Skip the virtual (and large!) jcr:system subtree
621 if (node
.getName().equals("jcr:system")) {
625 // Then the children nodes (recursive)
626 NodeIterator it
= node
.getNodes();
627 while (it
.hasNext()) {
628 Node childNode
= it
.nextNode();
629 debug(childNode
, log
);
632 // Then output the properties
633 PropertyIterator properties
= node
.getProperties();
634 // log.debug("Property are : ");
636 properties
: while (properties
.hasNext()) {
637 Property property
= properties
.nextProperty();
638 if (property
.getType() == PropertyType
.BINARY
)
639 continue properties
;// skip
640 if (property
.getDefinition().isMultiple()) {
641 // A multi-valued property, print all values
642 Value
[] values
= property
.getValues();
643 for (int i
= 0; i
< values
.length
; i
++) {
644 log
.debug(property
.getPath() + "=" + values
[i
].getString());
647 // A single-valued property
648 log
.debug(property
.getPath() + "=" + property
.getString());
651 } catch (Exception e
) {
652 log
.error("Could not debug " + node
, e
);
657 /** Logs the effective access control policies */
658 public static void logEffectiveAccessPolicies(Node node
) {
660 logEffectiveAccessPolicies(node
.getSession(), node
.getPath());
661 } catch (RepositoryException e
) {
662 log
.error("Cannot log effective access policies of " + node
, e
);
666 /** Logs the effective access control policies */
667 public static void logEffectiveAccessPolicies(Session session
, String path
) {
668 if (!log
.isDebugEnabled())
672 AccessControlPolicy
[] effectivePolicies
= session
.getAccessControlManager().getEffectivePolicies(path
);
673 if (effectivePolicies
.length
> 0) {
674 for (AccessControlPolicy policy
: effectivePolicies
) {
675 if (policy
instanceof AccessControlList
) {
676 AccessControlList acl
= (AccessControlList
) policy
;
677 log
.debug("Access control list for " + path
+ "\n" + accessControlListSummary(acl
));
681 log
.debug("No effective access control policy for " + path
);
683 } catch (RepositoryException e
) {
684 log
.error("Cannot log effective access policies of " + path
, e
);
688 /** Returns a human-readable summary of this access control list. */
689 public static String
accessControlListSummary(AccessControlList acl
) {
690 StringBuffer buf
= new StringBuffer("");
692 for (AccessControlEntry ace
: acl
.getAccessControlEntries()) {
693 buf
.append('\t').append(ace
.getPrincipal().getName()).append('\n');
694 for (Privilege priv
: ace
.getPrivileges())
695 buf
.append("\t\t").append(priv
.getName()).append('\n');
697 return buf
.toString();
698 } catch (RepositoryException e
) {
699 throw new ArgeoJcrException("Cannot write summary of " + acl
, e
);
704 * Copies recursively the content of a node to another one. Do NOT copy the
705 * property values of {@link NodeType#MIX_CREATED} and
706 * {@link NodeType#MIX_LAST_MODIFIED}, but update the
707 * {@link Property#JCR_LAST_MODIFIED} and {@link Property#JCR_LAST_MODIFIED_BY}
708 * properties if the target node has the {@link NodeType#MIX_LAST_MODIFIED}
711 public static void copy(Node fromNode
, Node toNode
) {
713 if (toNode
.getDefinition().isProtected())
716 // process properties
717 PropertyIterator pit
= fromNode
.getProperties();
718 properties
: while (pit
.hasNext()) {
719 Property fromProperty
= pit
.nextProperty();
720 String propertyName
= fromProperty
.getName();
721 if (toNode
.hasProperty(propertyName
) && toNode
.getProperty(propertyName
).getDefinition().isProtected())
724 if (fromProperty
.getDefinition().isProtected())
727 if (propertyName
.equals("jcr:created") || propertyName
.equals("jcr:createdBy")
728 || propertyName
.equals("jcr:lastModified") || propertyName
.equals("jcr:lastModifiedBy"))
731 if (fromProperty
.isMultiple()) {
732 toNode
.setProperty(propertyName
, fromProperty
.getValues());
734 toNode
.setProperty(propertyName
, fromProperty
.getValue());
738 // update jcr:lastModified and jcr:lastModifiedBy in toNode in case
739 // they existed, before adding the mixins
740 updateLastModified(toNode
);
743 for (NodeType mixinType
: fromNode
.getMixinNodeTypes()) {
744 toNode
.addMixin(mixinType
.getName());
747 // process children nodes
748 NodeIterator nit
= fromNode
.getNodes();
749 while (nit
.hasNext()) {
750 Node fromChild
= nit
.nextNode();
751 Integer index
= fromChild
.getIndex();
752 String nodeRelPath
= fromChild
.getName() + "[" + index
+ "]";
754 if (toNode
.hasNode(nodeRelPath
))
755 toChild
= toNode
.getNode(nodeRelPath
);
757 toChild
= toNode
.addNode(fromChild
.getName(), fromChild
.getPrimaryNodeType().getName());
758 copy(fromChild
, toChild
);
760 } catch (RepositoryException e
) {
761 throw new ArgeoJcrException("Cannot copy " + fromNode
+ " to " + toNode
, e
);
766 * Check whether all first-level properties (except jcr:* properties) are equal.
767 * Skip jcr:* properties
769 public static Boolean
allPropertiesEquals(Node reference
, Node observed
, Boolean onlyCommonProperties
) {
771 PropertyIterator pit
= reference
.getProperties();
772 props
: while (pit
.hasNext()) {
773 Property propReference
= pit
.nextProperty();
774 String propName
= propReference
.getName();
775 if (propName
.startsWith("jcr:"))
778 if (!observed
.hasProperty(propName
))
779 if (onlyCommonProperties
)
783 // TODO: deal with multiple property values?
784 if (!observed
.getProperty(propName
).getValue().equals(propReference
.getValue()))
788 } catch (RepositoryException e
) {
789 throw new ArgeoJcrException("Cannot check all properties equals of " + reference
+ " and " + observed
, e
);
793 public static Map
<String
, PropertyDiff
> diffProperties(Node reference
, Node observed
) {
794 Map
<String
, PropertyDiff
> diffs
= new TreeMap
<String
, PropertyDiff
>();
795 diffPropertiesLevel(diffs
, null, reference
, observed
);
800 * Compare the properties of two nodes. Recursivity to child nodes is not yet
801 * supported. Skip jcr:* properties.
803 static void diffPropertiesLevel(Map
<String
, PropertyDiff
> diffs
, String baseRelPath
, Node reference
,
806 // check removed and modified
807 PropertyIterator pit
= reference
.getProperties();
808 props
: while (pit
.hasNext()) {
809 Property p
= pit
.nextProperty();
810 String name
= p
.getName();
811 if (name
.startsWith("jcr:"))
814 if (!observed
.hasProperty(name
)) {
815 String relPath
= propertyRelPath(baseRelPath
, name
);
816 PropertyDiff pDiff
= new PropertyDiff(PropertyDiff
.REMOVED
, relPath
, p
.getValue(), null);
817 diffs
.put(relPath
, pDiff
);
819 if (p
.isMultiple()) {
820 // FIXME implement multiple
822 Value referenceValue
= p
.getValue();
823 Value newValue
= observed
.getProperty(name
).getValue();
824 if (!referenceValue
.equals(newValue
)) {
825 String relPath
= propertyRelPath(baseRelPath
, name
);
826 PropertyDiff pDiff
= new PropertyDiff(PropertyDiff
.MODIFIED
, relPath
, referenceValue
,
828 diffs
.put(relPath
, pDiff
);
834 pit
= observed
.getProperties();
835 props
: while (pit
.hasNext()) {
836 Property p
= pit
.nextProperty();
837 String name
= p
.getName();
838 if (name
.startsWith("jcr:"))
840 if (!reference
.hasProperty(name
)) {
841 if (p
.isMultiple()) {
842 // FIXME implement multiple
844 String relPath
= propertyRelPath(baseRelPath
, name
);
845 PropertyDiff pDiff
= new PropertyDiff(PropertyDiff
.ADDED
, relPath
, null, p
.getValue());
846 diffs
.put(relPath
, pDiff
);
850 } catch (RepositoryException e
) {
851 throw new ArgeoJcrException("Cannot diff " + reference
+ " and " + observed
, e
);
856 * Compare only a restricted list of properties of two nodes. No recursivity.
859 public static Map
<String
, PropertyDiff
> diffProperties(Node reference
, Node observed
, List
<String
> properties
) {
860 Map
<String
, PropertyDiff
> diffs
= new TreeMap
<String
, PropertyDiff
>();
862 Iterator
<String
> pit
= properties
.iterator();
864 props
: while (pit
.hasNext()) {
865 String name
= pit
.next();
866 if (!reference
.hasProperty(name
)) {
867 if (!observed
.hasProperty(name
))
869 Value val
= observed
.getProperty(name
).getValue();
871 // empty String but not null
872 if ("".equals(val
.getString()))
874 } catch (Exception e
) {
875 // not parseable as String, silent
877 PropertyDiff pDiff
= new PropertyDiff(PropertyDiff
.ADDED
, name
, null, val
);
878 diffs
.put(name
, pDiff
);
879 } else if (!observed
.hasProperty(name
)) {
880 PropertyDiff pDiff
= new PropertyDiff(PropertyDiff
.REMOVED
, name
,
881 reference
.getProperty(name
).getValue(), null);
882 diffs
.put(name
, pDiff
);
884 Value referenceValue
= reference
.getProperty(name
).getValue();
885 Value newValue
= observed
.getProperty(name
).getValue();
886 if (!referenceValue
.equals(newValue
)) {
887 PropertyDiff pDiff
= new PropertyDiff(PropertyDiff
.MODIFIED
, name
, referenceValue
, newValue
);
888 diffs
.put(name
, pDiff
);
892 } catch (RepositoryException e
) {
893 throw new ArgeoJcrException("Cannot diff " + reference
+ " and " + observed
, e
);
898 /** Builds a property relPath to be used in the diff. */
899 private static String
propertyRelPath(String baseRelPath
, String propertyName
) {
900 if (baseRelPath
== null)
903 return baseRelPath
+ '/' + propertyName
;
907 * Normalizes a name so that it can be stored in contexts not supporting names
908 * with ':' (typically databases). Replaces ':' by '_'.
910 public static String
normalize(String name
) {
911 return name
.replace(':', '_');
915 * Replaces characters which are invalid in a JCR name by '_'. Currently not
918 * @see JcrUtils#INVALID_NAME_CHARACTERS
920 public static String
replaceInvalidChars(String name
) {
921 return replaceInvalidChars(name
, '_');
925 * Replaces characters which are invalid in a JCR name. Currently not
928 * @see JcrUtils#INVALID_NAME_CHARACTERS
930 public static String
replaceInvalidChars(String name
, char replacement
) {
931 boolean modified
= false;
932 char[] arr
= name
.toCharArray();
933 for (int i
= 0; i
< arr
.length
; i
++) {
935 invalid
: for (char invalid
: INVALID_NAME_CHARACTERS
) {
937 arr
[i
] = replacement
;
944 return new String(arr
);
946 // do not create new object if unnecessary
951 // * Removes forbidden characters from a path, replacing them with '_'
953 // * @deprecated use {@link #replaceInvalidChars(String)} instead
955 // public static String removeForbiddenCharacters(String str) {
956 // return str.replace('[', '_').replace(']', '_').replace('/', '_').replace('*', '_');
960 /** Cleanly disposes a {@link Binary} even if it is null. */
961 public static void closeQuietly(Binary binary
) {
967 /** Retrieve a {@link Binary} as a byte array */
968 public static byte[] getBinaryAsBytes(Property property
) {
969 ByteArrayOutputStream out
= new ByteArrayOutputStream();
970 InputStream in
= null;
971 Binary binary
= null;
973 binary
= property
.getBinary();
974 in
= binary
.getStream();
975 IOUtils
.copy(in
, out
);
976 return out
.toByteArray();
977 } catch (Exception e
) {
978 throw new ArgeoJcrException("Cannot read binary " + property
+ " as bytes", e
);
980 IOUtils
.closeQuietly(out
);
981 IOUtils
.closeQuietly(in
);
982 closeQuietly(binary
);
986 /** Writes a {@link Binary} from a byte array */
987 public static void setBinaryAsBytes(Node node
, String property
, byte[] bytes
) {
988 InputStream in
= null;
989 Binary binary
= null;
991 in
= new ByteArrayInputStream(bytes
);
992 binary
= node
.getSession().getValueFactory().createBinary(in
);
993 node
.setProperty(property
, binary
);
994 } catch (Exception e
) {
995 throw new ArgeoJcrException("Cannot read binary " + property
+ " as bytes", e
);
997 IOUtils
.closeQuietly(in
);
998 closeQuietly(binary
);
1003 * Creates depth from a string (typically a username) by adding levels based on
1004 * its first characters: "aBcD",2 becomes a/aB
1006 public static String
firstCharsToPath(String str
, Integer nbrOfChars
) {
1007 if (str
.length() < nbrOfChars
)
1008 throw new ArgeoJcrException("String " + str
+ " length must be greater or equal than " + nbrOfChars
);
1009 StringBuffer path
= new StringBuffer("");
1010 StringBuffer curr
= new StringBuffer("");
1011 for (int i
= 0; i
< nbrOfChars
; i
++) {
1012 curr
.append(str
.charAt(i
));
1014 if (i
< nbrOfChars
- 1)
1017 return path
.toString();
1021 * Discards the current changes in the session attached to this node. To be used
1022 * typically in a catch block.
1024 * @see #discardQuietly(Session)
1026 public static void discardUnderlyingSessionQuietly(Node node
) {
1028 discardQuietly(node
.getSession());
1029 } catch (RepositoryException e
) {
1030 log
.warn("Cannot quietly discard session of node " + node
+ ": " + e
.getMessage());
1035 * Discards the current changes in a session by calling
1036 * {@link Session#refresh(boolean)} with <code>false</code>, only logging
1037 * potential errors when doing so. To be used typically in a catch block.
1039 public static void discardQuietly(Session session
) {
1041 if (session
!= null)
1042 session
.refresh(false);
1043 } catch (RepositoryException e
) {
1044 log
.warn("Cannot quietly discard session " + session
+ ": " + e
.getMessage());
1049 * Login to a workspace with implicit credentials, creates the workspace with
1050 * these credentials if it does not already exist.
1052 public static Session
loginOrCreateWorkspace(Repository repository
, String workspaceName
)
1053 throws RepositoryException
{
1054 Session workspaceSession
= null;
1055 Session defaultSession
= null;
1058 workspaceSession
= repository
.login(workspaceName
);
1059 } catch (NoSuchWorkspaceException e
) {
1060 // try to create workspace
1061 defaultSession
= repository
.login();
1062 defaultSession
.getWorkspace().createWorkspace(workspaceName
);
1063 workspaceSession
= repository
.login(workspaceName
);
1065 return workspaceSession
;
1067 logoutQuietly(defaultSession
);
1071 /** Logs out the session, not throwing any exception, even if it is null. */
1072 public static void logoutQuietly(Session session
) {
1074 if (session
!= null)
1075 if (session
.isLive())
1077 } catch (Exception e
) {
1083 * Convenient method to add a listener. uuids passed as null, deep=true,
1084 * local=true, only one node type
1086 public static void addListener(Session session
, EventListener listener
, int eventTypes
, String basePath
,
1089 session
.getWorkspace().getObservationManager().addEventListener(listener
, eventTypes
, basePath
, true, null,
1090 nodeType
== null ?
null : new String
[] { nodeType
}, true);
1091 } catch (RepositoryException e
) {
1092 throw new ArgeoJcrException("Cannot add JCR listener " + listener
+ " to session " + session
, e
);
1096 /** Removes a listener without throwing exception */
1097 public static void removeListenerQuietly(Session session
, EventListener listener
) {
1098 if (session
== null || !session
.isLive())
1101 session
.getWorkspace().getObservationManager().removeEventListener(listener
);
1102 } catch (RepositoryException e
) {
1108 * Quietly unregisters an {@link EventListener} from the udnerlying workspace of
1111 public static void unregisterQuietly(Node node
, EventListener eventListener
) {
1113 unregisterQuietly(node
.getSession().getWorkspace(), eventListener
);
1114 } catch (RepositoryException e
) {
1116 if (log
.isTraceEnabled())
1117 log
.trace("Could not unregister event listener " + eventListener
);
1121 /** Quietly unregisters an {@link EventListener} from this workspace */
1122 public static void unregisterQuietly(Workspace workspace
, EventListener eventListener
) {
1123 if (eventListener
== null)
1126 workspace
.getObservationManager().removeEventListener(eventListener
);
1127 } catch (RepositoryException e
) {
1129 if (log
.isTraceEnabled())
1130 log
.trace("Could not unregister event listener " + eventListener
);
1135 * If this node is has the {@link NodeType#MIX_LAST_MODIFIED} mixin, it updates
1136 * the {@link Property#JCR_LAST_MODIFIED} property with the current time and the
1137 * {@link Property#JCR_LAST_MODIFIED_BY} property with the underlying session
1138 * user id. In Jackrabbit 2.x,
1139 * <a href="https://issues.apache.org/jira/browse/JCR-2233">these properties are
1140 * not automatically updated</a>, hence the need for manual update. The session
1143 public static void updateLastModified(Node node
) {
1145 if (!node
.isNodeType(NodeType
.MIX_LAST_MODIFIED
))
1146 node
.addMixin(NodeType
.MIX_LAST_MODIFIED
);
1147 node
.setProperty(Property
.JCR_LAST_MODIFIED
, new GregorianCalendar());
1148 node
.setProperty(Property
.JCR_LAST_MODIFIED_BY
, node
.getSession().getUserID());
1149 } catch (RepositoryException e
) {
1150 throw new ArgeoJcrException("Cannot update last modified on " + node
, e
);
1155 * Update lastModified recursively until this parent.
1160 * the base path, null is equivalent to "/"
1162 public static void updateLastModifiedAndParents(Node node
, String untilPath
) {
1164 if (untilPath
!= null && !node
.getPath().startsWith(untilPath
))
1165 throw new ArgeoJcrException(node
+ " is not under " + untilPath
);
1166 updateLastModified(node
);
1167 if (untilPath
== null) {
1168 if (!node
.getPath().equals("/"))
1169 updateLastModifiedAndParents(node
.getParent(), untilPath
);
1171 if (!node
.getPath().equals(untilPath
))
1172 updateLastModifiedAndParents(node
.getParent(), untilPath
);
1174 } catch (RepositoryException e
) {
1175 throw new ArgeoJcrException("Cannot update lastModified from " + node
+ " until " + untilPath
, e
);
1180 * Returns a String representing the short version (see
1181 * <a href="http://jackrabbit.apache.org/node-type-notation.html"> Node type
1182 * Notation </a> attributes grammar) of the main business attributes of this
1183 * property definition
1187 public static String
getPropertyDefinitionAsString(Property prop
) {
1188 StringBuffer sbuf
= new StringBuffer();
1190 if (prop
.getDefinition().isAutoCreated())
1192 if (prop
.getDefinition().isMandatory())
1194 if (prop
.getDefinition().isProtected())
1196 if (prop
.getDefinition().isMultiple())
1198 } catch (RepositoryException re
) {
1199 throw new ArgeoJcrException("unexpected error while getting property definition as String", re
);
1201 return sbuf
.toString();
1205 * Estimate the sub tree size from current node. Computation is based on the Jcr
1206 * {@link Property#getLength()} method. Note : it is not the exact size used on
1207 * the disk by the current part of the JCR Tree.
1210 public static long getNodeApproxSize(Node node
) {
1211 long curNodeSize
= 0;
1213 PropertyIterator pi
= node
.getProperties();
1214 while (pi
.hasNext()) {
1215 Property prop
= pi
.nextProperty();
1216 if (prop
.isMultiple()) {
1217 int nb
= prop
.getLengths().length
;
1218 for (int i
= 0; i
< nb
; i
++) {
1219 curNodeSize
+= (prop
.getLengths()[i
] > 0 ? prop
.getLengths()[i
] : 0);
1222 curNodeSize
+= (prop
.getLength() > 0 ? prop
.getLength() : 0);
1225 NodeIterator ni
= node
.getNodes();
1226 while (ni
.hasNext())
1227 curNodeSize
+= getNodeApproxSize(ni
.nextNode());
1229 } catch (RepositoryException re
) {
1230 throw new ArgeoJcrException("Unexpected error while recursively determining node size.", re
);
1239 * Convenience method for adding a single privilege to a principal (user or
1240 * role), typically jcr:all
1242 public synchronized static void addPrivilege(Session session
, String path
, String principal
, String privilege
)
1243 throws RepositoryException
{
1244 List
<Privilege
> privileges
= new ArrayList
<Privilege
>();
1245 privileges
.add(session
.getAccessControlManager().privilegeFromName(privilege
));
1246 addPrivileges(session
, path
, new SimplePrincipal(principal
), privileges
);
1250 * Add privileges on a path to a {@link Principal}. The path must already exist.
1251 * Session is saved. Synchronized to prevent concurrent modifications of the
1254 public synchronized static Boolean
addPrivileges(Session session
, String path
, Principal principal
,
1255 List
<Privilege
> privs
) throws RepositoryException
{
1256 // make sure the session is in line with the persisted state
1257 session
.refresh(false);
1258 AccessControlManager acm
= session
.getAccessControlManager();
1259 AccessControlList acl
= getAccessControlList(acm
, path
);
1261 accessControlEntries
: for (AccessControlEntry ace
: acl
.getAccessControlEntries()) {
1262 Principal currentPrincipal
= ace
.getPrincipal();
1263 if (currentPrincipal
.getName().equals(principal
.getName())) {
1264 Privilege
[] currentPrivileges
= ace
.getPrivileges();
1265 if (currentPrivileges
.length
!= privs
.size())
1266 break accessControlEntries
;
1267 for (int i
= 0; i
< currentPrivileges
.length
; i
++) {
1268 Privilege currP
= currentPrivileges
[i
];
1269 Privilege p
= privs
.get(i
);
1270 if (!currP
.getName().equals(p
.getName())) {
1271 break accessControlEntries
;
1278 Privilege
[] privileges
= privs
.toArray(new Privilege
[privs
.size()]);
1279 acl
.addAccessControlEntry(principal
, privileges
);
1280 acm
.setPolicy(path
, acl
);
1281 if (log
.isDebugEnabled()) {
1282 StringBuffer privBuf
= new StringBuffer();
1283 for (Privilege priv
: privs
)
1284 privBuf
.append(priv
.getName());
1285 log
.debug("Added privileges " + privBuf
+ " to " + principal
.getName() + " on " + path
+ " in '"
1286 + session
.getWorkspace().getName() + "'");
1288 session
.refresh(true);
1293 /** Gets access control list for this path, throws exception if not found */
1294 public synchronized static AccessControlList
getAccessControlList(AccessControlManager acm
, String path
)
1295 throws RepositoryException
{
1296 // search for an access control list
1297 AccessControlList acl
= null;
1298 AccessControlPolicyIterator policyIterator
= acm
.getApplicablePolicies(path
);
1299 if (policyIterator
.hasNext()) {
1300 while (policyIterator
.hasNext()) {
1301 AccessControlPolicy acp
= policyIterator
.nextAccessControlPolicy();
1302 if (acp
instanceof AccessControlList
)
1303 acl
= ((AccessControlList
) acp
);
1306 AccessControlPolicy
[] existingPolicies
= acm
.getPolicies(path
);
1307 for (AccessControlPolicy acp
: existingPolicies
) {
1308 if (acp
instanceof AccessControlList
)
1309 acl
= ((AccessControlList
) acp
);
1315 throw new ArgeoJcrException("ACL not found at " + path
);
1318 /** Clear authorizations for a user at this path */
1319 public synchronized static void clearAccessControList(Session session
, String path
, String username
)
1320 throws RepositoryException
{
1321 AccessControlManager acm
= session
.getAccessControlManager();
1322 AccessControlList acl
= getAccessControlList(acm
, path
);
1323 for (AccessControlEntry ace
: acl
.getAccessControlEntries()) {
1324 if (ace
.getPrincipal().getName().equals(username
)) {
1325 acl
.removeAccessControlEntry(ace
);
1328 // the new access control list must be applied otherwise this call:
1329 // acl.removeAccessControlEntry(ace); has no effect
1330 acm
.setPolicy(path
, acl
);
1337 * Creates the nodes making the path as {@link NodeType#NT_FOLDER}
1339 public static Node
mkfolders(Session session
, String path
) {
1340 return mkdirs(session
, path
, NodeType
.NT_FOLDER
, NodeType
.NT_FOLDER
, false);
1344 * Copy only nt:folder and nt:file, without their additional types and
1348 * if true copies folders as well, otherwise only first level files
1349 * @return how many files were copied
1351 public static Long
copyFiles(Node fromNode
, Node toNode
, Boolean recursive
, JcrMonitor monitor
, boolean onlyAdd
) {
1354 Binary binary
= null;
1355 InputStream in
= null;
1357 NodeIterator fromChildren
= fromNode
.getNodes();
1358 children
: while (fromChildren
.hasNext()) {
1359 if (monitor
!= null && monitor
.isCanceled())
1360 throw new ArgeoJcrException("Copy cancelled before it was completed");
1362 Node fromChild
= fromChildren
.nextNode();
1363 String fileName
= fromChild
.getName();
1364 if (fromChild
.isNodeType(NodeType
.NT_FILE
)) {
1365 if (onlyAdd
&& toNode
.hasNode(fileName
)) {
1366 monitor
.subTask("Skip existing " + fileName
);
1370 if (monitor
!= null)
1371 monitor
.subTask("Copy " + fileName
);
1372 binary
= fromChild
.getNode(Node
.JCR_CONTENT
).getProperty(Property
.JCR_DATA
).getBinary();
1373 in
= binary
.getStream();
1374 copyStreamAsFile(toNode
, fileName
, in
);
1375 IOUtils
.closeQuietly(in
);
1376 closeQuietly(binary
);
1379 toNode
.getSession().save();
1382 if (log
.isDebugEnabled())
1383 log
.debug("Copied file " + fromChild
.getPath());
1384 if (monitor
!= null)
1386 } else if (fromChild
.isNodeType(NodeType
.NT_FOLDER
) && recursive
) {
1388 if (toNode
.hasNode(fileName
)) {
1389 toChildFolder
= toNode
.getNode(fileName
);
1390 if (!toChildFolder
.isNodeType(NodeType
.NT_FOLDER
))
1391 throw new ArgeoJcrException(toChildFolder
+ " is not of type nt:folder");
1393 toChildFolder
= toNode
.addNode(fileName
, NodeType
.NT_FOLDER
);
1396 toNode
.getSession().save();
1398 count
= count
+ copyFiles(fromChild
, toChildFolder
, recursive
, monitor
, onlyAdd
);
1402 } catch (RepositoryException e
) {
1403 throw new ArgeoJcrException("Cannot copy files between " + fromNode
+ " and " + toNode
);
1405 // in case there was an exception
1406 IOUtils
.closeQuietly(in
);
1407 closeQuietly(binary
);
1412 * Iteratively count all file nodes in subtree, inefficient but can be useful
1413 * when query are poorly supported, such as in remoting.
1415 public static Long
countFiles(Node node
) {
1416 Long localCount
= 0l;
1418 for (NodeIterator nit
= node
.getNodes(); nit
.hasNext();) {
1419 Node child
= nit
.nextNode();
1420 if (child
.isNodeType(NodeType
.NT_FOLDER
))
1421 localCount
= localCount
+ countFiles(child
);
1422 else if (child
.isNodeType(NodeType
.NT_FILE
))
1423 localCount
= localCount
+ 1;
1425 } catch (RepositoryException e
) {
1426 throw new ArgeoJcrException("Cannot count all children of " + node
);
1432 * Copy a file as an nt:file, assuming an nt:folder hierarchy. The session is
1435 * @return the created file node
1437 public static Node
copyFile(Node folderNode
, File file
) {
1438 InputStream in
= null;
1440 in
= new FileInputStream(file
);
1441 return copyStreamAsFile(folderNode
, file
.getName(), in
);
1442 } catch (IOException e
) {
1443 throw new ArgeoJcrException("Cannot copy file " + file
+ " under " + folderNode
, e
);
1445 IOUtils
.closeQuietly(in
);
1449 /** Copy bytes as an nt:file */
1450 public static Node
copyBytesAsFile(Node folderNode
, String fileName
, byte[] bytes
) {
1451 InputStream in
= null;
1453 in
= new ByteArrayInputStream(bytes
);
1454 return copyStreamAsFile(folderNode
, fileName
, in
);
1455 } catch (Exception e
) {
1456 throw new ArgeoJcrException("Cannot copy file " + fileName
+ " under " + folderNode
, e
);
1458 IOUtils
.closeQuietly(in
);
1463 * Copy a stream as an nt:file, assuming an nt:folder hierarchy. The session is
1466 * @return the created file node
1468 public static Node
copyStreamAsFile(Node folderNode
, String fileName
, InputStream in
) {
1469 Binary binary
= null;
1473 if (folderNode
.hasNode(fileName
)) {
1474 fileNode
= folderNode
.getNode(fileName
);
1475 if (!fileNode
.isNodeType(NodeType
.NT_FILE
))
1476 throw new ArgeoJcrException(fileNode
+ " is not of type nt:file");
1477 // we assume that the content node is already there
1478 contentNode
= fileNode
.getNode(Node
.JCR_CONTENT
);
1480 fileNode
= folderNode
.addNode(fileName
, NodeType
.NT_FILE
);
1481 contentNode
= fileNode
.addNode(Node
.JCR_CONTENT
, NodeType
.NT_RESOURCE
);
1483 binary
= contentNode
.getSession().getValueFactory().createBinary(in
);
1484 contentNode
.setProperty(Property
.JCR_DATA
, binary
);
1486 } catch (Exception e
) {
1487 throw new ArgeoJcrException("Cannot create file node " + fileName
+ " under " + folderNode
, e
);
1489 closeQuietly(binary
);
1493 /** Computes the checksum of an nt:file */
1494 public static String
checksumFile(Node fileNode
, String algorithm
) {
1496 InputStream in
= null;
1498 data
= fileNode
.getNode(Node
.JCR_CONTENT
).getProperty(Property
.JCR_DATA
).getBinary();
1499 in
= data
.getStream();
1500 return DigestUtils
.digest(algorithm
, in
);
1501 } catch (RepositoryException e
) {
1502 throw new ArgeoJcrException("Cannot checksum file " + fileNode
, e
);
1504 IOUtils
.closeQuietly(in
);