001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: *
017: * $Header:$
018: */
019: package org.apache.beehive.controls.system.jdbc;
020:
021: import java.lang.reflect.Method;
022: import java.sql.CallableStatement;
023: import java.sql.Connection;
024: import java.sql.DatabaseMetaData;
025: import java.sql.DriverManager;
026: import java.sql.PreparedStatement;
027: import java.sql.ResultSet;
028: import java.sql.SQLException;
029: import java.util.Calendar;
030: import java.util.HashMap;
031: import java.util.Iterator;
032: import java.util.Map;
033: import java.util.Properties;
034: import java.util.Vector;
035: import javax.naming.NamingException;
036: import javax.naming.Context;
037: import javax.sql.DataSource;
038:
039: import org.apache.beehive.controls.api.ControlException;
040: import org.apache.beehive.controls.api.bean.ControlImplementation;
041: import org.apache.beehive.controls.api.bean.Extensible;
042: import org.apache.beehive.controls.api.context.ControlBeanContext;
043: import org.apache.beehive.controls.api.context.ResourceContext;
044: import org.apache.beehive.controls.api.context.ResourceContext.ResourceEvents;
045: import org.apache.beehive.controls.api.events.EventHandler;
046: import org.apache.beehive.controls.system.jdbc.parser.SqlParser;
047: import org.apache.beehive.controls.system.jdbc.parser.SqlStatement;
048: import org.apache.commons.logging.Log;
049: import org.apache.commons.logging.LogFactory;
050:
051: /**
052: * The implementation class for the database controller.
053: */
054: @ControlImplementation
055: public class JdbcControlImpl implements JdbcControl, Extensible,
056: java.io.Serializable {
057:
058: //
059: // contexts provided by the beehive controls runtime
060: //
061: @org.apache.beehive.controls.api.context.Context
062: protected ControlBeanContext _context;
063: @org.apache.beehive.controls.api.context.Context
064: protected ResourceContext _resourceContext;
065:
066: protected transient Connection _connection;
067: protected transient ConnectionDataSource _connectionDataSource;
068: protected transient DataSource _dataSource;
069: protected transient ConnectionDriver _connectionDriver;
070:
071: private Calendar _cal;
072: private transient Vector<PreparedStatement> _resources;
073:
074: private static final String EMPTY_STRING = "";
075: private static final Log LOGGER = LogFactory
076: .getLog(JdbcControlImpl.class);
077: private static final ResultSetMapper DEFAULT_MAPPER = new DefaultObjectResultSetMapper();
078: private static final SqlParser _sqlParser = new SqlParser();
079:
080: protected static final HashMap<Class, ResultSetMapper> _resultMappers = new HashMap<Class, ResultSetMapper>();
081: protected static Class<?> _xmlObjectClass;
082:
083: //
084: // initialize the result mapper table
085: //
086: static {
087: _resultMappers.put(ResultSet.class,
088: new DefaultResultSetMapper());
089: _resultMappers.put(Iterator.class,
090: new DefaultIteratorResultSetMapper());
091:
092: try {
093: _xmlObjectClass = Class
094: .forName("org.apache.xmlbeans.XmlObject");
095: _resultMappers.put(_xmlObjectClass,
096: new DefaultXmlObjectResultSetMapper());
097: } catch (ClassNotFoundException e) {
098: // noop: OK if not found, just can't support mapping to an XmlObject
099: }
100: }
101:
102: /**
103: * Constructor
104: */
105: public JdbcControlImpl() {
106: }
107:
108: /**
109: * Invoked by the controls runtime when a new instance of this class is aquired by the runtime
110: */
111: @EventHandler(field="_resourceContext",eventSet=ResourceEvents.class,eventName="onAcquire")
112: public void onAquire() {
113:
114: if (LOGGER.isDebugEnabled()) {
115: LOGGER.debug("Enter: onAquire()");
116: }
117:
118: try {
119: getConnection();
120: } catch (SQLException se) {
121: throw new ControlException(
122: "SQL Exception while attempting to connect to database.",
123: se);
124: }
125: }
126:
127: /**
128: * Invoked by the controls runtime when an instance of this class is released by the runtime
129: */
130: @EventHandler(field="_resourceContext",eventSet=ResourceContext.ResourceEvents.class,eventName="onRelease")
131: public void onRelease() {
132:
133: if (LOGGER.isDebugEnabled()) {
134: LOGGER.debug("Enter: onRelease()");
135: }
136:
137: for (PreparedStatement ps : getResources()) {
138: try {
139: ps.close();
140: } catch (SQLException sqe) {
141: }
142: }
143: getResources().clear();
144:
145: if (_connection != null) {
146: try {
147: _connection.close();
148: } catch (SQLException e) {
149: throw new ControlException(
150: "SQL Exception while attempting to close database connection.",
151: e);
152: }
153: }
154:
155: _connection = null;
156: _connectionDataSource = null;
157: _connectionDriver = null;
158: }
159:
160: /**
161: * Returns a database connection to the server associated with the control.
162: * The connection type is specified by a ConnectionDataSource or ConnectionDriver annotation on the control class
163: * which extends this control.
164: * <p/>
165: * It is typically not necessary to call this method when using the control.
166: */
167: public Connection getConnection() throws SQLException {
168:
169: if (_connection == null) {
170:
171: _connectionDataSource = _context
172: .getControlPropertySet(ConnectionDataSource.class);
173: _connectionDriver = _context
174: .getControlPropertySet(ConnectionDriver.class);
175: final ConnectionOptions connectionOptions = _context
176: .getControlPropertySet(ConnectionOptions.class);
177:
178: if (_connectionDataSource != null
179: && _connectionDataSource.jndiName() != null) {
180: _connection = getConnectionFromDataSource(
181: _connectionDataSource.jndiName(),
182: _connectionDataSource.jndiContextFactory());
183:
184: } else if (_connectionDriver != null
185: && _connectionDriver.databaseDriverClass() != null) {
186: _connection = getConnectionFromDriverManager(
187: _connectionDriver.databaseDriverClass(),
188: _connectionDriver.databaseURL(),
189: _connectionDriver.userName(), _connectionDriver
190: .password(), _connectionDriver
191: .properties());
192: } else {
193: throw new ControlException("no @\'"
194: + ConnectionDataSource.class.getName()
195: + "\' or \'" + ConnectionDriver.class.getName()
196: + "\' property found.");
197: }
198:
199: //
200: // set any specifed connection options
201: //
202: if (connectionOptions != null) {
203:
204: if (_connection.isReadOnly() != connectionOptions
205: .readOnly()) {
206: _connection.setReadOnly(connectionOptions
207: .readOnly());
208: }
209:
210: DatabaseMetaData dbMetadata = _connection.getMetaData();
211:
212: final HoldabilityType holdability = connectionOptions
213: .resultSetHoldability();
214: if (holdability != HoldabilityType.DRIVER_DEFAULT) {
215: if (dbMetadata
216: .supportsResultSetHoldability(holdability
217: .getHoldability())) {
218: _connection.setHoldability(holdability
219: .getHoldability());
220: } else {
221: throw new ControlException(
222: "Database does not support ResultSet holdability type: "
223: + holdability.toString());
224: }
225: }
226:
227: setTypeMappers(connectionOptions.typeMappers());
228: }
229: }
230:
231: return _connection;
232: }
233:
234: /**
235: * Called by the Controls runtime to handle calls to methods of an extensible control.
236: *
237: * @param method The extended operation that was called.
238: * @param args Parameters of the operation.
239: * @return The value that should be returned by the operation.
240: * @throws Throwable any exception declared on the extended operation may be
241: * thrown. If a checked exception is thrown from the implementation that is not declared
242: * on the original interface, it will be wrapped in a ControlException.
243: */
244: public Object invoke(Method method, Object[] args) throws Throwable {
245:
246: if (LOGGER.isDebugEnabled()) {
247: LOGGER.debug("Enter: invoke()");
248: }
249: assert _connection.isClosed() == false : "invoke(): JDBC Connection has been closed!!!!";
250: return execPreparedStatement(method, args);
251: }
252:
253: /**
254: * Sets the {@link Calendar} used when working with time/date types
255: */
256: public void setDataSourceCalendar(Calendar cal) {
257: _cal = (Calendar) cal.clone();
258: }
259:
260: /**
261: * Returns the {@link Calendar} used when working with time/date types.
262: *
263: * @return the {@link Calendar} to use with this {@link DataSource}
264: */
265: public Calendar getDataSourceCalendar() {
266: return _cal;
267: }
268:
269: // /////////////////////////////////////////// Protected Methods ////////////////////////////////////////////
270:
271: /**
272: * Create and exec a {@link PreparedStatement}
273: *
274: * @param method the method to invoke
275: * @param args the method's arguments
276: * @return the return value from the {@link PreparedStatement}
277: * @throws Throwable any exception that occurs; the caller should handle these appropriately
278: */
279: protected Object execPreparedStatement(Method method, Object[] args)
280: throws Throwable {
281:
282: final SQL methodSQL = (SQL) _context.getMethodPropertySet(
283: method, SQL.class);
284: if (methodSQL == null || methodSQL.statement() == null) {
285: throw new ControlException("Method " + method.getName()
286: + " is missing @SQL annotation");
287: }
288:
289: setTypeMappers(methodSQL.typeMappersOverride());
290:
291: //
292: // build the statement and execute it
293: //
294:
295: PreparedStatement ps = null;
296: try {
297: Class returnType = method.getReturnType();
298:
299: SqlStatement sqlStatement = _sqlParser.parse(methodSQL
300: .statement());
301: ps = sqlStatement.createPreparedStatement(_context,
302: _connection, _cal, method, args);
303:
304: if (LOGGER.isInfoEnabled()) {
305: LOGGER.info("PreparedStatement: "
306: + sqlStatement.createPreparedStatementString(
307: _context, _connection, method, args));
308: }
309:
310: //
311: // special processing for batch updates
312: //
313: if (sqlStatement.isBatchUpdate()) {
314: return ps.executeBatch();
315: }
316:
317: //
318: // execute the statement
319: //
320: boolean hasResults = ps.execute();
321:
322: //
323: // callable statement processing
324: //
325: if (sqlStatement.isCallableStatement()) {
326: SQLParameter[] params = (SQLParameter[]) args[0];
327: for (int i = 0; i < params.length; i++) {
328: if (params[i].dir != SQLParameter.IN) {
329: params[i].value = ((CallableStatement) ps)
330: .getObject(i + 1);
331: }
332: }
333: return null;
334: }
335:
336: //
337: // process returned data
338: //
339: ResultSet rs = null;
340: int updateCount = ps.getUpdateCount();
341:
342: if (hasResults) {
343: rs = ps.getResultSet();
344: }
345:
346: if (sqlStatement.getsGeneratedKeys()) {
347: rs = ps.getGeneratedKeys();
348: hasResults = true;
349: }
350:
351: if (!hasResults && updateCount > -1) {
352: boolean moreResults = ps.getMoreResults();
353: int tempUpdateCount = ps.getUpdateCount();
354:
355: while ((moreResults && rs == null)
356: || tempUpdateCount > -1) {
357: if (moreResults) {
358: rs = ps.getResultSet();
359: hasResults = true;
360: moreResults = false;
361: tempUpdateCount = -1;
362: } else {
363: moreResults = ps.getMoreResults();
364: tempUpdateCount = ps.getUpdateCount();
365: }
366: }
367: }
368:
369: Object returnObject = null;
370: if (hasResults) {
371:
372: //
373: // if a result set mapper was specified in the methods annotation, use it
374: // otherwise find the mapper for the return type in the hashmap
375: //
376: final Class resultSetMapperClass = methodSQL
377: .resultSetMapper();
378: final ResultSetMapper rsm;
379: if (!UndefinedResultSetMapper.class
380: .isAssignableFrom(resultSetMapperClass)) {
381: if (ResultSetMapper.class
382: .isAssignableFrom(resultSetMapperClass)) {
383: rsm = (ResultSetMapper) resultSetMapperClass
384: .newInstance();
385: } else {
386: throw new ControlException(
387: "Result set mappers must be subclasses of ResultSetMapper.class!");
388: }
389: } else {
390: if (_resultMappers.containsKey(returnType)) {
391: rsm = _resultMappers.get(returnType);
392: } else {
393: if (_xmlObjectClass != null
394: && _xmlObjectClass
395: .isAssignableFrom(returnType)) {
396: rsm = _resultMappers.get(_xmlObjectClass);
397: } else {
398: rsm = DEFAULT_MAPPER;
399: }
400: }
401: }
402:
403: returnObject = rsm.mapToResultType(_context, method,
404: rs, _cal);
405: if (rsm.canCloseResultSet() == false) {
406: getResources().add(ps);
407: }
408:
409: //
410: // empty ResultSet
411: //
412: } else {
413: if (returnType.equals(Void.TYPE)) {
414: returnObject = null;
415: } else if (returnType.equals(Integer.TYPE)) {
416: returnObject = new Integer(updateCount);
417: } else if (!sqlStatement.isCallableStatement()) {
418: throw new ControlException("Method "
419: + method.getName()
420: + "is DML but does not return void or int");
421: }
422: }
423: return returnObject;
424:
425: } finally {
426: // Keep statements open that have in-use result sets
427: if (ps != null && !getResources().contains(ps)) {
428: ps.close();
429: }
430: }
431: }
432:
433: // /////////////////////////////////////////// Private Methods ////////////////////////////////////////////
434:
435: /**
436: * Get a connection from a DataSource.
437: *
438: * @param jndiName Specifed in the subclasse's ConnectionDataSource annotation
439: * @param jndiFactory Specified in the subclasse's ConnectionDataSource Annotation.
440: * @return null if a connection cannot be established
441: * @throws SQLException
442: */
443: private Connection getConnectionFromDataSource(String jndiName,
444: Class<? extends JdbcControl.JndiContextFactory> jndiFactory)
445: throws SQLException {
446:
447: Connection con = null;
448: try {
449: JndiContextFactory jf = (JndiContextFactory) jndiFactory
450: .newInstance();
451: Context jndiContext = jf.getContext();
452: _dataSource = (DataSource) jndiContext.lookup(jndiName);
453: con = _dataSource.getConnection();
454: } catch (IllegalAccessException iae) {
455: throw new ControlException("IllegalAccessException:", iae);
456: } catch (InstantiationException ie) {
457: throw new ControlException("InstantiationException:", ie);
458: } catch (NamingException ne) {
459: throw new ControlException("NamingException:", ne);
460: }
461: return con;
462: }
463:
464: /**
465: * Get a JDBC connection from the DriverManager.
466: *
467: * @param dbDriverClassName Specified in the subclasse's ConnectionDriver annotation.
468: * @param dbUrlStr Specified in the subclasse's ConnectionDriver annotation.
469: * @param userName Specified in the subclasse's ConnectionDriver annotation.
470: * @param password Specified in the subclasse's ConnectionDriver annotation.
471: * @return null if a connection cannot be established.
472: * @throws SQLException
473: */
474: private Connection getConnectionFromDriverManager(
475: String dbDriverClassName, String dbUrlStr, String userName,
476: String password, String propertiesString)
477: throws SQLException {
478:
479: Connection con = null;
480: try {
481: Class.forName(dbDriverClassName);
482: if (!EMPTY_STRING.equals(userName)) {
483: con = DriverManager.getConnection(dbUrlStr, userName,
484: password);
485: } else if (!EMPTY_STRING.equals(propertiesString)) {
486: Properties props = parseProperties(propertiesString);
487: if (props == null) {
488: throw new ControlException(
489: "Invalid properties annotation value: "
490: + propertiesString);
491: }
492: con = DriverManager.getConnection(dbUrlStr, props);
493: } else {
494: con = DriverManager.getConnection(dbUrlStr);
495: }
496: } catch (ClassNotFoundException e) {
497: throw new ControlException(
498: "Database driver class not found!", e);
499: }
500: return con;
501: }
502:
503: /**
504: * Get the Vector of Statements which we need to keep open.
505: * @return Vector of PreparedStatement
506: */
507: private Vector<PreparedStatement> getResources() {
508: if (_resources == null) {
509: _resources = new Vector<PreparedStatement>();
510: }
511: return _resources;
512: }
513:
514: /**
515: * Parse the propertiesString into a Properties object. The string must have the format of:
516: * propertyName=propertyValue;propertyName=propertyValue;...
517: *
518: * @param propertiesString
519: * @return A Properties instance or null if parse fails
520: */
521: private Properties parseProperties(String propertiesString) {
522: Properties properties = null;
523: String[] propPairs = propertiesString.split(";");
524: if (propPairs.length > 0) {
525: properties = new Properties();
526: for (String propPair : propPairs) {
527: int eq = propPair.indexOf('=');
528: assert eq > -1 : "Invalid properties syntax: "
529: + propertiesString;
530: properties.put(propPair.substring(0, eq), propPair
531: .substring(eq + 1, propPair.length()));
532: }
533: }
534: return properties;
535: }
536:
537: /**
538: * Set any custom type mappers specifed in the annotation for the connection. Used for mapping SQL UDTs to
539: * java classes.
540: *
541: * @param typeMappers An array of TypeMapper.
542: */
543: private void setTypeMappers(TypeMapper[] typeMappers)
544: throws SQLException {
545:
546: if (typeMappers.length > 0) {
547: Map<String, Class<?>> mappers = _connection.getTypeMap();
548: for (TypeMapper t : typeMappers) {
549: mappers.put(t.UDTName(), t.mapperClass());
550: }
551: _connection.setTypeMap(mappers);
552: }
553: }
554: }
|