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