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