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