Fix ' when using JCR query based on MessageFormat.
[lgpl/argeo-commons.git] / org.argeo.jcr / src / org / argeo / jcr / Jcr.java
index fe73b53f428a44fbc7ec7271757e0c99e40e4ca5..72e325d35a40c40ad22a712ae7561fd33ae6ed87 100644 (file)
@@ -1,8 +1,13 @@
 package org.argeo.jcr;
 
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
 import java.math.BigDecimal;
+import java.text.MessageFormat;
 import java.time.Instant;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Calendar;
 import java.util.Collections;
 import java.util.Date;
@@ -22,12 +27,16 @@ import javax.jcr.Session;
 import javax.jcr.Value;
 import javax.jcr.Workspace;
 import javax.jcr.nodetype.NodeType;
+import javax.jcr.query.Query;
+import javax.jcr.query.QueryManager;
 import javax.jcr.security.Privilege;
 import javax.jcr.version.Version;
 import javax.jcr.version.VersionHistory;
 import javax.jcr.version.VersionIterator;
 import javax.jcr.version.VersionManager;
 
+import org.apache.commons.io.IOUtils;
+
 /**
  * Utility class whose purpose is to make using JCR less verbose by
  * systematically using unchecked exceptions and returning <code>null</code>
@@ -94,62 +103,70 @@ public class Jcr {
 
        /**
         * @see Node#isNodeType(String)
-        * @throws IllegalStateException caused by {@link RepositoryException}
+        * @throws JcrException caused by {@link RepositoryException}
         */
        public static boolean isNodeType(Node node, String nodeTypeName) {
                try {
                        return node.isNodeType(nodeTypeName);
                } catch (RepositoryException e) {
-                       throw new IllegalStateException("Cannot get whether " + node + " is of type " + nodeTypeName, e);
+                       throw new JcrException("Cannot get whether " + node + " is of type " + nodeTypeName, e);
                }
        }
 
        /**
         * @see Node#hasNodes()
-        * @throws IllegalStateException caused by {@link RepositoryException}
+        * @throws JcrException caused by {@link RepositoryException}
         */
        public static boolean hasNodes(Node node) {
                try {
                        return node.hasNodes();
                } catch (RepositoryException e) {
-                       throw new IllegalStateException("Cannot get whether " + node + " has children.", e);
+                       throw new JcrException("Cannot get whether " + node + " has children.", e);
                }
        }
 
        /**
         * @see Node#getParent()
-        * @throws IllegalStateException caused by {@link RepositoryException}
+        * @throws JcrException caused by {@link RepositoryException}
         */
        public static Node getParent(Node node) {
                try {
                        return isRoot(node) ? null : node.getParent();
                } catch (RepositoryException e) {
-                       throw new IllegalStateException("Cannot get parent of " + node, e);
+                       throw new JcrException("Cannot get parent of " + node, e);
                }
        }
 
+       /**
+        * @see Node#getParent()
+        * @throws JcrException caused by {@link RepositoryException}
+        */
+       public static String getParentPath(Node node) {
+               return getPath(getParent(node));
+       }
+
        /**
         * Whether this node is the root node.
         * 
-        * @throws IllegalStateException caused by {@link RepositoryException}
+        * @throws JcrException caused by {@link RepositoryException}
         */
        public static boolean isRoot(Node node) {
                try {
                        return node.getDepth() == 0;
                } catch (RepositoryException e) {
-                       throw new IllegalStateException("Cannot get depth of " + node, e);
+                       throw new JcrException("Cannot get depth of " + node, e);
                }
        }
 
        /**
         * @see Node#getPath()
-        * @throws IllegalStateException caused by {@link RepositoryException}
+        * @throws JcrException caused by {@link RepositoryException}
         */
        public static String getPath(Node node) {
                try {
                        return node.getPath();
                } catch (RepositoryException e) {
-                       throw new IllegalStateException("Cannot get path of " + node, e);
+                       throw new JcrException("Cannot get path of " + node, e);
                }
        }
 
@@ -164,25 +181,67 @@ public class Jcr {
 
        /**
         * @see Node#getIdentifier()
-        * @throws IllegalStateException caused by {@link RepositoryException}
+        * @throws JcrException caused by {@link RepositoryException}
         */
        public static String getIdentifier(Node node) {
                try {
                        return node.getIdentifier();
                } catch (RepositoryException e) {
-                       throw new IllegalStateException("Cannot get identifier of " + node, e);
+                       throw new JcrException("Cannot get identifier of " + node, e);
                }
        }
 
        /**
         * @see Node#getName()
-        * @throws IllegalStateException caused by {@link RepositoryException}
+        * @throws JcrException caused by {@link RepositoryException}
         */
        public static String getName(Node node) {
                try {
                        return node.getName();
                } catch (RepositoryException e) {
-                       throw new IllegalStateException("Cannot get name of " + node, e);
+                       throw new JcrException("Cannot get name of " + node, e);
+               }
+       }
+
+       /**
+        * Returns the node name with its current index (useful for re-ordering).
+        * 
+        * @see Node#getName()
+        * @see Node#getIndex()
+        * @throws JcrException caused by {@link RepositoryException}
+        */
+       public static String getIndexedName(Node node) {
+               try {
+                       return node.getName() + "[" + node.getIndex() + "]";
+               } catch (RepositoryException e) {
+                       throw new JcrException("Cannot get name of " + node, e);
+               }
+       }
+
+       /**
+        * @see Node#getProperty(String)
+        * @throws JcrException caused by {@link RepositoryException}
+        */
+       public static Property getProperty(Node node, String property) {
+               try {
+                       if (node.hasProperty(property))
+                               return node.getProperty(property);
+                       else
+                               return null;
+               } catch (RepositoryException e) {
+                       throw new JcrException("Cannot get property " + property + " of " + node, e);
+               }
+       }
+
+       /**
+        * @see Node#getIndex()
+        * @throws JcrException caused by {@link RepositoryException}
+        */
+       public static int getIndex(Node node) {
+               try {
+                       return node.getIndex();
+               } catch (RepositoryException e) {
+                       throw new JcrException("Cannot get index of " + node, e);
                }
        }
 
@@ -212,20 +271,20 @@ public class Jcr {
        /**
         * @return the children as an {@link Iterable} for use in for-each llops.
         * @see Node#getNodes()
-        * @throws IllegalStateException caused by {@link RepositoryException}
+        * @throws JcrException caused by {@link RepositoryException}
         */
        public static Iterable<Node> nodes(Node node) {
                try {
                        return iterate(node.getNodes());
                } catch (RepositoryException e) {
-                       throw new IllegalStateException("Cannot get children of " + node, e);
+                       throw new JcrException("Cannot get children of " + node, e);
                }
        }
 
        /**
         * @return the children as a (possibly empty) {@link List}.
         * @see Node#getNodes()
-        * @throws IllegalStateException caused by {@link RepositoryException}
+        * @throws JcrException caused by {@link RepositoryException}
         */
        public static List<Node> getNodes(Node node) {
                List<Node> nodes = new ArrayList<>();
@@ -238,14 +297,14 @@ public class Jcr {
                        } else
                                return nodes;
                } catch (RepositoryException e) {
-                       throw new IllegalStateException("Cannot get children of " + node, e);
+                       throw new JcrException("Cannot get children of " + node, e);
                }
        }
 
        /**
         * @return the child or <code>null</node> if not found
         * @see Node#getNode(String)
-        * @throws IllegalStateException caused by {@link RepositoryException}
+        * @throws JcrException caused by {@link RepositoryException}
         */
        public static Node getNode(Node node, String child) {
                try {
@@ -254,14 +313,14 @@ public class Jcr {
                        else
                                return null;
                } catch (RepositoryException e) {
-                       throw new IllegalStateException("Cannot get child of " + node, e);
+                       throw new JcrException("Cannot get child of " + node, e);
                }
        }
 
        /**
         * @return the node at this path or <code>null</node> if not found
         * @see Session#getNode(String)
-        * @throws IllegalStateException caused by {@link RepositoryException}
+        * @throws JcrException caused by {@link RepositoryException}
         */
        public static Node getNode(Session session, String path) {
                try {
@@ -270,14 +329,93 @@ public class Jcr {
                        else
                                return null;
                } catch (RepositoryException e) {
-                       throw new IllegalStateException("Cannot get node " + path, e);
+                       throw new JcrException("Cannot get node " + path, e);
+               }
+       }
+
+       /**
+        * Add a node to this parent, setting its primary type and its mixins.
+        * 
+        * @param parent      the parent node
+        * @param name        the name of the node, if <code>null</code>, the primary
+        *                    type will be used (typically for XML structures)
+        * @param primaryType the primary type, if <code>null</code>
+        *                    {@link NodeType#NT_UNSTRUCTURED} will be used.
+        * @param mixins      the mixins
+        * @return the created node
+        * @see Node#addNode(String, String)
+        * @see Node#addMixin(String)
+        */
+       public static Node addNode(Node parent, String name, String primaryType, String... mixins) {
+               if (name == null && primaryType == null)
+                       throw new IllegalArgumentException("Both node name and primary type cannot be null");
+               try {
+                       Node newNode = parent.addNode(name == null ? primaryType : name,
+                                       primaryType == null ? NodeType.NT_UNSTRUCTURED : primaryType);
+                       for (String mixin : mixins) {
+                               newNode.addMixin(mixin);
+                       }
+                       return newNode;
+               } catch (RepositoryException e) {
+                       throw new JcrException("Cannot add node " + name + " to " + parent, e);
+               }
+       }
+
+       /**
+        * Add an {@link NodeType#NT_BASE} node to this parent.
+        * 
+        * @param parent the parent node
+        * @param name   the name of the node, cannot be <code>null</code>
+        * @return the created node
+        * 
+        * @see Node#addNode(String)
+        */
+       public static Node addNode(Node parent, String name) {
+               if (name == null)
+                       throw new IllegalArgumentException("Node name cannot be null");
+               try {
+                       Node newNode = parent.addNode(name);
+                       return newNode;
+               } catch (RepositoryException e) {
+                       throw new JcrException("Cannot add node " + name + " to " + parent, e);
+               }
+       }
+
+       /**
+        * Add mixins to a node.
+        * 
+        * @param node   the node
+        * @param mixins the mixins
+        * @return the created node
+        * @see Node#addMixin(String)
+        */
+       public static void addMixin(Node node, String... mixins) {
+               try {
+                       for (String mixin : mixins) {
+                               node.addMixin(mixin);
+                       }
+               } catch (RepositoryException e) {
+                       throw new JcrException("Cannot add mixins " + Arrays.asList(mixins) + " to " + node, e);
+               }
+       }
+
+       /**
+        * Removes this node.
+        * 
+        * @see Node#remove()
+        */
+       public static void remove(Node node) {
+               try {
+                       node.remove();
+               } catch (RepositoryException e) {
+                       throw new JcrException("Cannot remove node " + node, e);
                }
        }
 
        /**
         * @return the node with htis id or <code>null</node> if not found
         * @see Session#getNodeByIdentifier(String)
-        * @throws IllegalStateException caused by {@link RepositoryException}
+        * @throws JcrException caused by {@link RepositoryException}
         */
        public static Node getNodeById(Session session, String id) {
                try {
@@ -285,7 +423,7 @@ public class Jcr {
                } catch (ItemNotFoundException e) {
                        return null;
                } catch (RepositoryException e) {
-                       throw new IllegalStateException("Cannot get node with id " + id, e);
+                       throw new JcrException("Cannot get node with id " + id, e);
                }
        }
 
@@ -293,24 +431,62 @@ public class Jcr {
         * Set a property to the given value, or remove it if the value is
         * <code>null</code>.
         * 
-        * @throws IllegalStateException caused by {@link RepositoryException}
+        * @throws JcrException caused by {@link RepositoryException}
         */
        public static void set(Node node, String property, Object value) {
                try {
-                       if (!node.hasProperty(property))
-                               throw new IllegalArgumentException("No property " + property + " in " + node);
+                       if (!node.hasProperty(property)) {
+                               if (value != null) {
+                                       if (value instanceof List) {// multiple
+                                               List<?> lst = (List<?>) value;
+                                               String[] values = new String[lst.size()];
+                                               for (int i = 0; i < lst.size(); i++) {
+                                                       values[i] = lst.get(i).toString();
+                                               }
+                                               node.setProperty(property, values);
+                                       } else {
+                                               node.setProperty(property, value.toString());
+                                       }
+                               }
+                               return;
+                       }
                        Property prop = node.getProperty(property);
                        if (value == null) {
                                prop.remove();
                                return;
                        }
 
+                       // multiple
+                       if (value instanceof List) {
+                               List<?> lst = (List<?>) value;
+                               String[] values = new String[lst.size()];
+                               // TODO better cast?
+                               for (int i = 0; i < lst.size(); i++) {
+                                       values[i] = lst.get(i).toString();
+                               }
+                               if (!prop.isMultiple())
+                                       prop.remove();
+                               node.setProperty(property, values);
+                               return;
+                       }
+
+                       // single
+                       if (prop.isMultiple()) {
+                               prop.remove();
+                               node.setProperty(property, value.toString());
+                               return;
+                       }
+
                        if (value instanceof String)
                                prop.setValue((String) value);
                        else if (value instanceof Long)
                                prop.setValue((Long) value);
+                       else if (value instanceof Integer)
+                               prop.setValue(((Integer) value).longValue());
                        else if (value instanceof Double)
                                prop.setValue((Double) value);
+                       else if (value instanceof Float)
+                               prop.setValue(((Float) value).doubleValue());
                        else if (value instanceof Calendar)
                                prop.setValue((Calendar) value);
                        else if (value instanceof BigDecimal)
@@ -327,7 +503,7 @@ public class Jcr {
                        } else // try with toString()
                                prop.setValue(value.toString());
                } catch (RepositoryException e) {
-                       throw new IllegalStateException("Cannot set property " + property + " of " + node + " to " + value, e);
+                       throw new JcrException("Cannot set property " + property + " of " + node + " to " + value, e);
                }
        }
 
@@ -337,28 +513,38 @@ public class Jcr {
         * @return the value of
         *         {@link Node#getProperty(String)}.{@link Property#getString()} or
         *         <code>null</code> if the property does not exist.
-        * @throws IllegalStateException caused by {@link RepositoryException}
+        * @throws JcrException caused by {@link RepositoryException}
         */
        public static String get(Node node, String property) {
                return get(node, property, null);
        }
 
        /**
-        * Get property as a {@link String}.
+        * Get property as a {@link String}. If the property is multiple it returns the
+        * first value.
         * 
         * @return the value of
         *         {@link Node#getProperty(String)}.{@link Property#getString()} or
         *         <code>defaultValue</code> if the property does not exist.
-        * @throws IllegalStateException caused by {@link RepositoryException}
+        * @throws JcrException caused by {@link RepositoryException}
         */
        public static String get(Node node, String property, String defaultValue) {
                try {
-                       if (node.hasProperty(property))
-                               return node.getProperty(property).getString();
-                       else
+                       if (node.hasProperty(property)) {
+                               Property p = node.getProperty(property);
+                               if (!p.isMultiple())
+                                       return p.getString();
+                               else {
+                                       Value[] values = p.getValues();
+                                       if (values.length == 0)
+                                               return defaultValue;
+                                       else
+                                               return values[0].getString();
+                               }
+                       } else
                                return defaultValue;
                } catch (RepositoryException e) {
-                       throw new IllegalStateException("Cannot retrieve property " + property + " from " + node);
+                       throw new JcrException("Cannot retrieve property " + property + " from " + node, e);
                }
        }
 
@@ -367,7 +553,7 @@ public class Jcr {
         * 
         * @return {@link Node#getProperty(String)} or <code>null</code> if the property
         *         does not exist.
-        * @throws IllegalStateException caused by {@link RepositoryException}
+        * @throws JcrException caused by {@link RepositoryException}
         */
        public static Value getValue(Node node, String property) {
                try {
@@ -376,7 +562,7 @@ public class Jcr {
                        else
                                return null;
                } catch (RepositoryException e) {
-                       throw new IllegalStateException("Cannot retrieve property " + property + " from " + node);
+                       throw new JcrException("Cannot retrieve property " + property + " from " + node, e);
                }
        }
 
@@ -386,29 +572,21 @@ public class Jcr {
         * @return the value of {@link Node#getProperty(String)} or
         *         <code>defaultValue</code> if the property does not exist.
         * @throws IllegalArgumentException if the value could not be cast
-        * @throws IllegalStateException    in case of unexpected
+        * @throws JcrException             in case of unexpected
         *                                  {@link RepositoryException}
         */
        @SuppressWarnings("unchecked")
        public static <T> T getAs(Node node, String property, T defaultValue) {
                try {
+                       // TODO deal with multiple
                        if (node.hasProperty(property)) {
                                Property p = node.getProperty(property);
                                try {
-                                       switch (p.getType()) {
-                                       case PropertyType.STRING:
-                                               return (T) node.getProperty(property).getString();
-                                       case PropertyType.DOUBLE:
-                                               return (T) (Double) node.getProperty(property).getDouble();
-                                       case PropertyType.LONG:
-                                               return (T) (Long) node.getProperty(property).getLong();
-                                       case PropertyType.BOOLEAN:
-                                               return (T) (Boolean) node.getProperty(property).getBoolean();
-                                       case PropertyType.DATE:
-                                               return (T) node.getProperty(property).getDate();
-                                       default:
-                                               return (T) node.getProperty(property).getString();
+                                       if (p.isMultiple()) {
+                                               throw new UnsupportedOperationException("Multiple values properties are not supported");
                                        }
+                                       Value value = p.getValue();
+                                       return (T) get(value);
                                } catch (ClassCastException e) {
                                        throw new IllegalArgumentException(
                                                        "Cannot cast property of type " + PropertyType.nameFromValue(p.getType()), e);
@@ -417,7 +595,87 @@ public class Jcr {
                                return defaultValue;
                        }
                } catch (RepositoryException e) {
-                       throw new IllegalStateException("Cannot retrieve property " + property + " from " + node);
+                       throw new JcrException("Cannot retrieve property " + property + " from " + node, e);
+               }
+       }
+
+       /**
+        * Get a multiple property as a list, doing a best effort to cast it as the
+        * target list.
+        * 
+        * @return the value of {@link Node#getProperty(String)}.
+        * @throws IllegalArgumentException if the value could not be cast
+        * @throws JcrException             in case of unexpected
+        *                                  {@link RepositoryException}
+        */
+       public static <T> List<T> getMultiple(Node node, String property) {
+               try {
+                       if (node.hasProperty(property)) {
+                               Property p = node.getProperty(property);
+                               return getMultiple(p);
+                       } else {
+                               return null;
+                       }
+               } catch (RepositoryException e) {
+                       throw new JcrException("Cannot retrieve multiple values property " + property + " from " + node, e);
+               }
+       }
+
+       /**
+        * Get a multiple property as a list, doing a best effort to cast it as the
+        * target list.
+        */
+       @SuppressWarnings("unchecked")
+       public static <T> List<T> getMultiple(Property p) {
+               try {
+                       List<T> res = new ArrayList<>();
+                       if (!p.isMultiple()) {
+                               res.add((T) get(p.getValue()));
+                               return res;
+                       }
+                       Value[] values = p.getValues();
+                       for (Value value : values) {
+                               res.add((T) get(value));
+                       }
+                       return res;
+               } catch (ClassCastException | RepositoryException e) {
+                       throw new IllegalArgumentException("Cannot get property " + p, e);
+               }
+       }
+
+       /** Cast a {@link Value} to a standard Java object. */
+       public static Object get(Value value) {
+               Binary binary = null;
+               try {
+                       switch (value.getType()) {
+                       case PropertyType.STRING:
+                               return value.getString();
+                       case PropertyType.DOUBLE:
+                               return (Double) value.getDouble();
+                       case PropertyType.LONG:
+                               return (Long) value.getLong();
+                       case PropertyType.BOOLEAN:
+                               return (Boolean) value.getBoolean();
+                       case PropertyType.DATE:
+                               return value.getDate();
+                       case PropertyType.BINARY:
+                               binary = value.getBinary();
+                               byte[] arr = null;
+                               try (InputStream in = binary.getStream(); ByteArrayOutputStream out = new ByteArrayOutputStream();) {
+                                       IOUtils.copy(in, out);
+                                       arr = out.toByteArray();
+                               } catch (IOException e) {
+                                       throw new RuntimeException("Cannot read binary from " + value, e);
+                               }
+                               return arr;
+                       default:
+                               return value.getString();
+                       }
+               } catch (RepositoryException e) {
+                       throw new JcrException("Cannot cast value from " + value, e);
+               } finally {
+                       if (binary != null)
+                               binary.dispose();
                }
        }
 
@@ -436,7 +694,7 @@ public class Jcr {
                try {
                        return node.getSession();
                } catch (RepositoryException e) {
-                       throw new IllegalStateException("Cannot retrieve session related to " + node, e);
+                       throw new JcrException("Cannot retrieve session related to " + node, e);
                }
        }
 
@@ -445,7 +703,16 @@ public class Jcr {
                try {
                        return session.getRootNode();
                } catch (RepositoryException e) {
-                       throw new IllegalStateException("Cannot get root node for " + session, e);
+                       throw new JcrException("Cannot get root node for " + session, e);
+               }
+       }
+
+       /** Whether this item exists. */
+       public static boolean itemExists(Session session, String path) {
+               try {
+                       return session.itemExists(path);
+               } catch (RepositoryException e) {
+                       throw new JcrException("Cannot check whether " + path + " exists", e);
                }
        }
 
@@ -463,7 +730,7 @@ public class Jcr {
                        if (session.hasPendingChanges())
                                session.save();
                } catch (RepositoryException e) {
-                       throw new IllegalStateException("Cannot save session related to " + node + " in workspace "
+                       throw new JcrException("Cannot save session related to " + node + " in workspace "
                                        + session(node).getWorkspace().getName(), e);
                }
        }
@@ -506,7 +773,7 @@ public class Jcr {
                        Session session = node.getSession();
                        JcrUtils.addPrivilege(session, node.getPath(), principal, privilege);
                } catch (RepositoryException e) {
-                       throw new IllegalStateException("Cannot add privilege " + privilege + " to " + node, e);
+                       throw new JcrException("Cannot add privilege " + privilege + " to " + node, e);
                }
        }
 
@@ -518,7 +785,7 @@ public class Jcr {
                try {
                        return node.isCheckedOut();
                } catch (RepositoryException e) {
-                       throw new IllegalStateException("Cannot retrieve checked out status of " + node, e);
+                       throw new JcrException("Cannot retrieve checked out status of " + node, e);
                }
        }
 
@@ -527,7 +794,7 @@ public class Jcr {
                try {
                        versionManager(node).checkpoint(node.getPath());
                } catch (RepositoryException e) {
-                       throw new IllegalStateException("Cannot check in " + node, e);
+                       throw new JcrException("Cannot check in " + node, e);
                }
        }
 
@@ -536,7 +803,7 @@ public class Jcr {
                try {
                        versionManager(node).checkin(node.getPath());
                } catch (RepositoryException e) {
-                       throw new IllegalStateException("Cannot check in " + node, e);
+                       throw new JcrException("Cannot check in " + node, e);
                }
        }
 
@@ -545,7 +812,7 @@ public class Jcr {
                try {
                        versionManager(node).checkout(node.getPath());
                } catch (RepositoryException e) {
-                       throw new IllegalStateException("Cannot check out " + node, e);
+                       throw new JcrException("Cannot check out " + node, e);
                }
        }
 
@@ -554,7 +821,7 @@ public class Jcr {
                try {
                        return node.getSession().getWorkspace().getVersionManager();
                } catch (RepositoryException e) {
-                       throw new IllegalStateException("Cannot get version manager from " + node, e);
+                       throw new JcrException("Cannot get version manager from " + node, e);
                }
        }
 
@@ -563,7 +830,7 @@ public class Jcr {
                try {
                        return versionManager(node).getVersionHistory(node.getPath());
                } catch (RepositoryException e) {
-                       throw new IllegalStateException("Cannot get version history from " + node, e);
+                       throw new JcrException("Cannot get version history from " + node, e);
                }
        }
 
@@ -581,7 +848,7 @@ public class Jcr {
                        Collections.reverse(lst);
                        return lst;
                } catch (RepositoryException e) {
-                       throw new IllegalStateException("Cannot get linear versions from " + versionHistory, e);
+                       throw new JcrException("Cannot get linear versions from " + versionHistory, e);
                }
        }
 
@@ -590,7 +857,7 @@ public class Jcr {
                try {
                        return version.getFrozenNode();
                } catch (RepositoryException e) {
-                       throw new IllegalStateException("Cannot get frozen node from " + version, e);
+                       throw new JcrException("Cannot get frozen node from " + version, e);
                }
        }
 
@@ -599,7 +866,7 @@ public class Jcr {
                try {
                        return versionManager(node).getBaseVersion(node.getPath());
                } catch (RepositoryException e) {
-                       throw new IllegalStateException("Cannot get base version from " + node, e);
+                       throw new JcrException("Cannot get base version from " + node, e);
                }
        }
 
@@ -617,7 +884,7 @@ public class Jcr {
                                throw new IllegalArgumentException(fileNode + " must be a file.");
                        return getBinarySize(fileNode.getNode(Node.JCR_CONTENT).getProperty(Property.JCR_DATA).getBinary());
                } catch (RepositoryException e) {
-                       throw new IllegalStateException("Cannot get file size of " + fileNode, e);
+                       throw new JcrException("Cannot get file size of " + fileNode, e);
                }
        }
 
@@ -628,8 +895,77 @@ public class Jcr {
                                return binary.getSize();
                        }
                } catch (RepositoryException e) {
-                       throw new IllegalStateException("Cannot get file size of binary " + binaryArg, e);
+                       throw new JcrException("Cannot get file size of binary " + binaryArg, e);
+               }
+       }
+
+       // QUERY
+       /** Creates a JCR-SQL2 query using {@link MessageFormat}. */
+       public static Query createQuery(QueryManager qm, String sql, Object... args) {
+               // fix single quotes
+               sql = sql.replaceAll("'", "''");
+               String query = MessageFormat.format(sql, args);
+               try {
+                       return qm.createQuery(query, Query.JCR_SQL2);
+               } catch (RepositoryException e) {
+                       throw new JcrException("Cannot create JCR-SQL2 query from " + query, e);
+               }
+       }
+
+       /** Executes a JCR-SQL2 query using {@link MessageFormat}. */
+       public static NodeIterator executeQuery(QueryManager qm, String sql, Object... args) {
+               Query query = createQuery(qm, sql, args);
+               try {
+                       return query.execute().getNodes();
+               } catch (RepositoryException e) {
+                       throw new JcrException("Cannot execute query " + sql + " with arguments " + Arrays.asList(args), e);
+               }
+       }
+
+       /** Executes a JCR-SQL2 query using {@link MessageFormat}. */
+       public static NodeIterator executeQuery(Session session, String sql, Object... args) {
+               QueryManager queryManager;
+               try {
+                       queryManager = session.getWorkspace().getQueryManager();
+               } catch (RepositoryException e) {
+                       throw new JcrException("Cannot get query manager from session " + session, e);
+               }
+               return executeQuery(queryManager, sql, args);
+       }
+
+       /**
+        * Executes a JCR-SQL2 query using {@link MessageFormat}, which must return a
+        * single node at most.
+        * 
+        * @return the node or <code>null</code> if not found.
+        */
+       public static Node getNode(QueryManager qm, String sql, Object... args) {
+               NodeIterator nit = executeQuery(qm, sql, args);
+               if (nit.hasNext()) {
+                       Node node = nit.nextNode();
+                       if (nit.hasNext())
+                               throw new IllegalStateException(
+                                               "Query " + sql + " with arguments " + Arrays.asList(args) + " returned more than one node.");
+                       return node;
+               } else {
+                       return null;
+               }
+       }
+
+       /**
+        * Executes a JCR-SQL2 query using {@link MessageFormat}, which must return a
+        * single node at most.
+        * 
+        * @return the node or <code>null</code> if not found.
+        */
+       public static Node getNode(Session session, String sql, Object... args) {
+               QueryManager queryManager;
+               try {
+                       queryManager = session.getWorkspace().getQueryManager();
+               } catch (RepositoryException e) {
+                       throw new JcrException("Cannot get query manager from session " + session, e);
                }
+               return getNode(queryManager, sql, args);
        }
 
        /** Singleton. */