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
.ArgeoMonitor
;
68 import org
.argeo
.util
.security
.DigestUtils
;
69 import org
.argeo
.util
.security
.SimplePrincipal
;
71 /** Utility methods to simplify common JCR operations. */
72 public class JcrUtils
implements ArgeoJcrConstants
{
74 final private static Log log
= LogFactory
.getLog(JcrUtils
.class);
77 * Not complete yet. See
78 * http://www.day.com/specs/jcr/2.0/3_Repository_Model.html#3.2.2%20Local
81 public final static char[] INVALID_NAME_CHARACTERS
= { '/', ':', '[', ']',
87 /** Prevents instantiation */
92 * Queries one single node.
94 * @return one single node or null if none was found
95 * @throws ArgeoException
96 * if more than one node was found
98 public static Node
querySingleNode(Query query
) {
99 NodeIterator nodeIterator
;
101 QueryResult queryResult
= query
.execute();
102 nodeIterator
= queryResult
.getNodes();
103 } catch (RepositoryException e
) {
104 throw new ArgeoException("Cannot execute query " + query
, e
);
107 if (nodeIterator
.hasNext())
108 node
= nodeIterator
.nextNode();
112 if (nodeIterator
.hasNext())
113 throw new ArgeoException("Query returned more than one node.");
117 /** Retrieves the node name from the provided path */
118 public static String
nodeNameFromPath(String path
) {
119 if (path
.equals("/"))
121 if (path
.charAt(0) != '/')
122 throw new ArgeoException("Path " + path
+ " must start with a '/'");
124 if (pathT
.charAt(pathT
.length() - 1) == '/')
125 pathT
= pathT
.substring(0, pathT
.length() - 2);
127 int index
= pathT
.lastIndexOf('/');
128 return pathT
.substring(index
+ 1);
131 /** Retrieves the parent path of the provided path */
132 public static String
parentPath(String path
) {
133 if (path
.equals("/"))
134 throw new ArgeoException("Root path '/' has no parent path");
135 if (path
.charAt(0) != '/')
136 throw new ArgeoException("Path " + path
+ " must start with a '/'");
138 if (pathT
.charAt(pathT
.length() - 1) == '/')
139 pathT
= pathT
.substring(0, pathT
.length() - 2);
141 int index
= pathT
.lastIndexOf('/');
142 return pathT
.substring(0, index
);
145 /** The provided data as a path ('/' at the end, not the beginning) */
146 public static String
dateAsPath(Calendar cal
) {
147 return dateAsPath(cal
, false);
151 * Creates a deep path based on a URL:
152 * http://subdomain.example.com/to/content?args =>
153 * com/example/subdomain/to/content
155 public static String
urlAsPath(String url
) {
157 URL u
= new URL(url
);
158 StringBuffer path
= new StringBuffer(url
.length());
160 path
.append(hostAsPath(u
.getHost()));
161 // we don't put port since it may not always be there and may change
162 path
.append(u
.getPath());
163 return path
.toString();
164 } catch (MalformedURLException e
) {
165 throw new ArgeoException("Cannot generate URL path for " + url
, e
);
169 /** Set the {@link NodeType#NT_ADDRESS} properties based on this URL. */
170 public static void urlToAddressProperties(Node node
, String url
) {
172 URL u
= new URL(url
);
173 node
.setProperty(Property
.JCR_PROTOCOL
, u
.getProtocol());
174 node
.setProperty(Property
.JCR_HOST
, u
.getHost());
175 node
.setProperty(Property
.JCR_PORT
, Integer
.toString(u
.getPort()));
176 node
.setProperty(Property
.JCR_PATH
, normalizePath(u
.getPath()));
177 } catch (Exception e
) {
178 throw new ArgeoException("Cannot set URL " + url
179 + " as nt:address properties", e
);
183 /** Build URL based on the {@link NodeType#NT_ADDRESS} properties. */
184 public static String
urlFromAddressProperties(Node node
) {
187 node
.getProperty(Property
.JCR_PROTOCOL
).getString(), node
188 .getProperty(Property
.JCR_HOST
).getString(),
189 (int) node
.getProperty(Property
.JCR_PORT
).getLong(), node
190 .getProperty(Property
.JCR_PATH
).getString());
192 } catch (Exception e
) {
193 throw new ArgeoException(
194 "Cannot get URL from nt:address properties of " + node
, e
);
198 /** Make sure that: starts with '/', do not end with '/', do not have '//' */
199 public static String
normalizePath(String path
) {
200 List
<String
> tokens
= tokenize(path
);
201 StringBuffer buf
= new StringBuffer(path
.length());
202 for (String token
: tokens
) {
206 return buf
.toString();
210 * Creates a path from a FQDN, inverting the order of the component:
211 * www.argeo.org => org.argeo.www
213 public static String
hostAsPath(String host
) {
214 StringBuffer path
= new StringBuffer(host
.length());
215 String
[] hostTokens
= host
.split("\\.");
216 for (int i
= hostTokens
.length
- 1; i
>= 0; i
--) {
217 path
.append(hostTokens
[i
]);
221 return path
.toString();
225 * The provided data as a path ('/' at the end, not the beginning)
230 * whether to add hour as well
232 public static String
dateAsPath(Calendar cal
, Boolean addHour
) {
233 StringBuffer buf
= new StringBuffer(14);
235 buf
.append(cal
.get(Calendar
.YEAR
));
238 int month
= cal
.get(Calendar
.MONTH
) + 1;
245 int day
= cal
.get(Calendar
.DAY_OF_MONTH
);
253 int hour
= cal
.get(Calendar
.HOUR_OF_DAY
);
260 return buf
.toString();
264 /** Converts in one call a string into a gregorian calendar. */
265 public static Calendar
parseCalendar(DateFormat dateFormat
, String value
) {
267 Date date
= dateFormat
.parse(value
);
268 Calendar calendar
= new GregorianCalendar();
269 calendar
.setTime(date
);
271 } catch (ParseException e
) {
272 throw new ArgeoException("Cannot parse " + value
273 + " with date format " + dateFormat
, e
);
278 /** The last element of a path. */
279 public static String
lastPathElement(String path
) {
280 if (path
.charAt(path
.length() - 1) == '/')
281 throw new ArgeoException("Path " + path
+ " cannot end with '/'");
282 int index
= path
.lastIndexOf('/');
284 throw new ArgeoException("Cannot find last path element for "
286 return path
.substring(index
+ 1);
290 * Call {@link Node#getName()} without exceptions (useful in super
293 public static String
getNameQuietly(Node node
) {
295 return node
.getName();
296 } catch (RepositoryException e
) {
297 throw new ArgeoException("Cannot get name from " + node
, e
);
302 * Call {@link Node#getProperty(String)} without exceptions (useful in super
305 public static String
getStringPropertyQuietly(Node node
, String propertyName
) {
307 return node
.getProperty(propertyName
).getString();
308 } catch (RepositoryException e
) {
309 throw new ArgeoException("Cannot get name from " + node
, e
);
314 * Routine that get the child with this name, adding id it does not already
317 public static Node
getOrAdd(Node parent
, String childName
,
318 String childPrimaryNodeType
) throws RepositoryException
{
319 return parent
.hasNode(childName
) ? parent
.getNode(childName
) : parent
320 .addNode(childName
, childPrimaryNodeType
);
324 * Routine that get the child with this name, adding id it does not already
327 public static Node
getOrAdd(Node parent
, String childName
)
328 throws RepositoryException
{
329 return parent
.hasNode(childName
) ? parent
.getNode(childName
) : parent
333 /** Convert a {@link NodeIterator} to a list of {@link Node} */
334 public static List
<Node
> nodeIteratorToList(NodeIterator nodeIterator
) {
335 List
<Node
> nodes
= new ArrayList
<Node
>();
336 while (nodeIterator
.hasNext()) {
337 nodes
.add(nodeIterator
.nextNode());
347 * Concisely get the string value of a property or null if this node doesn't
350 public static String
get(Node node
, String propertyName
) {
352 if (!node
.hasProperty(propertyName
))
354 return node
.getProperty(propertyName
).getString();
355 } catch (RepositoryException e
) {
356 throw new ArgeoException("Cannot get property " + propertyName
361 /** Concisely get the boolean value of a property */
362 public static Boolean
check(Node node
, String propertyName
) {
364 return node
.getProperty(propertyName
).getBoolean();
365 } catch (RepositoryException e
) {
366 throw new ArgeoException("Cannot get property " + propertyName
371 /** Concisely get the bytes array value of a property */
372 public static byte[] getBytes(Node node
, String propertyName
) {
374 return getBinaryAsBytes(node
.getProperty(propertyName
));
375 } catch (RepositoryException e
) {
376 throw new ArgeoException("Cannot get property " + propertyName
381 /** Creates the nodes making path, if they don't exist. */
382 public static Node
mkdirs(Session session
, String path
) {
383 return mkdirs(session
, path
, null, null, false);
387 * use {@link #mkdirs(Session, String, String, String, Boolean)} instead.
392 public static Node
mkdirs(Session session
, String path
, String type
,
393 Boolean versioning
) {
394 return mkdirs(session
, path
, type
, type
, false);
399 * the type of the leaf node
401 public static Node
mkdirs(Session session
, String path
, String type
) {
402 return mkdirs(session
, path
, type
, null, false);
406 * Synchronized and save is performed, to avoid race conditions in
407 * initializers leading to duplicate nodes.
409 public synchronized static Node
mkdirsSafe(Session session
, String path
,
412 if (session
.hasPendingChanges())
413 throw new ArgeoException(
414 "Session has pending changes, save them first.");
415 Node node
= mkdirs(session
, path
, type
);
418 } catch (RepositoryException e
) {
419 discardQuietly(session
);
420 throw new ArgeoException("Cannot safely make directories", e
);
424 public synchronized static Node
mkdirsSafe(Session session
, String path
) {
425 return mkdirsSafe(session
, path
, null);
429 * Creates the nodes making path, if they don't exist. This is up to the
430 * caller to save the session. Use with caution since it can create
431 * duplicate nodes if used concurrently.
433 public static Node
mkdirs(Session session
, String path
, String type
,
434 String intermediaryNodeType
, Boolean versioning
) {
436 if (path
.equals('/'))
437 return session
.getRootNode();
439 if (session
.itemExists(path
)) {
440 Node node
= session
.getNode(path
);
442 if (type
!= null && !node
.isNodeType(type
)
443 && !node
.getPath().equals("/"))
444 throw new ArgeoException("Node " + node
445 + " exists but is of type "
446 + node
.getPrimaryNodeType().getName()
447 + " not of type " + type
);
448 // TODO: check versioning
452 StringBuffer current
= new StringBuffer("/");
453 Node currentNode
= session
.getRootNode();
454 Iterator
<String
> it
= tokenize(path
).iterator();
455 while (it
.hasNext()) {
456 String part
= it
.next();
457 current
.append(part
).append('/');
458 if (!session
.itemExists(current
.toString())) {
459 if (!it
.hasNext() && type
!= null)
460 currentNode
= currentNode
.addNode(part
, type
);
461 else if (it
.hasNext() && intermediaryNodeType
!= null)
462 currentNode
= currentNode
.addNode(part
,
463 intermediaryNodeType
);
465 currentNode
= currentNode
.addNode(part
);
467 currentNode
.addMixin(NodeType
.MIX_VERSIONABLE
);
468 if (log
.isTraceEnabled())
469 log
.debug("Added folder " + part
+ " as " + current
);
471 currentNode
= (Node
) session
.getItem(current
.toString());
475 } catch (RepositoryException e
) {
476 discardQuietly(session
);
477 throw new ArgeoException("Cannot mkdirs " + path
, e
);
482 /** Convert a path to the list of its tokens */
483 public static List
<String
> tokenize(String path
) {
484 List
<String
> tokens
= new ArrayList
<String
>();
485 boolean optimized
= false;
487 String
[] rawTokens
= path
.split("/");
488 for (String token
: rawTokens
) {
489 if (!token
.equals(""))
493 StringBuffer curr
= new StringBuffer();
494 char[] arr
= path
.toCharArray();
495 chars
: for (int i
= 0; i
< arr
.length
; i
++) {
498 if (i
== 0 || (i
== arr
.length
- 1))
500 if (curr
.length() > 0) {
501 tokens
.add(curr
.toString());
502 curr
= new StringBuffer();
507 if (curr
.length() > 0) {
508 tokens
.add(curr
.toString());
509 curr
= new StringBuffer();
512 return Collections
.unmodifiableList(tokens
);
516 * Safe and repository implementation independent registration of a
519 public static void registerNamespaceSafely(Session session
, String prefix
,
522 registerNamespaceSafely(session
.getWorkspace()
523 .getNamespaceRegistry(), prefix
, uri
);
524 } catch (RepositoryException e
) {
525 throw new ArgeoException("Cannot find namespace registry", e
);
530 * Safe and repository implementation independent registration of a
533 public static void registerNamespaceSafely(NamespaceRegistry nr
,
534 String prefix
, String uri
) {
536 String
[] prefixes
= nr
.getPrefixes();
537 for (String pref
: prefixes
)
538 if (pref
.equals(prefix
)) {
539 String registeredUri
= nr
.getURI(pref
);
540 if (!registeredUri
.equals(uri
))
541 throw new ArgeoException("Prefix " + pref
542 + " already registered for URI "
544 + " which is different from provided URI "
549 nr
.registerNamespace(prefix
, uri
);
550 } catch (RepositoryException e
) {
551 throw new ArgeoException("Cannot register namespace " + uri
552 + " under prefix " + prefix
, e
);
556 /** Recursively outputs the contents of the given node. */
557 public static void debug(Node node
) {
561 /** Recursively outputs the contents of the given node. */
562 public static void debug(Node node
, Log log
) {
564 // First output the node path
565 log
.debug(node
.getPath());
566 // Skip the virtual (and large!) jcr:system subtree
567 if (node
.getName().equals("jcr:system")) {
571 // Then the children nodes (recursive)
572 NodeIterator it
= node
.getNodes();
573 while (it
.hasNext()) {
574 Node childNode
= it
.nextNode();
575 debug(childNode
, log
);
578 // Then output the properties
579 PropertyIterator properties
= node
.getProperties();
580 // log.debug("Property are : ");
582 properties
: while (properties
.hasNext()) {
583 Property property
= properties
.nextProperty();
584 if (property
.getType() == PropertyType
.BINARY
)
585 continue properties
;// skip
586 if (property
.getDefinition().isMultiple()) {
587 // A multi-valued property, print all values
588 Value
[] values
= property
.getValues();
589 for (int i
= 0; i
< values
.length
; i
++) {
590 log
.debug(property
.getPath() + "="
591 + values
[i
].getString());
594 // A single-valued property
595 log
.debug(property
.getPath() + "=" + property
.getString());
598 } catch (Exception e
) {
599 log
.error("Could not debug " + node
, e
);
604 /** Logs the effective access control policies */
605 public static void logEffectiveAccessPolicies(Node node
) {
607 logEffectiveAccessPolicies(node
.getSession(), node
.getPath());
608 } catch (RepositoryException e
) {
609 log
.error("Cannot log effective access policies of " + node
, e
);
613 /** Logs the effective access control policies */
614 public static void logEffectiveAccessPolicies(Session session
, String path
) {
615 if (!log
.isDebugEnabled())
619 AccessControlPolicy
[] effectivePolicies
= session
620 .getAccessControlManager().getEffectivePolicies(path
);
621 if (effectivePolicies
.length
> 0) {
622 for (AccessControlPolicy policy
: effectivePolicies
) {
623 if (policy
instanceof AccessControlList
) {
624 AccessControlList acl
= (AccessControlList
) policy
;
625 log
.debug("Access control list for " + path
+ "\n"
626 + accessControlListSummary(acl
));
630 log
.debug("No effective access control policy for " + path
);
632 } catch (RepositoryException e
) {
633 log
.error("Cannot log effective access policies of " + path
, e
);
637 /** Returns a human-readable summary of this access control list. */
638 public static String
accessControlListSummary(AccessControlList acl
) {
639 StringBuffer buf
= new StringBuffer("");
641 for (AccessControlEntry ace
: acl
.getAccessControlEntries()) {
642 buf
.append('\t').append(ace
.getPrincipal().getName())
644 for (Privilege priv
: ace
.getPrivileges())
645 buf
.append("\t\t").append(priv
.getName()).append('\n');
647 return buf
.toString();
648 } catch (RepositoryException e
) {
649 throw new ArgeoException("Cannot write summary of " + acl
, e
);
654 * Copies recursively the content of a node to another one. Do NOT copy the
655 * property values of {@link NodeType#MIX_CREATED} and
656 * {@link NodeType#MIX_LAST_MODIFIED}, but update the
657 * {@link Property#JCR_LAST_MODIFIED} and
658 * {@link Property#JCR_LAST_MODIFIED_BY} properties if the target node has
659 * the {@link NodeType#MIX_LAST_MODIFIED} mixin.
661 public static void copy(Node fromNode
, Node toNode
) {
663 if (toNode
.getDefinition().isProtected())
666 // process properties
667 PropertyIterator pit
= fromNode
.getProperties();
668 properties
: while (pit
.hasNext()) {
669 Property fromProperty
= pit
.nextProperty();
670 String propertyName
= fromProperty
.getName();
671 if (toNode
.hasProperty(propertyName
)
672 && toNode
.getProperty(propertyName
).getDefinition()
676 if (fromProperty
.getDefinition().isProtected())
679 if (propertyName
.equals("jcr:created")
680 || propertyName
.equals("jcr:createdBy")
681 || propertyName
.equals("jcr:lastModified")
682 || propertyName
.equals("jcr:lastModifiedBy"))
685 if (fromProperty
.isMultiple()) {
686 toNode
.setProperty(propertyName
, fromProperty
.getValues());
688 toNode
.setProperty(propertyName
, fromProperty
.getValue());
692 // update jcr:lastModified and jcr:lastModifiedBy in toNode in case
693 // they existed, before adding the mixins
694 updateLastModified(toNode
);
697 for (NodeType mixinType
: fromNode
.getMixinNodeTypes()) {
698 toNode
.addMixin(mixinType
.getName());
701 // process children nodes
702 NodeIterator nit
= fromNode
.getNodes();
703 while (nit
.hasNext()) {
704 Node fromChild
= nit
.nextNode();
705 Integer index
= fromChild
.getIndex();
706 String nodeRelPath
= fromChild
.getName() + "[" + index
+ "]";
708 if (toNode
.hasNode(nodeRelPath
))
709 toChild
= toNode
.getNode(nodeRelPath
);
711 toChild
= toNode
.addNode(fromChild
.getName(), fromChild
712 .getPrimaryNodeType().getName());
713 copy(fromChild
, toChild
);
715 } catch (RepositoryException e
) {
716 throw new ArgeoException("Cannot copy " + fromNode
+ " to "
722 * Check whether all first-level properties (except jcr:* properties) are
723 * equal. Skip jcr:* properties
725 public static Boolean
allPropertiesEquals(Node reference
, Node observed
,
726 Boolean onlyCommonProperties
) {
728 PropertyIterator pit
= reference
.getProperties();
729 props
: while (pit
.hasNext()) {
730 Property propReference
= pit
.nextProperty();
731 String propName
= propReference
.getName();
732 if (propName
.startsWith("jcr:"))
735 if (!observed
.hasProperty(propName
))
736 if (onlyCommonProperties
)
740 // TODO: deal with multiple property values?
741 if (!observed
.getProperty(propName
).getValue()
742 .equals(propReference
.getValue()))
746 } catch (RepositoryException e
) {
747 throw new ArgeoException("Cannot check all properties equals of "
748 + reference
+ " and " + observed
, e
);
752 public static Map
<String
, PropertyDiff
> diffProperties(Node reference
,
754 Map
<String
, PropertyDiff
> diffs
= new TreeMap
<String
, PropertyDiff
>();
755 diffPropertiesLevel(diffs
, null, reference
, observed
);
760 * Compare the properties of two nodes. Recursivity to child nodes is not
761 * yet supported. Skip jcr:* properties.
763 static void diffPropertiesLevel(Map
<String
, PropertyDiff
> diffs
,
764 String baseRelPath
, Node reference
, Node observed
) {
766 // check removed and modified
767 PropertyIterator pit
= reference
.getProperties();
768 props
: while (pit
.hasNext()) {
769 Property p
= pit
.nextProperty();
770 String name
= p
.getName();
771 if (name
.startsWith("jcr:"))
774 if (!observed
.hasProperty(name
)) {
775 String relPath
= propertyRelPath(baseRelPath
, name
);
776 PropertyDiff pDiff
= new PropertyDiff(PropertyDiff
.REMOVED
,
777 relPath
, p
.getValue(), null);
778 diffs
.put(relPath
, pDiff
);
780 if (p
.isMultiple()) {
781 // FIXME implement multiple
783 Value referenceValue
= p
.getValue();
784 Value newValue
= observed
.getProperty(name
).getValue();
785 if (!referenceValue
.equals(newValue
)) {
786 String relPath
= propertyRelPath(baseRelPath
, name
);
787 PropertyDiff pDiff
= new PropertyDiff(
788 PropertyDiff
.MODIFIED
, relPath
,
789 referenceValue
, newValue
);
790 diffs
.put(relPath
, pDiff
);
796 pit
= observed
.getProperties();
797 props
: while (pit
.hasNext()) {
798 Property p
= pit
.nextProperty();
799 String name
= p
.getName();
800 if (name
.startsWith("jcr:"))
802 if (!reference
.hasProperty(name
)) {
803 if (p
.isMultiple()) {
804 // FIXME implement multiple
806 String relPath
= propertyRelPath(baseRelPath
, name
);
807 PropertyDiff pDiff
= new PropertyDiff(
808 PropertyDiff
.ADDED
, relPath
, null, p
.getValue());
809 diffs
.put(relPath
, pDiff
);
813 } catch (RepositoryException e
) {
814 throw new ArgeoException("Cannot diff " + reference
+ " and "
820 * Compare only a restricted list of properties of two nodes. No
824 public static Map
<String
, PropertyDiff
> diffProperties(Node reference
,
825 Node observed
, List
<String
> properties
) {
826 Map
<String
, PropertyDiff
> diffs
= new TreeMap
<String
, PropertyDiff
>();
828 Iterator
<String
> pit
= properties
.iterator();
830 props
: while (pit
.hasNext()) {
831 String name
= pit
.next();
832 if (!reference
.hasProperty(name
)) {
833 if (!observed
.hasProperty(name
))
835 Value val
= observed
.getProperty(name
).getValue();
837 // empty String but not null
838 if ("".equals(val
.getString()))
840 } catch (Exception e
) {
841 // not parseable as String, silent
843 PropertyDiff pDiff
= new PropertyDiff(PropertyDiff
.ADDED
,
845 diffs
.put(name
, pDiff
);
846 } else if (!observed
.hasProperty(name
)) {
847 PropertyDiff pDiff
= new PropertyDiff(PropertyDiff
.REMOVED
,
848 name
, reference
.getProperty(name
).getValue(), null);
849 diffs
.put(name
, pDiff
);
851 Value referenceValue
= reference
.getProperty(name
)
853 Value newValue
= observed
.getProperty(name
).getValue();
854 if (!referenceValue
.equals(newValue
)) {
855 PropertyDiff pDiff
= new PropertyDiff(
856 PropertyDiff
.MODIFIED
, name
, referenceValue
,
858 diffs
.put(name
, pDiff
);
862 } catch (RepositoryException e
) {
863 throw new ArgeoException("Cannot diff " + reference
+ " and "
869 /** Builds a property relPath to be used in the diff. */
870 private static String
propertyRelPath(String baseRelPath
,
871 String propertyName
) {
872 if (baseRelPath
== null)
875 return baseRelPath
+ '/' + propertyName
;
879 * Normalizes a name so that it can be stored in contexts not supporting
880 * names with ':' (typically databases). Replaces ':' by '_'.
882 public static String
normalize(String name
) {
883 return name
.replace(':', '_');
887 * Replaces characters which are invalid in a JCR name by '_'. Currently not
890 * @see JcrUtils#INVALID_NAME_CHARACTERS
892 public static String
replaceInvalidChars(String name
) {
893 return replaceInvalidChars(name
, '_');
897 * Replaces characters which are invalid in a JCR name. Currently not
900 * @see JcrUtils#INVALID_NAME_CHARACTERS
902 public static String
replaceInvalidChars(String name
, char replacement
) {
903 boolean modified
= false;
904 char[] arr
= name
.toCharArray();
905 for (int i
= 0; i
< arr
.length
; i
++) {
907 invalid
: for (char invalid
: INVALID_NAME_CHARACTERS
) {
909 arr
[i
] = replacement
;
916 return new String(arr
);
918 // do not create new object if unnecessary
923 * Removes forbidden characters from a path, replacing them with '_'
925 * @deprecated use {@link #replaceInvalidChars(String)} instead
927 public static String
removeForbiddenCharacters(String str
) {
928 return str
.replace('[', '_').replace(']', '_').replace('/', '_')
933 /** Cleanly disposes a {@link Binary} even if it is null. */
934 public static void closeQuietly(Binary binary
) {
940 /** Retrieve a {@link Binary} as a byte array */
941 public static byte[] getBinaryAsBytes(Property property
) {
942 ByteArrayOutputStream out
= new ByteArrayOutputStream();
943 InputStream in
= null;
944 Binary binary
= null;
946 binary
= property
.getBinary();
947 in
= binary
.getStream();
948 IOUtils
.copy(in
, out
);
949 return out
.toByteArray();
950 } catch (Exception e
) {
951 throw new ArgeoException("Cannot read binary " + property
954 IOUtils
.closeQuietly(out
);
955 IOUtils
.closeQuietly(in
);
956 closeQuietly(binary
);
960 /** Writes a {@link Binary} from a byte array */
961 public static void setBinaryAsBytes(Node node
, String property
, byte[] bytes
) {
962 InputStream in
= null;
963 Binary binary
= null;
965 in
= new ByteArrayInputStream(bytes
);
966 binary
= node
.getSession().getValueFactory().createBinary(in
);
967 node
.setProperty(property
, binary
);
968 } catch (Exception e
) {
969 throw new ArgeoException("Cannot read binary " + property
972 IOUtils
.closeQuietly(in
);
973 closeQuietly(binary
);
978 * Creates depth from a string (typically a username) by adding levels based
979 * on its first characters: "aBcD",2 => a/aB
981 public static String
firstCharsToPath(String str
, Integer nbrOfChars
) {
982 if (str
.length() < nbrOfChars
)
983 throw new ArgeoException("String " + str
984 + " length must be greater or equal than " + nbrOfChars
);
985 StringBuffer path
= new StringBuffer("");
986 StringBuffer curr
= new StringBuffer("");
987 for (int i
= 0; i
< nbrOfChars
; i
++) {
988 curr
.append(str
.charAt(i
));
990 if (i
< nbrOfChars
- 1)
993 return path
.toString();
997 * Discards the current changes in the session attached to this node. To be
998 * used typically in a catch block.
1000 * @see #discardQuietly(Session)
1002 public static void discardUnderlyingSessionQuietly(Node node
) {
1004 discardQuietly(node
.getSession());
1005 } catch (RepositoryException e
) {
1006 log
.warn("Cannot quietly discard session of node " + node
+ ": "
1012 * Discards the current changes in a session by calling
1013 * {@link Session#refresh(boolean)} with <code>false</code>, only logging
1014 * potential errors when doing so. To be used typically in a catch block.
1016 public static void discardQuietly(Session session
) {
1018 if (session
!= null)
1019 session
.refresh(false);
1020 } catch (RepositoryException e
) {
1021 log
.warn("Cannot quietly discard session " + session
+ ": "
1027 * Login to a workspace with implicit credentials, creates the workspace
1028 * with these credentials if it does not already exist.
1030 public static Session
loginOrCreateWorkspace(Repository repository
,
1031 String workspaceName
) throws RepositoryException
{
1032 Session workspaceSession
= null;
1033 Session defaultSession
= null;
1036 workspaceSession
= repository
.login(workspaceName
);
1037 } catch (NoSuchWorkspaceException e
) {
1038 // try to create workspace
1039 defaultSession
= repository
.login();
1040 defaultSession
.getWorkspace().createWorkspace(workspaceName
);
1041 workspaceSession
= repository
.login(workspaceName
);
1043 return workspaceSession
;
1045 logoutQuietly(defaultSession
);
1049 /** Logs out the session, not throwing any exception, even if it is null. */
1050 public static void logoutQuietly(Session session
) {
1052 if (session
!= null)
1053 if (session
.isLive())
1055 } catch (Exception e
) {
1061 * Convenient method to add a listener. uuids passed as null, deep=true,
1062 * local=true, only one node type
1064 public static void addListener(Session session
, EventListener listener
,
1065 int eventTypes
, String basePath
, String nodeType
) {
1067 session
.getWorkspace()
1068 .getObservationManager()
1075 nodeType
== null ?
null : new String
[] { nodeType
},
1077 } catch (RepositoryException e
) {
1078 throw new ArgeoException("Cannot add JCR listener " + listener
1079 + " to session " + session
, e
);
1083 /** Removes a listener without throwing exception */
1084 public static void removeListenerQuietly(Session session
,
1085 EventListener listener
) {
1086 if (session
== null || !session
.isLive())
1089 session
.getWorkspace().getObservationManager()
1090 .removeEventListener(listener
);
1091 } catch (RepositoryException e
) {
1097 * Quietly unregisters an {@link EventListener} from the udnerlying
1098 * workspace of this node.
1100 public static void unregisterQuietly(Node node
, EventListener eventListener
) {
1102 unregisterQuietly(node
.getSession().getWorkspace(), eventListener
);
1103 } catch (RepositoryException e
) {
1105 if (log
.isTraceEnabled())
1106 log
.trace("Could not unregister event listener "
1111 /** Quietly unregisters an {@link EventListener} from this workspace */
1112 public static void unregisterQuietly(Workspace workspace
,
1113 EventListener eventListener
) {
1114 if (eventListener
== null)
1117 workspace
.getObservationManager()
1118 .removeEventListener(eventListener
);
1119 } catch (RepositoryException e
) {
1121 if (log
.isTraceEnabled())
1122 log
.trace("Could not unregister event listener "
1128 * If this node is has the {@link NodeType#MIX_LAST_MODIFIED} mixin, it
1129 * updates the {@link Property#JCR_LAST_MODIFIED} property with the current
1130 * time and the {@link Property#JCR_LAST_MODIFIED_BY} property with the
1131 * underlying session user id. In Jackrabbit 2.x, <a
1132 * href="https://issues.apache.org/jira/browse/JCR-2233">these properties
1133 * are not automatically updated</a>, hence the need for manual update. The
1134 * session is not saved.
1136 public static void updateLastModified(Node node
) {
1138 if (!node
.isNodeType(NodeType
.MIX_LAST_MODIFIED
))
1139 node
.addMixin(NodeType
.MIX_LAST_MODIFIED
);
1140 node
.setProperty(Property
.JCR_LAST_MODIFIED
,
1141 new GregorianCalendar());
1142 node
.setProperty(Property
.JCR_LAST_MODIFIED_BY
, node
.getSession()
1144 } catch (RepositoryException e
) {
1145 throw new ArgeoException("Cannot update last modified on " + node
,
1150 /** Update lastModified recursively until this parent. */
1151 public static void updateLastModifiedAndParents(Node node
, String untilPath
) {
1153 if (!node
.getPath().startsWith(untilPath
))
1154 throw new ArgeoException(node
+ " is not under " + untilPath
);
1155 updateLastModified(node
);
1156 if (!node
.getPath().equals(untilPath
))
1157 updateLastModifiedAndParents(node
.getParent(), untilPath
);
1158 } catch (RepositoryException e
) {
1159 throw new ArgeoException("Cannot update lastModified from " + node
1160 + " until " + untilPath
, e
);
1165 * Returns a String representing the short version (see <a
1166 * href="http://jackrabbit.apache.org/node-type-notation.html"> Node type
1167 * Notation </a> attributes grammar) of the main business attributes of this
1168 * property definition
1172 public static String
getPropertyDefinitionAsString(Property prop
) {
1173 StringBuffer sbuf
= new StringBuffer();
1175 if (prop
.getDefinition().isAutoCreated())
1177 if (prop
.getDefinition().isMandatory())
1179 if (prop
.getDefinition().isProtected())
1181 if (prop
.getDefinition().isMultiple())
1183 } catch (RepositoryException re
) {
1184 throw new ArgeoException(
1185 "unexpected error while getting property definition as String",
1188 return sbuf
.toString();
1192 * Estimate the sub tree size from current node. Computation is based on the
1193 * Jcr {@link Property.getLength()} method. Note : it is not the exact size
1194 * used on the disk by the current part of the JCR Tree.
1197 public static long getNodeApproxSize(Node node
) {
1198 long curNodeSize
= 0;
1200 PropertyIterator pi
= node
.getProperties();
1201 while (pi
.hasNext()) {
1202 Property prop
= pi
.nextProperty();
1203 if (prop
.isMultiple()) {
1204 int nb
= prop
.getLengths().length
;
1205 for (int i
= 0; i
< nb
; i
++) {
1206 curNodeSize
+= (prop
.getLengths()[i
] > 0 ? prop
1207 .getLengths()[i
] : 0);
1210 curNodeSize
+= (prop
.getLength() > 0 ? prop
.getLength() : 0);
1213 NodeIterator ni
= node
.getNodes();
1214 while (ni
.hasNext())
1215 curNodeSize
+= getNodeApproxSize(ni
.nextNode());
1217 } catch (RepositoryException re
) {
1218 throw new ArgeoException(
1219 "Unexpected error while recursively determining node size.",
1229 * Convenience method for adding a single privilege to a principal (user or
1230 * role), typically jcr:all
1232 public synchronized static void addPrivilege(Session session
, String path
,
1233 String principal
, String privilege
) throws RepositoryException
{
1234 List
<Privilege
> privileges
= new ArrayList
<Privilege
>();
1235 privileges
.add(session
.getAccessControlManager().privilegeFromName(
1237 addPrivileges(session
, path
, new SimplePrincipal(principal
), privileges
);
1241 * Add privileges on a path to a {@link Principal}. The path must already
1242 * exist. Session is saved. Synchronized to prevent concurrent modifications
1245 public synchronized static void addPrivileges(Session session
, String path
,
1246 Principal principal
, List
<Privilege
> privs
)
1247 throws RepositoryException
{
1248 // make sure the session is in line with the persisted state
1249 session
.refresh(false);
1250 AccessControlManager acm
= session
.getAccessControlManager();
1251 AccessControlList acl
= getAccessControlList(acm
, path
);
1252 acl
.addAccessControlEntry(principal
,
1253 privs
.toArray(new Privilege
[privs
.size()]));
1254 acm
.setPolicy(path
, acl
);
1255 // if (log.isTraceEnabled()) {
1256 // StringBuffer privBuf = new StringBuffer();
1257 // for (Privilege priv : privs)
1258 // privBuf.append(priv.getName());
1259 // log.trace("Added privileges " + privBuf + " to " + principal
1260 // + " on " + path);
1262 session
.refresh(true);
1266 /** Gets access control list for this path, throws exception if not found */
1267 public synchronized static AccessControlList
getAccessControlList(
1268 AccessControlManager acm
, String path
) throws RepositoryException
{
1269 // search for an access control list
1270 AccessControlList acl
= null;
1271 AccessControlPolicyIterator policyIterator
= acm
1272 .getApplicablePolicies(path
);
1273 if (policyIterator
.hasNext()) {
1274 while (policyIterator
.hasNext()) {
1275 AccessControlPolicy acp
= policyIterator
1276 .nextAccessControlPolicy();
1277 if (acp
instanceof AccessControlList
)
1278 acl
= ((AccessControlList
) acp
);
1281 AccessControlPolicy
[] existingPolicies
= acm
.getPolicies(path
);
1282 for (AccessControlPolicy acp
: existingPolicies
) {
1283 if (acp
instanceof AccessControlList
)
1284 acl
= ((AccessControlList
) acp
);
1290 throw new ArgeoException("ACL not found at " + path
);
1293 /** Clear authorizations for a user at this path */
1294 public synchronized static void clearAccessControList(Session session
,
1295 String path
, String username
) throws RepositoryException
{
1296 AccessControlManager acm
= session
.getAccessControlManager();
1297 AccessControlList acl
= getAccessControlList(acm
, path
);
1298 for (AccessControlEntry ace
: acl
.getAccessControlEntries()) {
1299 if (ace
.getPrincipal().getName().equals(username
)) {
1300 acl
.removeAccessControlEntry(ace
);
1309 * Creates the nodes making the path as {@link NodeType#NT_FOLDER}
1311 public static Node
mkfolders(Session session
, String path
) {
1312 return mkdirs(session
, path
, NodeType
.NT_FOLDER
, NodeType
.NT_FOLDER
,
1317 * Copy only nt:folder and nt:file, without their additional types and
1321 * if true copies folders as well, otherwise only first level
1323 * @return how many files were copied
1325 public static Long
copyFiles(Node fromNode
, Node toNode
, Boolean recursive
,
1326 ArgeoMonitor monitor
) {
1329 Binary binary
= null;
1330 InputStream in
= null;
1332 NodeIterator fromChildren
= fromNode
.getNodes();
1333 while (fromChildren
.hasNext()) {
1334 if (monitor
!= null && monitor
.isCanceled())
1335 throw new ArgeoException(
1336 "Copy cancelled before it was completed");
1338 Node fromChild
= fromChildren
.nextNode();
1339 String fileName
= fromChild
.getName();
1340 if (fromChild
.isNodeType(NodeType
.NT_FILE
)) {
1341 if (monitor
!= null)
1342 monitor
.subTask("Copy " + fileName
);
1343 binary
= fromChild
.getNode(Node
.JCR_CONTENT
)
1344 .getProperty(Property
.JCR_DATA
).getBinary();
1345 in
= binary
.getStream();
1346 copyStreamAsFile(toNode
, fileName
, in
);
1347 IOUtils
.closeQuietly(in
);
1348 closeQuietly(binary
);
1351 toNode
.getSession().save();
1354 if (log
.isDebugEnabled())
1355 log
.debug("Copied file " + fromChild
.getPath());
1356 if (monitor
!= null)
1358 } else if (fromChild
.isNodeType(NodeType
.NT_FOLDER
)
1361 if (toNode
.hasNode(fileName
)) {
1362 toChildFolder
= toNode
.getNode(fileName
);
1363 if (!toChildFolder
.isNodeType(NodeType
.NT_FOLDER
))
1364 throw new ArgeoException(toChildFolder
1365 + " is not of type nt:folder");
1367 toChildFolder
= toNode
.addNode(fileName
,
1368 NodeType
.NT_FOLDER
);
1371 toNode
.getSession().save();
1374 + copyFiles(fromChild
, toChildFolder
, recursive
,
1379 } catch (RepositoryException e
) {
1380 throw new ArgeoException("Cannot copy files between " + fromNode
1381 + " and " + toNode
);
1383 // in case there was an exception
1384 IOUtils
.closeQuietly(in
);
1385 closeQuietly(binary
);
1390 * Iteratively count all file nodes in subtree, inefficient but can be
1391 * useful when query are poorly supported, such as in remoting.
1393 public static Long
countFiles(Node node
) {
1394 Long localCount
= 0l;
1396 for (NodeIterator nit
= node
.getNodes(); nit
.hasNext();) {
1397 Node child
= nit
.nextNode();
1398 if (child
.isNodeType(NodeType
.NT_FOLDER
))
1399 localCount
= localCount
+ countFiles(child
);
1400 else if (child
.isNodeType(NodeType
.NT_FILE
))
1401 localCount
= localCount
+ 1;
1403 } catch (RepositoryException e
) {
1404 throw new ArgeoException("Cannot count all children of " + node
);
1410 * Copy a file as an nt:file, assuming an nt:folder hierarchy. The session
1413 * @return the created file node
1415 public static Node
copyFile(Node folderNode
, File file
) {
1416 InputStream in
= null;
1418 in
= new FileInputStream(file
);
1419 return copyStreamAsFile(folderNode
, file
.getName(), in
);
1420 } catch (IOException e
) {
1421 throw new ArgeoException("Cannot copy file " + file
+ " under "
1424 IOUtils
.closeQuietly(in
);
1428 /** Copy bytes as an nt:file */
1429 public static Node
copyBytesAsFile(Node folderNode
, String fileName
,
1431 InputStream in
= null;
1433 in
= new ByteArrayInputStream(bytes
);
1434 return copyStreamAsFile(folderNode
, fileName
, in
);
1435 } catch (Exception e
) {
1436 throw new ArgeoException("Cannot copy file " + fileName
+ " under "
1439 IOUtils
.closeQuietly(in
);
1444 * Copy a stream as an nt:file, assuming an nt:folder hierarchy. The session
1447 * @return the created file node
1449 public static Node
copyStreamAsFile(Node folderNode
, String fileName
,
1451 Binary binary
= null;
1455 if (folderNode
.hasNode(fileName
)) {
1456 fileNode
= folderNode
.getNode(fileName
);
1457 if (!fileNode
.isNodeType(NodeType
.NT_FILE
))
1458 throw new ArgeoException(fileNode
1459 + " is not of type nt:file");
1460 // we assume that the content node is already there
1461 contentNode
= fileNode
.getNode(Node
.JCR_CONTENT
);
1463 fileNode
= folderNode
.addNode(fileName
, NodeType
.NT_FILE
);
1464 contentNode
= fileNode
.addNode(Node
.JCR_CONTENT
,
1465 NodeType
.NT_RESOURCE
);
1467 binary
= contentNode
.getSession().getValueFactory()
1469 contentNode
.setProperty(Property
.JCR_DATA
, binary
);
1471 } catch (Exception e
) {
1472 throw new ArgeoException("Cannot create file node " + fileName
1473 + " under " + folderNode
, e
);
1475 closeQuietly(binary
);
1479 /** Computes the checksum of an nt:file */
1480 public static String
checksumFile(Node fileNode
, String algorithm
) {
1482 InputStream in
= null;
1484 data
= fileNode
.getNode(Node
.JCR_CONTENT
)
1485 .getProperty(Property
.JCR_DATA
).getBinary();
1486 in
= data
.getStream();
1487 return DigestUtils
.digest(algorithm
, in
);
1488 } catch (RepositoryException e
) {
1489 throw new ArgeoException("Cannot checksum file " + fileNode
, e
);
1491 IOUtils
.closeQuietly(in
);