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