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