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
.HashMap
;
35 import java
.util
.Iterator
;
36 import java
.util
.List
;
38 import java
.util
.TreeMap
;
40 import javax
.jcr
.Binary
;
41 import javax
.jcr
.NamespaceRegistry
;
42 import javax
.jcr
.NoSuchWorkspaceException
;
43 import javax
.jcr
.Node
;
44 import javax
.jcr
.NodeIterator
;
45 import javax
.jcr
.Property
;
46 import javax
.jcr
.PropertyIterator
;
47 import javax
.jcr
.PropertyType
;
48 import javax
.jcr
.Repository
;
49 import javax
.jcr
.RepositoryException
;
50 import javax
.jcr
.RepositoryFactory
;
51 import javax
.jcr
.Session
;
52 import javax
.jcr
.Value
;
53 import javax
.jcr
.Workspace
;
54 import javax
.jcr
.nodetype
.NodeType
;
55 import javax
.jcr
.observation
.EventListener
;
56 import javax
.jcr
.query
.Query
;
57 import javax
.jcr
.query
.QueryResult
;
58 import javax
.jcr
.security
.AccessControlEntry
;
59 import javax
.jcr
.security
.AccessControlList
;
60 import javax
.jcr
.security
.AccessControlManager
;
61 import javax
.jcr
.security
.AccessControlPolicy
;
62 import javax
.jcr
.security
.AccessControlPolicyIterator
;
63 import javax
.jcr
.security
.Privilege
;
64 import javax
.jcr
.version
.VersionManager
;
66 import org
.apache
.commons
.io
.IOUtils
;
67 import org
.apache
.commons
.logging
.Log
;
68 import org
.apache
.commons
.logging
.LogFactory
;
69 import org
.argeo
.ArgeoException
;
70 import org
.argeo
.util
.security
.DigestUtils
;
71 import org
.argeo
.util
.security
.SimplePrincipal
;
73 /** Utility methods to simplify common JCR operations. */
74 public class JcrUtils
implements ArgeoJcrConstants
{
76 private final static Log log
= LogFactory
.getLog(JcrUtils
.class);
79 * Not complete yet. See
80 * http://www.day.com/specs/jcr/2.0/3_Repository_Model.html#3.2.2%20Local
83 public final static char[] INVALID_NAME_CHARACTERS
= { '/', ':', '[', ']',
89 /** Prevents instantiation */
94 * Queries one single node.
96 * @return one single node or null if none was found
97 * @throws ArgeoException
98 * if more than one node was found
100 public static Node
querySingleNode(Query query
) {
101 NodeIterator nodeIterator
;
103 QueryResult queryResult
= query
.execute();
104 nodeIterator
= queryResult
.getNodes();
105 } catch (RepositoryException e
) {
106 throw new ArgeoException("Cannot execute query " + query
, e
);
109 if (nodeIterator
.hasNext())
110 node
= nodeIterator
.nextNode();
114 if (nodeIterator
.hasNext())
115 throw new ArgeoException("Query returned more than one node.");
119 /** Retrieves the parent path of the provided path */
120 public static String
parentPath(String path
) {
121 if (path
.equals("/"))
122 throw new ArgeoException("Root path '/' has no parent path");
123 if (path
.charAt(0) != '/')
124 throw new ArgeoException("Path " + path
+ " must start with a '/'");
126 if (pathT
.charAt(pathT
.length() - 1) == '/')
127 pathT
= pathT
.substring(0, pathT
.length() - 2);
129 int index
= pathT
.lastIndexOf('/');
130 return pathT
.substring(0, index
);
133 /** The provided data as a path ('/' at the end, not the beginning) */
134 public static String
dateAsPath(Calendar cal
) {
135 return dateAsPath(cal
, false);
139 * Creates a deep path based on a URL:
140 * http://subdomain.example.com/to/content?args =>
141 * com/example/subdomain/to/content
143 public static String
urlAsPath(String url
) {
145 URL u
= new URL(url
);
146 StringBuffer path
= new StringBuffer(url
.length());
148 path
.append(hostAsPath(u
.getHost()));
149 // we don't put port since it may not always be there and may change
150 path
.append(u
.getPath());
151 return path
.toString();
152 } catch (MalformedURLException e
) {
153 throw new ArgeoException("Cannot generate URL path for " + url
, e
);
157 /** Set the {@link NodeType#NT_ADDRESS} properties based on this URL. */
158 public static void urlToAddressProperties(Node node
, String url
) {
160 URL u
= new URL(url
);
161 node
.setProperty(Property
.JCR_PROTOCOL
, u
.getProtocol());
162 node
.setProperty(Property
.JCR_HOST
, u
.getHost());
163 node
.setProperty(Property
.JCR_PORT
, Integer
.toString(u
.getPort()));
164 node
.setProperty(Property
.JCR_PATH
, normalizePath(u
.getPath()));
165 } catch (Exception e
) {
166 throw new ArgeoException("Cannot set URL " + url
167 + " as nt:address properties", e
);
171 /** Build URL based on the {@link NodeType#NT_ADDRESS} properties. */
172 public static String
urlFromAddressProperties(Node node
) {
175 node
.getProperty(Property
.JCR_PROTOCOL
).getString(), node
176 .getProperty(Property
.JCR_HOST
).getString(),
177 (int) node
.getProperty(Property
.JCR_PORT
).getLong(), node
178 .getProperty(Property
.JCR_PATH
).getString());
180 } catch (Exception e
) {
181 throw new ArgeoException(
182 "Cannot get URL from nt:address properties of " + node
, e
);
186 /** Make sure that: starts with '/', do not end with '/', do not have '//' */
187 public static String
normalizePath(String path
) {
188 List
<String
> tokens
= tokenize(path
);
189 StringBuffer buf
= new StringBuffer(path
.length());
190 for (String token
: tokens
) {
194 return buf
.toString();
198 * Creates a path from a FQDN, inverting the order of the component:
199 * www.argeo.org => org.argeo.www
201 public static String
hostAsPath(String host
) {
202 StringBuffer path
= new StringBuffer(host
.length());
203 String
[] hostTokens
= host
.split("\\.");
204 for (int i
= hostTokens
.length
- 1; i
>= 0; i
--) {
205 path
.append(hostTokens
[i
]);
209 return path
.toString();
213 * The provided data as a path ('/' at the end, not the beginning)
218 * whether to add hour as well
220 public static String
dateAsPath(Calendar cal
, Boolean addHour
) {
221 StringBuffer buf
= new StringBuffer(14);
223 buf
.append(cal
.get(Calendar
.YEAR
));
226 int month
= cal
.get(Calendar
.MONTH
) + 1;
233 int day
= cal
.get(Calendar
.DAY_OF_MONTH
);
241 int hour
= cal
.get(Calendar
.HOUR_OF_DAY
);
248 return buf
.toString();
252 /** Converts in one call a string into a gregorian calendar. */
253 public static Calendar
parseCalendar(DateFormat dateFormat
, String value
) {
255 Date date
= dateFormat
.parse(value
);
256 Calendar calendar
= new GregorianCalendar();
257 calendar
.setTime(date
);
259 } catch (ParseException e
) {
260 throw new ArgeoException("Cannot parse " + value
261 + " with date format " + dateFormat
, e
);
266 /** The last element of a path. */
267 public static String
lastPathElement(String path
) {
268 if (path
.charAt(path
.length() - 1) == '/')
269 throw new ArgeoException("Path " + path
+ " cannot end with '/'");
270 int index
= path
.lastIndexOf('/');
272 throw new ArgeoException("Cannot find last path element for "
274 return path
.substring(index
+ 1);
278 * Routine that get the child with this name, adding id it does not already
281 public static Node
getOrAdd(Node parent
, String childName
,
282 String childPrimaryNodeType
) throws RepositoryException
{
283 return parent
.hasNode(childName
) ? parent
.getNode(childName
) : parent
284 .addNode(childName
, childPrimaryNodeType
);
288 * Routine that get the child with this name, adding id it does not already
291 public static Node
getOrAdd(Node parent
, String childName
)
292 throws RepositoryException
{
293 return parent
.hasNode(childName
) ? parent
.getNode(childName
) : parent
297 /** Convert a {@link NodeIterator} to a list of {@link Node} */
298 public static List
<Node
> nodeIteratorToList(NodeIterator nodeIterator
) {
299 List
<Node
> nodes
= new ArrayList
<Node
>();
300 while (nodeIterator
.hasNext()) {
301 nodes
.add(nodeIterator
.nextNode());
310 /** Concisely get the string value of a property */
311 public static String
get(Node node
, String propertyName
) {
313 return node
.getProperty(propertyName
).getString();
314 } catch (RepositoryException e
) {
315 throw new ArgeoException("Cannot get property " + propertyName
320 /** Concisely get the boolean value of a property */
321 public static Boolean
check(Node node
, String propertyName
) {
323 return node
.getProperty(propertyName
).getBoolean();
324 } catch (RepositoryException e
) {
325 throw new ArgeoException("Cannot get property " + propertyName
330 /** Concisely get the bytes array value of a property */
331 public static byte[] getBytes(Node node
, String propertyName
) {
333 return getBinaryAsBytes(node
.getProperty(propertyName
));
334 } catch (RepositoryException e
) {
335 throw new ArgeoException("Cannot get property " + propertyName
340 /** Creates the nodes making path, if they don't exist. */
341 public static Node
mkdirs(Session session
, String path
) {
342 return mkdirs(session
, path
, null, null, false);
346 * use {@link #mkdirs(Session, String, String, String, Boolean)} instead.
351 public static Node
mkdirs(Session session
, String path
, String type
,
352 Boolean versioning
) {
353 return mkdirs(session
, path
, type
, type
, false);
358 * the type of the leaf node
360 public static Node
mkdirs(Session session
, String path
, String type
) {
361 return mkdirs(session
, path
, type
, null, false);
365 * Synchronized and save is performed, to avoid race conditions in
366 * initializers leading to duplicate nodes.
368 public synchronized static Node
mkdirsSafe(Session session
, String path
,
371 if (session
.hasPendingChanges())
372 throw new ArgeoException(
373 "Session has pending changes, save them first.");
374 Node node
= mkdirs(session
, path
, type
);
377 } catch (RepositoryException e
) {
378 discardQuietly(session
);
379 throw new ArgeoException("Cannot safely make directories", e
);
383 public synchronized static Node
mkdirsSafe(Session session
, String path
) {
384 return mkdirsSafe(session
, path
, null);
388 * Creates the nodes making the path as {@link NodeType#NT_FOLDER}
390 public static Node
mkfolders(Session session
, String path
) {
391 return mkdirs(session
, path
, NodeType
.NT_FOLDER
, NodeType
.NT_FOLDER
,
396 * Creates the nodes making path, if they don't exist. This is up to the
397 * caller to save the session. Use with caution since it can create
398 * duplicate nodes if used concurrently.
400 public static Node
mkdirs(Session session
, String path
, String type
,
401 String intermediaryNodeType
, Boolean versioning
) {
403 if (path
.equals('/'))
404 return session
.getRootNode();
406 if (session
.itemExists(path
)) {
407 Node node
= session
.getNode(path
);
409 if (type
!= null && !node
.isNodeType(type
)
410 && !node
.getPath().equals("/"))
411 throw new ArgeoException("Node " + node
412 + " exists but is of type "
413 + node
.getPrimaryNodeType().getName()
414 + " not of type " + type
);
415 // TODO: check versioning
419 StringBuffer current
= new StringBuffer("/");
420 Node currentNode
= session
.getRootNode();
421 Iterator
<String
> it
= tokenize(path
).iterator();
422 while (it
.hasNext()) {
423 String part
= it
.next();
424 current
.append(part
).append('/');
425 if (!session
.itemExists(current
.toString())) {
426 if (!it
.hasNext() && type
!= null)
427 currentNode
= currentNode
.addNode(part
, type
);
428 else if (it
.hasNext() && intermediaryNodeType
!= null)
429 currentNode
= currentNode
.addNode(part
,
430 intermediaryNodeType
);
432 currentNode
= currentNode
.addNode(part
);
434 currentNode
.addMixin(NodeType
.MIX_VERSIONABLE
);
435 if (log
.isTraceEnabled())
436 log
.debug("Added folder " + part
+ " as " + current
);
438 currentNode
= (Node
) session
.getItem(current
.toString());
442 } catch (RepositoryException e
) {
443 discardQuietly(session
);
444 throw new ArgeoException("Cannot mkdirs " + path
, e
);
449 /** Convert a path to the list of its tokens */
450 public static List
<String
> tokenize(String path
) {
451 List
<String
> tokens
= new ArrayList
<String
>();
452 boolean optimized
= false;
454 String
[] rawTokens
= path
.split("/");
455 for (String token
: rawTokens
) {
456 if (!token
.equals(""))
460 StringBuffer curr
= new StringBuffer();
461 char[] arr
= path
.toCharArray();
462 chars
: for (int i
= 0; i
< arr
.length
; i
++) {
465 if (i
== 0 || (i
== arr
.length
- 1))
467 if (curr
.length() > 0) {
468 tokens
.add(curr
.toString());
469 curr
= new StringBuffer();
474 if (curr
.length() > 0) {
475 tokens
.add(curr
.toString());
476 curr
= new StringBuffer();
479 return Collections
.unmodifiableList(tokens
);
483 * Safe and repository implementation independent registration of a
486 public static void registerNamespaceSafely(Session session
, String prefix
,
489 registerNamespaceSafely(session
.getWorkspace()
490 .getNamespaceRegistry(), prefix
, uri
);
491 } catch (RepositoryException e
) {
492 throw new ArgeoException("Cannot find namespace registry", e
);
497 * Safe and repository implementation independent registration of a
500 public static void registerNamespaceSafely(NamespaceRegistry nr
,
501 String prefix
, String uri
) {
503 String
[] prefixes
= nr
.getPrefixes();
504 for (String pref
: prefixes
)
505 if (pref
.equals(prefix
)) {
506 String registeredUri
= nr
.getURI(pref
);
507 if (!registeredUri
.equals(uri
))
508 throw new ArgeoException("Prefix " + pref
509 + " already registered for URI "
511 + " which is different from provided URI "
516 nr
.registerNamespace(prefix
, uri
);
517 } catch (RepositoryException e
) {
518 throw new ArgeoException("Cannot register namespace " + uri
519 + " under prefix " + prefix
, e
);
523 /** Recursively outputs the contents of the given node. */
524 public static void debug(Node node
) {
528 /** Recursively outputs the contents of the given node. */
529 public static void debug(Node node
, Log log
) {
531 // First output the node path
532 log
.debug(node
.getPath());
533 // Skip the virtual (and large!) jcr:system subtree
534 if (node
.getName().equals("jcr:system")) {
538 // Then the children nodes (recursive)
539 NodeIterator it
= node
.getNodes();
540 while (it
.hasNext()) {
541 Node childNode
= it
.nextNode();
542 debug(childNode
, log
);
545 // Then output the properties
546 PropertyIterator properties
= node
.getProperties();
547 // log.debug("Property are : ");
549 properties
: while (properties
.hasNext()) {
550 Property property
= properties
.nextProperty();
551 if (property
.getType() == PropertyType
.BINARY
)
552 continue properties
;// skip
553 if (property
.getDefinition().isMultiple()) {
554 // A multi-valued property, print all values
555 Value
[] values
= property
.getValues();
556 for (int i
= 0; i
< values
.length
; i
++) {
557 log
.debug(property
.getPath() + "="
558 + values
[i
].getString());
561 // A single-valued property
562 log
.debug(property
.getPath() + "=" + property
.getString());
565 } catch (Exception e
) {
566 log
.error("Could not debug " + node
, e
);
571 /** Logs the effective access control policies */
572 public static void logEffectiveAccessPolicies(Node node
) {
574 logEffectiveAccessPolicies(node
.getSession(), node
.getPath());
575 } catch (RepositoryException e
) {
576 log
.error("Cannot log effective access policies of " + node
, e
);
580 /** Logs the effective access control policies */
581 public static void logEffectiveAccessPolicies(Session session
, String path
) {
582 if (!log
.isDebugEnabled())
586 AccessControlPolicy
[] effectivePolicies
= session
587 .getAccessControlManager().getEffectivePolicies(path
);
588 if (effectivePolicies
.length
> 0) {
589 for (AccessControlPolicy policy
: effectivePolicies
) {
590 if (policy
instanceof AccessControlList
) {
591 AccessControlList acl
= (AccessControlList
) policy
;
592 log
.debug("Access control list for " + path
+ "\n"
593 + accessControlListSummary(acl
));
597 log
.debug("No effective access control policy for " + path
);
599 } catch (RepositoryException e
) {
600 log
.error("Cannot log effective access policies of " + path
, e
);
604 /** Returns a human-readable summary of this access control list. */
605 public static String
accessControlListSummary(AccessControlList acl
) {
606 StringBuffer buf
= new StringBuffer("");
608 for (AccessControlEntry ace
: acl
.getAccessControlEntries()) {
609 buf
.append('\t').append(ace
.getPrincipal().getName())
611 for (Privilege priv
: ace
.getPrivileges())
612 buf
.append("\t\t").append(priv
.getName()).append('\n');
614 return buf
.toString();
615 } catch (RepositoryException e
) {
616 throw new ArgeoException("Cannot write summary of " + acl
, e
);
621 * Copies recursively the content of a node to another one. Do NOT copy the
622 * property values of {@link NodeType#MIX_CREATED} and
623 * {@link NodeType#MIX_LAST_MODIFIED}, but update the
624 * {@link Property#JCR_LAST_MODIFIED} and
625 * {@link Property#JCR_LAST_MODIFIED_BY} properties if the target node has
626 * the {@link NodeType#MIX_LAST_MODIFIED} mixin.
628 public static void copy(Node fromNode
, Node toNode
) {
630 // process properties
631 PropertyIterator pit
= fromNode
.getProperties();
632 properties
: while (pit
.hasNext()) {
633 Property fromProperty
= pit
.nextProperty();
634 String propertyName
= fromProperty
.getName();
635 if (toNode
.hasProperty(propertyName
)
636 && toNode
.getProperty(propertyName
).getDefinition()
640 if (fromProperty
.getDefinition().isProtected())
643 if (propertyName
.equals("jcr:created")
644 || propertyName
.equals("jcr:createdBy")
645 || propertyName
.equals("jcr:lastModified")
646 || propertyName
.equals("jcr:lastModifiedBy"))
649 if (fromProperty
.isMultiple()) {
650 toNode
.setProperty(propertyName
, fromProperty
.getValues());
652 toNode
.setProperty(propertyName
, fromProperty
.getValue());
656 // update jcr:lastModified and jcr:lastModifiedBy in toNode in case
657 // they existed, before adding the mixins
658 updateLastModified(toNode
);
661 for (NodeType mixinType
: fromNode
.getMixinNodeTypes()) {
662 toNode
.addMixin(mixinType
.getName());
665 // process children nodes
666 NodeIterator nit
= fromNode
.getNodes();
667 while (nit
.hasNext()) {
668 Node fromChild
= nit
.nextNode();
669 Integer index
= fromChild
.getIndex();
670 String nodeRelPath
= fromChild
.getName() + "[" + index
+ "]";
672 if (toNode
.hasNode(nodeRelPath
))
673 toChild
= toNode
.getNode(nodeRelPath
);
675 toChild
= toNode
.addNode(fromChild
.getName(), fromChild
676 .getPrimaryNodeType().getName());
677 copy(fromChild
, toChild
);
679 } catch (RepositoryException e
) {
680 throw new ArgeoException("Cannot copy " + fromNode
+ " to "
686 * Check whether all first-level properties (except jcr:* properties) are
687 * equal. Skip jcr:* properties
689 public static Boolean
allPropertiesEquals(Node reference
, Node observed
,
690 Boolean onlyCommonProperties
) {
692 PropertyIterator pit
= reference
.getProperties();
693 props
: while (pit
.hasNext()) {
694 Property propReference
= pit
.nextProperty();
695 String propName
= propReference
.getName();
696 if (propName
.startsWith("jcr:"))
699 if (!observed
.hasProperty(propName
))
700 if (onlyCommonProperties
)
704 // TODO: deal with multiple property values?
705 if (!observed
.getProperty(propName
).getValue()
706 .equals(propReference
.getValue()))
710 } catch (RepositoryException e
) {
711 throw new ArgeoException("Cannot check all properties equals of "
712 + reference
+ " and " + observed
, e
);
716 public static Map
<String
, PropertyDiff
> diffProperties(Node reference
,
718 Map
<String
, PropertyDiff
> diffs
= new TreeMap
<String
, PropertyDiff
>();
719 diffPropertiesLevel(diffs
, null, reference
, observed
);
724 * Compare the properties of two nodes. Recursivity to child nodes is not
725 * yet supported. Skip jcr:* properties.
727 static void diffPropertiesLevel(Map
<String
, PropertyDiff
> diffs
,
728 String baseRelPath
, Node reference
, Node observed
) {
730 // check removed and modified
731 PropertyIterator pit
= reference
.getProperties();
732 props
: while (pit
.hasNext()) {
733 Property p
= pit
.nextProperty();
734 String name
= p
.getName();
735 if (name
.startsWith("jcr:"))
738 if (!observed
.hasProperty(name
)) {
739 String relPath
= propertyRelPath(baseRelPath
, name
);
740 PropertyDiff pDiff
= new PropertyDiff(PropertyDiff
.REMOVED
,
741 relPath
, p
.getValue(), null);
742 diffs
.put(relPath
, pDiff
);
744 if (p
.isMultiple()) {
745 // FIXME implement multiple
747 Value referenceValue
= p
.getValue();
748 Value newValue
= observed
.getProperty(name
).getValue();
749 if (!referenceValue
.equals(newValue
)) {
750 String relPath
= propertyRelPath(baseRelPath
, name
);
751 PropertyDiff pDiff
= new PropertyDiff(
752 PropertyDiff
.MODIFIED
, relPath
,
753 referenceValue
, newValue
);
754 diffs
.put(relPath
, pDiff
);
760 pit
= observed
.getProperties();
761 props
: while (pit
.hasNext()) {
762 Property p
= pit
.nextProperty();
763 String name
= p
.getName();
764 if (name
.startsWith("jcr:"))
766 if (!reference
.hasProperty(name
)) {
767 if (p
.isMultiple()) {
768 // FIXME implement multiple
770 String relPath
= propertyRelPath(baseRelPath
, name
);
771 PropertyDiff pDiff
= new PropertyDiff(
772 PropertyDiff
.ADDED
, relPath
, null, p
.getValue());
773 diffs
.put(relPath
, pDiff
);
777 } catch (RepositoryException e
) {
778 throw new ArgeoException("Cannot diff " + reference
+ " and "
784 * Compare only a restricted list of properties of two nodes. No
788 public static Map
<String
, PropertyDiff
> diffProperties(Node reference
,
789 Node observed
, List
<String
> properties
) {
790 Map
<String
, PropertyDiff
> diffs
= new TreeMap
<String
, PropertyDiff
>();
792 Iterator
<String
> pit
= properties
.iterator();
794 props
: while (pit
.hasNext()) {
795 String name
= pit
.next();
796 if (!reference
.hasProperty(name
)) {
797 if (!observed
.hasProperty(name
))
799 Value val
= observed
.getProperty(name
).getValue();
801 // empty String but not null
802 if ("".equals(val
.getString()))
804 } catch (Exception e
) {
805 // not parseable as String, silent
807 PropertyDiff pDiff
= new PropertyDiff(PropertyDiff
.ADDED
,
809 diffs
.put(name
, pDiff
);
810 } else if (!observed
.hasProperty(name
)) {
811 PropertyDiff pDiff
= new PropertyDiff(PropertyDiff
.REMOVED
,
812 name
, reference
.getProperty(name
).getValue(), null);
813 diffs
.put(name
, pDiff
);
815 Value referenceValue
= reference
.getProperty(name
)
817 Value newValue
= observed
.getProperty(name
).getValue();
818 if (!referenceValue
.equals(newValue
)) {
819 PropertyDiff pDiff
= new PropertyDiff(
820 PropertyDiff
.MODIFIED
, name
, referenceValue
,
822 diffs
.put(name
, pDiff
);
826 } catch (RepositoryException e
) {
827 throw new ArgeoException("Cannot diff " + reference
+ " and "
833 /** Builds a property relPath to be used in the diff. */
834 private static String
propertyRelPath(String baseRelPath
,
835 String propertyName
) {
836 if (baseRelPath
== null)
839 return baseRelPath
+ '/' + propertyName
;
843 * Normalizes a name so that it can be stored in contexts not supporting
844 * names with ':' (typically databases). Replaces ':' by '_'.
846 public static String
normalize(String name
) {
847 return name
.replace(':', '_');
851 * Replaces characters which are invalid in a JCR name by '_'. Currently not
854 * @see JcrUtils#INVALID_NAME_CHARACTERS
856 public static String
replaceInvalidChars(String name
) {
857 return replaceInvalidChars(name
, '_');
861 * Replaces characters which are invalid in a JCR name. Currently not
864 * @see JcrUtils#INVALID_NAME_CHARACTERS
866 public static String
replaceInvalidChars(String name
, char replacement
) {
867 boolean modified
= false;
868 char[] arr
= name
.toCharArray();
869 for (int i
= 0; i
< arr
.length
; i
++) {
871 invalid
: for (char invalid
: INVALID_NAME_CHARACTERS
) {
873 arr
[i
] = replacement
;
880 return new String(arr
);
882 // do not create new object if unnecessary
887 * Removes forbidden characters from a path, replacing them with '_'
889 * @deprecated use {@link #replaceInvalidChars(String)} instead
891 public static String
removeForbiddenCharacters(String str
) {
892 return str
.replace('[', '_').replace(']', '_').replace('/', '_')
897 /** Cleanly disposes a {@link Binary} even if it is null. */
898 public static void closeQuietly(Binary binary
) {
904 /** Retrieve a {@link Binary} as a byte array */
905 public static byte[] getBinaryAsBytes(Property property
) {
906 ByteArrayOutputStream out
= new ByteArrayOutputStream();
907 InputStream in
= null;
908 Binary binary
= null;
910 binary
= property
.getBinary();
911 in
= binary
.getStream();
912 IOUtils
.copy(in
, out
);
913 return out
.toByteArray();
914 } catch (Exception e
) {
915 throw new ArgeoException("Cannot read binary " + property
918 IOUtils
.closeQuietly(out
);
919 IOUtils
.closeQuietly(in
);
920 closeQuietly(binary
);
924 /** Writes a {@link Binary} from a byte array */
925 public static void setBinaryAsBytes(Node node
, String property
, byte[] bytes
) {
926 InputStream in
= null;
927 Binary binary
= null;
929 in
= new ByteArrayInputStream(bytes
);
930 binary
= node
.getSession().getValueFactory().createBinary(in
);
931 node
.setProperty(property
, binary
);
932 } catch (Exception e
) {
933 throw new ArgeoException("Cannot read binary " + property
936 IOUtils
.closeQuietly(in
);
937 closeQuietly(binary
);
942 * Copy a file as an nt:file, assuming an nt:folder hierarchy. The session
945 * @return the created file node
947 public static Node
copyFile(Node folderNode
, File file
) {
948 InputStream in
= null;
950 in
= new FileInputStream(file
);
951 return copyStreamAsFile(folderNode
, file
.getName(), in
);
952 } catch (IOException e
) {
953 throw new ArgeoException("Cannot copy file " + file
+ " under "
956 IOUtils
.closeQuietly(in
);
960 /** Copy bytes as an nt:file */
961 public static Node
copyBytesAsFile(Node folderNode
, String fileName
,
963 InputStream in
= null;
965 in
= new ByteArrayInputStream(bytes
);
966 return copyStreamAsFile(folderNode
, fileName
, in
);
967 } catch (Exception e
) {
968 throw new ArgeoException("Cannot copy file " + fileName
+ " under "
971 IOUtils
.closeQuietly(in
);
976 * Copy a stream as an nt:file, assuming an nt:folder hierarchy. The session
979 * @return the created file node
981 public static Node
copyStreamAsFile(Node folderNode
, String fileName
,
983 Binary binary
= null;
987 if (folderNode
.hasNode(fileName
)) {
988 fileNode
= folderNode
.getNode(fileName
);
989 // we assume that the content node is already there
990 contentNode
= fileNode
.getNode(Node
.JCR_CONTENT
);
992 fileNode
= folderNode
.addNode(fileName
, NodeType
.NT_FILE
);
993 contentNode
= fileNode
.addNode(Node
.JCR_CONTENT
,
994 NodeType
.NT_RESOURCE
);
996 binary
= contentNode
.getSession().getValueFactory()
998 contentNode
.setProperty(Property
.JCR_DATA
, binary
);
1000 } catch (Exception e
) {
1001 throw new ArgeoException("Cannot create file node " + fileName
1002 + " under " + folderNode
, e
);
1004 closeQuietly(binary
);
1008 /** Computes the checksum of an nt:file */
1009 public static String
checksumFile(Node fileNode
, String algorithm
) {
1011 InputStream in
= null;
1013 data
= fileNode
.getNode(Node
.JCR_CONTENT
)
1014 .getProperty(Property
.JCR_DATA
).getBinary();
1015 in
= data
.getStream();
1016 return DigestUtils
.digest(algorithm
, in
);
1017 } catch (RepositoryException e
) {
1018 throw new ArgeoException("Cannot checksum file " + fileNode
, e
);
1020 IOUtils
.closeQuietly(in
);
1026 * Creates depth from a string (typically a username) by adding levels based
1027 * on its first characters: "aBcD",2 => a/aB
1029 public static String
firstCharsToPath(String str
, Integer nbrOfChars
) {
1030 if (str
.length() < nbrOfChars
)
1031 throw new ArgeoException("String " + str
1032 + " length must be greater or equal than " + nbrOfChars
);
1033 StringBuffer path
= new StringBuffer("");
1034 StringBuffer curr
= new StringBuffer("");
1035 for (int i
= 0; i
< nbrOfChars
; i
++) {
1036 curr
.append(str
.charAt(i
));
1038 if (i
< nbrOfChars
- 1)
1041 return path
.toString();
1045 * Wraps the call to the repository factory based on parameter
1046 * {@link ArgeoJcrConstants#JCR_REPOSITORY_ALIAS} in order to simplify it
1047 * and protect against future API changes.
1049 public static Repository
getRepositoryByAlias(
1050 RepositoryFactory repositoryFactory
, String alias
) {
1052 Map
<String
, String
> parameters
= new HashMap
<String
, String
>();
1053 parameters
.put(JCR_REPOSITORY_ALIAS
, alias
);
1054 return repositoryFactory
.getRepository(parameters
);
1055 } catch (RepositoryException e
) {
1056 throw new ArgeoException(
1057 "Unexpected exception when trying to retrieve repository with alias "
1063 * Wraps the call to the repository factory based on parameter
1064 * {@link ArgeoJcrConstants#JCR_REPOSITORY_URI} in order to simplify it and
1065 * protect against future API changes.
1067 public static Repository
getRepositoryByUri(
1068 RepositoryFactory repositoryFactory
, String uri
) {
1070 Map
<String
, String
> parameters
= new HashMap
<String
, String
>();
1071 parameters
.put(JCR_REPOSITORY_URI
, uri
);
1072 return repositoryFactory
.getRepository(parameters
);
1073 } catch (RepositoryException e
) {
1074 throw new ArgeoException(
1075 "Unexpected exception when trying to retrieve repository with uri "
1081 * Discards the current changes in the session attached to this node. To be
1082 * used typically in a catch block.
1084 * @see #discardQuietly(Session)
1086 public static void discardUnderlyingSessionQuietly(Node node
) {
1088 discardQuietly(node
.getSession());
1089 } catch (RepositoryException e
) {
1090 log
.warn("Cannot quietly discard session of node " + node
+ ": "
1096 * Discards the current changes in a session by calling
1097 * {@link Session#refresh(boolean)} with <code>false</code>, only logging
1098 * potential errors when doing so. To be used typically in a catch block.
1100 public static void discardQuietly(Session session
) {
1102 if (session
!= null)
1103 session
.refresh(false);
1104 } catch (RepositoryException e
) {
1105 log
.warn("Cannot quietly discard session " + session
+ ": "
1111 * Login to a workspace with implicit credentials, creates the workspace
1112 * with these credentials if it does not already exist.
1114 public static Session
loginOrCreateWorkspace(Repository repository
,
1115 String workspaceName
) throws RepositoryException
{
1116 Session workspaceSession
= null;
1117 Session defaultSession
= null;
1120 workspaceSession
= repository
.login(workspaceName
);
1121 } catch (NoSuchWorkspaceException e
) {
1122 // try to create workspace
1123 defaultSession
= repository
.login();
1124 defaultSession
.getWorkspace().createWorkspace(workspaceName
);
1125 workspaceSession
= repository
.login(workspaceName
);
1127 return workspaceSession
;
1129 logoutQuietly(defaultSession
);
1133 /** Logs out the session, not throwing any exception, even if it is null. */
1134 public static void logoutQuietly(Session session
) {
1136 if (session
!= null)
1137 if (session
.isLive())
1139 } catch (Exception e
) {
1145 * Convenient method to add a listener. uuids passed as null, deep=true,
1146 * local=true, only one node type
1148 public static void addListener(Session session
, EventListener listener
,
1149 int eventTypes
, String basePath
, String nodeType
) {
1151 session
.getWorkspace()
1152 .getObservationManager()
1153 .addEventListener(listener
, eventTypes
, basePath
, true,
1154 null, new String
[] { nodeType
}, true);
1155 } catch (RepositoryException e
) {
1156 throw new ArgeoException("Cannot add JCR listener " + listener
1157 + " to session " + session
, e
);
1161 /** Removes a listener without throwing exception */
1162 public static void removeListenerQuietly(Session session
,
1163 EventListener listener
) {
1164 if (session
== null || !session
.isLive())
1167 session
.getWorkspace().getObservationManager()
1168 .removeEventListener(listener
);
1169 } catch (RepositoryException e
) {
1174 /** Returns the home node of the session user or null if none was found. */
1175 public static Node
getUserHome(Session session
) {
1176 String userID
= session
.getUserID();
1177 return getUserHome(session
, userID
);
1180 /** User home path is NOT configurable */
1181 public static String
getUserHomePath(String username
) {
1182 String homeBasePath
= DEFAULT_HOME_BASE_PATH
;
1183 return homeBasePath
+ '/' + firstCharsToPath(username
, 2) + '/'
1188 * Returns the home node of the session user or null if none was found.
1191 * the session to use in order to perform the search, this can be
1192 * a session with a different user ID than the one searched,
1193 * typically when a system or admin session is used.
1195 * the username of the user
1197 public static Node
getUserHome(Session session
, String username
) {
1199 String homePath
= getUserHomePath(username
);
1200 return session
.itemExists(homePath
) ? session
.getNode(homePath
)
1202 // kept for example of QOM queries
1203 // QueryObjectModelFactory qomf = session.getWorkspace()
1204 // .getQueryManager().getQOMFactory();
1205 // Selector userHomeSel = qomf.selector(ArgeoTypes.ARGEO_USER_HOME,
1207 // DynamicOperand userIdDop = qomf.propertyValue("userHome",
1208 // ArgeoNames.ARGEO_USER_ID);
1209 // StaticOperand userIdSop = qomf.literal(session.getValueFactory()
1210 // .createValue(username));
1211 // Constraint constraint = qomf.comparison(userIdDop,
1212 // QueryObjectModelFactory.JCR_OPERATOR_EQUAL_TO, userIdSop);
1213 // Query query = qomf.createQuery(userHomeSel, constraint, null,
1215 // Node userHome = JcrUtils.querySingleNode(query);
1216 } catch (RepositoryException e
) {
1217 throw new ArgeoException("Cannot find home for user " + username
, e
);
1222 * Creates an Argeo user home, does nothing if it already exists. Session is
1225 public static Node
createUserHomeIfNeeded(Session session
, String username
) {
1227 String homePath
= getUserHomePath(username
);
1228 if (session
.itemExists(homePath
))
1229 return session
.getNode(homePath
);
1231 Node userHome
= JcrUtils
.mkdirs(session
, homePath
);
1232 userHome
.addMixin(ArgeoTypes
.ARGEO_USER_HOME
);
1233 userHome
.setProperty(ArgeoNames
.ARGEO_USER_ID
, username
);
1236 } catch (RepositoryException e
) {
1237 discardQuietly(session
);
1238 throw new ArgeoException("Cannot create home for " + username
1239 + " in workspace " + session
.getWorkspace().getName(), e
);
1244 * Creates a user profile in the home of this user. Creates the home if
1245 * needed, but throw an exception if a profile already exists. The session
1246 * is not saved and the node is in a checkedOut state (that is, it requires
1247 * a subsequent checkin after saving the session).
1249 public static Node
createUserProfile(Session session
, String username
) {
1251 Node userHome
= createUserHomeIfNeeded(session
, username
);
1252 if (userHome
.hasNode(ArgeoNames
.ARGEO_PROFILE
))
1253 throw new ArgeoException(
1254 "There is already a user profile under " + userHome
);
1255 Node userProfile
= userHome
.addNode(ArgeoNames
.ARGEO_PROFILE
);
1256 userProfile
.addMixin(ArgeoTypes
.ARGEO_USER_PROFILE
);
1257 userProfile
.setProperty(ArgeoNames
.ARGEO_USER_ID
, username
);
1258 userProfile
.setProperty(ArgeoNames
.ARGEO_ENABLED
, true);
1259 userProfile
.setProperty(ArgeoNames
.ARGEO_ACCOUNT_NON_EXPIRED
, true);
1260 userProfile
.setProperty(ArgeoNames
.ARGEO_ACCOUNT_NON_LOCKED
, true);
1261 userProfile
.setProperty(ArgeoNames
.ARGEO_CREDENTIALS_NON_EXPIRED
,
1264 } catch (RepositoryException e
) {
1265 discardQuietly(session
);
1266 throw new ArgeoException("Cannot create user profile for "
1267 + username
+ " in workspace "
1268 + session
.getWorkspace().getName(), e
);
1273 * Create user profile if needed, the session IS saved.
1275 * @return the user profile
1277 public static Node
createUserProfileIfNeeded(Session securitySession
,
1280 Node userHome
= JcrUtils
.createUserHomeIfNeeded(securitySession
,
1282 Node userProfile
= userHome
.hasNode(ArgeoNames
.ARGEO_PROFILE
) ? userHome
1283 .getNode(ArgeoNames
.ARGEO_PROFILE
) : JcrUtils
1284 .createUserProfile(securitySession
, username
);
1285 if (securitySession
.hasPendingChanges())
1286 securitySession
.save();
1287 VersionManager versionManager
= securitySession
.getWorkspace()
1288 .getVersionManager();
1289 if (versionManager
.isCheckedOut(userProfile
.getPath()))
1290 versionManager
.checkin(userProfile
.getPath());
1292 } catch (RepositoryException e
) {
1293 discardQuietly(securitySession
);
1294 throw new ArgeoException("Cannot create user profile for "
1295 + username
+ " in workspace "
1296 + securitySession
.getWorkspace().getName(), e
);
1300 /** Creates an Argeo user home. */
1301 // public static Node createUserHome(Session session, String homeBasePath,
1302 // String username) {
1304 // if (session == null)
1305 // throw new ArgeoException("Session is null");
1306 // if (session.hasPendingChanges())
1307 // throw new ArgeoException(
1308 // "Session has pending changes, save them first");
1310 // String homePath = getUserHomePath(username);
1312 // if (session.itemExists(homePath)) {
1314 // throw new ArgeoException(
1315 // "Trying to create a user home that already exists");
1316 // } catch (Exception e) {
1317 // // we use this workaround to be sure to get the stack trace
1318 // // to identify the sink of the bug.
1319 // log.warn("trying to create an already existing userHome at path:"
1320 // + homePath + ". Stack trace : ");
1321 // e.printStackTrace();
1325 // Node userHome = JcrUtils.mkdirs(session, homePath);
1326 // Node userProfile;
1327 // if (userHome.hasNode(ArgeoNames.ARGEO_PROFILE)) {
1328 // log.warn("userProfile node already exists for userHome path: "
1329 // + homePath + ". We do not add a new one");
1331 // userProfile = userHome.addNode(ArgeoNames.ARGEO_PROFILE);
1332 // userProfile.addMixin(ArgeoTypes.ARGEO_USER_PROFILE);
1333 // // session.getWorkspace().getVersionManager()
1334 // // .checkout(userProfile.getPath());
1335 // userProfile.setProperty(ArgeoNames.ARGEO_USER_ID, username);
1337 // session.getWorkspace().getVersionManager()
1338 // .checkin(userProfile.getPath());
1339 // // we need to save the profile before adding the user home type
1341 // userHome.addMixin(ArgeoTypes.ARGEO_USER_HOME);
1344 // http://jackrabbit.510166.n4.nabble.com/Jackrabbit-2-0-beta-6-Problem-adding-a-Mixin-type-with-mandatory-properties-after-setting-propertiesn-td1290332.html
1345 // userHome.setProperty(ArgeoNames.ARGEO_USER_ID, username);
1348 // } catch (RepositoryException e) {
1349 // discardQuietly(session);
1350 // throw new ArgeoException("Cannot create home node for user "
1356 * Returns user home has path, embedding exceptions. Contrary to
1357 * {@link #getUserHome(Session)}, it never returns null but throws and
1358 * exception if not found.
1360 * @deprecated use getUserHome() instead, throwing an exception if it
1364 public static String
getUserHomePath(Session session
) {
1365 String userID
= session
.getUserID();
1367 String homePath
= getUserHomePath(userID
);
1368 if (session
.itemExists(homePath
))
1371 throw new ArgeoException("No home registered for " + userID
);
1372 } catch (RepositoryException e
) {
1373 throw new ArgeoException("Cannot find user home path", e
);
1378 * @return null if not found *
1380 public static Node
getUserProfile(Session session
, String username
) {
1382 Node userHome
= getUserHome(session
, username
);
1383 if (userHome
== null)
1385 if (userHome
.hasNode(ArgeoNames
.ARGEO_PROFILE
))
1386 return userHome
.getNode(ArgeoNames
.ARGEO_PROFILE
);
1389 } catch (RepositoryException e
) {
1390 throw new ArgeoException(
1391 "Cannot find profile for user " + username
, e
);
1396 * Get the profile of the user attached to this session.
1398 public static Node
getUserProfile(Session session
) {
1399 String userID
= session
.getUserID();
1400 return getUserProfile(session
, userID
);
1404 * Quietly unregisters an {@link EventListener} from the udnerlying
1405 * workspace of this node.
1407 public static void unregisterQuietly(Node node
, EventListener eventListener
) {
1409 unregisterQuietly(node
.getSession().getWorkspace(), eventListener
);
1410 } catch (RepositoryException e
) {
1412 if (log
.isTraceEnabled())
1413 log
.trace("Could not unregister event listener "
1418 /** Quietly unregisters an {@link EventListener} from this workspace */
1419 public static void unregisterQuietly(Workspace workspace
,
1420 EventListener eventListener
) {
1421 if (eventListener
== null)
1424 workspace
.getObservationManager()
1425 .removeEventListener(eventListener
);
1426 } catch (RepositoryException e
) {
1428 if (log
.isTraceEnabled())
1429 log
.trace("Could not unregister event listener "
1435 * If this node is has the {@link NodeType#MIX_LAST_MODIFIED} mixin, it
1436 * updates the {@link Property#JCR_LAST_MODIFIED} property with the current
1437 * time and the {@link Property#JCR_LAST_MODIFIED_BY} property with the
1438 * underlying session user id. In Jackrabbit 2.x, <a
1439 * href="https://issues.apache.org/jira/browse/JCR-2233">these properties
1440 * are not automatically updated</a>, hence the need for manual update. The
1441 * session is not saved.
1443 public static void updateLastModified(Node node
) {
1445 if (!node
.isNodeType(NodeType
.MIX_LAST_MODIFIED
))
1446 node
.addMixin(NodeType
.MIX_LAST_MODIFIED
);
1447 node
.setProperty(Property
.JCR_LAST_MODIFIED
,
1448 new GregorianCalendar());
1449 node
.setProperty(Property
.JCR_LAST_MODIFIED_BY
, node
.getSession()
1451 } catch (RepositoryException e
) {
1452 throw new ArgeoException("Cannot update last modified on " + node
,
1457 /** Update lastModified recursively until this parent. */
1458 public static void updateLastModifiedAndParents(Node node
, String untilPath
) {
1460 if (!node
.getPath().startsWith(untilPath
))
1461 throw new ArgeoException(node
+ " is not under " + untilPath
);
1462 updateLastModified(node
);
1463 if (!node
.getPath().equals(untilPath
))
1464 updateLastModifiedAndParents(node
.getParent(), untilPath
);
1465 } catch (RepositoryException e
) {
1466 throw new ArgeoException("Cannot update lastModified from " + node
1467 + " until " + untilPath
, e
);
1472 * Returns a String representing the short version (see <a
1473 * href="http://jackrabbit.apache.org/node-type-notation.html"> Node type
1474 * Notation </a> attributes grammar) of the main business attributes of this
1475 * property definition
1479 public static String
getPropertyDefinitionAsString(Property prop
) {
1480 StringBuffer sbuf
= new StringBuffer();
1482 if (prop
.getDefinition().isAutoCreated())
1484 if (prop
.getDefinition().isMandatory())
1486 if (prop
.getDefinition().isProtected())
1488 if (prop
.getDefinition().isMultiple())
1490 } catch (RepositoryException re
) {
1491 throw new ArgeoException(
1492 "unexpected error while getting property definition as String",
1495 return sbuf
.toString();
1499 * Estimate the sub tree size from current node. Computation is based on the
1500 * Jcr {@link Property.getLength()} method. Note : it is not the exact size
1501 * used on the disk by the current part of the JCR Tree.
1504 public static long getNodeApproxSize(Node node
) {
1505 long curNodeSize
= 0;
1507 PropertyIterator pi
= node
.getProperties();
1508 while (pi
.hasNext()) {
1509 Property prop
= pi
.nextProperty();
1510 if (prop
.isMultiple()) {
1511 int nb
= prop
.getLengths().length
;
1512 for (int i
= 0; i
< nb
; i
++) {
1513 curNodeSize
+= (prop
.getLengths()[i
] > 0 ? prop
1514 .getLengths()[i
] : 0);
1517 curNodeSize
+= (prop
.getLength() > 0 ? prop
.getLength() : 0);
1520 NodeIterator ni
= node
.getNodes();
1521 while (ni
.hasNext())
1522 curNodeSize
+= getNodeApproxSize(ni
.nextNode());
1524 } catch (RepositoryException re
) {
1525 throw new ArgeoException(
1526 "Unexpected error while recursively determining node size.",
1536 * Convenience method for adding a single privilege to a principal (user or
1537 * role), typically jcr:all
1539 public static void addPrivilege(Session session
, String path
,
1540 String principal
, String privilege
) throws RepositoryException
{
1541 List
<Privilege
> privileges
= new ArrayList
<Privilege
>();
1542 privileges
.add(session
.getAccessControlManager().privilegeFromName(
1544 addPrivileges(session
, path
, new SimplePrincipal(principal
), privileges
);
1548 * Add privileges on a path to a {@link Principal}. The path must already
1549 * exist. Session is saved.
1551 public static void addPrivileges(Session session
, String path
,
1552 Principal principal
, List
<Privilege
> privs
)
1553 throws RepositoryException
{
1554 AccessControlManager acm
= session
.getAccessControlManager();
1555 // search for an access control list
1556 AccessControlList acl
= null;
1557 AccessControlPolicyIterator policyIterator
= acm
1558 .getApplicablePolicies(path
);
1559 if (policyIterator
.hasNext()) {
1560 while (policyIterator
.hasNext()) {
1561 AccessControlPolicy acp
= policyIterator
1562 .nextAccessControlPolicy();
1563 if (acp
instanceof AccessControlList
)
1564 acl
= ((AccessControlList
) acp
);
1567 AccessControlPolicy
[] existingPolicies
= acm
.getPolicies(path
);
1568 for (AccessControlPolicy acp
: existingPolicies
) {
1569 if (acp
instanceof AccessControlList
)
1570 acl
= ((AccessControlList
) acp
);
1575 acl
.addAccessControlEntry(principal
,
1576 privs
.toArray(new Privilege
[privs
.size()]));
1577 acm
.setPolicy(path
, acl
);
1578 if (log
.isDebugEnabled())
1579 log
.debug("Added privileges " + privs
+ " to " + principal
1582 throw new ArgeoException("Don't know how to apply privileges "
1583 + privs
+ " to " + principal
+ " on " + path
);