You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@chemistry.apache.org by ju...@apache.org on 2011/04/01 13:59:47 UTC

svn commit: r1087664 [2/3] - in /chemistry/opencmis/trunk: chemistry-opencmis-client/chemistry-opencmis-client-bindings/src/test/java/org/apache/chemistry/opencmis/client/bindings/framework/ chemistry-opencmis-server/chemistry-opencmis-server-jcr/ chem...

Added: chemistry/opencmis/trunk/chemistry-opencmis-server/chemistry-opencmis-server-jcr/src/main/java/org/apache/chemistry/opencmis/jcr/query/Evaluator.java
URL: http://svn.apache.org/viewvc/chemistry/opencmis/trunk/chemistry-opencmis-server/chemistry-opencmis-server-jcr/src/main/java/org/apache/chemistry/opencmis/jcr/query/Evaluator.java?rev=1087664&view=auto
==============================================================================
--- chemistry/opencmis/trunk/chemistry-opencmis-server/chemistry-opencmis-server-jcr/src/main/java/org/apache/chemistry/opencmis/jcr/query/Evaluator.java (added)
+++ chemistry/opencmis/trunk/chemistry-opencmis-server/chemistry-opencmis-server-jcr/src/main/java/org/apache/chemistry/opencmis/jcr/query/Evaluator.java Fri Apr  1 11:59:46 2011
@@ -0,0 +1,119 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.chemistry.opencmis.jcr.query;
+
+import java.util.GregorianCalendar;
+import java.util.List;
+
+/**
+ * Evaluator for CMIS query parse trees.
+ *
+ * @param <T>  The result type of the evaluation of the parse tree.
+ * @see ParseTreeWalker
+ */
+public interface Evaluator<T> {
+
+    /** Create a new instance of this <code>Evaluator</code>. */
+    Evaluator<T> op();
+
+    /** Handle {@link org.apache.chemistry.opencmis.server.support.query.CmisQlStrictLexer#NOT} nodes */
+    T not(T op);
+
+    /** Handle {@link org.apache.chemistry.opencmis.server.support.query.CmisQlStrictLexer#AND} nodes */
+    T and(T op1, T op2);
+
+    /** Handle {@link org.apache.chemistry.opencmis.server.support.query.CmisQlStrictLexer#OR} nodes */
+    T or(T op1, T op2);
+
+    /** Handle {@link org.apache.chemistry.opencmis.server.support.query.CmisQlStrictLexer#EQ} nodes */
+    T eq(T op1, T op2);
+
+    /** Handle {@link org.apache.chemistry.opencmis.server.support.query.CmisQlStrictLexer#NEQ} nodes */
+    T neq(T op1, T op2);
+
+    /** Handle {@link org.apache.chemistry.opencmis.server.support.query.CmisQlStrictLexer#GT} nodes */
+    T gt(T op1, T op2);
+
+    /** Handle {@link org.apache.chemistry.opencmis.server.support.query.CmisQlStrictLexer#GTEQ} nodes */
+    T gteq(T op1, T op2);
+
+    /** Handle {@link org.apache.chemistry.opencmis.server.support.query.CmisQlStrictLexer#LT} nodes */
+    T lt(T op1, T op2);
+
+    /** Handle {@link org.apache.chemistry.opencmis.server.support.query.CmisQlStrictLexer#LTEQ} nodes */
+    T lteq(T op1, T op2);
+
+    /** Handle {@link org.apache.chemistry.opencmis.server.support.query.CmisQlStrictLexer#IN} nodes */
+    T in(T op1, T op2);
+
+    /** Handle {@link org.apache.chemistry.opencmis.server.support.query.CmisQlStrictLexer#NOT_IN} nodes */
+    T notIn(T op1, T op2);
+
+    /** Handle {@link org.apache.chemistry.opencmis.server.support.query.CmisQlStrictLexer#IN_ANY} nodes */
+    T inAny(T op1, T op2);
+
+    /** Handle {@link org.apache.chemistry.opencmis.server.support.query.CmisQlStrictLexer#NOT_IN_ANY} nodes */
+    T notInAny(T op1, T op2);
+
+    /** Handle {@link org.apache.chemistry.opencmis.server.support.query.CmisQlStrictLexer#EQ_ANY} nodes */
+    T eqAny(T op1, T op2);
+
+    /** Handle {@link org.apache.chemistry.opencmis.server.support.query.CmisQlStrictLexer#IS_NULL} nodes */
+    T isNull(T op);
+
+    /** Handle {@link org.apache.chemistry.opencmis.server.support.query.CmisQlStrictLexer#IS_NOT_NULL} nodes */
+    T notIsNull(T op);
+
+    /** Handle {@link org.apache.chemistry.opencmis.server.support.query.CmisQlStrictLexer#LIKE} nodes */
+    T like(T op1, T op2);
+
+    /** Handle {@link org.apache.chemistry.opencmis.server.support.query.CmisQlStrictLexer#NOT_LIKE} nodes */
+    T notLike(T op1, T op2);
+
+    /** Handle {@link org.apache.chemistry.opencmis.server.support.query.CmisQlStrictLexer#CONTAINS} nodes */
+    T contains(T op1, T op2);
+
+    /** Handle {@link org.apache.chemistry.opencmis.server.support.query.CmisQlStrictLexer#IN_FOLDER} nodes */
+    T inFolder(T op1, T op2);
+
+    /** Handle {@link org.apache.chemistry.opencmis.server.support.query.CmisQlStrictLexer#IN_TREE} nodes */
+    T inTree(T op1, T op2);
+
+    /** Handle {@link org.apache.chemistry.opencmis.server.support.query.CmisQlStrictLexer#IN_LIST} nodes */
+    T list(List<T> ops);
+
+    /** Handle {@link org.apache.chemistry.opencmis.server.support.query.CmisQlStrictLexer#BOOL_LIT} nodes */
+    T value(boolean value);
+
+    /** Handle {@link org.apache.chemistry.opencmis.server.support.query.CmisQlStrictLexer#NUM_LIT} nodes */
+    T value(double value);
+
+    /** Handle {@link org.apache.chemistry.opencmis.server.support.query.CmisQlStrictLexer#NUM_LIT} nodes */
+    T value(long value);
+
+    /** Handle {@link org.apache.chemistry.opencmis.server.support.query.CmisQlStrictLexer#STRING_LIT} nodes */
+    T value(String value);
+
+    /** Handle {@link org.apache.chemistry.opencmis.server.support.query.CmisQlStrictLexer#TIME_LIT} nodes */
+    T value(GregorianCalendar value);
+
+    /** Handle {@link org.apache.chemistry.opencmis.server.support.query.CmisQlStrictLexer#COL} nodes */
+    T col(String name);
+}

Added: chemistry/opencmis/trunk/chemistry-opencmis-server/chemistry-opencmis-server-jcr/src/main/java/org/apache/chemistry/opencmis/jcr/query/EvaluatorBase.java
URL: http://svn.apache.org/viewvc/chemistry/opencmis/trunk/chemistry-opencmis-server/chemistry-opencmis-server-jcr/src/main/java/org/apache/chemistry/opencmis/jcr/query/EvaluatorBase.java?rev=1087664&view=auto
==============================================================================
--- chemistry/opencmis/trunk/chemistry-opencmis-server/chemistry-opencmis-server-jcr/src/main/java/org/apache/chemistry/opencmis/jcr/query/EvaluatorBase.java (added)
+++ chemistry/opencmis/trunk/chemistry-opencmis-server/chemistry-opencmis-server-jcr/src/main/java/org/apache/chemistry/opencmis/jcr/query/EvaluatorBase.java Fri Apr  1 11:59:46 2011
@@ -0,0 +1,148 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.chemistry.opencmis.jcr.query;
+
+import org.apache.chemistry.opencmis.commons.exceptions.CmisNotSupportedException;
+
+import java.util.GregorianCalendar;
+import java.util.List;
+
+/**
+ * This abstract base class implements all methods of the {@link Evaluator} interface
+ * by throwing a {@link CmisNotSupportedException}.
+ */
+public abstract class EvaluatorBase<T> implements Evaluator<T> {
+    public Evaluator<T> op() {
+        throw new CmisNotSupportedException();
+    }
+
+    public T not(T op) {
+        throw new CmisNotSupportedException("Not supported in query: not");
+    }
+
+    public T and(T op1, T op2) {
+        throw new CmisNotSupportedException("Not supported in query: and");
+    }
+
+    public T or(T op1, T op2) {
+        throw new CmisNotSupportedException("Not supported in query: or");
+    }
+
+    public T eq(T op1, T op2) {
+        throw new CmisNotSupportedException("Not supported in query: =");
+    }
+
+    public T neq(T op1, T op2) {
+        throw new CmisNotSupportedException("Not supported in query: !=");
+    }
+
+    public T gt(T op1, T op2) {
+        throw new CmisNotSupportedException("Not supported in query: >");
+    }
+
+    public T gteq(T op1, T op2) {
+        throw new CmisNotSupportedException("Not supported in query: >=");
+    }
+
+    public T lt(T op1, T op2) {
+        throw new CmisNotSupportedException("Not supported in query: <");
+    }
+
+    public T lteq(T op1, T op2) {
+        throw new CmisNotSupportedException("Not supported in query: <=");
+    }
+
+    public T in(T op1, T op2) {
+        throw new CmisNotSupportedException("Not supported in query: in");
+    }
+
+    public T notIn(T op1, T op2) {
+        throw new CmisNotSupportedException("Not supported in query: not in");
+    }
+
+    public T inAny(T op1, T op2) {
+        throw new CmisNotSupportedException("Not supported in query: in");
+    }
+
+    public T notInAny(T op1, T op2) {
+        throw new CmisNotSupportedException("Not supported in query: not in");
+    }
+
+    public T eqAny(T op1, T op2) {
+        throw new CmisNotSupportedException("Not supported in query: = ANY");
+    }
+
+    public T isNull(T op) {
+        throw new CmisNotSupportedException("Not supported in query: is null");
+    }
+
+    public T notIsNull(T op) {
+        throw new CmisNotSupportedException("Not supported in query: is not null");
+    }
+
+    public T like(T op1, T op2) {
+        throw new CmisNotSupportedException("Not supported in query: like");
+    }
+
+    public T notLike(T op1, T op2) {
+        throw new CmisNotSupportedException("Not supported in query: not like");
+    }
+
+    public T contains(T op1, T op2) {
+        throw new CmisNotSupportedException("Not supported in query: contains");
+    }
+
+    public T inFolder(T op1, T op2) {
+        throw new CmisNotSupportedException("Not supported in query: in_folder");
+    }
+
+    public T inTree(T op1, T op2) {
+        throw new CmisNotSupportedException("Not supported in query: in_tree");
+    }
+
+    public T list(List<T> ops) {
+        throw new CmisNotSupportedException("Not supported in query: list");
+    }
+
+    public T value(boolean value) {
+        throw new CmisNotSupportedException("Not supported in query: boolean value " + value);
+    }
+
+    public T value(double value) {
+        throw new CmisNotSupportedException("Not supported in query: double value " + value);
+    }
+
+    public T value(long value) {
+        throw new CmisNotSupportedException("Not supported in query: long value " + value);
+    }
+
+    public T value(String value) {
+        throw new CmisNotSupportedException("Not supported in query: string value " + value);
+    }
+
+    public T value(GregorianCalendar value) {
+        throw new CmisNotSupportedException("Not supported in query: date value " + value);
+    }
+
+    public T col(String name) {
+        throw new CmisNotSupportedException("Not supported in query: column name " + name);
+    }
+
+}

Added: chemistry/opencmis/trunk/chemistry-opencmis-server/chemistry-opencmis-server-jcr/src/main/java/org/apache/chemistry/opencmis/jcr/query/EvaluatorXPath.java
URL: http://svn.apache.org/viewvc/chemistry/opencmis/trunk/chemistry-opencmis-server/chemistry-opencmis-server-jcr/src/main/java/org/apache/chemistry/opencmis/jcr/query/EvaluatorXPath.java?rev=1087664&view=auto
==============================================================================
--- chemistry/opencmis/trunk/chemistry-opencmis-server/chemistry-opencmis-server-jcr/src/main/java/org/apache/chemistry/opencmis/jcr/query/EvaluatorXPath.java (added)
+++ chemistry/opencmis/trunk/chemistry-opencmis-server/chemistry-opencmis-server-jcr/src/main/java/org/apache/chemistry/opencmis/jcr/query/EvaluatorXPath.java Fri Apr  1 11:59:46 2011
@@ -0,0 +1,459 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.chemistry.opencmis.jcr.query;
+
+import org.apache.chemistry.opencmis.jcr.util.ISO8601;
+import org.apache.chemistry.opencmis.jcr.util.Iterables;
+
+import java.util.GregorianCalendar;
+import java.util.List;
+
+/**
+ * This implementation of {@link Evaluator} results in an instance of a {@link XPathBuilder} which
+ * can be used to validated the where clause of the original CMIS query and translate it to a
+ * corresponding (i.e. semantically equal) XPath condition. 
+ */
+public class EvaluatorXPath extends EvaluatorBase<XPathBuilder> {
+
+    @Override
+    public Evaluator<XPathBuilder> op() {
+        // New instance delegates these methods to this instance in order
+        // to account for the case where these methods are overridden.
+        return new EvaluatorXPath() {
+            @Override
+            protected String jcrPathFromId(String id) {
+                return EvaluatorXPath.this.jcrPathFromId(id);
+            }
+
+            @Override
+            protected String jcrPathFromCol(String name) {
+                return EvaluatorXPath.this.jcrPathFromCol(name);
+            }
+        };
+    }
+
+    @Override
+    public XPathBuilder not(final XPathBuilder op) {
+        return new XPathBuilder() {
+            public String xPath() {
+                if (eval(true) != null) {
+                    return eval(true) ? "true()" : "false()";
+                }
+                else {
+                    return "not(" + op.xPath() + ")";
+                }
+            }
+            public Boolean eval(Boolean folderPredicateValuation) {
+                return not(op.eval(folderPredicateValuation));
+            }
+
+            public Iterable<XPathBuilder> folderPredicates() {
+                return op.folderPredicates();
+            }
+        };
+    }
+
+    @Override
+    public XPathBuilder and(final XPathBuilder op1, final XPathBuilder op2) {
+        return new XPathBuilder() {
+            public String xPath() {
+                if (eval(true) != null) {
+                    return eval(true) ? "true()" : "false()";
+                }
+                else if (op1.eval(true) != null) {  // if not null, op1 must be true -> shortcut evaluation to op2
+                    return op2.xPath();
+                }
+                else if (op2.eval(true) != null) {  // if not null, op2 must be true -> shortcut evaluation to op1
+                    return op1.xPath();
+                }
+                else {
+                    return op1.xPath() + " and " + op2.xPath();
+                }
+            }
+            public Boolean eval(Boolean folderPredicateValuation) {
+                return and(op1.eval(folderPredicateValuation), op2.eval(folderPredicateValuation));
+            }
+
+            public Iterable<XPathBuilder> folderPredicates() {
+                return Iterables.concat(op1.folderPredicates(), op2.folderPredicates());
+            }
+        };
+    }
+
+    @Override
+    public XPathBuilder or(final XPathBuilder op1, final XPathBuilder op2) {
+        return new XPathBuilder() {
+            public String xPath() {
+                if (eval(true) != null) {
+                    return eval(true) ? "true()" : "false()";
+                }
+                else if (op1.eval(true) != null) {  // if not null, op1 must be false -> shortcut evaluation to op2
+                    return op2.xPath();
+                }
+                else if (op2.eval(true) != null) {  // if not null, op2 must be false -> shortcut evaluation to op2
+                    return op1.xPath();
+                }
+                else {
+                    return "(" + op1.xPath() + " or " + op2.xPath() + ")";
+                }
+            }
+            public Boolean eval(Boolean folderPredicateValuation) {
+                return or(op1.eval(folderPredicateValuation), op2.eval(folderPredicateValuation));
+            }
+
+            public Iterable<XPathBuilder> folderPredicates() {
+                return Iterables.concat(op1.folderPredicates(), op2.folderPredicates());
+            }
+        };
+    }
+
+    @Override
+    public XPathBuilder eq(XPathBuilder op1, XPathBuilder op2) {
+        return new RelOpBuilder(op1, " = ", op2);
+    }
+
+    @Override
+    public XPathBuilder neq(XPathBuilder op1, XPathBuilder op2) {
+        return new RelOpBuilder(op1, " != ", op2);
+    }
+
+    @Override
+    public XPathBuilder gt(XPathBuilder op1, XPathBuilder op2) {
+        return new RelOpBuilder(op1, " > ", op2);
+    }
+
+    @Override
+    public XPathBuilder gteq(XPathBuilder op1, XPathBuilder op2) {
+        return new RelOpBuilder(op1, " >= ", op2);
+    }
+
+    @Override
+    public XPathBuilder lt(XPathBuilder op1, XPathBuilder op2) {
+        return new RelOpBuilder(op1, " < ", op2);
+    }
+
+    @Override
+    public XPathBuilder lteq(XPathBuilder op1, XPathBuilder op2) {
+        return new RelOpBuilder(op1, " <= ", op2);
+    }
+
+    @Override
+    public XPathBuilder in(XPathBuilder op1, XPathBuilder op2) {
+        return super.in(op1, op2);    // todo implement in
+    }
+
+    @Override
+    public XPathBuilder notIn(XPathBuilder op1, XPathBuilder op2) {
+        return super.notIn(op1, op2);    // todo implement notIn
+    }
+
+    @Override
+    public XPathBuilder inAny(XPathBuilder op1, XPathBuilder op2) {
+        return super.inAny(op1, op2);    // todo implement inAny
+    }
+
+    @Override
+    public XPathBuilder notInAny(XPathBuilder op1, XPathBuilder op2) {
+        return super.notInAny(op1, op2);    // todo implement notInAny
+    }
+
+    @Override
+    public XPathBuilder eqAny(XPathBuilder op1, XPathBuilder op2) {
+        return super.eqAny(op1, op2);    // todo implement eqAny
+    }
+
+    @Override
+    public XPathBuilder isNull(XPathBuilder op) {
+        return new FunctionBuilder(op);
+    }
+
+    @Override
+    public XPathBuilder notIsNull(XPathBuilder op) {
+        return new FunctionBuilder("not", op);
+    }
+
+    @Override
+    public XPathBuilder like(XPathBuilder op1, XPathBuilder op2) {
+        return new FunctionBuilder("jcr:like", op1, op2);
+    }
+
+    @Override
+    public XPathBuilder notLike(XPathBuilder op1, XPathBuilder op2) {
+        return new FunctionBuilder("jcr:like", op1, op2) {
+            @Override
+            public String xPath() {
+                return "not(" + super.xPath() + ")"; 
+            }
+        };
+    }
+
+    @Override
+    public XPathBuilder contains(XPathBuilder op1, XPathBuilder op2) {
+        return new FunctionBuilder("jcr:contains", ".", op2);
+    }
+
+    @Override
+    public XPathBuilder inFolder(XPathBuilder op1,XPathBuilder op2) {
+        return new FolderPredicateBuilder(op2.xPath(), false);
+    }
+
+    @Override
+    public XPathBuilder inTree(XPathBuilder op1, XPathBuilder op2) {
+        return new FolderPredicateBuilder(op2.xPath(), true);
+    }
+
+    @Override
+    public XPathBuilder list(List<XPathBuilder> ops) {
+        return super.list(ops);    // todo implement list
+    }
+
+    @Override
+    public XPathBuilder value(boolean value) {
+        return new LiteralBuilder(value);
+    }
+
+    @Override
+    public XPathBuilder value(double value) {
+        return new LiteralBuilder(value);
+    }
+
+    @Override
+    public XPathBuilder value(long value) {
+        return new LiteralBuilder(value);
+    }
+
+    @Override
+    public XPathBuilder value(String value) {
+        return new LiteralBuilder(value);
+    }
+
+    @Override
+    public XPathBuilder value(GregorianCalendar value) {
+        return new LiteralBuilder(value);
+    }
+
+    @Override
+    public XPathBuilder col(String name) {
+        return new ColRefBuilder(name);
+    }
+
+    //------------------------------------------< protected >---
+
+    /**
+     * Resolve from a CMIS object id to the corresponding absolute JCR path.
+     * This default implementations simply returns <code>id</code>.
+     */
+    protected String jcrPathFromId(String id) {
+        return id;
+    }
+
+    /**
+     * Resolve from a column name in the query to the corresponding
+     * relative JCR path. The path must be relative to the context node.
+     * This default implementations simply returns <code>name</code>.
+     */
+    protected String jcrPathFromCol(String name) {
+        return name; 
+    }
+
+    //------------------------------------------< private >---
+
+    /**
+     * @return  <code>null</code> if <code>b</code> is <code>null</code>, <code>!b</code> otherwise.
+     */
+    private static Boolean not(Boolean b) {
+        return b == null ? null : !b;
+    }
+
+    /**
+     * @return 
+     *  <ul><li><code>true</code> if either of <code>b1</code> and <code>b2</code> is <code>true</code>,</li>
+     *      <li><code>false</code> if both <code>b1</code> and <code>b2</code> are <code>false</code>,</li>
+     *      <li><code>null</code> otherwise.</li></ul>
+     */
+    private static Boolean or(Boolean b1, Boolean b2) {
+        return Boolean.TRUE.equals(b1) || Boolean.TRUE.equals(b2)
+                ? Boolean.TRUE
+                : Boolean.FALSE.equals(b1) && Boolean.FALSE.equals(b2)
+                        ? Boolean.FALSE
+                        : null;
+    }
+
+    /**
+     * @return
+     *  <ul><li><code>false</code> if either of <code>b1</code> and <code>b2</code> is <code>false</code>,</li>
+     *      <li><code>true</code> if both <code>b1</code> and <code>b2</code> are <code>true</code>,</li>
+     *      <li><code>null</code> otherwise.</li></ul>
+     */
+    private static Boolean and(Boolean b1, Boolean b2) {
+        return Boolean.FALSE.equals(b1) || Boolean.FALSE.equals(b2)
+                ? Boolean.FALSE
+                : Boolean.TRUE.equals(b1) && Boolean.TRUE.equals(b2)
+                        ? Boolean.TRUE
+                        : null;
+    }
+
+    private static class RelOpBuilder implements XPathBuilder {
+        private final String relOp;
+        private final XPathBuilder op1;
+        private final XPathBuilder op2;
+
+        public RelOpBuilder(XPathBuilder op1, String relOp, XPathBuilder op2) {
+            this.relOp = relOp;
+            this.op1 = op1;
+            this.op2 = op2;
+        }
+
+        public String xPath() {
+            return op1.xPath() + relOp + op2.xPath();
+        }
+
+        public Boolean eval(Boolean folderPredicateValuation) {
+            return null;
+        }
+
+        public Iterable<XPathBuilder> folderPredicates() {
+            return Iterables.concat(op1.folderPredicates(), op2.folderPredicates());
+        }
+    }
+
+    private class FolderPredicateBuilder implements XPathBuilder {
+        private final String folderId;
+        private final boolean includeDescendants;
+        
+        public FolderPredicateBuilder(String folderId, boolean includeDescendants) {
+            this.folderId = stripQuotes(folderId);
+            this.includeDescendants = includeDescendants;
+        }
+
+        public String xPath() {
+            return jcrPathFromId(folderId) + (includeDescendants ? "//" : "/");
+        }
+
+        public Boolean eval(Boolean folderPredicateValuation) {
+            return folderPredicateValuation;
+        }
+
+        public Iterable<XPathBuilder> folderPredicates() {
+            return Iterables.singleton((XPathBuilder) this);
+        }
+
+        private String stripQuotes(String string) {
+            return (string.startsWith("'") || string.startsWith("\"")) && string.length() >= 2
+                    ? string.substring(1, string.length() - 1)
+                    : string;
+        }
+
+    }
+
+    private abstract static class PrimitiveBuilder implements XPathBuilder {
+        public Boolean eval(Boolean folderPredicateValuation) {
+            return null;
+        }
+
+        public Iterable<XPathBuilder> folderPredicates() {
+            return Iterables.empty();
+        }
+    }
+
+    private static class LiteralBuilder extends PrimitiveBuilder  {
+        private final String xPath;
+
+        public LiteralBuilder(String value) {
+            xPath = "'" + value + "'";
+        }
+
+        public LiteralBuilder(boolean value) {
+            xPath = Boolean.toString(value);
+        }
+
+        public LiteralBuilder(long value) {
+            xPath = Long.toString(value);
+        }
+
+        public LiteralBuilder(double value) {
+            xPath = Double.toString(value);
+        }
+
+        public LiteralBuilder(GregorianCalendar value) {
+            xPath = "xs:dateTime('" + ISO8601.format(value) + "')";
+        }
+
+        public String xPath() {
+            return xPath;
+        }
+    }
+
+    private class ColRefBuilder extends PrimitiveBuilder  {
+        private final String colRef;
+
+        public ColRefBuilder(String colRef) {
+            this.colRef = colRef;
+        }
+
+        public String xPath() {
+            return jcrPathFromCol(colRef);   
+        }
+
+    }
+
+    private static class FunctionBuilder extends PrimitiveBuilder {
+        private final String function;
+        private String op1Str;
+        private final XPathBuilder op1;
+        private final XPathBuilder op2;
+
+        private FunctionBuilder(String function, String op1Str, XPathBuilder op1, XPathBuilder op2) {
+            this.function = function;
+            this.op1Str = op1Str;
+            this.op1 = op1;
+            this.op2 = op2;
+        }
+
+        public FunctionBuilder(String function, XPathBuilder op1, XPathBuilder op2) {
+            this(function, null, op1, op2);
+        }
+
+        public FunctionBuilder(String function, XPathBuilder op1) {
+            this(function, null, op1, null);
+        }
+
+        public FunctionBuilder(String function, String op1, XPathBuilder op2) {
+            this(function, op1, null, op2);
+
+        }
+
+        public FunctionBuilder(XPathBuilder op1) {
+            this(null, op1, null);
+        }
+
+        public String xPath() {
+            if (op1Str == null) {
+                op1Str = op1.xPath();
+            }
+
+            return function == null
+                    ? op1Str
+                    : function + "(" + op1Str + (op2 == null ? "" : ", " + op2.xPath()) + ")";
+        }
+
+    }
+
+}

Added: chemistry/opencmis/trunk/chemistry-opencmis-server/chemistry-opencmis-server-jcr/src/main/java/org/apache/chemistry/opencmis/jcr/query/IdentifierMap.java
URL: http://svn.apache.org/viewvc/chemistry/opencmis/trunk/chemistry-opencmis-server/chemistry-opencmis-server-jcr/src/main/java/org/apache/chemistry/opencmis/jcr/query/IdentifierMap.java?rev=1087664&view=auto
==============================================================================
--- chemistry/opencmis/trunk/chemistry-opencmis-server/chemistry-opencmis-server-jcr/src/main/java/org/apache/chemistry/opencmis/jcr/query/IdentifierMap.java (added)
+++ chemistry/opencmis/trunk/chemistry-opencmis-server/chemistry-opencmis-server-jcr/src/main/java/org/apache/chemistry/opencmis/jcr/query/IdentifierMap.java Fri Apr  1 11:59:46 2011
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.chemistry.opencmis.jcr.query;
+
+/**
+ * The methods of this class map CMIS identifiers to JCR identifiers. Each implementation
+ * of this interface is bound to a specific CMIS object type. That is, it implements the
+ * identifier maps for that object type. 
+ */
+public interface IdentifierMap {
+
+    /**
+     * Map a column name in the CMIS query to the corresponding relative JCR path.
+     * The path must be relative to the context node.
+     *
+     * @param name  column name
+     * @return  relative JCR path
+     */
+    String jcrPathFromCol(String name);
+
+    /**
+     * JCR type name corresponding to the CMIS type bound to this instance.
+     * @see #jcrTypeCondition()
+     *
+     * @return  name of the JCR type
+     */
+    String jcrTypeName();
+
+    /**
+     * Create and additional condition in order for the query to only return nodes
+     * of the right type. This condition and-ed to the condition determined by the
+     * CMIS query's where clause.
+     * <p/>
+     * A CMIS query for non versionable documents should for example result in the
+     * following XPath query:
+     * <p/>
+     * <pre>
+     *   element(*, nt:file)[not(@jcr:mixinTypes = 'mix:simpleVersionable')]
+     * </pre>
+     * Here the element test is covered by {@link #jcrTypeName()}
+     * while the predicate is covered by this method.
+     *
+     * @see #jcrTypeName()
+     *
+     * @return  Additional condition or <code>null</code> if none.
+     */
+    String jcrTypeCondition();
+}

Added: chemistry/opencmis/trunk/chemistry-opencmis-server/chemistry-opencmis-server-jcr/src/main/java/org/apache/chemistry/opencmis/jcr/query/ParseTreeWalker.java
URL: http://svn.apache.org/viewvc/chemistry/opencmis/trunk/chemistry-opencmis-server/chemistry-opencmis-server-jcr/src/main/java/org/apache/chemistry/opencmis/jcr/query/ParseTreeWalker.java?rev=1087664&view=auto
==============================================================================
--- chemistry/opencmis/trunk/chemistry-opencmis-server/chemistry-opencmis-server-jcr/src/main/java/org/apache/chemistry/opencmis/jcr/query/ParseTreeWalker.java (added)
+++ chemistry/opencmis/trunk/chemistry-opencmis-server/chemistry-opencmis-server-jcr/src/main/java/org/apache/chemistry/opencmis/jcr/query/ParseTreeWalker.java Fri Apr  1 11:59:46 2011
@@ -0,0 +1,271 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.chemistry.opencmis.jcr.query;
+
+import org.antlr.runtime.tree.Tree;
+import org.apache.chemistry.opencmis.commons.exceptions.CmisInvalidArgumentException;
+import org.apache.chemistry.opencmis.commons.exceptions.CmisRuntimeException;
+import org.apache.chemistry.opencmis.server.support.query.CalendarHelper;
+import org.apache.chemistry.opencmis.server.support.query.CmisQlStrictLexer;
+import org.apache.chemistry.opencmis.server.support.query.PredicateWalkerBase;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This implementation of {@link PredicateWalkerBase} traverses the parse tree of a CMIS query.
+ * It uses an {@link Evaluator} to accumulate the result of the traversal. <code>Evaluator</code>
+ * has a corresponding method for each {@link Tree#getType() node type} in the parse tree.
+ * <code>ParseTreeWalker</code> calls these methods while traversing the parse tree passing an
+ * <code>Evaluator</code> for each of the corresponding operation's arguments.
+ * </br>
+ * The {@link #walkPredicate(Tree)} serves as entry point for traversing a parse tree. After
+ * successful traversal, the result is obtained from the {@link #getResult()} method.
+ *
+ * @param <T>  type of the result determined by the <code>Evaluator</code> used.
+ */
+public class ParseTreeWalker<T> implements PredicateWalkerBase {
+
+    private final Evaluator<T> evaluator;
+    private T result;
+
+    /**
+     * Create a new instance for traversing CMIS query parse trees.
+     *
+     * @param evaluator  <code>Evaluator</code> for evaluating the nodes of the parse tree
+     */
+    public ParseTreeWalker(Evaluator<T> evaluator) {
+        this.evaluator = evaluator;
+    }
+
+    /**
+     * Retrieve the result of a successful traversal.
+     *
+     * @return  result of traversal or <code>null</code> if either not yet traversed, an error occurred
+     *      on traversal or the query has an empty where clause. 
+     */
+    public T getResult() {
+        return result;
+    }
+
+    //------------------------------------------< PredicateWalkerBase >---
+    
+    public Boolean walkPredicate(Tree node) {
+        result = null;
+        result = walkPredicate(evaluator, node);
+        return false; // Return value is ignored by caller
+    }
+
+    //------------------------------------------< protected >---
+
+    /** For extensibility. */
+    protected T walkOtherExpr(Evaluator evaluator, Tree node) {
+        throw new CmisRuntimeException("Unknown node type: " + node.getType() + " (" + node.getText() + ")");
+    }
+
+    /** For extensibility. */
+    protected T walkOtherPredicate(Evaluator evaluator, Tree node) {
+        throw new CmisRuntimeException("Unknown node type: " + node.getType() + " (" + node.getText() + ")");
+    }
+
+    //------------------------------------------< private >---
+
+    private T walkPredicate(Evaluator<T> evaluator, Tree node) {
+        switch (node.getType()) {
+            case CmisQlStrictLexer.NOT:
+                return evaluator.not(walkPredicate(evaluator.op(), node.getChild(0)));
+            case CmisQlStrictLexer.AND:
+                return evaluator.and(
+                        walkPredicate(evaluator.op(), node.getChild(0)),
+                        walkPredicate(evaluator.op(), node.getChild(1)));
+            case CmisQlStrictLexer.OR:
+                return evaluator.or(
+                        walkPredicate(evaluator.op(), node.getChild(0)),
+                        walkPredicate(evaluator.op(), node.getChild(1)));
+            case CmisQlStrictLexer.EQ:
+                return evaluator.eq(
+                        walkExpr(evaluator.op(), node.getChild(0)),
+                        walkExpr(evaluator.op(), node.getChild(1)));
+            case CmisQlStrictLexer.NEQ:
+                return evaluator.neq(
+                        walkExpr(evaluator.op(), node.getChild(0)),
+                        walkExpr(evaluator.op(), node.getChild(1)));
+            case CmisQlStrictLexer.GT:
+                return evaluator.gt(
+                        walkExpr(evaluator.op(), node.getChild(0)),
+                        walkExpr(evaluator.op(), node.getChild(1)));
+            case CmisQlStrictLexer.GTEQ:
+                return evaluator.gteq(
+                        walkExpr(evaluator.op(), node.getChild(0)),
+                        walkExpr(evaluator.op(), node.getChild(1)));
+            case CmisQlStrictLexer.LT:
+                return evaluator.lt(
+                        walkExpr(evaluator.op(), node.getChild(0)),
+                        walkExpr(evaluator.op(), node.getChild(1)));
+            case CmisQlStrictLexer.LTEQ:
+                return evaluator.lteq(
+                        walkExpr(evaluator.op(), node.getChild(0)),
+                        walkExpr(evaluator.op(), node.getChild(1)));
+            case CmisQlStrictLexer.IN:
+                return evaluator.in(
+                        walkExpr(evaluator.op(), node.getChild(0)),
+                        walkExpr(evaluator.op(), node.getChild(1)));
+            case CmisQlStrictLexer.NOT_IN:
+                return evaluator.notIn(
+                        walkExpr(evaluator.op(), node.getChild(0)),
+                        walkExpr(evaluator.op(), node.getChild(1)));
+            case CmisQlStrictLexer.IN_ANY:
+                return evaluator.inAny(
+                        walkExpr(evaluator.op(), node.getChild(0)),
+                        walkExpr(evaluator.op(), node.getChild(1)));
+            case CmisQlStrictLexer.NOT_IN_ANY:
+                return evaluator.notInAny(
+                        walkExpr(evaluator.op(), node.getChild(0)),
+                        walkExpr(evaluator.op(), node.getChild(1)));
+            case CmisQlStrictLexer.EQ_ANY:
+                return evaluator.eqAny(
+                        walkExpr(evaluator.op(), node.getChild(0)),
+                        walkExpr(evaluator.op(), node.getChild(1)));
+            case CmisQlStrictLexer.IS_NULL:
+                return evaluator.isNull(walkExpr(evaluator.op(), node.getChild(0)));
+            case CmisQlStrictLexer.IS_NOT_NULL:
+                return evaluator.notIsNull(walkExpr(evaluator.op(), node.getChild(0)));
+            case CmisQlStrictLexer.LIKE:
+                return evaluator.like(
+                        walkExpr(evaluator.op(), node.getChild(0)),
+                        walkExpr(evaluator.op(), node.getChild(1)));
+            case CmisQlStrictLexer.NOT_LIKE:
+                return evaluator.notLike(
+                        walkExpr(evaluator.op(), node.getChild(0)),
+                        walkExpr(evaluator.op(), node.getChild(1)));
+            case CmisQlStrictLexer.CONTAINS:
+                if (node.getChildCount() == 1) {
+                    return evaluator.contains(
+                            null,
+                            walkExpr(evaluator.op(), node.getChild(0)));
+                }
+                else {
+                    return evaluator.contains(
+                            walkExpr(evaluator.op(), node.getChild(0)),
+                            walkExpr(evaluator.op(), node.getChild(1)));
+                }
+            case CmisQlStrictLexer.IN_FOLDER:
+                if (node.getChildCount() == 1) {
+                    return evaluator.inFolder(
+                            null,
+                            walkExpr(evaluator.op(), node.getChild(0)));
+                }
+                else {
+                    return evaluator.inFolder(
+                            walkExpr(evaluator.op(), node.getChild(0)),
+                            walkExpr(evaluator.op(), node.getChild(1)));
+                }
+            case CmisQlStrictLexer.IN_TREE:
+                if (node.getChildCount() == 1) {
+                    return evaluator.inTree(
+                            null,
+                            walkExpr(evaluator.op(), node.getChild(0)));
+                }
+                else {
+                    return evaluator.inTree(
+                            walkExpr(evaluator.op(), node.getChild(0)),
+                            walkExpr(evaluator.op(), node.getChild(1)));
+                }
+            default:
+                return walkOtherPredicate(evaluator, node);
+        }
+    }
+
+    private T walkExpr(Evaluator<T> evaluator, Tree node) {
+        switch (node.getType()) {
+            case CmisQlStrictLexer.BOOL_LIT:
+                return walkBoolean(evaluator, node);
+            case CmisQlStrictLexer.NUM_LIT:
+                return walkNumber(evaluator, node);
+            case CmisQlStrictLexer.STRING_LIT:
+                return walkString(evaluator, node);
+            case CmisQlStrictLexer.TIME_LIT:
+                return walkTimestamp(evaluator, node);
+            case CmisQlStrictLexer.IN_LIST:
+                return evaluator.list(walkList(evaluator, node));
+            case CmisQlStrictLexer.COL:
+                return walkCol(evaluator, node);
+            default:
+                return walkOtherExpr(evaluator, node);
+        }
+    }
+
+    private List<T> walkList(Evaluator<T> evaluator, Tree node) {
+        int n = node.getChildCount();
+        List<T> result = new ArrayList<T>(n);
+        for (int i = 0; i < n; i++) {
+            result.add(walkExpr(evaluator.op(), node.getChild(i)));
+        }
+        return result;
+    }
+
+    private T walkBoolean(Evaluator<T> evaluator, Tree node) {
+        String s = node.getText();
+
+        if ("true".equalsIgnoreCase(s)) {
+            return evaluator.value(true);
+        }
+        else if ("false".equalsIgnoreCase(s)) {
+            return evaluator.value(false);
+        }
+        else {
+            throw new CmisInvalidArgumentException("Not a boolean: " + s);
+        }
+    }
+
+    private T walkNumber(Evaluator<T> evaluator, Tree node) {
+        String s = node.getText();
+        try {
+            return s.contains(".") || s.contains("e") || s.contains("E")
+                    ? evaluator.value(Double.valueOf(s))
+                    : evaluator.value(Long.valueOf(s));
+        }
+        catch (NumberFormatException e) {
+            throw new CmisInvalidArgumentException("Not a number: " + s);
+        }
+    }
+
+    private T walkString(Evaluator<T> evaluator, Tree node) {
+        String s = node.getText();
+        s = s.substring(1, s.length() - 1);
+        return evaluator.value(s.replace("''", "'"));  // un-escape quotes
+    }
+
+    private T walkTimestamp(Evaluator<T> evaluator, Tree node) {
+        String s = node.getText();
+        s = s.substring(s.indexOf('\'') + 1, s.length() - 1);
+        try {
+            return evaluator.value(CalendarHelper.fromString(s));
+        }
+        catch (IllegalArgumentException e) {
+            throw new CmisInvalidArgumentException("Not a date time value: " + s);
+        }
+    }
+
+    private T walkCol(Evaluator<T> evaluator, Tree node) {
+        return evaluator.col(node.getChild(0).getText());
+    }
+
+}

Added: chemistry/opencmis/trunk/chemistry-opencmis-server/chemistry-opencmis-server-jcr/src/main/java/org/apache/chemistry/opencmis/jcr/query/QueryTranslator.java
URL: http://svn.apache.org/viewvc/chemistry/opencmis/trunk/chemistry-opencmis-server/chemistry-opencmis-server-jcr/src/main/java/org/apache/chemistry/opencmis/jcr/query/QueryTranslator.java?rev=1087664&view=auto
==============================================================================
--- chemistry/opencmis/trunk/chemistry-opencmis-server/chemistry-opencmis-server-jcr/src/main/java/org/apache/chemistry/opencmis/jcr/query/QueryTranslator.java (added)
+++ chemistry/opencmis/trunk/chemistry-opencmis-server/chemistry-opencmis-server-jcr/src/main/java/org/apache/chemistry/opencmis/jcr/query/QueryTranslator.java Fri Apr  1 11:59:46 2011
@@ -0,0 +1,259 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.chemistry.opencmis.jcr.query;
+
+import org.apache.chemistry.opencmis.commons.definitions.TypeDefinition;
+import org.apache.chemistry.opencmis.commons.exceptions.CmisInvalidArgumentException;
+import org.apache.chemistry.opencmis.jcr.JcrTypeManager;
+import org.apache.chemistry.opencmis.server.support.query.QueryObject;
+import org.apache.chemistry.opencmis.server.support.query.QueryObject.SortSpec;
+import org.apache.chemistry.opencmis.server.support.query.QueryUtil;
+
+import java.util.List;
+
+/**
+ * Abstract base class for translating a CMIS query statement to a JCR XPath
+ * query statement.
+ * Overriding class need to implement methods for mapping CMIS ids to JCR paths,
+ * CMIS property names to JCR property names, CMIS type names to JCR type name and
+ * in addition a method for adding further constraints to a query based on a CMIS
+ * type. 
+ */
+public abstract class QueryTranslator {
+    private final JcrTypeManager typeManager;
+    private final EvaluatorXPath evaluator;
+    private QueryObject queryObject;
+
+    /**
+     * Create a new query translator which uses the provided <code>typeManager</code>
+     * to resolve CMIS type names to CMIS types.
+     * 
+     * @param typeManager
+     */
+    public QueryTranslator(JcrTypeManager typeManager) {
+        this.typeManager = typeManager;
+        evaluator = new EvaluatorXPath() {
+
+            @Override
+            protected String jcrPathFromId(String id) {
+                return QueryTranslator.this.jcrPathFromId(id);
+            }
+
+            @Override
+            protected String jcrPathFromCol(String name) {
+                return QueryTranslator.this.jcrPathFromCol(queryObject.getMainFromName(), name);
+            }
+        };
+    }
+
+    /**
+     * @return  the {@link QueryObject} from the last translation performed through
+     *      {@link QueryTranslator#translateToXPath(String)}.
+     */
+    public QueryObject getQueryObject() {
+        return queryObject;
+    }
+
+    /**
+     * Translate a CMIS query statement to a JCR XPath query statement.
+     * 
+     * @param statement
+     * @return
+     */
+    public String translateToXPath(String statement) {
+        QueryUtil queryUtil = new QueryUtil();
+        queryObject = new QueryObject(typeManager);
+        ParseTreeWalker<XPathBuilder> parseTreeWalker = new ParseTreeWalker<XPathBuilder>(evaluator);
+        queryUtil.traverseStatementAndCatchExc(statement, queryObject, parseTreeWalker);
+
+        XPathBuilder parseResult = parseTreeWalker.getResult();
+        TypeDefinition fromType = getFromName(queryObject);
+
+        String pathExpression = buildPathExpression(fromType, getFolderPredicate(parseResult));
+        String elementTest = buildElementTest(fromType);
+        String predicates = buildPredicates(fromType, getCondition(parseResult));
+        String orderByClause = buildOrderByClause(fromType, queryObject.getOrderBys());
+        return "/jcr:root" + pathExpression + elementTest + predicates + orderByClause;
+    }
+
+    //------------------------------------------< protected >---
+
+    /**
+     * Map a CMIS objectId to an absolute JCR path. This method is called to
+     * resolve the folder if of folder predicates (i.e. IN_FOLDER, IN_TREE).
+     *
+     * @param id  objectId
+     * @return  absolute JCR path corresponding to <code>id</code>.
+     */
+    protected abstract String jcrPathFromId(String id);
+
+    /**
+     * Map a column name in the CMIS query to the corresponding relative JCR path.
+     * The path must be relative to the context node.
+     *
+     * @param fromType  Type on which the CMIS query is performed
+     * @param name  column name
+     * @return  relative JCR path 
+     */
+    protected abstract String jcrPathFromCol(TypeDefinition fromType, String name);
+
+    /**
+     * Map a CMIS type to the corresponding JCR type name.
+     * @see #jcrTypeCondition(TypeDefinition)
+     *
+     * @param fromType  CMIS type
+     * @return  name of the JCR type corresponding to <code>fromType</code>
+     */
+    protected abstract String jcrTypeName(TypeDefinition fromType);
+
+    /**
+     * Create and additional condition in order for the query to only return nodes
+     * of the right type. This condition and-ed to the condition determined by the
+     * CMIS query's where clause.
+     * <p/>
+     * A CMIS query for non versionable documents should for example result in the
+     * following XPath query:
+     * <p/>
+     * <pre>
+     *   element(*, nt:file)[not(@jcr:mixinTypes = 'mix:simpleVersionable')]
+     * </pre>
+     * Here the element test is covered by {@link #jcrTypeName(TypeDefinition)}
+     * while the predicate is covered by this method.  
+     *
+     * @see #jcrTypeName(TypeDefinition)
+     *
+     * @param fromType
+     * @return  Additional condition or <code>null</code> if none. 
+     */
+    protected abstract String jcrTypeCondition(TypeDefinition fromType);  
+
+    /**
+     * Build a XPath path expression for the CMIS type queried for and a folder predicate.
+     *
+     * @param fromType  CMIS type queried for
+     * @param folderPredicate  folder predicate
+     * @return  a valid XPath path expression or <code>null</code> if none.
+     */
+    protected String buildPathExpression(TypeDefinition fromType, String folderPredicate) {
+        return folderPredicate == null ? "//" : folderPredicate;
+    }
+
+    /**
+     * Build a XPath element test for the given CMIS type.
+     *
+     * @param fromType  CMIS type queried for
+     * @return  a valid XPath element test. 
+     */
+    protected String buildElementTest(TypeDefinition fromType) {
+        return "element(*," + jcrTypeName(fromType) + ")";
+    }
+
+    /**
+     * Build a XPath predicate for the given CMIS type and an additional condition.
+     * The additional condition should be and-ed to the condition resulting from
+     * evaluating <code>fromType</code>.
+     *
+     * @param fromType  CMIS type queried for
+     * @param condition  additional condition.
+     * @return  a valid XPath predicate or <code>null</code> if none. 
+     */
+    protected String buildPredicates(TypeDefinition fromType, String condition) {
+        String typeCondition = jcrTypeCondition(fromType);
+
+        if (typeCondition == null) {
+            return condition == null ? "" : "[" + condition + "]";
+        }
+        else if (condition == null) {
+            return "[" + typeCondition + "]";
+        }
+        else {
+            return "[" + typeCondition + " and " + condition + "]"; 
+        }
+    }
+
+    /**
+     * Build a XPath order by clause for the given CMIS type and a list of {@link SortSpec}s.
+     *
+     * @param fromType  CMIS type queried for
+     * @param orderBys  <code>SortSpec</code>s
+     * @return  a valid XPath order by clause 
+     */
+    protected String buildOrderByClause(TypeDefinition fromType, List<SortSpec> orderBys) {
+        StringBuilder orderSpecs = new StringBuilder();
+
+        for (SortSpec orderBy : orderBys) {
+            String selector = jcrPathFromCol(fromType, orderBy.getSelector().getName());  
+            boolean ascending = orderBy.isAscending();
+
+            if (orderSpecs.length() > 0) {
+                orderSpecs.append(',');
+            }
+
+            orderSpecs
+                .append(selector)
+                .append(' ')
+                .append(ascending ? "ascending" : "descending");
+        }
+
+        return orderSpecs.length() > 0
+                ? "order by " + orderSpecs
+                : "";
+    }
+
+    //------------------------------------------< private >---
+
+    private static String getFolderPredicate(XPathBuilder parseResult) {
+        if (parseResult == null) {
+            return null;
+        }
+        
+        String folderPredicate = null;
+        for (XPathBuilder p : parseResult.folderPredicates()) {
+            if (folderPredicate == null) {
+                folderPredicate = p.xPath();
+            }
+            else {
+                throw new CmisInvalidArgumentException("Query may only contain a single folder predicate");
+            }
+        }
+
+        // See the class comment on XPathBuilder for details on affirmative literals
+        if (folderPredicate != null &&                             // IF has single folder predicate
+            !Boolean.FALSE.equals(parseResult.eval(false))) {      // AND folder predicate is not affirmative
+            throw new CmisInvalidArgumentException("Folder predicate " + folderPredicate + " is not affirmative.");
+        }
+
+        return folderPredicate;
+    }
+
+    private static TypeDefinition getFromName(QueryObject queryObject) {
+        if (queryObject.getTypes().size() != 1) {
+            throw new CmisInvalidArgumentException("From must contain one single reference");
+        }
+        return queryObject.getMainFromName();
+    }
+
+    private static String getCondition(XPathBuilder parseResult) {
+        // No condition if either parseResult is null or when it evaluates to true under
+        // the valuation which assigns true to the folder predicate.
+        return parseResult == null || Boolean.TRUE.equals(parseResult.eval(true)) ? null : parseResult.xPath();
+    }
+    
+}

Added: chemistry/opencmis/trunk/chemistry-opencmis-server/chemistry-opencmis-server-jcr/src/main/java/org/apache/chemistry/opencmis/jcr/query/XPathBuilder.java
URL: http://svn.apache.org/viewvc/chemistry/opencmis/trunk/chemistry-opencmis-server/chemistry-opencmis-server-jcr/src/main/java/org/apache/chemistry/opencmis/jcr/query/XPathBuilder.java?rev=1087664&view=auto
==============================================================================
--- chemistry/opencmis/trunk/chemistry-opencmis-server/chemistry-opencmis-server-jcr/src/main/java/org/apache/chemistry/opencmis/jcr/query/XPathBuilder.java (added)
+++ chemistry/opencmis/trunk/chemistry-opencmis-server/chemistry-opencmis-server-jcr/src/main/java/org/apache/chemistry/opencmis/jcr/query/XPathBuilder.java Fri Apr  1 11:59:46 2011
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.chemistry.opencmis.jcr.query;
+
+/**
+ * This result type of {@link EvaluatorXPath} provides means for partially evaluating
+ * the underlying query's condition. This allows to determine whether there is a semantically
+ * equivalent translation from the CMIS query's where clause to an XPath condition.
+ * <br/>
+ * Specifically <code>EvaluatorXPath</code> only supports a single folder predicate. That
+ * is the original CMIS query must not contain more than one IN_TREE or IN_FOLDER
+ * predicate respectively. Furthermore that single folder predicate must be affirmative.
+ * A literal <code>p</code> in a boolean expression <code>X</code> is affirmative if there
+ * exists a boolean expression <code>Y</code> such that <code>p &and; Y = X</code>.
+ * <em>Note</em>: a single folder predicate is affirmative if any only if
+ * {@link #eval(Boolean) <code>eval(false)</code>} return <code>false</code>.  
+ * <br/>
+ * Only if both conditions hold will the XPath translation provided the {@link #xPath()}
+ * method be valid.
+ */
+public interface XPathBuilder {
+
+    /**
+     * Translation of the underlying CMIS query's where clause to a XPath condition.
+     * The string is only valid if there is no more than one folder predicate and
+     * the folder predicate is in affirmative position.
+     */
+    String xPath();
+
+    /**
+     * Evaluate the query condition for a given valuation of the folder predicate terms.
+     *
+     * @param folderPredicateValuation  valuation for the folder predicate terms. Use <code>null</code>
+     *      for none.
+     * @return  result of the partial evaluation. <code>null</code> means that the value of the
+     *      query condition is not determined the value passed for <code>folderPredicateValuation</code>.
+     */
+    Boolean eval(Boolean folderPredicateValuation);
+
+    /**
+     * The folder predicates contained in this query's condition.
+     */
+    Iterable<XPathBuilder> folderPredicates();
+}

Added: chemistry/opencmis/trunk/chemistry-opencmis-server/chemistry-opencmis-server-jcr/src/main/java/org/apache/chemistry/opencmis/jcr/util/ISO8601.java
URL: http://svn.apache.org/viewvc/chemistry/opencmis/trunk/chemistry-opencmis-server/chemistry-opencmis-server-jcr/src/main/java/org/apache/chemistry/opencmis/jcr/util/ISO8601.java?rev=1087664&view=auto
==============================================================================
--- chemistry/opencmis/trunk/chemistry-opencmis-server/chemistry-opencmis-server-jcr/src/main/java/org/apache/chemistry/opencmis/jcr/util/ISO8601.java (added)
+++ chemistry/opencmis/trunk/chemistry-opencmis-server/chemistry-opencmis-server-jcr/src/main/java/org/apache/chemistry/opencmis/jcr/util/ISO8601.java Fri Apr  1 11:59:46 2011
@@ -0,0 +1,320 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.chemistry.opencmis.jcr.util;
+
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+import java.util.TimeZone;
+
+/**
+ * The <code>ISO8601</code> utility class provides helper methods
+ * to deal with date/time formatting using a specific ISO8601-compliant
+ * format (see <a href="http://www.w3.org/TR/NOTE-datetime">ISO 8601</a>).
+ * <p/>
+ * The currently supported format is:
+ * <pre>
+ *   &plusmn;YYYY-MM-DDThh:mm:ss.SSSTZD
+ * </pre>
+ * where:
+ * <pre>
+ *   &plusmn;YYYY = four-digit year with optional sign where values <= 0 are
+ *           denoting years BCE and values > 0 are denoting years CE,
+ *           e.g. -0001 denotes the year 2 BCE, 0000 denotes the year 1 BCE,
+ *           0001 denotes the year 1 CE, and so on...
+ *   MM    = two-digit month (01=January, etc.)
+ *   DD    = two-digit day of month (01 through 31)
+ *   hh    = two digits of hour (00 through 23) (am/pm NOT allowed)
+ *   mm    = two digits of minute (00 through 59)
+ *   ss    = two digits of second (00 through 59)
+ *   SSS   = three digits of milliseconds (000 through 999)
+ *   TZD   = time zone designator, Z for Zulu (i.e. UTC) or an offset from UTC
+ *           in the form of +hh:mm or -hh:mm
+ * </pre>
+ * <p/>
+ * <em>Note:</em> This class is copied from org.apache.jackrabbit.util.ISO8601
+ */
+public final class ISO8601 {
+    private ISO8601() { }
+
+    /**
+     * Parses an ISO8601-compliant date/time string.
+     *
+     * @param text the date/time string to be parsed
+     * @return a <code>Calendar</code>, or <code>null</code> if the input could
+     *         not be parsed
+     * @throws IllegalArgumentException if a <code>null</code> argument is passed
+     */
+    public static Calendar parse(String text) {
+        if (text == null) {
+            throw new IllegalArgumentException("argument can not be null");
+        }
+
+        // check optional leading sign
+        char sign;
+        int start;
+        if (text.startsWith("-")) {
+            sign = '-';
+            start = 1;
+        } else if (text.startsWith("+")) {
+            sign = '+';
+            start = 1;
+        } else {
+            sign = '+'; // no sign specified, implied '+'
+            start = 0;
+        }
+
+        /**
+         * the expected format of the remainder of the string is:
+         * YYYY-MM-DDThh:mm:ss.SSSTZD
+         *
+         * note that we cannot use java.text.SimpleDateFormat for
+         * parsing because it can't handle years <= 0 and TZD's
+         */
+
+        int year, month, day, hour, min, sec, ms;
+        String tzID;
+        try {
+            // year (YYYY)
+            year = Integer.parseInt(text.substring(start, start + 4));
+            start += 4;
+            // delimiter '-'
+            if (text.charAt(start) != '-') {
+                return null;
+            }
+            start++;
+            // month (MM)
+            month = Integer.parseInt(text.substring(start, start + 2));
+            start += 2;
+            // delimiter '-'
+            if (text.charAt(start) != '-') {
+                return null;
+            }
+            start++;
+            // day (DD)
+            day = Integer.parseInt(text.substring(start, start + 2));
+            start += 2;
+            // delimiter 'T'
+            if (text.charAt(start) != 'T') {
+                return null;
+            }
+            start++;
+            // hour (hh)
+            hour = Integer.parseInt(text.substring(start, start + 2));
+            start += 2;
+            // delimiter ':'
+            if (text.charAt(start) != ':') {
+                return null;
+            }
+            start++;
+            // minute (mm)
+            min = Integer.parseInt(text.substring(start, start + 2));
+            start += 2;
+            // delimiter ':'
+            if (text.charAt(start) != ':') {
+                return null;
+            }
+            start++;
+            // second (ss)
+            sec = Integer.parseInt(text.substring(start, start + 2));
+            start += 2;
+            // delimiter '.'
+            if (text.charAt(start) != '.') {
+                return null;
+            }
+            start++;
+            // millisecond (SSS)
+            ms = Integer.parseInt(text.substring(start, start + 3));
+            start += 3;
+            // time zone designator (Z or +00:00 or -00:00)
+            if (text.charAt(start) == '+' || text.charAt(start) == '-') {
+                // offset to UTC specified in the format +00:00/-00:00
+                tzID = "GMT" + text.substring(start);
+            } else if (text.substring(start).equals("Z")) {
+                tzID = "GMT";
+            } else {
+                // invalid time zone designator
+                return null;
+            }
+        } catch (IndexOutOfBoundsException e) {
+            return null;
+        } catch (NumberFormatException e) {
+            return null;
+        }
+
+        TimeZone tz = TimeZone.getTimeZone(tzID);
+        // verify id of returned time zone (getTimeZone defaults to "GMT")
+        if (!tz.getID().equals(tzID)) {
+            // invalid time zone
+            return null;
+        }
+
+        // initialize Calendar object
+        Calendar cal = Calendar.getInstance(tz);
+        cal.setLenient(false);
+        // year and era
+        if (sign == '-' || year == 0) {
+            // not CE, need to set era (BCE) and adjust year
+            cal.set(Calendar.YEAR, year + 1);
+            cal.set(Calendar.ERA, GregorianCalendar.BC);
+        } else {
+            cal.set(Calendar.YEAR, year);
+            cal.set(Calendar.ERA, GregorianCalendar.AD);
+        }
+        // month (0-based!)
+        cal.set(Calendar.MONTH, month - 1);
+        // day of month
+        cal.set(Calendar.DAY_OF_MONTH, day);
+        // hour
+        cal.set(Calendar.HOUR_OF_DAY, hour);
+        // minute
+        cal.set(Calendar.MINUTE, min);
+        // second
+        cal.set(Calendar.SECOND, sec);
+        // millisecond
+        cal.set(Calendar.MILLISECOND, ms);
+
+        try {
+            /**
+             * the following call will trigger an IllegalArgumentException
+             * if any of the set values are illegal or out of range
+             */
+            cal.getTime();
+            /**
+             * in addition check the validity of the year
+             */
+            getYear(cal);
+        } catch (IllegalArgumentException e) {
+            return null;
+        }
+
+        return cal;
+    }
+
+    /**
+     * Formats a <code>Calendar</code> value into an ISO8601-compliant
+     * date/time string.
+     *
+     * @param cal the time value to be formatted into a date/time string.
+     * @return the formatted date/time string.
+     * @throws IllegalArgumentException if a <code>null</code> argument is passed
+     * or the calendar cannot be represented as defined by ISO 8601 (i.e. year
+     * with more than four digits).
+     */
+    public static String format(Calendar cal) throws IllegalArgumentException {
+        if (cal == null) {
+            throw new IllegalArgumentException("argument can not be null");
+        }
+
+        /**
+         * the format of the date/time string is:
+         * YYYY-MM-DDThh:mm:ss.SSSTZD
+         *
+         * note that we cannot use java.text.SimpleDateFormat for
+         * formatting because it can't handle years <= 0 and TZD's
+         */
+        StringBuffer buf = new StringBuffer();
+        // year ([-]YYYY)
+        appendZeroPaddedInt(buf, getYear(cal), 4);
+        buf.append('-');
+        // month (MM)
+        appendZeroPaddedInt(buf, cal.get(Calendar.MONTH) + 1, 2);
+        buf.append('-');
+        // day (DD)
+        appendZeroPaddedInt(buf, cal.get(Calendar.DAY_OF_MONTH), 2);
+        buf.append('T');
+        // hour (hh)
+        appendZeroPaddedInt(buf, cal.get(Calendar.HOUR_OF_DAY), 2);
+        buf.append(':');
+        // minute (mm)
+        appendZeroPaddedInt(buf, cal.get(Calendar.MINUTE), 2);
+        buf.append(':');
+        // second (ss)
+        appendZeroPaddedInt(buf, cal.get(Calendar.SECOND), 2);
+        buf.append('.');
+        // millisecond (SSS)
+        appendZeroPaddedInt(buf, cal.get(Calendar.MILLISECOND), 3);
+        // time zone designator (Z or +00:00 or -00:00)
+        TimeZone tz = cal.getTimeZone();
+        // determine offset of timezone from UTC (incl. daylight saving)
+        int offset = tz.getOffset(cal.getTimeInMillis());
+        if (offset == 0) {
+            buf.append('Z');
+        } 
+        else {
+            int hours = Math.abs(offset / (60 * 1000) / 60);
+            int minutes = Math.abs(offset / (60 * 1000) % 60);
+            buf.append(offset < 0 ? '-' : '+');
+            appendZeroPaddedInt(buf, hours, 2);
+            buf.append(':');
+            appendZeroPaddedInt(buf, minutes, 2);
+        }
+        return buf.toString();
+    }
+
+    /**
+     * Returns the astronomical year of the given calendar.
+     *
+     * @param cal a calendar instance.
+     * @return the astronomical year.
+     * @throws IllegalArgumentException if calendar cannot be represented as
+     *                                  defined by ISO 8601 (i.e. year with more
+     *                                  than four digits).
+     */
+    public static int getYear(Calendar cal) throws IllegalArgumentException {
+        // determine era and adjust year if necessary
+        int year = cal.get(Calendar.YEAR);
+        if (cal.isSet(Calendar.ERA)
+                && cal.get(Calendar.ERA) == GregorianCalendar.BC) {
+
+            // calculate year using astronomical system:
+            // year n BCE => astronomical year -n + 1
+            year = 0 - year + 1;
+        }
+
+        if (year > 9999 || year < -9999) {
+            throw new IllegalArgumentException("Calendar has more than four " +
+                    "year digits, cannot be formatted as ISO8601: " + year);
+        }
+        return year;
+    }
+
+    /**
+     * Appends a zero-padded number to the given string buffer.
+     * <p/>
+     * This is an internal helper method which doesn't perform any
+     * validation on the given arguments.
+     *
+     * @param buf String buffer to append to
+     * @param n number to append
+     * @param precision number of digits to append
+     */
+    private static void appendZeroPaddedInt(StringBuffer buf, int n, int precision) {
+        if (n < 0) {
+            buf.append('-');
+            n = -n;
+        }
+
+        for (int exp = precision - 1; exp > 0; exp--) {
+            if (n < Math.pow(10, exp)) {
+                buf.append('0');
+            } else {
+                break;
+            }
+        }
+        buf.append(n);
+    }
+}

Added: chemistry/opencmis/trunk/chemistry-opencmis-server/chemistry-opencmis-server-jcr/src/main/java/org/apache/chemistry/opencmis/jcr/util/Iterables.java
URL: http://svn.apache.org/viewvc/chemistry/opencmis/trunk/chemistry-opencmis-server/chemistry-opencmis-server-jcr/src/main/java/org/apache/chemistry/opencmis/jcr/util/Iterables.java?rev=1087664&view=auto
==============================================================================
--- chemistry/opencmis/trunk/chemistry-opencmis-server/chemistry-opencmis-server-jcr/src/main/java/org/apache/chemistry/opencmis/jcr/util/Iterables.java (added)
+++ chemistry/opencmis/trunk/chemistry-opencmis-server/chemistry-opencmis-server-jcr/src/main/java/org/apache/chemistry/opencmis/jcr/util/Iterables.java Fri Apr  1 11:59:46 2011
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.chemistry.opencmis.jcr.util;
+
+import org.apache.commons.collections.iterators.EmptyIterator;
+import org.apache.commons.collections.iterators.IteratorChain;
+import org.apache.commons.collections.iterators.SingletonIterator;
+
+import java.util.Iterator;
+
+public class Iterables {
+    private Iterables() {}
+
+    public static <T> Iterable<T> concat(final Iterable<T> it1, final Iterable<T> it2) {
+        return new Iterable<T>() {
+            @SuppressWarnings("unchecked")
+            public Iterator<T> iterator() {
+                return new IteratorChain(it1.iterator(), it2.iterator());
+            }
+        };
+    }
+
+    public static <T> Iterable<T> singleton(final T element) {
+        return new Iterable<T>() {
+            @SuppressWarnings("unchecked")
+            public Iterator<T> iterator() {
+                return new SingletonIterator(element);
+            }
+        };
+    }
+
+    public static <T> Iterable<T> empty() {
+        return new Iterable<T>() {
+            @SuppressWarnings("unchecked")
+            public Iterator<T> iterator() {
+                return EmptyIterator.INSTANCE;
+            }
+        };
+    }
+}

Modified: chemistry/opencmis/trunk/chemistry-opencmis-server/chemistry-opencmis-server-jcr/src/main/java/org/apache/chemistry/opencmis/jcr/util/Util.java
URL: http://svn.apache.org/viewvc/chemistry/opencmis/trunk/chemistry-opencmis-server/chemistry-opencmis-server-jcr/src/main/java/org/apache/chemistry/opencmis/jcr/util/Util.java?rev=1087664&r1=1087663&r2=1087664&view=diff
==============================================================================
--- chemistry/opencmis/trunk/chemistry-opencmis-server/chemistry-opencmis-server-jcr/src/main/java/org/apache/chemistry/opencmis/jcr/util/Util.java (original)
+++ chemistry/opencmis/trunk/chemistry-opencmis-server/chemistry-opencmis-server-jcr/src/main/java/org/apache/chemistry/opencmis/jcr/util/Util.java Fri Apr  1 11:59:46 2011
@@ -82,4 +82,13 @@ public final class Util {
         return result.toString();
     }
 
+    /**
+     * Escapes a JCR path such that it can be used in a XPath query
+     * @param path
+     * @return  escaped path
+     */
+    public static String escape(String path) {
+        return replace(path, " ", "_x0020_"); // fixme do more thorough escaping of path
+    }
+
 }

Added: chemistry/opencmis/trunk/chemistry-opencmis-server/chemistry-opencmis-server-jcr/src/test/java/org/apache/chemistry/opencmis/jcr/query/QueryTranslatorTest.java
URL: http://svn.apache.org/viewvc/chemistry/opencmis/trunk/chemistry-opencmis-server/chemistry-opencmis-server-jcr/src/test/java/org/apache/chemistry/opencmis/jcr/query/QueryTranslatorTest.java?rev=1087664&view=auto
==============================================================================
--- chemistry/opencmis/trunk/chemistry-opencmis-server/chemistry-opencmis-server-jcr/src/test/java/org/apache/chemistry/opencmis/jcr/query/QueryTranslatorTest.java (added)
+++ chemistry/opencmis/trunk/chemistry-opencmis-server/chemistry-opencmis-server-jcr/src/test/java/org/apache/chemistry/opencmis/jcr/query/QueryTranslatorTest.java Fri Apr  1 11:59:46 2011
@@ -0,0 +1,295 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.chemistry.opencmis.jcr.query;
+
+import org.antlr.runtime.RecognitionException;
+import org.apache.chemistry.opencmis.commons.definitions.TypeDefinition;
+import org.apache.chemistry.opencmis.commons.exceptions.CmisInvalidArgumentException;
+import org.apache.chemistry.opencmis.commons.exceptions.CmisNotSupportedException;
+import org.apache.chemistry.opencmis.jcr.JcrTypeManager;
+import org.apache.chemistry.opencmis.jcr.util.ISO8601;
+import org.apache.chemistry.opencmis.server.support.query.CalendarHelper;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.GregorianCalendar;
+
+import static org.junit.Assert.*;
+
+public class QueryTranslatorTest {
+    private String jcrTypeCondition;
+
+    QueryTranslator queryTranslator = new QueryTranslator(new JcrTypeManager()) {
+        @Override
+        protected String jcrPathFromId(String id) {
+            assertNotNull(id);
+            return "/jcr:" + id;
+        }
+
+        @Override
+        protected String jcrPathFromCol(TypeDefinition fromType, String name) {
+            assertNotNull(fromType);
+            assertNotNull(name);
+            return name.replace("cmis:", "@jcr:");
+        }
+
+        @Override
+        protected String jcrTypeName(TypeDefinition fromType) {
+            assertNotNull(fromType);
+            return fromType.getQueryName().replace("cmis:", "jcr:");
+        }
+
+        @Override
+        protected String jcrTypeCondition(TypeDefinition fromType) {
+            assertNotNull(fromType);
+            return jcrTypeCondition;
+        }
+    };
+
+    @Test
+    public void testQueryTranslator() {
+        assertEquals(
+            "/jcr:root//element(*,jcr:document)",
+            queryTranslator.translateToXPath("select * from cmis:document"));
+
+        assertEquals(
+            "/jcr:root//element(*,jcr:document)[@jcr:isLatestVersion = 'foo']",
+            queryTranslator.translateToXPath("select * from cmis:document where cmis:isLatestVersion='foo'"));
+
+        assertEquals(
+            "/jcr:root//element(*,jcr:document)[jcr:like(@jcr:isLatestVersion, 'foo')]",
+            queryTranslator.translateToXPath("select * from cmis:document where cmis:isLatestVersion LIKE 'foo'"));
+
+        assertEquals(
+            "/jcr:root//element(*,jcr:document)[not(jcr:like(@jcr:isLatestVersion, 'foo'))]",
+            queryTranslator.translateToXPath(
+                    "select * from cmis:document where cmis:isLatestVersion NOT LIKE 'foo'"));
+
+        assertEquals(
+            "/jcr:root//element(*,jcr:document)[@jcr:isLatestVersion = 'foo' and @jcr:name != 'baz']",
+            queryTranslator.translateToXPath(
+                    "select * from cmis:document where cmis:isLatestVersion='foo' AND cmis:name<>'baz'"));
+
+        assertEquals(
+            "/jcr:root//element(*,jcr:document)[not((@jcr:isLatestVersion > 'foo' or @jcr:name < 1.0))]",
+            queryTranslator.translateToXPath(
+                    "select * from cmis:document where NOT (cmis:isLatestVersion>'foo' OR cmis:name< 1.0)"));
+
+
+        assertEquals(
+            "/jcr:root//element(*,jcr:document)[(@jcr:name = 'foo' or @jcr:objectId = 'baz' and @jcr:createdBy = 'bar')]",
+            queryTranslator.translateToXPath(
+                    "select * from cmis:document where cmis:name = 'foo' or cmis:objectId = 'baz' " +
+                    "and cmis:createdBy = 'bar'"));
+
+        assertEquals(
+            "/jcr:root//element(*,jcr:document)[(@jcr:name = 'foo' and @jcr:objectId = 'baz' or @jcr:createdBy = 'bar')]",
+            queryTranslator.translateToXPath(
+                    "select * from cmis:document where cmis:name = 'foo' and cmis:objectId = 'baz' " +
+                    "or cmis:createdBy = 'bar'"));
+
+        assertEquals(
+            "/jcr:root//element(*,jcr:document)[@jcr:name = 'foo' and (@jcr:objectId = 'baz' or @jcr:createdBy = 'bar')]",
+            queryTranslator.translateToXPath(
+                "select * from cmis:document where cmis:name = 'foo' and (cmis:objectId = 'baz' " +
+                "or cmis:createdBy = 'bar')"));
+
+        assertEquals(
+                "/jcr:root/jcr:folderId/element(*,jcr:document)",
+                queryTranslator.translateToXPath(
+                        "select * from cmis:document where IN_FOLDER('folderId')"));
+
+        assertEquals(
+            "/jcr:root/jcr:folderId/element(*,jcr:document)",
+            queryTranslator.translateToXPath("select * from cmis:document where not(not(IN_FOLDER('folderId')))"));
+
+        assertEquals(
+            "/jcr:root/jcr:folderId//element(*,jcr:document)",
+            queryTranslator.translateToXPath("select * from cmis:document where IN_TREE('folderId')"));
+
+        assertEquals(
+            "/jcr:root/jcr:folderId//element(*,jcr:document)",
+            queryTranslator.translateToXPath("select * from cmis:document where not(not(IN_TREE('folderId')))"));
+
+        assertEquals(
+            "/jcr:root/jcr:folderId/element(*,jcr:document)[@jcr:name <= 1]",
+            queryTranslator.translateToXPath(
+                    "select * from cmis:document where IN_FOLDER('folderId') AND cmis:name <= 1"));
+
+        assertEquals(
+            "/jcr:root/jcr:folderId//element(*,jcr:document)[@jcr:name >= 'name' and @jcr:name = true]",
+            queryTranslator.translateToXPath(
+                    "select * from cmis:document where IN_TREE('folderId') AND cmis:name >= 'name' " +
+                    "AND cmis:name = TRUE"));
+
+        GregorianCalendar date = new GregorianCalendar();
+        assertEquals(
+            "/jcr:root/jcr:folderId/element(*,jcr:document)[not(@jcr:creationDate = xs:dateTime('" +
+                    ISO8601.format(date) + "'))]",
+            queryTranslator.translateToXPath(
+                    "select * from cmis:document where NOT(NOT IN_FOLDER('folderId') OR cmis:creationDate = TIMESTAMP '" +
+                    CalendarHelper.toString(date) + "')"));
+
+        // fixme: need fix for CMIS-344
+        assertEquals(
+            "/jcr:root//element(*,jcr:document)[jcr:contains(., '\u4E2D\u6587')]",
+            queryTranslator.translateToXPath("select * from cmis:document where contains('\u4E2D\u6587')"));
+    }
+
+    @Test
+    public void testQueryWithOrderBy() {
+        assertEquals(
+            "/jcr:root//element(*,jcr:document)order by @jcr:name ascending",
+            queryTranslator.translateToXPath("select * from cmis:document order by cmis:name"));
+
+        assertEquals(
+            "/jcr:root//element(*,jcr:document)[@jcr:isLatestVersion = 'foo']order by @jcr:name descending",
+            queryTranslator.translateToXPath(
+                "select * from cmis:document where cmis:isLatestVersion='foo' order by cmis:name desc"));
+
+        assertEquals(
+            "/jcr:root//element(*,jcr:document)[jcr:like(@jcr:isLatestVersion, 'foo')]order by @jcr:name ascending," +
+                "@jcr:objectId descending",
+            queryTranslator.translateToXPath(
+                "select * from cmis:document where cmis:isLatestVersion LIKE 'foo' order by cmis:name asc, cmis:objectId desc"));
+    }
+
+    @Test
+    public void testQueryTranslatorWithTypeCondition() {
+        jcrTypeCondition = "@jcr:primaryType = nt:base";
+
+        assertEquals(
+            "/jcr:root//element(*,jcr:document)[@jcr:primaryType = nt:base]",
+            queryTranslator.translateToXPath("select * from cmis:document"));
+
+        assertEquals(
+            "/jcr:root//element(*,jcr:document)[@jcr:primaryType = nt:base and @jcr:isLatestVersion = 'foo']",
+            queryTranslator.translateToXPath("select * from cmis:document where cmis:isLatestVersion='foo'"));
+
+        assertEquals(
+            "/jcr:root/jcr:folderId/element(*,jcr:document)[@jcr:primaryType = nt:base]",
+            queryTranslator.translateToXPath("select * from cmis:document where IN_FOLDER('folderId')"));
+
+        assertEquals(
+            "/jcr:root/jcr:folderId/element(*,jcr:document)[@jcr:primaryType = nt:base]",
+            queryTranslator.translateToXPath("select * from cmis:document where not(not(IN_FOLDER('folderId')))"));
+
+        assertEquals(
+            "/jcr:root/jcr:folderId//element(*,jcr:document)[@jcr:primaryType = nt:base]",
+            queryTranslator.translateToXPath("select * from cmis:document where IN_TREE('folderId')"));
+
+        assertEquals(
+            "/jcr:root/jcr:folderId//element(*,jcr:document)[@jcr:primaryType = nt:base]",
+            queryTranslator.translateToXPath("select * from cmis:document where not(not(IN_TREE('folderId')))"));
+        
+        assertEquals(
+            "/jcr:root/jcr:folderId/element(*,jcr:document)[@jcr:primaryType = nt:base and @jcr:name <= 1]",
+            queryTranslator.translateToXPath(
+                    "select * from cmis:document where IN_FOLDER('folderId') AND cmis:name <= 1"));
+    }
+
+    @Test
+    public void testQueryTranslatorQueryTooSpecific() {
+        try {
+            queryTranslator.translateToXPath(
+                "select * from cmis:document where NOT IN_FOLDER('folderId')");
+            fail();
+        }
+        catch (CmisInvalidArgumentException expected) { }
+
+        try {
+            queryTranslator.translateToXPath(
+                "select * from cmis:document where NOT(NOT IN_FOLDER('folderId') AND cmis:name = 'name')");
+            fail();
+        }
+        catch (CmisInvalidArgumentException expected) { }
+
+        try {
+            queryTranslator.translateToXPath(
+                "select * from cmis:document where IN_FOLDER('folderId') OR cmis:name = 'name'");
+            fail();
+        }
+        catch (CmisInvalidArgumentException expected) { }
+
+        try {
+            queryTranslator.translateToXPath(
+                "select * from cmis:document where NOT(IN_FOLDER('folderId') AND cmis:name = 'name')");
+            fail();
+        }
+        catch (CmisInvalidArgumentException expected) { }
+
+        try {
+            queryTranslator.translateToXPath(
+                "select * from cmis:document where IN_FOLDER('folder1Id') OR IN_TREE('folder2Id')");
+            fail();
+        }
+        catch (CmisInvalidArgumentException expected) { }
+
+        try {
+            queryTranslator.translateToXPath(
+                "select * from cmis:document where IN_FOLDER('folder1Id') AND NOT IN_TREE('folder2Id')");
+            fail();
+        }
+        catch (CmisInvalidArgumentException expected) { }
+    }
+
+    @Test
+    public void testNotImplemented() throws IOException, RecognitionException {
+        try {
+            queryTranslator.translateToXPath("select * from cmis:document where cmis:name in (1,2,3)");
+            fail();
+        }
+        catch (CmisNotSupportedException expected) {}
+
+        try {
+            queryTranslator.translateToXPath("select * from cmis:document where 'foo' = ANY cmis:name");
+            fail();
+        }
+        catch (CmisNotSupportedException expected) {}
+    }
+
+    @Test
+    public void testInvalidQuery() throws IOException, RecognitionException {
+        try {
+            queryTranslator.translateToXPath("");
+            fail();
+        }
+        catch (CmisInvalidArgumentException expected) {}
+
+        try {
+            queryTranslator.translateToXPath("select * from cmis:something");
+            fail();
+        }
+        catch (CmisInvalidArgumentException expected) {}
+
+        try {
+            queryTranslator.translateToXPath("select * from cmis:document WHERE");
+            fail();
+        }
+        catch (CmisInvalidArgumentException expected) {}
+
+        try {
+            queryTranslator.translateToXPath("select * from cmis:document WHERE cmis:something = 'foo'");
+            fail();
+        }
+        catch (CmisInvalidArgumentException expected) {}
+    }
+
+}