http://git-wip-us.apache.org/repos/asf/cayenne/blob/f734851f/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/BindNotEqualDirective.java ---------------------------------------------------------------------- diff --git a/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/BindNotEqualDirective.java b/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/BindNotEqualDirective.java new file mode 100644 index 0000000..af548da --- /dev/null +++ b/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/BindNotEqualDirective.java @@ -0,0 +1,56 @@ +/***************************************************************** + * 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 org.apache.cayenne.access.translator.ParameterBinding; +import org.apache.velocity.context.InternalContextAdapter; + +/** + * A custom Velocity directive to create a PreparedStatement parameter text for "<>?". + * If null value is encountered, generated text will look like "IS NOT NULL". Usage in + * Velocity template is "WHERE SOME_COLUMN #bindNotEqual($xyz)". + * + * @since 1.1 + */ +public class BindNotEqualDirective extends BindDirective { + + @Override + public String getName() { + return "bindNotEqual"; + } + + @Override + protected void render( + InternalContextAdapter context, + Writer writer, + ParameterBinding binding) throws IOException { + + if (binding.getValue() != null) { + bind(context, binding); + writer.write("<> ?"); + } + else { + writer.write("IS NOT NULL"); + } + } +}
http://git-wip-us.apache.org/repos/asf/cayenne/blob/f734851f/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/BindObjectEqualDirective.java ---------------------------------------------------------------------- diff --git a/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/BindObjectEqualDirective.java b/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/BindObjectEqualDirective.java new file mode 100644 index 0000000..8394673 --- /dev/null +++ b/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/BindObjectEqualDirective.java @@ -0,0 +1,164 @@ +/***************************************************************** + * 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.sql.Types; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; + +import org.apache.cayenne.ObjectId; +import org.apache.cayenne.Persistent; +import org.apache.cayenne.access.translator.ParameterBinding; +import org.apache.cayenne.dba.TypesMapping; +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.parser.node.Node; + +/** + * A custom Velocity directive to create a set of SQL conditions to match an ObjectId of + * an object. Usage in Velocity template is "WHERE #bindObjectEqual($object)" or "WHERE + * #bindObjectEqual($object $columns $idValues)". + * + * @since 3.0 + */ +public class BindObjectEqualDirective extends BindDirective { + + @Override + public String getName() { + return "bindObjectEqual"; + } + + @Override + public boolean render(InternalContextAdapter context, Writer writer, Node node) + throws IOException, ResourceNotFoundException, ParseErrorException, + MethodInvocationException { + + Object object = getChild(context, node, 0); + Map idMap = toIdMap(object); + + Object sqlColumns = getChild(context, node, 1); + Object idColumns = getChild(context, node, 2); + + if (idMap == null) { + // assume null object, and bind all null values + + if (sqlColumns == null || idColumns == null) { + throw new ParseErrorException("Invalid parameters. " + + "Either object has to be set " + + "or sqlColumns and idColumns or both."); + } + + idMap = Collections.EMPTY_MAP; + } + else if (sqlColumns == null || idColumns == null) { + // infer SQL columns from ID columns + sqlColumns = idMap.keySet().toArray(); + idColumns = sqlColumns; + } + + Object[] sqlColumnsArray = toArray(sqlColumns); + Object[] idColumnsArray = toArray(idColumns); + + if (sqlColumnsArray.length != idColumnsArray.length) { + throw new ParseErrorException( + "SQL columns and ID columns arrays have different sizes."); + } + + for (int i = 0; i < sqlColumnsArray.length; i++) { + + Object value = idMap.get(idColumnsArray[i]); + + int jdbcType = (value != null) ? TypesMapping.getSqlTypeByJava(value + .getClass()) : Types.INTEGER; + + renderColumn(context, writer, sqlColumnsArray[i], i); + writer.write(' '); + render(context, writer, new ParameterBinding(value, jdbcType, -1)); + } + + return true; + } + + protected Object[] toArray(Object columns) { + if (columns instanceof Collection) { + return ((Collection) columns).toArray(); + } + else if (columns.getClass().isArray()) { + return (Object[]) columns; + } + else { + return new Object[] { + columns + }; + } + } + + protected Map toIdMap(Object object) throws ParseErrorException { + if (object instanceof Persistent) { + return ((Persistent) object).getObjectId().getIdSnapshot(); + } + else if (object instanceof ObjectId) { + return ((ObjectId) object).getIdSnapshot(); + } + else if(object instanceof Map) { + return (Map) object; + } + else if (object != null) { + throw new ParseErrorException( + "Invalid object parameter, expected Persistent or ObjectId or null: " + + object); + } + else { + return null; + } + } + + protected void renderColumn( + InternalContextAdapter context, + Writer writer, + Object columnName, + int columnIndex) throws IOException { + + if (columnIndex > 0) { + writer.write(" AND "); + } + + writer.write(columnName.toString()); + } + + @Override + protected void render( + InternalContextAdapter context, + Writer writer, + ParameterBinding binding) throws IOException { + + if (binding.getValue() != null) { + bind(context, binding); + writer.write("= ?"); + } + else { + writer.write("IS NULL"); + } + } +} http://git-wip-us.apache.org/repos/asf/cayenne/blob/f734851f/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/BindObjectNotEqualDirective.java ---------------------------------------------------------------------- diff --git a/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/BindObjectNotEqualDirective.java b/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/BindObjectNotEqualDirective.java new file mode 100644 index 0000000..6c95852 --- /dev/null +++ b/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/BindObjectNotEqualDirective.java @@ -0,0 +1,70 @@ +/***************************************************************** + * 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 org.apache.cayenne.access.translator.ParameterBinding; +import org.apache.velocity.context.InternalContextAdapter; + +/** + * A custom Velocity directive to create a set of SQL conditions to check unequality of an + * ObjectId of an object. Usage in Velocity template is "WHERE + * #bindObjectNotEqual($object)" or "WHERE #bindObjectNotEqual($object $columns + * $idValues)". + * + * @since 3.0 + */ +public class BindObjectNotEqualDirective extends BindObjectEqualDirective { + + @Override + public String getName() { + return "bindObjectNotEqual"; + } + + @Override + protected void renderColumn( + InternalContextAdapter context, + Writer writer, + Object columnName, + int columnIndex) throws IOException { + + if (columnIndex > 0) { + writer.write(" OR "); + } + + writer.write(columnName.toString()); + } + + @Override + protected void render( + InternalContextAdapter context, + Writer writer, + ParameterBinding binding) throws IOException { + + if (binding.getValue() != null) { + bind(context, binding); + writer.write("<> ?"); + } + else { + writer.write("IS NOT NULL"); + } + } +} http://git-wip-us.apache.org/repos/asf/cayenne/blob/f734851f/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/ChainDirective.java ---------------------------------------------------------------------- diff --git a/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/ChainDirective.java b/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/ChainDirective.java new file mode 100644 index 0000000..acd0f8c --- /dev/null +++ b/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/ChainDirective.java @@ -0,0 +1,112 @@ +/***************************************************************** + * 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.StringWriter; +import java.io.Writer; + +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.directive.Directive; +import org.apache.velocity.runtime.parser.node.ASTDirective; +import org.apache.velocity.runtime.parser.node.Node; + +/** + * A custom Velocity directive to conditionally join a number of {@link ChunkDirective chunks}. + * Usage of chain is the following: + * + * <pre> + * #chain(operator) - e.g. #chain(' AND ') + * #chain(operator prefix) - e.g. #chain(' AND ' 'WHERE ')</pre> + * + * <p><code>operator</code> (e.g. AND, OR, etc.) is used to join chunks that are included + * in a chain. <code>prefix</code> is inserted if a chain contains at least one chunk. + * </p> + * + * @since 1.1 + */ +public class ChainDirective extends Directive { + + @Override + public String getName() { + return "chain"; + } + + @Override + public int getType() { + return BLOCK; + } + + @Override + public boolean render(InternalContextAdapter context, Writer writer, Node node) + throws + IOException, + ResourceNotFoundException, + ParseErrorException, + MethodInvocationException { + + int size = node.jjtGetNumChildren(); + if (size == 0) { + return true; + } + + // BLOCK is the last child + Node block = node.jjtGetChild(node.jjtGetNumChildren() - 1); + String join = (size > 1) ? (String) node.jjtGetChild(0).value(context) : ""; + String prefix = (size > 2) ? (String) node.jjtGetChild(1).value(context) : ""; + + // if there is a conditional prefix, use a separate buffer for children + StringWriter childWriter = new StringWriter(30); + + int len = block.jjtGetNumChildren(); + int includedChunks = 0; + for (int i = 0; i < len; i++) { + Node child = block.jjtGetChild(i); + + // if this is a "chunk", evaluate its expression and prepend join if included... + if (child instanceof ASTDirective + && "chunk".equals(((ASTDirective) child).getDirectiveName())) { + + if (child.jjtGetNumChildren() < 2 + || child.jjtGetChild(0).value(context) != null) { + + if (includedChunks > 0) { + childWriter.write(join); + } + + includedChunks++; + } + } + + child.render(context, childWriter); + } + + if (includedChunks > 0) { + childWriter.flush(); + writer.write(prefix); + writer.write(childWriter.toString()); + } + + return true; + } +} http://git-wip-us.apache.org/repos/asf/cayenne/blob/f734851f/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/ChunkDirective.java ---------------------------------------------------------------------- diff --git a/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/ChunkDirective.java b/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/ChunkDirective.java new file mode 100644 index 0000000..5ff0a5e --- /dev/null +++ b/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/ChunkDirective.java @@ -0,0 +1,75 @@ +/***************************************************************** + * 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 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.directive.Directive; +import org.apache.velocity.runtime.parser.node.Node; + +/** + * A custom Velocity directive to describe a conditional chunk of a {@link ChainDirective chain}. + * Usage of chunk is the following: + * + * <pre> + * #chunk()...#end - e.g. #chunk()A = 5#end + * #chunk($paramKey)...#end - e.g. #chunk($a)A = $a#end + * </pre> + * <p> + * If context contains paramKey and it's value isn't null, chunk is included in the + * chain, and if it is not the first chunk, it is prefixed with chain join (OR/AND). + * If context doesn't contain paramKey or it's value is null, chunk is skipped. + * @since 1.1 + */ +public class ChunkDirective extends Directive { + + @Override + public String getName() { + return "chunk"; + } + + @Override + public int getType() { + return BLOCK; + } + + @Override + public boolean render(InternalContextAdapter context, Writer writer, Node node) + throws IOException, ResourceNotFoundException, ParseErrorException, + MethodInvocationException { + + // first child is an expression, second is BLOCK + if (node.jjtGetNumChildren() > 1 && node.jjtGetChild(0).value(context) == null) { + // skip this chunk + return false; + } + + // BLOCK is the last child + Node block = node.jjtGetChild(node.jjtGetNumChildren() - 1); + block.render(context, writer); + return true; + } + +} http://git-wip-us.apache.org/repos/asf/cayenne/blob/f734851f/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/ResultDirective.java ---------------------------------------------------------------------- diff --git a/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/ResultDirective.java b/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/ResultDirective.java new file mode 100644 index 0000000..5973188 --- /dev/null +++ b/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/ResultDirective.java @@ -0,0 +1,202 @@ +/***************************************************************** + * 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.math.BigDecimal; +import java.math.BigInteger; +import java.sql.Date; +import java.sql.Time; +import java.sql.Timestamp; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import org.apache.cayenne.access.jdbc.ColumnDescriptor; +import org.apache.cayenne.util.Util; +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.directive.Directive; +import org.apache.velocity.runtime.parser.node.Node; + +/** + * A custom Velocity directive to describe a ResultSet column. There are the + * following possible invocation formats inside the template: + * + * <pre> + * #result(column_name) - e.g. #result('ARTIST_ID') + * #result(column_name java_type) - e.g. #result('ARTIST_ID' 'String') + * #result(column_name java_type column_alias) - e.g. #result('ARTIST_ID' 'String' 'ID') + * #result(column_name java_type column_alias data_row_key) - e.g. #result('ARTIST_ID' 'String' 'ID' 'toArtist.ID') + * </pre> + * + * <p> + * 'data_row_key' is needed if SQL 'column_alias' is not appropriate as a + * DataRow key on the Cayenne side. One common case when this happens is when a + * DataRow retrieved from a query is mapped using joint prefetch keys. In this + * case DataRow must use DB_PATH expressions for joint column keys, and their + * format is incompatible with most databases alias format. + * </p> + * <p> + * Most common Java types used in JDBC can be specified without a package. This + * includes all numeric types, primitives, String, SQL dates, BigDecimal and + * BigInteger. + * </p> + * + * @since 1.1 + */ +public class ResultDirective extends Directive { + + private static final Map<String, String> typesGuess; + + static { + // init default types + typesGuess = new HashMap<>(); + + // primitives + typesGuess.put("long", Long.class.getName()); + typesGuess.put("double", Double.class.getName()); + typesGuess.put("byte", Byte.class.getName()); + typesGuess.put("boolean", Boolean.class.getName()); + typesGuess.put("float", Float.class.getName()); + typesGuess.put("short", Short.class.getName()); + typesGuess.put("int", Integer.class.getName()); + + // numeric + typesGuess.put("Long", Long.class.getName()); + typesGuess.put("Double", Double.class.getName()); + typesGuess.put("Byte", Byte.class.getName()); + typesGuess.put("Boolean", Boolean.class.getName()); + typesGuess.put("Float", Float.class.getName()); + typesGuess.put("Short", Short.class.getName()); + typesGuess.put("Integer", Integer.class.getName()); + + // other + typesGuess.put("String", String.class.getName()); + typesGuess.put("Date", Date.class.getName()); + typesGuess.put("Time", Time.class.getName()); + typesGuess.put("Timestamp", Timestamp.class.getName()); + typesGuess.put("BigDecimal", BigDecimal.class.getName()); + typesGuess.put("BigInteger", BigInteger.class.getName()); + } + + @Override + public String getName() { + return "result"; + } + + @Override + public int getType() { + return LINE; + } + + @Override + public boolean render(InternalContextAdapter context, Writer writer, Node node) throws IOException, + ResourceNotFoundException, ParseErrorException, MethodInvocationException { + + String column = getChildAsString(context, node, 0); + if (column == null) { + throw new ParseErrorException("Column name expected at line " + node.getLine() + ", column " + + node.getColumn()); + } + + String alias = getChildAsString(context, node, 2); + String dataRowKey = getChildAsString(context, node, 3); + + // determine what we want to name this column in a resulting DataRow... + String label = (!Util.isEmptyString(dataRowKey)) ? dataRowKey : (!Util.isEmptyString(alias)) ? alias : null; + + ColumnDescriptor columnDescriptor = new ColumnDescriptor(); + columnDescriptor.setName(column); + columnDescriptor.setDataRowKey(label); + + String type = getChildAsString(context, node, 1); + if (type != null) { + columnDescriptor.setJavaClass(guessType(type)); + } + + // TODO: andrus 6/27/2007 - this is an unofficial jdbcType parameter + // that is added + // temporarily pending CAY-813 implementation for the sake of EJBQL + // query... + Object jdbcType = getChild(context, node, 4); + if (jdbcType instanceof Number) { + columnDescriptor.setJdbcType(((Number) jdbcType).intValue()); + } + + writer.write(column); + + // append column alias if needed. + + // Note that if table aliases are used, this logic will result in SQL + // like + // "t0.ARTIST_NAME AS ARTIST_NAME". Doing extra regex matching to handle + // this + // won't probably buy us much. + if (!Util.isEmptyString(alias) && !alias.equals(column)) { + writer.write(" AS "); + writer.write(alias); + } + + bindResult(context, columnDescriptor); + return true; + } + + protected Object getChild(InternalContextAdapter context, Node node, int i) throws MethodInvocationException { + return (i >= 0 && i < node.jjtGetNumChildren()) ? node.jjtGetChild(i).value(context) : null; + } + + /** + * Returns a directive argument at a given index converted to String. + * + * @since 1.2 + */ + protected String getChildAsString(InternalContextAdapter context, Node node, int i) + throws MethodInvocationException { + Object value = getChild(context, node, i); + return (value != null) ? value.toString() : null; + } + + /** + * Converts "short" type notation to the fully qualified class name. Right + * now supports all major standard SQL types, including primitives. All + * other types are expected to be fully qualified, and are not converted. + */ + protected String guessType(String type) { + String guessed = typesGuess.get(type); + return guessed != null ? guessed : type; + } + + /** + * Adds value to the list of result columns in the context. + */ + protected void bindResult(InternalContextAdapter context, ColumnDescriptor columnDescriptor) { + + Collection<Object> resultColumns = (Collection<Object>) context.getInternalUserContext().get( + VelocitySQLTemplateProcessor.RESULT_COLUMNS_LIST_KEY); + + if (resultColumns != null) { + resultColumns.add(columnDescriptor); + } + } +} http://git-wip-us.apache.org/repos/asf/cayenne/blob/f734851f/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/SQLTemplateResourceManager.java ---------------------------------------------------------------------- diff --git a/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/SQLTemplateResourceManager.java b/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/SQLTemplateResourceManager.java new file mode 100644 index 0000000..085e2be --- /dev/null +++ b/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/SQLTemplateResourceManager.java @@ -0,0 +1,106 @@ +/***************************************************************** + * 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.ByteArrayInputStream; +import java.io.InputStream; +import java.util.Map; + +import org.apache.commons.collections.ExtendedProperties; +import org.apache.commons.collections.map.LRUMap; +import org.apache.velocity.Template; +import org.apache.velocity.exception.ParseErrorException; +import org.apache.velocity.exception.ResourceNotFoundException; +import org.apache.velocity.runtime.RuntimeServices; +import org.apache.velocity.runtime.resource.Resource; +import org.apache.velocity.runtime.resource.ResourceManager; +import org.apache.velocity.runtime.resource.loader.ResourceLoader; + +/** + * An implementation of the Velocity ResourceManager and ResourceLoader that + * creates templates from in-memory Strings. + * + * @since 1.1 + */ +// class must be public since it is instantiated by Velocity via reflection. +public class SQLTemplateResourceManager + extends ResourceLoader + implements ResourceManager { + + protected Map<String, Template> templateCache; + + public void initialize(RuntimeServices rs) { + super.rsvc = rs; + this.templateCache = new LRUMap(100); + } + + public void clearCache() { + templateCache.clear(); + } + + /** + * Returns a Velocity Resource which is a Template for the given SQL. + */ + public Resource getResource(String resourceName, int resourceType, String encoding) + throws ResourceNotFoundException, ParseErrorException { + + synchronized (templateCache) { + Template resource = templateCache.get(resourceName); + + if (resource == null) { + resource = new Template(); + resource.setRuntimeServices(rsvc); + resource.setResourceLoader(this); + resource.setName(resourceName); + resource.setEncoding(encoding); + resource.process(); + + templateCache.put(resourceName, resource); + } + + return resource; + } + } + + public String getLoaderNameForResource(String resourceName) { + return getClass().getName(); + } + + @Override + public long getLastModified(Resource resource) { + return -1; + } + + @Override + public InputStream getResourceStream(String source) + throws ResourceNotFoundException { + return new ByteArrayInputStream(source.getBytes()); + } + + @Override + public void init(ExtendedProperties configuration) { + + } + + @Override + public boolean isSourceModified(Resource resource) { + return false; + } +} http://git-wip-us.apache.org/repos/asf/cayenne/blob/f734851f/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/VelocityModule.java ---------------------------------------------------------------------- diff --git a/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/VelocityModule.java b/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/VelocityModule.java new file mode 100644 index 0000000..c199bdb --- /dev/null +++ b/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/VelocityModule.java @@ -0,0 +1,35 @@ +/***************************************************************** + * 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 org.apache.cayenne.access.jdbc.SQLTemplateProcessor; +import org.apache.cayenne.di.Binder; +import org.apache.cayenne.di.Module; + +/** + * @since 4.1 + */ +public class VelocityModule implements Module { + + @Override + public void configure(Binder binder) { + binder.bind(SQLTemplateProcessor.class).to(VelocitySQLTemplateProcessor.class); + } +} http://git-wip-us.apache.org/repos/asf/cayenne/blob/f734851f/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/VelocitySQLTemplateProcessor.java ---------------------------------------------------------------------- diff --git a/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/VelocitySQLTemplateProcessor.java b/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/VelocitySQLTemplateProcessor.java new file mode 100644 index 0000000..a39cffb --- /dev/null +++ b/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/VelocitySQLTemplateProcessor.java @@ -0,0 +1,209 @@ +/***************************************************************** + * 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 org.apache.cayenne.CayenneRuntimeException; +import org.apache.cayenne.access.jdbc.ColumnDescriptor; +import org.apache.cayenne.access.jdbc.SQLStatement; +import org.apache.cayenne.access.jdbc.SQLTemplateProcessor; +import org.apache.cayenne.access.translator.ParameterBinding; +import org.apache.cayenne.exp.ExpressionException; +import org.apache.cayenne.template.SQLTemplateRenderingUtils; +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; + +import java.io.StringReader; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Processor for SQL velocity templates. + * + * @see org.apache.cayenne.query.SQLTemplate + * @since 4.0 + */ +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) { + + // strip off leading "$" + String paramName = node.getFirstToken().image.substring(1); + + // only consider the first instance of each named parameter + if (!params.containsKey(paramName)) { + + if (i >= positionalParams.size()) { + throw new ExpressionException("Too few parameters to bind template: " + positionalParams.size()); + } + + params.put(paramName, positionalParams.get(i)); + 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"; + + protected RuntimeInstance velocityRuntime; + protected SQLTemplateRenderingUtils renderingUtils; + + public VelocitySQLTemplateProcessor() { + this.renderingUtils = new SQLTemplateRenderingUtils(); + this.velocityRuntime = new RuntimeInstance(); + + // set null logger + velocityRuntime.addProperty(RuntimeConstants.RUNTIME_LOG_LOGSYSTEM, new NullLogChute()); + + velocityRuntime + .addProperty(RuntimeConstants.RESOURCE_MANAGER_CLASS, SQLTemplateResourceManager.class.getName()); + velocityRuntime.addProperty("userdirective", BindDirective.class.getName()); + velocityRuntime.addProperty("userdirective", BindEqualDirective.class.getName()); + velocityRuntime.addProperty("userdirective", BindNotEqualDirective.class.getName()); + velocityRuntime.addProperty("userdirective", BindObjectEqualDirective.class.getName()); + velocityRuntime.addProperty("userdirective", BindObjectNotEqualDirective.class.getName()); + velocityRuntime.addProperty("userdirective", ResultDirective.class.getName()); + velocityRuntime.addProperty("userdirective", ChainDirective.class.getName()); + velocityRuntime.addProperty("userdirective", ChunkDirective.class.getName()); + try { + velocityRuntime.init(); + } catch (Exception ex) { + throw new CayenneRuntimeException("Error setting up Velocity RuntimeInstance.", ex); + } + + } + + /** + * Builds and returns a SQLStatement based on SQL template and a set of + * parameters. During rendering, VelocityContext exposes the following as + * variables: all parameters in the map, {@link SQLTemplateRenderingUtils} + * as a "helper" variable and SQLStatement object as "statement" variable. + */ + @Override + public SQLStatement processTemplate(String template, Map<String, ?> parameters) { + // have to make a copy of parameter map since we are gonna modify it.. + Map<String, Object> internalParameters = (parameters != null && !parameters.isEmpty()) ? new HashMap<>( + 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<>(); + + 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<>(); + List<ColumnDescriptor> results = new ArrayList<>(); + 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(parameters), template, parsedTemplate); + } catch (Exception e) { + throw new CayenneRuntimeException("Error processing Velocity template", e); + } + + ParameterBinding[] bindingsArray = new ParameterBinding[bindings.size()]; + bindings.toArray(bindingsArray); + + ColumnDescriptor[] resultsArray = new ColumnDescriptor[results.size()]; + results.toArray(resultsArray); + + return new SQLStatement(sql, resultsArray, bindingsArray); + } + + 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; + try { + nodeTree = velocityRuntime.parse(new StringReader(template), template); + } catch (ParseException pex) { + throw new CayenneRuntimeException("Error parsing template '%s' : %s", template, pex.getMessage()); + } + + if (nodeTree == null) { + throw new CayenneRuntimeException("Error parsing template %s", template); + } + + return nodeTree; + } +} http://git-wip-us.apache.org/repos/asf/cayenne/blob/f734851f/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/VelocityServerModuleProvider.java ---------------------------------------------------------------------- diff --git a/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/VelocityServerModuleProvider.java b/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/VelocityServerModuleProvider.java new file mode 100644 index 0000000..b51a5a4 --- /dev/null +++ b/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/VelocityServerModuleProvider.java @@ -0,0 +1,50 @@ +/***************************************************************** + * 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.util.Collection; +import java.util.Collections; + +import org.apache.cayenne.configuration.server.CayenneServerModuleProvider; +import org.apache.cayenne.configuration.server.ServerModule; +import org.apache.cayenne.di.Module; + +/** + * @since 4.1 + */ +public class VelocityServerModuleProvider implements CayenneServerModuleProvider { + + @Override + public Module module() { + return new VelocityModule(); + } + + @Override + public Class<? extends Module> moduleType() { + return VelocityModule.class; + } + + @SuppressWarnings("unchecked") + @Override + public Collection<Class<? extends Module>> overrides() { + Collection modules = Collections.singletonList(ServerModule.class); + return modules; + } +} http://git-wip-us.apache.org/repos/asf/cayenne/blob/f734851f/cayenne-velocity/src/main/resources/META-INF/services/org.apache.cayenne.configuration.server.CayenneServerModuleProvider ---------------------------------------------------------------------- diff --git a/cayenne-velocity/src/main/resources/META-INF/services/org.apache.cayenne.configuration.server.CayenneServerModuleProvider b/cayenne-velocity/src/main/resources/META-INF/services/org.apache.cayenne.configuration.server.CayenneServerModuleProvider new file mode 100644 index 0000000..7228c91 --- /dev/null +++ b/cayenne-velocity/src/main/resources/META-INF/services/org.apache.cayenne.configuration.server.CayenneServerModuleProvider @@ -0,0 +1,20 @@ +################################################################## +# 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. +################################################################## + +org.apache.cayenne.velocity.VelocityServerModuleProvider \ No newline at end of file http://git-wip-us.apache.org/repos/asf/cayenne/blob/f734851f/cayenne-velocity/src/test/java/org/apache/cayenne/velocity/SQLTemplateResourceManagerTest.java ---------------------------------------------------------------------- diff --git a/cayenne-velocity/src/test/java/org/apache/cayenne/velocity/SQLTemplateResourceManagerTest.java b/cayenne-velocity/src/test/java/org/apache/cayenne/velocity/SQLTemplateResourceManagerTest.java new file mode 100644 index 0000000..9ef2229 --- /dev/null +++ b/cayenne-velocity/src/test/java/org/apache/cayenne/velocity/SQLTemplateResourceManagerTest.java @@ -0,0 +1,77 @@ +/***************************************************************** + * 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 static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.Reader; + +import org.apache.velocity.Template; +import org.apache.velocity.runtime.RuntimeConstants; +import org.apache.velocity.runtime.RuntimeServices; +import org.apache.velocity.runtime.parser.node.SimpleNode; +import org.apache.velocity.runtime.resource.Resource; +import org.apache.velocity.runtime.resource.ResourceManager; +import org.junit.Before; +import org.junit.Test; + +public class SQLTemplateResourceManagerTest { + + private SQLTemplateResourceManager rm; + + @Before + public void before() throws Exception { + + RuntimeServices rs = mock(RuntimeServices.class); + when(rs.parse(any(Reader.class), anyString(), anyBoolean())).thenReturn(new SimpleNode(1)); + when(rs.parse(any(Reader.class), anyString())).thenReturn(new SimpleNode(1)); + + this.rm = new SQLTemplateResourceManager(); + rm.initialize(rs); + } + + @Test + public void testFetResource() throws Exception { + + Resource resource = rm.getResource("abc", ResourceManager.RESOURCE_TEMPLATE, RuntimeConstants.ENCODING_DEFAULT); + + assertTrue(resource instanceof Template); + + // must be cached... + assertSame(resource, + rm.getResource("abc", ResourceManager.RESOURCE_TEMPLATE, RuntimeConstants.ENCODING_DEFAULT)); + + // new resource must be different + assertNotSame(resource, + rm.getResource("xyz", ResourceManager.RESOURCE_TEMPLATE, RuntimeConstants.ENCODING_DEFAULT)); + + // after clearing cache, resource must be refreshed + rm.clearCache(); + assertNotSame(resource, + rm.getResource("abc", ResourceManager.RESOURCE_TEMPLATE, RuntimeConstants.ENCODING_DEFAULT)); + } +} http://git-wip-us.apache.org/repos/asf/cayenne/blob/f734851f/cayenne-velocity/src/test/java/org/apache/cayenne/velocity/VelocitySQLTemplateProcessorTest.java ---------------------------------------------------------------------- diff --git a/cayenne-velocity/src/test/java/org/apache/cayenne/velocity/VelocitySQLTemplateProcessorTest.java b/cayenne-velocity/src/test/java/org/apache/cayenne/velocity/VelocitySQLTemplateProcessorTest.java new file mode 100644 index 0000000..504179b --- /dev/null +++ b/cayenne-velocity/src/test/java/org/apache/cayenne/velocity/VelocitySQLTemplateProcessorTest.java @@ -0,0 +1,234 @@ +/***************************************************************** + * 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.sql.Types; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.apache.cayenne.CayenneDataObject; +import org.apache.cayenne.DataObject; +import org.apache.cayenne.ObjectId; +import org.apache.cayenne.access.jdbc.SQLStatement; +import org.apache.cayenne.access.translator.ParameterBinding; +import org.junit.Before; +import org.junit.Test; + +public class VelocitySQLTemplateProcessorTest { + + private VelocitySQLTemplateProcessor processor; + + @Before + public void before() { + processor = new VelocitySQLTemplateProcessor(); + } + + @Test + public void testProcessTemplateUnchanged1() throws Exception { + String sqlTemplate = "SELECT * FROM ME"; + + SQLStatement compiled = processor.processTemplate(sqlTemplate, Collections.<String, Object> emptyMap()); + + assertEquals(sqlTemplate, compiled.getSql()); + assertEquals(0, compiled.getBindings().length); + } + + @Test + public void testProcessTemplateUnchanged2() throws Exception { + String sqlTemplate = "SELECT a.b as XYZ FROM $SYSTEM_TABLE"; + + SQLStatement compiled = processor.processTemplate(sqlTemplate, Collections.<String, Object> emptyMap()); + + assertEquals(sqlTemplate, compiled.getSql()); + assertEquals(0, compiled.getBindings().length); + } + + @Test + public void testProcessTemplateSimpleDynamicContent() throws Exception { + String sqlTemplate = "SELECT * FROM ME WHERE $a"; + + Map<String, Object> map = Collections.<String, Object> singletonMap("a", "VALUE_OF_A"); + SQLStatement compiled = processor.processTemplate(sqlTemplate, map); + + assertEquals("SELECT * FROM ME WHERE VALUE_OF_A", compiled.getSql()); + + // bindings are not populated, since no "bind" macro is used. + assertEquals(0, compiled.getBindings().length); + } + + @Test + public void testProcessTemplateBind() throws Exception { + String sqlTemplate = "SELECT * FROM ME WHERE " + + "COLUMN1 = #bind($a 'VARCHAR') AND COLUMN2 = #bind($b 'INTEGER')"; + Map<String, Object> map = Collections.<String, Object> singletonMap("a", "VALUE_OF_A"); + SQLStatement compiled = processor.processTemplate(sqlTemplate, map); + + assertEquals("SELECT * FROM ME WHERE COLUMN1 = ? AND COLUMN2 = ?", compiled.getSql()); + assertEquals(2, compiled.getBindings().length); + assertBindingValue("VALUE_OF_A", compiled.getBindings()[0]); + assertBindingValue(null, compiled.getBindings()[1]); + } + + @Test + public void testProcessTemplateBindGuessVarchar() throws Exception { + String sqlTemplate = "SELECT * FROM ME WHERE COLUMN1 = #bind($a)"; + Map<String, Object> map = Collections.<String, Object> singletonMap("a", "VALUE_OF_A"); + + SQLStatement compiled = processor.processTemplate(sqlTemplate, map); + + assertEquals(1, compiled.getBindings().length); + assertBindingType(Types.VARCHAR, compiled.getBindings()[0]); + } + + @Test + public void testProcessTemplateBindGuessInteger() throws Exception { + String sqlTemplate = "SELECT * FROM ME WHERE COLUMN1 = #bind($a)"; + Map<String, Object> map = Collections.<String, Object> singletonMap("a", 4); + + SQLStatement compiled = processor.processTemplate(sqlTemplate, map); + + assertEquals(1, compiled.getBindings().length); + assertBindingType(Types.INTEGER, compiled.getBindings()[0]); + } + + @Test + public void testProcessTemplateBindEqual() throws Exception { + String sqlTemplate = "SELECT * FROM ME WHERE COLUMN #bindEqual($a 'VARCHAR')"; + + SQLStatement compiled = processor.processTemplate(sqlTemplate, Collections.<String, Object> emptyMap()); + + assertEquals("SELECT * FROM ME WHERE COLUMN IS NULL", compiled.getSql()); + assertEquals(0, compiled.getBindings().length); + + Map<String, Object> map = Collections.<String, Object> singletonMap("a", "VALUE_OF_A"); + + compiled = processor.processTemplate(sqlTemplate, map); + + assertEquals("SELECT * FROM ME WHERE COLUMN = ?", compiled.getSql()); + assertEquals(1, compiled.getBindings().length); + assertBindingValue("VALUE_OF_A", compiled.getBindings()[0]); + } + + @Test + public void testProcessTemplateBindNotEqual() throws Exception { + String sqlTemplate = "SELECT * FROM ME WHERE COLUMN #bindNotEqual($a 'VARCHAR')"; + + SQLStatement compiled = processor.processTemplate(sqlTemplate, Collections.<String, Object> emptyMap()); + + assertEquals("SELECT * FROM ME WHERE COLUMN IS NOT NULL", compiled.getSql()); + assertEquals(0, compiled.getBindings().length); + + Map<String, Object> map = Collections.<String, Object> singletonMap("a", "VALUE_OF_A"); + + compiled = processor.processTemplate(sqlTemplate, map); + + assertEquals("SELECT * FROM ME WHERE COLUMN <> ?", compiled.getSql()); + assertEquals(1, compiled.getBindings().length); + assertBindingValue("VALUE_OF_A", compiled.getBindings()[0]); + } + + @Test + public void testProcessTemplateID() throws Exception { + String sqlTemplate = "SELECT * FROM ME WHERE COLUMN1 = #bind($helper.cayenneExp($a, 'db:ID_COLUMN'))"; + + DataObject dataObject = new CayenneDataObject(); + dataObject.setObjectId(new ObjectId("T", "ID_COLUMN", 5)); + + Map<String, Object> map = Collections.<String, Object> singletonMap("a", dataObject); + + SQLStatement compiled = processor.processTemplate(sqlTemplate, map); + + assertEquals("SELECT * FROM ME WHERE COLUMN1 = ?", compiled.getSql()); + assertEquals(1, compiled.getBindings().length); + assertBindingValue(new Integer(5), compiled.getBindings()[0]); + } + + @Test + public void testProcessTemplateNotEqualID() throws Exception { + String sqlTemplate = "SELECT * FROM ME WHERE " + + "COLUMN1 #bindNotEqual($helper.cayenneExp($a, 'db:ID_COLUMN1')) " + + "AND COLUMN2 #bindNotEqual($helper.cayenneExp($a, 'db:ID_COLUMN2'))"; + + Map<String, Object> idMap = new HashMap<>(); + idMap.put("ID_COLUMN1", new Integer(3)); + idMap.put("ID_COLUMN2", "aaa"); + ObjectId id = new ObjectId("T", idMap); + DataObject dataObject = new CayenneDataObject(); + dataObject.setObjectId(id); + + Map<String, Object> map = Collections.<String, Object> singletonMap("a", dataObject); + + SQLStatement compiled = processor.processTemplate(sqlTemplate, map); + + assertEquals("SELECT * FROM ME WHERE COLUMN1 <> ? AND COLUMN2 <> ?", compiled.getSql()); + assertEquals(2, compiled.getBindings().length); + assertBindingValue(new Integer(3), compiled.getBindings()[0]); + assertBindingValue("aaa", compiled.getBindings()[1]); + } + + @Test + public void testProcessTemplateConditions() throws Exception { + String sqlTemplate = "SELECT * FROM ME #if($a) WHERE COLUMN1 > #bind($a)#end"; + + Map<String, Object> map = Collections.<String, Object> singletonMap("a", "VALUE_OF_A"); + + SQLStatement compiled = processor.processTemplate(sqlTemplate, map); + + assertEquals("SELECT * FROM ME WHERE COLUMN1 > ?", compiled.getSql()); + assertEquals(1, compiled.getBindings().length); + assertBindingValue("VALUE_OF_A", compiled.getBindings()[0]); + + compiled = processor.processTemplate(sqlTemplate, Collections.<String, Object> emptyMap()); + + assertEquals("SELECT * FROM ME ", compiled.getSql()); + assertEquals(0, compiled.getBindings().length); + } + + @Test + public void testProcessTemplateBindCollection() throws Exception { + String sqlTemplate = "SELECT * FROM ME WHERE COLUMN IN (#bind($list 'VARCHAR'))"; + + Map<String, Object> map = Collections.<String, Object> singletonMap("list", Arrays.asList("a", "b", "c")); + SQLStatement compiled = new VelocitySQLTemplateProcessor().processTemplate(sqlTemplate, map); + + assertEquals("SELECT * FROM ME WHERE COLUMN IN (?,?,?)", compiled.getSql()); + assertEquals(3, compiled.getBindings().length); + + compiled = processor.processTemplate(sqlTemplate, map); + assertBindingValue("a", compiled.getBindings()[0]); + assertBindingValue("b", compiled.getBindings()[1]); + assertBindingValue("c", compiled.getBindings()[2]); + } + + private void assertBindingValue(Object expectedValue, Object binding) { + assertTrue("Not a binding!", binding instanceof ParameterBinding); + assertEquals(expectedValue, ((ParameterBinding) binding).getValue()); + } + + private void assertBindingType(Integer expectedType, Object binding) { + assertTrue("Not a binding!", binding instanceof ParameterBinding); + assertEquals(expectedType, ((ParameterBinding) binding).getJdbcType()); + } +} http://git-wip-us.apache.org/repos/asf/cayenne/blob/f734851f/cayenne-velocity/src/test/java/org/apache/cayenne/velocity/VelocitySQLTemplateProcessor_ChainTest.java ---------------------------------------------------------------------- diff --git a/cayenne-velocity/src/test/java/org/apache/cayenne/velocity/VelocitySQLTemplateProcessor_ChainTest.java b/cayenne-velocity/src/test/java/org/apache/cayenne/velocity/VelocitySQLTemplateProcessor_ChainTest.java new file mode 100644 index 0000000..c864918 --- /dev/null +++ b/cayenne-velocity/src/test/java/org/apache/cayenne/velocity/VelocitySQLTemplateProcessor_ChainTest.java @@ -0,0 +1,184 @@ +/***************************************************************** + * 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 static org.junit.Assert.assertEquals; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.apache.cayenne.access.jdbc.SQLStatement; +import org.junit.Before; +import org.junit.Test; + +public class VelocitySQLTemplateProcessor_ChainTest { + + private VelocitySQLTemplateProcessor processor; + + @Before + public void before() { + processor = new VelocitySQLTemplateProcessor(); + } + + @Test + public void testProcessTemplateNoChunks() throws Exception { + // whatever is inside the chain, it should render as empty if there + // is no chunks... + + SQLStatement compiled = processor.processTemplate("#chain(' AND ') #end", + Collections.<String, Object> emptyMap()); + assertEquals("", compiled.getSql()); + + compiled = processor.processTemplate("#chain(' AND ') garbage #end", Collections.<String, Object> emptyMap()); + assertEquals("", compiled.getSql()); + + compiled = processor.processTemplate("#chain(' AND ' 'PREFIX') #end", Collections.<String, Object> emptyMap()); + + assertEquals("", compiled.getSql()); + compiled = processor.processTemplate("#chain(' AND ' 'PREFIX') garbage #end", + Collections.<String, Object> emptyMap()); + + assertEquals("", compiled.getSql()); + } + + @Test + public void testProcessTemplateFullChain() throws Exception { + String template = "#chain(' OR ')" + "#chunk($a)$a#end" + "#chunk($b)$b#end" + "#chunk($c)$c#end" + "#end"; + + Map<String, Object> map = new HashMap<>(); + map.put("a", "[A]"); + map.put("b", "[B]"); + map.put("c", "[C]"); + + SQLStatement compiled = processor.processTemplate(template, map); + assertEquals("[A] OR [B] OR [C]", compiled.getSql()); + } + + @Test + public void testProcessTemplateFullChainAndPrefix() throws Exception { + String template = "#chain(' OR ' 'WHERE ')" + "#chunk($a)$a#end" + "#chunk($b)$b#end" + "#chunk($c)$c#end" + + "#end"; + + Map<String, Object> map = new HashMap<>(); + map.put("a", "[A]"); + map.put("b", "[B]"); + map.put("c", "[C]"); + + SQLStatement compiled = processor.processTemplate(template, map); + assertEquals("WHERE [A] OR [B] OR [C]", compiled.getSql()); + } + + @Test + public void testProcessTemplatePartialChainMiddle() throws Exception { + String template = "#chain(' OR ' 'WHERE ')" + "#chunk($a)$a#end" + "#chunk($b)$b#end" + "#chunk($c)$c#end" + + "#end"; + + Map<String, Object> map = new HashMap<>(); + map.put("a", "[A]"); + map.put("c", "[C]"); + + SQLStatement compiled = processor.processTemplate(template, map); + assertEquals("WHERE [A] OR [C]", compiled.getSql()); + } + + @Test + public void testProcessTemplatePartialChainStart() throws Exception { + String template = "#chain(' OR ' 'WHERE ')" + "#chunk($a)$a#end" + "#chunk($b)$b#end" + "#chunk($c)$c#end" + + "#end"; + + Map<String, Object> map = new HashMap<>(); + map.put("b", "[B]"); + map.put("c", "[C]"); + + SQLStatement compiled = processor.processTemplate(template, map); + assertEquals("WHERE [B] OR [C]", compiled.getSql()); + } + + @Test + public void testProcessTemplatePartialChainEnd() throws Exception { + String template = "#chain(' OR ' 'WHERE ')" + "#chunk($a)$a#end" + "#chunk($b)$b#end" + "#chunk($c)$c#end" + + "#end"; + + Map<String, Object> map = new HashMap<>(); + map.put("a", "[A]"); + map.put("b", "[B]"); + + SQLStatement compiled = processor.processTemplate(template, map); + assertEquals("WHERE [A] OR [B]", compiled.getSql()); + } + + @Test + public void testProcessTemplateChainWithGarbage() throws Exception { + String template = "#chain(' OR ' 'WHERE ')" + "#chunk($a)$a#end" + " some other stuff" + "#chunk($c)$c#end" + + "#end"; + + Map<String, Object> map = new HashMap<>(); + map.put("a", "[A]"); + map.put("c", "[C]"); + + SQLStatement compiled = processor.processTemplate(template, map); + assertEquals("WHERE [A] some other stuff OR [C]", compiled.getSql()); + } + + @Test + public void testProcessTemplateChainUnconditionalChunks() throws Exception { + String template = "#chain(' OR ' 'WHERE ')" + "#chunk()C1#end" + "#chunk()C2#end" + "#chunk()C3#end" + "#end"; + + SQLStatement compiled = processor.processTemplate(template, Collections.<String, Object> emptyMap()); + assertEquals("WHERE C1 OR C2 OR C3", compiled.getSql()); + } + + @Test + public void testProcessTemplateEmptyChain() throws Exception { + String template = "#chain(' OR ' 'WHERE ')" + "#chunk($a)$a#end" + "#chunk($b)$b#end" + "#chunk($c)$c#end" + + "#end"; + + SQLStatement compiled = processor.processTemplate(template, Collections.<String, Object> emptyMap()); + assertEquals("", compiled.getSql()); + } + + @Test + public void testProcessTemplateWithFalseOrZero1() throws Exception { + String template = "#chain(' OR ' 'WHERE ')" + "#chunk($a)[A]#end" + "#chunk($b)[B]#end" + "#chunk($c)$c#end" + + "#end"; + + Map<String, Object> map = new HashMap<>(); + map.put("a", false); + map.put("b", 0); + + SQLStatement compiled = processor.processTemplate(template, map); + assertEquals("WHERE [A] OR [B]", compiled.getSql()); + } + + @Test + public void testProcessTemplateWithFalseOrZero2() throws Exception { + String template = "#chain(' OR ' 'WHERE ')" + "#chunk($a)$a#end" + "#chunk($b)$b#end" + "#chunk($c)$c#end" + + "#end"; + + Map<String, Object> map = new HashMap<>(); + map.put("a", false); + map.put("b", 0); + + SQLStatement compiled = processor.processTemplate(template, map); + assertEquals("WHERE false OR 0", compiled.getSql()); + } + +} http://git-wip-us.apache.org/repos/asf/cayenne/blob/f734851f/cayenne-velocity/src/test/java/org/apache/cayenne/velocity/VelocitySQLTemplateProcessor_SelectTest.java ---------------------------------------------------------------------- diff --git a/cayenne-velocity/src/test/java/org/apache/cayenne/velocity/VelocitySQLTemplateProcessor_SelectTest.java b/cayenne-velocity/src/test/java/org/apache/cayenne/velocity/VelocitySQLTemplateProcessor_SelectTest.java new file mode 100644 index 0000000..74a4d22 --- /dev/null +++ b/cayenne-velocity/src/test/java/org/apache/cayenne/velocity/VelocitySQLTemplateProcessor_SelectTest.java @@ -0,0 +1,109 @@ +/***************************************************************** + * 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import java.util.Collections; + +import org.apache.cayenne.access.jdbc.ColumnDescriptor; +import org.apache.cayenne.access.jdbc.SQLStatement; +import org.junit.Before; +import org.junit.Test; + +public class VelocitySQLTemplateProcessor_SelectTest { + + private VelocitySQLTemplateProcessor processor; + + @Before + public void before() { + processor = new VelocitySQLTemplateProcessor(); + } + + @Test + public void testProcessTemplateUnchanged() throws Exception { + String sqlTemplate = "SELECT * FROM ME"; + + SQLStatement compiled = processor.processTemplate(sqlTemplate, Collections.<String, Object> emptyMap()); + + assertEquals(sqlTemplate, compiled.getSql()); + assertEquals(0, compiled.getBindings().length); + assertEquals(0, compiled.getResultColumns().length); + } + + @Test + public void testProcessSelectTemplate1() throws Exception { + String sqlTemplate = "SELECT #result('A') FROM ME"; + + SQLStatement compiled = processor.processTemplate(sqlTemplate, Collections.<String, Object> emptyMap()); + + assertEquals("SELECT A FROM ME", compiled.getSql()); + assertEquals(0, compiled.getBindings().length); + assertEquals(1, compiled.getResultColumns().length); + assertEquals("A", compiled.getResultColumns()[0].getName()); + assertNull(compiled.getResultColumns()[0].getJavaClass()); + } + + @Test + public void testProcessSelectTemplate2() throws Exception { + String sqlTemplate = "SELECT #result('A' 'String') FROM ME"; + + SQLStatement compiled = processor.processTemplate(sqlTemplate, Collections.<String, Object> emptyMap()); + + assertEquals("SELECT A FROM ME", compiled.getSql()); + assertEquals(0, compiled.getBindings().length); + + assertEquals(1, compiled.getResultColumns().length); + assertEquals("A", compiled.getResultColumns()[0].getName()); + assertEquals("java.lang.String", compiled.getResultColumns()[0].getJavaClass()); + } + + @Test + public void testProcessSelectTemplate3() throws Exception { + String sqlTemplate = "SELECT #result('A' 'String' 'B') FROM ME"; + + SQLStatement compiled = processor.processTemplate(sqlTemplate, Collections.<String, Object> emptyMap()); + + assertEquals("SELECT A AS B FROM ME", compiled.getSql()); + assertEquals(0, compiled.getBindings().length); + + assertEquals(1, compiled.getResultColumns().length); + ColumnDescriptor column = compiled.getResultColumns()[0]; + assertEquals("A", column.getName()); + assertEquals("B", column.getDataRowKey()); + assertEquals("java.lang.String", column.getJavaClass()); + } + + @Test + public void testProcessSelectTemplate4() throws Exception { + String sqlTemplate = "SELECT #result('A'), #result('B'), #result('C') FROM ME"; + + SQLStatement compiled = processor.processTemplate(sqlTemplate, Collections.<String, Object> emptyMap()); + + assertEquals("SELECT A, B, C FROM ME", compiled.getSql()); + assertEquals(0, compiled.getBindings().length); + + assertEquals(3, compiled.getResultColumns().length); + assertEquals("A", compiled.getResultColumns()[0].getName()); + assertEquals("B", compiled.getResultColumns()[1].getName()); + assertEquals("C", compiled.getResultColumns()[2].getName()); + } +} http://git-wip-us.apache.org/repos/asf/cayenne/blob/f734851f/docs/doc/pom.xml ---------------------------------------------------------------------- diff --git a/docs/doc/pom.xml b/docs/doc/pom.xml index 14c569c..d3daa0a 100644 --- a/docs/doc/pom.xml +++ b/docs/doc/pom.xml @@ -46,11 +46,6 @@ <artifactId>cayenne-modeler</artifactId> <version>${project.version}</version> </dependency> - - <dependency> - <groupId>org.apache.velocity</groupId> - <artifactId>velocity</artifactId> - </dependency> <dependency> <groupId>commons-collections</groupId> http://git-wip-us.apache.org/repos/asf/cayenne/blob/f734851f/modeler/cayenne-modeler/pom.xml ---------------------------------------------------------------------- diff --git a/modeler/cayenne-modeler/pom.xml b/modeler/cayenne-modeler/pom.xml index 6f9fc2e..bbc4645 100644 --- a/modeler/cayenne-modeler/pom.xml +++ b/modeler/cayenne-modeler/pom.xml @@ -32,15 +32,6 @@ <packaging>jar</packaging> <dependencies> - <dependency> - <groupId>junit</groupId> - <artifactId>junit</artifactId> - </dependency> - - <dependency> - <groupId>commons-collections</groupId> - <artifactId>commons-collections</artifactId> - </dependency> <dependency> <groupId>org.slf4j</groupId> @@ -48,11 +39,6 @@ </dependency> <dependency> - <groupId>org.apache.velocity</groupId> - <artifactId>velocity</artifactId> - </dependency> - - <dependency> <groupId>com.jgoodies</groupId> <artifactId>forms</artifactId> </dependency> @@ -93,8 +79,14 @@ </dependency> <dependency> + <groupId>jgraph</groupId> + <artifactId>jgraph</artifactId> + </dependency> + + <dependency> <groupId>com.mockrunner</groupId> <artifactId>mockrunner-jdbc</artifactId> + <scope>test</scope> <exclusions> <exclusion> <!-- this one have old Xerces dependency that clashes with JDK's one --> @@ -105,8 +97,9 @@ </dependency> <dependency> - <groupId>jgraph</groupId> - <artifactId>jgraph</artifactId> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <scope>test</scope> </dependency> </dependencies> http://git-wip-us.apache.org/repos/asf/cayenne/blob/f734851f/modeler/cayenne-wocompat/pom.xml ---------------------------------------------------------------------- diff --git a/modeler/cayenne-wocompat/pom.xml b/modeler/cayenne-wocompat/pom.xml index d3544f9..3ad0344 100644 --- a/modeler/cayenne-wocompat/pom.xml +++ b/modeler/cayenne-wocompat/pom.xml @@ -29,36 +29,40 @@ <packaging>jar</packaging> <dependencies> + + <dependency> + <groupId>org.apache.cayenne</groupId> + <artifactId>cayenne-server</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.apache.cayenne</groupId> + <artifactId>cayenne-dbsync</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>commons-lang</groupId> + <artifactId>commons-lang</artifactId> + </dependency> + <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> + <scope>test</scope> </dependency> - <dependency> <groupId>org.apache.cayenne.build-tools</groupId> <artifactId>cayenne-test-utilities</artifactId> <version>${project.version}</version> <scope>test</scope> </dependency> - <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> <scope>test</scope> </dependency> - <dependency> - <groupId>org.apache.cayenne</groupId> - <artifactId>cayenne-server</artifactId> - <version>${project.version}</version> - </dependency> - - <dependency> - <groupId>org.apache.cayenne</groupId> - <artifactId>cayenne-dbsync</artifactId> - <version>${project.version}</version> - </dependency> - </dependencies> + </dependencies> <profiles> <profile> http://git-wip-us.apache.org/repos/asf/cayenne/blob/f734851f/pom.xml ---------------------------------------------------------------------- diff --git a/pom.xml b/pom.xml index 92f39f0..e3be55c 100644 --- a/pom.xml +++ b/pom.xml @@ -68,6 +68,7 @@ <module>cayenne-project</module> <module>cayenne-project-compatibility</module> <module>cayenne-server</module> + <module>cayenne-velocity</module> <module>eventbridges</module> <module>itests</module> <module>maven-plugins</module> @@ -319,7 +320,11 @@ <artifactId>commons-collections</artifactId> <version>3.2.1</version> </dependency> - + <dependency> + <groupId>commons-lang</groupId> + <artifactId>commons-lang</artifactId> + <version>2.4</version> + </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-dbcp2</artifactId>