/*****************************************************************
* 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.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;
}
}