2 * Copyright (C) 2010 Mathieu Baudier <mbaudier@argeo.org>
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package org
.argeo
.jcr
;
19 import java
.net
.MalformedURLException
;
21 import java
.text
.DateFormat
;
22 import java
.text
.ParseException
;
23 import java
.util
.Calendar
;
24 import java
.util
.Date
;
25 import java
.util
.GregorianCalendar
;
26 import java
.util
.HashMap
;
27 import java
.util
.Iterator
;
28 import java
.util
.List
;
30 import java
.util
.StringTokenizer
;
31 import java
.util
.TreeMap
;
33 import javax
.jcr
.Binary
;
34 import javax
.jcr
.NamespaceRegistry
;
35 import javax
.jcr
.Node
;
36 import javax
.jcr
.NodeIterator
;
37 import javax
.jcr
.Property
;
38 import javax
.jcr
.PropertyIterator
;
39 import javax
.jcr
.Repository
;
40 import javax
.jcr
.RepositoryException
;
41 import javax
.jcr
.RepositoryFactory
;
42 import javax
.jcr
.Session
;
43 import javax
.jcr
.Value
;
44 import javax
.jcr
.nodetype
.NodeType
;
45 import javax
.jcr
.query
.Query
;
46 import javax
.jcr
.query
.QueryResult
;
47 import javax
.jcr
.query
.qom
.Constraint
;
48 import javax
.jcr
.query
.qom
.DynamicOperand
;
49 import javax
.jcr
.query
.qom
.QueryObjectModelFactory
;
50 import javax
.jcr
.query
.qom
.Selector
;
51 import javax
.jcr
.query
.qom
.StaticOperand
;
53 import org
.apache
.commons
.logging
.Log
;
54 import org
.apache
.commons
.logging
.LogFactory
;
55 import org
.argeo
.ArgeoException
;
57 /** Utility methods to simplify common JCR operations. */
58 public class JcrUtils
implements ArgeoJcrConstants
{
59 private final static Log log
= LogFactory
.getLog(JcrUtils
.class);
61 /** Prevents instantiation */
66 * Queries one single node.
68 * @return one single node or null if none was found
69 * @throws ArgeoException
70 * if more than one node was found
72 public static Node
querySingleNode(Query query
) {
73 NodeIterator nodeIterator
;
75 QueryResult queryResult
= query
.execute();
76 nodeIterator
= queryResult
.getNodes();
77 } catch (RepositoryException e
) {
78 throw new ArgeoException("Cannot execute query " + query
, e
);
81 if (nodeIterator
.hasNext())
82 node
= nodeIterator
.nextNode();
86 if (nodeIterator
.hasNext())
87 throw new ArgeoException("Query returned more than one node.");
91 /** Removes forbidden characters from a path, replacing them with '_' */
92 public static String
removeForbiddenCharacters(String str
) {
93 return str
.replace('[', '_').replace(']', '_').replace('/', '_')
98 /** Retrieves the parent path of the provided path */
99 public static String
parentPath(String path
) {
100 if (path
.equals("/"))
101 throw new ArgeoException("Root path '/' has no parent path");
102 if (path
.charAt(0) != '/')
103 throw new ArgeoException("Path " + path
+ " must start with a '/'");
105 if (pathT
.charAt(pathT
.length() - 1) == '/')
106 pathT
= pathT
.substring(0, pathT
.length() - 2);
108 int index
= pathT
.lastIndexOf('/');
109 return pathT
.substring(0, index
);
112 /** The provided data as a path ('/' at the end, not the beginning) */
113 public static String
dateAsPath(Calendar cal
) {
114 return dateAsPath(cal
, false);
118 * Creates a deep path based on a URL:
119 * http://subdomain.example.com/to/content?args =>
120 * com/example/subdomain/to/content
122 public static String
urlAsPath(String url
) {
124 URL u
= new URL(url
);
125 StringBuffer path
= new StringBuffer(url
.length());
127 path
.append(hostAsPath(u
.getHost()));
128 // we don't put port since it may not always be there and may change
129 path
.append(u
.getPath());
130 return path
.toString();
131 } catch (MalformedURLException e
) {
132 throw new ArgeoException("Cannot generate URL path for " + url
, e
);
137 * Creates a path from a FQDN, inverting the order of the component:
138 * www.argeo.org => org.argeo.www
140 public static String
hostAsPath(String host
) {
141 StringBuffer path
= new StringBuffer(host
.length());
142 String
[] hostTokens
= host
.split("\\.");
143 for (int i
= hostTokens
.length
- 1; i
>= 0; i
--) {
144 path
.append(hostTokens
[i
]);
148 return path
.toString();
152 * The provided data as a path ('/' at the end, not the beginning)
157 * whether to add hour as well
159 public static String
dateAsPath(Calendar cal
, Boolean addHour
) {
160 StringBuffer buf
= new StringBuffer(14);
161 buf
.append('Y').append(cal
.get(Calendar
.YEAR
));// 5
163 int month
= cal
.get(Calendar
.MONTH
) + 1;
167 buf
.append(month
);// 3
169 int day
= cal
.get(Calendar
.DAY_OF_MONTH
);
172 buf
.append('D').append(day
);// 3
175 int hour
= cal
.get(Calendar
.HOUR_OF_DAY
);
178 buf
.append('H').append(hour
);// 3
181 return buf
.toString();
185 /** Converts in one call a string into a gregorian calendar. */
186 public static Calendar
parseCalendar(DateFormat dateFormat
, String value
) {
188 Date date
= dateFormat
.parse(value
);
189 Calendar calendar
= new GregorianCalendar();
190 calendar
.setTime(date
);
192 } catch (ParseException e
) {
193 throw new ArgeoException("Cannot parse " + value
194 + " with date format " + dateFormat
, e
);
199 /** The last element of a path. */
200 public static String
lastPathElement(String path
) {
201 if (path
.charAt(path
.length() - 1) == '/')
202 throw new ArgeoException("Path " + path
+ " cannot end with '/'");
203 int index
= path
.lastIndexOf('/');
205 throw new ArgeoException("Cannot find last path element for "
207 return path
.substring(index
+ 1);
210 /** Creates the nodes making path, if they don't exist. */
211 public static Node
mkdirs(Session session
, String path
) {
212 return mkdirs(session
, path
, null, null, false);
216 * @deprecated use {@link #mkdirs(Session, String, String, String, Boolean)}
220 public static Node
mkdirs(Session session
, String path
, String type
,
221 Boolean versioning
) {
222 return mkdirs(session
, path
, type
, type
, false);
227 * the type of the leaf node
229 public static Node
mkdirs(Session session
, String path
, String type
) {
230 return mkdirs(session
, path
, type
, null, false);
234 * Creates the nodes making path, if they don't exist. This is up to the
235 * caller to save the session.
237 public static Node
mkdirs(Session session
, String path
, String type
,
238 String intermediaryNodeType
, Boolean versioning
) {
240 if (path
.equals('/'))
241 return session
.getRootNode();
243 if (session
.itemExists(path
)) {
244 Node node
= session
.getNode(path
);
247 && !type
.equals(node
.getPrimaryNodeType().getName()))
248 throw new ArgeoException("Node " + node
249 + " exists but is of type "
250 + node
.getPrimaryNodeType().getName()
251 + " not of type " + type
);
252 // TODO: check versioning
256 StringTokenizer st
= new StringTokenizer(path
, "/");
257 StringBuffer current
= new StringBuffer("/");
258 Node currentNode
= session
.getRootNode();
259 while (st
.hasMoreTokens()) {
260 String part
= st
.nextToken();
261 current
.append(part
).append('/');
262 if (!session
.itemExists(current
.toString())) {
263 if (!st
.hasMoreTokens() && type
!= null)
264 currentNode
= currentNode
.addNode(part
, type
);
265 else if (st
.hasMoreTokens() && intermediaryNodeType
!= null)
266 currentNode
= currentNode
.addNode(part
,
267 intermediaryNodeType
);
269 currentNode
= currentNode
.addNode(part
);
271 currentNode
.addMixin(NodeType
.MIX_VERSIONABLE
);
272 if (log
.isTraceEnabled())
273 log
.debug("Added folder " + part
+ " as " + current
);
275 currentNode
= (Node
) session
.getItem(current
.toString());
280 } catch (RepositoryException e
) {
281 throw new ArgeoException("Cannot mkdirs " + path
, e
);
286 * Safe and repository implementation independent registration of a
289 public static void registerNamespaceSafely(Session session
, String prefix
,
292 registerNamespaceSafely(session
.getWorkspace()
293 .getNamespaceRegistry(), prefix
, uri
);
294 } catch (RepositoryException e
) {
295 throw new ArgeoException("Cannot find namespace registry", e
);
300 * Safe and repository implementation independent registration of a
303 public static void registerNamespaceSafely(NamespaceRegistry nr
,
304 String prefix
, String uri
) {
306 String
[] prefixes
= nr
.getPrefixes();
307 for (String pref
: prefixes
)
308 if (pref
.equals(prefix
)) {
309 String registeredUri
= nr
.getURI(pref
);
310 if (!registeredUri
.equals(uri
))
311 throw new ArgeoException("Prefix " + pref
312 + " already registered for URI "
314 + " which is different from provided URI "
319 nr
.registerNamespace(prefix
, uri
);
320 } catch (RepositoryException e
) {
321 throw new ArgeoException("Cannot register namespace " + uri
322 + " under prefix " + prefix
, e
);
326 /** Recursively outputs the contents of the given node. */
327 public static void debug(Node node
) {
329 // First output the node path
330 log
.debug(node
.getPath());
331 // Skip the virtual (and large!) jcr:system subtree
332 if (node
.getName().equals("jcr:system")) {
336 // Then the children nodes (recursive)
337 NodeIterator it
= node
.getNodes();
338 while (it
.hasNext()) {
339 Node childNode
= it
.nextNode();
343 // Then output the properties
344 PropertyIterator properties
= node
.getProperties();
345 // log.debug("Property are : ");
347 while (properties
.hasNext()) {
348 Property property
= properties
.nextProperty();
349 if (property
.getDefinition().isMultiple()) {
350 // A multi-valued property, print all values
351 Value
[] values
= property
.getValues();
352 for (int i
= 0; i
< values
.length
; i
++) {
353 log
.debug(property
.getPath() + "="
354 + values
[i
].getString());
357 // A single-valued property
358 log
.debug(property
.getPath() + "=" + property
.getString());
361 } catch (Exception e
) {
362 log
.error("Could not debug " + node
, e
);
368 * Copies recursively the content of a node to another one. Mixin are NOT
371 public static void copy(Node fromNode
, Node toNode
) {
373 PropertyIterator pit
= fromNode
.getProperties();
374 properties
: while (pit
.hasNext()) {
375 Property fromProperty
= pit
.nextProperty();
376 String propertyName
= fromProperty
.getName();
377 if (toNode
.hasProperty(propertyName
)
378 && toNode
.getProperty(propertyName
).getDefinition()
382 toNode
.setProperty(fromProperty
.getName(),
383 fromProperty
.getValue());
386 NodeIterator nit
= fromNode
.getNodes();
387 while (nit
.hasNext()) {
388 Node fromChild
= nit
.nextNode();
389 Integer index
= fromChild
.getIndex();
390 String nodeRelPath
= fromChild
.getName() + "[" + index
+ "]";
392 if (toNode
.hasNode(nodeRelPath
))
393 toChild
= toNode
.getNode(nodeRelPath
);
395 toChild
= toNode
.addNode(fromChild
.getName(), fromChild
396 .getPrimaryNodeType().getName());
397 copy(fromChild
, toChild
);
399 } catch (RepositoryException e
) {
400 throw new ArgeoException("Cannot copy " + fromNode
+ " to "
406 * Check whether all first-level properties (except jcr:* properties) are
407 * equal. Skip jcr:* properties
409 public static Boolean
allPropertiesEquals(Node reference
, Node observed
,
410 Boolean onlyCommonProperties
) {
412 PropertyIterator pit
= reference
.getProperties();
413 props
: while (pit
.hasNext()) {
414 Property propReference
= pit
.nextProperty();
415 String propName
= propReference
.getName();
416 if (propName
.startsWith("jcr:"))
419 if (!observed
.hasProperty(propName
))
420 if (onlyCommonProperties
)
424 // TODO: deal with multiple property values?
425 if (!observed
.getProperty(propName
).getValue()
426 .equals(propReference
.getValue()))
430 } catch (RepositoryException e
) {
431 throw new ArgeoException("Cannot check all properties equals of "
432 + reference
+ " and " + observed
, e
);
436 public static Map
<String
, PropertyDiff
> diffProperties(Node reference
,
438 Map
<String
, PropertyDiff
> diffs
= new TreeMap
<String
, PropertyDiff
>();
439 diffPropertiesLevel(diffs
, null, reference
, observed
);
444 * Compare the properties of two nodes. Recursivity to child nodes is not
445 * yet supported. Skip jcr:* properties.
447 static void diffPropertiesLevel(Map
<String
, PropertyDiff
> diffs
,
448 String baseRelPath
, Node reference
, Node observed
) {
450 // check removed and modified
451 PropertyIterator pit
= reference
.getProperties();
452 props
: while (pit
.hasNext()) {
453 Property p
= pit
.nextProperty();
454 String name
= p
.getName();
455 if (name
.startsWith("jcr:"))
458 if (!observed
.hasProperty(name
)) {
459 String relPath
= propertyRelPath(baseRelPath
, name
);
460 PropertyDiff pDiff
= new PropertyDiff(PropertyDiff
.REMOVED
,
461 relPath
, p
.getValue(), null);
462 diffs
.put(relPath
, pDiff
);
466 Value referenceValue
= p
.getValue();
467 Value newValue
= observed
.getProperty(name
).getValue();
468 if (!referenceValue
.equals(newValue
)) {
469 String relPath
= propertyRelPath(baseRelPath
, name
);
470 PropertyDiff pDiff
= new PropertyDiff(
471 PropertyDiff
.MODIFIED
, relPath
, referenceValue
,
473 diffs
.put(relPath
, pDiff
);
478 pit
= observed
.getProperties();
479 props
: while (pit
.hasNext()) {
480 Property p
= pit
.nextProperty();
481 String name
= p
.getName();
482 if (name
.startsWith("jcr:"))
484 if (!reference
.hasProperty(name
)) {
485 String relPath
= propertyRelPath(baseRelPath
, name
);
486 PropertyDiff pDiff
= new PropertyDiff(PropertyDiff
.ADDED
,
487 relPath
, null, p
.getValue());
488 diffs
.put(relPath
, pDiff
);
491 } catch (RepositoryException e
) {
492 throw new ArgeoException("Cannot diff " + reference
+ " and "
498 * Compare only a restricted list of properties of two nodes. No
502 public static Map
<String
, PropertyDiff
> diffProperties(Node reference
,
503 Node observed
, List
<String
> properties
) {
504 Map
<String
, PropertyDiff
> diffs
= new TreeMap
<String
, PropertyDiff
>();
506 Iterator
<String
> pit
= properties
.iterator();
508 props
: while (pit
.hasNext()) {
509 String name
= pit
.next();
510 if (!reference
.hasProperty(name
)) {
511 if (!observed
.hasProperty(name
))
513 Value val
= observed
.getProperty(name
).getValue();
515 // empty String but not null
516 if ("".equals(val
.getString()))
518 } catch (Exception e
) {
519 // not parseable as String, silent
521 PropertyDiff pDiff
= new PropertyDiff(PropertyDiff
.ADDED
,
523 diffs
.put(name
, pDiff
);
524 } else if (!observed
.hasProperty(name
)) {
525 PropertyDiff pDiff
= new PropertyDiff(PropertyDiff
.REMOVED
,
526 name
, reference
.getProperty(name
).getValue(), null);
527 diffs
.put(name
, pDiff
);
529 Value referenceValue
= reference
.getProperty(name
)
531 Value newValue
= observed
.getProperty(name
).getValue();
532 if (!referenceValue
.equals(newValue
)) {
533 PropertyDiff pDiff
= new PropertyDiff(
534 PropertyDiff
.MODIFIED
, name
, referenceValue
,
536 diffs
.put(name
, pDiff
);
540 } catch (RepositoryException e
) {
541 throw new ArgeoException("Cannot diff " + reference
+ " and "
547 /** Builds a property relPath to be used in the diff. */
548 private static String
propertyRelPath(String baseRelPath
,
549 String propertyName
) {
550 if (baseRelPath
== null)
553 return baseRelPath
+ '/' + propertyName
;
557 * Normalize a name so taht it can be stores in contexts not supporting
558 * names with ':' (typically databases). Replaces ':' by '_'.
560 public static String
normalize(String name
) {
561 return name
.replace(':', '_');
564 /** Cleanly disposes a {@link Binary} even if it is null. */
565 public static void closeQuietly(Binary binary
) {
572 * Creates depth from a string (typically a username) by adding levels based
573 * on its first characters: "aBcD",2 => a/aB
575 public static String
firstCharsToPath(String str
, Integer nbrOfChars
) {
576 if (str
.length() < nbrOfChars
)
577 throw new ArgeoException("String " + str
578 + " length must be greater or equal than " + nbrOfChars
);
579 StringBuffer path
= new StringBuffer("");
580 StringBuffer curr
= new StringBuffer("");
581 for (int i
= 0; i
< nbrOfChars
; i
++) {
582 curr
.append(str
.charAt(i
));
584 if (i
< nbrOfChars
- 1)
587 return path
.toString();
591 * Wraps the call to the repository factory based on parameter
592 * {@link ArgeoJcrConstants#JCR_REPOSITORY_ALIAS} in order to simplify it
593 * and protect against future API changes.
595 public static Repository
getRepositoryByAlias(
596 RepositoryFactory repositoryFactory
, String alias
) {
598 Map
<String
, String
> parameters
= new HashMap
<String
, String
>();
599 parameters
.put(JCR_REPOSITORY_ALIAS
, alias
);
600 return repositoryFactory
.getRepository(parameters
);
601 } catch (RepositoryException e
) {
602 throw new ArgeoException(
603 "Unexpected exception when trying to retrieve repository with alias "
609 * Wraps the call to the repository factory based on parameter
610 * {@link ArgeoJcrConstants#JCR_REPOSITORY_URI} in order to simplify it and
611 * protect against future API changes.
613 public static Repository
getRepositoryByUri(
614 RepositoryFactory repositoryFactory
, String uri
) {
616 Map
<String
, String
> parameters
= new HashMap
<String
, String
>();
617 parameters
.put(JCR_REPOSITORY_URI
, uri
);
618 return repositoryFactory
.getRepository(parameters
);
619 } catch (RepositoryException e
) {
620 throw new ArgeoException(
621 "Unexpected exception when trying to retrieve repository with uri "
627 * Discards the current changes in a session by calling
628 * {@link Session#refresh(boolean)} with <code>false</code>, only logging
629 * potential errors when doing so. To be used typically in a catch block.
631 public static void discardQuietly(Session session
) {
634 session
.refresh(false);
635 } catch (RepositoryException e
) {
636 log
.warn("Cannot quietly discard session " + session
+ ": "
641 /** Logs out the session, not throwing any exception, even if it is null. */
642 public static void logoutQuietly(Session session
) {
647 /** Returns the home node of the session user or null if none was found. */
648 public static Node
getUserHome(Session session
) {
649 String userID
= session
.getUserID();
650 return getUserHome(session
, userID
);
654 * Returns user home has path, embedding exceptions. Contrary to
655 * {@link #getUserHome(Session)}, it never returns null but throws and
656 * exception if not found.
658 public static String
getUserHomePath(Session session
) {
659 String userID
= session
.getUserID();
661 Node userHome
= getUserHome(session
, userID
);
662 if (userHome
!= null)
663 return userHome
.getPath();
665 throw new ArgeoException("No home registered for " + userID
);
666 } catch (RepositoryException e
) {
667 throw new ArgeoException("Cannot find user home path", e
);
671 /** Get the profile of the user attached to this session. */
672 public static Node
getUserProfile(Session session
) {
673 String userID
= session
.getUserID();
674 return getUserProfile(session
, userID
);
678 * Returns the home node of the session user or null if none was found.
681 * the session to use in order to perform the search, this can be
682 * a session with a different user ID than the one searched,
683 * typically when a system or admin session is used.
685 * the username of the user
687 public static Node
getUserHome(Session session
, String username
) {
689 QueryObjectModelFactory qomf
= session
.getWorkspace()
690 .getQueryManager().getQOMFactory();
692 // query the user home for this user id
693 Selector userHomeSel
= qomf
.selector(ArgeoTypes
.ARGEO_USER_HOME
,
695 DynamicOperand userIdDop
= qomf
.propertyValue("userHome",
696 ArgeoNames
.ARGEO_USER_ID
);
697 StaticOperand userIdSop
= qomf
.literal(session
.getValueFactory()
698 .createValue(username
));
699 Constraint constraint
= qomf
.comparison(userIdDop
,
700 QueryObjectModelFactory
.JCR_OPERATOR_EQUAL_TO
, userIdSop
);
701 Query query
= qomf
.createQuery(userHomeSel
, constraint
, null, null);
702 Node userHome
= JcrUtils
.querySingleNode(query
);
704 } catch (RepositoryException e
) {
705 throw new ArgeoException("Cannot find home for user " + username
, e
);
709 public static Node
getUserProfile(Session session
, String username
) {
711 QueryObjectModelFactory qomf
= session
.getWorkspace()
712 .getQueryManager().getQOMFactory();
713 Selector sel
= qomf
.selector(ArgeoTypes
.ARGEO_USER_PROFILE
,
715 DynamicOperand userIdDop
= qomf
.propertyValue("userProfile",
716 ArgeoNames
.ARGEO_USER_ID
);
717 StaticOperand userIdSop
= qomf
.literal(session
.getValueFactory()
718 .createValue(username
));
719 Constraint constraint
= qomf
.comparison(userIdDop
,
720 QueryObjectModelFactory
.JCR_OPERATOR_EQUAL_TO
, userIdSop
);
721 Query query
= qomf
.createQuery(sel
, constraint
, null, null);
722 Node userHome
= JcrUtils
.querySingleNode(query
);
724 } catch (RepositoryException e
) {
725 throw new ArgeoException(
726 "Cannot find profile for user " + username
, e
);
730 public static Node
createUserHome(Session session
, String homeBasePath
,
734 throw new ArgeoException("Session is null");
735 if (session
.hasPendingChanges())
736 throw new ArgeoException(
737 "Session has pending changes, save them first");
738 String homePath
= homeBasePath
+ '/'
739 + firstCharsToPath(username
, 2) + '/' + username
;
740 Node userHome
= JcrUtils
.mkdirs(session
, homePath
);
742 Node userProfile
= userHome
.addNode(ArgeoNames
.ARGEO_PROFILE
);
743 userProfile
.addMixin(ArgeoTypes
.ARGEO_USER_PROFILE
);
744 userProfile
.setProperty(ArgeoNames
.ARGEO_USER_ID
, username
);
746 // we need to save the profile before adding the user home type
747 userHome
.addMixin(ArgeoTypes
.ARGEO_USER_HOME
);
749 // http://jackrabbit.510166.n4.nabble.com/Jackrabbit-2-0-beta-6-Problem-adding-a-Mixin-type-with-mandatory-properties-after-setting-propertiesn-td1290332.html
750 userHome
.setProperty(ArgeoNames
.ARGEO_USER_ID
, username
);
753 } catch (RepositoryException e
) {
754 discardQuietly(session
);
755 throw new ArgeoException("Cannot create home node for user "