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