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