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
.Iterator
;
27 import java
.util
.List
;
29 import java
.util
.StringTokenizer
;
30 import java
.util
.TreeMap
;
32 import javax
.jcr
.Binary
;
33 import javax
.jcr
.NamespaceRegistry
;
34 import javax
.jcr
.Node
;
35 import javax
.jcr
.NodeIterator
;
36 import javax
.jcr
.Property
;
37 import javax
.jcr
.PropertyIterator
;
38 import javax
.jcr
.RepositoryException
;
39 import javax
.jcr
.Session
;
40 import javax
.jcr
.Value
;
41 import javax
.jcr
.nodetype
.NodeType
;
42 import javax
.jcr
.query
.Query
;
43 import javax
.jcr
.query
.QueryResult
;
45 import org
.apache
.commons
.logging
.Log
;
46 import org
.apache
.commons
.logging
.LogFactory
;
47 import org
.argeo
.ArgeoException
;
49 /** Utility methods to simplify common JCR operations. */
50 public class JcrUtils
{
51 private final static Log log
= LogFactory
.getLog(JcrUtils
.class);
54 * Queries one single node.
56 * @return one single node or null if none was found
57 * @throws ArgeoException
58 * if more than one node was found
60 public static Node
querySingleNode(Query query
) {
61 NodeIterator nodeIterator
;
63 QueryResult queryResult
= query
.execute();
64 nodeIterator
= queryResult
.getNodes();
65 } catch (RepositoryException e
) {
66 throw new ArgeoException("Cannot execute query " + query
, e
);
69 if (nodeIterator
.hasNext())
70 node
= nodeIterator
.nextNode();
74 if (nodeIterator
.hasNext())
75 throw new ArgeoException("Query returned more than one node.");
79 /** Removes forbidden characters from a path, replacing them with '_' */
80 public static String
removeForbiddenCharacters(String str
) {
81 return str
.replace('[', '_').replace(']', '_').replace('/', '_')
86 /** Retrieves the parent path of the provided path */
87 public static String
parentPath(String path
) {
89 throw new ArgeoException("Root path '/' has no parent path");
90 if (path
.charAt(0) != '/')
91 throw new ArgeoException("Path " + path
+ " must start with a '/'");
93 if (pathT
.charAt(pathT
.length() - 1) == '/')
94 pathT
= pathT
.substring(0, pathT
.length() - 2);
96 int index
= pathT
.lastIndexOf('/');
97 return pathT
.substring(0, index
);
100 /** The provided data as a path ('/' at the end, not the beginning) */
101 public static String
dateAsPath(Calendar cal
) {
102 return dateAsPath(cal
, false);
106 * Creates a deep path based on a URL:
107 * http://subdomain.example.com/to/content?args =>
108 * com/example/subdomain/to/content
110 public static String
urlAsPath(String url
) {
112 URL u
= new URL(url
);
113 StringBuffer path
= new StringBuffer(url
.length());
115 String
[] hostTokens
= u
.getHost().split("\\.");
116 for (int i
= hostTokens
.length
- 1; i
>= 0; i
--)
117 path
.append(hostTokens
[i
]).append('/');
118 // we don't put port since it may not always be there and may change
119 path
.append(u
.getPath());
120 return path
.toString();
121 } catch (MalformedURLException e
) {
122 throw new ArgeoException("Cannot generate URL path for " + url
, e
);
127 * The provided data as a path ('/' at the end, not the beginning)
132 * whether to add hour as well
134 public static String
dateAsPath(Calendar cal
, Boolean addHour
) {
135 StringBuffer buf
= new StringBuffer(14);
136 buf
.append('Y').append(cal
.get(Calendar
.YEAR
));// 5
138 int month
= cal
.get(Calendar
.MONTH
) + 1;
142 buf
.append(month
);// 3
144 int day
= cal
.get(Calendar
.DAY_OF_MONTH
);
147 buf
.append('D').append(day
);// 3
150 int hour
= cal
.get(Calendar
.HOUR_OF_DAY
);
153 buf
.append('H').append(hour
);// 3
156 return buf
.toString();
160 /** Converts in one call a string into a gregorian calendar. */
161 public static Calendar
parseCalendar(DateFormat dateFormat
, String value
) {
163 Date date
= dateFormat
.parse(value
);
164 Calendar calendar
= new GregorianCalendar();
165 calendar
.setTime(date
);
167 } catch (ParseException e
) {
168 throw new ArgeoException("Cannot parse " + value
169 + " with date format " + dateFormat
, e
);
174 /** Converts the FQDN of an host into a path (converts '.' into '/'). */
175 public static String
hostAsPath(String host
) {
176 // TODO : inverse order of the elements (to have org/argeo/test IO
178 return host
.replace('.', '/');
181 /** The last element of a path. */
182 public static String
lastPathElement(String path
) {
183 if (path
.charAt(path
.length() - 1) == '/')
184 throw new ArgeoException("Path " + path
+ " cannot end with '/'");
185 int index
= path
.lastIndexOf('/');
187 throw new ArgeoException("Cannot find last path element for "
189 return path
.substring(index
+ 1);
192 /** Creates the nodes making path, if they don't exist. */
193 public static Node
mkdirs(Session session
, String path
) {
194 return mkdirs(session
, path
, null, null, false);
198 * @deprecated use {@link #mkdirs(Session, String, String, String, Boolean)}
202 public static Node
mkdirs(Session session
, String path
, String type
,
203 Boolean versioning
) {
204 return mkdirs(session
, path
, type
, type
, false);
209 * the type of the leaf node
211 public static Node
mkdirs(Session session
, String path
, String type
) {
212 return mkdirs(session
, path
, type
, null, false);
215 /** Creates the nodes making path, if they don't exist. */
216 public static Node
mkdirs(Session session
, String path
, String type
,
217 String intermediaryNodeType
, Boolean versioning
) {
219 if (path
.equals('/'))
220 return session
.getRootNode();
222 if (session
.itemExists(path
)) {
223 Node node
= session
.getNode(path
);
226 && !type
.equals(node
.getPrimaryNodeType().getName()))
227 throw new ArgeoException("Node " + node
228 + " exists but is of type "
229 + node
.getPrimaryNodeType().getName()
230 + " not of type " + type
);
231 // TODO: check versioning
235 StringTokenizer st
= new StringTokenizer(path
, "/");
236 StringBuffer current
= new StringBuffer("/");
237 Node currentNode
= session
.getRootNode();
238 while (st
.hasMoreTokens()) {
239 String part
= st
.nextToken();
240 current
.append(part
).append('/');
241 if (!session
.itemExists(current
.toString())) {
242 if (!st
.hasMoreTokens() && type
!= null)
243 currentNode
= currentNode
.addNode(part
, type
);
244 else if (st
.hasMoreTokens() && intermediaryNodeType
!= null)
245 currentNode
= currentNode
.addNode(part
,
246 intermediaryNodeType
);
248 currentNode
= currentNode
.addNode(part
);
250 currentNode
.addMixin(NodeType
.MIX_VERSIONABLE
);
251 if (log
.isTraceEnabled())
252 log
.debug("Added folder " + part
+ " as " + current
);
254 currentNode
= (Node
) session
.getItem(current
.toString());
259 } catch (RepositoryException e
) {
260 throw new ArgeoException("Cannot mkdirs " + path
, e
);
265 * Safe and repository implementation independent registration of a
268 public static void registerNamespaceSafely(Session session
, String prefix
,
271 registerNamespaceSafely(session
.getWorkspace()
272 .getNamespaceRegistry(), prefix
, uri
);
273 } catch (RepositoryException e
) {
274 throw new ArgeoException("Cannot find namespace registry", e
);
279 * Safe and repository implementation independent registration of a
282 public static void registerNamespaceSafely(NamespaceRegistry nr
,
283 String prefix
, String uri
) {
285 String
[] prefixes
= nr
.getPrefixes();
286 for (String pref
: prefixes
)
287 if (pref
.equals(prefix
)) {
288 String registeredUri
= nr
.getURI(pref
);
289 if (!registeredUri
.equals(uri
))
290 throw new ArgeoException("Prefix " + pref
291 + " already registered for URI "
293 + " which is different from provided URI "
298 nr
.registerNamespace(prefix
, uri
);
299 } catch (RepositoryException e
) {
300 throw new ArgeoException("Cannot register namespace " + uri
301 + " under prefix " + prefix
, e
);
305 /** Recursively outputs the contents of the given node. */
306 public static void debug(Node node
) {
308 // First output the node path
309 log
.debug(node
.getPath());
310 // Skip the virtual (and large!) jcr:system subtree
311 if (node
.getName().equals("jcr:system")) {
315 // Then the children nodes (recursive)
316 NodeIterator it
= node
.getNodes();
317 while (it
.hasNext()) {
318 Node childNode
= it
.nextNode();
322 // Then output the properties
323 PropertyIterator properties
= node
.getProperties();
324 // log.debug("Property are : ");
326 while (properties
.hasNext()) {
327 Property property
= properties
.nextProperty();
328 if (property
.getDefinition().isMultiple()) {
329 // A multi-valued property, print all values
330 Value
[] values
= property
.getValues();
331 for (int i
= 0; i
< values
.length
; i
++) {
332 log
.debug(property
.getPath() + "="
333 + values
[i
].getString());
336 // A single-valued property
337 log
.debug(property
.getPath() + "=" + property
.getString());
340 } catch (Exception e
) {
341 log
.error("Could not debug " + node
, e
);
347 * Copies recursively the content of a node to another one. Mixin are NOT
350 public static void copy(Node fromNode
, Node toNode
) {
352 PropertyIterator pit
= fromNode
.getProperties();
353 properties
: while (pit
.hasNext()) {
354 Property fromProperty
= pit
.nextProperty();
355 String propertyName
= fromProperty
.getName();
356 if (toNode
.hasProperty(propertyName
)
357 && toNode
.getProperty(propertyName
).getDefinition()
361 toNode
.setProperty(fromProperty
.getName(),
362 fromProperty
.getValue());
365 NodeIterator nit
= fromNode
.getNodes();
366 while (nit
.hasNext()) {
367 Node fromChild
= nit
.nextNode();
368 Integer index
= fromChild
.getIndex();
369 String nodeRelPath
= fromChild
.getName() + "[" + index
+ "]";
371 if (toNode
.hasNode(nodeRelPath
))
372 toChild
= toNode
.getNode(nodeRelPath
);
374 toChild
= toNode
.addNode(fromChild
.getName(), fromChild
375 .getPrimaryNodeType().getName());
376 copy(fromChild
, toChild
);
378 } catch (RepositoryException e
) {
379 throw new ArgeoException("Cannot copy " + fromNode
+ " to "
385 * Check whether all first-level properties (except jcr:* properties) are
386 * equal. Skip jcr:* properties
388 public static Boolean
allPropertiesEquals(Node reference
, Node observed
,
389 Boolean onlyCommonProperties
) {
391 PropertyIterator pit
= reference
.getProperties();
392 props
: while (pit
.hasNext()) {
393 Property propReference
= pit
.nextProperty();
394 String propName
= propReference
.getName();
395 if (propName
.startsWith("jcr:"))
398 if (!observed
.hasProperty(propName
))
399 if (onlyCommonProperties
)
403 // TODO: deal with multiple property values?
404 if (!observed
.getProperty(propName
).getValue()
405 .equals(propReference
.getValue()))
409 } catch (RepositoryException e
) {
410 throw new ArgeoException("Cannot check all properties equals of "
411 + reference
+ " and " + observed
, e
);
415 public static Map
<String
, PropertyDiff
> diffProperties(Node reference
,
417 Map
<String
, PropertyDiff
> diffs
= new TreeMap
<String
, PropertyDiff
>();
418 diffPropertiesLevel(diffs
, null, reference
, observed
);
423 * Compare the properties of two nodes. Recursivity to child nodes is not
424 * yet supported. Skip jcr:* properties.
426 static void diffPropertiesLevel(Map
<String
, PropertyDiff
> diffs
,
427 String baseRelPath
, Node reference
, Node observed
) {
429 // check removed and modified
430 PropertyIterator pit
= reference
.getProperties();
431 props
: while (pit
.hasNext()) {
432 Property p
= pit
.nextProperty();
433 String name
= p
.getName();
434 if (name
.startsWith("jcr:"))
437 if (!observed
.hasProperty(name
)) {
438 String relPath
= propertyRelPath(baseRelPath
, name
);
439 PropertyDiff pDiff
= new PropertyDiff(PropertyDiff
.REMOVED
,
440 relPath
, p
.getValue(), null);
441 diffs
.put(relPath
, pDiff
);
445 Value referenceValue
= p
.getValue();
446 Value newValue
= observed
.getProperty(name
).getValue();
447 if (!referenceValue
.equals(newValue
)) {
448 String relPath
= propertyRelPath(baseRelPath
, name
);
449 PropertyDiff pDiff
= new PropertyDiff(
450 PropertyDiff
.MODIFIED
, relPath
, referenceValue
,
452 diffs
.put(relPath
, pDiff
);
457 pit
= observed
.getProperties();
458 props
: while (pit
.hasNext()) {
459 Property p
= pit
.nextProperty();
460 String name
= p
.getName();
461 if (name
.startsWith("jcr:"))
463 if (!reference
.hasProperty(name
)) {
464 String relPath
= propertyRelPath(baseRelPath
, name
);
465 PropertyDiff pDiff
= new PropertyDiff(PropertyDiff
.ADDED
,
466 relPath
, null, p
.getValue());
467 diffs
.put(relPath
, pDiff
);
470 } catch (RepositoryException e
) {
471 throw new ArgeoException("Cannot diff " + reference
+ " and "
477 * Compare only a restricted list of properties of two nodes. No
481 public static Map
<String
, PropertyDiff
> diffProperties(Node reference
,
482 Node observed
, List
<String
> properties
) {
483 Map
<String
, PropertyDiff
> diffs
= new TreeMap
<String
, PropertyDiff
>();
485 Iterator
<String
> pit
= properties
.iterator();
487 props
: while (pit
.hasNext()) {
488 String name
= pit
.next();
489 if (!reference
.hasProperty(name
)) {
490 if (!observed
.hasProperty(name
))
492 Value val
= observed
.getProperty(name
).getValue();
494 // empty String but not null
495 if ("".equals(val
.getString()))
497 } catch (Exception e
) {
498 // not parseable as String, silent
500 PropertyDiff pDiff
= new PropertyDiff(PropertyDiff
.ADDED
,
502 diffs
.put(name
, pDiff
);
503 } else if (!observed
.hasProperty(name
)) {
504 PropertyDiff pDiff
= new PropertyDiff(PropertyDiff
.REMOVED
,
505 name
, reference
.getProperty(name
).getValue(), null);
506 diffs
.put(name
, pDiff
);
508 Value referenceValue
= reference
.getProperty(name
)
510 Value newValue
= observed
.getProperty(name
).getValue();
511 if (!referenceValue
.equals(newValue
)) {
512 PropertyDiff pDiff
= new PropertyDiff(
513 PropertyDiff
.MODIFIED
, name
, referenceValue
,
515 diffs
.put(name
, pDiff
);
519 } catch (RepositoryException e
) {
520 throw new ArgeoException("Cannot diff " + reference
+ " and "
526 /** Builds a property relPath to be used in the diff. */
527 private static String
propertyRelPath(String baseRelPath
,
528 String propertyName
) {
529 if (baseRelPath
== null)
532 return baseRelPath
+ '/' + propertyName
;
536 * Normalize a name so taht it can be stores in contexts not supporting
537 * names with ':' (typically databases). Replaces ':' by '_'.
539 public static String
normalize(String name
) {
540 return name
.replace(':', '_');
543 public static void closeQuietly(Binary binary
) {