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