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