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