]> git.argeo.org Git - lgpl/argeo-commons.git/blob - server/runtime/org.argeo.server.jcr/src/main/java/org/argeo/jcr/JcrUtils.java
Introduce Tabular Content
[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 * @deprecated use {@link #mkdirs(Session, String, String, String, Boolean)}
229 * instead.
230 */
231 @Deprecated
232 public static Node mkdirs(Session session, String path, String type,
233 Boolean versioning) {
234 return mkdirs(session, path, type, type, false);
235 }
236
237 /**
238 * @param type
239 * the type of the leaf node
240 */
241 public static Node mkdirs(Session session, String path, String type) {
242 return mkdirs(session, path, type, null, false);
243 }
244
245 /**
246 * Creates the nodes making path, if they don't exist. This is up to the
247 * caller to save the session.
248 */
249 public static Node mkdirs(Session session, String path, String type,
250 String intermediaryNodeType, Boolean versioning) {
251 try {
252 if (path.equals('/'))
253 return session.getRootNode();
254
255 if (session.itemExists(path)) {
256 Node node = session.getNode(path);
257 // check type
258 if (type != null
259 && !type.equals(node.getPrimaryNodeType().getName()))
260 throw new ArgeoException("Node " + node
261 + " exists but is of type "
262 + node.getPrimaryNodeType().getName()
263 + " not of type " + type);
264 // TODO: check versioning
265 return node;
266 }
267
268 StringTokenizer st = new StringTokenizer(path, "/");
269 StringBuffer current = new StringBuffer("/");
270 Node currentNode = session.getRootNode();
271 while (st.hasMoreTokens()) {
272 String part = st.nextToken();
273 current.append(part).append('/');
274 if (!session.itemExists(current.toString())) {
275 if (!st.hasMoreTokens() && type != null)
276 currentNode = currentNode.addNode(part, type);
277 else if (st.hasMoreTokens() && intermediaryNodeType != null)
278 currentNode = currentNode.addNode(part,
279 intermediaryNodeType);
280 else
281 currentNode = currentNode.addNode(part);
282 if (versioning)
283 currentNode.addMixin(NodeType.MIX_VERSIONABLE);
284 if (log.isTraceEnabled())
285 log.debug("Added folder " + part + " as " + current);
286 } else {
287 currentNode = (Node) session.getItem(current.toString());
288 }
289 }
290 // session.save();
291 return currentNode;
292 } catch (RepositoryException e) {
293 throw new ArgeoException("Cannot mkdirs " + path, e);
294 }
295 }
296
297 /**
298 * Safe and repository implementation independent registration of a
299 * namespace.
300 */
301 public static void registerNamespaceSafely(Session session, String prefix,
302 String uri) {
303 try {
304 registerNamespaceSafely(session.getWorkspace()
305 .getNamespaceRegistry(), prefix, uri);
306 } catch (RepositoryException e) {
307 throw new ArgeoException("Cannot find namespace registry", e);
308 }
309 }
310
311 /**
312 * Safe and repository implementation independent registration of a
313 * namespace.
314 */
315 public static void registerNamespaceSafely(NamespaceRegistry nr,
316 String prefix, String uri) {
317 try {
318 String[] prefixes = nr.getPrefixes();
319 for (String pref : prefixes)
320 if (pref.equals(prefix)) {
321 String registeredUri = nr.getURI(pref);
322 if (!registeredUri.equals(uri))
323 throw new ArgeoException("Prefix " + pref
324 + " already registered for URI "
325 + registeredUri
326 + " which is different from provided URI "
327 + uri);
328 else
329 return;// skip
330 }
331 nr.registerNamespace(prefix, uri);
332 } catch (RepositoryException e) {
333 throw new ArgeoException("Cannot register namespace " + uri
334 + " under prefix " + prefix, e);
335 }
336 }
337
338 /** Recursively outputs the contents of the given node. */
339 public static void debug(Node node) {
340 debug(node, log);
341 }
342
343 /** Recursively outputs the contents of the given node. */
344 public static void debug(Node node, Log log) {
345 try {
346 // First output the node path
347 log.debug(node.getPath());
348 // Skip the virtual (and large!) jcr:system subtree
349 if (node.getName().equals("jcr:system")) {
350 return;
351 }
352
353 // Then the children nodes (recursive)
354 NodeIterator it = node.getNodes();
355 while (it.hasNext()) {
356 Node childNode = it.nextNode();
357 debug(childNode);
358 }
359
360 // Then output the properties
361 PropertyIterator properties = node.getProperties();
362 // log.debug("Property are : ");
363
364 while (properties.hasNext()) {
365 Property property = properties.nextProperty();
366 if (property.getDefinition().isMultiple()) {
367 // A multi-valued property, print all values
368 Value[] values = property.getValues();
369 for (int i = 0; i < values.length; i++) {
370 log.debug(property.getPath() + "="
371 + values[i].getString());
372 }
373 } else {
374 // A single-valued property
375 log.debug(property.getPath() + "=" + property.getString());
376 }
377 }
378 } catch (Exception e) {
379 log.error("Could not debug " + node, e);
380 }
381
382 }
383
384 /**
385 * Copies recursively the content of a node to another one. Do NOT copy the
386 * property values of {@link NodeType#MIX_CREATED} and
387 * {@link NodeType#MIX_LAST_MODIFIED}, but update the
388 * {@link Property#JCR_LAST_MODIFIED} and
389 * {@link Property#JCR_LAST_MODIFIED_BY} properties if the target node has
390 * the {@link NodeType#MIX_LAST_MODIFIED} mixin.
391 */
392 public static void copy(Node fromNode, Node toNode) {
393 try {
394 // process properties
395 PropertyIterator pit = fromNode.getProperties();
396 properties: while (pit.hasNext()) {
397 Property fromProperty = pit.nextProperty();
398 String propertyName = fromProperty.getName();
399 if (toNode.hasProperty(propertyName)
400 && toNode.getProperty(propertyName).getDefinition()
401 .isProtected())
402 continue properties;
403
404 if (fromProperty.getDefinition().isProtected())
405 continue properties;
406
407 if (propertyName.equals("jcr:created")
408 || propertyName.equals("jcr:createdBy")
409 || propertyName.equals("jcr:lastModified")
410 || propertyName.equals("jcr:lastModifiedBy"))
411 continue properties;
412
413 if (fromProperty.isMultiple()) {
414 toNode.setProperty(propertyName, fromProperty.getValues());
415 } else {
416 toNode.setProperty(propertyName, fromProperty.getValue());
417 }
418 }
419
420 // update jcr:lastModified and jcr:lastModifiedBy in toNode in case
421 // they existed, before adding the mixins
422 updateLastModified(toNode);
423
424 // add mixins
425 for (NodeType mixinType : fromNode.getMixinNodeTypes()) {
426 toNode.addMixin(mixinType.getName());
427 }
428
429 // process children nodes
430 NodeIterator nit = fromNode.getNodes();
431 while (nit.hasNext()) {
432 Node fromChild = nit.nextNode();
433 Integer index = fromChild.getIndex();
434 String nodeRelPath = fromChild.getName() + "[" + index + "]";
435 Node toChild;
436 if (toNode.hasNode(nodeRelPath))
437 toChild = toNode.getNode(nodeRelPath);
438 else
439 toChild = toNode.addNode(fromChild.getName(), fromChild
440 .getPrimaryNodeType().getName());
441 copy(fromChild, toChild);
442 }
443 } catch (RepositoryException e) {
444 throw new ArgeoException("Cannot copy " + fromNode + " to "
445 + toNode, e);
446 }
447 }
448
449 /**
450 * Check whether all first-level properties (except jcr:* properties) are
451 * equal. Skip jcr:* properties
452 */
453 public static Boolean allPropertiesEquals(Node reference, Node observed,
454 Boolean onlyCommonProperties) {
455 try {
456 PropertyIterator pit = reference.getProperties();
457 props: while (pit.hasNext()) {
458 Property propReference = pit.nextProperty();
459 String propName = propReference.getName();
460 if (propName.startsWith("jcr:"))
461 continue props;
462
463 if (!observed.hasProperty(propName))
464 if (onlyCommonProperties)
465 continue props;
466 else
467 return false;
468 // TODO: deal with multiple property values?
469 if (!observed.getProperty(propName).getValue()
470 .equals(propReference.getValue()))
471 return false;
472 }
473 return true;
474 } catch (RepositoryException e) {
475 throw new ArgeoException("Cannot check all properties equals of "
476 + reference + " and " + observed, e);
477 }
478 }
479
480 public static Map<String, PropertyDiff> diffProperties(Node reference,
481 Node observed) {
482 Map<String, PropertyDiff> diffs = new TreeMap<String, PropertyDiff>();
483 diffPropertiesLevel(diffs, null, reference, observed);
484 return diffs;
485 }
486
487 /**
488 * Compare the properties of two nodes. Recursivity to child nodes is not
489 * yet supported. Skip jcr:* properties.
490 */
491 static void diffPropertiesLevel(Map<String, PropertyDiff> diffs,
492 String baseRelPath, Node reference, Node observed) {
493 try {
494 // check removed and modified
495 PropertyIterator pit = reference.getProperties();
496 props: while (pit.hasNext()) {
497 Property p = pit.nextProperty();
498 String name = p.getName();
499 if (name.startsWith("jcr:"))
500 continue props;
501
502 if (!observed.hasProperty(name)) {
503 String relPath = propertyRelPath(baseRelPath, name);
504 PropertyDiff pDiff = new PropertyDiff(PropertyDiff.REMOVED,
505 relPath, p.getValue(), null);
506 diffs.put(relPath, pDiff);
507 } else {
508 if (p.isMultiple())
509 continue props;
510 Value referenceValue = p.getValue();
511 Value newValue = observed.getProperty(name).getValue();
512 if (!referenceValue.equals(newValue)) {
513 String relPath = propertyRelPath(baseRelPath, name);
514 PropertyDiff pDiff = new PropertyDiff(
515 PropertyDiff.MODIFIED, relPath, referenceValue,
516 newValue);
517 diffs.put(relPath, pDiff);
518 }
519 }
520 }
521 // check added
522 pit = observed.getProperties();
523 props: while (pit.hasNext()) {
524 Property p = pit.nextProperty();
525 String name = p.getName();
526 if (name.startsWith("jcr:"))
527 continue props;
528 if (!reference.hasProperty(name)) {
529 String relPath = propertyRelPath(baseRelPath, name);
530 PropertyDiff pDiff = new PropertyDiff(PropertyDiff.ADDED,
531 relPath, null, p.getValue());
532 diffs.put(relPath, pDiff);
533 }
534 }
535 } catch (RepositoryException e) {
536 throw new ArgeoException("Cannot diff " + reference + " and "
537 + observed, e);
538 }
539 }
540
541 /**
542 * Compare only a restricted list of properties of two nodes. No
543 * recursivity.
544 *
545 */
546 public static Map<String, PropertyDiff> diffProperties(Node reference,
547 Node observed, List<String> properties) {
548 Map<String, PropertyDiff> diffs = new TreeMap<String, PropertyDiff>();
549 try {
550 Iterator<String> pit = properties.iterator();
551
552 props: while (pit.hasNext()) {
553 String name = pit.next();
554 if (!reference.hasProperty(name)) {
555 if (!observed.hasProperty(name))
556 continue props;
557 Value val = observed.getProperty(name).getValue();
558 try {
559 // empty String but not null
560 if ("".equals(val.getString()))
561 continue props;
562 } catch (Exception e) {
563 // not parseable as String, silent
564 }
565 PropertyDiff pDiff = new PropertyDiff(PropertyDiff.ADDED,
566 name, null, val);
567 diffs.put(name, pDiff);
568 } else if (!observed.hasProperty(name)) {
569 PropertyDiff pDiff = new PropertyDiff(PropertyDiff.REMOVED,
570 name, reference.getProperty(name).getValue(), null);
571 diffs.put(name, pDiff);
572 } else {
573 Value referenceValue = reference.getProperty(name)
574 .getValue();
575 Value newValue = observed.getProperty(name).getValue();
576 if (!referenceValue.equals(newValue)) {
577 PropertyDiff pDiff = new PropertyDiff(
578 PropertyDiff.MODIFIED, name, referenceValue,
579 newValue);
580 diffs.put(name, pDiff);
581 }
582 }
583 }
584 } catch (RepositoryException e) {
585 throw new ArgeoException("Cannot diff " + reference + " and "
586 + observed, e);
587 }
588 return diffs;
589 }
590
591 /** Builds a property relPath to be used in the diff. */
592 private static String propertyRelPath(String baseRelPath,
593 String propertyName) {
594 if (baseRelPath == null)
595 return propertyName;
596 else
597 return baseRelPath + '/' + propertyName;
598 }
599
600 /**
601 * Normalizes a name so that it can be stored in contexts not supporting
602 * names with ':' (typically databases). Replaces ':' by '_'.
603 */
604 public static String normalize(String name) {
605 return name.replace(':', '_');
606 }
607
608 /**
609 * Replaces characters which are invalid in a JCR name by '_'. Currently not
610 * exhaustive.
611 *
612 * @see JcrUtils#INVALID_NAME_CHARACTERS
613 */
614 public static String replaceInvalidChars(String name) {
615 return replaceInvalidChars(name, '_');
616 }
617
618 /**
619 * Replaces characters which are invalid in a JCR name. Currently not
620 * exhaustive.
621 *
622 * @see JcrUtils#INVALID_NAME_CHARACTERS
623 */
624 public static String replaceInvalidChars(String name, char replacement) {
625 boolean modified = false;
626 char[] arr = name.toCharArray();
627 for (int i = 0; i < arr.length; i++) {
628 char c = arr[i];
629 invalid: for (char invalid : INVALID_NAME_CHARACTERS) {
630 if (c == invalid) {
631 arr[i] = replacement;
632 modified = true;
633 break invalid;
634 }
635 }
636 }
637 if (modified)
638 return new String(arr);
639 else
640 // do not create new object if unnecessary
641 return name;
642 }
643
644 /**
645 * Removes forbidden characters from a path, replacing them with '_'
646 *
647 * @deprecated use {@link #replaceInvalidChars(String)} instead
648 */
649 public static String removeForbiddenCharacters(String str) {
650 return str.replace('[', '_').replace(']', '_').replace('/', '_')
651 .replace('*', '_');
652
653 }
654
655 /** Cleanly disposes a {@link Binary} even if it is null. */
656 public static void closeQuietly(Binary binary) {
657 if (binary == null)
658 return;
659 binary.dispose();
660 }
661
662 /**
663 * Creates depth from a string (typically a username) by adding levels based
664 * on its first characters: "aBcD",2 => a/aB
665 */
666 public static String firstCharsToPath(String str, Integer nbrOfChars) {
667 if (str.length() < nbrOfChars)
668 throw new ArgeoException("String " + str
669 + " length must be greater or equal than " + nbrOfChars);
670 StringBuffer path = new StringBuffer("");
671 StringBuffer curr = new StringBuffer("");
672 for (int i = 0; i < nbrOfChars; i++) {
673 curr.append(str.charAt(i));
674 path.append(curr);
675 if (i < nbrOfChars - 1)
676 path.append('/');
677 }
678 return path.toString();
679 }
680
681 /**
682 * Wraps the call to the repository factory based on parameter
683 * {@link ArgeoJcrConstants#JCR_REPOSITORY_ALIAS} in order to simplify it
684 * and protect against future API changes.
685 */
686 public static Repository getRepositoryByAlias(
687 RepositoryFactory repositoryFactory, String alias) {
688 try {
689 Map<String, String> parameters = new HashMap<String, String>();
690 parameters.put(JCR_REPOSITORY_ALIAS, alias);
691 return repositoryFactory.getRepository(parameters);
692 } catch (RepositoryException e) {
693 throw new ArgeoException(
694 "Unexpected exception when trying to retrieve repository with alias "
695 + alias, e);
696 }
697 }
698
699 /**
700 * Wraps the call to the repository factory based on parameter
701 * {@link ArgeoJcrConstants#JCR_REPOSITORY_URI} in order to simplify it and
702 * protect against future API changes.
703 */
704 public static Repository getRepositoryByUri(
705 RepositoryFactory repositoryFactory, String uri) {
706 try {
707 Map<String, String> parameters = new HashMap<String, String>();
708 parameters.put(JCR_REPOSITORY_URI, uri);
709 return repositoryFactory.getRepository(parameters);
710 } catch (RepositoryException e) {
711 throw new ArgeoException(
712 "Unexpected exception when trying to retrieve repository with uri "
713 + uri, e);
714 }
715 }
716
717 /**
718 * Discards the current changes in the session attached to this node. To be
719 * used typically in a catch block.
720 *
721 * @see #discardQuietly(Session)
722 */
723 public static void discardUnderlyingSessionQuietly(Node node) {
724 try {
725 discardQuietly(node.getSession());
726 } catch (RepositoryException e) {
727 log.warn("Cannot quietly discard session of node " + node + ": "
728 + e.getMessage());
729 }
730 }
731
732 /**
733 * Discards the current changes in a session by calling
734 * {@link Session#refresh(boolean)} with <code>false</code>, only logging
735 * potential errors when doing so. To be used typically in a catch block.
736 */
737 public static void discardQuietly(Session session) {
738 try {
739 if (session != null)
740 session.refresh(false);
741 } catch (RepositoryException e) {
742 log.warn("Cannot quietly discard session " + session + ": "
743 + e.getMessage());
744 }
745 }
746
747 /** Logs out the session, not throwing any exception, even if it is null. */
748 public static void logoutQuietly(Session session) {
749 try {
750 if (session != null)
751 if (session.isLive())
752 session.logout();
753 } catch (Exception e) {
754 // silent
755 }
756 }
757
758 /** Returns the home node of the session user or null if none was found. */
759 public static Node getUserHome(Session session) {
760 String userID = session.getUserID();
761 return getUserHome(session, userID);
762 }
763
764 /**
765 * Returns user home has path, embedding exceptions. Contrary to
766 * {@link #getUserHome(Session)}, it never returns null but throws and
767 * exception if not found.
768 */
769 public static String getUserHomePath(Session session) {
770 String userID = session.getUserID();
771 try {
772 Node userHome = getUserHome(session, userID);
773 if (userHome != null)
774 return userHome.getPath();
775 else
776 throw new ArgeoException("No home registered for " + userID);
777 } catch (RepositoryException e) {
778 throw new ArgeoException("Cannot find user home path", e);
779 }
780 }
781
782 /** Get the profile of the user attached to this session. */
783 public static Node getUserProfile(Session session) {
784 String userID = session.getUserID();
785 return getUserProfile(session, userID);
786 }
787
788 /**
789 * Returns the home node of the session user or null if none was found.
790 *
791 * @param session
792 * the session to use in order to perform the search, this can be
793 * a session with a different user ID than the one searched,
794 * typically when a system or admin session is used.
795 * @param username
796 * the username of the user
797 */
798 public static Node getUserHome(Session session, String username) {
799 try {
800 QueryObjectModelFactory qomf = session.getWorkspace()
801 .getQueryManager().getQOMFactory();
802
803 // query the user home for this user id
804 Selector userHomeSel = qomf.selector(ArgeoTypes.ARGEO_USER_HOME,
805 "userHome");
806 DynamicOperand userIdDop = qomf.propertyValue("userHome",
807 ArgeoNames.ARGEO_USER_ID);
808 StaticOperand userIdSop = qomf.literal(session.getValueFactory()
809 .createValue(username));
810 Constraint constraint = qomf.comparison(userIdDop,
811 QueryObjectModelFactory.JCR_OPERATOR_EQUAL_TO, userIdSop);
812 Query query = qomf.createQuery(userHomeSel, constraint, null, null);
813 Node userHome = JcrUtils.querySingleNode(query);
814 return userHome;
815 } catch (RepositoryException e) {
816 throw new ArgeoException("Cannot find home for user " + username, e);
817 }
818 }
819
820 public static Node getUserProfile(Session session, String username) {
821 try {
822 QueryObjectModelFactory qomf = session.getWorkspace()
823 .getQueryManager().getQOMFactory();
824 Selector sel = qomf.selector(ArgeoTypes.ARGEO_USER_PROFILE,
825 "userProfile");
826 DynamicOperand userIdDop = qomf.propertyValue("userProfile",
827 ArgeoNames.ARGEO_USER_ID);
828 StaticOperand userIdSop = qomf.literal(session.getValueFactory()
829 .createValue(username));
830 Constraint constraint = qomf.comparison(userIdDop,
831 QueryObjectModelFactory.JCR_OPERATOR_EQUAL_TO, userIdSop);
832 Query query = qomf.createQuery(sel, constraint, null, null);
833 Node userHome = JcrUtils.querySingleNode(query);
834 return userHome;
835 } catch (RepositoryException e) {
836 throw new ArgeoException(
837 "Cannot find profile for user " + username, e);
838 }
839 }
840
841 /** Creates an Argeo user home. */
842 public static Node createUserHome(Session session, String homeBasePath,
843 String username) {
844 try {
845 if (session == null)
846 throw new ArgeoException("Session is null");
847 if (session.hasPendingChanges())
848 throw new ArgeoException(
849 "Session has pending changes, save them first");
850
851 String homePath = homeBasePath + '/'
852 + firstCharsToPath(username, 2) + '/' + username;
853
854 if (session.itemExists(homePath)) {
855 try {
856 throw new ArgeoException(
857 "Trying to create a user home that already exists");
858 } catch (Exception e) {
859 // we use this workaround to be sure to get the stack trace
860 // to identify the sink of the bug.
861 log.warn("trying to create an already existing userHome at path:"
862 + homePath + ". Stack trace : ");
863 e.printStackTrace();
864 }
865 }
866
867 Node userHome = JcrUtils.mkdirs(session, homePath);
868 Node userProfile;
869 if (userHome.hasNode(ArgeoNames.ARGEO_PROFILE)) {
870 log.warn("userProfile node already exists for userHome path: "
871 + homePath + ". We do not add a new one");
872 } else {
873 userProfile = userHome.addNode(ArgeoNames.ARGEO_PROFILE);
874 userProfile.addMixin(ArgeoTypes.ARGEO_USER_PROFILE);
875 userProfile.setProperty(ArgeoNames.ARGEO_USER_ID, username);
876 session.save();
877 // we need to save the profile before adding the user home type
878 }
879 userHome.addMixin(ArgeoTypes.ARGEO_USER_HOME);
880 // see
881 // http://jackrabbit.510166.n4.nabble.com/Jackrabbit-2-0-beta-6-Problem-adding-a-Mixin-type-with-mandatory-properties-after-setting-propertiesn-td1290332.html
882 userHome.setProperty(ArgeoNames.ARGEO_USER_ID, username);
883 session.save();
884 return userHome;
885 } catch (RepositoryException e) {
886 discardQuietly(session);
887 throw new ArgeoException("Cannot create home node for user "
888 + username, e);
889 }
890 }
891
892 /**
893 * Quietly unregisters an {@link EventListener} from the udnerlying
894 * workspace of this node.
895 */
896 public static void unregisterQuietly(Node node, EventListener eventListener) {
897 try {
898 unregisterQuietly(node.getSession().getWorkspace(), eventListener);
899 } catch (RepositoryException e) {
900 // silent
901 if (log.isTraceEnabled())
902 log.trace("Could not unregister event listener "
903 + eventListener);
904 }
905 }
906
907 /** Quietly unregisters an {@link EventListener} from this workspace */
908 public static void unregisterQuietly(Workspace workspace,
909 EventListener eventListener) {
910 if (eventListener == null)
911 return;
912 try {
913 workspace.getObservationManager()
914 .removeEventListener(eventListener);
915 } catch (RepositoryException e) {
916 // silent
917 if (log.isTraceEnabled())
918 log.trace("Could not unregister event listener "
919 + eventListener);
920 }
921 }
922
923 /**
924 * If this node is has the {@link NodeType#MIX_LAST_MODIFIED} mixin, it
925 * updates the {@link Property#JCR_LAST_MODIFIED} property with the current
926 * time and the {@link Property#JCR_LAST_MODIFIED_BY} property with the
927 * underlying session user id. In Jackrabbit 2.x, <a
928 * href="https://issues.apache.org/jira/browse/JCR-2233">these properties
929 * are not automatically updated</a>, hence the need for manual update. The
930 * session is not saved.
931 */
932 public static void updateLastModified(Node node) {
933 try {
934 if (node.isNodeType(NodeType.MIX_LAST_MODIFIED)) {
935 node.setProperty(Property.JCR_LAST_MODIFIED,
936 new GregorianCalendar());
937 node.setProperty(Property.JCR_LAST_MODIFIED_BY, node
938 .getSession().getUserID());
939 }
940 } catch (RepositoryException e) {
941 throw new ArgeoException("Cannot update last modified", e);
942 }
943 }
944
945 /**
946 * Returns a String representing the short version (see <a
947 * href="http://jackrabbit.apache.org/node-type-notation.html"> Node type
948 * Notation </a> attributes grammar) of the main business attributes of this
949 * property definition
950 *
951 * @param prop
952 */
953 public static String getPropertyDefinitionAsString(Property prop) {
954 StringBuffer sbuf = new StringBuffer();
955 try {
956 if (prop.getDefinition().isAutoCreated())
957 sbuf.append("a");
958 if (prop.getDefinition().isMandatory())
959 sbuf.append("m");
960 if (prop.getDefinition().isProtected())
961 sbuf.append("p");
962 if (prop.getDefinition().isMultiple())
963 sbuf.append("*");
964 } catch (RepositoryException re) {
965 throw new ArgeoException(
966 "unexpected error while getting property definition as String",
967 re);
968 }
969 return sbuf.toString();
970 }
971 }