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