You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cayenne.apache.org by aa...@apache.org on 2014/11/02 21:03:06 UTC
git commit: CAY-1966 SQLTemplate/SQLSelect positional parameter
binding
Repository: cayenne
Updated Branches:
refs/heads/master 2584967e7 -> 14e9dc3bd
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/master
Commit: 14e9dc3bdad37e841cfa6bf00f17f80f6eab86b1
Parents: 2584967
Author: aadamchik <aa...@apache.org>
Authored: Sun Nov 2 22:59:04 2014 +0300
Committer: aadamchik <aa...@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 > #bind($xyz)"</code> produces
- * <code>"WHERE SOME_COLUMN > ?"</code> and also places the value of the "xyz" parameter
- * in the context "bindings" collection.
+ * <code>"WHERE SOME_COLUMN > ?"</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");