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