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