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