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