]> git.argeo.org Git - lgpl/argeo-commons.git/blob - JcrUtils.java
7d8b6565e652d5abdbdf3843ff5bbc66f012bd27
[lgpl/argeo-commons.git] / JcrUtils.java
1 /*
2 * Copyright (C) 2010 Mathieu Baudier <mbaudier@argeo.org>
3 *
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
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
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.
15 */
16
17 package org.argeo.jcr;
18
19 import java.io.ByteArrayInputStream;
20 import java.io.ByteArrayOutputStream;
21 import java.io.InputStream;
22 import java.net.MalformedURLException;
23 import java.net.URL;
24 import java.security.Principal;
25 import java.text.DateFormat;
26 import java.text.ParseException;
27 import java.util.ArrayList;
28 import java.util.Calendar;
29 import java.util.Collections;
30 import java.util.Date;
31 import java.util.GregorianCalendar;
32 import java.util.HashMap;
33 import java.util.Iterator;
34 import java.util.List;
35 import java.util.Map;
36 import java.util.TreeMap;
37
38 import javax.jcr.Binary;
39 import javax.jcr.NamespaceRegistry;
40 import javax.jcr.Node;
41 import javax.jcr.NodeIterator;
42 import javax.jcr.Property;
43 import javax.jcr.PropertyIterator;
44 import javax.jcr.PropertyType;
45 import javax.jcr.Repository;
46 import javax.jcr.RepositoryException;
47 import javax.jcr.RepositoryFactory;
48 import javax.jcr.Session;
49 import javax.jcr.Value;
50 import javax.jcr.Workspace;
51 import javax.jcr.nodetype.NodeType;
52 import javax.jcr.observation.EventListener;
53 import javax.jcr.query.Query;
54 import javax.jcr.query.QueryResult;
55 import javax.jcr.security.AccessControlEntry;
56 import javax.jcr.security.AccessControlList;
57 import javax.jcr.security.AccessControlManager;
58 import javax.jcr.security.AccessControlPolicy;
59 import javax.jcr.security.AccessControlPolicyIterator;
60 import javax.jcr.security.Privilege;
61 import javax.jcr.version.VersionManager;
62
63 import org.apache.commons.io.IOUtils;
64 import org.apache.commons.logging.Log;
65 import org.apache.commons.logging.LogFactory;
66 import org.argeo.ArgeoException;
67 import org.argeo.util.security.SimplePrincipal;
68
69 /** Utility methods to simplify common JCR operations. */
70 public class JcrUtils implements ArgeoJcrConstants {
71
72 private final static Log log = LogFactory.getLog(JcrUtils.class);
73
74 /**
75 * Not complete yet. See
76 * http://www.day.com/specs/jcr/2.0/3_Repository_Model.html#3.2.2%20Local
77 * %20Names
78 */
79 public final static char[] INVALID_NAME_CHARACTERS = { '/', ':', '[', ']',
80 '|', '*', /*
81 * invalid XML chars :
82 */
83 '<', '>', '&' };
84
85 /** Prevents instantiation */
86 private JcrUtils() {
87 }
88
89 /**
90 * Queries one single node.
91 *
92 * @return one single node or null if none was found
93 * @throws ArgeoException
94 * if more than one node was found
95 */
96 public static Node querySingleNode(Query query) {
97 NodeIterator nodeIterator;
98 try {
99 QueryResult queryResult = query.execute();
100 nodeIterator = queryResult.getNodes();
101 } catch (RepositoryException e) {
102 throw new ArgeoException("Cannot execute query " + query, e);
103 }
104 Node node;
105 if (nodeIterator.hasNext())
106 node = nodeIterator.nextNode();
107 else
108 return null;
109
110 if (nodeIterator.hasNext())
111 throw new ArgeoException("Query returned more than one node.");
112 return node;
113 }
114
115 /** Retrieves the parent path of the provided path */
116 public static String parentPath(String path) {
117 if (path.equals("/"))
118 throw new ArgeoException("Root path '/' has no parent path");
119 if (path.charAt(0) != '/')
120 throw new ArgeoException("Path " + path + " must start with a '/'");
121 String pathT = path;
122 if (pathT.charAt(pathT.length() - 1) == '/')
123 pathT = pathT.substring(0, pathT.length() - 2);
124
125 int index = pathT.lastIndexOf('/');
126 return pathT.substring(0, index);
127 }
128
129 /** The provided data as a path ('/' at the end, not the beginning) */
130 public static String dateAsPath(Calendar cal) {
131 return dateAsPath(cal, false);
132 }
133
134 /**
135 * Creates a deep path based on a URL:
136 * http://subdomain.example.com/to/content?args =>
137 * com/example/subdomain/to/content
138 */
139 public static String urlAsPath(String url) {
140 try {
141 URL u = new URL(url);
142 StringBuffer path = new StringBuffer(url.length());
143 // invert host
144 path.append(hostAsPath(u.getHost()));
145 // we don't put port since it may not always be there and may change
146 path.append(u.getPath());
147 return path.toString();
148 } catch (MalformedURLException e) {
149 throw new ArgeoException("Cannot generate URL path for " + url, e);
150 }
151 }
152
153 /** Set the {@link NodeType#NT_ADDRESS} properties based on this URL. */
154 public static void urlToAddressProperties(Node node, String url) {
155 try {
156 URL u = new URL(url);
157 node.setProperty(Property.JCR_PROTOCOL, u.getProtocol());
158 node.setProperty(Property.JCR_HOST, u.getHost());
159 node.setProperty(Property.JCR_PORT, Integer.toString(u.getPort()));
160 node.setProperty(Property.JCR_PATH, normalizePath(u.getPath()));
161 } catch (Exception e) {
162 throw new ArgeoException("Cannot set URL " + url
163 + " as nt:address properties", e);
164 }
165 }
166
167 /** Build URL based on the {@link NodeType#NT_ADDRESS} properties. */
168 public static String urlFromAddressProperties(Node node) {
169 try {
170 URL u = new URL(
171 node.getProperty(Property.JCR_PROTOCOL).getString(), node
172 .getProperty(Property.JCR_HOST).getString(),
173 (int) node.getProperty(Property.JCR_PORT).getLong(), node
174 .getProperty(Property.JCR_PATH).getString());
175 return u.toString();
176 } catch (Exception e) {
177 throw new ArgeoException(
178 "Cannot get URL from nt:address properties of " + node, e);
179 }
180 }
181
182 /** Make sure that: starts with '/', do not end with '/', do not have '//' */
183 public static String normalizePath(String path) {
184 List<String> tokens = tokenize(path);
185 StringBuffer buf = new StringBuffer(path.length());
186 for (String token : tokens) {
187 buf.append('/');
188 buf.append(token);
189 }
190 return buf.toString();
191 }
192
193 /**
194 * Creates a path from a FQDN, inverting the order of the component:
195 * www.argeo.org => org.argeo.www
196 */
197 public static String hostAsPath(String host) {
198 StringBuffer path = new StringBuffer(host.length());
199 String[] hostTokens = host.split("\\.");
200 for (int i = hostTokens.length - 1; i >= 0; i--) {
201 path.append(hostTokens[i]);
202 if (i != 0)
203 path.append('/');
204 }
205 return path.toString();
206 }
207
208 /**
209 * The provided data as a path ('/' at the end, not the beginning)
210 *
211 * @param cal
212 * the date
213 * @param addHour
214 * whether to add hour as well
215 */
216 public static String dateAsPath(Calendar cal, Boolean addHour) {
217 StringBuffer buf = new StringBuffer(14);
218 buf.append('Y');
219 buf.append(cal.get(Calendar.YEAR));
220 buf.append('/');
221
222 int month = cal.get(Calendar.MONTH) + 1;
223 buf.append('M');
224 if (month < 10)
225 buf.append(0);
226 buf.append(month);
227 buf.append('/');
228
229 int day = cal.get(Calendar.DAY_OF_MONTH);
230 buf.append('D');
231 if (day < 10)
232 buf.append(0);
233 buf.append(day);
234 buf.append('/');
235
236 if (addHour) {
237 int hour = cal.get(Calendar.HOUR_OF_DAY);
238 buf.append('H');
239 if (hour < 10)
240 buf.append(0);
241 buf.append(hour);
242 buf.append('/');
243 }
244 return buf.toString();
245
246 }
247
248 /** Converts in one call a string into a gregorian calendar. */
249 public static Calendar parseCalendar(DateFormat dateFormat, String value) {
250 try {
251 Date date = dateFormat.parse(value);
252 Calendar calendar = new GregorianCalendar();
253 calendar.setTime(date);
254 return calendar;
255 } catch (ParseException e) {
256 throw new ArgeoException("Cannot parse " + value
257 + " with date format " + dateFormat, e);
258 }
259
260 }
261
262 /** The last element of a path. */
263 public static String lastPathElement(String path) {
264 if (path.charAt(path.length() - 1) == '/')
265 throw new ArgeoException("Path " + path + " cannot end with '/'");
266 int index = path.lastIndexOf('/');
267 if (index < 0)
268 throw new ArgeoException("Cannot find last path element for "
269 + path);
270 return path.substring(index + 1);
271 }
272
273 /**
274 * Routine that get the child with this name, adding id it does not already
275 * exist
276 */
277 public static Node getOrAdd(Node parent, String childName,
278 String childPrimaryNodeType) throws RepositoryException {
279 return parent.hasNode(childName) ? parent.getNode(childName) : parent
280 .addNode(childName, childPrimaryNodeType);
281 }
282
283 /**
284 * Routine that get the child with this name, adding id it does not already
285 * exist
286 */
287 public static Node getOrAdd(Node parent, String childName)
288 throws RepositoryException {
289 return parent.hasNode(childName) ? parent.getNode(childName) : parent
290 .addNode(childName);
291 }
292
293 /** Creates the nodes making path, if they don't exist. */
294 public static Node mkdirs(Session session, String path) {
295 return mkdirs(session, path, null, null, false);
296 }
297
298 /**
299 * use {@link #mkdirs(Session, String, String, String, Boolean)} instead.
300 *
301 * @deprecated
302 */
303 @Deprecated
304 public static Node mkdirs(Session session, String path, String type,
305 Boolean versioning) {
306 return mkdirs(session, path, type, type, false);
307 }
308
309 /**
310 * @param type
311 * the type of the leaf node
312 */
313 public static Node mkdirs(Session session, String path, String type) {
314 return mkdirs(session, path, type, null, false);
315 }
316
317 /**
318 * Synchronized and save is performed, to avoid race conditions in
319 * initializers leading to duplicate nodes.
320 */
321 public synchronized static Node mkdirsSafe(Session session, String path,
322 String type) {
323 try {
324 if (session.hasPendingChanges())
325 throw new ArgeoException(
326 "Session has pending changes, save them first.");
327 Node node = mkdirs(session, path, type);
328 session.save();
329 return node;
330 } catch (RepositoryException e) {
331 discardQuietly(session);
332 throw new ArgeoException("Cannot safely make directories", e);
333 }
334 }
335
336 public synchronized static Node mkdirsSafe(Session session, String path) {
337 return mkdirsSafe(session, path, null);
338 }
339
340 /**
341 * Creates the nodes making path, if they don't exist. This is up to the
342 * caller to save the session. Use with caution since it can create
343 * duplicate nodes if used concurrently.
344 */
345 public static Node mkdirs(Session session, String path, String type,
346 String intermediaryNodeType, Boolean versioning) {
347 try {
348 if (path.equals('/'))
349 return session.getRootNode();
350
351 if (session.itemExists(path)) {
352 Node node = session.getNode(path);
353 // check type
354 if (type != null && !node.isNodeType(type))
355 throw new ArgeoException("Node " + node
356 + " exists but is of type "
357 + node.getPrimaryNodeType().getName()
358 + " not of type " + type);
359 // TODO: check versioning
360 return node;
361 }
362
363 StringBuffer current = new StringBuffer("/");
364 Node currentNode = session.getRootNode();
365 Iterator<String> it = tokenize(path).iterator();
366 while (it.hasNext()) {
367 String part = it.next();
368 current.append(part).append('/');
369 if (!session.itemExists(current.toString())) {
370 if (!it.hasNext() && type != null)
371 currentNode = currentNode.addNode(part, type);
372 else if (it.hasNext() && intermediaryNodeType != null)
373 currentNode = currentNode.addNode(part,
374 intermediaryNodeType);
375 else
376 currentNode = currentNode.addNode(part);
377 if (versioning)
378 currentNode.addMixin(NodeType.MIX_VERSIONABLE);
379 if (log.isTraceEnabled())
380 log.debug("Added folder " + part + " as " + current);
381 } else {
382 currentNode = (Node) session.getItem(current.toString());
383 }
384 }
385 return currentNode;
386 } catch (RepositoryException e) {
387 discardQuietly(session);
388 throw new ArgeoException("Cannot mkdirs " + path, e);
389 } finally {
390 }
391 }
392
393 /** Convert a path to the list of its tokens */
394 public static List<String> tokenize(String path) {
395 List<String> tokens = new ArrayList<String>();
396 boolean optimized = false;
397 if (!optimized) {
398 String[] rawTokens = path.split("/");
399 for (String token : rawTokens) {
400 if (!token.equals(""))
401 tokens.add(token);
402 }
403 } else {
404 StringBuffer curr = new StringBuffer();
405 char[] arr = path.toCharArray();
406 chars: for (int i = 0; i < arr.length; i++) {
407 char c = arr[i];
408 if (c == '/') {
409 if (i == 0 || (i == arr.length - 1))
410 continue chars;
411 if (curr.length() > 0) {
412 tokens.add(curr.toString());
413 curr = new StringBuffer();
414 }
415 } else
416 curr.append(c);
417 }
418 if (curr.length() > 0) {
419 tokens.add(curr.toString());
420 curr = new StringBuffer();
421 }
422 }
423 return Collections.unmodifiableList(tokens);
424 }
425
426 /**
427 * Safe and repository implementation independent registration of a
428 * namespace.
429 */
430 public static void registerNamespaceSafely(Session session, String prefix,
431 String uri) {
432 try {
433 registerNamespaceSafely(session.getWorkspace()
434 .getNamespaceRegistry(), prefix, uri);
435 } catch (RepositoryException e) {
436 throw new ArgeoException("Cannot find namespace registry", e);
437 }
438 }
439
440 /**
441 * Safe and repository implementation independent registration of a
442 * namespace.
443 */
444 public static void registerNamespaceSafely(NamespaceRegistry nr,
445 String prefix, String uri) {
446 try {
447 String[] prefixes = nr.getPrefixes();
448 for (String pref : prefixes)
449 if (pref.equals(prefix)) {
450 String registeredUri = nr.getURI(pref);
451 if (!registeredUri.equals(uri))
452 throw new ArgeoException("Prefix " + pref
453 + " already registered for URI "
454 + registeredUri
455 + " which is different from provided URI "
456 + uri);
457 else
458 return;// skip
459 }
460 nr.registerNamespace(prefix, uri);
461 } catch (RepositoryException e) {
462 throw new ArgeoException("Cannot register namespace " + uri
463 + " under prefix " + prefix, e);
464 }
465 }
466
467 /** Recursively outputs the contents of the given node. */
468 public static void debug(Node node) {
469 debug(node, log);
470 }
471
472 /** Recursively outputs the contents of the given node. */
473 public static void debug(Node node, Log log) {
474 try {
475 // First output the node path
476 log.debug(node.getPath());
477 // Skip the virtual (and large!) jcr:system subtree
478 if (node.getName().equals("jcr:system")) {
479 return;
480 }
481
482 // Then the children nodes (recursive)
483 NodeIterator it = node.getNodes();
484 while (it.hasNext()) {
485 Node childNode = it.nextNode();
486 debug(childNode, log);
487 }
488
489 // Then output the properties
490 PropertyIterator properties = node.getProperties();
491 // log.debug("Property are : ");
492
493 properties: while (properties.hasNext()) {
494 Property property = properties.nextProperty();
495 if (property.getType() == PropertyType.BINARY)
496 continue properties;// skip
497 if (property.getDefinition().isMultiple()) {
498 // A multi-valued property, print all values
499 Value[] values = property.getValues();
500 for (int i = 0; i < values.length; i++) {
501 log.debug(property.getPath() + "="
502 + values[i].getString());
503 }
504 } else {
505 // A single-valued property
506 log.debug(property.getPath() + "=" + property.getString());
507 }
508 }
509 } catch (Exception e) {
510 log.error("Could not debug " + node, e);
511 }
512
513 }
514
515 /** Logs the effective access control policies */
516 public static void logEffectiveAccessPolicies(Node node) {
517 try {
518 logEffectiveAccessPolicies(node.getSession(), node.getPath());
519 } catch (RepositoryException e) {
520 log.error("Cannot log effective access policies of " + node, e);
521 }
522 }
523
524 /** Logs the effective access control policies */
525 public static void logEffectiveAccessPolicies(Session session, String path) {
526 if (!log.isDebugEnabled())
527 return;
528
529 try {
530 AccessControlPolicy[] effectivePolicies = session
531 .getAccessControlManager().getEffectivePolicies(path);
532 if (effectivePolicies.length > 0) {
533 for (AccessControlPolicy policy : effectivePolicies) {
534 if (policy instanceof AccessControlList) {
535 AccessControlList acl = (AccessControlList) policy;
536 log.debug("Access control list for " + path + "\n"
537 + accessControlListSummary(acl));
538 }
539 }
540 } else {
541 log.debug("No effective access control policy for " + path);
542 }
543 } catch (RepositoryException e) {
544 log.error("Cannot log effective access policies of " + path, e);
545 }
546 }
547
548 /** Returns a human-readable summary of this access control list. */
549 public static String accessControlListSummary(AccessControlList acl) {
550 StringBuffer buf = new StringBuffer("");
551 try {
552 for (AccessControlEntry ace : acl.getAccessControlEntries()) {
553 buf.append('\t').append(ace.getPrincipal().getName())
554 .append('\n');
555 for (Privilege priv : ace.getPrivileges())
556 buf.append("\t\t").append(priv.getName()).append('\n');
557 }
558 return buf.toString();
559 } catch (RepositoryException e) {
560 throw new ArgeoException("Cannot write summary of " + acl, e);
561 }
562 }
563
564 /**
565 * Copies recursively the content of a node to another one. Do NOT copy the
566 * property values of {@link NodeType#MIX_CREATED} and
567 * {@link NodeType#MIX_LAST_MODIFIED}, but update the
568 * {@link Property#JCR_LAST_MODIFIED} and
569 * {@link Property#JCR_LAST_MODIFIED_BY} properties if the target node has
570 * the {@link NodeType#MIX_LAST_MODIFIED} mixin.
571 */
572 public static void copy(Node fromNode, Node toNode) {
573 try {
574 // process properties
575 PropertyIterator pit = fromNode.getProperties();
576 properties: while (pit.hasNext()) {
577 Property fromProperty = pit.nextProperty();
578 String propertyName = fromProperty.getName();
579 if (toNode.hasProperty(propertyName)
580 && toNode.getProperty(propertyName).getDefinition()
581 .isProtected())
582 continue properties;
583
584 if (fromProperty.getDefinition().isProtected())
585 continue properties;
586
587 if (propertyName.equals("jcr:created")
588 || propertyName.equals("jcr:createdBy")
589 || propertyName.equals("jcr:lastModified")
590 || propertyName.equals("jcr:lastModifiedBy"))
591 continue properties;
592
593 if (fromProperty.isMultiple()) {
594 toNode.setProperty(propertyName, fromProperty.getValues());
595 } else {
596 toNode.setProperty(propertyName, fromProperty.getValue());
597 }
598 }
599
600 // update jcr:lastModified and jcr:lastModifiedBy in toNode in case
601 // they existed, before adding the mixins
602 updateLastModified(toNode);
603
604 // add mixins
605 for (NodeType mixinType : fromNode.getMixinNodeTypes()) {
606 toNode.addMixin(mixinType.getName());
607 }
608
609 // process children nodes
610 NodeIterator nit = fromNode.getNodes();
611 while (nit.hasNext()) {
612 Node fromChild = nit.nextNode();
613 Integer index = fromChild.getIndex();
614 String nodeRelPath = fromChild.getName() + "[" + index + "]";
615 Node toChild;
616 if (toNode.hasNode(nodeRelPath))
617 toChild = toNode.getNode(nodeRelPath);
618 else
619 toChild = toNode.addNode(fromChild.getName(), fromChild
620 .getPrimaryNodeType().getName());
621 copy(fromChild, toChild);
622 }
623 } catch (RepositoryException e) {
624 throw new ArgeoException("Cannot copy " + fromNode + " to "
625 + toNode, e);
626 }
627 }
628
629 /**
630 * Check whether all first-level properties (except jcr:* properties) are
631 * equal. Skip jcr:* properties
632 */
633 public static Boolean allPropertiesEquals(Node reference, Node observed,
634 Boolean onlyCommonProperties) {
635 try {
636 PropertyIterator pit = reference.getProperties();
637 props: while (pit.hasNext()) {
638 Property propReference = pit.nextProperty();
639 String propName = propReference.getName();
640 if (propName.startsWith("jcr:"))
641 continue props;
642
643 if (!observed.hasProperty(propName))
644 if (onlyCommonProperties)
645 continue props;
646 else
647 return false;
648 // TODO: deal with multiple property values?
649 if (!observed.getProperty(propName).getValue()
650 .equals(propReference.getValue()))
651 return false;
652 }
653 return true;
654 } catch (RepositoryException e) {
655 throw new ArgeoException("Cannot check all properties equals of "
656 + reference + " and " + observed, e);
657 }
658 }
659
660 public static Map<String, PropertyDiff> diffProperties(Node reference,
661 Node observed) {
662 Map<String, PropertyDiff> diffs = new TreeMap<String, PropertyDiff>();
663 diffPropertiesLevel(diffs, null, reference, observed);
664 return diffs;
665 }
666
667 /**
668 * Compare the properties of two nodes. Recursivity to child nodes is not
669 * yet supported. Skip jcr:* properties.
670 */
671 static void diffPropertiesLevel(Map<String, PropertyDiff> diffs,
672 String baseRelPath, Node reference, Node observed) {
673 try {
674 // check removed and modified
675 PropertyIterator pit = reference.getProperties();
676 props: while (pit.hasNext()) {
677 Property p = pit.nextProperty();
678 String name = p.getName();
679 if (name.startsWith("jcr:"))
680 continue props;
681
682 if (!observed.hasProperty(name)) {
683 String relPath = propertyRelPath(baseRelPath, name);
684 PropertyDiff pDiff = new PropertyDiff(PropertyDiff.REMOVED,
685 relPath, p.getValue(), null);
686 diffs.put(relPath, pDiff);
687 } else {
688 if (p.isMultiple()) {
689 // FIXME implement multiple
690 } else {
691 Value referenceValue = p.getValue();
692 Value newValue = observed.getProperty(name).getValue();
693 if (!referenceValue.equals(newValue)) {
694 String relPath = propertyRelPath(baseRelPath, name);
695 PropertyDiff pDiff = new PropertyDiff(
696 PropertyDiff.MODIFIED, relPath,
697 referenceValue, newValue);
698 diffs.put(relPath, pDiff);
699 }
700 }
701 }
702 }
703 // check added
704 pit = observed.getProperties();
705 props: while (pit.hasNext()) {
706 Property p = pit.nextProperty();
707 String name = p.getName();
708 if (name.startsWith("jcr:"))
709 continue props;
710 if (!reference.hasProperty(name)) {
711 if (p.isMultiple()) {
712 // FIXME implement multiple
713 } else {
714 String relPath = propertyRelPath(baseRelPath, name);
715 PropertyDiff pDiff = new PropertyDiff(
716 PropertyDiff.ADDED, relPath, null, p.getValue());
717 diffs.put(relPath, pDiff);
718 }
719 }
720 }
721 } catch (RepositoryException e) {
722 throw new ArgeoException("Cannot diff " + reference + " and "
723 + observed, e);
724 }
725 }
726
727 /**
728 * Compare only a restricted list of properties of two nodes. No
729 * recursivity.
730 *
731 */
732 public static Map<String, PropertyDiff> diffProperties(Node reference,
733 Node observed, List<String> properties) {
734 Map<String, PropertyDiff> diffs = new TreeMap<String, PropertyDiff>();
735 try {
736 Iterator<String> pit = properties.iterator();
737
738 props: while (pit.hasNext()) {
739 String name = pit.next();
740 if (!reference.hasProperty(name)) {
741 if (!observed.hasProperty(name))
742 continue props;
743 Value val = observed.getProperty(name).getValue();
744 try {
745 // empty String but not null
746 if ("".equals(val.getString()))
747 continue props;
748 } catch (Exception e) {
749 // not parseable as String, silent
750 }
751 PropertyDiff pDiff = new PropertyDiff(PropertyDiff.ADDED,
752 name, null, val);
753 diffs.put(name, pDiff);
754 } else if (!observed.hasProperty(name)) {
755 PropertyDiff pDiff = new PropertyDiff(PropertyDiff.REMOVED,
756 name, reference.getProperty(name).getValue(), null);
757 diffs.put(name, pDiff);
758 } else {
759 Value referenceValue = reference.getProperty(name)
760 .getValue();
761 Value newValue = observed.getProperty(name).getValue();
762 if (!referenceValue.equals(newValue)) {
763 PropertyDiff pDiff = new PropertyDiff(
764 PropertyDiff.MODIFIED, name, referenceValue,
765 newValue);
766 diffs.put(name, pDiff);
767 }
768 }
769 }
770 } catch (RepositoryException e) {
771 throw new ArgeoException("Cannot diff " + reference + " and "
772 + observed, e);
773 }
774 return diffs;
775 }
776
777 /** Builds a property relPath to be used in the diff. */
778 private static String propertyRelPath(String baseRelPath,
779 String propertyName) {
780 if (baseRelPath == null)
781 return propertyName;
782 else
783 return baseRelPath + '/' + propertyName;
784 }
785
786 /**
787 * Normalizes a name so that it can be stored in contexts not supporting
788 * names with ':' (typically databases). Replaces ':' by '_'.
789 */
790 public static String normalize(String name) {
791 return name.replace(':', '_');
792 }
793
794 /**
795 * Replaces characters which are invalid in a JCR name by '_'. Currently not
796 * exhaustive.
797 *
798 * @see JcrUtils#INVALID_NAME_CHARACTERS
799 */
800 public static String replaceInvalidChars(String name) {
801 return replaceInvalidChars(name, '_');
802 }
803
804 /**
805 * Replaces characters which are invalid in a JCR name. Currently not
806 * exhaustive.
807 *
808 * @see JcrUtils#INVALID_NAME_CHARACTERS
809 */
810 public static String replaceInvalidChars(String name, char replacement) {
811 boolean modified = false;
812 char[] arr = name.toCharArray();
813 for (int i = 0; i < arr.length; i++) {
814 char c = arr[i];
815 invalid: for (char invalid : INVALID_NAME_CHARACTERS) {
816 if (c == invalid) {
817 arr[i] = replacement;
818 modified = true;
819 break invalid;
820 }
821 }
822 }
823 if (modified)
824 return new String(arr);
825 else
826 // do not create new object if unnecessary
827 return name;
828 }
829
830 /**
831 * Removes forbidden characters from a path, replacing them with '_'
832 *
833 * @deprecated use {@link #replaceInvalidChars(String)} instead
834 */
835 public static String removeForbiddenCharacters(String str) {
836 return str.replace('[', '_').replace(']', '_').replace('/', '_')
837 .replace('*', '_');
838
839 }
840
841 /** Cleanly disposes a {@link Binary} even if it is null. */
842 public static void closeQuietly(Binary binary) {
843 if (binary == null)
844 return;
845 binary.dispose();
846 }
847
848 /** Retrieve a {@link Binary} as a byte array */
849 public static byte[] getBinaryAsBytes(Property property) {
850 ByteArrayOutputStream out = new ByteArrayOutputStream();
851 InputStream in = null;
852 Binary binary = null;
853 try {
854 binary = property.getBinary();
855 in = binary.getStream();
856 IOUtils.copy(in, out);
857 return out.toByteArray();
858 } catch (Exception e) {
859 throw new ArgeoException("Cannot read binary " + property
860 + " as bytes", e);
861 } finally {
862 IOUtils.closeQuietly(out);
863 IOUtils.closeQuietly(in);
864 closeQuietly(binary);
865 }
866 }
867
868 /** Writes a {@link Binary} from a byte array */
869 public static void setBinaryAsBytes(Node node, String property, byte[] bytes) {
870 InputStream in = null;
871 Binary binary = null;
872 try {
873 in = new ByteArrayInputStream(bytes);
874 binary = node.getSession().getValueFactory().createBinary(in);
875 node.setProperty(property, binary);
876 } catch (Exception e) {
877 throw new ArgeoException("Cannot read binary " + property
878 + " as bytes", e);
879 } finally {
880 IOUtils.closeQuietly(in);
881 closeQuietly(binary);
882 }
883 }
884
885 /**
886 * Creates depth from a string (typically a username) by adding levels based
887 * on its first characters: "aBcD",2 => a/aB
888 */
889 public static String firstCharsToPath(String str, Integer nbrOfChars) {
890 if (str.length() < nbrOfChars)
891 throw new ArgeoException("String " + str
892 + " length must be greater or equal than " + nbrOfChars);
893 StringBuffer path = new StringBuffer("");
894 StringBuffer curr = new StringBuffer("");
895 for (int i = 0; i < nbrOfChars; i++) {
896 curr.append(str.charAt(i));
897 path.append(curr);
898 if (i < nbrOfChars - 1)
899 path.append('/');
900 }
901 return path.toString();
902 }
903
904 /**
905 * Wraps the call to the repository factory based on parameter
906 * {@link ArgeoJcrConstants#JCR_REPOSITORY_ALIAS} in order to simplify it
907 * and protect against future API changes.
908 */
909 public static Repository getRepositoryByAlias(
910 RepositoryFactory repositoryFactory, String alias) {
911 try {
912 Map<String, String> parameters = new HashMap<String, String>();
913 parameters.put(JCR_REPOSITORY_ALIAS, alias);
914 return repositoryFactory.getRepository(parameters);
915 } catch (RepositoryException e) {
916 throw new ArgeoException(
917 "Unexpected exception when trying to retrieve repository with alias "
918 + alias, e);
919 }
920 }
921
922 /**
923 * Wraps the call to the repository factory based on parameter
924 * {@link ArgeoJcrConstants#JCR_REPOSITORY_URI} in order to simplify it and
925 * protect against future API changes.
926 */
927 public static Repository getRepositoryByUri(
928 RepositoryFactory repositoryFactory, String uri) {
929 try {
930 Map<String, String> parameters = new HashMap<String, String>();
931 parameters.put(JCR_REPOSITORY_URI, uri);
932 return repositoryFactory.getRepository(parameters);
933 } catch (RepositoryException e) {
934 throw new ArgeoException(
935 "Unexpected exception when trying to retrieve repository with uri "
936 + uri, e);
937 }
938 }
939
940 /**
941 * Discards the current changes in the session attached to this node. To be
942 * used typically in a catch block.
943 *
944 * @see #discardQuietly(Session)
945 */
946 public static void discardUnderlyingSessionQuietly(Node node) {
947 try {
948 discardQuietly(node.getSession());
949 } catch (RepositoryException e) {
950 log.warn("Cannot quietly discard session of node " + node + ": "
951 + e.getMessage());
952 }
953 }
954
955 /**
956 * Discards the current changes in a session by calling
957 * {@link Session#refresh(boolean)} with <code>false</code>, only logging
958 * potential errors when doing so. To be used typically in a catch block.
959 */
960 public static void discardQuietly(Session session) {
961 try {
962 if (session != null)
963 session.refresh(false);
964 } catch (RepositoryException e) {
965 log.warn("Cannot quietly discard session " + session + ": "
966 + e.getMessage());
967 }
968 }
969
970 /** Logs out the session, not throwing any exception, even if it is null. */
971 public static void logoutQuietly(Session session) {
972 try {
973 if (session != null)
974 if (session.isLive())
975 session.logout();
976 } catch (Exception e) {
977 // silent
978 }
979 }
980
981 /**
982 * Convenient method to add a listener. uuids passed as null, deep=true,
983 * local=true, only one node type
984 */
985 public static void addListener(Session session, EventListener listener,
986 int eventTypes, String basePath, String nodeType) {
987 try {
988 session.getWorkspace()
989 .getObservationManager()
990 .addEventListener(listener, eventTypes, basePath, true,
991 null, new String[] { nodeType }, true);
992 } catch (RepositoryException e) {
993 throw new ArgeoException("Cannot add JCR listener " + listener
994 + " to session " + session, e);
995 }
996 }
997
998 /** Removes a listener without throwing exception */
999 public static void removeListenerQuietly(Session session,
1000 EventListener listener) {
1001 if (session == null || !session.isLive())
1002 return;
1003 try {
1004 session.getWorkspace().getObservationManager()
1005 .removeEventListener(listener);
1006 } catch (RepositoryException e) {
1007 // silent
1008 }
1009 }
1010
1011 /** Returns the home node of the session user or null if none was found. */
1012 public static Node getUserHome(Session session) {
1013 String userID = session.getUserID();
1014 return getUserHome(session, userID);
1015 }
1016
1017 /** User home path is NOT configurable */
1018 public static String getUserHomePath(String username) {
1019 String homeBasePath = DEFAULT_HOME_BASE_PATH;
1020 return homeBasePath + '/' + firstCharsToPath(username, 2) + '/'
1021 + username;
1022 }
1023
1024 /**
1025 * Returns the home node of the session user or null if none was found.
1026 *
1027 * @param session
1028 * the session to use in order to perform the search, this can be
1029 * a session with a different user ID than the one searched,
1030 * typically when a system or admin session is used.
1031 * @param username
1032 * the username of the user
1033 */
1034 public static Node getUserHome(Session session, String username) {
1035 try {
1036 String homePath = getUserHomePath(username);
1037 return session.itemExists(homePath) ? session.getNode(homePath)
1038 : null;
1039 // kept for example of QOM queries
1040 // QueryObjectModelFactory qomf = session.getWorkspace()
1041 // .getQueryManager().getQOMFactory();
1042 // Selector userHomeSel = qomf.selector(ArgeoTypes.ARGEO_USER_HOME,
1043 // "userHome");
1044 // DynamicOperand userIdDop = qomf.propertyValue("userHome",
1045 // ArgeoNames.ARGEO_USER_ID);
1046 // StaticOperand userIdSop = qomf.literal(session.getValueFactory()
1047 // .createValue(username));
1048 // Constraint constraint = qomf.comparison(userIdDop,
1049 // QueryObjectModelFactory.JCR_OPERATOR_EQUAL_TO, userIdSop);
1050 // Query query = qomf.createQuery(userHomeSel, constraint, null,
1051 // null);
1052 // Node userHome = JcrUtils.querySingleNode(query);
1053 } catch (RepositoryException e) {
1054 throw new ArgeoException("Cannot find home for user " + username, e);
1055 }
1056 }
1057
1058 /**
1059 * Creates an Argeo user home, does nothing if it already exists. Session is
1060 * NOT saved.
1061 */
1062 public static Node createUserHomeIfNeeded(Session session, String username) {
1063 try {
1064 String homePath = getUserHomePath(username);
1065 if (session.itemExists(homePath))
1066 return session.getNode(homePath);
1067 else {
1068 Node userHome = JcrUtils.mkdirs(session, homePath);
1069 userHome.addMixin(ArgeoTypes.ARGEO_USER_HOME);
1070 userHome.setProperty(ArgeoNames.ARGEO_USER_ID, username);
1071 return userHome;
1072 }
1073 } catch (RepositoryException e) {
1074 discardQuietly(session);
1075 throw new ArgeoException("Cannot create home for " + username
1076 + " in workspace " + session.getWorkspace().getName(), e);
1077 }
1078 }
1079
1080 /**
1081 * Creates a user profile in the home of this user. Creates the home if
1082 * needed, but throw an exception if a profile already exists. The session
1083 * is not saved and the node is in a checkedOut state (that is, it requires
1084 * a subsequent checkin after saving the session).
1085 */
1086 public static Node createUserProfile(Session session, String username) {
1087 try {
1088 Node userHome = createUserHomeIfNeeded(session, username);
1089 if (userHome.hasNode(ArgeoNames.ARGEO_PROFILE))
1090 throw new ArgeoException(
1091 "There is already a user profile under " + userHome);
1092 Node userProfile = userHome.addNode(ArgeoNames.ARGEO_PROFILE);
1093 userProfile.addMixin(ArgeoTypes.ARGEO_USER_PROFILE);
1094 userProfile.setProperty(ArgeoNames.ARGEO_USER_ID, username);
1095 userProfile.setProperty(ArgeoNames.ARGEO_ENABLED, true);
1096 userProfile.setProperty(ArgeoNames.ARGEO_ACCOUNT_NON_EXPIRED, true);
1097 userProfile.setProperty(ArgeoNames.ARGEO_ACCOUNT_NON_LOCKED, true);
1098 userProfile.setProperty(ArgeoNames.ARGEO_CREDENTIALS_NON_EXPIRED,
1099 true);
1100 return userProfile;
1101 } catch (RepositoryException e) {
1102 discardQuietly(session);
1103 throw new ArgeoException("Cannot create user profile for "
1104 + username + " in workspace "
1105 + session.getWorkspace().getName(), e);
1106 }
1107 }
1108
1109 /**
1110 * Create user profile if needed, the session IS saved.
1111 *
1112 * @return the user profile
1113 */
1114 public static Node createUserProfileIfNeeded(Session securitySession,
1115 String username) {
1116 try {
1117 Node userHome = JcrUtils.createUserHomeIfNeeded(securitySession,
1118 username);
1119 Node userProfile = userHome.hasNode(ArgeoNames.ARGEO_PROFILE) ? userHome
1120 .getNode(ArgeoNames.ARGEO_PROFILE) : JcrUtils
1121 .createUserProfile(securitySession, username);
1122 if (securitySession.hasPendingChanges())
1123 securitySession.save();
1124 VersionManager versionManager = securitySession.getWorkspace()
1125 .getVersionManager();
1126 if (versionManager.isCheckedOut(userProfile.getPath()))
1127 versionManager.checkin(userProfile.getPath());
1128 return userProfile;
1129 } catch (RepositoryException e) {
1130 discardQuietly(securitySession);
1131 throw new ArgeoException("Cannot create user profile for "
1132 + username + " in workspace "
1133 + securitySession.getWorkspace().getName(), e);
1134 }
1135 }
1136
1137 /** Creates an Argeo user home. */
1138 // public static Node createUserHome(Session session, String homeBasePath,
1139 // String username) {
1140 // try {
1141 // if (session == null)
1142 // throw new ArgeoException("Session is null");
1143 // if (session.hasPendingChanges())
1144 // throw new ArgeoException(
1145 // "Session has pending changes, save them first");
1146 //
1147 // String homePath = getUserHomePath(username);
1148 //
1149 // if (session.itemExists(homePath)) {
1150 // try {
1151 // throw new ArgeoException(
1152 // "Trying to create a user home that already exists");
1153 // } catch (Exception e) {
1154 // // we use this workaround to be sure to get the stack trace
1155 // // to identify the sink of the bug.
1156 // log.warn("trying to create an already existing userHome at path:"
1157 // + homePath + ". Stack trace : ");
1158 // e.printStackTrace();
1159 // }
1160 // }
1161 //
1162 // Node userHome = JcrUtils.mkdirs(session, homePath);
1163 // Node userProfile;
1164 // if (userHome.hasNode(ArgeoNames.ARGEO_PROFILE)) {
1165 // log.warn("userProfile node already exists for userHome path: "
1166 // + homePath + ". We do not add a new one");
1167 // } else {
1168 // userProfile = userHome.addNode(ArgeoNames.ARGEO_PROFILE);
1169 // userProfile.addMixin(ArgeoTypes.ARGEO_USER_PROFILE);
1170 // // session.getWorkspace().getVersionManager()
1171 // // .checkout(userProfile.getPath());
1172 // userProfile.setProperty(ArgeoNames.ARGEO_USER_ID, username);
1173 // session.save();
1174 // session.getWorkspace().getVersionManager()
1175 // .checkin(userProfile.getPath());
1176 // // we need to save the profile before adding the user home type
1177 // }
1178 // userHome.addMixin(ArgeoTypes.ARGEO_USER_HOME);
1179 // // see
1180 // //
1181 // http://jackrabbit.510166.n4.nabble.com/Jackrabbit-2-0-beta-6-Problem-adding-a-Mixin-type-with-mandatory-properties-after-setting-propertiesn-td1290332.html
1182 // userHome.setProperty(ArgeoNames.ARGEO_USER_ID, username);
1183 // session.save();
1184 // return userHome;
1185 // } catch (RepositoryException e) {
1186 // discardQuietly(session);
1187 // throw new ArgeoException("Cannot create home node for user "
1188 // + username, e);
1189 // }
1190 // }
1191
1192 /**
1193 * Returns user home has path, embedding exceptions. Contrary to
1194 * {@link #getUserHome(Session)}, it never returns null but throws and
1195 * exception if not found.
1196 *
1197 * @deprecated use getUserHome() instead, throwing an exception if it
1198 * returns null
1199 */
1200 @Deprecated
1201 public static String getUserHomePath(Session session) {
1202 String userID = session.getUserID();
1203 try {
1204 String homePath = getUserHomePath(userID);
1205 if (session.itemExists(homePath))
1206 return homePath;
1207 else
1208 throw new ArgeoException("No home registered for " + userID);
1209 } catch (RepositoryException e) {
1210 throw new ArgeoException("Cannot find user home path", e);
1211 }
1212 }
1213
1214 /**
1215 * @return null if not found *
1216 */
1217 public static Node getUserProfile(Session session, String username) {
1218 try {
1219 Node userHome = getUserHome(session, username);
1220 if (userHome == null)
1221 return null;
1222 if (userHome.hasNode(ArgeoNames.ARGEO_PROFILE))
1223 return userHome.getNode(ArgeoNames.ARGEO_PROFILE);
1224 else
1225 return null;
1226 } catch (RepositoryException e) {
1227 throw new ArgeoException(
1228 "Cannot find profile for user " + username, e);
1229 }
1230 }
1231
1232 /**
1233 * Get the profile of the user attached to this session.
1234 */
1235 public static Node getUserProfile(Session session) {
1236 String userID = session.getUserID();
1237 return getUserProfile(session, userID);
1238 }
1239
1240 /**
1241 * Quietly unregisters an {@link EventListener} from the udnerlying
1242 * workspace of this node.
1243 */
1244 public static void unregisterQuietly(Node node, EventListener eventListener) {
1245 try {
1246 unregisterQuietly(node.getSession().getWorkspace(), eventListener);
1247 } catch (RepositoryException e) {
1248 // silent
1249 if (log.isTraceEnabled())
1250 log.trace("Could not unregister event listener "
1251 + eventListener);
1252 }
1253 }
1254
1255 /** Quietly unregisters an {@link EventListener} from this workspace */
1256 public static void unregisterQuietly(Workspace workspace,
1257 EventListener eventListener) {
1258 if (eventListener == null)
1259 return;
1260 try {
1261 workspace.getObservationManager()
1262 .removeEventListener(eventListener);
1263 } catch (RepositoryException e) {
1264 // silent
1265 if (log.isTraceEnabled())
1266 log.trace("Could not unregister event listener "
1267 + eventListener);
1268 }
1269 }
1270
1271 /**
1272 * If this node is has the {@link NodeType#MIX_LAST_MODIFIED} mixin, it
1273 * updates the {@link Property#JCR_LAST_MODIFIED} property with the current
1274 * time and the {@link Property#JCR_LAST_MODIFIED_BY} property with the
1275 * underlying session user id. In Jackrabbit 2.x, <a
1276 * href="https://issues.apache.org/jira/browse/JCR-2233">these properties
1277 * are not automatically updated</a>, hence the need for manual update. The
1278 * session is not saved.
1279 */
1280 public static void updateLastModified(Node node) {
1281 try {
1282 if (!node.isNodeType(NodeType.MIX_LAST_MODIFIED))
1283 node.addMixin(NodeType.MIX_LAST_MODIFIED);
1284 node.setProperty(Property.JCR_LAST_MODIFIED,
1285 new GregorianCalendar());
1286 node.setProperty(Property.JCR_LAST_MODIFIED_BY, node.getSession()
1287 .getUserID());
1288 } catch (RepositoryException e) {
1289 throw new ArgeoException("Cannot update last modified on " + node,
1290 e);
1291 }
1292 }
1293
1294 /** Update lastModified recursively until this parent. */
1295 public static void updateLastModifiedAndParents(Node node, String untilPath) {
1296 try {
1297 if (!node.getPath().startsWith(untilPath))
1298 throw new ArgeoException(node + " is not under " + untilPath);
1299 updateLastModified(node);
1300 if (!node.getPath().equals(untilPath))
1301 updateLastModifiedAndParents(node.getParent(), untilPath);
1302 } catch (RepositoryException e) {
1303 throw new ArgeoException("Cannot update lastModified from " + node
1304 + " until " + untilPath, e);
1305 }
1306 }
1307
1308 /**
1309 * Returns a String representing the short version (see <a
1310 * href="http://jackrabbit.apache.org/node-type-notation.html"> Node type
1311 * Notation </a> attributes grammar) of the main business attributes of this
1312 * property definition
1313 *
1314 * @param prop
1315 */
1316 public static String getPropertyDefinitionAsString(Property prop) {
1317 StringBuffer sbuf = new StringBuffer();
1318 try {
1319 if (prop.getDefinition().isAutoCreated())
1320 sbuf.append("a");
1321 if (prop.getDefinition().isMandatory())
1322 sbuf.append("m");
1323 if (prop.getDefinition().isProtected())
1324 sbuf.append("p");
1325 if (prop.getDefinition().isMultiple())
1326 sbuf.append("*");
1327 } catch (RepositoryException re) {
1328 throw new ArgeoException(
1329 "unexpected error while getting property definition as String",
1330 re);
1331 }
1332 return sbuf.toString();
1333 }
1334
1335 /**
1336 * Estimate the sub tree size from current node. Computation is based on the
1337 * Jcr {@link Property.getLength()} method. Note : it is not the exact size
1338 * used on the disk by the current part of the JCR Tree.
1339 */
1340
1341 public static long getNodeApproxSize(Node node) {
1342 long curNodeSize = 0;
1343 try {
1344 PropertyIterator pi = node.getProperties();
1345 while (pi.hasNext()) {
1346 Property prop = pi.nextProperty();
1347 if (prop.isMultiple()) {
1348 int nb = prop.getLengths().length;
1349 for (int i = 0; i < nb; i++) {
1350 curNodeSize += (prop.getLengths()[i] > 0 ? prop
1351 .getLengths()[i] : 0);
1352 }
1353 } else
1354 curNodeSize += (prop.getLength() > 0 ? prop.getLength() : 0);
1355 }
1356
1357 NodeIterator ni = node.getNodes();
1358 while (ni.hasNext())
1359 curNodeSize += getNodeApproxSize(ni.nextNode());
1360 return curNodeSize;
1361 } catch (RepositoryException re) {
1362 throw new ArgeoException(
1363 "Unexpected error while recursively determining node size.",
1364 re);
1365 }
1366 }
1367
1368 /*
1369 * SECURITY
1370 */
1371
1372 /**
1373 * Convenience method for adding a single privilege to a principal (user or
1374 * role), typically jcr:all
1375 */
1376 public static void addPrivilege(Session session, String path,
1377 String principal, String privilege) throws RepositoryException {
1378 List<Privilege> privileges = new ArrayList<Privilege>();
1379 privileges.add(session.getAccessControlManager().privilegeFromName(
1380 privilege));
1381 addPrivileges(session, path, new SimplePrincipal(principal), privileges);
1382 }
1383
1384 /**
1385 * Add privileges on a path to a {@link Principal}. The path must already
1386 * exist.
1387 */
1388 public static void addPrivileges(Session session, String path,
1389 Principal principal, List<Privilege> privs)
1390 throws RepositoryException {
1391 AccessControlManager acm = session.getAccessControlManager();
1392 // search for an access control list
1393 AccessControlList acl = null;
1394 AccessControlPolicyIterator policyIterator = acm
1395 .getApplicablePolicies(path);
1396 if (policyIterator.hasNext()) {
1397 while (policyIterator.hasNext()) {
1398 AccessControlPolicy acp = policyIterator
1399 .nextAccessControlPolicy();
1400 if (acp instanceof AccessControlList)
1401 acl = ((AccessControlList) acp);
1402 }
1403 } else {
1404 AccessControlPolicy[] existingPolicies = acm.getPolicies(path);
1405 for (AccessControlPolicy acp : existingPolicies) {
1406 if (acp instanceof AccessControlList)
1407 acl = ((AccessControlList) acp);
1408 }
1409 }
1410
1411 if (acl != null) {
1412 acl.addAccessControlEntry(principal,
1413 privs.toArray(new Privilege[privs.size()]));
1414 acm.setPolicy(path, acl);
1415 if (log.isDebugEnabled())
1416 log.debug("Added privileges " + privs + " to " + principal
1417 + " on " + path);
1418 } else {
1419 throw new ArgeoException("Don't know how to apply privileges "
1420 + privs + " to " + principal + " on " + path);
1421 }
1422 }
1423
1424 }