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