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