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