JdbcMapper/beehive-jdbc-control/src/main/java/org/apache/beehive/controls/system/jdbc/JdbcControlImpl.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);
}
}
}