CAY-1966 SQLTemplate/SQLSelect positional parameter binding

    * positional parameter bindings in SQLtemplate


Project: http://git-wip-us.apache.org/repos/asf/cayenne/repo
Commit: http://git-wip-us.apache.org/repos/asf/cayenne/commit/14e9dc3b
Tree: http://git-wip-us.apache.org/repos/asf/cayenne/tree/14e9dc3b
Diff: http://git-wip-us.apache.org/repos/asf/cayenne/diff/14e9dc3b

Branch: refs/heads/CAY-1946
Commit: 14e9dc3bdad37e841cfa6bf00f17f80f6eab86b1
Parents: 2584967
Author: aadamchik <aadamc...@apache.org>
Authored: Sun Nov 2 22:59:04 2014 +0300
Committer: aadamchik <aadamc...@apache.org>
Committed: Sun Nov 2 22:59:04 2014 +0300

----------------------------------------------------------------------
 .../apache/cayenne/query/SQLTemplateTest.java   |   5 +-
 .../cayenne/access/jdbc/SQLTemplateAction.java  |  53 +++--
 .../access/jdbc/SQLTemplateProcessor.java       |   7 +
 .../org/apache/cayenne/query/SQLSelect.java     |  14 +-
 .../org/apache/cayenne/query/SQLTemplate.java   |  48 ++++-
 .../apache/cayenne/velocity/BindDirective.java  | 211 +++++++++----------
 .../cayenne/velocity/VelocityParamSequence.java |  68 ++++++
 .../velocity/VelocitySQLTemplateProcessor.java  | 101 +++++++--
 .../org/apache/cayenne/query/SQLTemplateIT.java |  71 ++++++-
 .../apache/cayenne/query/SQLTemplateTest.java   |  41 +++-
 10 files changed, 458 insertions(+), 161 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cayenne/blob/14e9dc3b/cayenne-client/src/test/java/org/apache/cayenne/query/SQLTemplateTest.java
----------------------------------------------------------------------
diff --git 
a/cayenne-client/src/test/java/org/apache/cayenne/query/SQLTemplateTest.java 
b/cayenne-client/src/test/java/org/apache/cayenne/query/SQLTemplateTest.java
index b727d83..d432fe1 100644
--- a/cayenne-client/src/test/java/org/apache/cayenne/query/SQLTemplateTest.java
+++ b/cayenne-client/src/test/java/org/apache/cayenne/query/SQLTemplateTest.java
@@ -23,7 +23,6 @@ import static org.junit.Assert.assertNotSame;
 import static org.junit.Assert.assertTrue;
 
 import java.util.Collections;
-import java.util.Map;
 
 import org.apache.cayenne.map.EntityResolver;
 import org.apache.cayenne.remote.hessian.service.HessianUtil;
@@ -45,9 +44,7 @@ public class SQLTemplateTest {
 
                // set immutable parameters ... query must recast them to 
mutable
                // version
-               @SuppressWarnings("unchecked")
-               Map<String, Object>[] parameters = new Map[] { 
Collections.EMPTY_MAP };
-               o.setParameters(parameters);
+               o.setParams(Collections.<String, Object> emptyMap());
 
                HessianUtil.cloneViaClientServerSerialization(o, new 
EntityResolver());
        }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/14e9dc3b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SQLTemplateAction.java
----------------------------------------------------------------------
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SQLTemplateAction.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SQLTemplateAction.java
index 2e0d767..9c76fe9 100644
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SQLTemplateAction.java
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SQLTemplateAction.java
@@ -101,16 +101,54 @@ public class SQLTemplateAction implements SQLAction {
                }
 
                boolean loggable = dataNode.getJdbcEventLogger().isLoggable();
+               List<Number> counts = new ArrayList<Number>();
+
+               // bind either positional or named parameters;
+               // for legacy reasons named parameters are processed as a 
batch.. this
+               // should go away after 4.0; newer positional parameter only 
support a
+               // single set of values.
+               if (query.getPositionalParams().isEmpty()) {
+                       runWithNamedParametersBatch(connection, callback, 
template, counts, loggable);
+               } else {
+                       runWithPositionalParameters(connection, callback, 
template, counts, loggable);
+               }
+
+               // notify of combined counts of all queries inside SQLTemplate
+               // multiplied by the
+               // number of parameter sets...
+               int[] ints = new int[counts.size()];
+               for (int i = 0; i < ints.length; i++) {
+                       ints[i] = counts.get(i).intValue();
+               }
+
+               callback.nextBatchCount(query, ints);
+       }
+
+       private void runWithPositionalParameters(Connection connection, 
OperationObserver callback, String template,
+                       Collection<Number> counts, boolean loggable) throws 
Exception {
+
+               SQLStatement compiled = 
dataNode.getSqlTemplateProcessor().processTemplate(template,
+                               query.getPositionalParams());
+
+               if (loggable) {
+                       
dataNode.getJdbcEventLogger().logQuery(compiled.getSql(), 
Arrays.asList(compiled.getBindings()));
+               }
+
+               execute(connection, callback, compiled, counts);
+       }
+
+       @SuppressWarnings("deprecation")
+       private void runWithNamedParametersBatch(Connection connection, 
OperationObserver callback, String template,
+                       Collection<Number> counts, boolean loggable) throws 
Exception {
+
                int size = query.parametersSize();
 
                // zero size indicates a one-shot query with no parameters
                // so fake a single entry batch...
                int batchSize = (size > 0) ? size : 1;
 
-               List<Number> counts = new ArrayList<Number>(batchSize);
-
                // for now supporting deprecated batch parameters...
-               @SuppressWarnings({ "deprecation", "unchecked" })
+               @SuppressWarnings("unchecked")
                Iterator<Map<String, ?>> it = (size > 0) ? 
query.parametersIterator() : IteratorUtils
                                .singletonIterator(Collections.emptyMap());
                for (int i = 0; i < batchSize; i++) {
@@ -125,15 +163,6 @@ public class SQLTemplateAction implements SQLAction {
                        execute(connection, callback, compiled, counts);
                }
 
-               // notify of combined counts of all queries inside SQLTemplate
-               // multiplied by the
-               // number of parameter sets...
-               int[] ints = new int[counts.size()];
-               for (int i = 0; i < ints.length; i++) {
-                       ints[i] = counts.get(i).intValue();
-               }
-
-               callback.nextBatchCount(query, ints);
        }
 
        protected void execute(Connection connection, OperationObserver 
callback, SQLStatement compiled,

http://git-wip-us.apache.org/repos/asf/cayenne/blob/14e9dc3b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SQLTemplateProcessor.java
----------------------------------------------------------------------
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SQLTemplateProcessor.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SQLTemplateProcessor.java
index 3873494..fc6ef62 100644
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SQLTemplateProcessor.java
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SQLTemplateProcessor.java
@@ -18,6 +18,7 @@
  ****************************************************************/
 package org.apache.cayenne.access.jdbc;
 
+import java.util.List;
 import java.util.Map;
 
 /**
@@ -30,4 +31,10 @@ public interface SQLTemplateProcessor {
         * of parameters.
         */
        SQLStatement processTemplate(String template, Map<String, ?> 
parameters);
+
+       /**
+        * Builds and returns a SQLStatement based on SQL template String and a 
list
+        * of positional parameters.
+        */
+       SQLStatement processTemplate(String template, List<Object> 
positionalParameters);
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/14e9dc3b/cayenne-server/src/main/java/org/apache/cayenne/query/SQLSelect.java
----------------------------------------------------------------------
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/query/SQLSelect.java 
b/cayenne-server/src/main/java/org/apache/cayenne/query/SQLSelect.java
index 7f36267..fc413fa 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/query/SQLSelect.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/query/SQLSelect.java
@@ -89,7 +89,7 @@ public class SQLSelect<T> extends IndirectQuery implements 
Select<T> {
        protected StringBuilder sqlBuffer;
        protected QueryCacheStrategy cacheStrategy;
        protected String[] cacheGroups;
-       protected Map<String, Object> parameters;
+       protected Map<String, Object> params;
        protected CapsStrategy columnNameCaps;
        protected int limit;
        protected int offset;
@@ -103,7 +103,7 @@ public class SQLSelect<T> extends IndirectQuery implements 
Select<T> {
        public SQLSelect(Class<T> persistentType, String sql) {
                this.persistentType = persistentType;
                this.sqlBuffer = sql != null ? new StringBuilder(sql) : new 
StringBuilder();
-               this.parameters = new HashMap<String, Object>();
+               this.params = new HashMap<String, Object>();
                this.limit = QueryMetadata.FETCH_LIMIT_DEFAULT;
                this.offset = QueryMetadata.FETCH_OFFSET_DEFAULT;
                this.pageSize = QueryMetadata.PAGE_SIZE_DEFAULT;
@@ -148,7 +148,7 @@ public class SQLSelect<T> extends IndirectQuery implements 
Select<T> {
        }
 
        public SQLSelect<T> params(String name, Object value) {
-               parameters.put(name, value);
+               params.put(name, value);
                this.replacementQuery = null;
                return this;
        }
@@ -162,12 +162,12 @@ public class SQLSelect<T> extends IndirectQuery 
implements Select<T> {
        }
 
        /**
-        * Returns mutable map of parameters that will be bound to SQL. A 
caller is
-        * free to add/remove parameters from the returned map as needed.
+        * Returns a mutable map of parameters that will be bound to SQL. A 
caller
+        * is free to add/remove parameters from the returned map as needed.
         * Alternatively one may use chained {@link #params(String, Object)}
         */
        public Map<String, Object> getParams() {
-               return parameters;
+               return params;
        }
 
        @Override
@@ -195,7 +195,7 @@ public class SQLSelect<T> extends IndirectQuery implements 
Select<T> {
                template.setDefaultTemplate(getSql());
                template.setCacheGroups(cacheGroups);
                template.setCacheStrategy(cacheStrategy);
-               template.setParams(parameters);
+               template.setParams(params);
                template.setColumnNamesCapitalization(columnNameCaps);
                template.setFetchLimit(limit);
                template.setFetchOffset(offset);

http://git-wip-us.apache.org/repos/asf/cayenne/blob/14e9dc3b/cayenne-server/src/main/java/org/apache/cayenne/query/SQLTemplate.java
----------------------------------------------------------------------
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/query/SQLTemplate.java 
b/cayenne-server/src/main/java/org/apache/cayenne/query/SQLTemplate.java
index 3d3514b..dd4b176 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/query/SQLTemplate.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/query/SQLTemplate.java
@@ -19,10 +19,12 @@
 
 package org.apache.cayenne.query;
 
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Iterator;
+import java.util.List;
 import java.util.Map;
 import java.util.TreeSet;
 
@@ -82,6 +84,7 @@ public class SQLTemplate extends AbstractQuery implements 
ParameterizedQuery, XM
        protected String defaultTemplate;
        protected Map<String, String> templates;
        protected Map<String, ?>[] parameters;
+       protected List<Object> positionalParams;
        protected CapsStrategy columnNamesCapitalization;
        protected SQLResult result;
        private String dataNodeName;
@@ -323,15 +326,42 @@ public class SQLTemplate extends AbstractQuery implements 
ParameterizedQuery, XM
        }
 
        /**
-        * Initializes parameters map of this query.
+        * Initializes named parameter of this query. Note that calling this 
method
+        * will reset any positional parameters.
         * 
         * @since 4.0
         */
        @SuppressWarnings("unchecked")
-       public void setParams(Map<String, ?> parameters) {
+       public void setParams(Map<String, ?> params) {
+
+               // since named parameters are specified, resetting positional
+               // parameters
+               this.positionalParams = null;
+
                // calling a deprecated method until we can remove 
multi-parameter-batch
                // deprecation.
-               setParameters(parameters);
+               setParameters(params);
+       }
+
+       /**
+        * Initializes positional parameters of the query. This is a positional
+        * style of binding. Names of variables in the expression are ignored 
and
+        * parameters are bound in order they are found in the expression. E.g. 
if
+        * the same name is mentioned twice, it can be bound to two different
+        * values. If declared and provided parameters counts are mismatched, an
+        * exception will be thrown.
+        * <p>
+        * Note that calling this method will reset any previously set *named*
+        * parameters.
+        * 
+        * @since 4.0
+        */
+       public void setParamsArray(Object... params) {
+               // since positional parameters are specified, resetting named
+               // parameters
+               this.parameters = null;
+
+               this.positionalParams = params != null ? Arrays.asList(params) 
: null;
        }
 
        /**
@@ -552,7 +582,7 @@ public class SQLTemplate extends AbstractQuery implements 
ParameterizedQuery, XM
        }
 
        /**
-        * Returns a map of parameters.
+        * Returns a map of named parameters that will be bound to SQL.
         * 
         * @since 4.0
         */
@@ -562,6 +592,15 @@ public class SQLTemplate extends AbstractQuery implements 
ParameterizedQuery, XM
        }
 
        /**
+        * Returns a list of positional parameters that will be bound to SQL.
+        * 
+        * @since 4.0
+        */
+       public List<Object> getPositionalParams() {
+               return positionalParams != null ? positionalParams : 
Collections.emptyList();
+       }
+
+       /**
         * Utility method to get the first set of parameters, since most queries
         * will only have one.
         * 
@@ -581,6 +620,7 @@ public class SQLTemplate extends AbstractQuery implements 
ParameterizedQuery, XM
         *             batches of parameters are superseded by the use of
         *             {@link QueryChain}.
         */
+       @SuppressWarnings("unchecked")
        @Deprecated
        public void setParameters(Map<String, ?>... parameters) {
 

http://git-wip-us.apache.org/repos/asf/cayenne/blob/14e9dc3b/cayenne-server/src/main/java/org/apache/cayenne/velocity/BindDirective.java
----------------------------------------------------------------------
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/velocity/BindDirective.java 
b/cayenne-server/src/main/java/org/apache/cayenne/velocity/BindDirective.java
index 6e7e83e..a2b50a2 100644
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/velocity/BindDirective.java
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/velocity/BindDirective.java
@@ -35,8 +35,8 @@ import org.apache.velocity.runtime.directive.Directive;
 import org.apache.velocity.runtime.parser.node.Node;
 
 /**
- * A custom Velocity directive to create a PreparedStatement parameter text. 
There are the
- * following possible invocation formats inside the template:
+ * A custom Velocity directive to create a PreparedStatement parameter text.
+ * There are the following possible invocation formats inside the template:
  * 
  * <pre>
  * #bind(value) - e.g. #bind($xyz)
@@ -51,16 +51,17 @@ import org.apache.velocity.runtime.parser.node.Node;
  * </p>
  * <p>
  * <code>"WHERE SOME_COLUMN &gt; #bind($xyz)"</code> produces
- * <code>"WHERE SOME_COLUMN &gt; ?"</code> and also places the value of the 
"xyz" parameter
- * in the context "bindings" collection.
+ * <code>"WHERE SOME_COLUMN &gt; ?"</code> and also places the value of the
+ * "xyz" parameter in the context "bindings" collection.
  * </p>
  * <p>
  * <strong>Binding ID column of a DataObject value:</strong>
  * </p>
  * <p>
  * <code>"WHERE ID_COL1 = #bind($helper.cayenneExp($xyz, 'db:ID_COL2')) 
- * AND ID_COL2 = #bind($helper.cayenneExp($xyz, 'db:ID_COL2'))"</code> 
produces <code>"WHERE ID_COL1 = ? AND ID_COL2 = ?"</code> and also places the
- * values of id columns of the DataObject parameter "xyz" in the context 
"bindings"
+ * AND ID_COL2 = #bind($helper.cayenneExp($xyz, 'db:ID_COL2'))"</code> produces
+ * <code>"WHERE ID_COL1 = ? AND ID_COL2 = ?"</code> and also places the values
+ * of id columns of the DataObject parameter "xyz" in the context "bindings"
  * collection.
  * </p>
  * 
@@ -68,109 +69,97 @@ import org.apache.velocity.runtime.parser.node.Node;
  */
 public class BindDirective extends Directive {
 
-    @Override
-    public String getName() {
-        return "bind";
-    }
-
-    @Override
-    public int getType() {
-        return LINE;
-    }
-
-    /**
-     * Extracts the value of the object property to render and passes control 
to
-     * {@link #render(InternalContextAdapter, Writer, ParameterBinding)} to do 
the actual
-     * rendering.
-     */
-    @Override
-    public boolean render(InternalContextAdapter context, Writer writer, Node 
node)
-            throws IOException, ResourceNotFoundException, ParseErrorException,
-            MethodInvocationException {
-
-        Object value = getChild(context, node, 0);
-        Object type = getChild(context, node, 1);
-        int scale = ConversionUtil.toInt(getChild(context, node, 2), -1);
-        String typeString = type != null ? type.toString() : null;
-
-        if (value instanceof Collection) {
-            Iterator<?> it = ((Collection) value).iterator();
-            while (it.hasNext()) {
-                render(context, writer, node, it.next(), typeString, scale);
-
-                if (it.hasNext()) {
-                    writer.write(',');
-                }
-            }
-        }
-        else {
-            render(context, writer, node, value, typeString, scale);
-        }
-
-        return true;
-    }
-
-    /**
-     * @since 3.0
-     */
-    protected void render(
-            InternalContextAdapter context,
-            Writer writer,
-            Node node,
-            Object value,
-            String typeString,
-            int scale) throws IOException, ParseErrorException {
-
-        int jdbcType = TypesMapping.NOT_DEFINED;
-        if (typeString != null) {
-            jdbcType = TypesMapping.getSqlTypeByName(typeString);
-        }
-        else if (value != null) {
-            jdbcType = TypesMapping.getSqlTypeByJava(value.getClass());
-        } else {
-            // value is null, set JDBC type to NULL
-               jdbcType = TypesMapping.getSqlTypeByName(TypesMapping.SQL_NULL);
-        }
-
-        if (jdbcType == TypesMapping.NOT_DEFINED) {
-            throw new ParseErrorException("Can't determine JDBC type of 
binding ("
-                    + value
-                    + ", "
-                    + typeString
-                    + ") at line "
-                    + node.getLine()
-                    + ", column "
-                    + node.getColumn());
-        }
-
-        render(context, writer, new ParameterBinding(value, jdbcType, scale));
-    }
-
-    protected void render(
-            InternalContextAdapter context,
-            Writer writer,
-            ParameterBinding binding) throws IOException {
-
-        bind(context, binding);
-        writer.write('?');
-    }
-
-    protected Object getChild(InternalContextAdapter context, Node node, int i)
-            throws MethodInvocationException {
-        return (i >= 0 && i < node.jjtGetNumChildren()) ? 
node.jjtGetChild(i).value(
-                context) : null;
-    }
-
-    /**
-     * Adds value to the list of bindings in the context.
-     */
-    protected void bind(InternalContextAdapter context, ParameterBinding 
binding) {
-
-        Collection bindings = (Collection) 
context.getInternalUserContext().get(
-                VelocitySQLTemplateProcessor.BINDINGS_LIST_KEY);
-
-        if (bindings != null) {
-            bindings.add(binding);
-        }
-    }
+       @Override
+       public String getName() {
+               return "bind";
+       }
+
+       @Override
+       public int getType() {
+               return LINE;
+       }
+
+       /**
+        * Extracts the value of the object property to render and passes 
control to
+        * {@link #render(InternalContextAdapter, Writer, ParameterBinding)} to 
do
+        * the actual rendering.
+        */
+       @Override
+       public boolean render(InternalContextAdapter context, Writer writer, 
Node node) throws IOException,
+                       ResourceNotFoundException, ParseErrorException, 
MethodInvocationException {
+
+               Object value = getChild(context, node, 0);
+               Object type = getChild(context, node, 1);
+               int scale = ConversionUtil.toInt(getChild(context, node, 2), 
-1);
+               String typeString = type != null ? type.toString() : null;
+
+               if (value instanceof Collection) {
+                       Iterator<?> it = ((Collection) value).iterator();
+                       while (it.hasNext()) {
+                               render(context, writer, node, it.next(), 
typeString, scale);
+
+                               if (it.hasNext()) {
+                                       writer.write(',');
+                               }
+                       }
+               } else {
+                       render(context, writer, node, value, typeString, scale);
+               }
+
+               return true;
+       }
+
+       /**
+        * @since 3.0
+        */
+       protected void render(InternalContextAdapter context, Writer writer, 
Node node, Object value, String typeString,
+                       int scale) throws IOException, ParseErrorException {
+
+               int jdbcType = TypesMapping.NOT_DEFINED;
+               if (typeString != null) {
+                       jdbcType = TypesMapping.getSqlTypeByName(typeString);
+               } else if (value != null) {
+                       jdbcType = 
TypesMapping.getSqlTypeByJava(value.getClass());
+               } else {
+                       // value is null, set JDBC type to NULL
+                       jdbcType = 
TypesMapping.getSqlTypeByName(TypesMapping.SQL_NULL);
+               }
+
+               if (jdbcType == TypesMapping.NOT_DEFINED) {
+                       throw new ParseErrorException("Can't determine JDBC 
type of binding (" + value + ", " + typeString
+                                       + ") at line " + node.getLine() + ", 
column " + node.getColumn());
+               }
+
+               render(context, writer, new ParameterBinding(value, jdbcType, 
scale));
+       }
+
+       protected void render(InternalContextAdapter context, Writer writer, 
ParameterBinding binding) throws IOException {
+
+               bind(context, binding);
+               writer.write('?');
+       }
+
+       protected Object getChild(InternalContextAdapter context, Node node, 
int i) throws MethodInvocationException {
+               Object child = (i >= 0 && i < node.jjtGetNumChildren()) ? 
node.jjtGetChild(i).value(context) : null;
+
+               // unwrap postional parameters
+               if (child instanceof VelocityParamSequence) {
+                       child = ((VelocityParamSequence) child).next();
+               }
+
+               return child;
+       }
+
+       /**
+        * Adds value to the list of bindings in the context.
+        */
+       protected void bind(InternalContextAdapter context, ParameterBinding 
binding) {
+
+               Collection bindings = (Collection) 
context.getInternalUserContext().get(
+                               VelocitySQLTemplateProcessor.BINDINGS_LIST_KEY);
+
+               if (bindings != null) {
+                       bindings.add(binding);
+               }
+       }
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/14e9dc3b/cayenne-server/src/main/java/org/apache/cayenne/velocity/VelocityParamSequence.java
----------------------------------------------------------------------
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/velocity/VelocityParamSequence.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/velocity/VelocityParamSequence.java
new file mode 100644
index 0000000..089e7f4
--- /dev/null
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/velocity/VelocityParamSequence.java
@@ -0,0 +1,68 @@
+/*****************************************************************
+ *   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.cayenne.velocity;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.exception.MethodInvocationException;
+import org.apache.velocity.exception.ParseErrorException;
+import org.apache.velocity.exception.ResourceNotFoundException;
+import org.apache.velocity.runtime.Renderable;
+
+/**
+ * A parameter value container that helps to may a single velocity variable to 
a
+ * sequence of positional parameters.
+ * 
+ * @since 4.0
+ */
+class VelocityParamSequence implements Renderable {
+
+       private List<Object> parameters;
+       private int index;
+
+       VelocityParamSequence() {
+               this.parameters = new ArrayList<Object>();
+       }
+
+       void add(Object parameter) {
+               parameters.add(parameter);
+       }
+
+       Object next() {
+               return parameters.get(index++);
+       }
+
+       @Override
+       public boolean render(InternalContextAdapter context, Writer writer) 
throws IOException, MethodInvocationException,
+                       ParseErrorException, ResourceNotFoundException {
+
+               // rewind the list regardless of whether we produce any output
+               Object next = next();
+
+               if (context.getAllowRendering()) {
+                       writer.write(String.valueOf(next));
+               }
+               return true;
+       }
+
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/cayenne/blob/14e9dc3b/cayenne-server/src/main/java/org/apache/cayenne/velocity/VelocitySQLTemplateProcessor.java
----------------------------------------------------------------------
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/velocity/VelocitySQLTemplateProcessor.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/velocity/VelocitySQLTemplateProcessor.java
index 89e7952..be5641d 100644
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/velocity/VelocitySQLTemplateProcessor.java
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/velocity/VelocitySQLTemplateProcessor.java
@@ -31,13 +31,16 @@ import org.apache.cayenne.access.jdbc.ColumnDescriptor;
 import org.apache.cayenne.access.jdbc.ParameterBinding;
 import org.apache.cayenne.access.jdbc.SQLStatement;
 import org.apache.cayenne.access.jdbc.SQLTemplateProcessor;
+import org.apache.cayenne.exp.ExpressionException;
 import org.apache.velocity.VelocityContext;
 import org.apache.velocity.context.InternalContextAdapterImpl;
 import org.apache.velocity.runtime.RuntimeConstants;
 import org.apache.velocity.runtime.RuntimeInstance;
 import org.apache.velocity.runtime.log.NullLogChute;
 import org.apache.velocity.runtime.parser.ParseException;
+import org.apache.velocity.runtime.parser.node.ASTReference;
 import org.apache.velocity.runtime.parser.node.SimpleNode;
+import org.apache.velocity.runtime.visitor.BaseVisitor;
 
 /**
  * Processor for SQL velocity templates.
@@ -47,6 +50,45 @@ import org.apache.velocity.runtime.parser.node.SimpleNode;
  */
 public class VelocitySQLTemplateProcessor implements SQLTemplateProcessor {
 
+       private final class PositionalParamMapper extends BaseVisitor {
+
+               private int i;
+               private List<Object> positionalParams;
+               private Map<String, Object> params;
+
+               PositionalParamMapper(List<Object> positionalParams, 
Map<String, Object> params) {
+                       this.positionalParams = positionalParams;
+                       this.params = params;
+               }
+
+               @Override
+               public Object visit(ASTReference node, Object data) {
+
+                       if (i >= positionalParams.size()) {
+                               throw new ExpressionException("Too few 
parameters to bind template: " + positionalParams.size());
+                       }
+
+                       // strip off leading "$"
+                       String paramName = 
node.getFirstToken().image.substring(1);
+                       VelocityParamSequence sequence = 
(VelocityParamSequence) params.get(paramName);
+                       if (sequence == null) {
+                               sequence = new VelocityParamSequence();
+                               params.put(paramName, sequence);
+                       }
+
+                       sequence.add(positionalParams.get(i++));
+
+                       return data;
+               }
+
+               void onFinish() {
+                       if (i < positionalParams.size()) {
+                               throw new ExpressionException("Too many 
parameters to bind template. Expected: " + i + ", actual: "
+                                               + positionalParams.size());
+                       }
+               }
+       }
+
        static final String BINDINGS_LIST_KEY = "bindings";
        static final String RESULT_COLUMNS_LIST_KEY = "resultColumns";
        static final String HELPER_KEY = "helper";
@@ -91,15 +133,34 @@ public class VelocitySQLTemplateProcessor implements 
SQLTemplateProcessor {
                Map<String, Object> internalParameters = (parameters != null && 
!parameters.isEmpty()) ? new HashMap<String, Object>(
                                parameters) : new HashMap<String, Object>(5);
 
+               SimpleNode parsedTemplate = parse(template);
+               return processTemplate(template, parsedTemplate, 
internalParameters);
+       }
+
+       @Override
+       public SQLStatement processTemplate(String template, List<Object> 
positionalParameters) {
+
+               SimpleNode parsedTemplate = parse(template);
+
+               Map<String, Object> internalParameters = new HashMap<String, 
Object>();
+
+               PositionalParamMapper visitor = new 
PositionalParamMapper(positionalParameters, internalParameters);
+               parsedTemplate.jjtAccept(visitor, null);
+               visitor.onFinish();
+
+               return processTemplate(template, parsedTemplate, 
internalParameters);
+       }
+
+       SQLStatement processTemplate(String template, SimpleNode 
parsedTemplate, Map<String, Object> parameters) {
                List<ParameterBinding> bindings = new 
ArrayList<ParameterBinding>();
                List<ColumnDescriptor> results = new 
ArrayList<ColumnDescriptor>();
-               internalParameters.put(BINDINGS_LIST_KEY, bindings);
-               internalParameters.put(RESULT_COLUMNS_LIST_KEY, results);
-               internalParameters.put(HELPER_KEY, renderingUtils);
+               parameters.put(BINDINGS_LIST_KEY, bindings);
+               parameters.put(RESULT_COLUMNS_LIST_KEY, results);
+               parameters.put(HELPER_KEY, renderingUtils);
 
                String sql;
                try {
-                       sql = buildStatement(new 
VelocityContext(internalParameters), template);
+                       sql = buildStatement(new VelocityContext(parameters), 
template, parsedTemplate);
                } catch (Exception e) {
                        throw new CayenneRuntimeException("Error processing 
Velocity template", e);
                }
@@ -113,12 +174,24 @@ public class VelocitySQLTemplateProcessor implements 
SQLTemplateProcessor {
                return new SQLStatement(sql, resultsArray, bindingsArray);
        }
 
-       String buildStatement(VelocityContext context, String template) throws 
Exception {
-               // Note: this method is a reworked version of
-               // org.apache.velocity.app.Velocity.evaluate(..)
-               // cleaned up to avoid using any Velocity singletons
+       String buildStatement(VelocityContext context, String template, 
SimpleNode parsedTemplate) throws Exception {
+
+               // ... not sure what InternalContextAdapter is for...
+               InternalContextAdapterImpl ica = new 
InternalContextAdapterImpl(context);
+               ica.pushCurrentTemplateName(template);
 
                StringWriter out = new StringWriter(template.length());
+               try {
+                       parsedTemplate.init(ica, velocityRuntime);
+                       parsedTemplate.render(ica, out);
+                       return out.toString();
+               } finally {
+                       ica.popCurrentTemplateName();
+               }
+       }
+
+       private SimpleNode parse(String template) {
+
                SimpleNode nodeTree = null;
 
                try {
@@ -131,16 +204,6 @@ public class VelocitySQLTemplateProcessor implements 
SQLTemplateProcessor {
                        throw new CayenneRuntimeException("Error parsing 
template " + template);
                }
 
-               // ... not sure what InternalContextAdapter is for...
-               InternalContextAdapterImpl ica = new 
InternalContextAdapterImpl(context);
-               ica.pushCurrentTemplateName(template);
-
-               try {
-                       nodeTree.init(ica, velocityRuntime);
-                       nodeTree.render(ica, out);
-                       return out.toString();
-               } finally {
-                       ica.popCurrentTemplateName();
-               }
+               return nodeTree;
        }
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/14e9dc3b/cayenne-server/src/test/java/org/apache/cayenne/query/SQLTemplateIT.java
----------------------------------------------------------------------
diff --git 
a/cayenne-server/src/test/java/org/apache/cayenne/query/SQLTemplateIT.java 
b/cayenne-server/src/test/java/org/apache/cayenne/query/SQLTemplateIT.java
index 3d2b989..5696eba 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/query/SQLTemplateIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/query/SQLTemplateIT.java
@@ -19,6 +19,8 @@
 
 package org.apache.cayenne.query;
 
+import java.sql.SQLException;
+import java.sql.Types;
 import java.util.List;
 
 import org.apache.cayenne.CayenneRuntimeException;
@@ -27,9 +29,10 @@ import org.apache.cayenne.access.DataContext;
 import org.apache.cayenne.di.Inject;
 import org.apache.cayenne.map.DataMap;
 import org.apache.cayenne.test.jdbc.DBHelper;
+import org.apache.cayenne.test.jdbc.TableHelper;
+import org.apache.cayenne.testdo.testmap.Painting;
 import org.apache.cayenne.unit.di.server.ServerCase;
 import org.apache.cayenne.unit.di.server.UseServerRuntime;
-import org.apache.cayenne.util.Util;
 
 @UseServerRuntime(ServerCase.TESTMAP_PROJECT)
 public class SQLTemplateIT extends ServerCase {
@@ -40,6 +43,8 @@ public class SQLTemplateIT extends ServerCase {
        @Inject
        private DBHelper dbHelper;
 
+       private TableHelper tPainting;
+
        @Override
        protected void setUpAfterInjection() throws Exception {
                dbHelper.deleteAll("PAINTING_INFO");
@@ -47,6 +52,10 @@ public class SQLTemplateIT extends ServerCase {
                dbHelper.deleteAll("ARTIST_EXHIBIT");
                dbHelper.deleteAll("ARTIST_GROUP");
                dbHelper.deleteAll("ARTIST");
+
+               tPainting = new TableHelper(dbHelper, "PAINTING");
+               tPainting.setColumns("PAINTING_ID", "ARTIST_ID", 
"PAINTING_TITLE", "ESTIMATED_PRICE").setColumnTypes(
+                               Types.INTEGER, Types.BIGINT, Types.VARCHAR, 
Types.DECIMAL);
        }
 
        public void testSQLTemplateForDataMap() {
@@ -83,4 +92,64 @@ public class SQLTemplateIT extends ServerCase {
                assertTrue("If fetchingDataRows is false and ObjectEntity not 
set, shoulb be thrown exception",
                                gotRuntimeException);
        }
+
+       public void testSQLTemplate_PositionalParams() throws SQLException {
+
+               String sql = "INSERT INTO PAINTING (PAINTING_ID, 
PAINTING_TITLE, ESTIMATED_PRICE) "
+                               + "VALUES ($b, '$n', #bind($c 'INTEGER'))";
+
+               SQLTemplate q1 = new SQLTemplate(Painting.class, sql);
+               q1.setParamsArray(76, "The Fiddler", 10005);
+               context.performNonSelectingQuery(q1);
+
+               assertEquals("The Fiddler", 
tPainting.getString("PAINTING_TITLE"));
+               assertEquals(76, tPainting.getInt("PAINTING_ID"));
+               assertEquals(10005.d, tPainting.getDouble("ESTIMATED_PRICE"), 
0.001);
+       }
+
+       public void testSQLTemplate_PositionalParams_RepeatingVars() throws 
SQLException {
+
+               String sql = "INSERT INTO PAINTING (PAINTING_ID, 
PAINTING_TITLE, ESTIMATED_PRICE) "
+                               + "VALUES ($b, '$n', #bind($b 'INTEGER'))";
+
+               SQLTemplate q1 = new SQLTemplate(Painting.class, sql);
+               q1.setParamsArray(11, "The Fiddler", 4567);
+               context.performNonSelectingQuery(q1);
+
+               assertEquals("The Fiddler", 
tPainting.getString("PAINTING_TITLE"));
+               assertEquals(11, tPainting.getInt("PAINTING_ID"));
+               assertEquals(4567.d, tPainting.getDouble("ESTIMATED_PRICE"), 
0.001);
+       }
+
+       public void testSQLTemplate_PositionalParams_ToFewParams() throws 
SQLException {
+
+               String sql = "INSERT INTO PAINTING (PAINTING_ID, 
PAINTING_TITLE, ESTIMATED_PRICE) "
+                               + "VALUES ($b, '$n', #bind($b 'INTEGER'))";
+
+               SQLTemplate q1 = new SQLTemplate(Painting.class, sql);
+               q1.setParamsArray(11, "The Fiddler");
+
+               try {
+                       context.performNonSelectingQuery(q1);
+                       fail("Exception not thrown on parameter length 
mismatch");
+               } catch (CayenneRuntimeException e) {
+                       // expected
+               }
+       }
+
+       public void testSQLTemplate_PositionalParams_ToManyParams() throws 
SQLException {
+
+               String sql = "INSERT INTO PAINTING (PAINTING_ID, 
PAINTING_TITLE, ESTIMATED_PRICE) "
+                               + "VALUES ($b, '$n', #bind($b 'INTEGER'))";
+
+               SQLTemplate q1 = new SQLTemplate(Painting.class, sql);
+               q1.setParamsArray(11, "The Fiddler", 2345, 333);
+
+               try {
+                       context.performNonSelectingQuery(q1);
+                       fail("Exception not thrown on parameter length 
mismatch");
+               } catch (CayenneRuntimeException e) {
+                       // expected
+               }
+       }
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/14e9dc3b/cayenne-server/src/test/java/org/apache/cayenne/query/SQLTemplateTest.java
----------------------------------------------------------------------
diff --git 
a/cayenne-server/src/test/java/org/apache/cayenne/query/SQLTemplateTest.java 
b/cayenne-server/src/test/java/org/apache/cayenne/query/SQLTemplateTest.java
index dc5df3f..a88a6c8 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/query/SQLTemplateTest.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/query/SQLTemplateTest.java
@@ -19,12 +19,13 @@
 package org.apache.cayenne.query;
 
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNotSame;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
 
+import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 
@@ -37,7 +38,6 @@ public class SQLTemplateTest {
        public void testSetParams() throws Exception {
                SQLTemplate query = new SQLTemplate();
 
-               assertNotNull(query.getParams());
                assertTrue(query.getParams().isEmpty());
 
                Map<String, Object> params = new HashMap<String, Object>();
@@ -47,11 +47,46 @@ public class SQLTemplateTest {
                assertEquals(params, query.getParams());
 
                query.setParams(null);
-               assertNotNull(query.getParams());
                assertTrue(query.getParams().isEmpty());
        }
 
        @Test
+       public void testSetParamsArray() throws Exception {
+               SQLTemplate query = new SQLTemplate();
+
+               assertTrue(query.getPositionalParams().isEmpty());
+
+               query.setParamsArray("N", "m");
+               assertEquals(Arrays.asList("N", "m"), 
query.getPositionalParams());
+
+               query.setParamsArray();
+               assertTrue(query.getPositionalParams().isEmpty());
+       }
+
+       @Test
+       public void testSetParams_MixingStyles() throws Exception {
+
+               SQLTemplate query = new SQLTemplate();
+
+               assertTrue(query.getParams().isEmpty());
+               assertTrue(query.getPositionalParams().isEmpty());
+
+               Map<String, Object> params = Collections.<String, Object> 
singletonMap("a", "b");
+               query.setParams(params);
+               assertEquals(params, query.getParams());
+               assertTrue(query.getPositionalParams().isEmpty());
+
+               query.setParamsArray("D", "G");
+               assertEquals(Arrays.asList("D", "G"), 
query.getPositionalParams());
+               assertTrue(query.getParams().isEmpty());
+
+               // even resetting named to null should result in resetting 
positional
+               query.setParams(null);
+               assertTrue(query.getParams().isEmpty());
+               assertTrue(query.getPositionalParams().isEmpty());
+       }
+
+       @Test
        public void testGetDefaultTemplate() {
                SQLTemplate query = new SQLTemplate();
                query.setDefaultTemplate("AAA # BBB");

Reply via email to