507 lines
19 KiB
Java
507 lines
19 KiB
Java
/*
|
|
* 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.
|
|
*
|
|
* $Header:$
|
|
*/
|
|
package org.apache.beehive.controls.system.jdbc;
|
|
|
|
import java.lang.reflect.Method;
|
|
import java.sql.CallableStatement;
|
|
import java.sql.Connection;
|
|
import java.sql.DatabaseMetaData;
|
|
import java.sql.DriverManager;
|
|
import java.sql.PreparedStatement;
|
|
import java.sql.ResultSet;
|
|
import java.sql.SQLException;
|
|
import java.util.Calendar;
|
|
import java.util.HashMap;
|
|
import java.util.Iterator;
|
|
import java.util.Map;
|
|
import java.util.Properties;
|
|
import java.util.Vector;
|
|
import javax.naming.NamingException;
|
|
import javax.naming.Context;
|
|
import javax.sql.DataSource;
|
|
|
|
import org.apache.beehive.controls.api.ControlException;
|
|
import org.apache.beehive.controls.api.bean.ControlImplementation;
|
|
import org.apache.beehive.controls.api.bean.Extensible;
|
|
import org.apache.beehive.controls.api.context.ControlBeanContext;
|
|
import org.apache.beehive.controls.api.context.ResourceContext;
|
|
import org.apache.beehive.controls.api.context.ResourceContext.ResourceEvents;
|
|
import org.apache.beehive.controls.api.events.EventHandler;
|
|
import org.apache.beehive.controls.system.jdbc.parser.SqlParser;
|
|
import org.apache.beehive.controls.system.jdbc.parser.SqlStatement;
|
|
import org.apache.commons.logging.Log;
|
|
import org.apache.commons.logging.LogFactory;
|
|
|
|
/**
|
|
* The implementation class for the database controller.
|
|
*/
|
|
@ControlImplementation
|
|
public class JdbcControlImpl implements JdbcControl, Extensible, java.io.Serializable {
|
|
|
|
//
|
|
// contexts provided by the beehive controls runtime
|
|
//
|
|
@org.apache.beehive.controls.api.context.Context
|
|
protected ControlBeanContext _context;
|
|
@org.apache.beehive.controls.api.context.Context
|
|
protected ResourceContext _resourceContext;
|
|
|
|
protected transient Connection _connection;
|
|
protected transient ConnectionDataSource _connectionDataSource;
|
|
protected transient DataSource _dataSource;
|
|
protected transient ConnectionDriver _connectionDriver;
|
|
|
|
private Calendar _cal;
|
|
private transient Vector<PreparedStatement> _resources;
|
|
|
|
private static final String EMPTY_STRING = "";
|
|
private static final Log LOGGER = LogFactory.getLog(JdbcControlImpl.class);
|
|
private static final ResultSetMapper DEFAULT_MAPPER = new NewDefaultObjectResultSetMapper();
|
|
private static final SqlParser _sqlParser = new SqlParser();
|
|
|
|
protected static final HashMap<Class, ResultSetMapper> _resultMappers = new HashMap<Class, ResultSetMapper>();
|
|
protected static Class<?> _xmlObjectClass;
|
|
|
|
//
|
|
// initialize the result mapper table
|
|
//
|
|
static {
|
|
_resultMappers.put(ResultSet.class, new DefaultResultSetMapper());
|
|
|
|
try {
|
|
_xmlObjectClass = Class.forName("org.apache.xmlbeans.XmlObject");
|
|
_resultMappers.put(_xmlObjectClass, new DefaultXmlObjectResultSetMapper());
|
|
} catch (ClassNotFoundException e) {
|
|
// noop: OK if not found, just can't support mapping to an XmlObject
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Constructor
|
|
*/
|
|
public JdbcControlImpl() { }
|
|
|
|
/**
|
|
* Invoked by the controls runtime when a new instance of this class is aquired by the runtime
|
|
*/
|
|
@EventHandler(field = "_resourceContext", eventSet = ResourceEvents.class, eventName = "onAcquire")
|
|
public void onAquire() {
|
|
|
|
if (LOGGER.isDebugEnabled()) {
|
|
LOGGER.debug("Enter: onAquire()");
|
|
}
|
|
|
|
try {
|
|
getConnection();
|
|
} catch (SQLException se) {
|
|
throw new ControlException("SQL Exception while attempting to connect to database.", se);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Invoked by the controls runtime when an instance of this class is released by the runtime
|
|
*/
|
|
@EventHandler(field = "_resourceContext", eventSet = ResourceContext.ResourceEvents.class, eventName = "onRelease")
|
|
public void onRelease() {
|
|
|
|
if (LOGGER.isDebugEnabled()) {
|
|
LOGGER.debug("Enter: onRelease()");
|
|
}
|
|
|
|
for (PreparedStatement ps : getResources()) {
|
|
try {
|
|
ps.close();
|
|
} catch (SQLException sqe) {
|
|
}
|
|
}
|
|
getResources().clear();
|
|
|
|
if (_connection != null) {
|
|
try {
|
|
_connection.close();
|
|
} catch (SQLException e) {
|
|
throw new ControlException("SQL Exception while attempting to close database connection.", e);
|
|
}
|
|
}
|
|
|
|
_connection = null;
|
|
_connectionDataSource = null;
|
|
_connectionDriver = null;
|
|
}
|
|
|
|
/**
|
|
* Returns a database connection to the server associated with the control.
|
|
* The connection type is specified by a ConnectionDataSource or ConnectionDriver annotation on the control class
|
|
* which extends this control.
|
|
* <p/>
|
|
* It is typically not necessary to call this method when using the control.
|
|
*/
|
|
public Connection getConnection() throws SQLException {
|
|
|
|
if (_connection == null) {
|
|
|
|
_connectionDataSource = _context.getControlPropertySet(ConnectionDataSource.class);
|
|
_connectionDriver = _context.getControlPropertySet(ConnectionDriver.class);
|
|
final ConnectionOptions connectionOptions = _context.getControlPropertySet(ConnectionOptions.class);
|
|
|
|
if (_connectionDataSource != null && _connectionDataSource.jndiName() != null) {
|
|
_connection = getConnectionFromDataSource(_connectionDataSource.jndiName(),
|
|
_connectionDataSource.jndiContextFactory());
|
|
|
|
} else if (_connectionDriver != null && _connectionDriver.databaseDriverClass() != null) {
|
|
_connection = getConnectionFromDriverManager(_connectionDriver.databaseDriverClass(),
|
|
_connectionDriver.databaseURL(),
|
|
_connectionDriver.userName(),
|
|
_connectionDriver.password(),
|
|
_connectionDriver.properties());
|
|
} else {
|
|
throw new ControlException("no @\'" + ConnectionDataSource.class.getName()
|
|
+ "\' or \'" + ConnectionDriver.class.getName() + "\' property found.");
|
|
}
|
|
|
|
//
|
|
// set any specifed connection options
|
|
//
|
|
if (connectionOptions != null) {
|
|
|
|
if (_connection.isReadOnly() != connectionOptions.readOnly()) {
|
|
_connection.setReadOnly(connectionOptions.readOnly());
|
|
}
|
|
|
|
DatabaseMetaData dbMetadata = _connection.getMetaData();
|
|
|
|
final HoldabilityType holdability = connectionOptions.resultSetHoldability();
|
|
if (holdability != HoldabilityType.DRIVER_DEFAULT) {
|
|
if (dbMetadata.supportsResultSetHoldability(holdability.getHoldability())) {
|
|
_connection.setHoldability(holdability.getHoldability());
|
|
} else {
|
|
throw new ControlException("Database does not support ResultSet holdability type: "
|
|
+ holdability.toString());
|
|
}
|
|
}
|
|
|
|
setTypeMappers(connectionOptions.typeMappers());
|
|
}
|
|
}
|
|
|
|
return _connection;
|
|
}
|
|
|
|
|
|
/**
|
|
* Called by the Controls runtime to handle calls to methods of an extensible control.
|
|
*
|
|
* @param method The extended operation that was called.
|
|
* @param args Parameters of the operation.
|
|
* @return The value that should be returned by the operation.
|
|
* @throws Throwable any exception declared on the extended operation may be
|
|
* thrown. If a checked exception is thrown from the implementation that is not declared
|
|
* on the original interface, it will be wrapped in a ControlException.
|
|
*/
|
|
public Object invoke(Method method, Object[] args) throws Throwable {
|
|
|
|
if (LOGGER.isDebugEnabled()) {
|
|
LOGGER.debug("Enter: invoke()");
|
|
}
|
|
assert _connection.isClosed() == false : "invoke(): JDBC Connection has been closed!!!!";
|
|
return execPreparedStatement(method, args);
|
|
}
|
|
|
|
/**
|
|
* Sets the {@link Calendar} used when working with time/date types
|
|
*/
|
|
public void setDataSourceCalendar(Calendar cal) {
|
|
_cal = (Calendar) cal.clone();
|
|
}
|
|
|
|
/**
|
|
* Returns the {@link Calendar} used when working with time/date types.
|
|
*
|
|
* @return the {@link Calendar} to use with this {@link DataSource}
|
|
*/
|
|
public Calendar getDataSourceCalendar() {
|
|
return _cal;
|
|
}
|
|
|
|
// /////////////////////////////////////////// Protected Methods ////////////////////////////////////////////
|
|
|
|
|
|
/**
|
|
* Create and exec a {@link PreparedStatement}
|
|
*
|
|
* @param method the method to invoke
|
|
* @param args the method's arguments
|
|
* @return the return value from the {@link PreparedStatement}
|
|
* @throws Throwable any exception that occurs; the caller should handle these appropriately
|
|
*/
|
|
protected Object execPreparedStatement(Method method, Object[] args)
|
|
throws Throwable {
|
|
|
|
final SQL methodSQL = (SQL) _context.getMethodPropertySet(method, SQL.class);
|
|
if (methodSQL == null || methodSQL.statement() == null) {
|
|
throw new ControlException("Method " + method.getName() + " is missing @SQL annotation");
|
|
}
|
|
|
|
setTypeMappers(methodSQL.typeMappersOverride());
|
|
|
|
//
|
|
// build the statement and execute it
|
|
//
|
|
|
|
PreparedStatement ps = null;
|
|
try {
|
|
Class returnType = method.getReturnType();
|
|
|
|
SqlStatement sqlStatement = _sqlParser.parse(methodSQL.statement());
|
|
ps = sqlStatement.createPreparedStatement(_context, _connection, _cal, method, args);
|
|
|
|
if (LOGGER.isInfoEnabled()) {
|
|
LOGGER.info("PreparedStatement: "
|
|
+ sqlStatement.createPreparedStatementString(_context, _connection, method, args));
|
|
}
|
|
|
|
//
|
|
// special processing for batch updates
|
|
//
|
|
if (sqlStatement.isBatchUpdate()) {
|
|
return ps.executeBatch();
|
|
}
|
|
|
|
//
|
|
// execute the statement
|
|
//
|
|
boolean hasResults = ps.execute();
|
|
|
|
|
|
//
|
|
// callable statement processing
|
|
//
|
|
if (sqlStatement.isCallableStatement()) {
|
|
SQLParameter[] params = (SQLParameter[]) args[0];
|
|
for (int i = 0; i < params.length; i++) {
|
|
if (params[i].dir != SQLParameter.IN) {
|
|
params[i].value = ((CallableStatement) ps).getObject(i + 1);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
|
|
//
|
|
// process returned data
|
|
//
|
|
ResultSet rs = null;
|
|
int updateCount = ps.getUpdateCount();
|
|
|
|
if (hasResults) {
|
|
rs = ps.getResultSet();
|
|
}
|
|
|
|
if (sqlStatement.getsGeneratedKeys()) {
|
|
rs = ps.getGeneratedKeys();
|
|
hasResults = true;
|
|
}
|
|
|
|
if (!hasResults && updateCount > -1) {
|
|
boolean moreResults = ps.getMoreResults();
|
|
int tempUpdateCount = ps.getUpdateCount();
|
|
|
|
while ((moreResults && rs == null) || tempUpdateCount > -1) {
|
|
if (moreResults) {
|
|
rs = ps.getResultSet();
|
|
hasResults = true;
|
|
moreResults = false;
|
|
tempUpdateCount = -1;
|
|
} else {
|
|
moreResults = ps.getMoreResults();
|
|
tempUpdateCount = ps.getUpdateCount();
|
|
}
|
|
}
|
|
}
|
|
|
|
Object returnObject = null;
|
|
if (hasResults) {
|
|
|
|
//
|
|
// if a result set mapper was specified in the methods annotation, use it
|
|
// otherwise find the mapper for the return type in the hashmap
|
|
//
|
|
final Class resultSetMapperClass = methodSQL.resultSetMapper();
|
|
final ResultSetMapper rsm;
|
|
if (!UndefinedResultSetMapper.class.isAssignableFrom(resultSetMapperClass)) {
|
|
if (ResultSetMapper.class.isAssignableFrom(resultSetMapperClass)) {
|
|
rsm = (ResultSetMapper) resultSetMapperClass.newInstance();
|
|
} else {
|
|
throw new ControlException("Result set mappers must be subclasses of ResultSetMapper.class!");
|
|
}
|
|
} else {
|
|
if (_resultMappers.containsKey(returnType)) {
|
|
rsm = _resultMappers.get(returnType);
|
|
} else {
|
|
if (_xmlObjectClass != null && _xmlObjectClass.isAssignableFrom(returnType)) {
|
|
rsm = _resultMappers.get(_xmlObjectClass);
|
|
} else {
|
|
rsm = DEFAULT_MAPPER;
|
|
}
|
|
}
|
|
}
|
|
|
|
returnObject = rsm.mapToResultType(_context, method, rs, _cal);
|
|
if (rsm.canCloseResultSet() == false) {
|
|
getResources().add(ps);
|
|
}
|
|
|
|
//
|
|
// empty ResultSet
|
|
//
|
|
} else {
|
|
if (returnType.equals(Void.TYPE)) {
|
|
returnObject = null;
|
|
} else if (returnType.equals(Integer.TYPE)) {
|
|
returnObject = new Integer(updateCount);
|
|
} else if (!sqlStatement.isCallableStatement()) {
|
|
throw new ControlException("Method " + method.getName() + "is DML but does not return void or int");
|
|
}
|
|
}
|
|
return returnObject;
|
|
|
|
} finally {
|
|
// Keep statements open that have in-use result sets
|
|
if (ps != null && !getResources().contains(ps)) {
|
|
ps.close();
|
|
}
|
|
}
|
|
}
|
|
|
|
// /////////////////////////////////////////// Private Methods ////////////////////////////////////////////
|
|
|
|
/**
|
|
* Get a connection from a DataSource.
|
|
*
|
|
* @param jndiName Specifed in the subclasse's ConnectionDataSource annotation
|
|
* @param jndiFactory Specified in the subclasse's ConnectionDataSource Annotation.
|
|
* @return null if a connection cannot be established
|
|
* @throws SQLException
|
|
*/
|
|
private Connection getConnectionFromDataSource(String jndiName,
|
|
Class<? extends JdbcControl.JndiContextFactory> jndiFactory)
|
|
throws SQLException
|
|
{
|
|
|
|
Connection con = null;
|
|
try {
|
|
JndiContextFactory jf = (JndiContextFactory) jndiFactory.newInstance();
|
|
Context jndiContext = jf.getContext();
|
|
_dataSource = (DataSource) jndiContext.lookup(jndiName);
|
|
con = _dataSource.getConnection();
|
|
} catch (IllegalAccessException iae) {
|
|
throw new ControlException("IllegalAccessException:", iae);
|
|
} catch (InstantiationException ie) {
|
|
throw new ControlException("InstantiationException:", ie);
|
|
} catch (NamingException ne) {
|
|
throw new ControlException("NamingException:", ne);
|
|
}
|
|
return con;
|
|
}
|
|
|
|
/**
|
|
* Get a JDBC connection from the DriverManager.
|
|
*
|
|
* @param dbDriverClassName Specified in the subclasse's ConnectionDriver annotation.
|
|
* @param dbUrlStr Specified in the subclasse's ConnectionDriver annotation.
|
|
* @param userName Specified in the subclasse's ConnectionDriver annotation.
|
|
* @param password Specified in the subclasse's ConnectionDriver annotation.
|
|
* @return null if a connection cannot be established.
|
|
* @throws SQLException
|
|
*/
|
|
private Connection getConnectionFromDriverManager(String dbDriverClassName, String dbUrlStr,
|
|
String userName, String password, String propertiesString)
|
|
throws SQLException
|
|
{
|
|
|
|
Connection con = null;
|
|
try {
|
|
Class.forName(dbDriverClassName);
|
|
if (!EMPTY_STRING.equals(userName)) {
|
|
con = DriverManager.getConnection(dbUrlStr, userName, password);
|
|
} else if (!EMPTY_STRING.equals(propertiesString)) {
|
|
Properties props = parseProperties(propertiesString);
|
|
if (props == null) {
|
|
throw new ControlException("Invalid properties annotation value: " + propertiesString);
|
|
}
|
|
con = DriverManager.getConnection(dbUrlStr, props);
|
|
} else {
|
|
con = DriverManager.getConnection(dbUrlStr);
|
|
}
|
|
} catch (ClassNotFoundException e) {
|
|
throw new ControlException("Database driver class not found!", e);
|
|
}
|
|
return con;
|
|
}
|
|
|
|
/**
|
|
* Get the Vector of Statements which we need to keep open.
|
|
* @return Vector of PreparedStatement
|
|
*/
|
|
private Vector<PreparedStatement> getResources() {
|
|
if (_resources == null) {
|
|
_resources = new Vector<PreparedStatement>();
|
|
}
|
|
return _resources;
|
|
}
|
|
|
|
/**
|
|
* Parse the propertiesString into a Properties object. The string must have the format of:
|
|
* propertyName=propertyValue;propertyName=propertyValue;...
|
|
*
|
|
* @param propertiesString
|
|
* @return A Properties instance or null if parse fails
|
|
*/
|
|
private Properties parseProperties(String propertiesString) {
|
|
Properties properties = null;
|
|
String[] propPairs = propertiesString.split(";");
|
|
if (propPairs.length > 0) {
|
|
properties = new Properties();
|
|
for (String propPair : propPairs) {
|
|
int eq = propPair.indexOf('=');
|
|
assert eq > -1 : "Invalid properties syntax: " + propertiesString;
|
|
properties.put(propPair.substring(0, eq), propPair.substring(eq + 1, propPair.length()));
|
|
}
|
|
}
|
|
return properties;
|
|
}
|
|
|
|
/**
|
|
* Set any custom type mappers specifed in the annotation for the connection. Used for mapping SQL UDTs to
|
|
* java classes.
|
|
*
|
|
* @param typeMappers An array of TypeMapper.
|
|
*/
|
|
private void setTypeMappers(TypeMapper[] typeMappers) throws SQLException {
|
|
|
|
if (typeMappers.length > 0) {
|
|
Map<String, Class<?>> mappers = _connection.getTypeMap();
|
|
for (TypeMapper t : typeMappers) {
|
|
mappers.put(t.UDTName(), t.mapperClass());
|
|
}
|
|
_connection.setTypeMap(mappers);
|
|
}
|
|
}
|
|
}
|