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