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 > #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");