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