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