]> git.argeo.org Git - lgpl/argeo-commons.git/blob - server/runtime/org.argeo.server.jcr/src/main/java/org/argeo/jcr/JcrUtils.java
6dfb4a017f57da6870d9ad0a59c81ea11d3aae76
[lgpl/argeo-commons.git] / server / runtime / org.argeo.server.jcr / src / main / java / org / argeo / jcr / JcrUtils.java
1 /*
2 * Copyright (C) 2010 Mathieu Baudier <mbaudier@argeo.org>
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
17 package org.argeo.jcr;
18
19 import java.net.MalformedURLException;
20 import java.net.URL;
21 import java.text.DateFormat;
22 import java.text.ParseException;
23 import java.util.Calendar;
24 import java.util.Date;
25 import java.util.GregorianCalendar;
26 import java.util.HashMap;
27 import java.util.Iterator;
28 import java.util.List;
29 import java.util.Map;
30 import java.util.StringTokenizer;
31 import java.util.TreeMap;
32
33 import javax.jcr.Binary;
34 import javax.jcr.NamespaceRegistry;
35 import javax.jcr.Node;
36 import javax.jcr.NodeIterator;
37 import javax.jcr.Property;
38 import javax.jcr.PropertyIterator;
39 import javax.jcr.Repository;
40 import javax.jcr.RepositoryException;
41 import javax.jcr.RepositoryFactory;
42 import javax.jcr.Session;
43 import javax.jcr.Value;
44 import javax.jcr.Workspace;
45 import javax.jcr.nodetype.NodeType;
46 import javax.jcr.observation.EventListener;
47 import javax.jcr.query.Query;
48 import javax.jcr.query.QueryResult;
49 import javax.jcr.query.qom.Constraint;
50 import javax.jcr.query.qom.DynamicOperand;
51 import javax.jcr.query.qom.QueryObjectModelFactory;
52 import javax.jcr.query.qom.Selector;
53 import javax.jcr.query.qom.StaticOperand;
54
55 import org.apache.commons.logging.Log;
56 import org.apache.commons.logging.LogFactory;
57 import org.argeo.ArgeoException;
58
59 /** Utility methods to simplify common JCR operations. */
60 public class JcrUtils implements ArgeoJcrConstants {
61 private final static Log log = LogFactory.getLog(JcrUtils.class);
62
63 /**
64 * Not complete yet. See
65 * http://www.day.com/specs/jcr/2.0/3_Repository_Model.html#3.2.2%20Local
66 * %20Names
67 */
68 public final static char[] INVALID_NAME_CHARACTERS = { '/', ':', '[', ']',
69 '|', '*', /*
70 * invalid XML chars :
71 */
72 '<', '>', '&' };
73
74 /** Prevents instantiation */
75 private JcrUtils() {
76 }
77
78 /**
79 * Queries one single node.
80 *
81 * @return one single node or null if none was found
82 * @throws ArgeoException
83 * if more than one node was found
84 */
85 public static Node querySingleNode(Query query) {
86 NodeIterator nodeIterator;
87 try {
88 QueryResult queryResult = query.execute();
89 nodeIterator = queryResult.getNodes();
90 } catch (RepositoryException e) {
91 throw new ArgeoException("Cannot execute query " + query, e);
92 }
93 Node node;
94 if (nodeIterator.hasNext())
95 node = nodeIterator.nextNode();
96 else
97 return null;
98
99 if (nodeIterator.hasNext())
100 throw new ArgeoException("Query returned more than one node.");
101 return node;
102 }
103
104 /** Retrieves the parent path of the provided path */
105 public static String parentPath(String path) {
106 if (path.equals("/"))
107 throw new ArgeoException("Root path '/' has no parent path");
108 if (path.charAt(0) != '/')
109 throw new ArgeoException("Path " + path + " must start with a '/'");
110 String pathT = path;
111 if (pathT.charAt(pathT.length() - 1) == '/')
112 pathT = pathT.substring(0, pathT.length() - 2);
113
114 int index = pathT.lastIndexOf('/');
115 return pathT.substring(0, index);
116 }
117
118 /** The provided data as a path ('/' at the end, not the beginning) */
119 public static String dateAsPath(Calendar cal) {
120 return dateAsPath(cal, false);
121 }
122
123 /**
124 * Creates a deep path based on a URL:
125 * http://subdomain.example.com/to/content?args =>
126 * com/example/subdomain/to/content
127 */
128 public static String urlAsPath(String url) {
129 try {
130 URL u = new URL(url);
131 StringBuffer path = new StringBuffer(url.length());
132 // invert host
133 path.append(hostAsPath(u.getHost()));
134 // we don't put port since it may not always be there and may change
135 path.append(u.getPath());
136 return path.toString();
137 } catch (MalformedURLException e) {
138 throw new ArgeoException("Cannot generate URL path for " + url, e);
139 }
140 }
141
142 /**
143 * Creates a path from a FQDN, inverting the order of the component:
144 * www.argeo.org => org.argeo.www
145 */
146 public static String hostAsPath(String host) {
147 StringBuffer path = new StringBuffer(host.length());
148 String[] hostTokens = host.split("\\.");
149 for (int i = hostTokens.length - 1; i >= 0; i--) {
150 path.append(hostTokens[i]);
151 if (i != 0)
152 path.append('/');
153 }
154 return path.toString();
155 }
156
157 /**
158 * The provided data as a path ('/' at the end, not the beginning)
159 *
160 * @param cal
161 * the date
162 * @param addHour
163 * whether to add hour as well
164 */
165 public static String dateAsPath(Calendar cal, Boolean addHour) {
166 StringBuffer buf = new StringBuffer(14);
167 buf.append('Y');
168 buf.append(cal.get(Calendar.YEAR));
169 buf.append('/');
170
171 int month = cal.get(Calendar.MONTH) + 1;
172 buf.append('M');
173 if (month < 10)
174 buf.append(0);
175 buf.append(month);
176 buf.append('/');
177
178 int day = cal.get(Calendar.DAY_OF_MONTH);
179 buf.append('D');
180 if (day < 10)
181 buf.append(0);
182 buf.append(day);
183 buf.append('/');
184
185 if (addHour) {
186 int hour = cal.get(Calendar.HOUR_OF_DAY);
187 buf.append('H');
188 if (hour < 10)
189 buf.append(0);
190 buf.append(hour);
191 buf.append('/');
192 }
193 return buf.toString();
194
195 }
196
197 /** Converts in one call a string into a gregorian calendar. */
198 public static Calendar parseCalendar(DateFormat dateFormat, String value) {
199 try {
200 Date date = dateFormat.parse(value);
201 Calendar calendar = new GregorianCalendar();
202 calendar.setTime(date);
203 return calendar;
204 } catch (ParseException e) {
205 throw new ArgeoException("Cannot parse " + value
206 + " with date format " + dateFormat, e);
207 }
208
209 }
210
211 /** The last element of a path. */
212 public static String lastPathElement(String path) {
213 if (path.charAt(path.length() - 1) == '/')
214 throw new ArgeoException("Path " + path + " cannot end with '/'");
215 int index = path.lastIndexOf('/');
216 if (index < 0)
217 throw new ArgeoException("Cannot find last path element for "
218 + path);
219 return path.substring(index + 1);
220 }
221
222 /** Creates the nodes making path, if they don't exist. */
223 public static Node mkdirs(Session session, String path) {
224 return mkdirs(session, path, null, null, false);
225 }
226
227 /**
228 * use {@link #mkdirs(Session, String, String, String, Boolean)} instead.
229 *
230 * @deprecated
231 */
232 @Deprecated
233 public static Node mkdirs(Session session, String path, String type,
234 Boolean versioning) {
235 return mkdirs(session, path, type, type, false);
236 }
237
238 /**
239 * @param type
240 * the type of the leaf node
241 */
242 public static Node mkdirs(Session session, String path, String type) {
243 return mkdirs(session, path, type, null, false);
244 }
245
246 /**
247 * Creates the nodes making path, if they don't exist. This is up to the
248 * caller to save the session.
249 */
250 public static Node mkdirs(Session session, String path, String type,
251 String intermediaryNodeType, Boolean versioning) {
252 try {
253 if (path.equals('/'))
254 return session.getRootNode();
255
256 if (session.itemExists(path)) {
257 Node node = session.getNode(path);
258 // check type
259 if (type != null
260 && !type.equals(node.getPrimaryNodeType().getName()))
261 throw new ArgeoException("Node " + node
262 + " exists but is of type "
263 + node.getPrimaryNodeType().getName()
264 + " not of type " + type);
265 // TODO: check versioning
266 return node;
267 }
268
269 StringTokenizer st = new StringTokenizer(path, "/");
270 StringBuffer current = new StringBuffer("/");
271 Node currentNode = session.getRootNode();
272 while (st.hasMoreTokens()) {
273 String part = st.nextToken();
274 current.append(part).append('/');
275 if (!session.itemExists(current.toString())) {
276 if (!st.hasMoreTokens() && type != null)
277 currentNode = currentNode.addNode(part, type);
278 else if (st.hasMoreTokens() && intermediaryNodeType != null)
279 currentNode = currentNode.addNode(part,
280 intermediaryNodeType);
281 else
282 currentNode = currentNode.addNode(part);
283 if (versioning)
284 currentNode.addMixin(NodeType.MIX_VERSIONABLE);
285 if (log.isTraceEnabled())
286 log.debug("Added folder " + part + " as " + current);
287 } else {
288 currentNode = (Node) session.getItem(current.toString());
289 }
290 }
291 // session.save();
292 return currentNode;
293 } catch (RepositoryException e) {
294 throw new ArgeoException("Cannot mkdirs " + path, e);
295 }
296 }
297
298 /**
299 * Safe and repository implementation independent registration of a
300 * namespace.
301 */
302 public static void registerNamespaceSafely(Session session, String prefix,
303 String uri) {
304 try {
305 registerNamespaceSafely(session.getWorkspace()
306 .getNamespaceRegistry(), prefix, uri);
307 } catch (RepositoryException e) {
308 throw new ArgeoException("Cannot find namespace registry", e);
309 }
310 }
311
312 /**
313 * Safe and repository implementation independent registration of a
314 * namespace.
315 */
316 public static void registerNamespaceSafely(NamespaceRegistry nr,
317 String prefix, String uri) {
318 try {
319 String[] prefixes = nr.getPrefixes();
320 for (String pref : prefixes)
321 if (pref.equals(prefix)) {
322 String registeredUri = nr.getURI(pref);
323 if (!registeredUri.equals(uri))
324 throw new ArgeoException("Prefix " + pref
325 + " already registered for URI "
326 + registeredUri
327 + " which is different from provided URI "
328 + uri);
329 else
330 return;// skip
331 }
332 nr.registerNamespace(prefix, uri);
333 } catch (RepositoryException e) {
334 throw new ArgeoException("Cannot register namespace " + uri
335 + " under prefix " + prefix, e);
336 }
337 }
338
339 /** Recursively outputs the contents of the given node. */
340 public static void debug(Node node) {
341 debug(node, log);
342 }
343
344 /** Recursively outputs the contents of the given node. */
345 public static void debug(Node node, Log log) {
346 try {
347 // First output the node path
348 log.debug(node.getPath());
349 // Skip the virtual (and large!) jcr:system subtree
350 if (node.getName().equals("jcr:system")) {
351 return;
352 }
353
354 // Then the children nodes (recursive)
355 NodeIterator it = node.getNodes();
356 while (it.hasNext()) {
357 Node childNode = it.nextNode();
358 debug(childNode);
359 }
360
361 // Then output the properties
362 PropertyIterator properties = node.getProperties();
363 // log.debug("Property are : ");
364
365 while (properties.hasNext()) {
366 Property property = properties.nextProperty();
367 if (property.getDefinition().isMultiple()) {
368 // A multi-valued property, print all values
369 Value[] values = property.getValues();
370 for (int i = 0; i < values.length; i++) {
371 log.debug(property.getPath() + "="
372 + values[i].getString());
373 }
374 } else {
375 // A single-valued property
376 log.debug(property.getPath() + "=" + property.getString());
377 }
378 }
379 } catch (Exception e) {
380 log.error("Could not debug " + node, e);
381 }
382
383 }
384
385 /**
386 * Copies recursively the content of a node to another one. Do NOT copy the
387 * property values of {@link NodeType#MIX_CREATED} and
388 * {@link NodeType#MIX_LAST_MODIFIED}, but update the
389 * {@link Property#JCR_LAST_MODIFIED} and
390 * {@link Property#JCR_LAST_MODIFIED_BY} properties if the target node has
391 * the {@link NodeType#MIX_LAST_MODIFIED} mixin.
392 */
393 public static void copy(Node fromNode, Node toNode) {
394 try {
395 // process properties
396 PropertyIterator pit = fromNode.getProperties();
397 properties: while (pit.hasNext()) {
398 Property fromProperty = pit.nextProperty();
399 String propertyName = fromProperty.getName();
400 if (toNode.hasProperty(propertyName)
401 && toNode.getProperty(propertyName).getDefinition()
402 .isProtected())
403 continue properties;
404
405 if (fromProperty.getDefinition().isProtected())
406 continue properties;
407
408 if (propertyName.equals("jcr:created")
409 || propertyName.equals("jcr:createdBy")
410 || propertyName.equals("jcr:lastModified")
411 || propertyName.equals("jcr:lastModifiedBy"))
412 continue properties;
413
414 if (fromProperty.isMultiple()) {
415 toNode.setProperty(propertyName, fromProperty.getValues());
416 } else {
417 toNode.setProperty(propertyName, fromProperty.getValue());
418 }
419 }
420
421 // update jcr:lastModified and jcr:lastModifiedBy in toNode in case
422 // they existed, before adding the mixins
423 updateLastModified(toNode);
424
425 // add mixins
426 for (NodeType mixinType : fromNode.getMixinNodeTypes()) {
427 toNode.addMixin(mixinType.getName());
428 }
429
430 // process children nodes
431 NodeIterator nit = fromNode.getNodes();
432 while (nit.hasNext()) {
433 Node fromChild = nit.nextNode();
434 Integer index = fromChild.getIndex();
435 String nodeRelPath = fromChild.getName() + "[" + index + "]";
436 Node toChild;
437 if (toNode.hasNode(nodeRelPath))
438 toChild = toNode.getNode(nodeRelPath);
439 else
440 toChild = toNode.addNode(fromChild.getName(), fromChild
441 .getPrimaryNodeType().getName());
442 copy(fromChild, toChild);
443 }
444 } catch (RepositoryException e) {
445 throw new ArgeoException("Cannot copy " + fromNode + " to "
446 + toNode, e);
447 }
448 }
449
450 /**
451 * Check whether all first-level properties (except jcr:* properties) are
452 * equal. Skip jcr:* properties
453 */
454 public static Boolean allPropertiesEquals(Node reference, Node observed,
455 Boolean onlyCommonProperties) {
456 try {
457 PropertyIterator pit = reference.getProperties();
458 props: while (pit.hasNext()) {
459 Property propReference = pit.nextProperty();
460 String propName = propReference.getName();
461 if (propName.startsWith("jcr:"))
462 continue props;
463
464 if (!observed.hasProperty(propName))
465 if (onlyCommonProperties)
466 continue props;
467 else
468 return false;
469 // TODO: deal with multiple property values?
470 if (!observed.getProperty(propName).getValue()
471 .equals(propReference.getValue()))
472 return false;
473 }
474 return true;
475 } catch (RepositoryException e) {
476 throw new ArgeoException("Cannot check all properties equals of "
477 + reference + " and " + observed, e);
478 }
479 }
480
481 public static Map<String, PropertyDiff> diffProperties(Node reference,
482 Node observed) {
483 Map<String, PropertyDiff> diffs = new TreeMap<String, PropertyDiff>();
484 diffPropertiesLevel(diffs, null, reference, observed);
485 return diffs;
486 }
487
488 /**
489 * Compare the properties of two nodes. Recursivity to child nodes is not
490 * yet supported. Skip jcr:* properties.
491 */
492 static void diffPropertiesLevel(Map<String, PropertyDiff> diffs,
493 String baseRelPath, Node reference, Node observed) {
494 try {
495 // check removed and modified
496 PropertyIterator pit = reference.getProperties();
497 props: while (pit.hasNext()) {
498 Property p = pit.nextProperty();
499 String name = p.getName();
500 if (name.startsWith("jcr:"))
501 continue props;
502
503 if (!observed.hasProperty(name)) {
504 String relPath = propertyRelPath(baseRelPath, name);
505 PropertyDiff pDiff = new PropertyDiff(PropertyDiff.REMOVED,
506 relPath, p.getValue(), null);
507 diffs.put(relPath, pDiff);
508 } else {
509 if (p.isMultiple())
510 continue props;
511 Value referenceValue = p.getValue();
512 Value newValue = observed.getProperty(name).getValue();
513 if (!referenceValue.equals(newValue)) {
514 String relPath = propertyRelPath(baseRelPath, name);
515 PropertyDiff pDiff = new PropertyDiff(
516 PropertyDiff.MODIFIED, relPath, referenceValue,
517 newValue);
518 diffs.put(relPath, pDiff);
519 }
520 }
521 }
522 // check added
523 pit = observed.getProperties();
524 props: while (pit.hasNext()) {
525 Property p = pit.nextProperty();
526 String name = p.getName();
527 if (name.startsWith("jcr:"))
528 continue props;
529 if (!reference.hasProperty(name)) {
530 String relPath = propertyRelPath(baseRelPath, name);
531 PropertyDiff pDiff = new PropertyDiff(PropertyDiff.ADDED,
532 relPath, null, p.getValue());
533 diffs.put(relPath, pDiff);
534 }
535 }
536 } catch (RepositoryException e) {
537 throw new ArgeoException("Cannot diff " + reference + " and "
538 + observed, e);
539 }
540 }
541
542 /**
543 * Compare only a restricted list of properties of two nodes. No
544 * recursivity.
545 *
546 */
547 public static Map<String, PropertyDiff> diffProperties(Node reference,
548 Node observed, List<String> properties) {
549 Map<String, PropertyDiff> diffs = new TreeMap<String, PropertyDiff>();
550 try {
551 Iterator<String> pit = properties.iterator();
552
553 props: while (pit.hasNext()) {
554 String name = pit.next();
555 if (!reference.hasProperty(name)) {
556 if (!observed.hasProperty(name))
557 continue props;
558 Value val = observed.getProperty(name).getValue();
559 try {
560 // empty String but not null
561 if ("".equals(val.getString()))
562 continue props;
563 } catch (Exception e) {
564 // not parseable as String, silent
565 }
566 PropertyDiff pDiff = new PropertyDiff(PropertyDiff.ADDED,
567 name, null, val);
568 diffs.put(name, pDiff);
569 } else if (!observed.hasProperty(name)) {
570 PropertyDiff pDiff = new PropertyDiff(PropertyDiff.REMOVED,
571 name, reference.getProperty(name).getValue(), null);
572 diffs.put(name, pDiff);
573 } else {
574 Value referenceValue = reference.getProperty(name)
575 .getValue();
576 Value newValue = observed.getProperty(name).getValue();
577 if (!referenceValue.equals(newValue)) {
578 PropertyDiff pDiff = new PropertyDiff(
579 PropertyDiff.MODIFIED, name, referenceValue,
580 newValue);
581 diffs.put(name, pDiff);
582 }
583 }
584 }
585 } catch (RepositoryException e) {
586 throw new ArgeoException("Cannot diff " + reference + " and "
587 + observed, e);
588 }
589 return diffs;
590 }
591
592 /** Builds a property relPath to be used in the diff. */
593 private static String propertyRelPath(String baseRelPath,
594 String propertyName) {
595 if (baseRelPath == null)
596 return propertyName;
597 else
598 return baseRelPath + '/' + propertyName;
599 }
600
601 /**
602 * Normalizes a name so that it can be stored in contexts not supporting
603 * names with ':' (typically databases). Replaces ':' by '_'.
604 */
605 public static String normalize(String name) {
606 return name.replace(':', '_');
607 }
608
609 /**
610 * Replaces characters which are invalid in a JCR name by '_'. Currently not
611 * exhaustive.
612 *
613 * @see JcrUtils#INVALID_NAME_CHARACTERS
614 */
615 public static String replaceInvalidChars(String name) {
616 return replaceInvalidChars(name, '_');
617 }
618
619 /**
620 * Replaces characters which are invalid in a JCR name. Currently not
621 * exhaustive.
622 *
623 * @see JcrUtils#INVALID_NAME_CHARACTERS
624 */
625 public static String replaceInvalidChars(String name, char replacement) {
626 boolean modified = false;
627 char[] arr = name.toCharArray();
628 for (int i = 0; i < arr.length; i++) {
629 char c = arr[i];
630 invalid: for (char invalid : INVALID_NAME_CHARACTERS) {
631 if (c == invalid) {
632 arr[i] = replacement;
633 modified = true;
634 break invalid;
635 }
636 }
637 }
638 if (modified)
639 return new String(arr);
640 else
641 // do not create new object if unnecessary
642 return name;
643 }
644
645 /**
646 * Removes forbidden characters from a path, replacing them with '_'
647 *
648 * @deprecated use {@link #replaceInvalidChars(String)} instead
649 */
650 public static String removeForbiddenCharacters(String str) {
651 return str.replace('[', '_').replace(']', '_').replace('/', '_')
652 .replace('*', '_');
653
654 }
655
656 /** Cleanly disposes a {@link Binary} even if it is null. */
657 public static void closeQuietly(Binary binary) {
658 if (binary == null)
659 return;
660 binary.dispose();
661 }
662
663 /**
664 * Creates depth from a string (typically a username) by adding levels based
665 * on its first characters: "aBcD",2 => a/aB
666 */
667 public static String firstCharsToPath(String str, Integer nbrOfChars) {
668 if (str.length() < nbrOfChars)
669 throw new ArgeoException("String " + str
670 + " length must be greater or equal than " + nbrOfChars);
671 StringBuffer path = new StringBuffer("");
672 StringBuffer curr = new StringBuffer("");
673 for (int i = 0; i < nbrOfChars; i++) {
674 curr.append(str.charAt(i));
675 path.append(curr);
676 if (i < nbrOfChars - 1)
677 path.append('/');
678 }
679 return path.toString();
680 }
681
682 /**
683 * Wraps the call to the repository factory based on parameter
684 * {@link ArgeoJcrConstants#JCR_REPOSITORY_ALIAS} in order to simplify it
685 * and protect against future API changes.
686 */
687 public static Repository getRepositoryByAlias(
688 RepositoryFactory repositoryFactory, String alias) {
689 try {
690 Map<String, String> parameters = new HashMap<String, String>();
691 parameters.put(JCR_REPOSITORY_ALIAS, alias);
692 return repositoryFactory.getRepository(parameters);
693 } catch (RepositoryException e) {
694 throw new ArgeoException(
695 "Unexpected exception when trying to retrieve repository with alias "
696 + alias, e);
697 }
698 }
699
700 /**
701 * Wraps the call to the repository factory based on parameter
702 * {@link ArgeoJcrConstants#JCR_REPOSITORY_URI} in order to simplify it and
703 * protect against future API changes.
704 */
705 public static Repository getRepositoryByUri(
706 RepositoryFactory repositoryFactory, String uri) {
707 try {
708 Map<String, String> parameters = new HashMap<String, String>();
709 parameters.put(JCR_REPOSITORY_URI, uri);
710 return repositoryFactory.getRepository(parameters);
711 } catch (RepositoryException e) {
712 throw new ArgeoException(
713 "Unexpected exception when trying to retrieve repository with uri "
714 + uri, e);
715 }
716 }
717
718 /**
719 * Discards the current changes in the session attached to this node. To be
720 * used typically in a catch block.
721 *
722 * @see #discardQuietly(Session)
723 */
724 public static void discardUnderlyingSessionQuietly(Node node) {
725 try {
726 discardQuietly(node.getSession());
727 } catch (RepositoryException e) {
728 log.warn("Cannot quietly discard session of node " + node + ": "
729 + e.getMessage());
730 }
731 }
732
733 /**
734 * Discards the current changes in a session by calling
735 * {@link Session#refresh(boolean)} with <code>false</code>, only logging
736 * potential errors when doing so. To be used typically in a catch block.
737 */
738 public static void discardQuietly(Session session) {
739 try {
740 if (session != null)
741 session.refresh(false);
742 } catch (RepositoryException e) {
743 log.warn("Cannot quietly discard session " + session + ": "
744 + e.getMessage());
745 }
746 }
747
748 /** Logs out the session, not throwing any exception, even if it is null. */
749 public static void logoutQuietly(Session session) {
750 try {
751 if (session != null)
752 if (session.isLive())
753 session.logout();
754 } catch (Exception e) {
755 // silent
756 }
757 }
758
759 /** Returns the home node of the session user or null if none was found. */
760 public static Node getUserHome(Session session) {
761 String userID = session.getUserID();
762 return getUserHome(session, userID);
763 }
764
765 /**
766 * Returns user home has path, embedding exceptions. Contrary to
767 * {@link #getUserHome(Session)}, it never returns null but throws and
768 * exception if not found.
769 */
770 public static String getUserHomePath(Session session) {
771 String userID = session.getUserID();
772 try {
773 Node userHome = getUserHome(session, userID);
774 if (userHome != null)
775 return userHome.getPath();
776 else
777 throw new ArgeoException("No home registered for " + userID);
778 } catch (RepositoryException e) {
779 throw new ArgeoException("Cannot find user home path", e);
780 }
781 }
782
783 /** Get the profile of the user attached to this session. */
784 public static Node getUserProfile(Session session) {
785 String userID = session.getUserID();
786 return getUserProfile(session, userID);
787 }
788
789 /**
790 * Returns the home node of the session user or null if none was found.
791 *
792 * @param session
793 * the session to use in order to perform the search, this can be
794 * a session with a different user ID than the one searched,
795 * typically when a system or admin session is used.
796 * @param username
797 * the username of the user
798 */
799 public static Node getUserHome(Session session, String username) {
800 try {
801 QueryObjectModelFactory qomf = session.getWorkspace()
802 .getQueryManager().getQOMFactory();
803
804 // query the user home for this user id
805 Selector userHomeSel = qomf.selector(ArgeoTypes.ARGEO_USER_HOME,
806 "userHome");
807 DynamicOperand userIdDop = qomf.propertyValue("userHome",
808 ArgeoNames.ARGEO_USER_ID);
809 StaticOperand userIdSop = qomf.literal(session.getValueFactory()
810 .createValue(username));
811 Constraint constraint = qomf.comparison(userIdDop,
812 QueryObjectModelFactory.JCR_OPERATOR_EQUAL_TO, userIdSop);
813 Query query = qomf.createQuery(userHomeSel, constraint, null, null);
814 Node userHome = JcrUtils.querySingleNode(query);
815 return userHome;
816 } catch (RepositoryException e) {
817 throw new ArgeoException("Cannot find home for user " + username, e);
818 }
819 }
820
821 public static Node getUserProfile(Session session, String username) {
822 try {
823 QueryObjectModelFactory qomf = session.getWorkspace()
824 .getQueryManager().getQOMFactory();
825 Selector sel = qomf.selector(ArgeoTypes.ARGEO_USER_PROFILE,
826 "userProfile");
827 DynamicOperand userIdDop = qomf.propertyValue("userProfile",
828 ArgeoNames.ARGEO_USER_ID);
829 StaticOperand userIdSop = qomf.literal(session.getValueFactory()
830 .createValue(username));
831 Constraint constraint = qomf.comparison(userIdDop,
832 QueryObjectModelFactory.JCR_OPERATOR_EQUAL_TO, userIdSop);
833 Query query = qomf.createQuery(sel, constraint, null, null);
834 Node userHome = JcrUtils.querySingleNode(query);
835 return userHome;
836 } catch (RepositoryException e) {
837 throw new ArgeoException(
838 "Cannot find profile for user " + username, e);
839 }
840 }
841
842 /** Creates an Argeo user home. */
843 public static Node createUserHome(Session session, String homeBasePath,
844 String username) {
845 try {
846 if (session == null)
847 throw new ArgeoException("Session is null");
848 if (session.hasPendingChanges())
849 throw new ArgeoException(
850 "Session has pending changes, save them first");
851
852 String homePath = homeBasePath + '/'
853 + firstCharsToPath(username, 2) + '/' + username;
854
855 if (session.itemExists(homePath)) {
856 try {
857 throw new ArgeoException(
858 "Trying to create a user home that already exists");
859 } catch (Exception e) {
860 // we use this workaround to be sure to get the stack trace
861 // to identify the sink of the bug.
862 log.warn("trying to create an already existing userHome at path:"
863 + homePath + ". Stack trace : ");
864 e.printStackTrace();
865 }
866 }
867
868 Node userHome = JcrUtils.mkdirs(session, homePath);
869 Node userProfile;
870 if (userHome.hasNode(ArgeoNames.ARGEO_PROFILE)) {
871 log.warn("userProfile node already exists for userHome path: "
872 + homePath + ". We do not add a new one");
873 } else {
874 userProfile = userHome.addNode(ArgeoNames.ARGEO_PROFILE);
875 userProfile.addMixin(ArgeoTypes.ARGEO_USER_PROFILE);
876 userProfile.setProperty(ArgeoNames.ARGEO_USER_ID, username);
877 session.save();
878 // we need to save the profile before adding the user home type
879 }
880 userHome.addMixin(ArgeoTypes.ARGEO_USER_HOME);
881 // see
882 // http://jackrabbit.510166.n4.nabble.com/Jackrabbit-2-0-beta-6-Problem-adding-a-Mixin-type-with-mandatory-properties-after-setting-propertiesn-td1290332.html
883 userHome.setProperty(ArgeoNames.ARGEO_USER_ID, username);
884 session.save();
885 return userHome;
886 } catch (RepositoryException e) {
887 discardQuietly(session);
888 throw new ArgeoException("Cannot create home node for user "
889 + username, e);
890 }
891 }
892
893 /**
894 * Quietly unregisters an {@link EventListener} from the udnerlying
895 * workspace of this node.
896 */
897 public static void unregisterQuietly(Node node, EventListener eventListener) {
898 try {
899 unregisterQuietly(node.getSession().getWorkspace(), eventListener);
900 } catch (RepositoryException e) {
901 // silent
902 if (log.isTraceEnabled())
903 log.trace("Could not unregister event listener "
904 + eventListener);
905 }
906 }
907
908 /** Quietly unregisters an {@link EventListener} from this workspace */
909 public static void unregisterQuietly(Workspace workspace,
910 EventListener eventListener) {
911 if (eventListener == null)
912 return;
913 try {
914 workspace.getObservationManager()
915 .removeEventListener(eventListener);
916 } catch (RepositoryException e) {
917 // silent
918 if (log.isTraceEnabled())
919 log.trace("Could not unregister event listener "
920 + eventListener);
921 }
922 }
923
924 /**
925 * If this node is has the {@link NodeType#MIX_LAST_MODIFIED} mixin, it
926 * updates the {@link Property#JCR_LAST_MODIFIED} property with the current
927 * time and the {@link Property#JCR_LAST_MODIFIED_BY} property with the
928 * underlying session user id. In Jackrabbit 2.x, <a
929 * href="https://issues.apache.org/jira/browse/JCR-2233">these properties
930 * are not automatically updated</a>, hence the need for manual update. The
931 * session is not saved.
932 */
933 public static void updateLastModified(Node node) {
934 try {
935 if (node.isNodeType(NodeType.MIX_LAST_MODIFIED)) {
936 node.setProperty(Property.JCR_LAST_MODIFIED,
937 new GregorianCalendar());
938 node.setProperty(Property.JCR_LAST_MODIFIED_BY, node
939 .getSession().getUserID());
940 }
941 } catch (RepositoryException e) {
942 throw new ArgeoException("Cannot update last modified", e);
943 }
944 }
945
946 /**
947 * Returns a String representing the short version (see <a
948 * href="http://jackrabbit.apache.org/node-type-notation.html"> Node type
949 * Notation </a> attributes grammar) of the main business attributes of this
950 * property definition
951 *
952 * @param prop
953 */
954 public static String getPropertyDefinitionAsString(Property prop) {
955 StringBuffer sbuf = new StringBuffer();
956 try {
957 if (prop.getDefinition().isAutoCreated())
958 sbuf.append("a");
959 if (prop.getDefinition().isMandatory())
960 sbuf.append("m");
961 if (prop.getDefinition().isProtected())
962 sbuf.append("p");
963 if (prop.getDefinition().isMultiple())
964 sbuf.append("*");
965 } catch (RepositoryException re) {
966 throw new ArgeoException(
967 "unexpected error while getting property definition as String",
968 re);
969 }
970 return sbuf.toString();
971 }
972
973 /**
974 * Estimate the sub tree size from current node. Computation is based on the
975 * Jcr {@link Property.getLength()} method. Note : it is not the exact size
976 * used on the disk by the current part of the JCR Tree.
977 */
978
979 public static long getNodeApproxSize(Node node) {
980 long curNodeSize = 0;
981 try {
982 PropertyIterator pi = node.getProperties();
983 while (pi.hasNext()) {
984 Property prop = pi.nextProperty();
985 if (prop.isMultiple()) {
986 int nb = prop.getLengths().length;
987 for (int i = 0; i < nb; i++) {
988 curNodeSize += (prop.getLengths()[i] > 0 ? prop
989 .getLengths()[i] : 0);
990 }
991 } else
992 curNodeSize += (prop.getLength() > 0 ? prop.getLength() : 0);
993 }
994
995 NodeIterator ni = node.getNodes();
996 while (ni.hasNext())
997 curNodeSize += getNodeApproxSize(ni.nextNode());
998 return curNodeSize;
999 } catch (RepositoryException re) {
1000 throw new ArgeoException(
1001 "Unexpected error while recursively determining node size.",
1002 re);
1003 }
1004 }
1005 }