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