/*****************************************************************
* 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
* <p/>
* http://www.apache.org/licenses/LICENSE-2.0
* <p/>
* 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.access.translator.procedure;
import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.apache.cayenne.access.translator.ParameterBinding;
import org.apache.cayenne.access.translator.ProcedureParameterBinding;
import org.apache.cayenne.access.types.ExtendedType;
import org.apache.cayenne.dba.DbAdapter;
import org.apache.cayenne.log.JdbcEventLogger;
import org.apache.cayenne.log.NoopJdbcEventLogger;
import org.apache.cayenne.map.EntityResolver;
import org.apache.cayenne.map.Procedure;
import org.apache.cayenne.map.ProcedureParameter;
import org.apache.cayenne.query.ProcedureQuery;
/**
* Stored procedure query translator.
*/
public class ProcedureTranslator {
/**
* Helper class to make OUT and VOID parameters logger-friendly.
*/
static class NotInParam {
protected String type;
public NotInParam(String type) {
this.type = type;
}
@Override
public String toString() {
return type;
}
}
private static NotInParam OUT_PARAM = new NotInParam("[OUT]");
protected ProcedureQuery query;
protected Connection connection;
protected DbAdapter adapter;
protected EntityResolver entityResolver;
protected List<ProcedureParameter> callParams;
protected List<Object> values;
protected JdbcEventLogger logger;
public ProcedureTranslator() {
this.logger = NoopJdbcEventLogger.getInstance();
}
public void setQuery(ProcedureQuery query) {
this.query = query;
}
public void setConnection(Connection connection) {
this.connection = connection;
}
public void setAdapter(DbAdapter adapter) {
this.adapter = adapter;
}
/**
* @since 3.1
*/
public void setJdbcEventLogger(JdbcEventLogger logger) {
this.logger = logger;
}
/**
* @since 3.1
*/
public JdbcEventLogger getJdbcEventLogger() {
return logger;
}
/**
* @since 1.2
*/
public void setEntityResolver(EntityResolver entityResolver) {
this.entityResolver = entityResolver;
}
/**
* Creates an SQL String for the stored procedure call.
*/
protected String createSqlString() {
Procedure procedure = getProcedure();
StringBuilder buf = new StringBuilder();
int totalParams = callParams.size();
// check if procedure returns values
if (procedure.isReturningValue()) {
totalParams--;
buf.append("{? = call ");
} else {
buf.append("{call ");
}
buf.append(procedure.getFullyQualifiedName());
if (totalParams > 0) {
// unroll the loop
buf.append("(?");
for (int i = 1; i < totalParams; i++) {
buf.append(", ?");
}
buf.append(")");
}
buf.append("}");
return buf.toString();
}
/**
* Creates and binds a PreparedStatement to execute query SQL via JDBC.
*/
public PreparedStatement createStatement() throws Exception {
this.callParams = getProcedure().getCallParameters();
this.values = new ArrayList<>(callParams.size());
initValues();
String sqlStr = createSqlString();
if (logger.isLoggable()) {
// need to convert OUT/VOID parameters to loggable strings
ParameterBinding[] parameterBindings = new ParameterBinding[values.size()];
for (int i=0; i<values.size(); i++) {
ProcedureParameter procedureParameter = callParams.get(i);
Object value = values.get(i);
if(value instanceof NotInParam) {
value = value.toString();
}
parameterBindings[i] = new ParameterBinding(value,
procedureParameter.getType(), procedureParameter.getPrecision());
}
logger.logQuery(sqlStr, parameterBindings);
}
CallableStatement stmt = connection.prepareCall(sqlStr);
initStatement(stmt);
return stmt;
}
public Procedure getProcedure() {
return query.getMetaData(entityResolver).getProcedure();
}
public ProcedureQuery getProcedureQuery() {
return query;
}
/**
* Set IN and OUT parameters.
*/
protected void initStatement(CallableStatement stmt) throws Exception {
if (values != null && values.size() > 0) {
List<ProcedureParameter> params = getProcedure().getCallParameters();
int len = values.size();
for (int i = 0; i < len; i++) {
ProcedureParameter param = params.get(i);
// !Stored procedure parameter can be both in and out
// at the same time
if (param.isOutParam()) {
setOutParam(stmt, param, i + 1);
}
if (param.isInParameter()) {
setInParam(stmt, param, values.get(i), i + 1);
}
}
}
}
protected void initValues() {
Map<String, ?> queryValues = getProcedureQuery().getParameters();
// match values with parameters in the correct order.
// make an assumption that a missing value is NULL
// Any reason why this is bad?
for (ProcedureParameter param : callParams) {
if (param.getDirection() == ProcedureParameter.OUT_PARAMETER) {
values.add(OUT_PARAM);
} else {
values.add(queryValues.get(param.getName()));
}
}
}
/**
* Sets a single IN parameter of the CallableStatement.
*/
protected void setInParam(
CallableStatement stmt,
ProcedureParameter param,
Object val,
int pos) throws Exception {
ExtendedType extendedType = val != null
? adapter.getExtendedTypes().getRegisteredType(val.getClass())
: adapter.getExtendedTypes().getDefaultType();
ProcedureParameterBinding binding = new ProcedureParameterBinding(param);
binding.setStatementPosition(pos);
binding.setValue(val);
binding.setExtendedType(extendedType);
adapter.bindParameter(stmt, binding);
}
/**
* Sets a single OUT parameter of the CallableStatement.
*/
protected void setOutParam(CallableStatement stmt, ProcedureParameter param, int pos)
throws Exception {
int precision = param.getPrecision();
if (precision >= 0) {
stmt.registerOutParameter(pos, param.getType(), precision);
} else {
stmt.registerOutParameter(pos, param.getType());
}
}
}