]> git.argeo.org Git - lgpl/argeo-commons.git/blob - server/runtime/org.argeo.server.jcr/src/main/java/org/argeo/jcr/JcrUtils.java
Update SEBI :
[lgpl/argeo-commons.git] / server / runtime / org.argeo.server.jcr / src / main / java / org / argeo / jcr / JcrUtils.java
1 /*
2 * Copyright (C) 2010 Mathieu Baudier <mbaudier@argeo.org>
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package org.argeo.jcr;
18
19 import java.net.MalformedURLException;
20 import java.net.URL;
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;
29 import java.util.Map;
30 import java.util.StringTokenizer;
31 import java.util.TreeMap;
32
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;
52
53 import org.apache.commons.logging.Log;
54 import org.apache.commons.logging.LogFactory;
55 import org.argeo.ArgeoException;
56
57 /** Utility methods to simplify common JCR operations. */
58 public class JcrUtils implements ArgeoJcrConstants {
59 private final static Log log = LogFactory.getLog(JcrUtils.class);
60
61 /** Prevents instantiation */
62 private JcrUtils() {
63 }
64
65 /**
66 * Queries one single node.
67 *
68 * @return one single node or null if none was found
69 * @throws ArgeoException
70 * if more than one node was found
71 */
72 public static Node querySingleNode(Query query) {
73 NodeIterator nodeIterator;
74 try {
75 QueryResult queryResult = query.execute();
76 nodeIterator = queryResult.getNodes();
77 } catch (RepositoryException e) {
78 throw new ArgeoException("Cannot execute query " + query, e);
79 }
80 Node node;
81 if (nodeIterator.hasNext())
82 node = nodeIterator.nextNode();
83 else
84 return null;
85
86 if (nodeIterator.hasNext())
87 throw new ArgeoException("Query returned more than one node.");
88 return node;
89 }
90
91 /** Removes forbidden characters from a path, replacing them with '_' */
92 public static String removeForbiddenCharacters(String str) {
93 return str.replace('[', '_').replace(']', '_').replace('/', '_')
94 .replace('*', '_');
95
96 }
97
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 '/'");
104 String pathT = path;
105 if (pathT.charAt(pathT.length() - 1) == '/')
106 pathT = pathT.substring(0, pathT.length() - 2);
107
108 int index = pathT.lastIndexOf('/');
109 return pathT.substring(0, index);
110 }
111
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);
115 }
116
117 /**
118 * Creates a deep path based on a URL:
119 * http://subdomain.example.com/to/content?args =>
120 * com/example/subdomain/to/content
121 */
122 public static String urlAsPath(String url) {
123 try {
124 URL u = new URL(url);
125 StringBuffer path = new StringBuffer(url.length());
126 // invert host
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);
133 }
134 }
135
136 /**
137 * Creates a path from a FQDN, inverting the order of the component:
138 * www.argeo.org => org.argeo.www
139 */
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]);
145 if (i != 0)
146 path.append('/');
147 }
148 return path.toString();
149 }
150
151 /**
152 * The provided data as a path ('/' at the end, not the beginning)
153 *
154 * @param cal
155 * the date
156 * @param addHour
157 * whether to add hour as well
158 */
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
162 buf.append('/');// 1
163 int month = cal.get(Calendar.MONTH) + 1;
164 buf.append('M');
165 if (month < 10)
166 buf.append(0);
167 buf.append(month);// 3
168 buf.append('/');// 1
169 int day = cal.get(Calendar.DAY_OF_MONTH);
170 if (day < 10)
171 buf.append(0);
172 buf.append('D').append(day);// 3
173 buf.append('/');// 1
174 if (addHour) {
175 int hour = cal.get(Calendar.HOUR_OF_DAY);
176 if (hour < 10)
177 buf.append(0);
178 buf.append('H').append(hour);// 3
179 buf.append('/');// 1
180 }
181 return buf.toString();
182
183 }
184
185 /** Converts in one call a string into a gregorian calendar. */
186 public static Calendar parseCalendar(DateFormat dateFormat, String value) {
187 try {
188 Date date = dateFormat.parse(value);
189 Calendar calendar = new GregorianCalendar();
190 calendar.setTime(date);
191 return calendar;
192 } catch (ParseException e) {
193 throw new ArgeoException("Cannot parse " + value
194 + " with date format " + dateFormat, e);
195 }
196
197 }
198
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('/');
204 if (index < 0)
205 throw new ArgeoException("Cannot find last path element for "
206 + path);
207 return path.substring(index + 1);
208 }
209
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);
213 }
214
215 /**
216 * @deprecated use {@link #mkdirs(Session, String, String, String, Boolean)}
217 * instead.
218 */
219 @Deprecated
220 public static Node mkdirs(Session session, String path, String type,
221 Boolean versioning) {
222 return mkdirs(session, path, type, type, false);
223 }
224
225 /**
226 * @param type
227 * the type of the leaf node
228 */
229 public static Node mkdirs(Session session, String path, String type) {
230 return mkdirs(session, path, type, null, false);
231 }
232
233 /**
234 * Creates the nodes making path, if they don't exist. This is up to the
235 * caller to save the session.
236 */
237 public static Node mkdirs(Session session, String path, String type,
238 String intermediaryNodeType, Boolean versioning) {
239 try {
240 if (path.equals('/'))
241 return session.getRootNode();
242
243 if (session.itemExists(path)) {
244 Node node = session.getNode(path);
245 // check type
246 if (type != null
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
253 return node;
254 }
255
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);
268 else
269 currentNode = currentNode.addNode(part);
270 if (versioning)
271 currentNode.addMixin(NodeType.MIX_VERSIONABLE);
272 if (log.isTraceEnabled())
273 log.debug("Added folder " + part + " as " + current);
274 } else {
275 currentNode = (Node) session.getItem(current.toString());
276 }
277 }
278 // session.save();
279 return currentNode;
280 } catch (RepositoryException e) {
281 throw new ArgeoException("Cannot mkdirs " + path, e);
282 }
283 }
284
285 /**
286 * Safe and repository implementation independent registration of a
287 * namespace.
288 */
289 public static void registerNamespaceSafely(Session session, String prefix,
290 String uri) {
291 try {
292 registerNamespaceSafely(session.getWorkspace()
293 .getNamespaceRegistry(), prefix, uri);
294 } catch (RepositoryException e) {
295 throw new ArgeoException("Cannot find namespace registry", e);
296 }
297 }
298
299 /**
300 * Safe and repository implementation independent registration of a
301 * namespace.
302 */
303 public static void registerNamespaceSafely(NamespaceRegistry nr,
304 String prefix, String uri) {
305 try {
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 "
313 + registeredUri
314 + " which is different from provided URI "
315 + uri);
316 else
317 return;// skip
318 }
319 nr.registerNamespace(prefix, uri);
320 } catch (RepositoryException e) {
321 throw new ArgeoException("Cannot register namespace " + uri
322 + " under prefix " + prefix, e);
323 }
324 }
325
326 /** Recursively outputs the contents of the given node. */
327 public static void debug(Node node) {
328 try {
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")) {
333 return;
334 }
335
336 // Then the children nodes (recursive)
337 NodeIterator it = node.getNodes();
338 while (it.hasNext()) {
339 Node childNode = it.nextNode();
340 debug(childNode);
341 }
342
343 // Then output the properties
344 PropertyIterator properties = node.getProperties();
345 // log.debug("Property are : ");
346
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());
355 }
356 } else {
357 // A single-valued property
358 log.debug(property.getPath() + "=" + property.getString());
359 }
360 }
361 } catch (Exception e) {
362 log.error("Could not debug " + node, e);
363 }
364
365 }
366
367 /**
368 * Copies recursively the content of a node to another one. Mixin are NOT
369 * copied.
370 */
371 public static void copy(Node fromNode, Node toNode) {
372 try {
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()
379 .isProtected())
380 continue properties;
381
382 toNode.setProperty(fromProperty.getName(),
383 fromProperty.getValue());
384 }
385
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 + "]";
391 Node toChild;
392 if (toNode.hasNode(nodeRelPath))
393 toChild = toNode.getNode(nodeRelPath);
394 else
395 toChild = toNode.addNode(fromChild.getName(), fromChild
396 .getPrimaryNodeType().getName());
397 copy(fromChild, toChild);
398 }
399 } catch (RepositoryException e) {
400 throw new ArgeoException("Cannot copy " + fromNode + " to "
401 + toNode, e);
402 }
403 }
404
405 /**
406 * Check whether all first-level properties (except jcr:* properties) are
407 * equal. Skip jcr:* properties
408 */
409 public static Boolean allPropertiesEquals(Node reference, Node observed,
410 Boolean onlyCommonProperties) {
411 try {
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:"))
417 continue props;
418
419 if (!observed.hasProperty(propName))
420 if (onlyCommonProperties)
421 continue props;
422 else
423 return false;
424 // TODO: deal with multiple property values?
425 if (!observed.getProperty(propName).getValue()
426 .equals(propReference.getValue()))
427 return false;
428 }
429 return true;
430 } catch (RepositoryException e) {
431 throw new ArgeoException("Cannot check all properties equals of "
432 + reference + " and " + observed, e);
433 }
434 }
435
436 public static Map<String, PropertyDiff> diffProperties(Node reference,
437 Node observed) {
438 Map<String, PropertyDiff> diffs = new TreeMap<String, PropertyDiff>();
439 diffPropertiesLevel(diffs, null, reference, observed);
440 return diffs;
441 }
442
443 /**
444 * Compare the properties of two nodes. Recursivity to child nodes is not
445 * yet supported. Skip jcr:* properties.
446 */
447 static void diffPropertiesLevel(Map<String, PropertyDiff> diffs,
448 String baseRelPath, Node reference, Node observed) {
449 try {
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:"))
456 continue props;
457
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);
463 } else {
464 if (p.isMultiple())
465 continue props;
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,
472 newValue);
473 diffs.put(relPath, pDiff);
474 }
475 }
476 }
477 // check added
478 pit = observed.getProperties();
479 props: while (pit.hasNext()) {
480 Property p = pit.nextProperty();
481 String name = p.getName();
482 if (name.startsWith("jcr:"))
483 continue props;
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);
489 }
490 }
491 } catch (RepositoryException e) {
492 throw new ArgeoException("Cannot diff " + reference + " and "
493 + observed, e);
494 }
495 }
496
497 /**
498 * Compare only a restricted list of properties of two nodes. No
499 * recursivity.
500 *
501 */
502 public static Map<String, PropertyDiff> diffProperties(Node reference,
503 Node observed, List<String> properties) {
504 Map<String, PropertyDiff> diffs = new TreeMap<String, PropertyDiff>();
505 try {
506 Iterator<String> pit = properties.iterator();
507
508 props: while (pit.hasNext()) {
509 String name = pit.next();
510 if (!reference.hasProperty(name)) {
511 if (!observed.hasProperty(name))
512 continue props;
513 Value val = observed.getProperty(name).getValue();
514 try {
515 // empty String but not null
516 if ("".equals(val.getString()))
517 continue props;
518 } catch (Exception e) {
519 // not parseable as String, silent
520 }
521 PropertyDiff pDiff = new PropertyDiff(PropertyDiff.ADDED,
522 name, null, val);
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);
528 } else {
529 Value referenceValue = reference.getProperty(name)
530 .getValue();
531 Value newValue = observed.getProperty(name).getValue();
532 if (!referenceValue.equals(newValue)) {
533 PropertyDiff pDiff = new PropertyDiff(
534 PropertyDiff.MODIFIED, name, referenceValue,
535 newValue);
536 diffs.put(name, pDiff);
537 }
538 }
539 }
540 } catch (RepositoryException e) {
541 throw new ArgeoException("Cannot diff " + reference + " and "
542 + observed, e);
543 }
544 return diffs;
545 }
546
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)
551 return propertyName;
552 else
553 return baseRelPath + '/' + propertyName;
554 }
555
556 /**
557 * Normalize a name so taht it can be stores in contexts not supporting
558 * names with ':' (typically databases). Replaces ':' by '_'.
559 */
560 public static String normalize(String name) {
561 return name.replace(':', '_');
562 }
563
564 /** Cleanly disposes a {@link Binary} even if it is null. */
565 public static void closeQuietly(Binary binary) {
566 if (binary == null)
567 return;
568 binary.dispose();
569 }
570
571 /**
572 * Creates depth from a string (typically a username) by adding levels based
573 * on its first characters: "aBcD",2 => a/aB
574 */
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));
583 path.append(curr);
584 if (i < nbrOfChars - 1)
585 path.append('/');
586 }
587 return path.toString();
588 }
589
590 /**
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.
594 */
595 public static Repository getRepositoryByAlias(
596 RepositoryFactory repositoryFactory, String alias) {
597 try {
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 "
604 + alias, e);
605 }
606 }
607
608 /**
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.
612 */
613 public static Repository getRepositoryByUri(
614 RepositoryFactory repositoryFactory, String uri) {
615 try {
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 "
622 + uri, e);
623 }
624 }
625
626 /**
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.
630 */
631 public static void discardQuietly(Session session) {
632 try {
633 if (session != null)
634 session.refresh(false);
635 } catch (RepositoryException e) {
636 log.warn("Cannot quietly discard session " + session + ": "
637 + e.getMessage());
638 }
639 }
640
641 /** Logs out the session, not throwing any exception, even if it is null. */
642 public static void logoutQuietly(Session session) {
643 if (session != null)
644 session.logout();
645 }
646
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);
651 }
652
653 /**
654 * Returns the home node of the session user or null if none was found.
655 *
656 * @param session
657 * the session to use in order to perform the search, this can be
658 * a session with a different user ID than the one searched,
659 * typically when a system or admin session is used.
660 * @param userID
661 * the id of the user
662 */
663 public static Node getUserHome(Session session, String userID) {
664 try {
665 QueryObjectModelFactory qomf = session.getWorkspace()
666 .getQueryManager().getQOMFactory();
667
668 // query the user home for this user id
669 Selector userHomeSel = qomf.selector(ArgeoTypes.ARGEO_USER_HOME,
670 "userHome");
671 DynamicOperand userIdDop = qomf.propertyValue("userHome",
672 ArgeoNames.ARGEO_USER_ID);
673 StaticOperand userIdSop = qomf.literal(session.getValueFactory()
674 .createValue(userID));
675 Constraint constraint = qomf.comparison(userIdDop,
676 QueryObjectModelFactory.JCR_OPERATOR_EQUAL_TO, userIdSop);
677 Query query = qomf.createQuery(userHomeSel, constraint, null, null);
678 Node userHome = JcrUtils.querySingleNode(query);
679 return userHome;
680 } catch (RepositoryException e) {
681 throw new ArgeoException("Cannot find home for user " + userID, e);
682 }
683 }
684 }