3 import java
.io
.ByteArrayInputStream
;
4 import java
.io
.ByteArrayOutputStream
;
6 import java
.io
.FileInputStream
;
7 import java
.io
.IOException
;
8 import java
.io
.InputStream
;
9 import java
.io
.OutputStream
;
10 import java
.io
.PipedInputStream
;
11 import java
.io
.PipedOutputStream
;
12 import java
.net
.MalformedURLException
;
14 import java
.nio
.file
.Files
;
15 import java
.nio
.file
.Path
;
16 import java
.security
.MessageDigest
;
17 import java
.security
.NoSuchAlgorithmException
;
18 import java
.security
.Principal
;
19 import java
.text
.DateFormat
;
20 import java
.text
.ParseException
;
21 import java
.time
.Instant
;
22 import java
.util
.ArrayList
;
23 import java
.util
.Calendar
;
24 import java
.util
.Collections
;
25 import java
.util
.Date
;
26 import java
.util
.GregorianCalendar
;
27 import java
.util
.Iterator
;
28 import java
.util
.List
;
30 import java
.util
.TreeMap
;
32 import javax
.jcr
.Binary
;
33 import javax
.jcr
.Credentials
;
34 import javax
.jcr
.ImportUUIDBehavior
;
35 import javax
.jcr
.NamespaceRegistry
;
36 import javax
.jcr
.NoSuchWorkspaceException
;
37 import javax
.jcr
.Node
;
38 import javax
.jcr
.NodeIterator
;
39 import javax
.jcr
.Property
;
40 import javax
.jcr
.PropertyIterator
;
41 import javax
.jcr
.PropertyType
;
42 import javax
.jcr
.Repository
;
43 import javax
.jcr
.RepositoryException
;
44 import javax
.jcr
.Session
;
45 import javax
.jcr
.Value
;
46 import javax
.jcr
.Workspace
;
47 import javax
.jcr
.nodetype
.NoSuchNodeTypeException
;
48 import javax
.jcr
.nodetype
.NodeType
;
49 import javax
.jcr
.observation
.EventListener
;
50 import javax
.jcr
.query
.Query
;
51 import javax
.jcr
.query
.QueryResult
;
52 import javax
.jcr
.security
.AccessControlEntry
;
53 import javax
.jcr
.security
.AccessControlList
;
54 import javax
.jcr
.security
.AccessControlManager
;
55 import javax
.jcr
.security
.AccessControlPolicy
;
56 import javax
.jcr
.security
.AccessControlPolicyIterator
;
57 import javax
.jcr
.security
.Privilege
;
59 import org
.apache
.commons
.io
.IOUtils
;
61 /** Utility methods to simplify common JCR operations. */
62 public class JcrUtils
{
64 // final private static Log log = LogFactory.getLog(JcrUtils.class);
67 * Not complete yet. See
68 * http://www.day.com/specs/jcr/2.0/3_Repository_Model.html#3.2.2%20Local
71 public final static char[] INVALID_NAME_CHARACTERS
= { '/', ':', '[', ']', '|', '*', /* invalid for XML: */ '<',
74 /** Prevents instantiation */
79 * Queries one single node.
81 * @return one single node or null if none was found
82 * @throws JcrException if more than one node was found
84 public static Node
querySingleNode(Query query
) {
85 NodeIterator nodeIterator
;
87 QueryResult queryResult
= query
.execute();
88 nodeIterator
= queryResult
.getNodes();
89 } catch (RepositoryException e
) {
90 throw new JcrException("Cannot execute query " + query
, e
);
93 if (nodeIterator
.hasNext())
94 node
= nodeIterator
.nextNode();
98 if (nodeIterator
.hasNext())
99 throw new IllegalArgumentException("Query returned more than one node.");
103 /** Retrieves the node name from the provided path */
104 public static String
nodeNameFromPath(String path
) {
105 if (path
.equals("/"))
107 if (path
.charAt(0) != '/')
108 throw new IllegalArgumentException("Path " + path
+ " must start with a '/'");
110 if (pathT
.charAt(pathT
.length() - 1) == '/')
111 pathT
= pathT
.substring(0, pathT
.length() - 2);
113 int index
= pathT
.lastIndexOf('/');
114 return pathT
.substring(index
+ 1);
117 /** Retrieves the parent path of the provided path */
118 public static String
parentPath(String path
) {
119 if (path
.equals("/"))
120 throw new IllegalArgumentException("Root path '/' has no parent path");
121 if (path
.charAt(0) != '/')
122 throw new IllegalArgumentException("Path " + path
+ " must start with a '/'");
124 if (pathT
.charAt(pathT
.length() - 1) == '/')
125 pathT
= pathT
.substring(0, pathT
.length() - 2);
127 int index
= pathT
.lastIndexOf('/');
128 return pathT
.substring(0, index
);
131 /** The provided data as a path ('/' at the end, not the beginning) */
132 public static String
dateAsPath(Calendar cal
) {
133 return dateAsPath(cal
, false);
137 * Creates a deep path based on a URL:
138 * http://subdomain.example.com/to/content?args becomes
139 * com/example/subdomain/to/content
141 public static String
urlAsPath(String url
) {
143 URL u
= new URL(url
);
144 StringBuffer path
= new StringBuffer(url
.length());
146 path
.append(hostAsPath(u
.getHost()));
147 // we don't put port since it may not always be there and may change
148 path
.append(u
.getPath());
149 return path
.toString();
150 } catch (MalformedURLException e
) {
151 throw new IllegalArgumentException("Cannot generate URL path for " + url
, e
);
155 /** Set the {@link NodeType#NT_ADDRESS} properties based on this URL. */
156 public static void urlToAddressProperties(Node node
, String url
) {
158 URL u
= new URL(url
);
159 node
.setProperty(Property
.JCR_PROTOCOL
, u
.getProtocol());
160 node
.setProperty(Property
.JCR_HOST
, u
.getHost());
161 node
.setProperty(Property
.JCR_PORT
, Integer
.toString(u
.getPort()));
162 node
.setProperty(Property
.JCR_PATH
, normalizePath(u
.getPath()));
163 } catch (RepositoryException e
) {
164 throw new JcrException("Cannot set URL " + url
+ " as nt:address properties", e
);
165 } catch (MalformedURLException e
) {
166 throw new IllegalArgumentException("Cannot set URL " + url
+ " as nt:address properties", e
);
170 /** Build URL based on the {@link NodeType#NT_ADDRESS} properties. */
171 public static String
urlFromAddressProperties(Node node
) {
173 URL u
= new URL(node
.getProperty(Property
.JCR_PROTOCOL
).getString(),
174 node
.getProperty(Property
.JCR_HOST
).getString(),
175 (int) node
.getProperty(Property
.JCR_PORT
).getLong(),
176 node
.getProperty(Property
.JCR_PATH
).getString());
178 } catch (RepositoryException e
) {
179 throw new JcrException("Cannot get URL from nt:address properties of " + node
, e
);
180 } catch (MalformedURLException e
) {
181 throw new IllegalArgumentException("Cannot get URL from nt:address properties of " + node
, e
);
190 * Make sure that: starts with '/', do not end with '/', do not have '//'
192 public static String
normalizePath(String path
) {
193 List
<String
> tokens
= tokenize(path
);
194 StringBuffer buf
= new StringBuffer(path
.length());
195 for (String token
: tokens
) {
199 return buf
.toString();
203 * Creates a path from a FQDN, inverting the order of the component:
204 * www.argeo.org becomes org.argeo.www
206 public static String
hostAsPath(String host
) {
207 StringBuffer path
= new StringBuffer(host
.length());
208 String
[] hostTokens
= host
.split("\\.");
209 for (int i
= hostTokens
.length
- 1; i
>= 0; i
--) {
210 path
.append(hostTokens
[i
]);
214 return path
.toString();
218 * Creates a path from a UUID (e.g. 6ebda899-217d-4bf1-abe4-2839085c8f3c becomes
219 * 6ebda899-217d/4bf1/abe4/2839085c8f3c/). '/' at the end, not the beginning
221 public static String
uuidAsPath(String uuid
) {
222 StringBuffer path
= new StringBuffer(uuid
.length());
223 String
[] tokens
= uuid
.split("-");
224 for (int i
= 0; i
< tokens
.length
; i
++) {
225 path
.append(tokens
[i
]);
229 return path
.toString();
233 * The provided data as a path ('/' at the end, not the beginning)
235 * @param cal the date
236 * @param addHour whether to add hour as well
238 public static String
dateAsPath(Calendar cal
, Boolean addHour
) {
239 StringBuffer buf
= new StringBuffer(14);
241 buf
.append(cal
.get(Calendar
.YEAR
));
244 int month
= cal
.get(Calendar
.MONTH
) + 1;
251 int day
= cal
.get(Calendar
.DAY_OF_MONTH
);
259 int hour
= cal
.get(Calendar
.HOUR_OF_DAY
);
266 return buf
.toString();
270 /** Converts in one call a string into a gregorian calendar. */
271 public static Calendar
parseCalendar(DateFormat dateFormat
, String value
) {
273 Date date
= dateFormat
.parse(value
);
274 Calendar calendar
= new GregorianCalendar();
275 calendar
.setTime(date
);
277 } catch (ParseException e
) {
278 throw new IllegalArgumentException("Cannot parse " + value
+ " with date format " + dateFormat
, e
);
283 /** The last element of a path. */
284 public static String
lastPathElement(String path
) {
285 if (path
.charAt(path
.length() - 1) == '/')
286 throw new IllegalArgumentException("Path " + path
+ " cannot end with '/'");
287 int index
= path
.lastIndexOf('/');
290 return path
.substring(index
+ 1);
294 * Call {@link Node#getName()} without exceptions (useful in super
297 public static String
getNameQuietly(Node node
) {
299 return node
.getName();
300 } catch (RepositoryException e
) {
301 throw new JcrException("Cannot get name from " + node
, e
);
306 * Call {@link Node#getProperty(String)} without exceptions (useful in super
309 public static String
getStringPropertyQuietly(Node node
, String propertyName
) {
311 return node
.getProperty(propertyName
).getString();
312 } catch (RepositoryException e
) {
313 throw new JcrException("Cannot get name from " + node
, e
);
318 // * Routine that get the child with this name, adding it if it does not already
321 // public static Node getOrAdd(Node parent, String name, String primaryNodeType) throws RepositoryException {
322 // return parent.hasNode(name) ? parent.getNode(name) : parent.addNode(name, primaryNodeType);
326 * Routine that get the child with this name, adding it if it does not already
329 public static Node
getOrAdd(Node parent
, String name
, String primaryNodeType
, String
... mixinNodeTypes
)
330 throws RepositoryException
{
332 if (parent
.hasNode(name
)) {
333 node
= parent
.getNode(name
);
334 if (primaryNodeType
!= null && !node
.isNodeType(primaryNodeType
))
335 throw new IllegalArgumentException("Node " + node
+ " exists but is of primary node type "
336 + node
.getPrimaryNodeType().getName() + ", not " + primaryNodeType
);
337 for (String mixin
: mixinNodeTypes
) {
338 if (!node
.isNodeType(mixin
))
339 node
.addMixin(mixin
);
343 node
= primaryNodeType
!= null ? parent
.addNode(name
, primaryNodeType
) : parent
.addNode(name
);
344 for (String mixin
: mixinNodeTypes
) {
345 node
.addMixin(mixin
);
352 * Routine that get the child with this name, adding it if it does not already
355 public static Node
getOrAdd(Node parent
, String name
) throws RepositoryException
{
356 return parent
.hasNode(name
) ? parent
.getNode(name
) : parent
.addNode(name
);
359 /** Convert a {@link NodeIterator} to a list of {@link Node} */
360 public static List
<Node
> nodeIteratorToList(NodeIterator nodeIterator
) {
361 List
<Node
> nodes
= new ArrayList
<Node
>();
362 while (nodeIterator
.hasNext()) {
363 nodes
.add(nodeIterator
.nextNode());
373 * Concisely get the string value of a property or null if this node doesn't
376 public static String
get(Node node
, String propertyName
) {
378 if (!node
.hasProperty(propertyName
))
380 return node
.getProperty(propertyName
).getString();
381 } catch (RepositoryException e
) {
382 throw new JcrException("Cannot get property " + propertyName
+ " of " + node
, e
);
386 /** Concisely get the path of the given node. */
387 public static String
getPath(Node node
) {
389 return node
.getPath();
390 } catch (RepositoryException e
) {
391 throw new JcrException("Cannot get path of " + node
, e
);
395 /** Concisely get the boolean value of a property */
396 public static Boolean
check(Node node
, String propertyName
) {
398 return node
.getProperty(propertyName
).getBoolean();
399 } catch (RepositoryException e
) {
400 throw new JcrException("Cannot get property " + propertyName
+ " of " + node
, e
);
404 /** Concisely get the bytes array value of a property */
405 public static byte[] getBytes(Node node
, String propertyName
) {
407 return getBinaryAsBytes(node
.getProperty(propertyName
));
408 } catch (RepositoryException e
) {
409 throw new JcrException("Cannot get property " + propertyName
+ " of " + node
, e
);
418 * Create sub nodes relative to a parent node
420 public static Node
mkdirs(Node parentNode
, String relativePath
) {
421 return mkdirs(parentNode
, relativePath
, null, null);
425 * Create sub nodes relative to a parent node
427 * @param nodeType the type of the leaf node
429 public static Node
mkdirs(Node parentNode
, String relativePath
, String nodeType
) {
430 return mkdirs(parentNode
, relativePath
, nodeType
, null);
434 * Create sub nodes relative to a parent node
436 * @param nodeType the type of the leaf node
438 public static Node
mkdirs(Node parentNode
, String relativePath
, String nodeType
, String intermediaryNodeType
) {
439 List
<String
> tokens
= tokenize(relativePath
);
440 Node currParent
= parentNode
;
442 for (int i
= 0; i
< tokens
.size(); i
++) {
443 String name
= tokens
.get(i
);
444 if (currParent
.hasNode(name
)) {
445 currParent
= currParent
.getNode(name
);
447 if (i
!= (tokens
.size() - 1)) {// intermediary
448 currParent
= currParent
.addNode(name
, intermediaryNodeType
);
450 currParent
= currParent
.addNode(name
, nodeType
);
455 } catch (RepositoryException e
) {
456 throw new JcrException("Cannot mkdirs relative path " + relativePath
+ " from " + parentNode
, e
);
461 * Synchronized and save is performed, to avoid race conditions in initializers
462 * leading to duplicate nodes.
464 public synchronized static Node
mkdirsSafe(Session session
, String path
, String type
) {
466 if (session
.hasPendingChanges())
467 throw new IllegalStateException("Session has pending changes, save them first.");
468 Node node
= mkdirs(session
, path
, type
);
471 } catch (RepositoryException e
) {
472 discardQuietly(session
);
473 throw new JcrException("Cannot safely make directories", e
);
477 public synchronized static Node
mkdirsSafe(Session session
, String path
) {
478 return mkdirsSafe(session
, path
, null);
481 /** Creates the nodes making path, if they don't exist. */
482 public static Node
mkdirs(Session session
, String path
) {
483 return mkdirs(session
, path
, null, null, false);
487 * @param type the type of the leaf node
489 public static Node
mkdirs(Session session
, String path
, String type
) {
490 return mkdirs(session
, path
, type
, null, false);
494 * Creates the nodes making path, if they don't exist. This is up to the caller
495 * to save the session. Use with caution since it can create duplicate nodes if
496 * used concurrently. Requires read access to the root node of the workspace.
498 public static Node
mkdirs(Session session
, String path
, String type
, String intermediaryNodeType
,
499 Boolean versioning
) {
501 if (path
.equals("/"))
502 return session
.getRootNode();
504 if (session
.itemExists(path
)) {
505 Node node
= session
.getNode(path
);
507 if (type
!= null && !node
.isNodeType(type
) && !node
.getPath().equals("/"))
508 throw new IllegalArgumentException("Node " + node
+ " exists but is of type "
509 + node
.getPrimaryNodeType().getName() + " not of type " + type
);
510 // TODO: check versioning
514 // StringBuffer current = new StringBuffer("/");
515 // Node currentNode = session.getRootNode();
517 Node currentNode
= findClosestExistingParent(session
, path
);
518 String closestExistingParentPath
= currentNode
.getPath();
519 StringBuffer current
= new StringBuffer(closestExistingParentPath
);
520 if (!closestExistingParentPath
.endsWith("/"))
522 Iterator
<String
> it
= tokenize(path
.substring(closestExistingParentPath
.length())).iterator();
523 while (it
.hasNext()) {
524 String part
= it
.next();
525 current
.append(part
).append('/');
526 if (!session
.itemExists(current
.toString())) {
527 if (!it
.hasNext() && type
!= null)
528 currentNode
= currentNode
.addNode(part
, type
);
529 else if (it
.hasNext() && intermediaryNodeType
!= null)
530 currentNode
= currentNode
.addNode(part
, intermediaryNodeType
);
532 currentNode
= currentNode
.addNode(part
);
534 currentNode
.addMixin(NodeType
.MIX_VERSIONABLE
);
535 // if (log.isTraceEnabled())
536 // log.debug("Added folder " + part + " as " + current);
538 currentNode
= (Node
) session
.getItem(current
.toString());
542 } catch (RepositoryException e
) {
543 discardQuietly(session
);
544 throw new JcrException("Cannot mkdirs " + path
, e
);
549 private static Node
findClosestExistingParent(Session session
, String path
) throws RepositoryException
{
550 int idx
= path
.lastIndexOf('/');
552 return session
.getRootNode();
553 String parentPath
= path
.substring(0, idx
);
554 if (session
.itemExists(parentPath
))
555 return session
.getNode(parentPath
);
557 return findClosestExistingParent(session
, parentPath
);
560 /** Convert a path to the list of its tokens */
561 public static List
<String
> tokenize(String path
) {
562 List
<String
> tokens
= new ArrayList
<String
>();
563 boolean optimized
= false;
565 String
[] rawTokens
= path
.split("/");
566 for (String token
: rawTokens
) {
567 if (!token
.equals(""))
571 StringBuffer curr
= new StringBuffer();
572 char[] arr
= path
.toCharArray();
573 chars
: for (int i
= 0; i
< arr
.length
; i
++) {
576 if (i
== 0 || (i
== arr
.length
- 1))
578 if (curr
.length() > 0) {
579 tokens
.add(curr
.toString());
580 curr
= new StringBuffer();
585 if (curr
.length() > 0) {
586 tokens
.add(curr
.toString());
587 curr
= new StringBuffer();
590 return Collections
.unmodifiableList(tokens
);
594 // * use {@link #mkdirs(Session, String, String, String, Boolean)} instead.
599 // public static Node mkdirs(Session session, String path, String type,
600 // Boolean versioning) {
601 // return mkdirs(session, path, type, type, false);
605 * Safe and repository implementation independent registration of a namespace.
607 public static void registerNamespaceSafely(Session session
, String prefix
, String uri
) {
609 registerNamespaceSafely(session
.getWorkspace().getNamespaceRegistry(), prefix
, uri
);
610 } catch (RepositoryException e
) {
611 throw new JcrException("Cannot find namespace registry", e
);
616 * Safe and repository implementation independent registration of a namespace.
618 public static void registerNamespaceSafely(NamespaceRegistry nr
, String prefix
, String uri
) {
620 String
[] prefixes
= nr
.getPrefixes();
621 for (String pref
: prefixes
)
622 if (pref
.equals(prefix
)) {
623 String registeredUri
= nr
.getURI(pref
);
624 if (!registeredUri
.equals(uri
))
625 throw new IllegalArgumentException("Prefix " + pref
+ " already registered for URI "
626 + registeredUri
+ " which is different from provided URI " + uri
);
630 nr
.registerNamespace(prefix
, uri
);
631 } catch (RepositoryException e
) {
632 throw new JcrException("Cannot register namespace " + uri
+ " under prefix " + prefix
, e
);
636 // /** Recursively outputs the contents of the given node. */
637 // public static void debug(Node node) {
641 // /** Recursively outputs the contents of the given node. */
642 // public static void debug(Node node, Log log) {
644 // // First output the node path
645 // log.debug(node.getPath());
646 // // Skip the virtual (and large!) jcr:system subtree
647 // if (node.getName().equals("jcr:system")) {
651 // // Then the children nodes (recursive)
652 // NodeIterator it = node.getNodes();
653 // while (it.hasNext()) {
654 // Node childNode = it.nextNode();
655 // debug(childNode, log);
658 // // Then output the properties
659 // PropertyIterator properties = node.getProperties();
660 // // log.debug("Property are : ");
662 // properties: while (properties.hasNext()) {
663 // Property property = properties.nextProperty();
664 // if (property.getType() == PropertyType.BINARY)
665 // continue properties;// skip
666 // if (property.getDefinition().isMultiple()) {
667 // // A multi-valued property, print all values
668 // Value[] values = property.getValues();
669 // for (int i = 0; i < values.length; i++) {
670 // log.debug(property.getPath() + "=" + values[i].getString());
673 // // A single-valued property
674 // log.debug(property.getPath() + "=" + property.getString());
677 // } catch (Exception e) {
678 // log.error("Could not debug " + node, e);
683 // /** Logs the effective access control policies */
684 // public static void logEffectiveAccessPolicies(Node node) {
686 // logEffectiveAccessPolicies(node.getSession(), node.getPath());
687 // } catch (RepositoryException e) {
688 // log.error("Cannot log effective access policies of " + node, e);
692 // /** Logs the effective access control policies */
693 // public static void logEffectiveAccessPolicies(Session session, String path) {
694 // if (!log.isDebugEnabled())
698 // AccessControlPolicy[] effectivePolicies = session.getAccessControlManager().getEffectivePolicies(path);
699 // if (effectivePolicies.length > 0) {
700 // for (AccessControlPolicy policy : effectivePolicies) {
701 // if (policy instanceof AccessControlList) {
702 // AccessControlList acl = (AccessControlList) policy;
703 // log.debug("Access control list for " + path + "\n" + accessControlListSummary(acl));
707 // log.debug("No effective access control policy for " + path);
709 // } catch (RepositoryException e) {
710 // log.error("Cannot log effective access policies of " + path, e);
714 /** Returns a human-readable summary of this access control list. */
715 public static String
accessControlListSummary(AccessControlList acl
) {
716 StringBuffer buf
= new StringBuffer("");
718 for (AccessControlEntry ace
: acl
.getAccessControlEntries()) {
719 buf
.append('\t').append(ace
.getPrincipal().getName()).append('\n');
720 for (Privilege priv
: ace
.getPrivileges())
721 buf
.append("\t\t").append(priv
.getName()).append('\n');
723 return buf
.toString();
724 } catch (RepositoryException e
) {
725 throw new JcrException("Cannot write summary of " + acl
, e
);
729 /** Copy the whole workspace via a system view XML. */
730 public static void copyWorkspaceXml(Session fromSession
, Session toSession
) {
731 Workspace fromWorkspace
= fromSession
.getWorkspace();
732 Workspace toWorkspace
= toSession
.getWorkspace();
733 String errorMsg
= "Cannot copy workspace " + fromWorkspace
+ " to " + toWorkspace
+ " via XML.";
735 try (PipedInputStream in
= new PipedInputStream(1024 * 1024);) {
737 try (PipedOutputStream out
= new PipedOutputStream(in
)) {
738 fromSession
.exportSystemView("/", out
, false, false);
740 } catch (IOException e
) {
741 throw new RuntimeException(errorMsg
, e
);
742 } catch (RepositoryException e
) {
743 throw new JcrException(errorMsg
, e
);
745 }, "Copy workspace" + fromWorkspace
+ " to " + toWorkspace
).start();
747 toSession
.importXML("/", in
, ImportUUIDBehavior
.IMPORT_UUID_COLLISION_REPLACE_EXISTING
);
749 } catch (IOException e
) {
750 throw new RuntimeException(errorMsg
, e
);
751 } catch (RepositoryException e
) {
752 throw new JcrException(errorMsg
, e
);
757 * Copies recursively the content of a node to another one. Do NOT copy the
758 * property values of {@link NodeType#MIX_CREATED} and
759 * {@link NodeType#MIX_LAST_MODIFIED}, but update the
760 * {@link Property#JCR_LAST_MODIFIED} and {@link Property#JCR_LAST_MODIFIED_BY}
761 * properties if the target node has the {@link NodeType#MIX_LAST_MODIFIED}
764 public static void copy(Node fromNode
, Node toNode
) {
766 if (toNode
.getDefinition().isProtected())
770 for (NodeType mixinType
: fromNode
.getMixinNodeTypes()) {
772 toNode
.addMixin(mixinType
.getName());
773 } catch (NoSuchNodeTypeException e
) {
774 // ignore unknown mixins
779 // process properties
780 PropertyIterator pit
= fromNode
.getProperties();
781 properties
: while (pit
.hasNext()) {
782 Property fromProperty
= pit
.nextProperty();
783 String propertyName
= fromProperty
.getName();
784 if (toNode
.hasProperty(propertyName
) && toNode
.getProperty(propertyName
).getDefinition().isProtected())
787 if (fromProperty
.getDefinition().isProtected())
790 if (propertyName
.equals("jcr:created") || propertyName
.equals("jcr:createdBy")
791 || propertyName
.equals("jcr:lastModified") || propertyName
.equals("jcr:lastModifiedBy"))
794 if (fromProperty
.isMultiple()) {
795 toNode
.setProperty(propertyName
, fromProperty
.getValues());
797 toNode
.setProperty(propertyName
, fromProperty
.getValue());
801 // update jcr:lastModified and jcr:lastModifiedBy in toNode in case
802 // they existed, before adding the mixins
803 updateLastModified(toNode
, true);
805 // process children nodes
806 NodeIterator nit
= fromNode
.getNodes();
807 while (nit
.hasNext()) {
808 Node fromChild
= nit
.nextNode();
809 Integer index
= fromChild
.getIndex();
810 String nodeRelPath
= fromChild
.getName() + "[" + index
+ "]";
812 if (toNode
.hasNode(nodeRelPath
))
813 toChild
= toNode
.getNode(nodeRelPath
);
816 toChild
= toNode
.addNode(fromChild
.getName(), fromChild
.getPrimaryNodeType().getName());
817 } catch (NoSuchNodeTypeException e
) {
818 // ignore unknown primary types
823 copy(fromChild
, toChild
);
825 } catch (RepositoryException e
) {
826 throw new JcrException("Cannot copy " + fromNode
+ " to " + toNode
, e
);
831 * Check whether all first-level properties (except jcr:* properties) are equal.
832 * Skip jcr:* properties
834 public static Boolean
allPropertiesEquals(Node reference
, Node observed
, Boolean onlyCommonProperties
) {
836 PropertyIterator pit
= reference
.getProperties();
837 props
: while (pit
.hasNext()) {
838 Property propReference
= pit
.nextProperty();
839 String propName
= propReference
.getName();
840 if (propName
.startsWith("jcr:"))
843 if (!observed
.hasProperty(propName
))
844 if (onlyCommonProperties
)
848 // TODO: deal with multiple property values?
849 if (!observed
.getProperty(propName
).getValue().equals(propReference
.getValue()))
853 } catch (RepositoryException e
) {
854 throw new JcrException("Cannot check all properties equals of " + reference
+ " and " + observed
, e
);
858 public static Map
<String
, PropertyDiff
> diffProperties(Node reference
, Node observed
) {
859 Map
<String
, PropertyDiff
> diffs
= new TreeMap
<String
, PropertyDiff
>();
860 diffPropertiesLevel(diffs
, null, reference
, observed
);
865 * Compare the properties of two nodes. Recursivity to child nodes is not yet
866 * supported. Skip jcr:* properties.
868 static void diffPropertiesLevel(Map
<String
, PropertyDiff
> diffs
, String baseRelPath
, Node reference
,
871 // check removed and modified
872 PropertyIterator pit
= reference
.getProperties();
873 props
: while (pit
.hasNext()) {
874 Property p
= pit
.nextProperty();
875 String name
= p
.getName();
876 if (name
.startsWith("jcr:"))
879 if (!observed
.hasProperty(name
)) {
880 String relPath
= propertyRelPath(baseRelPath
, name
);
881 PropertyDiff pDiff
= new PropertyDiff(PropertyDiff
.REMOVED
, relPath
, p
.getValue(), null);
882 diffs
.put(relPath
, pDiff
);
884 if (p
.isMultiple()) {
885 // FIXME implement multiple
887 Value referenceValue
= p
.getValue();
888 Value newValue
= observed
.getProperty(name
).getValue();
889 if (!referenceValue
.equals(newValue
)) {
890 String relPath
= propertyRelPath(baseRelPath
, name
);
891 PropertyDiff pDiff
= new PropertyDiff(PropertyDiff
.MODIFIED
, relPath
, referenceValue
,
893 diffs
.put(relPath
, pDiff
);
899 pit
= observed
.getProperties();
900 props
: while (pit
.hasNext()) {
901 Property p
= pit
.nextProperty();
902 String name
= p
.getName();
903 if (name
.startsWith("jcr:"))
905 if (!reference
.hasProperty(name
)) {
906 if (p
.isMultiple()) {
907 // FIXME implement multiple
909 String relPath
= propertyRelPath(baseRelPath
, name
);
910 PropertyDiff pDiff
= new PropertyDiff(PropertyDiff
.ADDED
, relPath
, null, p
.getValue());
911 diffs
.put(relPath
, pDiff
);
915 } catch (RepositoryException e
) {
916 throw new JcrException("Cannot diff " + reference
+ " and " + observed
, e
);
921 * Compare only a restricted list of properties of two nodes. No recursivity.
924 public static Map
<String
, PropertyDiff
> diffProperties(Node reference
, Node observed
, List
<String
> properties
) {
925 Map
<String
, PropertyDiff
> diffs
= new TreeMap
<String
, PropertyDiff
>();
927 Iterator
<String
> pit
= properties
.iterator();
929 props
: while (pit
.hasNext()) {
930 String name
= pit
.next();
931 if (!reference
.hasProperty(name
)) {
932 if (!observed
.hasProperty(name
))
934 Value val
= observed
.getProperty(name
).getValue();
936 // empty String but not null
937 if ("".equals(val
.getString()))
939 } catch (Exception e
) {
940 // not parseable as String, silent
942 PropertyDiff pDiff
= new PropertyDiff(PropertyDiff
.ADDED
, name
, null, val
);
943 diffs
.put(name
, pDiff
);
944 } else if (!observed
.hasProperty(name
)) {
945 PropertyDiff pDiff
= new PropertyDiff(PropertyDiff
.REMOVED
, name
,
946 reference
.getProperty(name
).getValue(), null);
947 diffs
.put(name
, pDiff
);
949 Value referenceValue
= reference
.getProperty(name
).getValue();
950 Value newValue
= observed
.getProperty(name
).getValue();
951 if (!referenceValue
.equals(newValue
)) {
952 PropertyDiff pDiff
= new PropertyDiff(PropertyDiff
.MODIFIED
, name
, referenceValue
, newValue
);
953 diffs
.put(name
, pDiff
);
957 } catch (RepositoryException e
) {
958 throw new JcrException("Cannot diff " + reference
+ " and " + observed
, e
);
963 /** Builds a property relPath to be used in the diff. */
964 private static String
propertyRelPath(String baseRelPath
, String propertyName
) {
965 if (baseRelPath
== null)
968 return baseRelPath
+ '/' + propertyName
;
972 * Normalizes a name so that it can be stored in contexts not supporting names
973 * with ':' (typically databases). Replaces ':' by '_'.
975 public static String
normalize(String name
) {
976 return name
.replace(':', '_');
980 * Replaces characters which are invalid in a JCR name by '_'. Currently not
983 * @see JcrUtils#INVALID_NAME_CHARACTERS
985 public static String
replaceInvalidChars(String name
) {
986 return replaceInvalidChars(name
, '_');
990 * Replaces characters which are invalid in a JCR name. Currently not
993 * @see JcrUtils#INVALID_NAME_CHARACTERS
995 public static String
replaceInvalidChars(String name
, char replacement
) {
996 boolean modified
= false;
997 char[] arr
= name
.toCharArray();
998 for (int i
= 0; i
< arr
.length
; i
++) {
1000 invalid
: for (char invalid
: INVALID_NAME_CHARACTERS
) {
1002 arr
[i
] = replacement
;
1009 return new String(arr
);
1011 // do not create new object if unnecessary
1016 // * Removes forbidden characters from a path, replacing them with '_'
1018 // * @deprecated use {@link #replaceInvalidChars(String)} instead
1020 // public static String removeForbiddenCharacters(String str) {
1021 // return str.replace('[', '_').replace(']', '_').replace('/', '_').replace('*',
1026 /** Cleanly disposes a {@link Binary} even if it is null. */
1027 public static void closeQuietly(Binary binary
) {
1033 /** Retrieve a {@link Binary} as a byte array */
1034 public static byte[] getBinaryAsBytes(Property property
) {
1035 try (ByteArrayOutputStream out
= new ByteArrayOutputStream();
1036 Bin binary
= new Bin(property
);
1037 InputStream in
= binary
.getStream()) {
1038 IOUtils
.copy(in
, out
);
1039 return out
.toByteArray();
1040 } catch (RepositoryException e
) {
1041 throw new JcrException("Cannot read binary " + property
+ " as bytes", e
);
1042 } catch (IOException e
) {
1043 throw new RuntimeException("Cannot read binary " + property
+ " as bytes", e
);
1047 /** Writes a {@link Binary} from a byte array */
1048 public static void setBinaryAsBytes(Node node
, String property
, byte[] bytes
) {
1049 Binary binary
= null;
1050 try (InputStream in
= new ByteArrayInputStream(bytes
)) {
1051 binary
= node
.getSession().getValueFactory().createBinary(in
);
1052 node
.setProperty(property
, binary
);
1053 } catch (RepositoryException e
) {
1054 throw new JcrException("Cannot set binary " + property
+ " as bytes", e
);
1055 } catch (IOException e
) {
1056 throw new RuntimeException("Cannot set binary " + property
+ " as bytes", e
);
1058 closeQuietly(binary
);
1062 /** Writes a {@link Binary} from a byte array */
1063 public static void setBinaryAsBytes(Property prop
, byte[] bytes
) {
1064 Binary binary
= null;
1065 try (InputStream in
= new ByteArrayInputStream(bytes
)) {
1066 binary
= prop
.getSession().getValueFactory().createBinary(in
);
1067 prop
.setValue(binary
);
1068 } catch (RepositoryException e
) {
1069 throw new JcrException("Cannot set binary " + prop
+ " as bytes", e
);
1070 } catch (IOException e
) {
1071 throw new RuntimeException("Cannot set binary " + prop
+ " as bytes", e
);
1073 closeQuietly(binary
);
1078 * Creates depth from a string (typically a username) by adding levels based on
1079 * its first characters: "aBcD",2 becomes a/aB
1081 public static String
firstCharsToPath(String str
, Integer nbrOfChars
) {
1082 if (str
.length() < nbrOfChars
)
1083 throw new IllegalArgumentException("String " + str
+ " length must be greater or equal than " + nbrOfChars
);
1084 StringBuffer path
= new StringBuffer("");
1085 StringBuffer curr
= new StringBuffer("");
1086 for (int i
= 0; i
< nbrOfChars
; i
++) {
1087 curr
.append(str
.charAt(i
));
1089 if (i
< nbrOfChars
- 1)
1092 return path
.toString();
1096 * Discards the current changes in the session attached to this node. To be used
1097 * typically in a catch block.
1099 * @see #discardQuietly(Session)
1101 public static void discardUnderlyingSessionQuietly(Node node
) {
1103 discardQuietly(node
.getSession());
1104 } catch (RepositoryException e
) {
1110 * Discards the current changes in a session by calling
1111 * {@link Session#refresh(boolean)} with <code>false</code>, only logging
1112 * potential errors when doing so. To be used typically in a catch block.
1114 public static void discardQuietly(Session session
) {
1116 if (session
!= null)
1117 session
.refresh(false);
1118 } catch (RepositoryException e
) {
1124 * Login to a workspace with implicit credentials, creates the workspace with
1125 * these credentials if it does not already exist.
1127 public static Session
loginOrCreateWorkspace(Repository repository
, String workspaceName
)
1128 throws RepositoryException
{
1129 return loginOrCreateWorkspace(repository
, workspaceName
, null);
1133 * Login to a workspace with implicit credentials, creates the workspace with
1134 * these credentials if it does not already exist.
1136 public static Session
loginOrCreateWorkspace(Repository repository
, String workspaceName
, Credentials credentials
)
1137 throws RepositoryException
{
1138 Session workspaceSession
= null;
1139 Session defaultSession
= null;
1142 workspaceSession
= repository
.login(credentials
, workspaceName
);
1143 } catch (NoSuchWorkspaceException e
) {
1144 // try to create workspace
1145 defaultSession
= repository
.login(credentials
);
1146 defaultSession
.getWorkspace().createWorkspace(workspaceName
);
1147 workspaceSession
= repository
.login(credentials
, workspaceName
);
1149 return workspaceSession
;
1151 logoutQuietly(defaultSession
);
1156 * Logs out the session, not throwing any exception, even if it is null.
1157 * {@link Jcr#logout(Session)} should rather be used.
1159 public static void logoutQuietly(Session session
) {
1160 Jcr
.logout(session
);
1162 // if (session != null)
1163 // if (session.isLive())
1164 // session.logout();
1165 // } catch (Exception e) {
1171 * Convenient method to add a listener. uuids passed as null, deep=true,
1172 * local=true, only one node type
1174 public static void addListener(Session session
, EventListener listener
, int eventTypes
, String basePath
,
1177 session
.getWorkspace().getObservationManager().addEventListener(listener
, eventTypes
, basePath
, true, null,
1178 nodeType
== null ?
null : new String
[] { nodeType
}, true);
1179 } catch (RepositoryException e
) {
1180 throw new JcrException("Cannot add JCR listener " + listener
+ " to session " + session
, e
);
1184 /** Removes a listener without throwing exception */
1185 public static void removeListenerQuietly(Session session
, EventListener listener
) {
1186 if (session
== null || !session
.isLive())
1189 session
.getWorkspace().getObservationManager().removeEventListener(listener
);
1190 } catch (RepositoryException e
) {
1196 * Quietly unregisters an {@link EventListener} from the udnerlying workspace of
1199 public static void unregisterQuietly(Node node
, EventListener eventListener
) {
1201 unregisterQuietly(node
.getSession().getWorkspace(), eventListener
);
1202 } catch (RepositoryException e
) {
1207 /** Quietly unregisters an {@link EventListener} from this workspace */
1208 public static void unregisterQuietly(Workspace workspace
, EventListener eventListener
) {
1209 if (eventListener
== null)
1212 workspace
.getObservationManager().removeEventListener(eventListener
);
1213 } catch (RepositoryException e
) {
1219 * Checks whether {@link Property#JCR_LAST_MODIFIED} or (afterwards)
1220 * {@link Property#JCR_CREATED} are set and returns it as an {@link Instant}.
1222 public static Instant
getModified(Node node
) {
1223 Calendar calendar
= null;
1225 if (node
.hasProperty(Property
.JCR_LAST_MODIFIED
))
1226 calendar
= node
.getProperty(Property
.JCR_LAST_MODIFIED
).getDate();
1227 else if (node
.hasProperty(Property
.JCR_CREATED
))
1228 calendar
= node
.getProperty(Property
.JCR_CREATED
).getDate();
1230 throw new IllegalArgumentException("No modification time found in " + node
);
1231 return calendar
.toInstant();
1232 } catch (RepositoryException e
) {
1233 throw new JcrException("Cannot get modification time for " + node
, e
);
1239 * Get {@link Property#JCR_CREATED} as an {@link Instant}, if it is set.
1241 public static Instant
getCreated(Node node
) {
1242 Calendar calendar
= null;
1244 if (node
.hasProperty(Property
.JCR_CREATED
))
1245 calendar
= node
.getProperty(Property
.JCR_CREATED
).getDate();
1247 throw new IllegalArgumentException("No created time found in " + node
);
1248 return calendar
.toInstant();
1249 } catch (RepositoryException e
) {
1250 throw new JcrException("Cannot get created time for " + node
, e
);
1256 * Updates the {@link Property#JCR_LAST_MODIFIED} property with the current time
1257 * and the {@link Property#JCR_LAST_MODIFIED_BY} property with the underlying
1260 public static void updateLastModified(Node node
) {
1261 updateLastModified(node
, false);
1265 * Updates the {@link Property#JCR_LAST_MODIFIED} property with the current time
1266 * and the {@link Property#JCR_LAST_MODIFIED_BY} property with the underlying
1267 * session user id. In Jackrabbit 2.x,
1268 * <a href="https://issues.apache.org/jira/browse/JCR-2233">these properties are
1269 * not automatically updated</a>, hence the need for manual update. The session
1272 public static void updateLastModified(Node node
, boolean addMixin
) {
1274 if (addMixin
&& !node
.isNodeType(NodeType
.MIX_LAST_MODIFIED
))
1275 node
.addMixin(NodeType
.MIX_LAST_MODIFIED
);
1276 node
.setProperty(Property
.JCR_LAST_MODIFIED
, new GregorianCalendar());
1277 node
.setProperty(Property
.JCR_LAST_MODIFIED_BY
, node
.getSession().getUserID());
1278 } catch (RepositoryException e
) {
1279 throw new JcrException("Cannot update last modified on " + node
, e
);
1284 * Update lastModified recursively until this parent.
1286 * @param node the node
1287 * @param untilPath the base path, null is equivalent to "/"
1289 public static void updateLastModifiedAndParents(Node node
, String untilPath
) {
1290 updateLastModifiedAndParents(node
, untilPath
, true);
1294 * Update lastModified recursively until this parent.
1296 * @param node the node
1297 * @param untilPath the base path, null is equivalent to "/"
1299 public static void updateLastModifiedAndParents(Node node
, String untilPath
, boolean addMixin
) {
1301 if (untilPath
!= null && !node
.getPath().startsWith(untilPath
))
1302 throw new IllegalArgumentException(node
+ " is not under " + untilPath
);
1303 updateLastModified(node
, addMixin
);
1304 if (untilPath
== null) {
1305 if (!node
.getPath().equals("/"))
1306 updateLastModifiedAndParents(node
.getParent(), untilPath
, addMixin
);
1308 if (!node
.getPath().equals(untilPath
))
1309 updateLastModifiedAndParents(node
.getParent(), untilPath
, addMixin
);
1311 } catch (RepositoryException e
) {
1312 throw new JcrException("Cannot update lastModified from " + node
+ " until " + untilPath
, e
);
1317 * Returns a String representing the short version (see
1318 * <a href="http://jackrabbit.apache.org/node-type-notation.html"> Node type
1319 * Notation </a> attributes grammar) of the main business attributes of this
1320 * property definition
1324 public static String
getPropertyDefinitionAsString(Property prop
) {
1325 StringBuffer sbuf
= new StringBuffer();
1327 if (prop
.getDefinition().isAutoCreated())
1329 if (prop
.getDefinition().isMandatory())
1331 if (prop
.getDefinition().isProtected())
1333 if (prop
.getDefinition().isMultiple())
1335 } catch (RepositoryException re
) {
1336 throw new JcrException("unexpected error while getting property definition as String", re
);
1338 return sbuf
.toString();
1342 * Estimate the sub tree size from current node. Computation is based on the Jcr
1343 * {@link Property#getLength()} method. Note : it is not the exact size used on
1344 * the disk by the current part of the JCR Tree.
1347 public static long getNodeApproxSize(Node node
) {
1348 long curNodeSize
= 0;
1350 PropertyIterator pi
= node
.getProperties();
1351 while (pi
.hasNext()) {
1352 Property prop
= pi
.nextProperty();
1353 if (prop
.isMultiple()) {
1354 int nb
= prop
.getLengths().length
;
1355 for (int i
= 0; i
< nb
; i
++) {
1356 curNodeSize
+= (prop
.getLengths()[i
] > 0 ? prop
.getLengths()[i
] : 0);
1359 curNodeSize
+= (prop
.getLength() > 0 ? prop
.getLength() : 0);
1362 NodeIterator ni
= node
.getNodes();
1363 while (ni
.hasNext())
1364 curNodeSize
+= getNodeApproxSize(ni
.nextNode());
1366 } catch (RepositoryException re
) {
1367 throw new JcrException("Unexpected error while recursively determining node size.", re
);
1376 * Convenience method for adding a single privilege to a principal (user or
1377 * role), typically jcr:all
1379 public synchronized static void addPrivilege(Session session
, String path
, String principal
, String privilege
)
1380 throws RepositoryException
{
1381 List
<Privilege
> privileges
= new ArrayList
<Privilege
>();
1382 privileges
.add(session
.getAccessControlManager().privilegeFromName(privilege
));
1383 addPrivileges(session
, path
, new SimplePrincipal(principal
), privileges
);
1387 * Add privileges on a path to a {@link Principal}. The path must already exist.
1388 * Session is saved. Synchronized to prevent concurrent modifications of the
1391 public synchronized static Boolean
addPrivileges(Session session
, String path
, Principal principal
,
1392 List
<Privilege
> privs
) throws RepositoryException
{
1393 // make sure the session is in line with the persisted state
1394 session
.refresh(false);
1395 AccessControlManager acm
= session
.getAccessControlManager();
1396 AccessControlList acl
= getAccessControlList(acm
, path
);
1398 accessControlEntries
: for (AccessControlEntry ace
: acl
.getAccessControlEntries()) {
1399 Principal currentPrincipal
= ace
.getPrincipal();
1400 if (currentPrincipal
.getName().equals(principal
.getName())) {
1401 Privilege
[] currentPrivileges
= ace
.getPrivileges();
1402 if (currentPrivileges
.length
!= privs
.size())
1403 break accessControlEntries
;
1404 for (int i
= 0; i
< currentPrivileges
.length
; i
++) {
1405 Privilege currP
= currentPrivileges
[i
];
1406 Privilege p
= privs
.get(i
);
1407 if (!currP
.getName().equals(p
.getName())) {
1408 break accessControlEntries
;
1415 Privilege
[] privileges
= privs
.toArray(new Privilege
[privs
.size()]);
1416 acl
.addAccessControlEntry(principal
, privileges
);
1417 acm
.setPolicy(path
, acl
);
1418 // if (log.isDebugEnabled()) {
1419 // StringBuffer privBuf = new StringBuffer();
1420 // for (Privilege priv : privs)
1421 // privBuf.append(priv.getName());
1422 // log.debug("Added privileges " + privBuf + " to " + principal.getName() + " on " + path + " in '"
1423 // + session.getWorkspace().getName() + "'");
1425 session
.refresh(true);
1431 * Gets the first available access control list for this path, throws exception
1434 public synchronized static AccessControlList
getAccessControlList(AccessControlManager acm
, String path
)
1435 throws RepositoryException
{
1436 // search for an access control list
1437 AccessControlList acl
= null;
1438 AccessControlPolicyIterator policyIterator
= acm
.getApplicablePolicies(path
);
1439 applicablePolicies
: if (policyIterator
.hasNext()) {
1440 while (policyIterator
.hasNext()) {
1441 AccessControlPolicy acp
= policyIterator
.nextAccessControlPolicy();
1442 if (acp
instanceof AccessControlList
) {
1443 acl
= ((AccessControlList
) acp
);
1444 break applicablePolicies
;
1448 AccessControlPolicy
[] existingPolicies
= acm
.getPolicies(path
);
1449 existingPolicies
: for (AccessControlPolicy acp
: existingPolicies
) {
1450 if (acp
instanceof AccessControlList
) {
1451 acl
= ((AccessControlList
) acp
);
1452 break existingPolicies
;
1459 throw new IllegalArgumentException("ACL not found at " + path
);
1462 /** Clear authorizations for a user at this path */
1463 public synchronized static void clearAccessControList(Session session
, String path
, String username
)
1464 throws RepositoryException
{
1465 AccessControlManager acm
= session
.getAccessControlManager();
1466 AccessControlList acl
= getAccessControlList(acm
, path
);
1467 for (AccessControlEntry ace
: acl
.getAccessControlEntries()) {
1468 if (ace
.getPrincipal().getName().equals(username
)) {
1469 acl
.removeAccessControlEntry(ace
);
1472 // the new access control list must be applied otherwise this call:
1473 // acl.removeAccessControlEntry(ace); has no effect
1474 acm
.setPolicy(path
, acl
);
1475 session
.refresh(true);
1483 * Creates the nodes making the path as {@link NodeType#NT_FOLDER}
1485 public static Node
mkfolders(Session session
, String path
) {
1486 return mkdirs(session
, path
, NodeType
.NT_FOLDER
, NodeType
.NT_FOLDER
, false);
1490 * Copy only nt:folder and nt:file, without their additional types and
1493 * @param recursive if true copies folders as well, otherwise only first level
1495 * @return how many files were copied
1497 public static Long
copyFiles(Node fromNode
, Node toNode
, Boolean recursive
, JcrMonitor monitor
, boolean onlyAdd
) {
1500 // Binary binary = null;
1501 // InputStream in = null;
1503 NodeIterator fromChildren
= fromNode
.getNodes();
1504 children
: while (fromChildren
.hasNext()) {
1505 if (monitor
!= null && monitor
.isCanceled())
1506 throw new IllegalStateException("Copy cancelled before it was completed");
1508 Node fromChild
= fromChildren
.nextNode();
1509 String fileName
= fromChild
.getName();
1510 if (fromChild
.isNodeType(NodeType
.NT_FILE
)) {
1511 if (onlyAdd
&& toNode
.hasNode(fileName
)) {
1512 monitor
.subTask("Skip existing " + fileName
);
1516 if (monitor
!= null)
1517 monitor
.subTask("Copy " + fileName
);
1518 try (Bin binary
= new Bin(fromChild
.getNode(Node
.JCR_CONTENT
).getProperty(Property
.JCR_DATA
));
1519 InputStream in
= binary
.getStream();) {
1520 copyStreamAsFile(toNode
, fileName
, in
);
1521 } catch (IOException e
) {
1522 throw new RuntimeException("Cannot copy " + fileName
+ " to " + toNode
, e
);
1526 toNode
.getSession().save();
1529 // if (log.isDebugEnabled())
1530 // log.debug("Copied file " + fromChild.getPath());
1531 if (monitor
!= null)
1533 } else if (fromChild
.isNodeType(NodeType
.NT_FOLDER
) && recursive
) {
1535 if (toNode
.hasNode(fileName
)) {
1536 toChildFolder
= toNode
.getNode(fileName
);
1537 if (!toChildFolder
.isNodeType(NodeType
.NT_FOLDER
))
1538 throw new IllegalArgumentException(toChildFolder
+ " is not of type nt:folder");
1540 toChildFolder
= toNode
.addNode(fileName
, NodeType
.NT_FOLDER
);
1543 toNode
.getSession().save();
1545 count
= count
+ copyFiles(fromChild
, toChildFolder
, recursive
, monitor
, onlyAdd
);
1549 } catch (RepositoryException e
) {
1550 throw new JcrException("Cannot copy files between " + fromNode
+ " and " + toNode
, e
);
1552 // in case there was an exception
1553 // IOUtils.closeQuietly(in);
1554 // closeQuietly(binary);
1559 * Iteratively count all file nodes in subtree, inefficient but can be useful
1560 * when query are poorly supported, such as in remoting.
1562 public static Long
countFiles(Node node
) {
1563 Long localCount
= 0l;
1565 for (NodeIterator nit
= node
.getNodes(); nit
.hasNext();) {
1566 Node child
= nit
.nextNode();
1567 if (child
.isNodeType(NodeType
.NT_FOLDER
))
1568 localCount
= localCount
+ countFiles(child
);
1569 else if (child
.isNodeType(NodeType
.NT_FILE
))
1570 localCount
= localCount
+ 1;
1572 } catch (RepositoryException e
) {
1573 throw new JcrException("Cannot count all children of " + node
, e
);
1579 * Copy a file as an nt:file, assuming an nt:folder hierarchy. The session is
1582 * @return the created file node
1585 public static Node
copyFile(Node folderNode
, File file
) {
1586 try (InputStream in
= new FileInputStream(file
)) {
1587 return copyStreamAsFile(folderNode
, file
.getName(), in
);
1588 } catch (IOException e
) {
1589 throw new RuntimeException("Cannot copy file " + file
+ " under " + folderNode
, e
);
1593 /** Copy bytes as an nt:file */
1594 public static Node
copyBytesAsFile(Node folderNode
, String fileName
, byte[] bytes
) {
1595 // InputStream in = null;
1596 try (InputStream in
= new ByteArrayInputStream(bytes
)) {
1597 // in = new ByteArrayInputStream(bytes);
1598 return copyStreamAsFile(folderNode
, fileName
, in
);
1599 } catch (IOException e
) {
1600 throw new RuntimeException("Cannot copy file " + fileName
+ " under " + folderNode
, e
);
1602 // IOUtils.closeQuietly(in);
1607 * Copy a stream as an nt:file, assuming an nt:folder hierarchy. The session is
1610 * @return the created file node
1612 public static Node
copyStreamAsFile(Node folderNode
, String fileName
, InputStream in
) {
1613 Binary binary
= null;
1617 if (folderNode
.hasNode(fileName
)) {
1618 fileNode
= folderNode
.getNode(fileName
);
1619 if (!fileNode
.isNodeType(NodeType
.NT_FILE
))
1620 throw new IllegalArgumentException(fileNode
+ " is not of type nt:file");
1621 // we assume that the content node is already there
1622 contentNode
= fileNode
.getNode(Node
.JCR_CONTENT
);
1624 fileNode
= folderNode
.addNode(fileName
, NodeType
.NT_FILE
);
1625 contentNode
= fileNode
.addNode(Node
.JCR_CONTENT
, NodeType
.NT_UNSTRUCTURED
);
1627 binary
= contentNode
.getSession().getValueFactory().createBinary(in
);
1628 contentNode
.setProperty(Property
.JCR_DATA
, binary
);
1629 updateLastModified(contentNode
);
1631 } catch (RepositoryException e
) {
1632 throw new JcrException("Cannot create file node " + fileName
+ " under " + folderNode
, e
);
1634 closeQuietly(binary
);
1638 /** Read an an nt:file as an {@link InputStream}. */
1639 public static InputStream
getFileAsStream(Node fileNode
) throws RepositoryException
{
1640 return fileNode
.getNode(Node
.JCR_CONTENT
).getProperty(Property
.JCR_DATA
).getBinary().getStream();
1644 * Set the properties of {@link NodeType#MIX_MIMETYPE} on the content of this
1647 public static void setFileMimeType(Node fileNode
, String mimeType
, String encoding
) throws RepositoryException
{
1648 Node contentNode
= fileNode
.getNode(Node
.JCR_CONTENT
);
1649 if (mimeType
!= null)
1650 contentNode
.setProperty(Property
.JCR_MIMETYPE
, mimeType
);
1651 if (encoding
!= null)
1652 contentNode
.setProperty(Property
.JCR_ENCODING
, encoding
);
1653 // TODO remove properties if args are null?
1656 public static void copyFilesToFs(Node baseNode
, Path targetDir
, boolean recursive
) {
1658 Files
.createDirectories(targetDir
);
1659 for (NodeIterator nit
= baseNode
.getNodes(); nit
.hasNext();) {
1660 Node node
= nit
.nextNode();
1661 if (node
.isNodeType(NodeType
.NT_FILE
)) {
1662 Path filePath
= targetDir
.resolve(node
.getName());
1663 try (OutputStream out
= Files
.newOutputStream(filePath
); InputStream in
= getFileAsStream(node
)) {
1664 IOUtils
.copy(in
, out
);
1666 } else if (recursive
&& node
.isNodeType(NodeType
.NT_FOLDER
)) {
1667 Path dirPath
= targetDir
.resolve(node
.getName());
1668 copyFilesToFs(node
, dirPath
, true);
1671 } catch (RepositoryException e
) {
1672 throw new JcrException("Cannot copy " + baseNode
+ " to " + targetDir
, e
);
1673 } catch (IOException e
) {
1674 throw new RuntimeException("Cannot copy " + baseNode
+ " to " + targetDir
, e
);
1679 * Computes the checksum of an nt:file.
1681 * @deprecated use separate digest utilities
1684 public static String
checksumFile(Node fileNode
, String algorithm
) {
1685 try (InputStream in
= fileNode
.getNode(Node
.JCR_CONTENT
).getProperty(Property
.JCR_DATA
).getBinary()
1687 return digest(algorithm
, in
);
1688 } catch (IOException e
) {
1689 throw new RuntimeException("Cannot checksum file " + fileNode
+ " with algorithm " + algorithm
, e
);
1690 } catch (RepositoryException e
) {
1691 throw new JcrException("Cannot checksum file " + fileNode
+ " with algorithm " + algorithm
, e
);
1696 private static String
digest(String algorithm
, InputStream in
) {
1697 final Integer byteBufferCapacity
= 100 * 1024;// 100 KB
1699 MessageDigest digest
= MessageDigest
.getInstance(algorithm
);
1700 byte[] buffer
= new byte[byteBufferCapacity
];
1702 while ((read
= in
.read(buffer
)) > 0) {
1703 digest
.update(buffer
, 0, read
);
1706 byte[] checksum
= digest
.digest();
1707 String res
= encodeHexString(checksum
);
1709 } catch (IOException e
) {
1710 throw new RuntimeException("Cannot digest with algorithm " + algorithm
, e
);
1711 } catch (NoSuchAlgorithmException e
) {
1712 throw new IllegalArgumentException("Cannot digest with algorithm " + algorithm
, e
);
1718 * http://stackoverflow.com/questions/9655181/how-to-convert-a-byte-array-to
1719 * -a-hex-string-in-java
1722 private static String
encodeHexString(byte[] bytes
) {
1723 final char[] hexArray
= "0123456789abcdef".toCharArray();
1724 char[] hexChars
= new char[bytes
.length
* 2];
1725 for (int j
= 0; j
< bytes
.length
; j
++) {
1726 int v
= bytes
[j
] & 0xFF;
1727 hexChars
[j
* 2] = hexArray
[v
>>> 4];
1728 hexChars
[j
* 2 + 1] = hexArray
[v
& 0x0F];
1730 return new String(hexChars
);
1733 /** Export a subtree as a compact XML without namespaces. */
1734 public static void toSimpleXml(Node node
, StringBuilder sb
) throws RepositoryException
{
1736 String nodeName
= node
.getName();
1737 int colIndex
= nodeName
.indexOf(':');
1739 nodeName
= nodeName
.substring(colIndex
+ 1);
1741 sb
.append(nodeName
);
1742 PropertyIterator pit
= node
.getProperties();
1743 properties
: while (pit
.hasNext()) {
1744 Property p
= pit
.nextProperty();
1745 // skip multiple properties
1747 continue properties
;
1748 String propertyName
= p
.getName();
1749 int pcolIndex
= propertyName
.indexOf(':');
1750 // skip properties with namespaces
1752 continue properties
;
1754 if (p
.getType() == PropertyType
.BINARY
) {
1755 continue properties
;
1756 // TODO retrieve identifier?
1759 sb
.append(propertyName
);
1761 sb
.append('\"').append(p
.getString()).append('\"');
1764 if (node
.hasNodes()) {
1766 NodeIterator children
= node
.getNodes();
1767 while (children
.hasNext()) {
1768 toSimpleXml(children
.nextNode(), sb
);
1771 sb
.append(nodeName
);