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