]> git.argeo.org Git - gpl/argeo-jcr.git/blob - org.argeo.cms.jcr/src/org/argeo/jcr/JcrUtils.java
Work on supporting outpout stream for binary
[gpl/argeo-jcr.git] / org.argeo.cms.jcr / src / org / argeo / jcr / JcrUtils.java
1 package org.argeo.jcr;
2
3 import java.io.ByteArrayInputStream;
4 import java.io.ByteArrayOutputStream;
5 import java.io.File;
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;
13 import java.net.URL;
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;
29 import java.util.Map;
30 import java.util.TreeMap;
31
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;
58
59 import org.apache.commons.io.IOUtils;
60
61 /** Utility methods to simplify common JCR operations. */
62 public class JcrUtils {
63
64 // final private static Log log = LogFactory.getLog(JcrUtils.class);
65
66 /**
67 * Not complete yet. See
68 * http://www.day.com/specs/jcr/2.0/3_Repository_Model.html#3.2.2%20Local
69 * %20Names
70 */
71 public final static char[] INVALID_NAME_CHARACTERS = { '/', ':', '[', ']', '|', '*', /* invalid for XML: */ '<',
72 '>', '&' };
73
74 /** Prevents instantiation */
75 private JcrUtils() {
76 }
77
78 /**
79 * Queries one single node.
80 *
81 * @return one single node or null if none was found
82 * @throws JcrException if more than one node was found
83 */
84 public static Node querySingleNode(Query query) {
85 NodeIterator nodeIterator;
86 try {
87 QueryResult queryResult = query.execute();
88 nodeIterator = queryResult.getNodes();
89 } catch (RepositoryException e) {
90 throw new JcrException("Cannot execute query " + query, e);
91 }
92 Node node;
93 if (nodeIterator.hasNext())
94 node = nodeIterator.nextNode();
95 else
96 return null;
97
98 if (nodeIterator.hasNext())
99 throw new IllegalArgumentException("Query returned more than one node.");
100 return node;
101 }
102
103 /** Retrieves the node name from the provided path */
104 public static String nodeNameFromPath(String path) {
105 if (path.equals("/"))
106 return "";
107 if (path.charAt(0) != '/')
108 throw new IllegalArgumentException("Path " + path + " must start with a '/'");
109 String pathT = path;
110 if (pathT.charAt(pathT.length() - 1) == '/')
111 pathT = pathT.substring(0, pathT.length() - 2);
112
113 int index = pathT.lastIndexOf('/');
114 return pathT.substring(index + 1);
115 }
116
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 '/'");
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(0, index);
129 }
130
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);
134 }
135
136 /**
137 * Creates a deep path based on a URL:
138 * http://subdomain.example.com/to/content?args becomes
139 * com/example/subdomain/to/content
140 */
141 public static String urlAsPath(String url) {
142 try {
143 URL u = new URL(url);
144 StringBuffer path = new StringBuffer(url.length());
145 // invert host
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);
152 }
153 }
154
155 /** Set the {@link NodeType#NT_ADDRESS} properties based on this URL. */
156 public static void urlToAddressProperties(Node node, String url) {
157 try {
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);
167 }
168 }
169
170 /** Build URL based on the {@link NodeType#NT_ADDRESS} properties. */
171 public static String urlFromAddressProperties(Node node) {
172 try {
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());
177 return u.toString();
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);
182 }
183 }
184
185 /*
186 * PATH UTILITIES
187 */
188
189 /**
190 * Make sure that: starts with '/', do not end with '/', do not have '//'
191 */
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) {
196 buf.append('/');
197 buf.append(token);
198 }
199 return buf.toString();
200 }
201
202 /**
203 * Creates a path from a FQDN, inverting the order of the component:
204 * www.argeo.org becomes org.argeo.www
205 */
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]);
211 if (i != 0)
212 path.append('/');
213 }
214 return path.toString();
215 }
216
217 /**
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
220 */
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]);
226 if (i != 0)
227 path.append('/');
228 }
229 return path.toString();
230 }
231
232 /**
233 * The provided data as a path ('/' at the end, not the beginning)
234 *
235 * @param cal the date
236 * @param addHour whether to add hour as well
237 */
238 public static String dateAsPath(Calendar cal, Boolean addHour) {
239 StringBuffer buf = new StringBuffer(14);
240 buf.append('Y');
241 buf.append(cal.get(Calendar.YEAR));
242 buf.append('/');
243
244 int month = cal.get(Calendar.MONTH) + 1;
245 buf.append('M');
246 if (month < 10)
247 buf.append(0);
248 buf.append(month);
249 buf.append('/');
250
251 int day = cal.get(Calendar.DAY_OF_MONTH);
252 buf.append('D');
253 if (day < 10)
254 buf.append(0);
255 buf.append(day);
256 buf.append('/');
257
258 if (addHour) {
259 int hour = cal.get(Calendar.HOUR_OF_DAY);
260 buf.append('H');
261 if (hour < 10)
262 buf.append(0);
263 buf.append(hour);
264 buf.append('/');
265 }
266 return buf.toString();
267
268 }
269
270 /** Converts in one call a string into a gregorian calendar. */
271 public static Calendar parseCalendar(DateFormat dateFormat, String value) {
272 try {
273 Date date = dateFormat.parse(value);
274 Calendar calendar = new GregorianCalendar();
275 calendar.setTime(date);
276 return calendar;
277 } catch (ParseException e) {
278 throw new IllegalArgumentException("Cannot parse " + value + " with date format " + dateFormat, e);
279 }
280
281 }
282
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('/');
288 if (index < 0)
289 return path;
290 return path.substring(index + 1);
291 }
292
293 /**
294 * Call {@link Node#getName()} without exceptions (useful in super
295 * constructors).
296 */
297 public static String getNameQuietly(Node node) {
298 try {
299 return node.getName();
300 } catch (RepositoryException e) {
301 throw new JcrException("Cannot get name from " + node, e);
302 }
303 }
304
305 /**
306 * Call {@link Node#getProperty(String)} without exceptions (useful in super
307 * constructors).
308 */
309 public static String getStringPropertyQuietly(Node node, String propertyName) {
310 try {
311 return node.getProperty(propertyName).getString();
312 } catch (RepositoryException e) {
313 throw new JcrException("Cannot get name from " + node, e);
314 }
315 }
316
317 // /**
318 // * Routine that get the child with this name, adding it if it does not already
319 // * exist
320 // */
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);
323 // }
324
325 /**
326 * Routine that get the child with this name, adding it if it does not already
327 * exist
328 */
329 public static Node getOrAdd(Node parent, String name, String primaryNodeType, String... mixinNodeTypes)
330 throws RepositoryException {
331 Node node;
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);
340 }
341 return node;
342 } else {
343 node = primaryNodeType != null ? parent.addNode(name, primaryNodeType) : parent.addNode(name);
344 for (String mixin : mixinNodeTypes) {
345 node.addMixin(mixin);
346 }
347 return node;
348 }
349 }
350
351 /**
352 * Routine that get the child with this name, adding it if it does not already
353 * exist
354 */
355 public static Node getOrAdd(Node parent, String name) throws RepositoryException {
356 return parent.hasNode(name) ? parent.getNode(name) : parent.addNode(name);
357 }
358
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());
364 }
365 return nodes;
366 }
367
368 /*
369 * PROPERTIES
370 */
371
372 /**
373 * Concisely get the string value of a property or null if this node doesn't
374 * have this property
375 */
376 public static String get(Node node, String propertyName) {
377 try {
378 if (!node.hasProperty(propertyName))
379 return null;
380 return node.getProperty(propertyName).getString();
381 } catch (RepositoryException e) {
382 throw new JcrException("Cannot get property " + propertyName + " of " + node, e);
383 }
384 }
385
386 /** Concisely get the path of the given node. */
387 public static String getPath(Node node) {
388 try {
389 return node.getPath();
390 } catch (RepositoryException e) {
391 throw new JcrException("Cannot get path of " + node, e);
392 }
393 }
394
395 /** Concisely get the boolean value of a property */
396 public static Boolean check(Node node, String propertyName) {
397 try {
398 return node.getProperty(propertyName).getBoolean();
399 } catch (RepositoryException e) {
400 throw new JcrException("Cannot get property " + propertyName + " of " + node, e);
401 }
402 }
403
404 /** Concisely get the bytes array value of a property */
405 public static byte[] getBytes(Node node, String propertyName) {
406 try {
407 return getBinaryAsBytes(node.getProperty(propertyName));
408 } catch (RepositoryException e) {
409 throw new JcrException("Cannot get property " + propertyName + " of " + node, e);
410 }
411 }
412
413 /*
414 * MKDIRS
415 */
416
417 /**
418 * Create sub nodes relative to a parent node
419 */
420 public static Node mkdirs(Node parentNode, String relativePath) {
421 return mkdirs(parentNode, relativePath, null, null);
422 }
423
424 /**
425 * Create sub nodes relative to a parent node
426 *
427 * @param nodeType the type of the leaf node
428 */
429 public static Node mkdirs(Node parentNode, String relativePath, String nodeType) {
430 return mkdirs(parentNode, relativePath, nodeType, null);
431 }
432
433 /**
434 * Create sub nodes relative to a parent node
435 *
436 * @param nodeType the type of the leaf node
437 */
438 public static Node mkdirs(Node parentNode, String relativePath, String nodeType, String intermediaryNodeType) {
439 List<String> tokens = tokenize(relativePath);
440 Node currParent = parentNode;
441 try {
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);
446 } else {
447 if (i != (tokens.size() - 1)) {// intermediary
448 currParent = currParent.addNode(name, intermediaryNodeType);
449 } else {// leaf
450 currParent = currParent.addNode(name, nodeType);
451 }
452 }
453 }
454 return currParent;
455 } catch (RepositoryException e) {
456 throw new JcrException("Cannot mkdirs relative path " + relativePath + " from " + parentNode, e);
457 }
458 }
459
460 /**
461 * Synchronized and save is performed, to avoid race conditions in initializers
462 * leading to duplicate nodes.
463 */
464 public synchronized static Node mkdirsSafe(Session session, String path, String type) {
465 try {
466 if (session.hasPendingChanges())
467 throw new IllegalStateException("Session has pending changes, save them first.");
468 Node node = mkdirs(session, path, type);
469 session.save();
470 return node;
471 } catch (RepositoryException e) {
472 discardQuietly(session);
473 throw new JcrException("Cannot safely make directories", e);
474 }
475 }
476
477 public synchronized static Node mkdirsSafe(Session session, String path) {
478 return mkdirsSafe(session, path, null);
479 }
480
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);
484 }
485
486 /**
487 * @param type the type of the leaf node
488 */
489 public static Node mkdirs(Session session, String path, String type) {
490 return mkdirs(session, path, type, null, false);
491 }
492
493 /**
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.
497 */
498 public static Node mkdirs(Session session, String path, String type, String intermediaryNodeType,
499 Boolean versioning) {
500 try {
501 if (path.equals("/"))
502 return session.getRootNode();
503
504 if (session.itemExists(path)) {
505 Node node = session.getNode(path);
506 // check type
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
511 return node;
512 }
513
514 // StringBuffer current = new StringBuffer("/");
515 // Node currentNode = session.getRootNode();
516
517 Node currentNode = findClosestExistingParent(session, path);
518 String closestExistingParentPath = currentNode.getPath();
519 StringBuffer current = new StringBuffer(closestExistingParentPath);
520 if (!closestExistingParentPath.endsWith("/"))
521 current.append('/');
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);
531 else
532 currentNode = currentNode.addNode(part);
533 if (versioning)
534 currentNode.addMixin(NodeType.MIX_VERSIONABLE);
535 // if (log.isTraceEnabled())
536 // log.debug("Added folder " + part + " as " + current);
537 } else {
538 currentNode = (Node) session.getItem(current.toString());
539 }
540 }
541 return currentNode;
542 } catch (RepositoryException e) {
543 discardQuietly(session);
544 throw new JcrException("Cannot mkdirs " + path, e);
545 } finally {
546 }
547 }
548
549 private static Node findClosestExistingParent(Session session, String path) throws RepositoryException {
550 int idx = path.lastIndexOf('/');
551 if (idx == 0)
552 return session.getRootNode();
553 String parentPath = path.substring(0, idx);
554 if (session.itemExists(parentPath))
555 return session.getNode(parentPath);
556 else
557 return findClosestExistingParent(session, parentPath);
558 }
559
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;
564 if (!optimized) {
565 String[] rawTokens = path.split("/");
566 for (String token : rawTokens) {
567 if (!token.equals(""))
568 tokens.add(token);
569 }
570 } else {
571 StringBuffer curr = new StringBuffer();
572 char[] arr = path.toCharArray();
573 chars: for (int i = 0; i < arr.length; i++) {
574 char c = arr[i];
575 if (c == '/') {
576 if (i == 0 || (i == arr.length - 1))
577 continue chars;
578 if (curr.length() > 0) {
579 tokens.add(curr.toString());
580 curr = new StringBuffer();
581 }
582 } else
583 curr.append(c);
584 }
585 if (curr.length() > 0) {
586 tokens.add(curr.toString());
587 curr = new StringBuffer();
588 }
589 }
590 return Collections.unmodifiableList(tokens);
591 }
592
593 // /**
594 // * use {@link #mkdirs(Session, String, String, String, Boolean)} instead.
595 // *
596 // * @deprecated
597 // */
598 // @Deprecated
599 // public static Node mkdirs(Session session, String path, String type,
600 // Boolean versioning) {
601 // return mkdirs(session, path, type, type, false);
602 // }
603
604 /**
605 * Safe and repository implementation independent registration of a namespace.
606 */
607 public static void registerNamespaceSafely(Session session, String prefix, String uri) {
608 try {
609 registerNamespaceSafely(session.getWorkspace().getNamespaceRegistry(), prefix, uri);
610 } catch (RepositoryException e) {
611 throw new JcrException("Cannot find namespace registry", e);
612 }
613 }
614
615 /**
616 * Safe and repository implementation independent registration of a namespace.
617 */
618 public static void registerNamespaceSafely(NamespaceRegistry nr, String prefix, String uri) {
619 try {
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);
627 else
628 return;// skip
629 }
630 nr.registerNamespace(prefix, uri);
631 } catch (RepositoryException e) {
632 throw new JcrException("Cannot register namespace " + uri + " under prefix " + prefix, e);
633 }
634 }
635
636 // /** Recursively outputs the contents of the given node. */
637 // public static void debug(Node node) {
638 // debug(node, log);
639 // }
640 //
641 // /** Recursively outputs the contents of the given node. */
642 // public static void debug(Node node, Log log) {
643 // try {
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")) {
648 // return;
649 // }
650 //
651 // // Then the children nodes (recursive)
652 // NodeIterator it = node.getNodes();
653 // while (it.hasNext()) {
654 // Node childNode = it.nextNode();
655 // debug(childNode, log);
656 // }
657 //
658 // // Then output the properties
659 // PropertyIterator properties = node.getProperties();
660 // // log.debug("Property are : ");
661 //
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());
671 // }
672 // } else {
673 // // A single-valued property
674 // log.debug(property.getPath() + "=" + property.getString());
675 // }
676 // }
677 // } catch (Exception e) {
678 // log.error("Could not debug " + node, e);
679 // }
680 //
681 // }
682
683 // /** Logs the effective access control policies */
684 // public static void logEffectiveAccessPolicies(Node node) {
685 // try {
686 // logEffectiveAccessPolicies(node.getSession(), node.getPath());
687 // } catch (RepositoryException e) {
688 // log.error("Cannot log effective access policies of " + node, e);
689 // }
690 // }
691 //
692 // /** Logs the effective access control policies */
693 // public static void logEffectiveAccessPolicies(Session session, String path) {
694 // if (!log.isDebugEnabled())
695 // return;
696 //
697 // try {
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));
704 // }
705 // }
706 // } else {
707 // log.debug("No effective access control policy for " + path);
708 // }
709 // } catch (RepositoryException e) {
710 // log.error("Cannot log effective access policies of " + path, e);
711 // }
712 // }
713
714 /** Returns a human-readable summary of this access control list. */
715 public static String accessControlListSummary(AccessControlList acl) {
716 StringBuffer buf = new StringBuffer("");
717 try {
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');
722 }
723 return buf.toString();
724 } catch (RepositoryException e) {
725 throw new JcrException("Cannot write summary of " + acl, e);
726 }
727 }
728
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.";
734
735 try (PipedInputStream in = new PipedInputStream(1024 * 1024);) {
736 new Thread(() -> {
737 try (PipedOutputStream out = new PipedOutputStream(in)) {
738 fromSession.exportSystemView("/", out, false, false);
739 out.flush();
740 } catch (IOException e) {
741 throw new RuntimeException(errorMsg, e);
742 } catch (RepositoryException e) {
743 throw new JcrException(errorMsg, e);
744 }
745 }, "Copy workspace" + fromWorkspace + " to " + toWorkspace).start();
746
747 toSession.importXML("/", in, ImportUUIDBehavior.IMPORT_UUID_COLLISION_REPLACE_EXISTING);
748 toSession.save();
749 } catch (IOException e) {
750 throw new RuntimeException(errorMsg, e);
751 } catch (RepositoryException e) {
752 throw new JcrException(errorMsg, e);
753 }
754 }
755
756 /**
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}
762 * mixin.
763 */
764 public static void copy(Node fromNode, Node toNode) {
765 try {
766 if (toNode.getDefinition().isProtected())
767 return;
768
769 // add mixins
770 for (NodeType mixinType : fromNode.getMixinNodeTypes()) {
771 try {
772 toNode.addMixin(mixinType.getName());
773 } catch (NoSuchNodeTypeException e) {
774 // ignore unknown mixins
775 // TODO log it
776 }
777 }
778
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())
785 continue properties;
786
787 if (fromProperty.getDefinition().isProtected())
788 continue properties;
789
790 if (propertyName.equals("jcr:created") || propertyName.equals("jcr:createdBy")
791 || propertyName.equals("jcr:lastModified") || propertyName.equals("jcr:lastModifiedBy"))
792 continue properties;
793
794 if (fromProperty.isMultiple()) {
795 toNode.setProperty(propertyName, fromProperty.getValues());
796 } else {
797 toNode.setProperty(propertyName, fromProperty.getValue());
798 }
799 }
800
801 // update jcr:lastModified and jcr:lastModifiedBy in toNode in case
802 // they existed, before adding the mixins
803 updateLastModified(toNode, true);
804
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 + "]";
811 Node toChild;
812 if (toNode.hasNode(nodeRelPath))
813 toChild = toNode.getNode(nodeRelPath);
814 else {
815 try {
816 toChild = toNode.addNode(fromChild.getName(), fromChild.getPrimaryNodeType().getName());
817 } catch (NoSuchNodeTypeException e) {
818 // ignore unknown primary types
819 // TODO log it
820 return;
821 }
822 }
823 copy(fromChild, toChild);
824 }
825 } catch (RepositoryException e) {
826 throw new JcrException("Cannot copy " + fromNode + " to " + toNode, e);
827 }
828 }
829
830 /**
831 * Check whether all first-level properties (except jcr:* properties) are equal.
832 * Skip jcr:* properties
833 */
834 public static Boolean allPropertiesEquals(Node reference, Node observed, Boolean onlyCommonProperties) {
835 try {
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:"))
841 continue props;
842
843 if (!observed.hasProperty(propName))
844 if (onlyCommonProperties)
845 continue props;
846 else
847 return false;
848 // TODO: deal with multiple property values?
849 if (!observed.getProperty(propName).getValue().equals(propReference.getValue()))
850 return false;
851 }
852 return true;
853 } catch (RepositoryException e) {
854 throw new JcrException("Cannot check all properties equals of " + reference + " and " + observed, e);
855 }
856 }
857
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);
861 return diffs;
862 }
863
864 /**
865 * Compare the properties of two nodes. Recursivity to child nodes is not yet
866 * supported. Skip jcr:* properties.
867 */
868 static void diffPropertiesLevel(Map<String, PropertyDiff> diffs, String baseRelPath, Node reference,
869 Node observed) {
870 try {
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:"))
877 continue props;
878
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);
883 } else {
884 if (p.isMultiple()) {
885 // FIXME implement multiple
886 } else {
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,
892 newValue);
893 diffs.put(relPath, pDiff);
894 }
895 }
896 }
897 }
898 // check added
899 pit = observed.getProperties();
900 props: while (pit.hasNext()) {
901 Property p = pit.nextProperty();
902 String name = p.getName();
903 if (name.startsWith("jcr:"))
904 continue props;
905 if (!reference.hasProperty(name)) {
906 if (p.isMultiple()) {
907 // FIXME implement multiple
908 } else {
909 String relPath = propertyRelPath(baseRelPath, name);
910 PropertyDiff pDiff = new PropertyDiff(PropertyDiff.ADDED, relPath, null, p.getValue());
911 diffs.put(relPath, pDiff);
912 }
913 }
914 }
915 } catch (RepositoryException e) {
916 throw new JcrException("Cannot diff " + reference + " and " + observed, e);
917 }
918 }
919
920 /**
921 * Compare only a restricted list of properties of two nodes. No recursivity.
922 *
923 */
924 public static Map<String, PropertyDiff> diffProperties(Node reference, Node observed, List<String> properties) {
925 Map<String, PropertyDiff> diffs = new TreeMap<String, PropertyDiff>();
926 try {
927 Iterator<String> pit = properties.iterator();
928
929 props: while (pit.hasNext()) {
930 String name = pit.next();
931 if (!reference.hasProperty(name)) {
932 if (!observed.hasProperty(name))
933 continue props;
934 Value val = observed.getProperty(name).getValue();
935 try {
936 // empty String but not null
937 if ("".equals(val.getString()))
938 continue props;
939 } catch (Exception e) {
940 // not parseable as String, silent
941 }
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);
948 } else {
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);
954 }
955 }
956 }
957 } catch (RepositoryException e) {
958 throw new JcrException("Cannot diff " + reference + " and " + observed, e);
959 }
960 return diffs;
961 }
962
963 /** Builds a property relPath to be used in the diff. */
964 private static String propertyRelPath(String baseRelPath, String propertyName) {
965 if (baseRelPath == null)
966 return propertyName;
967 else
968 return baseRelPath + '/' + propertyName;
969 }
970
971 /**
972 * Normalizes a name so that it can be stored in contexts not supporting names
973 * with ':' (typically databases). Replaces ':' by '_'.
974 */
975 public static String normalize(String name) {
976 return name.replace(':', '_');
977 }
978
979 /**
980 * Replaces characters which are invalid in a JCR name by '_'. Currently not
981 * exhaustive.
982 *
983 * @see JcrUtils#INVALID_NAME_CHARACTERS
984 */
985 public static String replaceInvalidChars(String name) {
986 return replaceInvalidChars(name, '_');
987 }
988
989 /**
990 * Replaces characters which are invalid in a JCR name. Currently not
991 * exhaustive.
992 *
993 * @see JcrUtils#INVALID_NAME_CHARACTERS
994 */
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++) {
999 char c = arr[i];
1000 invalid: for (char invalid : INVALID_NAME_CHARACTERS) {
1001 if (c == invalid) {
1002 arr[i] = replacement;
1003 modified = true;
1004 break invalid;
1005 }
1006 }
1007 }
1008 if (modified)
1009 return new String(arr);
1010 else
1011 // do not create new object if unnecessary
1012 return name;
1013 }
1014
1015 // /**
1016 // * Removes forbidden characters from a path, replacing them with '_'
1017 // *
1018 // * @deprecated use {@link #replaceInvalidChars(String)} instead
1019 // */
1020 // public static String removeForbiddenCharacters(String str) {
1021 // return str.replace('[', '_').replace(']', '_').replace('/', '_').replace('*',
1022 // '_');
1023 //
1024 // }
1025
1026 /** Cleanly disposes a {@link Binary} even if it is null. */
1027 public static void closeQuietly(Binary binary) {
1028 if (binary == null)
1029 return;
1030 binary.dispose();
1031 }
1032
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);
1044 }
1045 }
1046
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);
1057 } finally {
1058 closeQuietly(binary);
1059 }
1060 }
1061
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);
1072 } finally {
1073 closeQuietly(binary);
1074 }
1075 }
1076
1077 /**
1078 * Creates depth from a string (typically a username) by adding levels based on
1079 * its first characters: "aBcD",2 becomes a/aB
1080 */
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));
1088 path.append(curr);
1089 if (i < nbrOfChars - 1)
1090 path.append('/');
1091 }
1092 return path.toString();
1093 }
1094
1095 /**
1096 * Discards the current changes in the session attached to this node. To be used
1097 * typically in a catch block.
1098 *
1099 * @see #discardQuietly(Session)
1100 */
1101 public static void discardUnderlyingSessionQuietly(Node node) {
1102 try {
1103 discardQuietly(node.getSession());
1104 } catch (RepositoryException e) {
1105 // silent
1106 }
1107 }
1108
1109 /**
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.
1113 */
1114 public static void discardQuietly(Session session) {
1115 try {
1116 if (session != null)
1117 session.refresh(false);
1118 } catch (RepositoryException e) {
1119 // silent
1120 }
1121 }
1122
1123 /**
1124 * Login to a workspace with implicit credentials, creates the workspace with
1125 * these credentials if it does not already exist.
1126 */
1127 public static Session loginOrCreateWorkspace(Repository repository, String workspaceName)
1128 throws RepositoryException {
1129 return loginOrCreateWorkspace(repository, workspaceName, null);
1130 }
1131
1132 /**
1133 * Login to a workspace with implicit credentials, creates the workspace with
1134 * these credentials if it does not already exist.
1135 */
1136 public static Session loginOrCreateWorkspace(Repository repository, String workspaceName, Credentials credentials)
1137 throws RepositoryException {
1138 Session workspaceSession = null;
1139 Session defaultSession = null;
1140 try {
1141 try {
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
1148 // work around non-atomicity of workspace creation in Jackrabbit
1149 // try {
1150 // Thread.sleep(5000);
1151 // } catch (InterruptedException e1) {
1152 // // ignore
1153 // }
1154
1155 workspaceSession = repository.login(credentials, workspaceName);
1156 }
1157 return workspaceSession;
1158 } finally {
1159 logoutQuietly(defaultSession);
1160 }
1161 }
1162
1163 /**
1164 * Logs out the session, not throwing any exception, even if it is null.
1165 * {@link Jcr#logout(Session)} should rather be used.
1166 */
1167 public static void logoutQuietly(Session session) {
1168 Jcr.logout(session);
1169 // try {
1170 // if (session != null)
1171 // if (session.isLive())
1172 // session.logout();
1173 // } catch (Exception e) {
1174 // // silent
1175 // }
1176 }
1177
1178 /**
1179 * Convenient method to add a listener. uuids passed as null, deep=true,
1180 * local=true, only one node type
1181 */
1182 public static void addListener(Session session, EventListener listener, int eventTypes, String basePath,
1183 String nodeType) {
1184 try {
1185 session.getWorkspace().getObservationManager().addEventListener(listener, eventTypes, basePath, true, null,
1186 nodeType == null ? null : new String[] { nodeType }, true);
1187 } catch (RepositoryException e) {
1188 throw new JcrException("Cannot add JCR listener " + listener + " to session " + session, e);
1189 }
1190 }
1191
1192 /** Removes a listener without throwing exception */
1193 public static void removeListenerQuietly(Session session, EventListener listener) {
1194 if (session == null || !session.isLive())
1195 return;
1196 try {
1197 session.getWorkspace().getObservationManager().removeEventListener(listener);
1198 } catch (RepositoryException e) {
1199 // silent
1200 }
1201 }
1202
1203 /**
1204 * Quietly unregisters an {@link EventListener} from the udnerlying workspace of
1205 * this node.
1206 */
1207 public static void unregisterQuietly(Node node, EventListener eventListener) {
1208 try {
1209 unregisterQuietly(node.getSession().getWorkspace(), eventListener);
1210 } catch (RepositoryException e) {
1211 // silent
1212 }
1213 }
1214
1215 /** Quietly unregisters an {@link EventListener} from this workspace */
1216 public static void unregisterQuietly(Workspace workspace, EventListener eventListener) {
1217 if (eventListener == null)
1218 return;
1219 try {
1220 workspace.getObservationManager().removeEventListener(eventListener);
1221 } catch (RepositoryException e) {
1222 // silent
1223 }
1224 }
1225
1226 /**
1227 * Checks whether {@link Property#JCR_LAST_MODIFIED} or (afterwards)
1228 * {@link Property#JCR_CREATED} are set and returns it as an {@link Instant}.
1229 */
1230 public static Instant getModified(Node node) {
1231 Calendar calendar = null;
1232 try {
1233 if (node.hasProperty(Property.JCR_LAST_MODIFIED))
1234 calendar = node.getProperty(Property.JCR_LAST_MODIFIED).getDate();
1235 else if (node.hasProperty(Property.JCR_CREATED))
1236 calendar = node.getProperty(Property.JCR_CREATED).getDate();
1237 else
1238 throw new IllegalArgumentException("No modification time found in " + node);
1239 return calendar.toInstant();
1240 } catch (RepositoryException e) {
1241 throw new JcrException("Cannot get modification time for " + node, e);
1242 }
1243
1244 }
1245
1246 /**
1247 * Get {@link Property#JCR_CREATED} as an {@link Instant}, if it is set.
1248 */
1249 public static Instant getCreated(Node node) {
1250 Calendar calendar = null;
1251 try {
1252 if (node.hasProperty(Property.JCR_CREATED))
1253 calendar = node.getProperty(Property.JCR_CREATED).getDate();
1254 else
1255 throw new IllegalArgumentException("No created time found in " + node);
1256 return calendar.toInstant();
1257 } catch (RepositoryException e) {
1258 throw new JcrException("Cannot get created time for " + node, e);
1259 }
1260
1261 }
1262
1263 /**
1264 * Updates the {@link Property#JCR_LAST_MODIFIED} property with the current time
1265 * and the {@link Property#JCR_LAST_MODIFIED_BY} property with the underlying
1266 * session user id.
1267 */
1268 public static void updateLastModified(Node node) {
1269 updateLastModified(node, false);
1270 }
1271
1272 /**
1273 * Updates the {@link Property#JCR_LAST_MODIFIED} property with the current time
1274 * and the {@link Property#JCR_LAST_MODIFIED_BY} property with the underlying
1275 * session user id. In Jackrabbit 2.x,
1276 * <a href="https://issues.apache.org/jira/browse/JCR-2233">these properties are
1277 * not automatically updated</a>, hence the need for manual update. The session
1278 * is not saved.
1279 */
1280 public static void updateLastModified(Node node, boolean addMixin) {
1281 try {
1282 if (addMixin && !node.isNodeType(NodeType.MIX_LAST_MODIFIED))
1283 node.addMixin(NodeType.MIX_LAST_MODIFIED);
1284 node.setProperty(Property.JCR_LAST_MODIFIED, new GregorianCalendar());
1285 node.setProperty(Property.JCR_LAST_MODIFIED_BY, node.getSession().getUserID());
1286 } catch (RepositoryException e) {
1287 throw new JcrException("Cannot update last modified on " + node, e);
1288 }
1289 }
1290
1291 /**
1292 * Update lastModified recursively until this parent.
1293 *
1294 * @param node the node
1295 * @param untilPath the base path, null is equivalent to "/"
1296 */
1297 public static void updateLastModifiedAndParents(Node node, String untilPath) {
1298 updateLastModifiedAndParents(node, untilPath, true);
1299 }
1300
1301 /**
1302 * Update lastModified recursively until this parent.
1303 *
1304 * @param node the node
1305 * @param untilPath the base path, null is equivalent to "/"
1306 */
1307 public static void updateLastModifiedAndParents(Node node, String untilPath, boolean addMixin) {
1308 try {
1309 if (untilPath != null && !node.getPath().startsWith(untilPath))
1310 throw new IllegalArgumentException(node + " is not under " + untilPath);
1311 updateLastModified(node, addMixin);
1312 if (untilPath == null) {
1313 if (!node.getPath().equals("/"))
1314 updateLastModifiedAndParents(node.getParent(), untilPath, addMixin);
1315 } else {
1316 if (!node.getPath().equals(untilPath))
1317 updateLastModifiedAndParents(node.getParent(), untilPath, addMixin);
1318 }
1319 } catch (RepositoryException e) {
1320 throw new JcrException("Cannot update lastModified from " + node + " until " + untilPath, e);
1321 }
1322 }
1323
1324 /**
1325 * Returns a String representing the short version (see
1326 * <a href="http://jackrabbit.apache.org/node-type-notation.html"> Node type
1327 * Notation </a> attributes grammar) of the main business attributes of this
1328 * property definition
1329 *
1330 * @param prop
1331 */
1332 public static String getPropertyDefinitionAsString(Property prop) {
1333 StringBuffer sbuf = new StringBuffer();
1334 try {
1335 if (prop.getDefinition().isAutoCreated())
1336 sbuf.append("a");
1337 if (prop.getDefinition().isMandatory())
1338 sbuf.append("m");
1339 if (prop.getDefinition().isProtected())
1340 sbuf.append("p");
1341 if (prop.getDefinition().isMultiple())
1342 sbuf.append("*");
1343 } catch (RepositoryException re) {
1344 throw new JcrException("unexpected error while getting property definition as String", re);
1345 }
1346 return sbuf.toString();
1347 }
1348
1349 /**
1350 * Estimate the sub tree size from current node. Computation is based on the Jcr
1351 * {@link Property#getLength()} method. Note : it is not the exact size used on
1352 * the disk by the current part of the JCR Tree.
1353 */
1354
1355 public static long getNodeApproxSize(Node node) {
1356 long curNodeSize = 0;
1357 try {
1358 PropertyIterator pi = node.getProperties();
1359 while (pi.hasNext()) {
1360 Property prop = pi.nextProperty();
1361 if (prop.isMultiple()) {
1362 int nb = prop.getLengths().length;
1363 for (int i = 0; i < nb; i++) {
1364 curNodeSize += (prop.getLengths()[i] > 0 ? prop.getLengths()[i] : 0);
1365 }
1366 } else
1367 curNodeSize += (prop.getLength() > 0 ? prop.getLength() : 0);
1368 }
1369
1370 NodeIterator ni = node.getNodes();
1371 while (ni.hasNext())
1372 curNodeSize += getNodeApproxSize(ni.nextNode());
1373 return curNodeSize;
1374 } catch (RepositoryException re) {
1375 throw new JcrException("Unexpected error while recursively determining node size.", re);
1376 }
1377 }
1378
1379 /*
1380 * SECURITY
1381 */
1382
1383 /**
1384 * Convenience method for adding a single privilege to a principal (user or
1385 * role), typically jcr:all. Session is saved.
1386 */
1387 public synchronized static void addPrivilege(Session session, String path, String principal, String privilege)
1388 throws RepositoryException {
1389 List<Privilege> privileges = new ArrayList<Privilege>();
1390 privileges.add(session.getAccessControlManager().privilegeFromName(privilege));
1391 addPrivileges(session, path, new SimplePrincipal(principal), privileges);
1392 }
1393
1394 /**
1395 * Add privileges on a path to a {@link Principal}. The path must already exist.
1396 * Session is saved. Synchronized to prevent concurrent modifications of the
1397 * same node.
1398 */
1399 public synchronized static Boolean addPrivileges(Session session, String path, Principal principal,
1400 List<Privilege> privs) throws RepositoryException {
1401 // make sure the session is in line with the persisted state
1402 session.refresh(false);
1403 AccessControlManager acm = session.getAccessControlManager();
1404 AccessControlList acl = getAccessControlList(acm, path);
1405
1406 accessControlEntries: for (AccessControlEntry ace : acl.getAccessControlEntries()) {
1407 Principal currentPrincipal = ace.getPrincipal();
1408 if (currentPrincipal.getName().equals(principal.getName())) {
1409 Privilege[] currentPrivileges = ace.getPrivileges();
1410 if (currentPrivileges.length != privs.size())
1411 break accessControlEntries;
1412 for (int i = 0; i < currentPrivileges.length; i++) {
1413 Privilege currP = currentPrivileges[i];
1414 Privilege p = privs.get(i);
1415 if (!currP.getName().equals(p.getName())) {
1416 break accessControlEntries;
1417 }
1418 }
1419 return false;
1420 }
1421 }
1422
1423 Privilege[] privileges = privs.toArray(new Privilege[privs.size()]);
1424 acl.addAccessControlEntry(principal, privileges);
1425 acm.setPolicy(path, acl);
1426 // if (log.isDebugEnabled()) {
1427 // StringBuffer privBuf = new StringBuffer();
1428 // for (Privilege priv : privs)
1429 // privBuf.append(priv.getName());
1430 // log.debug("Added privileges " + privBuf + " to " + principal.getName() + " on " + path + " in '"
1431 // + session.getWorkspace().getName() + "'");
1432 // }
1433 session.refresh(true);
1434 session.save();
1435 return true;
1436 }
1437
1438 /**
1439 * Gets the first available access control list for this path, throws exception
1440 * if not found
1441 */
1442 public synchronized static AccessControlList getAccessControlList(AccessControlManager acm, String path)
1443 throws RepositoryException {
1444 // search for an access control list
1445 AccessControlList acl = null;
1446 AccessControlPolicyIterator policyIterator = acm.getApplicablePolicies(path);
1447 applicablePolicies: if (policyIterator.hasNext()) {
1448 while (policyIterator.hasNext()) {
1449 AccessControlPolicy acp = policyIterator.nextAccessControlPolicy();
1450 if (acp instanceof AccessControlList) {
1451 acl = ((AccessControlList) acp);
1452 break applicablePolicies;
1453 }
1454 }
1455 } else {
1456 AccessControlPolicy[] existingPolicies = acm.getPolicies(path);
1457 existingPolicies: for (AccessControlPolicy acp : existingPolicies) {
1458 if (acp instanceof AccessControlList) {
1459 acl = ((AccessControlList) acp);
1460 break existingPolicies;
1461 }
1462 }
1463 }
1464 if (acl != null)
1465 return acl;
1466 else
1467 throw new IllegalArgumentException("ACL not found at " + path);
1468 }
1469
1470 /** Clear authorizations for a user at this path */
1471 public synchronized static void clearAccessControList(Session session, String path, String username)
1472 throws RepositoryException {
1473 AccessControlManager acm = session.getAccessControlManager();
1474 AccessControlList acl = getAccessControlList(acm, path);
1475 for (AccessControlEntry ace : acl.getAccessControlEntries()) {
1476 if (ace.getPrincipal().getName().equals(username)) {
1477 acl.removeAccessControlEntry(ace);
1478 }
1479 }
1480 // the new access control list must be applied otherwise this call:
1481 // acl.removeAccessControlEntry(ace); has no effect
1482 acm.setPolicy(path, acl);
1483 session.refresh(true);
1484 session.save();
1485 }
1486
1487 /*
1488 * FILES UTILITIES
1489 */
1490 /**
1491 * Creates the nodes making the path as {@link NodeType#NT_FOLDER}
1492 */
1493 public static Node mkfolders(Session session, String path) {
1494 return mkdirs(session, path, NodeType.NT_FOLDER, NodeType.NT_FOLDER, false);
1495 }
1496
1497 /**
1498 * Copy only nt:folder and nt:file, without their additional types and
1499 * properties.
1500 *
1501 * @param recursive if true copies folders as well, otherwise only first level
1502 * files
1503 * @return how many files were copied
1504 */
1505 public static Long copyFiles(Node fromNode, Node toNode, Boolean recursive, JcrMonitor monitor, boolean onlyAdd) {
1506 long count = 0l;
1507
1508 // Binary binary = null;
1509 // InputStream in = null;
1510 try {
1511 NodeIterator fromChildren = fromNode.getNodes();
1512 children: while (fromChildren.hasNext()) {
1513 if (monitor != null && monitor.isCanceled())
1514 throw new IllegalStateException("Copy cancelled before it was completed");
1515
1516 Node fromChild = fromChildren.nextNode();
1517 String fileName = fromChild.getName();
1518 if (fromChild.isNodeType(NodeType.NT_FILE)) {
1519 if (onlyAdd && toNode.hasNode(fileName)) {
1520 monitor.subTask("Skip existing " + fileName);
1521 continue children;
1522 }
1523
1524 if (monitor != null)
1525 monitor.subTask("Copy " + fileName);
1526 try (Bin binary = new Bin(fromChild.getNode(Node.JCR_CONTENT).getProperty(Property.JCR_DATA));
1527 InputStream in = binary.getStream();) {
1528 copyStreamAsFile(toNode, fileName, in);
1529 } catch (IOException e) {
1530 throw new RuntimeException("Cannot copy " + fileName + " to " + toNode, e);
1531 }
1532
1533 // save session
1534 toNode.getSession().save();
1535 count++;
1536
1537 // if (log.isDebugEnabled())
1538 // log.debug("Copied file " + fromChild.getPath());
1539 if (monitor != null)
1540 monitor.worked(1);
1541 } else if (fromChild.isNodeType(NodeType.NT_FOLDER) && recursive) {
1542 Node toChildFolder;
1543 if (toNode.hasNode(fileName)) {
1544 toChildFolder = toNode.getNode(fileName);
1545 if (!toChildFolder.isNodeType(NodeType.NT_FOLDER))
1546 throw new IllegalArgumentException(toChildFolder + " is not of type nt:folder");
1547 } else {
1548 toChildFolder = toNode.addNode(fileName, NodeType.NT_FOLDER);
1549
1550 // save session
1551 toNode.getSession().save();
1552 }
1553 count = count + copyFiles(fromChild, toChildFolder, recursive, monitor, onlyAdd);
1554 }
1555 }
1556 return count;
1557 } catch (RepositoryException e) {
1558 throw new JcrException("Cannot copy files between " + fromNode + " and " + toNode, e);
1559 } finally {
1560 // in case there was an exception
1561 // IOUtils.closeQuietly(in);
1562 // closeQuietly(binary);
1563 }
1564 }
1565
1566 /**
1567 * Iteratively count all file nodes in subtree, inefficient but can be useful
1568 * when query are poorly supported, such as in remoting.
1569 */
1570 public static Long countFiles(Node node) {
1571 Long localCount = 0l;
1572 try {
1573 for (NodeIterator nit = node.getNodes(); nit.hasNext();) {
1574 Node child = nit.nextNode();
1575 if (child.isNodeType(NodeType.NT_FOLDER))
1576 localCount = localCount + countFiles(child);
1577 else if (child.isNodeType(NodeType.NT_FILE))
1578 localCount = localCount + 1;
1579 }
1580 } catch (RepositoryException e) {
1581 throw new JcrException("Cannot count all children of " + node, e);
1582 }
1583 return localCount;
1584 }
1585
1586 /**
1587 * Copy a file as an nt:file, assuming an nt:folder hierarchy. The session is
1588 * NOT saved.
1589 *
1590 * @return the created file node
1591 */
1592 @Deprecated
1593 public static Node copyFile(Node folderNode, File file) {
1594 try (InputStream in = new FileInputStream(file)) {
1595 return copyStreamAsFile(folderNode, file.getName(), in);
1596 } catch (IOException e) {
1597 throw new RuntimeException("Cannot copy file " + file + " under " + folderNode, e);
1598 }
1599 }
1600
1601 /** Copy bytes as an nt:file */
1602 public static Node copyBytesAsFile(Node folderNode, String fileName, byte[] bytes) {
1603 // InputStream in = null;
1604 try (InputStream in = new ByteArrayInputStream(bytes)) {
1605 // in = new ByteArrayInputStream(bytes);
1606 return copyStreamAsFile(folderNode, fileName, in);
1607 } catch (IOException e) {
1608 throw new RuntimeException("Cannot copy file " + fileName + " under " + folderNode, e);
1609 // } finally {
1610 // IOUtils.closeQuietly(in);
1611 }
1612 }
1613
1614 /**
1615 * Copy a stream as an nt:file, assuming an nt:folder hierarchy. The session is
1616 * NOT saved.
1617 *
1618 * @return the created file node
1619 */
1620 public static Node copyStreamAsFile(Node folderNode, String fileName, InputStream in) {
1621 Binary binary = null;
1622 try {
1623 Node fileNode;
1624 Node contentNode;
1625 if (folderNode.hasNode(fileName)) {
1626 fileNode = folderNode.getNode(fileName);
1627 if (!fileNode.isNodeType(NodeType.NT_FILE))
1628 throw new IllegalArgumentException(fileNode + " is not of type nt:file");
1629 // we assume that the content node is already there
1630 contentNode = fileNode.getNode(Node.JCR_CONTENT);
1631 } else {
1632 fileNode = folderNode.addNode(fileName, NodeType.NT_FILE);
1633 contentNode = fileNode.addNode(Node.JCR_CONTENT, NodeType.NT_UNSTRUCTURED);
1634 }
1635 binary = contentNode.getSession().getValueFactory().createBinary(in);
1636 contentNode.setProperty(Property.JCR_DATA, binary);
1637 updateLastModified(contentNode);
1638 return fileNode;
1639 } catch (RepositoryException e) {
1640 throw new JcrException("Cannot create file node " + fileName + " under " + folderNode, e);
1641 } finally {
1642 closeQuietly(binary);
1643 }
1644 }
1645
1646 /** Read an an nt:file as an {@link InputStream}. */
1647 public static InputStream getFileAsStream(Node fileNode) throws RepositoryException {
1648 return fileNode.getNode(Node.JCR_CONTENT).getProperty(Property.JCR_DATA).getBinary().getStream();
1649 }
1650
1651 /**
1652 * Set the properties of {@link NodeType#MIX_MIMETYPE} on the content of this
1653 * file node.
1654 */
1655 public static void setFileMimeType(Node fileNode, String mimeType, String encoding) throws RepositoryException {
1656 Node contentNode = fileNode.getNode(Node.JCR_CONTENT);
1657 if (mimeType != null)
1658 contentNode.setProperty(Property.JCR_MIMETYPE, mimeType);
1659 if (encoding != null)
1660 contentNode.setProperty(Property.JCR_ENCODING, encoding);
1661 // TODO remove properties if args are null?
1662 }
1663
1664 public static void copyFilesToFs(Node baseNode, Path targetDir, boolean recursive) {
1665 try {
1666 Files.createDirectories(targetDir);
1667 for (NodeIterator nit = baseNode.getNodes(); nit.hasNext();) {
1668 Node node = nit.nextNode();
1669 if (node.isNodeType(NodeType.NT_FILE)) {
1670 Path filePath = targetDir.resolve(node.getName());
1671 try (OutputStream out = Files.newOutputStream(filePath); InputStream in = getFileAsStream(node)) {
1672 IOUtils.copy(in, out);
1673 }
1674 } else if (recursive && node.isNodeType(NodeType.NT_FOLDER)) {
1675 Path dirPath = targetDir.resolve(node.getName());
1676 copyFilesToFs(node, dirPath, true);
1677 }
1678 }
1679 } catch (RepositoryException e) {
1680 throw new JcrException("Cannot copy " + baseNode + " to " + targetDir, e);
1681 } catch (IOException e) {
1682 throw new RuntimeException("Cannot copy " + baseNode + " to " + targetDir, e);
1683 }
1684 }
1685
1686 /**
1687 * Computes the checksum of an nt:file.
1688 *
1689 * @deprecated use separate digest utilities
1690 */
1691 @Deprecated
1692 public static String checksumFile(Node fileNode, String algorithm) {
1693 try (InputStream in = fileNode.getNode(Node.JCR_CONTENT).getProperty(Property.JCR_DATA).getBinary()
1694 .getStream()) {
1695 return digest(algorithm, in);
1696 } catch (IOException e) {
1697 throw new RuntimeException("Cannot checksum file " + fileNode + " with algorithm " + algorithm, e);
1698 } catch (RepositoryException e) {
1699 throw new JcrException("Cannot checksum file " + fileNode + " with algorithm " + algorithm, e);
1700 }
1701 }
1702
1703 @Deprecated
1704 private static String digest(String algorithm, InputStream in) {
1705 final Integer byteBufferCapacity = 100 * 1024;// 100 KB
1706 try {
1707 MessageDigest digest = MessageDigest.getInstance(algorithm);
1708 byte[] buffer = new byte[byteBufferCapacity];
1709 int read = 0;
1710 while ((read = in.read(buffer)) > 0) {
1711 digest.update(buffer, 0, read);
1712 }
1713
1714 byte[] checksum = digest.digest();
1715 String res = encodeHexString(checksum);
1716 return res;
1717 } catch (IOException e) {
1718 throw new RuntimeException("Cannot digest with algorithm " + algorithm, e);
1719 } catch (NoSuchAlgorithmException e) {
1720 throw new IllegalArgumentException("Cannot digest with algorithm " + algorithm, e);
1721 }
1722 }
1723
1724 /**
1725 * From
1726 * http://stackoverflow.com/questions/9655181/how-to-convert-a-byte-array-to
1727 * -a-hex-string-in-java
1728 */
1729 @Deprecated
1730 private static String encodeHexString(byte[] bytes) {
1731 final char[] hexArray = "0123456789abcdef".toCharArray();
1732 char[] hexChars = new char[bytes.length * 2];
1733 for (int j = 0; j < bytes.length; j++) {
1734 int v = bytes[j] & 0xFF;
1735 hexChars[j * 2] = hexArray[v >>> 4];
1736 hexChars[j * 2 + 1] = hexArray[v & 0x0F];
1737 }
1738 return new String(hexChars);
1739 }
1740
1741 /** Export a subtree as a compact XML without namespaces. */
1742 public static void toSimpleXml(Node node, StringBuilder sb) throws RepositoryException {
1743 sb.append('<');
1744 String nodeName = node.getName();
1745 int colIndex = nodeName.indexOf(':');
1746 if (colIndex > 0) {
1747 nodeName = nodeName.substring(colIndex + 1);
1748 }
1749 sb.append(nodeName);
1750 PropertyIterator pit = node.getProperties();
1751 properties: while (pit.hasNext()) {
1752 Property p = pit.nextProperty();
1753 // skip multiple properties
1754 if (p.isMultiple())
1755 continue properties;
1756 String propertyName = p.getName();
1757 int pcolIndex = propertyName.indexOf(':');
1758 // skip properties with namespaces
1759 if (pcolIndex > 0)
1760 continue properties;
1761 // skip binaries
1762 if (p.getType() == PropertyType.BINARY) {
1763 continue properties;
1764 // TODO retrieve identifier?
1765 }
1766 sb.append(' ');
1767 sb.append(propertyName);
1768 sb.append('=');
1769 sb.append('\"').append(p.getString()).append('\"');
1770 }
1771
1772 if (node.hasNodes()) {
1773 sb.append('>');
1774 NodeIterator children = node.getNodes();
1775 while (children.hasNext()) {
1776 toSimpleXml(children.nextNode(), sb);
1777 }
1778 sb.append("</");
1779 sb.append(nodeName);
1780 sb.append('>');
1781 } else {
1782 sb.append("/>");
1783 }
1784 }
1785
1786 }