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