From c236f9fac7f43fa5a1d3780beaca2bec28eeaba9 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Thu, 23 Oct 2008 14:23:13 +0000 Subject: [PATCH] Initial import of JDBC module --- org.springframework.jdbc/build.xml | 6 + org.springframework.jdbc/ivy.xml | 38 + org.springframework.jdbc/pom.xml | 59 + .../jdbc/BadSqlGrammarException.java | 65 + .../CannotGetJdbcConnectionException.java | 50 + ...ncorrectResultSetColumnCountException.java | 74 + .../jdbc/InvalidResultSetAccessException.java | 75 + ...ffectedIncorrectNumberOfRowsException.java | 69 + .../jdbc/LobRetrievalFailureException.java | 48 + .../jdbc/SQLWarningException.java | 53 + .../jdbc/UncategorizedSQLException.java | 63 + .../jdbc/core/ArgPreparedStatementSetter.java | 61 + .../core/ArgTypePreparedStatementSetter.java | 87 ++ .../core/BatchPreparedStatementSetter.java | 56 + .../jdbc/core/BeanPropertyRowMapper.java | 270 ++++ .../jdbc/core/CallableStatementCallback.java | 77 + .../jdbc/core/CallableStatementCreator.java | 56 + .../core/CallableStatementCreatorFactory.java | 239 +++ .../jdbc/core/ColumnMapRowMapper.java | 99 ++ .../jdbc/core/ConnectionCallback.java | 69 + .../jdbc/core/DisposableSqlTypeValue.java | 38 + ...rruptibleBatchPreparedStatementSetter.java | 61 + .../jdbc/core/JdbcOperations.java | 936 +++++++++++ .../jdbc/core/JdbcTemplate.java | 1374 +++++++++++++++++ .../jdbc/core/ParameterDisposer.java | 46 + .../jdbc/core/ParameterMapper.java | 47 + .../jdbc/core/PreparedStatementCallback.java | 80 + .../jdbc/core/PreparedStatementCreator.java | 56 + .../core/PreparedStatementCreatorFactory.java | 321 ++++ .../jdbc/core/PreparedStatementSetter.java | 53 + .../jdbc/core/ResultSetExtractor.java | 63 + .../core/ResultSetSupportingSqlParameter.java | 136 ++ .../jdbc/core/RowCallbackHandler.java | 60 + .../jdbc/core/RowCountCallbackHandler.java | 137 ++ .../springframework/jdbc/core/RowMapper.java | 63 + .../core/RowMapperResultSetExtractor.java | 97 ++ .../jdbc/core/SingleColumnRowMapper.java | 185 +++ .../jdbc/core/SqlInOutParameter.java | 112 ++ .../jdbc/core/SqlOutParameter.java | 125 ++ .../jdbc/core/SqlParameter.java | 190 +++ .../jdbc/core/SqlParameterValue.java | 93 ++ .../jdbc/core/SqlProvider.java | 41 + .../jdbc/core/SqlReturnResultSet.java | 68 + .../jdbc/core/SqlReturnType.java | 65 + .../jdbc/core/SqlReturnUpdateCount.java | 41 + .../core/SqlRowSetResultSetExtractor.java | 86 ++ .../jdbc/core/SqlTypeValue.java | 67 + .../jdbc/core/StatementCallback.java | 73 + .../jdbc/core/StatementCreatorUtils.java | 409 +++++ .../core/metadata/CallMetaDataContext.java | 589 +++++++ .../core/metadata/CallMetaDataProvider.java | 175 +++ .../metadata/CallMetaDataProviderFactory.java | 140 ++ .../core/metadata/CallParameterMetaData.java | 78 + .../metadata/Db2CallMetaDataProvider.java | 70 + .../metadata/DerbyCallMetaDataProvider.java | 42 + .../metadata/GenericCallMetaDataProvider.java | 364 +++++ .../GenericTableMetaDataProvider.java | 421 +++++ .../metadata/HsqlTableMetaDataProvider.java | 44 + .../metadata/OracleCallMetaDataProvider.java | 82 + .../PostgresCallMetaDataProvider.java | 64 + .../PostgresTableMetaDataProvider.java | 34 + .../SqlServerCallMetaDataProvider.java | 59 + .../metadata/SybaseCallMetaDataProvider.java | 61 + .../core/metadata/TableMetaDataContext.java | 347 +++++ .../core/metadata/TableMetaDataProvider.java | 130 ++ .../TableMetaDataProviderFactory.java | 85 + .../core/metadata/TableParameterMetaData.java | 65 + .../jdbc/core/metadata/package.html | 7 + .../AbstractSqlParameterSource.java | 84 + .../BeanPropertySqlParameterSource.java | 103 ++ .../namedparam/MapSqlParameterSource.java | 165 ++ .../NamedParameterJdbcDaoSupport.java | 48 + .../NamedParameterJdbcOperations.java | 492 ++++++ .../NamedParameterJdbcTemplate.java | 278 ++++ .../core/namedparam/NamedParameterUtils.java | 386 +++++ .../jdbc/core/namedparam/ParsedSql.java | 144 ++ .../core/namedparam/SqlParameterSource.java | 84 + .../namedparam/SqlParameterSourceUtils.java | 110 ++ .../jdbc/core/namedparam/package.html | 16 + .../springframework/jdbc/core/package.html | 8 + .../jdbc/core/simple/AbstractJdbcCall.java | 407 +++++ .../jdbc/core/simple/AbstractJdbcInsert.java | 625 ++++++++ .../ParameterizedBeanPropertyRowMapper.java | 81 + .../core/simple/ParameterizedRowMapper.java | 43 + .../ParameterizedSingleColumnRowMapper.java | 68 + .../jdbc/core/simple/SimpleJdbcCall.java | 167 ++ .../core/simple/SimpleJdbcCallOperations.java | 157 ++ .../core/simple/SimpleJdbcDaoSupport.java | 50 + .../jdbc/core/simple/SimpleJdbcInsert.java | 129 ++ .../simple/SimpleJdbcInsertOperations.java | 134 ++ .../core/simple/SimpleJdbcOperations.java | 386 +++++ .../jdbc/core/simple/SimpleJdbcTemplate.java | 338 ++++ .../jdbc/core/simple/package.html | 17 + ...rruptibleBatchPreparedStatementSetter.java | 76 + ...tLobCreatingPreparedStatementCallback.java | 89 ++ ...bstractLobStreamingResultSetExtractor.java | 120 ++ .../core/support/AbstractSqlTypeValue.java | 79 + .../support/JdbcBeanDefinitionReader.java | 121 ++ .../jdbc/core/support/JdbcDaoSupport.java | 145 ++ .../jdbc/core/support/SqlLobValue.java | 215 +++ .../jdbc/core/support/package.html | 8 + .../jdbc/datasource/AbstractDataSource.java | 93 ++ .../AbstractDriverBasedDataSource.java | 161 ++ .../jdbc/datasource/ConnectionHandle.java | 44 + .../jdbc/datasource/ConnectionHolder.java | 205 +++ .../jdbc/datasource/ConnectionProxy.java | 47 + .../DataSourceTransactionManager.java | 363 +++++ .../jdbc/datasource/DataSourceUtils.java | 452 ++++++ .../jdbc/datasource/DelegatingDataSource.java | 120 ++ .../datasource/DriverManagerDataSource.java | 177 +++ .../IsolationLevelDataSourceAdapter.java | 165 ++ .../JdbcTransactionObjectSupport.java | 157 ++ .../LazyConnectionDataSourceProxy.java | 420 +++++ .../datasource/SimpleConnectionHandle.java | 63 + .../datasource/SimpleDriverDataSource.java | 142 ++ .../SingleConnectionDataSource.java | 344 +++++ .../jdbc/datasource/SmartDataSource.java | 52 + .../TransactionAwareDataSourceProxy.java | 246 +++ .../UserCredentialsDataSourceAdapter.java | 186 +++ .../WebSphereDataSourceAdapter.java | 177 +++ .../lookup/AbstractRoutingDataSource.java | 184 +++ .../lookup/BeanFactoryDataSourceLookup.java | 81 + .../datasource/lookup/DataSourceLookup.java | 42 + .../DataSourceLookupFailureException.java | 48 + .../IsolationLevelDataSourceRouter.java | 124 ++ .../lookup/JndiDataSourceLookup.java | 52 + .../lookup/MapDataSourceLookup.java | 117 ++ .../lookup/SingleDataSourceLookup.java | 49 + .../jdbc/datasource/lookup/package.html | 7 + .../jdbc/datasource/package.html | 9 + .../jdbc/object/BatchSqlUpdate.java | 247 +++ .../jdbc/object/MappingSqlQuery.java | 82 + .../object/MappingSqlQueryWithParameters.java | 119 ++ .../jdbc/object/RdbmsOperation.java | 473 ++++++ .../springframework/jdbc/object/SqlCall.java | 199 +++ .../jdbc/object/SqlFunction.java | 198 +++ .../jdbc/object/SqlOperation.java | 118 ++ .../springframework/jdbc/object/SqlQuery.java | 363 +++++ .../jdbc/object/SqlUpdate.java | 273 ++++ .../jdbc/object/StoredProcedure.java | 141 ++ .../jdbc/object/UpdatableSqlQuery.java | 102 ++ .../springframework/jdbc/object/package.html | 20 + .../org/springframework/jdbc/package.html | 23 + ...bstractFallbackSQLExceptionTranslator.java | 113 ++ .../CustomSQLErrorCodesTranslation.java | 70 + .../support/DatabaseMetaDataCallback.java | 46 + .../support/DatabaseStartupValidator.java | 154 ++ .../jdbc/support/GeneratedKeyHolder.java | 101 ++ .../jdbc/support/JdbcAccessor.java | 141 ++ .../jdbc/support/JdbcUtils.java | 463 ++++++ .../jdbc/support/KeyHolder.java | 76 + .../jdbc/support/MetaDataAccessException.java | 50 + .../SQLErrorCodeSQLExceptionTranslator.java | 388 +++++ .../jdbc/support/SQLErrorCodes.java | 179 +++ .../jdbc/support/SQLErrorCodesFactory.java | 253 +++ .../SQLExceptionSubclassTranslator.java | 104 ++ .../jdbc/support/SQLExceptionTranslator.java | 54 + .../SQLStateSQLExceptionTranslator.java | 134 ++ .../jdbc/support/SqlValue.java | 51 + .../AbstractColumnMaxValueIncrementer.java | 97 ++ .../AbstractDataFieldMaxValueIncrementer.java | 150 ++ .../AbstractSequenceMaxValueIncrementer.java | 94 ++ ...2MainframeSequenceMaxValueIncrementer.java | 53 + .../DB2SequenceMaxValueIncrementer.java | 53 + .../DataFieldMaxValueIncrementer.java | 55 + .../incrementer/DerbyMaxValueIncrementer.java | 170 ++ .../H2SequenceMaxValueIncrementer.java | 51 + .../incrementer/HsqlMaxValueIncrementer.java | 128 ++ .../HsqlSequenceMaxValueIncrementer.java | 56 + .../incrementer/MySQLMaxValueIncrementer.java | 131 ++ .../OracleSequenceMaxValueIncrementer.java | 52 + ...PostgreSQLSequenceMaxValueIncrementer.java | 51 + .../SqlServerMaxValueIncrementer.java | 117 ++ .../SybaseMaxValueIncrementer.java | 117 ++ .../jdbc/support/incrementer/package.html | 10 + .../jdbc/support/lob/AbstractLobHandler.java | 56 + .../jdbc/support/lob/DefaultLobHandler.java | 303 ++++ .../lob/JtaLobCreatorSynchronization.java | 65 + .../jdbc/support/lob/LobCreator.java | 139 ++ .../jdbc/support/lob/LobCreatorUtils.java | 84 + .../jdbc/support/lob/LobHandler.java | 210 +++ .../jdbc/support/lob/OracleLobHandler.java | 438 ++++++ .../jdbc/support/lob/PassThroughBlob.java | 97 ++ .../jdbc/support/lob/PassThroughClob.java | 107 ++ .../lob/SpringLobCreatorSynchronization.java | 80 + .../jdbc/support/lob/package.html | 11 + .../nativejdbc/C3P0NativeJdbcExtractor.java | 109 ++ .../CommonsDbcpNativeJdbcExtractor.java | 107 ++ .../nativejdbc/JBossNativeJdbcExtractor.java | 137 ++ .../nativejdbc/Jdbc4NativeJdbcExtractor.java | 63 + .../nativejdbc/NativeJdbcExtractor.java | 169 ++ .../NativeJdbcExtractorAdapter.java | 166 ++ .../nativejdbc/SimpleNativeJdbcExtractor.java | 141 ++ .../WebLogicNativeJdbcExtractor.java | 101 ++ .../WebSphereNativeJdbcExtractor.java | 105 ++ .../nativejdbc/XAPoolNativeJdbcExtractor.java | 75 + .../jdbc/support/nativejdbc/package.html | 10 + .../springframework/jdbc/support/package.html | 12 + .../rowset/ResultSetWrappingSqlRowSet.java | 693 +++++++++ .../ResultSetWrappingSqlRowSetMetaData.java | 203 +++ .../jdbc/support/rowset/SqlRowSet.java | 489 ++++++ .../support/rowset/SqlRowSetMetaData.java | 165 ++ .../jdbc/support/rowset/package.html | 8 + .../jdbc/support/sql-error-codes.xml | 210 +++ .../jdbc/support/xml/Jdbc4SqlXmlHandler.java | 162 ++ .../SqlXmlFeatureNotImplementedException.java | 46 + .../jdbc/support/xml/SqlXmlHandler.java | 221 +++ .../xml/SqlXmlObjectMappingHandler.java | 69 + .../jdbc/support/xml/SqlXmlValue.java | 33 + .../support/xml/XmlBinaryStreamProvider.java | 40 + .../xml/XmlCharacterStreamProvider.java | 40 + .../jdbc/support/xml/XmlResultProvider.java | 39 + .../jdbc/support/xml/package.html | 7 + .../src/main/java/overview.html | 7 + .../src/test/resources/log4j.xml | 28 + org.springframework.jdbc/template.mf | 22 + 216 files changed, 30681 insertions(+) create mode 100644 org.springframework.jdbc/build.xml create mode 100644 org.springframework.jdbc/ivy.xml create mode 100644 org.springframework.jdbc/pom.xml create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/BadSqlGrammarException.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/CannotGetJdbcConnectionException.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/IncorrectResultSetColumnCountException.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/InvalidResultSetAccessException.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/JdbcUpdateAffectedIncorrectNumberOfRowsException.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/LobRetrievalFailureException.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/SQLWarningException.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/UncategorizedSQLException.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/ArgPreparedStatementSetter.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/ArgTypePreparedStatementSetter.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/BatchPreparedStatementSetter.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/BeanPropertyRowMapper.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/CallableStatementCallback.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/CallableStatementCreator.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/CallableStatementCreatorFactory.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/ColumnMapRowMapper.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/ConnectionCallback.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/DisposableSqlTypeValue.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/InterruptibleBatchPreparedStatementSetter.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/JdbcOperations.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/ParameterDisposer.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/ParameterMapper.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/PreparedStatementCallback.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/PreparedStatementCreator.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/PreparedStatementCreatorFactory.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/PreparedStatementSetter.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/ResultSetExtractor.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/ResultSetSupportingSqlParameter.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/RowCallbackHandler.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/RowCountCallbackHandler.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/RowMapper.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/RowMapperResultSetExtractor.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/SingleColumnRowMapper.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/SqlInOutParameter.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/SqlOutParameter.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/SqlParameter.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/SqlParameterValue.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/SqlProvider.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/SqlReturnResultSet.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/SqlReturnType.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/SqlReturnUpdateCount.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/SqlRowSetResultSetExtractor.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/SqlTypeValue.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/StatementCallback.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/StatementCreatorUtils.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallMetaDataContext.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallMetaDataProvider.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallMetaDataProviderFactory.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallParameterMetaData.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/Db2CallMetaDataProvider.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/DerbyCallMetaDataProvider.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/GenericCallMetaDataProvider.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/GenericTableMetaDataProvider.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/HsqlTableMetaDataProvider.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/OracleCallMetaDataProvider.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/PostgresCallMetaDataProvider.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/PostgresTableMetaDataProvider.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/SqlServerCallMetaDataProvider.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/SybaseCallMetaDataProvider.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/TableMetaDataContext.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/TableMetaDataProvider.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/TableMetaDataProviderFactory.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/TableParameterMetaData.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/package.html create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/namedparam/AbstractSqlParameterSource.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/namedparam/BeanPropertySqlParameterSource.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/namedparam/MapSqlParameterSource.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcDaoSupport.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcOperations.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcTemplate.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterUtils.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/namedparam/ParsedSql.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/namedparam/SqlParameterSource.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/namedparam/SqlParameterSourceUtils.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/namedparam/package.html create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/package.html create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/simple/AbstractJdbcCall.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/simple/AbstractJdbcInsert.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/simple/ParameterizedBeanPropertyRowMapper.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/simple/ParameterizedRowMapper.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/simple/ParameterizedSingleColumnRowMapper.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcCall.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcCallOperations.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcDaoSupport.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcInsert.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcInsertOperations.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcOperations.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcTemplate.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/simple/package.html create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/support/AbstractInterruptibleBatchPreparedStatementSetter.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/support/AbstractLobCreatingPreparedStatementCallback.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/support/AbstractLobStreamingResultSetExtractor.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/support/AbstractSqlTypeValue.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/support/JdbcBeanDefinitionReader.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/support/JdbcDaoSupport.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/support/SqlLobValue.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/support/package.html create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/AbstractDataSource.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/AbstractDriverBasedDataSource.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/ConnectionHandle.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/ConnectionHolder.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/ConnectionProxy.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceTransactionManager.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/DelegatingDataSource.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/DriverManagerDataSource.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/IsolationLevelDataSourceAdapter.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/JdbcTransactionObjectSupport.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/LazyConnectionDataSourceProxy.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/SimpleConnectionHandle.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/SimpleDriverDataSource.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/SingleConnectionDataSource.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/SmartDataSource.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/TransactionAwareDataSourceProxy.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/UserCredentialsDataSourceAdapter.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/WebSphereDataSourceAdapter.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/AbstractRoutingDataSource.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/BeanFactoryDataSourceLookup.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/DataSourceLookup.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/DataSourceLookupFailureException.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/IsolationLevelDataSourceRouter.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/JndiDataSourceLookup.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/MapDataSourceLookup.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/SingleDataSourceLookup.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/package.html create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/package.html create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/object/BatchSqlUpdate.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/object/MappingSqlQuery.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/object/MappingSqlQueryWithParameters.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/object/RdbmsOperation.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/object/SqlCall.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/object/SqlFunction.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/object/SqlOperation.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/object/SqlQuery.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/object/SqlUpdate.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/object/StoredProcedure.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/object/UpdatableSqlQuery.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/object/package.html create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/package.html create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/AbstractFallbackSQLExceptionTranslator.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/CustomSQLErrorCodesTranslation.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/DatabaseMetaDataCallback.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/DatabaseStartupValidator.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/GeneratedKeyHolder.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/JdbcAccessor.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/JdbcUtils.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/KeyHolder.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/MetaDataAccessException.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodeSQLExceptionTranslator.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodes.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodesFactory.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/SQLExceptionSubclassTranslator.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/SQLExceptionTranslator.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/SQLStateSQLExceptionTranslator.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/SqlValue.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/incrementer/AbstractColumnMaxValueIncrementer.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/incrementer/AbstractDataFieldMaxValueIncrementer.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/incrementer/AbstractSequenceMaxValueIncrementer.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/incrementer/DB2MainframeSequenceMaxValueIncrementer.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/incrementer/DB2SequenceMaxValueIncrementer.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/incrementer/DataFieldMaxValueIncrementer.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/incrementer/DerbyMaxValueIncrementer.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/incrementer/H2SequenceMaxValueIncrementer.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/incrementer/HsqlMaxValueIncrementer.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/incrementer/HsqlSequenceMaxValueIncrementer.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/incrementer/MySQLMaxValueIncrementer.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/incrementer/OracleSequenceMaxValueIncrementer.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/incrementer/PostgreSQLSequenceMaxValueIncrementer.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/incrementer/SqlServerMaxValueIncrementer.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/incrementer/SybaseMaxValueIncrementer.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/incrementer/package.html create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/lob/AbstractLobHandler.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/lob/DefaultLobHandler.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/lob/JtaLobCreatorSynchronization.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/lob/LobCreator.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/lob/LobCreatorUtils.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/lob/LobHandler.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/lob/OracleLobHandler.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/lob/PassThroughBlob.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/lob/PassThroughClob.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/lob/SpringLobCreatorSynchronization.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/lob/package.html create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/nativejdbc/C3P0NativeJdbcExtractor.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/nativejdbc/CommonsDbcpNativeJdbcExtractor.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/nativejdbc/JBossNativeJdbcExtractor.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/nativejdbc/Jdbc4NativeJdbcExtractor.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/nativejdbc/NativeJdbcExtractor.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/nativejdbc/NativeJdbcExtractorAdapter.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/nativejdbc/SimpleNativeJdbcExtractor.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/nativejdbc/WebLogicNativeJdbcExtractor.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/nativejdbc/WebSphereNativeJdbcExtractor.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/nativejdbc/XAPoolNativeJdbcExtractor.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/nativejdbc/package.html create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/package.html create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/rowset/ResultSetWrappingSqlRowSet.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/rowset/ResultSetWrappingSqlRowSetMetaData.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/rowset/SqlRowSet.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/rowset/SqlRowSetMetaData.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/rowset/package.html create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/sql-error-codes.xml create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/xml/Jdbc4SqlXmlHandler.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/xml/SqlXmlFeatureNotImplementedException.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/xml/SqlXmlHandler.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/xml/SqlXmlObjectMappingHandler.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/xml/SqlXmlValue.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/xml/XmlBinaryStreamProvider.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/xml/XmlCharacterStreamProvider.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/xml/XmlResultProvider.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/xml/package.html create mode 100644 org.springframework.jdbc/src/main/java/overview.html create mode 100644 org.springframework.jdbc/src/test/resources/log4j.xml create mode 100644 org.springframework.jdbc/template.mf diff --git a/org.springframework.jdbc/build.xml b/org.springframework.jdbc/build.xml new file mode 100644 index 0000000000..c8b6069577 --- /dev/null +++ b/org.springframework.jdbc/build.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/org.springframework.jdbc/ivy.xml b/org.springframework.jdbc/ivy.xml new file mode 100644 index 0000000000..2aceb75ad3 --- /dev/null +++ b/org.springframework.jdbc/ivy.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/org.springframework.jdbc/pom.xml b/org.springframework.jdbc/pom.xml new file mode 100644 index 0000000000..b4cee14adc --- /dev/null +++ b/org.springframework.jdbc/pom.xml @@ -0,0 +1,59 @@ + + + 4.0.0 + org.springframework + org.springframework.core + jar + Spring Core Abstractions and Utilities + 3.0.0.M1 + + + com.springsource.repository.bundles.external + SpringSource Enterprise Bundle Repository - External Bundle Releases + http://repository.springsource.com/maven/bundles/external + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.5 + 1.5 + + + + + + + org.apache.commons + com.springsource.org.apache.commons.logging + 1.1.1 + + + org.apache.log4j + com.springsource.org.apache.log4j + 1.2.15 + true + + + org.apache.commons + com.springsource.org.apache.commons.collections + 3.2.0 + true + + + org.aspectj + com.springsource.org.aspectj.weaver + 1.6.2.RELEASE + true + + + org.objectweb.asm + com.springsource.org.objectweb.asm.commons + 2.2.3 + true + + + \ No newline at end of file diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/BadSqlGrammarException.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/BadSqlGrammarException.java new file mode 100644 index 0000000000..4e3389298b --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/BadSqlGrammarException.java @@ -0,0 +1,65 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc; + +import java.sql.SQLException; + +import org.springframework.dao.InvalidDataAccessResourceUsageException; + +/** + * Exception thrown when SQL specified is invalid. Such exceptions always have + * a java.sql.SQLException root cause. + * + *

It would be possible to have subclasses for no such table, no such column etc. + * A custom SQLExceptionTranslator could create such more specific exceptions, + * without affecting code using this class. + * + * @author Rod Johnson + * @see InvalidResultSetAccessException + */ +public class BadSqlGrammarException extends InvalidDataAccessResourceUsageException { + + private String sql; + + + /** + * Constructor for BadSqlGrammarException. + * @param task name of current task + * @param sql the offending SQL statement + * @param ex the root cause + */ + public BadSqlGrammarException(String task, String sql, SQLException ex) { + super(task + "; bad SQL grammar [" + sql + "]", ex); + this.sql = sql; + } + + + /** + * Return the wrapped SQLException. + */ + public SQLException getSQLException() { + return (SQLException) getCause(); + } + + /** + * Return the SQL that caused the problem. + */ + public String getSql() { + return this.sql; + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/CannotGetJdbcConnectionException.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/CannotGetJdbcConnectionException.java new file mode 100644 index 0000000000..186a4d88b2 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/CannotGetJdbcConnectionException.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc; + +import java.sql.SQLException; + +import org.springframework.dao.DataAccessResourceFailureException; + +/** + * Fatal exception thrown when we can't connect to an RDBMS using JDBC. + * + * @author Rod Johnson + */ +public class CannotGetJdbcConnectionException extends DataAccessResourceFailureException { + + /** + * Constructor for CannotGetJdbcConnectionException. + * @param msg the detail message + * @param ex SQLException root cause + */ + public CannotGetJdbcConnectionException(String msg, SQLException ex) { + super(msg, ex); + } + + /** + * Constructor for CannotGetJdbcConnectionException. + * @param msg the detail message + * @param ex ClassNotFoundException root cause + * @deprecated since Spring 2.5, in favor of throwing an + * IllegalStateException in case of the driver not being found + */ + public CannotGetJdbcConnectionException(String msg, ClassNotFoundException ex) { + super(msg, ex); + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/IncorrectResultSetColumnCountException.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/IncorrectResultSetColumnCountException.java new file mode 100644 index 0000000000..a11eee5b7c --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/IncorrectResultSetColumnCountException.java @@ -0,0 +1,74 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc; + +import org.springframework.dao.DataRetrievalFailureException; + +/** + * Data access exception thrown when a result set did not have the correct column count, + * for example when expecting a single column but getting 0 or more than 1 columns. + * + * @author Juergen Hoeller + * @since 2.0 + * @see org.springframework.dao.IncorrectResultSizeDataAccessException + */ +public class IncorrectResultSetColumnCountException extends DataRetrievalFailureException { + + private int expectedCount; + + private int actualCount; + + + /** + * Constructor for IncorrectResultSetColumnCountException. + * @param expectedCount the expected column count + * @param actualCount the actual column count + */ + public IncorrectResultSetColumnCountException(int expectedCount, int actualCount) { + super("Incorrect column count: expected " + expectedCount + ", actual " + actualCount); + this.expectedCount = expectedCount; + this.actualCount = actualCount; + } + + /** + * Constructor for IncorrectResultCountDataAccessException. + * @param msg the detail message + * @param expectedCount the expected column count + * @param actualCount the actual column count + */ + public IncorrectResultSetColumnCountException(String msg, int expectedCount, int actualCount) { + super(msg); + this.expectedCount = expectedCount; + this.actualCount = actualCount; + } + + + /** + * Return the expected column count. + */ + public int getExpectedCount() { + return this.expectedCount; + } + + /** + * Return the actual column count. + */ + public int getActualCount() { + return this.actualCount; + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/InvalidResultSetAccessException.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/InvalidResultSetAccessException.java new file mode 100644 index 0000000000..d62e1c1fa2 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/InvalidResultSetAccessException.java @@ -0,0 +1,75 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc; + +import java.sql.SQLException; + +import org.springframework.dao.InvalidDataAccessResourceUsageException; + +/** + * Exception thrown when a ResultSet has been accessed in an invalid fashion. + * Such exceptions always have a java.sql.SQLException root cause. + * + *

This typically happens when an invalid ResultSet column index or name + * has been specified. Also thrown by disconnected SqlRowSets. + * + * @author Juergen Hoeller + * @since 1.2 + * @see BadSqlGrammarException + * @see org.springframework.jdbc.support.rowset.SqlRowSet + */ +public class InvalidResultSetAccessException extends InvalidDataAccessResourceUsageException { + + private String sql; + + + /** + * Constructor for InvalidResultSetAccessException. + * @param task name of current task + * @param sql the offending SQL statement + * @param ex the root cause + */ + public InvalidResultSetAccessException(String task, String sql, SQLException ex) { + super(task + "; invalid ResultSet access for SQL [" + sql + "]", ex); + this.sql = sql; + } + + /** + * Constructor for InvalidResultSetAccessException. + * @param ex the root cause + */ + public InvalidResultSetAccessException(SQLException ex) { + super(ex.getMessage(), ex); + } + + + /** + * Return the wrapped SQLException. + */ + public SQLException getSQLException() { + return (SQLException) getCause(); + } + + /** + * Return the SQL that caused the problem. + * @return the offending SQL, if known + */ + public String getSql() { + return this.sql; + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/JdbcUpdateAffectedIncorrectNumberOfRowsException.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/JdbcUpdateAffectedIncorrectNumberOfRowsException.java new file mode 100644 index 0000000000..4c5592bdb7 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/JdbcUpdateAffectedIncorrectNumberOfRowsException.java @@ -0,0 +1,69 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed 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.springframework.jdbc; + +import org.springframework.dao.IncorrectUpdateSemanticsDataAccessException; + +/** + * Exception thrown when a JDBC update affects an unexpected number of rows. + * Typically we expect an update to affect a single row, meaning it's an + * error if it affects multiple rows. + * + * @author Rod Johnson + * @author Juergen Hoeller + */ +public class JdbcUpdateAffectedIncorrectNumberOfRowsException extends IncorrectUpdateSemanticsDataAccessException { + + /** Number of rows that should have been affected */ + private int expected; + + /** Number of rows that actually were affected */ + private int actual; + + + /** + * Constructor for JdbcUpdateAffectedIncorrectNumberOfRowsException. + * @param sql SQL we were tring to execute + * @param expected the expected number of rows affected + * @param actual the actual number of rows affected + */ + public JdbcUpdateAffectedIncorrectNumberOfRowsException(String sql, int expected, int actual) { + super("SQL update '" + sql + "' affected " + actual + " rows, not " + expected + " as expected"); + this.expected = expected; + this.actual = actual; + } + + + /** + * Return the number of rows that should have been affected. + */ + public int getExpectedRowsAffected() { + return this.expected; + } + + /** + * Return the number of rows that have actually been affected. + */ + public int getActualRowsAffected() { + return this.actual; + } + + public boolean wasDataUpdated() { + return (getActualRowsAffected() > 0); + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/LobRetrievalFailureException.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/LobRetrievalFailureException.java new file mode 100644 index 0000000000..f80ab4fbef --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/LobRetrievalFailureException.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2005 the original author or authors. + * + * Licensed 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.springframework.jdbc; + +import java.io.IOException; + +import org.springframework.dao.DataRetrievalFailureException; + +/** + * Exception to be thrown when a LOB could not be retrieved. + * + * @author Juergen Hoeller + * @since 1.0.2 + */ +public class LobRetrievalFailureException extends DataRetrievalFailureException { + + /** + * Constructor for LobRetrievalFailureException. + * @param msg the detail message + */ + public LobRetrievalFailureException(String msg) { + super(msg); + } + + /** + * Constructor for LobRetrievalFailureException. + * @param msg the detail message + * @param ex IOException root cause + */ + public LobRetrievalFailureException(String msg, IOException ex) { + super(msg, ex); + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/SQLWarningException.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/SQLWarningException.java new file mode 100644 index 0000000000..68a8d25c39 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/SQLWarningException.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed 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.springframework.jdbc; + +import java.sql.SQLWarning; + +import org.springframework.dao.UncategorizedDataAccessException; + +/** + * Exception thrown when we're not ignoring {@link java.sql.SQLWarning SQLWarnings}. + * + *

If a SQLWarning is reported, the operation completed, so we will need + * to explicitly roll it back if we're not happy when looking at the warning. + * We might choose to ignore (and log) the warning, or to wrap and throw it + * in the shape of this SQLWarningException instead. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see org.springframework.jdbc.core.JdbcTemplate#setIgnoreWarnings + */ +public class SQLWarningException extends UncategorizedDataAccessException { + + /** + * Constructor for SQLWarningException. + * @param msg the detail message + * @param ex the JDBC warning + */ + public SQLWarningException(String msg, SQLWarning ex) { + super(msg, ex); + } + + /** + * Return the underlying SQLWarning. + */ + public SQLWarning SQLWarning() { + return (SQLWarning) getCause(); + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/UncategorizedSQLException.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/UncategorizedSQLException.java new file mode 100644 index 0000000000..eccba39f5d --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/UncategorizedSQLException.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc; + +import java.sql.SQLException; + +import org.springframework.dao.UncategorizedDataAccessException; + +/** + * Exception thrown when we can't classify a SQLException into + * one of our generic data access exceptions. + * + * @author Rod Johnson + * @author Juergen Hoeller + */ +public class UncategorizedSQLException extends UncategorizedDataAccessException { + + /** SQL that led to the problem */ + private final String sql; + + + /** + * Constructor for UncategorizedSQLException. + * @param task name of current task + * @param sql the offending SQL statement + * @param ex the root cause + */ + public UncategorizedSQLException(String task, String sql, SQLException ex) { + super(task + "; uncategorized SQLException for SQL [" + sql + "]; SQL state [" + + ex.getSQLState() + "]; error code [" + ex.getErrorCode() + "]; " + ex.getMessage(), ex); + this.sql = sql; + } + + + /** + * Return the underlying SQLException. + */ + public SQLException getSQLException() { + return (SQLException) getCause(); + } + + /** + * Return the SQL that led to the problem. + */ + public String getSql() { + return this.sql; + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/ArgPreparedStatementSetter.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/ArgPreparedStatementSetter.java new file mode 100644 index 0000000000..e0d8c0ca87 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/ArgPreparedStatementSetter.java @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc.core; + +import java.sql.PreparedStatement; +import java.sql.SQLException; + +/** + * Simple adapter for PreparedStatementSetter that applies + * a given array of arguments. + * + * @author Juergen Hoeller + */ +class ArgPreparedStatementSetter implements PreparedStatementSetter, ParameterDisposer { + + private final Object[] args; + + + /** + * Create a new ArgPreparedStatementSetter for the given arguments. + * @param args the arguments to set + */ + public ArgPreparedStatementSetter(Object[] args) { + this.args = args; + } + + + public void setValues(PreparedStatement ps) throws SQLException { + if (this.args != null) { + for (int i = 0; i < this.args.length; i++) { + Object arg = this.args[i]; + if (arg instanceof SqlParameterValue) { + SqlParameterValue paramValue = (SqlParameterValue) arg; + StatementCreatorUtils.setParameterValue(ps, i + 1, paramValue, paramValue.getValue()); + } + else { + StatementCreatorUtils.setParameterValue(ps, i + 1, SqlTypeValue.TYPE_UNKNOWN, arg); + } + } + } + } + + public void cleanupParameters() { + StatementCreatorUtils.cleanupParameters(this.args); + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/ArgTypePreparedStatementSetter.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/ArgTypePreparedStatementSetter.java new file mode 100644 index 0000000000..f8cc54a3f3 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/ArgTypePreparedStatementSetter.java @@ -0,0 +1,87 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc.core; + +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Types; +import java.util.Collection; +import java.util.Iterator; + +import org.springframework.dao.InvalidDataAccessApiUsageException; + +/** + * Simple adapter for PreparedStatementSetter that applies + * given arrays of arguments and JDBC argument types. + * + * @author Juergen Hoeller + */ +class ArgTypePreparedStatementSetter implements PreparedStatementSetter, ParameterDisposer { + + private final Object[] args; + + private final int[] argTypes; + + + /** + * Create a new ArgTypePreparedStatementSetter for the given arguments. + * @param args the arguments to set + * @param argTypes the corresponding SQL types of the arguments + */ + public ArgTypePreparedStatementSetter(Object[] args, int[] argTypes) { + if ((args != null && argTypes == null) || (args == null && argTypes != null) || + (args != null && args.length != argTypes.length)) { + throw new InvalidDataAccessApiUsageException("args and argTypes parameters must match"); + } + this.args = args; + this.argTypes = argTypes; + } + + + public void setValues(PreparedStatement ps) throws SQLException { + int argIndx = 1; + if (this.args != null) { + for (int i = 0; i < this.args.length; i++) { + Object arg = this.args[i]; + if (arg instanceof Collection && this.argTypes[i] != Types.ARRAY) { + Collection entries = (Collection) arg; + for (Iterator it = entries.iterator(); it.hasNext();) { + Object entry = it.next(); + if (entry instanceof Object[]) { + Object[] valueArray = ((Object[])entry); + for (int k = 0; k < valueArray.length; k++) { + Object argValue = valueArray[k]; + StatementCreatorUtils.setParameterValue(ps, argIndx++, this.argTypes[i], argValue); + } + } + else { + StatementCreatorUtils.setParameterValue(ps, argIndx++, this.argTypes[i], entry); + } + } + } + else { + StatementCreatorUtils.setParameterValue(ps, argIndx++, this.argTypes[i], arg); + } + } + } + } + + public void cleanupParameters() { + StatementCreatorUtils.cleanupParameters(this.args); + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/BatchPreparedStatementSetter.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/BatchPreparedStatementSetter.java new file mode 100644 index 0000000000..8a8e523a5b --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/BatchPreparedStatementSetter.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc.core; + +import java.sql.PreparedStatement; +import java.sql.SQLException; + +/** + * Batch update callback interface used by the {@link JdbcTemplate} class. + * + *

This interface sets values on a {@link java.sql.PreparedStatement} provided + * by the JdbcTemplate class, for each of a number of updates in a batch using the + * same SQL. Implementations are responsible for setting any necessary parameters. + * SQL with placeholders will already have been supplied. + * + *

Implementations do not need to concern themselves with SQLExceptions + * that may be thrown from operations they attempt. The JdbcTemplate class will + * catch and handle SQLExceptions appropriately. + * + * @author Rod Johnson + * @since March 2, 2003 + * @see JdbcTemplate#batchUpdate(String, BatchPreparedStatementSetter) + * @see InterruptibleBatchPreparedStatementSetter + */ +public interface BatchPreparedStatementSetter { + + /** + * Set parameter values on the given PreparedStatement. + * @param ps the PreparedStatement to invoke setter methods on + * @param i index of the statement we're issuing in the batch, starting from 0 + * @throws SQLException if a SQLException is encountered + * (i.e. there is no need to catch SQLException) + */ + void setValues(PreparedStatement ps, int i) throws SQLException; + + /** + * Return the size of the batch. + * @return the number of statements in the batch + */ + int getBatchSize(); + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/BeanPropertyRowMapper.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/BeanPropertyRowMapper.java new file mode 100644 index 0000000000..88b17e7a3e --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/BeanPropertyRowMapper.java @@ -0,0 +1,270 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.core; + +import java.beans.PropertyDescriptor; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.BeanWrapper; +import org.springframework.beans.NotWritablePropertyException; +import org.springframework.beans.PropertyAccessorFactory; +import org.springframework.dao.DataRetrievalFailureException; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.jdbc.support.JdbcUtils; +import org.springframework.util.Assert; + +/** + * {@link RowMapper} implementation that converts a row into a new instance + * of the specified mapped target class. The mapped target class must be a + * top-level class and it must have a default or no-arg constructor. + * + *

Column values are mapped based on matching the column name as obtained from result set + * metadata to public setters for the corresponding properties. The names are matched either + * directly or by transforming a name separating the parts with underscores to the same name + * using "camel" case. + * + *

Mapping is provided for fields in the target class for many common types, e.g.: + * String, boolean, Boolean, byte, Byte, short, Short, int, Integer, long, Long, + * float, Float, double, Double, BigDecimal, java.util.Date, etc. + * + *

To facilitate mapping between columns and fields that don't have matching names, + * try using column aliases in the SQL statement like "select fname as first_name from customer". + * + *

Please note that this class is designed to provide convenience rather than high performance. + * For best performance consider using a custom RowMapper. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @since 2.5 + */ +public class BeanPropertyRowMapper implements RowMapper { + + /** Logger available to subclasses */ + protected final Log logger = LogFactory.getLog(getClass()); + + /** The class we are mapping to */ + private Class mappedClass; + + /** Whether we're strictly validating */ + private boolean checkFullyPopulated = false; + + /** Map of the fields we provide mapping for */ + private Map mappedFields; + + /** Set of bean properties we provide mapping for */ + private Set mappedProperties; + + + /** + * Create a new BeanPropertyRowMapper for bean-style configuration. + * @see #setMappedClass + * @see #setCheckFullyPopulated + */ + public BeanPropertyRowMapper() { + } + + /** + * Create a new BeanPropertyRowMapper, accepting unpopulated properties + * in the target bean. + * @param mappedClass the class that each row should be mapped to + */ + public BeanPropertyRowMapper(Class mappedClass) { + initialize(mappedClass); + } + + /** + * Create a new BeanPropertyRowMapper. + * @param mappedClass the class that each row should be mapped to + * @param checkFullyPopulated whether we're strictly validating that + * all bean properties have been mapped from corresponding database fields + */ + public BeanPropertyRowMapper(Class mappedClass, boolean checkFullyPopulated) { + initialize(mappedClass); + this.checkFullyPopulated = checkFullyPopulated; + } + + + /** + * Set the class that each row should be mapped to. + */ + public void setMappedClass(Class mappedClass) { + if (this.mappedClass == null) { + initialize(mappedClass); + } + else { + if (!this.mappedClass.equals(mappedClass)) { + throw new InvalidDataAccessApiUsageException("The mapped class can not be reassigned to map to " + + mappedClass + " since it is already providing mapping for " + this.mappedClass); + } + } + } + + /** + * Initialize the mapping metadata for the given class. + * @param mappedClass the mapped class. + */ + protected void initialize(Class mappedClass) { + this.mappedClass = mappedClass; + this.mappedFields = new HashMap(); + this.mappedProperties = new HashSet(); + PropertyDescriptor[] pds = BeanUtils.getPropertyDescriptors(mappedClass); + for (int i = 0; i < pds.length; i++) { + PropertyDescriptor pd = pds[i]; + if (pd.getWriteMethod() != null) { + this.mappedFields.put(pd.getName().toLowerCase(), pd); + String underscoredName = underscoreName(pd.getName()); + if (!pd.getName().toLowerCase().equals(underscoredName)) { + this.mappedFields.put(underscoredName, pd); + } + this.mappedProperties.add(pd.getName()); + } + } + } + + /** + * Convert a name in camelCase to an underscored name in lower case. + * Any upper case letters are converted to lower case with a preceding underscore. + * @param name the string containing original name + * @return the converted name + */ + private String underscoreName(String name) { + StringBuffer result = new StringBuffer(); + if (name != null && name.length() > 0) { + result.append(name.substring(0, 1).toLowerCase()); + for (int i = 1; i < name.length(); i++) { + String s = name.substring(i, i + 1); + if (s.equals(s.toUpperCase())) { + result.append("_"); + result.append(s.toLowerCase()); + } + else { + result.append(s); + } + } + } + return result.toString(); + } + + /** + * Get the class that we are mapping to. + */ + public final Class getMappedClass() { + return this.mappedClass; + } + + /** + * Set whether we're strictly validating that all bean properties have been + * mapped from corresponding database fields. + *

Default is false, accepting unpopulated properties in the + * target bean. + */ + public void setCheckFullyPopulated(boolean checkFullyPopulated) { + this.checkFullyPopulated = checkFullyPopulated; + } + + /** + * Return whether we're strictly validating that all bean properties have been + * mapped from corresponding database fields. + */ + public boolean isCheckFullyPopulated() { + return this.checkFullyPopulated; + } + + + /** + * Extract the values for all columns in the current row. + *

Utilizes public setters and result set metadata. + * @see java.sql.ResultSetMetaData + */ + public Object mapRow(ResultSet rs, int rowNumber) throws SQLException { + Assert.state(this.mappedClass != null, "Mapped class was not specified"); + Object mappedObject = BeanUtils.instantiateClass(this.mappedClass); + BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(mappedObject); + initBeanWrapper(bw); + + ResultSetMetaData rsmd = rs.getMetaData(); + int columnCount = rsmd.getColumnCount(); + Set populatedProperties = (isCheckFullyPopulated() ? new HashSet() : null); + + for (int index = 1; index <= columnCount; index++) { + String column = JdbcUtils.lookupColumnName(rsmd, index).toLowerCase(); + PropertyDescriptor pd = (PropertyDescriptor) this.mappedFields.get(column); + if (pd != null) { + try { + Object value = getColumnValue(rs, index, pd); + if (logger.isDebugEnabled() && rowNumber == 0) { + logger.debug("Mapping column '" + column + "' to property '" + + pd.getName() + "' of type " + pd.getPropertyType()); + } + bw.setPropertyValue(pd.getName(), value); + if (populatedProperties != null) { + populatedProperties.add(pd.getName()); + } + } + catch (NotWritablePropertyException ex) { + throw new DataRetrievalFailureException( + "Unable to map column " + column + " to property " + pd.getName(), ex); + } + } + } + + if (populatedProperties != null && !populatedProperties.equals(this.mappedProperties)) { + throw new InvalidDataAccessApiUsageException("Given ResultSet does not contain all fields " + + "necessary to populate object of class [" + this.mappedClass + "]: " + this.mappedProperties); + } + + return mappedObject; + } + + /** + * Initialize the given BeanWrapper to be used for row mapping. + * To be called for each row. + *

The default implementation is empty. Can be overridden in subclasses. + * @param bw the BeanWrapper to initialize + */ + protected void initBeanWrapper(BeanWrapper bw) { + } + + /** + * Retrieve a JDBC object value for the specified column. + *

The default implementation calls + * {@link JdbcUtils#getResultSetValue(java.sql.ResultSet, int, Class)}. + * Subclasses may override this to check specific value types upfront, + * or to post-process values return from getResultSetValue. + * @param rs is the ResultSet holding the data + * @param index is the column index + * @param pd the bean property that each result object is expected to match + * (or null if none specified) + * @return the Object value + * @throws SQLException in case of extraction failure + * @see org.springframework.jdbc.support.JdbcUtils#getResultSetValue(java.sql.ResultSet, int, Class) + */ + protected Object getColumnValue(ResultSet rs, int index, PropertyDescriptor pd) throws SQLException { + return JdbcUtils.getResultSetValue(rs, index, pd.getPropertyType()); + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/CallableStatementCallback.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/CallableStatementCallback.java new file mode 100644 index 0000000000..13dedcf35f --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/CallableStatementCallback.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2005 the original author or authors. + * + * Licensed 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.springframework.jdbc.core; + +import java.sql.CallableStatement; +import java.sql.SQLException; + +import org.springframework.dao.DataAccessException; + +/** + * Generic callback interface for code that operates on a CallableStatement. + * Allows to execute any number of operations on a single CallableStatement, + * for example a single execute call or repeated execute calls with varying + * parameters. + * + *

Used internally by JdbcTemplate, but also useful for application code. + * Note that the passed-in CallableStatement can have been created by the + * framework or by a custom CallableStatementCreator. However, the latter is + * hardly ever necessary, as most custom callback actions will perform updates + * in which case a standard CallableStatement is fine. Custom actions will + * always set parameter values themselves, so that CallableStatementCreator + * capability is not needed either. + * + * @author Juergen Hoeller + * @since 16.03.2004 + * @see JdbcTemplate#execute(String, CallableStatementCallback) + * @see JdbcTemplate#execute(CallableStatementCreator, CallableStatementCallback) + */ +public interface CallableStatementCallback { + + /** + * Gets called by JdbcTemplate.execute with an active JDBC + * CallableStatement. Does not need to care about closing the Statement + * or the Connection, or about handling transactions: this will all be + * handled by Spring's JdbcTemplate. + * + *

NOTE: Any ResultSets opened should be closed in finally blocks + * within the callback implementation. Spring will close the Statement + * object after the callback returned, but this does not necessarily imply + * that the ResultSet resources will be closed: the Statement objects might + * get pooled by the connection pool, with close calls only + * returning the object to the pool but not physically closing the resources. + * + *

If called without a thread-bound JDBC transaction (initiated by + * DataSourceTransactionManager), the code will simply get executed on the + * JDBC connection with its transactional semantics. If JdbcTemplate is + * configured to use a JTA-aware DataSource, the JDBC connection and thus + * the callback code will be transactional if a JTA transaction is active. + * + *

Allows for returning a result object created within the callback, i.e. + * a domain object or a collection of domain objects. A thrown RuntimeException + * is treated as application exception: it gets propagated to the caller of + * the template. + * + * @param cs active JDBC CallableStatement + * @return a result object, or null if none + * @throws SQLException if thrown by a JDBC method, to be auto-converted + * into a DataAccessException by a SQLExceptionTranslator + * @throws DataAccessException in case of custom exceptions + */ + Object doInCallableStatement(CallableStatement cs) throws SQLException, DataAccessException; + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/CallableStatementCreator.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/CallableStatementCreator.java new file mode 100644 index 0000000000..8a42702394 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/CallableStatementCreator.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2005 the original author or authors. + * + * Licensed 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.springframework.jdbc.core; + +import java.sql.CallableStatement; +import java.sql.Connection; +import java.sql.SQLException; + +/** + * One of the three central callback interfaces used by the JdbcTemplate class. + * This interface creates a CallableStatement given a connection, provided + * by the JdbcTemplate class. Implementations are responsible for providing + * SQL and any necessary parameters. + * + *

Implementations do not need to concern themselves with + * SQLExceptions that may be thrown from operations they attempt. + * The JdbcTemplate class will catch and handle SQLExceptions appropriately. + * + *

A PreparedStatementCreator should also implement the SqlProvider interface + * if it is able to provide the SQL it uses for PreparedStatement creation. + * This allows for better contextual information in case of exceptions. + * + * @author Rod Johnson + * @author Thomas Risberg + * @see JdbcTemplate#execute(CallableStatementCreator, CallableStatementCallback) + * @see JdbcTemplate#call + * @see SqlProvider + */ +public interface CallableStatementCreator { + + /** + * Create a callable statement in this connection. Allows implementations to use + * CallableStatements. + * @param con Connection to use to create statement + * @return a callable statement + * @throws SQLException there is no need to catch SQLExceptions + * that may be thrown in the implementation of this method. + * The JdbcTemplate class will handle them. + */ + CallableStatement createCallableStatement(Connection con) throws SQLException; + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/CallableStatementCreatorFactory.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/CallableStatementCreatorFactory.java new file mode 100644 index 0000000000..941bb81c64 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/CallableStatementCreatorFactory.java @@ -0,0 +1,239 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.core; + +import java.sql.CallableStatement; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.jdbc.support.nativejdbc.NativeJdbcExtractor; + +/** + * Helper class that efficiently creates multiple {@link CallableStatementCreator} + * objects with different parameters based on a SQL statement and a single + * set of parameter declarations. + * + * @author Rod Johnson + * @author Thomas Risberg + * @author Juergen Hoeller + */ +public class CallableStatementCreatorFactory { + + /** The SQL call string, which won't change when the parameters change. */ + private final String callString; + + /** List of SqlParameter objects. May not be null. */ + private final List declaredParameters; + + private int resultSetType = ResultSet.TYPE_FORWARD_ONLY; + + private boolean updatableResults = false; + + private NativeJdbcExtractor nativeJdbcExtractor; + + + /** + * Create a new factory. Will need to add parameters via the + * {@link #addParameter} method or have no parameters. + */ + public CallableStatementCreatorFactory(String callString) { + this.callString = callString; + this.declaredParameters = new LinkedList(); + } + + /** + * Create a new factory with the given SQL and the given parameters. + * @param callString the SQL call string + * @param declaredParameters list of {@link SqlParameter} objects + */ + public CallableStatementCreatorFactory(String callString, List declaredParameters) { + this.callString = callString; + this.declaredParameters = declaredParameters; + } + + + /** + * Add a new declared parameter. + *

Order of parameter addition is significant. + * @param param the parameter to add to the list of declared parameters + */ + public void addParameter(SqlParameter param) { + this.declaredParameters.add(param); + } + + /** + * Set whether to use prepared statements that return a specific type of ResultSet. + * specific type of ResultSet. + * @param resultSetType the ResultSet type + * @see java.sql.ResultSet#TYPE_FORWARD_ONLY + * @see java.sql.ResultSet#TYPE_SCROLL_INSENSITIVE + * @see java.sql.ResultSet#TYPE_SCROLL_SENSITIVE + */ + public void setResultSetType(int resultSetType) { + this.resultSetType = resultSetType; + } + + /** + * Set whether to use prepared statements capable of returning updatable ResultSets. + */ + public void setUpdatableResults(boolean updatableResults) { + this.updatableResults = updatableResults; + } + + /** + * Specify the NativeJdbcExtractor to use for unwrapping CallableStatements, if any. + */ + public void setNativeJdbcExtractor(NativeJdbcExtractor nativeJdbcExtractor) { + this.nativeJdbcExtractor = nativeJdbcExtractor; + } + + + /** + * Return a new CallableStatementCreator instance given this parameters. + * @param params list of parameters (may be null) + */ + public CallableStatementCreator newCallableStatementCreator(Map params) { + return new CallableStatementCreatorImpl(params != null ? params : new HashMap()); + } + + /** + * Return a new CallableStatementCreator instance given this parameter mapper. + * @param inParamMapper ParameterMapper implementation that will return a Map of parameters + */ + public CallableStatementCreator newCallableStatementCreator(ParameterMapper inParamMapper) { + return new CallableStatementCreatorImpl(inParamMapper); + } + + + /** + * CallableStatementCreator implementation returned by this class. + */ + private class CallableStatementCreatorImpl implements CallableStatementCreator, SqlProvider, ParameterDisposer { + + private ParameterMapper inParameterMapper; + + private Map inParameters; + + /** + * Create a new CallableStatementCreatorImpl. + * @param inParamMapper ParameterMapper implementation for mapping input parameters + */ + public CallableStatementCreatorImpl(ParameterMapper inParamMapper) { + this.inParameterMapper = inParamMapper; + } + + /** + * Create a new CallableStatementCreatorImpl. + * @param inParams list of SqlParameter objects + */ + public CallableStatementCreatorImpl(Map inParams) { + this.inParameters = inParams; + } + + public CallableStatement createCallableStatement(Connection con) throws SQLException { + // If we were given a ParameterMapper, we must let the mapper do its thing to create the Map. + if (this.inParameterMapper != null) { + this.inParameters = this.inParameterMapper.createMap(con); + } + else { + if (this.inParameters == null) { + throw new InvalidDataAccessApiUsageException( + "A ParameterMapper or a Map of parameters must be provided"); + } + } + + CallableStatement cs = null; + if (resultSetType == ResultSet.TYPE_FORWARD_ONLY && !updatableResults) { + cs = con.prepareCall(callString); + } + else { + cs = con.prepareCall(callString, resultSetType, + updatableResults ? ResultSet.CONCUR_UPDATABLE : ResultSet.CONCUR_READ_ONLY); + } + + // Determine CallabeStatement to pass to custom types. + CallableStatement csToUse = cs; + if (nativeJdbcExtractor != null) { + csToUse = nativeJdbcExtractor.getNativeCallableStatement(cs); + } + + int sqlColIndx = 1; + for (int i = 0; i < declaredParameters.size(); i++) { + SqlParameter declaredParam = (SqlParameter) declaredParameters.get(i); + if (!declaredParam.isResultsParameter()) { + // So, it's a call parameter - part of the call string. + // Get the value - it may still be null. + Object inValue = this.inParameters.get(declaredParam.getName()); + if (declaredParam instanceof ResultSetSupportingSqlParameter) { + // It's an output parameter: SqlReturnResultSet parameters already excluded. + // It need not (but may be) supplied by the caller. + if (declaredParam instanceof SqlOutParameter) { + if (declaredParam.getTypeName() != null) { + cs.registerOutParameter(sqlColIndx, declaredParam.getSqlType(), declaredParam.getTypeName()); + } + else { + if (declaredParam.getScale() != null) { + cs.registerOutParameter(sqlColIndx, declaredParam.getSqlType(), declaredParam.getScale().intValue()); + } + else { + cs.registerOutParameter(sqlColIndx, declaredParam.getSqlType()); + } + } + if (declaredParam.isInputValueProvided()) { + StatementCreatorUtils.setParameterValue(csToUse, sqlColIndx, declaredParam, inValue); + } + } + } + else { + // It's an input parameter; must be supplied by the caller. + if (!this.inParameters.containsKey(declaredParam.getName())) { + throw new InvalidDataAccessApiUsageException( + "Required input parameter '" + declaredParam.getName() + "' is missing"); + } + StatementCreatorUtils.setParameterValue(csToUse, sqlColIndx, declaredParam, inValue); + } + sqlColIndx++; + } + } + + return cs; + } + + public String getSql() { + return callString; + } + + public void cleanupParameters() { + if (this.inParameters != null) { + StatementCreatorUtils.cleanupParameters(this.inParameters.values()); + } + } + + public String toString() { + StringBuffer buf = new StringBuffer("CallableStatementCreatorFactory.CallableStatementCreatorImpl: sql=["); + buf.append(callString).append("]; parameters=").append(this.inParameters); + return buf.toString(); + } + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/ColumnMapRowMapper.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/ColumnMapRowMapper.java new file mode 100644 index 0000000000..7f4b7ff4ec --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/ColumnMapRowMapper.java @@ -0,0 +1,99 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc.core; + +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.util.Map; + +import org.springframework.core.CollectionFactory; +import org.springframework.jdbc.support.JdbcUtils; + +/** + * {@link RowMapper} implementation that creates a java.util.Map + * for each row, representing all columns as key-value pairs: one + * entry for each column, with the column name as key. + * + *

The Map implementation to use and the key to use for each column + * in the column Map can be customized through overriding + * {@link #createColumnMap} and {@link #getColumnKey}, respectively. + * + *

Note: By default, ColumnMapRowMapper will try to build a linked Map + * with case-insensitive keys, to preserve column order as well as allow any + * casing to be used for column names. This requires Commons Collections on the + * classpath (which will be autodetected). Else, the fallback is a standard linked + * HashMap, which will still preserve column order but requires the application + * to specify the column names in the same casing as exposed by the driver. + * + * @author Juergen Hoeller + * @since 1.2 + * @see JdbcTemplate#queryForList(String) + * @see JdbcTemplate#queryForMap(String) + */ +public class ColumnMapRowMapper implements RowMapper { + + public Object mapRow(ResultSet rs, int rowNum) throws SQLException { + ResultSetMetaData rsmd = rs.getMetaData(); + int columnCount = rsmd.getColumnCount(); + Map mapOfColValues = createColumnMap(columnCount); + for (int i = 1; i <= columnCount; i++) { + String key = getColumnKey(JdbcUtils.lookupColumnName(rsmd, i)); + Object obj = getColumnValue(rs, i); + mapOfColValues.put(key, obj); + } + return mapOfColValues; + } + + /** + * Create a Map instance to be used as column map. + *

By default, a linked case-insensitive Map will be created if possible, + * else a plain HashMap (see Spring's CollectionFactory). + * @param columnCount the column count, to be used as initial + * capacity for the Map + * @return the new Map instance + * @see org.springframework.core.CollectionFactory#createLinkedCaseInsensitiveMapIfPossible + */ + protected Map createColumnMap(int columnCount) { + return CollectionFactory.createLinkedCaseInsensitiveMapIfPossible(columnCount); + } + + /** + * Determine the key to use for the given column in the column Map. + * @param columnName the column name as returned by the ResultSet + * @return the column key to use + * @see java.sql.ResultSetMetaData#getColumnName + */ + protected String getColumnKey(String columnName) { + return columnName; + } + + /** + * Retrieve a JDBC object value for the specified column. + *

The default implementation uses the getObject method. + * Additionally, this implementation includes a "hack" to get around Oracle + * returning a non standard object for their TIMESTAMP datatype. + * @param rs is the ResultSet holding the data + * @param index is the column index + * @return the Object returned + * @see org.springframework.jdbc.support.JdbcUtils#getResultSetValue + */ + protected Object getColumnValue(ResultSet rs, int index) throws SQLException { + return JdbcUtils.getResultSetValue(rs, index); + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/ConnectionCallback.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/ConnectionCallback.java new file mode 100644 index 0000000000..4986887b4a --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/ConnectionCallback.java @@ -0,0 +1,69 @@ +/* + * Copyright 2002-2005 the original author or authors. + * + * Licensed 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.springframework.jdbc.core; + +import java.sql.Connection; +import java.sql.SQLException; + +import org.springframework.dao.DataAccessException; + +/** + * Generic callback interface for code that operates on a JDBC Connection. + * Allows to execute any number of operations on a single Connection, + * using any type and number of Statements. + * + *

This is particularly useful for delegating to existing data access code + * that expects a Connection to work on and throws SQLException. For newly + * written code, it is strongly recommended to use JdbcTemplate's more specific + * operations, for example a query or update variant. + * + * @author Juergen Hoeller + * @since 1.1.3 + * @see JdbcTemplate#execute(ConnectionCallback) + * @see JdbcTemplate#query + * @see JdbcTemplate#update + */ +public interface ConnectionCallback { + + /** + * Gets called by JdbcTemplate.execute with an active JDBC + * Connection. Does not need to care about activating or closing the + * Connection, or handling transactions. + * + *

If called without a thread-bound JDBC transaction (initiated by + * DataSourceTransactionManager), the code will simply get executed on the + * JDBC connection with its transactional semantics. If JdbcTemplate is + * configured to use a JTA-aware DataSource, the JDBC Connection and thus + * the callback code will be transactional if a JTA transaction is active. + * + *

Allows for returning a result object created within the callback, i.e. + * a domain object or a collection of domain objects. Note that there's special + * support for single step actions: see JdbcTemplate.queryForObject + * etc. A thrown RuntimeException is treated as application exception: + * it gets propagated to the caller of the template. + * + * @param con active JDBC Connection + * @return a result object, or null if none + * @throws SQLException if thrown by a JDBC method, to be auto-converted + * to a DataAccessException by a SQLExceptionTranslator + * @throws DataAccessException in case of custom exceptions + * @see JdbcTemplate#queryForObject(String, Class) + * @see JdbcTemplate#queryForRowSet(String) + */ + Object doInConnection(Connection con) throws SQLException, DataAccessException; + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/DisposableSqlTypeValue.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/DisposableSqlTypeValue.java new file mode 100644 index 0000000000..b813a4eb8e --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/DisposableSqlTypeValue.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.core; + +/** + * Subinterface of {@link SqlTypeValue} that adds a cleanup callback, + * to be invoked after the value has been set and the corresponding + * statement has been executed. + * + * @author Juergen Hoeller + * @since 1.1 + * @see org.springframework.jdbc.core.support.SqlLobValue + */ +public interface DisposableSqlTypeValue extends SqlTypeValue { + + /** + * Clean up resources held by this type value, + * for example the LobCreator in case of a SqlLobValue. + * @see org.springframework.jdbc.core.support.SqlLobValue#cleanup() + * @see org.springframework.jdbc.support.SqlValue#cleanup() + */ + void cleanup(); + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/InterruptibleBatchPreparedStatementSetter.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/InterruptibleBatchPreparedStatementSetter.java new file mode 100644 index 0000000000..08999f6ea4 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/InterruptibleBatchPreparedStatementSetter.java @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc.core; + +/** + * Extension of the {@link BatchPreparedStatementSetter} interface, + * adding a batch exhaustion check. + * + *

This interface allows you to signal the end of a batch rather than + * having to determine the exact batch size upfront. Batch size is still + * being honored but it is now the maximum size of the batch. + * + *

The {@link #isBatchExhausted} method is called after each call to + * {@link #setValues} to determine whether there were some values added, + * or if the batch was determined to be complete and no additional values + * were provided during the last call to setValues. + * + *

Consider extending the + * {@link org.springframework.jdbc.core.support.AbstractInterruptibleBatchPreparedStatementSetter} + * base class instead of implementing this interface directly, using a single + * setValuesIfAvailable callback method that checks for available + * values and sets them, returning whether values have actually been provided. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @since 2.0 + * @see JdbcTemplate#batchUpdate(String, BatchPreparedStatementSetter) + * @see org.springframework.jdbc.core.support.AbstractInterruptibleBatchPreparedStatementSetter + */ +public interface InterruptibleBatchPreparedStatementSetter extends BatchPreparedStatementSetter { + + /** + * Return whether the batch is complete, that is, whether there were no + * additional values added during the last setValues call. + *

NOTE: If this method returns true, any parameters + * that might have been set during the last setValues call will + * be ignored! Make sure that you set a corresponding internal flag if you + * detect exhaustion at the beginning of your setValues + * implementation, letting this method return true based on the flag. + * @param i index of the statement we're issuing in the batch, starting from 0 + * @return whether the batch is already exhausted + * @see #setValues + * @see org.springframework.jdbc.core.support.AbstractInterruptibleBatchPreparedStatementSetter#setValuesIfAvailable + */ + boolean isBatchExhausted(int i); + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/JdbcOperations.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/JdbcOperations.java new file mode 100644 index 0000000000..6e64330cdd --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/JdbcOperations.java @@ -0,0 +1,936 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc.core; + +import java.util.List; +import java.util.Map; + +import org.springframework.dao.DataAccessException; +import org.springframework.dao.IncorrectResultSizeDataAccessException; +import org.springframework.jdbc.support.KeyHolder; +import org.springframework.jdbc.support.rowset.SqlRowSet; + +/** + * Interface specifying a basic set of JDBC operations. + * Implemented by {@link JdbcTemplate}. Not often used directly, but a useful + * option to enhance testability, as it can easily be mocked or stubbed. + * + *

Alternatively, the standard JDBC infrastructure can be mocked. + * However, mocking this interface constitutes significantly less work. + * As an alternative to a mock objects approach to testing data access code, + * consider the powerful integration testing support provided in the + * org.springframework.test package, shipped in + * spring-mock.jar. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see JdbcTemplate + */ +public interface JdbcOperations { + + //------------------------------------------------------------------------- + // Methods dealing with a plain java.sql.Connection + //------------------------------------------------------------------------- + + /** + * Execute a JDBC data access operation, implemented as callback action + * working on a JDBC Connection. This allows for implementing arbitrary + * data access operations, within Spring's managed JDBC environment: + * that is, participating in Spring-managed transactions and converting + * JDBC SQLExceptions into Spring's DataAccessException hierarchy. + *

The callback action can return a result object, for example a + * domain object or a collection of domain objects. + * @param action the callback object that specifies the action + * @return a result object returned by the action, or null + * @throws DataAccessException if there is any problem + */ + Object execute(ConnectionCallback action) throws DataAccessException; + + + //------------------------------------------------------------------------- + // Methods dealing with static SQL (java.sql.Statement) + //------------------------------------------------------------------------- + + /** + * Execute a JDBC data access operation, implemented as callback action + * working on a JDBC Statement. This allows for implementing arbitrary data + * access operations on a single Statement, within Spring's managed JDBC + * environment: that is, participating in Spring-managed transactions and + * converting JDBC SQLExceptions into Spring's DataAccessException hierarchy. + *

The callback action can return a result object, for example a + * domain object or a collection of domain objects. + * @param action callback object that specifies the action + * @return a result object returned by the action, or null + * @throws DataAccessException if there is any problem + */ + Object execute(StatementCallback action) throws DataAccessException; + + /** + * Issue a single SQL execute, typically a DDL statement. + * @param sql static SQL to execute + * @throws DataAccessException if there is any problem + */ + void execute(String sql) throws DataAccessException; + + /** + * Execute a query given static SQL, reading the ResultSet with a + * ResultSetExtractor. + *

Uses a JDBC Statement, not a PreparedStatement. If you want to + * execute a static query with a PreparedStatement, use the overloaded + * query method with null as argument array. + * @param sql SQL query to execute + * @param rse object that will extract all rows of results + * @return an arbitrary result object, as returned by the ResultSetExtractor + * @throws DataAccessException if there is any problem executing the query + * @see #query(String, Object[], ResultSetExtractor) + */ + Object query(String sql, ResultSetExtractor rse) throws DataAccessException; + + /** + * Execute a query given static SQL, reading the ResultSet on a per-row + * basis with a RowCallbackHandler. + *

Uses a JDBC Statement, not a PreparedStatement. If you want to + * execute a static query with a PreparedStatement, use the overloaded + * query method with null as argument array. + * @param sql SQL query to execute + * @param rch object that will extract results, one row at a time + * @throws DataAccessException if there is any problem executing the query + * @see #query(String, Object[], RowCallbackHandler) + */ + void query(String sql, RowCallbackHandler rch) throws DataAccessException; + + /** + * Execute a query given static SQL, mapping each row to a Java object + * via a RowMapper. + *

Uses a JDBC Statement, not a PreparedStatement. If you want to + * execute a static query with a PreparedStatement, use the overloaded + * query method with null as argument array. + * @param sql SQL query to execute + * @param rowMapper object that will map one object per row + * @return the result List, containing mapped objects + * @throws DataAccessException if there is any problem executing the query + * @see #query(String, Object[], RowMapper) + */ + List query(String sql, RowMapper rowMapper) throws DataAccessException; + + /** + * Execute a query given static SQL, mapping a single result row to a Java + * object via a RowMapper. + *

Uses a JDBC Statement, not a PreparedStatement. If you want to + * execute a static query with a PreparedStatement, use the overloaded + * queryForObject method with null as argument array. + * @param sql SQL query to execute + * @param rowMapper object that will map one object per row + * @return the single mapped object + * @throws IncorrectResultSizeDataAccessException if the query does not + * return exactly one row + * @throws DataAccessException if there is any problem executing the query + * @see #queryForObject(String, Object[], RowMapper) + */ + Object queryForObject(String sql, RowMapper rowMapper) throws DataAccessException; + + /** + * Execute a query for a result object, given static SQL. + *

Uses a JDBC Statement, not a PreparedStatement. If you want to + * execute a static query with a PreparedStatement, use the overloaded + * queryForObject method with null as argument array. + *

This method is useful for running static SQL with a known outcome. + * The query is expected to be a single row/single column query; the returned + * result will be directly mapped to the corresponding object type. + * @param sql SQL query to execute + * @param requiredType the type that the result object is expected to match + * @return the result object of the required type, or null in case of SQL NULL + * @throws IncorrectResultSizeDataAccessException if the query does not return + * exactly one row, or does not return exactly one column in that row + * @throws DataAccessException if there is any problem executing the query + * @see #queryForObject(String, Object[], Class) + */ + Object queryForObject(String sql, Class requiredType) throws DataAccessException; + + /** + * Execute a query for a result Map, given static SQL. + *

Uses a JDBC Statement, not a PreparedStatement. If you want to + * execute a static query with a PreparedStatement, use the overloaded + * queryForMap method with null as argument array. + *

The query is expected to be a single row query; the result row will be + * mapped to a Map (one entry for each column, using the column name as the key). + * @param sql SQL query to execute + * @return the result Map (one entry for each column, using the + * column name as the key) + * @throws IncorrectResultSizeDataAccessException if the query does not + * return exactly one row + * @throws DataAccessException if there is any problem executing the query + * @see #queryForMap(String, Object[]) + * @see ColumnMapRowMapper + */ + Map queryForMap(String sql) throws DataAccessException; + + /** + * Execute a query that results in a long value, given static SQL. + *

Uses a JDBC Statement, not a PreparedStatement. If you want to + * execute a static query with a PreparedStatement, use the overloaded + * queryForLong method with null as argument array. + *

This method is useful for running static SQL with a known outcome. + * The query is expected to be a single row/single column query that results + * in a long value. + * @param sql SQL query to execute + * @return the long value, or 0 in case of SQL NULL + * @throws IncorrectResultSizeDataAccessException if the query does not return + * exactly one row, or does not return exactly one column in that row + * @throws DataAccessException if there is any problem executing the query + * @see #queryForLong(String, Object[]) + */ + long queryForLong(String sql) throws DataAccessException; + + /** + * Execute a query that results in an int value, given static SQL. + *

Uses a JDBC Statement, not a PreparedStatement. If you want to + * execute a static query with a PreparedStatement, use the overloaded + * queryForInt method with null as argument array. + *

This method is useful for running static SQL with a known outcome. + * The query is expected to be a single row/single column query that results + * in an int value. + * @param sql SQL query to execute + * @return the int value, or 0 in case of SQL NULL + * @throws IncorrectResultSizeDataAccessException if the query does not return + * exactly one row, or does not return exactly one column in that row + * @throws DataAccessException if there is any problem executing the query + * @see #queryForInt(String, Object[]) + */ + int queryForInt(String sql) throws DataAccessException; + + /** + * Execute a query for a result list, given static SQL. + *

Uses a JDBC Statement, not a PreparedStatement. If you want to + * execute a static query with a PreparedStatement, use the overloaded + * queryForList method with null as argument array. + *

The results will be mapped to a List (one entry for each row) of + * result objects, each of them matching the specified element type. + * @param sql SQL query to execute + * @param elementType the required type of element in the result list + * (for example, Integer.class) + * @return a List of objects that match the specified element type + * @throws DataAccessException if there is any problem executing the query + * @see #queryForList(String, Object[], Class) + * @see SingleColumnRowMapper + */ + List queryForList(String sql, Class elementType) throws DataAccessException; + + /** + * Execute a query for a result list, given static SQL. + *

Uses a JDBC Statement, not a PreparedStatement. If you want to + * execute a static query with a PreparedStatement, use the overloaded + * queryForList method with null as argument array. + *

The results will be mapped to a List (one entry for each row) of + * Maps (one entry for each column using the column name as the key). + * Each element in the list will be of the form returned by this interface's + * queryForMap() methods. + * @param sql SQL query to execute + * @return an List that contains a Map per row + * @throws DataAccessException if there is any problem executing the query + * @see #queryForList(String, Object[]) + */ + List queryForList(String sql) throws DataAccessException; + + /** + * Execute a query for a SqlRowSet, given static SQL. + *

Uses a JDBC Statement, not a PreparedStatement. If you want to + * execute a static query with a PreparedStatement, use the overloaded + * queryForRowSet method with null as argument array. + *

The results will be mapped to an SqlRowSet which holds the data in a + * disconnected fashion. This wrapper will translate any SQLExceptions thrown. + *

Note that that, for the default implementation, JDBC RowSet support needs to + * be available at runtime: by default, Sun's com.sun.rowset.CachedRowSetImpl + * class is used, which is part of JDK 1.5+ and also available separately as part of + * Sun's JDBC RowSet Implementations download (rowset.jar). + * @param sql SQL query to execute + * @return a SqlRowSet representation (possibly a wrapper around a + * javax.sql.rowset.CachedRowSet) + * @throws DataAccessException if there is any problem executing the query + * @see #queryForRowSet(String, Object[]) + * @see SqlRowSetResultSetExtractor + * @see javax.sql.rowset.CachedRowSet + */ + SqlRowSet queryForRowSet(String sql) throws DataAccessException; + + /** + * Issue a single SQL update operation (such as an insert, update or delete statement). + * @param sql static SQL to execute + * @return the number of rows affected + * @throws DataAccessException if there is any problem. + */ + int update(String sql) throws DataAccessException; + + /** + * Issue multiple SQL updates on a single JDBC Statement using batching. + *

Will fall back to separate updates on a single Statement if the JDBC + * driver does not support batch updates. + * @param sql defining an array of SQL statements that will be executed. + * @return an array of the number of rows affected by each statement + * @throws DataAccessException if there is any problem executing the batch + */ + int[] batchUpdate(String[] sql) throws DataAccessException; + + + //------------------------------------------------------------------------- + // Methods dealing with prepared statements + //------------------------------------------------------------------------- + + /** + * Execute a JDBC data access operation, implemented as callback action + * working on a JDBC PreparedStatement. This allows for implementing arbitrary + * data access operations on a single Statement, within Spring's managed + * JDBC environment: that is, participating in Spring-managed transactions + * and converting JDBC SQLExceptions into Spring's DataAccessException hierarchy. + *

The callback action can return a result object, for example a + * domain object or a collection of domain objects. + * @param psc object that can create a PreparedStatement given a Connection + * @param action callback object that specifies the action + * @return a result object returned by the action, or null + * @throws DataAccessException if there is any problem + */ + Object execute(PreparedStatementCreator psc, PreparedStatementCallback action) + throws DataAccessException; + + /** + * Execute a JDBC data access operation, implemented as callback action + * working on a JDBC PreparedStatement. This allows for implementing arbitrary + * data access operations on a single Statement, within Spring's managed + * JDBC environment: that is, participating in Spring-managed transactions + * and converting JDBC SQLExceptions into Spring's DataAccessException hierarchy. + *

The callback action can return a result object, for example a + * domain object or a collection of domain objects. + * @param sql SQL to execute + * @param action callback object that specifies the action + * @return a result object returned by the action, or null + * @throws DataAccessException if there is any problem + */ + Object execute(String sql, PreparedStatementCallback action) throws DataAccessException; + + /** + * Query using a prepared statement, reading the ResultSet with a + * ResultSetExtractor. + *

A PreparedStatementCreator can either be implemented directly or + * configured through a PreparedStatementCreatorFactory. + * @param psc object that can create a PreparedStatement given a Connection + * @param rse object that will extract results + * @return an arbitrary result object, as returned by the ResultSetExtractor + * @throws DataAccessException if there is any problem + * @see PreparedStatementCreatorFactory + */ + Object query(PreparedStatementCreator psc, ResultSetExtractor rse) throws DataAccessException; + + /** + * Query using a prepared statement, reading the ResultSet with a + * ResultSetExtractor. + * @param sql SQL query to execute + * @param pss object that knows how to set values on the prepared statement. + * If this is null, the SQL will be assumed to contain no bind parameters. + * Even if there are no bind parameters, this object may be used to + * set fetch size and other performance options. + * @param rse object that will extract results + * @return an arbitrary result object, as returned by the ResultSetExtractor + * @throws DataAccessException if there is any problem + */ + Object query(String sql, PreparedStatementSetter pss, ResultSetExtractor rse) + throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a list + * of arguments to bind to the query, reading the ResultSet with a + * ResultSetExtractor. + * @param sql SQL query to execute + * @param args arguments to bind to the query + * @param argTypes SQL types of the arguments + * (constants from java.sql.Types) + * @param rse object that will extract results + * @return an arbitrary result object, as returned by the ResultSetExtractor + * @throws DataAccessException if the query fails + * @see java.sql.Types + */ + Object query(String sql, Object[] args, int[] argTypes, ResultSetExtractor rse) + throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a list + * of arguments to bind to the query, reading the ResultSet with a + * ResultSetExtractor. + * @param sql SQL query to execute + * @param args arguments to bind to the query + * (leaving it to the PreparedStatement to guess the corresponding SQL type); + * may also contain {@link SqlParameterValue} objects which indicate not + * only the argument value but also the SQL type and optionally the scale + * @param rse object that will extract results + * @return an arbitrary result object, as returned by the ResultSetExtractor + * @throws DataAccessException if the query fails + */ + Object query(String sql, Object[] args, ResultSetExtractor rse) throws DataAccessException; + + /** + * Query using a prepared statement, reading the ResultSet on a per-row + * basis with a RowCallbackHandler. + *

A PreparedStatementCreator can either be implemented directly or + * configured through a PreparedStatementCreatorFactory. + * @param psc object that can create a PreparedStatement given a Connection + * @param rch object that will extract results, one row at a time + * @throws DataAccessException if there is any problem + * @see PreparedStatementCreatorFactory + */ + void query(PreparedStatementCreator psc, RowCallbackHandler rch) throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a + * PreparedStatementSetter implementation that knows how to bind values + * to the query, reading the ResultSet on a per-row basis with a + * RowCallbackHandler. + * @param sql SQL query to execute + * @param pss object that knows how to set values on the prepared statement. + * If this is null, the SQL will be assumed to contain no bind parameters. + * Even if there are no bind parameters, this object may be used to + * set fetch size and other performance options. + * @param rch object that will extract results, one row at a time + * @throws DataAccessException if the query fails + */ + void query(String sql, PreparedStatementSetter pss, RowCallbackHandler rch) + throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a list of + * arguments to bind to the query, reading the ResultSet on a per-row basis + * with a RowCallbackHandler. + * @param sql SQL query to execute + * @param args arguments to bind to the query + * @param argTypes SQL types of the arguments + * (constants from java.sql.Types) + * @param rch object that will extract results, one row at a time + * @throws DataAccessException if the query fails + * @see java.sql.Types + */ + void query(String sql, Object[] args, int[] argTypes, RowCallbackHandler rch) + throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a list of + * arguments to bind to the query, reading the ResultSet on a per-row basis + * with a RowCallbackHandler. + * @param sql SQL query to execute + * @param args arguments to bind to the query + * (leaving it to the PreparedStatement to guess the corresponding SQL type); + * may also contain {@link SqlParameterValue} objects which indicate not + * only the argument value but also the SQL type and optionally the scale + * @param rch object that will extract results, one row at a time + * @throws DataAccessException if the query fails + */ + void query(String sql, Object[] args, RowCallbackHandler rch) throws DataAccessException; + + /** + * Query using a prepared statement, mapping each row to a Java object + * via a RowMapper. + *

A PreparedStatementCreator can either be implemented directly or + * configured through a PreparedStatementCreatorFactory. + * @param psc object that can create a PreparedStatement given a Connection + * @param rowMapper object that will map one object per row + * @return the result List, containing mapped objects + * @throws DataAccessException if there is any problem + * @see PreparedStatementCreatorFactory + */ + List query(PreparedStatementCreator psc, RowMapper rowMapper) throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a + * PreparedStatementSetter implementation that knows how to bind values + * to the query, mapping each row to a Java object via a RowMapper. + * @param sql SQL query to execute + * @param pss object that knows how to set values on the prepared statement. + * If this is null, the SQL will be assumed to contain no bind parameters. + * Even if there are no bind parameters, this object may be used to + * set fetch size and other performance options. + * @param rowMapper object that will map one object per row + * @return the result List, containing mapped objects + * @throws DataAccessException if the query fails + */ + List query(String sql, PreparedStatementSetter pss, RowMapper rowMapper) + throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a list + * of arguments to bind to the query, mapping each row to a Java object + * via a RowMapper. + * @param sql SQL query to execute + * @param args arguments to bind to the query + * @param argTypes SQL types of the arguments + * (constants from java.sql.Types) + * @param rowMapper object that will map one object per row + * @return the result List, containing mapped objects + * @throws DataAccessException if the query fails + * @see java.sql.Types + */ + List query(String sql, Object[] args, int[] argTypes, RowMapper rowMapper) + throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a list + * of arguments to bind to the query, mapping each row to a Java object + * via a RowMapper. + * @param sql SQL query to execute + * @param args arguments to bind to the query + * (leaving it to the PreparedStatement to guess the corresponding SQL type); + * may also contain {@link SqlParameterValue} objects which indicate not + * only the argument value but also the SQL type and optionally the scale + * @param rowMapper object that will map one object per row + * @return the result List, containing mapped objects + * @throws DataAccessException if the query fails + */ + List query(String sql, Object[] args, RowMapper rowMapper) throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a list + * of arguments to bind to the query, mapping a single result row to a + * Java object via a RowMapper. + * @param sql SQL query to execute + * @param args arguments to bind to the query + * (leaving it to the PreparedStatement to guess the corresponding SQL type) + * @param argTypes SQL types of the arguments + * (constants from java.sql.Types) + * @param rowMapper object that will map one object per row + * @return the single mapped object + * @throws IncorrectResultSizeDataAccessException if the query does not + * return exactly one row + * @throws DataAccessException if the query fails + */ + Object queryForObject(String sql, Object[] args, int[] argTypes, RowMapper rowMapper) + throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a list + * of arguments to bind to the query, mapping a single result row to a + * Java object via a RowMapper. + * @param sql SQL query to execute + * @param args arguments to bind to the query + * (leaving it to the PreparedStatement to guess the corresponding SQL type); + * may also contain {@link SqlParameterValue} objects which indicate not + * only the argument value but also the SQL type and optionally the scale + * @param rowMapper object that will map one object per row + * @return the single mapped object + * @throws IncorrectResultSizeDataAccessException if the query does not + * return exactly one row + * @throws DataAccessException if the query fails + */ + Object queryForObject(String sql, Object[] args, RowMapper rowMapper) + throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a + * list of arguments to bind to the query, expecting a result object. + *

The query is expected to be a single row/single column query; the returned + * result will be directly mapped to the corresponding object type. + * @param sql SQL query to execute + * @param args arguments to bind to the query + * @param argTypes SQL types of the arguments + * (constants from java.sql.Types) + * @param requiredType the type that the result object is expected to match + * @return the result object of the required type, or null in case of SQL NULL + * @throws IncorrectResultSizeDataAccessException if the query does not return + * exactly one row, or does not return exactly one column in that row + * @throws DataAccessException if the query fails + * @see #queryForObject(String, Class) + * @see java.sql.Types + */ + Object queryForObject(String sql, Object[] args, int[] argTypes, Class requiredType) + throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a + * list of arguments to bind to the query, expecting a result object. + *

The query is expected to be a single row/single column query; the returned + * result will be directly mapped to the corresponding object type. + * @param sql SQL query to execute + * @param args arguments to bind to the query + * (leaving it to the PreparedStatement to guess the corresponding SQL type); + * may also contain {@link SqlParameterValue} objects which indicate not + * only the argument value but also the SQL type and optionally the scale + * @param requiredType the type that the result object is expected to match + * @return the result object of the required type, or null in case of SQL NULL + * @throws IncorrectResultSizeDataAccessException if the query does not return + * exactly one row, or does not return exactly one column in that row + * @throws DataAccessException if the query fails + * @see #queryForObject(String, Class) + */ + Object queryForObject(String sql, Object[] args, Class requiredType) throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a + * list of arguments to bind to the query, expecting a result Map. + *

The query is expected to be a single row query; the result row will be + * mapped to a Map (one entry for each column, using the column name as the key). + * @param sql SQL query to execute + * @param args arguments to bind to the query + * @param argTypes SQL types of the arguments + * (constants from java.sql.Types) + * @return the result Map (one entry for each column, using the + * column name as the key) + * @throws IncorrectResultSizeDataAccessException if the query does not + * return exactly one row + * @throws DataAccessException if the query fails + * @see #queryForMap(String) + * @see ColumnMapRowMapper + * @see java.sql.Types + */ + Map queryForMap(String sql, Object[] args, int[] argTypes) throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a + * list of arguments to bind to the query, expecting a result Map. + * The queryForMap() methods defined by this interface are appropriate + * when you don't have a domain model. Otherwise, consider using + * one of the queryForObject() methods. + *

The query is expected to be a single row query; the result row will be + * mapped to a Map (one entry for each column, using the column name as the key). + * @param sql SQL query to execute + * @param args arguments to bind to the query + * (leaving it to the PreparedStatement to guess the corresponding SQL type); + * may also contain {@link SqlParameterValue} objects which indicate not + * only the argument value but also the SQL type and optionally the scale + * @return the result Map (one entry for each column, using the + * column name as the key) + * @throws IncorrectResultSizeDataAccessException if the query does not + * return exactly one row + * @throws DataAccessException if the query fails + * @see #queryForMap(String) + * @see ColumnMapRowMapper + */ + Map queryForMap(String sql, Object[] args) throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a + * list of arguments to bind to the query, resulting in a long value. + *

The query is expected to be a single row/single column query that + * results in a long value. + * @param sql SQL query to execute + * @param args arguments to bind to the query + * @param argTypes SQL types of the arguments + * (constants from java.sql.Types) + * @return the long value, or 0 in case of SQL NULL + * @throws IncorrectResultSizeDataAccessException if the query does not return + * exactly one row, or does not return exactly one column in that row + * @throws DataAccessException if the query fails + * @see #queryForLong(String) + * @see java.sql.Types + */ + long queryForLong(String sql, Object[] args, int[] argTypes) throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a + * list of arguments to bind to the query, resulting in a long value. + *

The query is expected to be a single row/single column query that + * results in a long value. + * @param sql SQL query to execute + * @param args arguments to bind to the query + * (leaving it to the PreparedStatement to guess the corresponding SQL type); + * may also contain {@link SqlParameterValue} objects which indicate not + * only the argument value but also the SQL type and optionally the scale + * @return the long value, or 0 in case of SQL NULL + * @throws IncorrectResultSizeDataAccessException if the query does not return + * exactly one row, or does not return exactly one column in that row + * @throws DataAccessException if the query fails + * @see #queryForLong(String) + */ + long queryForLong(String sql, Object[] args) throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a + * list of arguments to bind to the query, resulting in an int value. + *

The query is expected to be a single row/single column query that + * results in an int value. + * @param sql SQL query to execute + * @param args arguments to bind to the query + * @param argTypes SQL types of the arguments + * (constants from java.sql.Types) + * @return the int value, or 0 in case of SQL NULL + * @throws IncorrectResultSizeDataAccessException if the query does not return + * exactly one row, or does not return exactly one column in that row + * @throws DataAccessException if the query fails + * @see #queryForInt(String) + * @see java.sql.Types + */ + int queryForInt(String sql, Object[] args, int[] argTypes) throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a + * list of arguments to bind to the query, resulting in an int value. + *

The query is expected to be a single row/single column query that + * results in an int value. + * @param sql SQL query to execute + * @param args arguments to bind to the query + * (leaving it to the PreparedStatement to guess the corresponding SQL type); + * may also contain {@link SqlParameterValue} objects which indicate not + * only the argument value but also the SQL type and optionally the scale + * @return the int value, or 0 in case of SQL NULL + * @throws IncorrectResultSizeDataAccessException if the query does not return + * exactly one row, or does not return exactly one column in that row + * @throws DataAccessException if the query fails + * @see #queryForInt(String) + */ + int queryForInt(String sql, Object[] args) throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a + * list of arguments to bind to the query, expecting a result list. + *

The results will be mapped to a List (one entry for each row) of + * result objects, each of them matching the specified element type. + * @param sql SQL query to execute + * @param args arguments to bind to the query + * @param argTypes SQL types of the arguments + * (constants from java.sql.Types) + * @param elementType the required type of element in the result list + * (for example, Integer.class) + * @return a List of objects that match the specified element type + * @throws DataAccessException if the query fails + * @see #queryForList(String, Class) + * @see SingleColumnRowMapper + */ + List queryForList(String sql, Object[] args, int[] argTypes, Class elementType) + throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a + * list of arguments to bind to the query, expecting a result list. + *

The results will be mapped to a List (one entry for each row) of + * result objects, each of them matching the specified element type. + * @param sql SQL query to execute + * @param args arguments to bind to the query + * (leaving it to the PreparedStatement to guess the corresponding SQL type); + * may also contain {@link SqlParameterValue} objects which indicate not + * only the argument value but also the SQL type and optionally the scale + * @param elementType the required type of element in the result list + * (for example, Integer.class) + * @return a List of objects that match the specified element type + * @throws DataAccessException if the query fails + * @see #queryForList(String, Class) + * @see SingleColumnRowMapper + */ + List queryForList(String sql, Object[] args, Class elementType) throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a + * list of arguments to bind to the query, expecting a result list. + *

The results will be mapped to a List (one entry for each row) of + * Maps (one entry for each column, using the column name as the key). + * Thus Each element in the list will be of the form returned by this interface's + * queryForMap() methods. + * @param sql SQL query to execute + * @param args arguments to bind to the query + * @param argTypes SQL types of the arguments + * (constants from java.sql.Types) + * @return a List that contains a Map per row + * @throws DataAccessException if the query fails + * @see #queryForList(String) + * @see java.sql.Types + */ + List queryForList(String sql, Object[] args, int[] argTypes) throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a + * list of arguments to bind to the query, expecting a result list. + *

The results will be mapped to a List (one entry for each row) of + * Maps (one entry for each column, using the column name as the key). + * Each element in the list will be of the form returned by this interface's + * queryForMap() methods. + * @param sql SQL query to execute + * @param args arguments to bind to the query + * (leaving it to the PreparedStatement to guess the corresponding SQL type); + * may also contain {@link SqlParameterValue} objects which indicate not + * only the argument value but also the SQL type and optionally the scale + * @return a List that contains a Map per row + * @throws DataAccessException if the query fails + * @see #queryForList(String) + */ + List queryForList(String sql, Object[] args) throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a + * list of arguments to bind to the query, expecting a SqlRowSet. + *

The results will be mapped to an SqlRowSet which holds the data in a + * disconnected fashion. This wrapper will translate any SQLExceptions thrown. + *

Note that that, for the default implementation, JDBC RowSet support needs to + * be available at runtime: by default, Sun's com.sun.rowset.CachedRowSetImpl + * class is used, which is part of JDK 1.5+ and also available separately as part of + * Sun's JDBC RowSet Implementations download (rowset.jar). + * @param sql SQL query to execute + * @param args arguments to bind to the query + * @param argTypes SQL types of the arguments + * (constants from java.sql.Types) + * @return a SqlRowSet representation (possibly a wrapper around a + * javax.sql.rowset.CachedRowSet) + * @throws DataAccessException if there is any problem executing the query + * @see #queryForRowSet(String) + * @see SqlRowSetResultSetExtractor + * @see javax.sql.rowset.CachedRowSet + * @see java.sql.Types + */ + SqlRowSet queryForRowSet(String sql, Object[] args, int[] argTypes) throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a + * list of arguments to bind to the query, expecting a SqlRowSet. + *

The results will be mapped to an SqlRowSet which holds the data in a + * disconnected fashion. This wrapper will translate any SQLExceptions thrown. + *

Note that that, for the default implementation, JDBC RowSet support needs to + * be available at runtime: by default, Sun's com.sun.rowset.CachedRowSetImpl + * class is used, which is part of JDK 1.5+ and also available separately as part of + * Sun's JDBC RowSet Implementations download (rowset.jar). + * @param sql SQL query to execute + * @param args arguments to bind to the query + * (leaving it to the PreparedStatement to guess the corresponding SQL type); + * may also contain {@link SqlParameterValue} objects which indicate not + * only the argument value but also the SQL type and optionally the scale + * @return a SqlRowSet representation (possibly a wrapper around a + * javax.sql.rowset.CachedRowSet) + * @throws DataAccessException if there is any problem executing the query + * @see #queryForRowSet(String) + * @see SqlRowSetResultSetExtractor + * @see javax.sql.rowset.CachedRowSet + */ + SqlRowSet queryForRowSet(String sql, Object[] args) throws DataAccessException; + + /** + * Issue a single SQL update operation (such as an insert, update or delete statement) + * using a PreparedStatementCreator to provide SQL and any required parameters. + *

A PreparedStatementCreator can either be implemented directly or + * configured through a PreparedStatementCreatorFactory. + * @param psc object that provides SQL and any necessary parameters + * @return the number of rows affected + * @throws DataAccessException if there is any problem issuing the update + * @see PreparedStatementCreatorFactory + */ + int update(PreparedStatementCreator psc) throws DataAccessException; + + /** + * Issue an update statement using a PreparedStatementCreator to provide SQL and + * any required parameters. Generated keys will be put into the given KeyHolder. + *

Note that the given PreparedStatementCreator has to create a statement + * with activated extraction of generated keys (a JDBC 3.0 feature). This can + * either be done directly or through using a PreparedStatementCreatorFactory. + * @param psc object that provides SQL and any necessary parameters + * @param generatedKeyHolder KeyHolder that will hold the generated keys + * @return the number of rows affected + * @throws DataAccessException if there is any problem issuing the update + * @see PreparedStatementCreatorFactory + * @see org.springframework.jdbc.support.GeneratedKeyHolder + */ + int update(PreparedStatementCreator psc, KeyHolder generatedKeyHolder) throws DataAccessException; + + /** + * Issue an update statement using a PreparedStatementSetter to set bind parameters, + * with given SQL. Simpler than using a PreparedStatementCreator as this method + * will create the PreparedStatement: The PreparedStatementSetter just needs to + * set parameters. + * @param sql SQL containing bind parameters + * @param pss helper that sets bind parameters. If this is null + * we run an update with static SQL. + * @return the number of rows affected + * @throws DataAccessException if there is any problem issuing the update + */ + int update(String sql, PreparedStatementSetter pss) throws DataAccessException; + + /** + * Issue a single SQL update operation (such as an insert, update or delete statement) + * via a prepared statement, binding the given arguments. + * @param sql SQL containing bind parameters + * @param args arguments to bind to the query + * @param argTypes SQL types of the arguments + * (constants from java.sql.Types) + * @return the number of rows affected + * @throws DataAccessException if there is any problem issuing the update + * @see java.sql.Types + */ + int update(String sql, Object[] args, int[] argTypes) throws DataAccessException; + + /** + * Issue a single SQL update operation (such as an insert, update or delete statement) + * via a prepared statement, binding the given arguments. + * @param sql SQL containing bind parameters + * @param args arguments to bind to the query + * (leaving it to the PreparedStatement to guess the corresponding SQL type); + * may also contain {@link SqlParameterValue} objects which indicate not + * only the argument value but also the SQL type and optionally the scale + * @return the number of rows affected + * @throws DataAccessException if there is any problem issuing the update + */ + int update(String sql, Object[] args) throws DataAccessException; + + /** + * Issue multiple update statements on a single PreparedStatement, + * using batch updates and a BatchPreparedStatementSetter to set values. + *

Will fall back to separate updates on a single PreparedStatement + * if the JDBC driver does not support batch updates. + * @param sql defining PreparedStatement that will be reused. + * All statements in the batch will use the same SQL. + * @param pss object to set parameters on the PreparedStatement + * created by this method + * @return an array of the number of rows affected by each statement + * @throws DataAccessException if there is any problem issuing the update + */ + int[] batchUpdate(String sql, BatchPreparedStatementSetter pss) throws DataAccessException; + + + //------------------------------------------------------------------------- + // Methods dealing with callable statements + //------------------------------------------------------------------------- + + /** + * Execute a JDBC data access operation, implemented as callback action + * working on a JDBC CallableStatement. This allows for implementing arbitrary + * data access operations on a single Statement, within Spring's managed + * JDBC environment: that is, participating in Spring-managed transactions + * and converting JDBC SQLExceptions into Spring's DataAccessException hierarchy. + *

The callback action can return a result object, for example a + * domain object or a collection of domain objects. + * @param csc object that can create a CallableStatement given a Connection + * @param action callback object that specifies the action + * @return a result object returned by the action, or null + * @throws DataAccessException if there is any problem + */ + Object execute(CallableStatementCreator csc, CallableStatementCallback action) + throws DataAccessException; + + /** + * Execute a JDBC data access operation, implemented as callback action + * working on a JDBC CallableStatement. This allows for implementing arbitrary + * data access operations on a single Statement, within Spring's managed + * JDBC environment: that is, participating in Spring-managed transactions + * and converting JDBC SQLExceptions into Spring's DataAccessException hierarchy. + *

The callback action can return a result object, for example a + * domain object or a collection of domain objects. + * @param callString the SQL call string to execute + * @param action callback object that specifies the action + * @return a result object returned by the action, or null + * @throws DataAccessException if there is any problem + */ + Object execute(String callString, CallableStatementCallback action) throws DataAccessException; + + /** + * Execute a SQL call using a CallableStatementCreator to provide SQL and any + * required parameters. + * @param csc object that provides SQL and any necessary parameters + * @param declaredParameters list of declared SqlParameter objects + * @return Map of extracted out parameters + * @throws DataAccessException if there is any problem issuing the update + */ + Map call(CallableStatementCreator csc, List declaredParameters) throws DataAccessException; + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java new file mode 100644 index 0000000000..00f9324b6f --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -0,0 +1,1374 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.core; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.sql.CallableStatement; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.SQLWarning; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import javax.sql.DataSource; + +import org.springframework.core.CollectionFactory; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.dao.support.DataAccessUtils; +import org.springframework.jdbc.SQLWarningException; +import org.springframework.jdbc.datasource.ConnectionProxy; +import org.springframework.jdbc.datasource.DataSourceUtils; +import org.springframework.jdbc.support.JdbcAccessor; +import org.springframework.jdbc.support.JdbcUtils; +import org.springframework.jdbc.support.KeyHolder; +import org.springframework.jdbc.support.nativejdbc.NativeJdbcExtractor; +import org.springframework.jdbc.support.rowset.SqlRowSet; +import org.springframework.util.Assert; + +/** + * This is the central class in the JDBC core package. + * It simplifies the use of JDBC and helps to avoid common errors. + * It executes core JDBC workflow, leaving application code to provide SQL + * and extract results. This class executes SQL queries or updates, initiating + * iteration over ResultSets and catching JDBC exceptions and translating + * them to the generic, more informative exception hierarchy defined in the + * org.springframework.dao package. + * + *

Code using this class need only implement callback interfaces, giving + * them a clearly defined contract. The {@link PreparedStatementCreator} callback + * interface creates a prepared statement given a Connection, providing SQL and + * any necessary parameters. The {@link ResultSetExtractor} interface extracts + * values from a ResultSet. See also {@link PreparedStatementSetter} and + * {@link RowMapper} for two popular alternative callback interfaces. + * + *

Can be used within a service implementation via direct instantiation + * with a DataSource reference, or get prepared in an application context + * and given to services as bean reference. Note: The DataSource should + * always be configured as a bean in the application context, in the first case + * given to the service directly, in the second case to the prepared template. + * + *

Because this class is parameterizable by the callback interfaces and + * the {@link org.springframework.jdbc.support.SQLExceptionTranslator} + * interface, there should be no need to subclass it. + * + *

All SQL operations performed by this class are logged at debug level, + * using "org.springframework.jdbc.core.JdbcTemplate" as log category. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Thomas Risberg + * @since May 3, 2001 + * @see PreparedStatementCreator + * @see PreparedStatementSetter + * @see CallableStatementCreator + * @see PreparedStatementCallback + * @see CallableStatementCallback + * @see ResultSetExtractor + * @see RowCallbackHandler + * @see RowMapper + * @see org.springframework.jdbc.support.SQLExceptionTranslator + */ +public class JdbcTemplate extends JdbcAccessor implements JdbcOperations { + + private static final String RETURN_RESULT_SET_PREFIX = "#result-set-"; + + private static final String RETURN_UPDATE_COUNT_PREFIX = "#update-count-"; + + + /** Custom NativeJdbcExtractor */ + private NativeJdbcExtractor nativeJdbcExtractor; + + /** If this variable is false, we will throw exceptions on SQL warnings */ + private boolean ignoreWarnings = true; + + /** + * If this variable is set to a non-zero value, it will be used for setting the + * fetchSize property on statements used for query processing. + */ + private int fetchSize = 0; + + /** + * If this variable is set to a non-zero value, it will be used for setting the + * maxRows property on statements used for query processing. + */ + private int maxRows = 0; + + /** + * If this variable is set to a non-zero value, it will be used for setting the + * queryTimeout property on statements used for query processing. + */ + private int queryTimeout = 0; + + /** + * If this variable is set to true then all results checking will be bypassed for any + * callable statement processing. This can be used to avoid a bug in some older Oracle + * JDBC drivers like 10.1.0.2. + */ + private boolean skipResultsProcessing = false; + + /** + * If this variable is set to true then all results from a stored procedure call + * that don't have a corresponding SqlOutParameter declaration will be bypassed. + * All other results processng will be take place unless the variable + * skipResultsProcessing is set to true + */ + private boolean skipUndeclaredResults = false; + + /** + * If this variable is set to true then execution of a CallableStatement will return + * the results in a Map that uses case insensitive names for the parameters if + * Commons Collections is available on the classpath. + */ + private boolean resultsMapCaseInsensitive = false; + + + /** + * Construct a new JdbcTemplate for bean usage. + *

Note: The DataSource has to be set before using the instance. + * @see #setDataSource + */ + public JdbcTemplate() { + } + + /** + * Construct a new JdbcTemplate, given a DataSource to obtain connections from. + *

Note: This will not trigger initialization of the exception translator. + * @param dataSource the JDBC DataSource to obtain connections from + */ + public JdbcTemplate(DataSource dataSource) { + setDataSource(dataSource); + afterPropertiesSet(); + } + + /** + * Construct a new JdbcTemplate, given a DataSource to obtain connections from. + *

Note: Depending on the "lazyInit" flag, initialization of the exception translator + * will be triggered. + * @param dataSource the JDBC DataSource to obtain connections from + * @param lazyInit whether to lazily initialize the SQLExceptionTranslator + */ + public JdbcTemplate(DataSource dataSource, boolean lazyInit) { + setDataSource(dataSource); + setLazyInit(lazyInit); + afterPropertiesSet(); + } + + + /** + * Set a NativeJdbcExtractor to extract native JDBC objects from wrapped handles. + * Useful if native Statement and/or ResultSet handles are expected for casting + * to database-specific implementation classes, but a connection pool that wraps + * JDBC objects is used (note: any pool will return wrapped Connections). + */ + public void setNativeJdbcExtractor(NativeJdbcExtractor extractor) { + this.nativeJdbcExtractor = extractor; + } + + /** + * Return the current NativeJdbcExtractor implementation. + */ + public NativeJdbcExtractor getNativeJdbcExtractor() { + return this.nativeJdbcExtractor; + } + + /** + * Set whether or not we want to ignore SQLWarnings. + *

Default is "true", swallowing and logging all warnings. Switch this flag + * to "false" to make the JdbcTemplate throw a SQLWarningException instead. + * @see java.sql.SQLWarning + * @see org.springframework.jdbc.SQLWarningException + * @see #handleWarnings + */ + public void setIgnoreWarnings(boolean ignoreWarnings) { + this.ignoreWarnings = ignoreWarnings; + } + + /** + * Return whether or not we ignore SQLWarnings. + */ + public boolean isIgnoreWarnings() { + return this.ignoreWarnings; + } + + /** + * Set the fetch size for this JdbcTemplate. This is important for processing + * large result sets: Setting this higher than the default value will increase + * processing speed at the cost of memory consumption; setting this lower can + * avoid transferring row data that will never be read by the application. + *

Default is 0, indicating to use the JDBC driver's default. + * @see java.sql.Statement#setFetchSize + */ + public void setFetchSize(int fetchSize) { + this.fetchSize = fetchSize; + } + + /** + * Return the fetch size specified for this JdbcTemplate. + */ + public int getFetchSize() { + return this.fetchSize; + } + + /** + * Set the maximum number of rows for this JdbcTemplate. This is important + * for processing subsets of large result sets, avoiding to read and hold + * the entire result set in the database or in the JDBC driver if we're + * never interested in the entire result in the first place (for example, + * when performing searches that might return a large number of matches). + *

Default is 0, indicating to use the JDBC driver's default. + * @see java.sql.Statement#setMaxRows + */ + public void setMaxRows(int maxRows) { + this.maxRows = maxRows; + } + + /** + * Return the maximum number of rows specified for this JdbcTemplate. + */ + public int getMaxRows() { + return this.maxRows; + } + + /** + * Set the query timeout for statements that this JdbcTemplate executes. + *

Default is 0, indicating to use the JDBC driver's default. + *

Note: Any timeout specified here will be overridden by the remaining + * transaction timeout when executing within a transaction that has a + * timeout specified at the transaction level. + * @see java.sql.Statement#setQueryTimeout + */ + public void setQueryTimeout(int queryTimeout) { + this.queryTimeout = queryTimeout; + } + + /** + * Return the query timeout for statements that this JdbcTemplate executes. + */ + public int getQueryTimeout() { + return this.queryTimeout; + } + + /** + * Set whether results processing should be skipped. Can be used to optimize callable + * statement processing when we know that no results are being passed back - the processing + * of out parameter will still take place. This can be used to avoid a bug in some older + * Oracle JDBC drivers like 10.1.0.2. + */ + public void setSkipResultsProcessing(boolean skipResultsProcessing) { + this.skipResultsProcessing = skipResultsProcessing; + } + + /** + * Return whether results processing should be skipped. + */ + public boolean isSkipResultsProcessing() { + return this.skipResultsProcessing; + } + + /** + * Set whether undelared results should be skipped. + */ + public void setSkipUndeclaredResults(boolean skipUndeclaredResults) { + this.skipUndeclaredResults = skipUndeclaredResults; + } + + /** + * Return whether undeclared results should be skipped. + */ + public boolean isSkipUndeclaredResults() { + return this.skipUndeclaredResults; + } + + /** + * Set whether execution of a CallableStatement will return the results in a Map + * that uses case insensitive names for the parameters. + */ + public void setResultsMapCaseInsensitive(boolean resultsMapCaseInsensitive) { + this.resultsMapCaseInsensitive = resultsMapCaseInsensitive; + } + + /** + * Return whether execution of a CallableStatement will return the results in a Map + * that uses case insensitive names for the parameters. + */ + public boolean isResultsMapCaseInsensitive() { + return this.resultsMapCaseInsensitive; + } + + + //------------------------------------------------------------------------- + // Methods dealing with a plain java.sql.Connection + //------------------------------------------------------------------------- + + public Object execute(ConnectionCallback action) throws DataAccessException { + Assert.notNull(action, "Callback object must not be null"); + + Connection con = DataSourceUtils.getConnection(getDataSource()); + try { + Connection conToUse = con; + if (this.nativeJdbcExtractor != null) { + // Extract native JDBC Connection, castable to OracleConnection or the like. + conToUse = this.nativeJdbcExtractor.getNativeConnection(con); + } + else { + // Create close-suppressing Connection proxy, also preparing returned Statements. + conToUse = createConnectionProxy(con); + } + return action.doInConnection(conToUse); + } + catch (SQLException ex) { + // Release Connection early, to avoid potential connection pool deadlock + // in the case when the exception translator hasn't been initialized yet. + DataSourceUtils.releaseConnection(con, getDataSource()); + con = null; + throw getExceptionTranslator().translate("ConnectionCallback", getSql(action), ex); + } + finally { + DataSourceUtils.releaseConnection(con, getDataSource()); + } + } + + /** + * Create a close-suppressing proxy for the given JDBC Connection. + * Called by the execute method. + *

The proxy also prepares returned JDBC Statements, applying + * statement settings such as fetch size, max rows, and query timeout. + * @param con the JDBC Connection to create a proxy for + * @return the Connection proxy + * @see java.sql.Connection#close() + * @see #execute(ConnectionCallback) + * @see #applyStatementSettings + */ + protected Connection createConnectionProxy(Connection con) { + return (Connection) Proxy.newProxyInstance( + ConnectionProxy.class.getClassLoader(), + new Class[] {ConnectionProxy.class}, + new CloseSuppressingInvocationHandler(con)); + } + + + //------------------------------------------------------------------------- + // Methods dealing with static SQL (java.sql.Statement) + //------------------------------------------------------------------------- + + public Object execute(StatementCallback action) throws DataAccessException { + Assert.notNull(action, "Callback object must not be null"); + + Connection con = DataSourceUtils.getConnection(getDataSource()); + Statement stmt = null; + try { + Connection conToUse = con; + if (this.nativeJdbcExtractor != null && + this.nativeJdbcExtractor.isNativeConnectionNecessaryForNativeStatements()) { + conToUse = this.nativeJdbcExtractor.getNativeConnection(con); + } + stmt = conToUse.createStatement(); + applyStatementSettings(stmt); + Statement stmtToUse = stmt; + if (this.nativeJdbcExtractor != null) { + stmtToUse = this.nativeJdbcExtractor.getNativeStatement(stmt); + } + Object result = action.doInStatement(stmtToUse); + handleWarnings(stmt); + return result; + } + catch (SQLException ex) { + // Release Connection early, to avoid potential connection pool deadlock + // in the case when the exception translator hasn't been initialized yet. + JdbcUtils.closeStatement(stmt); + stmt = null; + DataSourceUtils.releaseConnection(con, getDataSource()); + con = null; + throw getExceptionTranslator().translate("StatementCallback", getSql(action), ex); + } + finally { + JdbcUtils.closeStatement(stmt); + DataSourceUtils.releaseConnection(con, getDataSource()); + } + } + + public void execute(final String sql) throws DataAccessException { + if (logger.isDebugEnabled()) { + logger.debug("Executing SQL statement [" + sql + "]"); + } + + class ExecuteStatementCallback implements StatementCallback, SqlProvider { + public Object doInStatement(Statement stmt) throws SQLException { + stmt.execute(sql); + return null; + } + public String getSql() { + return sql; + } + } + execute(new ExecuteStatementCallback()); + } + + public Object query(final String sql, final ResultSetExtractor rse) throws DataAccessException { + Assert.notNull(sql, "SQL must not be null"); + Assert.notNull(rse, "ResultSetExtractor must not be null"); + if (logger.isDebugEnabled()) { + logger.debug("Executing SQL query [" + sql + "]"); + } + + class QueryStatementCallback implements StatementCallback, SqlProvider { + public Object doInStatement(Statement stmt) throws SQLException { + ResultSet rs = null; + try { + rs = stmt.executeQuery(sql); + ResultSet rsToUse = rs; + if (nativeJdbcExtractor != null) { + rsToUse = nativeJdbcExtractor.getNativeResultSet(rs); + } + return rse.extractData(rsToUse); + } + finally { + JdbcUtils.closeResultSet(rs); + } + } + public String getSql() { + return sql; + } + } + return execute(new QueryStatementCallback()); + } + + public void query(String sql, RowCallbackHandler rch) throws DataAccessException { + query(sql, new RowCallbackHandlerResultSetExtractor(rch)); + } + + public List query(String sql, RowMapper rowMapper) throws DataAccessException { + return (List) query(sql, new RowMapperResultSetExtractor(rowMapper)); + } + + public Map queryForMap(String sql) throws DataAccessException { + return (Map) queryForObject(sql, getColumnMapRowMapper()); + } + + public Object queryForObject(String sql, RowMapper rowMapper) throws DataAccessException { + List results = query(sql, rowMapper); + return DataAccessUtils.requiredSingleResult(results); + } + + public Object queryForObject(String sql, Class requiredType) throws DataAccessException { + return queryForObject(sql, getSingleColumnRowMapper(requiredType)); + } + + public long queryForLong(String sql) throws DataAccessException { + Number number = (Number) queryForObject(sql, Long.class); + return (number != null ? number.longValue() : 0); + } + + public int queryForInt(String sql) throws DataAccessException { + Number number = (Number) queryForObject(sql, Integer.class); + return (number != null ? number.intValue() : 0); + } + + public List queryForList(String sql, Class elementType) throws DataAccessException { + return query(sql, getSingleColumnRowMapper(elementType)); + } + + public List queryForList(String sql) throws DataAccessException { + return query(sql, getColumnMapRowMapper()); + } + + public SqlRowSet queryForRowSet(String sql) throws DataAccessException { + return (SqlRowSet) query(sql, new SqlRowSetResultSetExtractor()); + } + + public int update(final String sql) throws DataAccessException { + Assert.notNull(sql, "SQL must not be null"); + if (logger.isDebugEnabled()) { + logger.debug("Executing SQL update [" + sql + "]"); + } + + class UpdateStatementCallback implements StatementCallback, SqlProvider { + public Object doInStatement(Statement stmt) throws SQLException { + int rows = stmt.executeUpdate(sql); + if (logger.isDebugEnabled()) { + logger.debug("SQL update affected " + rows + " rows"); + } + return new Integer(rows); + } + public String getSql() { + return sql; + } + } + return ((Integer) execute(new UpdateStatementCallback())).intValue(); + } + + public int[] batchUpdate(final String[] sql) throws DataAccessException { + Assert.notEmpty(sql, "SQL array must not be empty"); + if (logger.isDebugEnabled()) { + logger.debug("Executing SQL batch update of " + sql.length + " statements"); + } + + class BatchUpdateStatementCallback implements StatementCallback, SqlProvider { + private String currSql; + public Object doInStatement(Statement stmt) throws SQLException, DataAccessException { + int[] rowsAffected = new int[sql.length]; + if (JdbcUtils.supportsBatchUpdates(stmt.getConnection())) { + for (int i = 0; i < sql.length; i++) { + this.currSql = sql[i]; + stmt.addBatch(sql[i]); + } + rowsAffected = stmt.executeBatch(); + } + else { + for (int i = 0; i < sql.length; i++) { + this.currSql = sql[i]; + if (!stmt.execute(sql[i])) { + rowsAffected[i] = stmt.getUpdateCount(); + } + else { + throw new InvalidDataAccessApiUsageException("Invalid batch SQL statement: " + sql[i]); + } + } + } + return rowsAffected; + } + public String getSql() { + return currSql; + } + } + return (int[]) execute(new BatchUpdateStatementCallback()); + } + + + //------------------------------------------------------------------------- + // Methods dealing with prepared statements + //------------------------------------------------------------------------- + + public Object execute(PreparedStatementCreator psc, PreparedStatementCallback action) + throws DataAccessException { + + Assert.notNull(psc, "PreparedStatementCreator must not be null"); + Assert.notNull(action, "Callback object must not be null"); + if (logger.isDebugEnabled()) { + String sql = getSql(psc); + logger.debug("Executing prepared SQL statement" + (sql != null ? " [" + sql + "]" : "")); + } + + Connection con = DataSourceUtils.getConnection(getDataSource()); + PreparedStatement ps = null; + try { + Connection conToUse = con; + if (this.nativeJdbcExtractor != null && + this.nativeJdbcExtractor.isNativeConnectionNecessaryForNativePreparedStatements()) { + conToUse = this.nativeJdbcExtractor.getNativeConnection(con); + } + ps = psc.createPreparedStatement(conToUse); + applyStatementSettings(ps); + PreparedStatement psToUse = ps; + if (this.nativeJdbcExtractor != null) { + psToUse = this.nativeJdbcExtractor.getNativePreparedStatement(ps); + } + Object result = action.doInPreparedStatement(psToUse); + handleWarnings(ps); + return result; + } + catch (SQLException ex) { + // Release Connection early, to avoid potential connection pool deadlock + // in the case when the exception translator hasn't been initialized yet. + if (psc instanceof ParameterDisposer) { + ((ParameterDisposer) psc).cleanupParameters(); + } + String sql = getSql(psc); + psc = null; + JdbcUtils.closeStatement(ps); + ps = null; + DataSourceUtils.releaseConnection(con, getDataSource()); + con = null; + throw getExceptionTranslator().translate("PreparedStatementCallback", sql, ex); + } + finally { + if (psc instanceof ParameterDisposer) { + ((ParameterDisposer) psc).cleanupParameters(); + } + JdbcUtils.closeStatement(ps); + DataSourceUtils.releaseConnection(con, getDataSource()); + } + } + + public Object execute(String sql, PreparedStatementCallback action) throws DataAccessException { + return execute(new SimplePreparedStatementCreator(sql), action); + } + + /** + * Query using a prepared statement, allowing for a PreparedStatementCreator + * and a PreparedStatementSetter. Most other query methods use this method, + * but application code will always work with either a creator or a setter. + * @param psc Callback handler that can create a PreparedStatement given a + * Connection + * @param pss object that knows how to set values on the prepared statement. + * If this is null, the SQL will be assumed to contain no bind parameters. + * @param rse object that will extract results. + * @return an arbitrary result object, as returned by the ResultSetExtractor + * @throws DataAccessException if there is any problem + */ + public Object query( + PreparedStatementCreator psc, final PreparedStatementSetter pss, final ResultSetExtractor rse) + throws DataAccessException { + + Assert.notNull(rse, "ResultSetExtractor must not be null"); + logger.debug("Executing prepared SQL query"); + + return execute(psc, new PreparedStatementCallback() { + public Object doInPreparedStatement(PreparedStatement ps) throws SQLException { + ResultSet rs = null; + try { + if (pss != null) { + pss.setValues(ps); + } + rs = ps.executeQuery(); + ResultSet rsToUse = rs; + if (nativeJdbcExtractor != null) { + rsToUse = nativeJdbcExtractor.getNativeResultSet(rs); + } + return rse.extractData(rsToUse); + } + finally { + JdbcUtils.closeResultSet(rs); + if (pss instanceof ParameterDisposer) { + ((ParameterDisposer) pss).cleanupParameters(); + } + } + } + }); + } + + public Object query(PreparedStatementCreator psc, ResultSetExtractor rse) throws DataAccessException { + return query(psc, null, rse); + } + + public Object query(String sql, PreparedStatementSetter pss, ResultSetExtractor rse) throws DataAccessException { + return query(new SimplePreparedStatementCreator(sql), pss, rse); + } + + public Object query(String sql, Object[] args, int[] argTypes, ResultSetExtractor rse) throws DataAccessException { + return query(sql, new ArgTypePreparedStatementSetter(args, argTypes), rse); + } + + public Object query(String sql, Object[] args, ResultSetExtractor rse) throws DataAccessException { + return query(sql, new ArgPreparedStatementSetter(args), rse); + } + + public void query(PreparedStatementCreator psc, RowCallbackHandler rch) throws DataAccessException { + query(psc, new RowCallbackHandlerResultSetExtractor(rch)); + } + + public void query(String sql, PreparedStatementSetter pss, RowCallbackHandler rch) throws DataAccessException { + query(sql, pss, new RowCallbackHandlerResultSetExtractor(rch)); + } + + public void query(String sql, Object[] args, int[] argTypes, RowCallbackHandler rch) throws DataAccessException { + query(sql, new ArgTypePreparedStatementSetter(args, argTypes), rch); + } + + public void query(String sql, Object[] args, RowCallbackHandler rch) throws DataAccessException { + query(sql, new ArgPreparedStatementSetter(args), rch); + } + + public List query(PreparedStatementCreator psc, RowMapper rowMapper) throws DataAccessException { + return (List) query(psc, new RowMapperResultSetExtractor(rowMapper)); + } + + public List query(String sql, PreparedStatementSetter pss, RowMapper rowMapper) throws DataAccessException { + return (List) query(sql, pss, new RowMapperResultSetExtractor(rowMapper)); + } + + public List query(String sql, Object[] args, int[] argTypes, RowMapper rowMapper) throws DataAccessException { + return (List) query(sql, args, argTypes, new RowMapperResultSetExtractor(rowMapper)); + } + + public List query(String sql, Object[] args, RowMapper rowMapper) throws DataAccessException { + return (List) query(sql, args, new RowMapperResultSetExtractor(rowMapper)); + } + + public Object queryForObject(String sql, Object[] args, int[] argTypes, RowMapper rowMapper) + throws DataAccessException { + + List results = (List) query(sql, args, argTypes, new RowMapperResultSetExtractor(rowMapper, 1)); + return DataAccessUtils.requiredSingleResult(results); + } + + public Object queryForObject(String sql, Object[] args, RowMapper rowMapper) throws DataAccessException { + List results = (List) query(sql, args, new RowMapperResultSetExtractor(rowMapper, 1)); + return DataAccessUtils.requiredSingleResult(results); + } + + public Object queryForObject(String sql, Object[] args, int[] argTypes, Class requiredType) + throws DataAccessException { + + return queryForObject(sql, args, argTypes, getSingleColumnRowMapper(requiredType)); + } + + public Object queryForObject(String sql, Object[] args, Class requiredType) throws DataAccessException { + return queryForObject(sql, args, getSingleColumnRowMapper(requiredType)); + } + + public Map queryForMap(String sql, Object[] args, int[] argTypes) throws DataAccessException { + return (Map) queryForObject(sql, args, argTypes, getColumnMapRowMapper()); + } + + public Map queryForMap(String sql, Object[] args) throws DataAccessException { + return (Map) queryForObject(sql, args, getColumnMapRowMapper()); + } + + public long queryForLong(String sql, Object[] args, int[] argTypes) throws DataAccessException { + Number number = (Number) queryForObject(sql, args, argTypes, Long.class); + return (number != null ? number.longValue() : 0); + } + + public long queryForLong(String sql, Object[] args) throws DataAccessException { + Number number = (Number) queryForObject(sql, args, Long.class); + return (number != null ? number.longValue() : 0); + } + + public int queryForInt(String sql, Object[] args, int[] argTypes) throws DataAccessException { + Number number = (Number) queryForObject(sql, args, argTypes, Integer.class); + return (number != null ? number.intValue() : 0); + } + + public int queryForInt(String sql, Object[] args) throws DataAccessException { + Number number = (Number) queryForObject(sql, args, Integer.class); + return (number != null ? number.intValue() : 0); + } + + public List queryForList(String sql, Object[] args, int[] argTypes, Class elementType) throws DataAccessException { + return query(sql, args, argTypes, getSingleColumnRowMapper(elementType)); + } + + public List queryForList(String sql, Object[] args, Class elementType) throws DataAccessException { + return query(sql, args, getSingleColumnRowMapper(elementType)); + } + + public List queryForList(String sql, Object[] args, int[] argTypes) throws DataAccessException { + return query(sql, args, argTypes, getColumnMapRowMapper()); + } + + public List queryForList(String sql, Object[] args) throws DataAccessException { + return query(sql, args, getColumnMapRowMapper()); + } + + public SqlRowSet queryForRowSet(String sql, Object[] args, int[] argTypes) throws DataAccessException { + return (SqlRowSet) query(sql, args, argTypes, new SqlRowSetResultSetExtractor()); + } + + public SqlRowSet queryForRowSet(String sql, Object[] args) throws DataAccessException { + return (SqlRowSet) query(sql, args, new SqlRowSetResultSetExtractor()); + } + + protected int update(final PreparedStatementCreator psc, final PreparedStatementSetter pss) + throws DataAccessException { + + logger.debug("Executing prepared SQL update"); + + Integer result = (Integer) execute(psc, new PreparedStatementCallback() { + public Object doInPreparedStatement(PreparedStatement ps) throws SQLException { + try { + if (pss != null) { + pss.setValues(ps); + } + int rows = ps.executeUpdate(); + if (logger.isDebugEnabled()) { + logger.debug("SQL update affected " + rows + " rows"); + } + return new Integer(rows); + } + finally { + if (pss instanceof ParameterDisposer) { + ((ParameterDisposer) pss).cleanupParameters(); + } + } + } + }); + return result.intValue(); + } + + public int update(PreparedStatementCreator psc) throws DataAccessException { + return update(psc, (PreparedStatementSetter) null); + } + + public int update(final PreparedStatementCreator psc, final KeyHolder generatedKeyHolder) + throws DataAccessException { + + Assert.notNull(generatedKeyHolder, "KeyHolder must not be null"); + logger.debug("Executing SQL update and returning generated keys"); + + Integer result = (Integer) execute(psc, new PreparedStatementCallback() { + public Object doInPreparedStatement(PreparedStatement ps) throws SQLException { + int rows = ps.executeUpdate(); + List generatedKeys = generatedKeyHolder.getKeyList(); + generatedKeys.clear(); + ResultSet keys = ps.getGeneratedKeys(); + if (keys != null) { + try { + RowMapper rowMapper = getColumnMapRowMapper(); + RowMapperResultSetExtractor rse = new RowMapperResultSetExtractor(rowMapper, 1); + generatedKeys.addAll((List) rse.extractData(keys)); + } + finally { + JdbcUtils.closeResultSet(keys); + } + } + if (logger.isDebugEnabled()) { + logger.debug("SQL update affected " + rows + " rows and returned " + generatedKeys.size() + " keys"); + } + return new Integer(rows); + } + }); + return result.intValue(); + } + + public int update(String sql, PreparedStatementSetter pss) throws DataAccessException { + return update(new SimplePreparedStatementCreator(sql), pss); + } + + public int update(String sql, Object[] args, int[] argTypes) throws DataAccessException { + return update(sql, new ArgTypePreparedStatementSetter(args, argTypes)); + } + + public int update(String sql, Object[] args) throws DataAccessException { + return update(sql, new ArgPreparedStatementSetter(args)); + } + + public int[] batchUpdate(String sql, final BatchPreparedStatementSetter pss) throws DataAccessException { + if (logger.isDebugEnabled()) { + logger.debug("Executing SQL batch update [" + sql + "]"); + } + + return (int[]) execute(sql, new PreparedStatementCallback() { + public Object doInPreparedStatement(PreparedStatement ps) throws SQLException { + try { + int batchSize = pss.getBatchSize(); + InterruptibleBatchPreparedStatementSetter ipss = + (pss instanceof InterruptibleBatchPreparedStatementSetter ? + (InterruptibleBatchPreparedStatementSetter) pss : null); + if (JdbcUtils.supportsBatchUpdates(ps.getConnection())) { + for (int i = 0; i < batchSize; i++) { + pss.setValues(ps, i); + if (ipss != null && ipss.isBatchExhausted(i)) { + break; + } + ps.addBatch(); + } + return ps.executeBatch(); + } + else { + List rowsAffected = new ArrayList(); + for (int i = 0; i < batchSize; i++) { + pss.setValues(ps, i); + if (ipss != null && ipss.isBatchExhausted(i)) { + break; + } + rowsAffected.add(new Integer(ps.executeUpdate())); + } + int[] rowsAffectedArray = new int[rowsAffected.size()]; + for (int i = 0; i < rowsAffectedArray.length; i++) { + rowsAffectedArray[i] = ((Integer) rowsAffected.get(i)).intValue(); + } + return rowsAffectedArray; + } + } + finally { + if (pss instanceof ParameterDisposer) { + ((ParameterDisposer) pss).cleanupParameters(); + } + } + } + }); + } + + + //------------------------------------------------------------------------- + // Methods dealing with callable statements + //------------------------------------------------------------------------- + + public Object execute(CallableStatementCreator csc, CallableStatementCallback action) + throws DataAccessException { + + Assert.notNull(csc, "CallableStatementCreator must not be null"); + Assert.notNull(action, "Callback object must not be null"); + if (logger.isDebugEnabled()) { + String sql = getSql(csc); + logger.debug("Calling stored procedure" + (sql != null ? " [" + sql + "]" : "")); + } + + Connection con = DataSourceUtils.getConnection(getDataSource()); + CallableStatement cs = null; + try { + Connection conToUse = con; + if (this.nativeJdbcExtractor != null) { + conToUse = this.nativeJdbcExtractor.getNativeConnection(con); + } + cs = csc.createCallableStatement(conToUse); + applyStatementSettings(cs); + CallableStatement csToUse = cs; + if (this.nativeJdbcExtractor != null) { + csToUse = this.nativeJdbcExtractor.getNativeCallableStatement(cs); + } + Object result = action.doInCallableStatement(csToUse); + handleWarnings(cs); + return result; + } + catch (SQLException ex) { + // Release Connection early, to avoid potential connection pool deadlock + // in the case when the exception translator hasn't been initialized yet. + if (csc instanceof ParameterDisposer) { + ((ParameterDisposer) csc).cleanupParameters(); + } + String sql = getSql(csc); + csc = null; + JdbcUtils.closeStatement(cs); + cs = null; + DataSourceUtils.releaseConnection(con, getDataSource()); + con = null; + throw getExceptionTranslator().translate("CallableStatementCallback", sql, ex); + } + finally { + if (csc instanceof ParameterDisposer) { + ((ParameterDisposer) csc).cleanupParameters(); + } + JdbcUtils.closeStatement(cs); + DataSourceUtils.releaseConnection(con, getDataSource()); + } + } + + public Object execute(String callString, CallableStatementCallback action) throws DataAccessException { + return execute(new SimpleCallableStatementCreator(callString), action); + } + + public Map call(CallableStatementCreator csc, List declaredParameters) throws DataAccessException { + final List updateCountParameters = new ArrayList(); + final List resultSetParameters = new ArrayList(); + final List callParameters = new ArrayList(); + for (int i = 0; i < declaredParameters.size(); i++) { + SqlParameter parameter = (SqlParameter) declaredParameters.get(i); + if (parameter.isResultsParameter()) { + if (parameter instanceof SqlReturnResultSet) { + resultSetParameters.add(parameter); + } + else { + updateCountParameters.add(parameter); + } + } + else { + callParameters.add(parameter); + } + } + return (Map) execute(csc, new CallableStatementCallback() { + public Object doInCallableStatement(CallableStatement cs) throws SQLException { + boolean retVal = cs.execute(); + int updateCount = cs.getUpdateCount(); + if (logger.isDebugEnabled()) { + logger.debug("CallableStatement.execute() returned '" + retVal + "'"); + logger.debug("CallableStatement.getUpdateCount() returned " + updateCount); + } + Map returnedResults = createResultsMap(); + if (retVal || updateCount != -1) { + returnedResults.putAll(extractReturnedResults(cs, updateCountParameters, resultSetParameters, updateCount)); + } + returnedResults.putAll(extractOutputParameters(cs, callParameters)); + return returnedResults; + } + }); + } + + /** + * Extract returned ResultSets from the completed stored procedure. + * @param cs JDBC wrapper for the stored procedure + * @param updateCountParameters Parameter list of declared update count parameters for the stored procedure + * @param resultSetParameters Parameter list of declared resturn resultSet parameters for the stored procedure + * @return Map that contains returned results + */ + protected Map extractReturnedResults( + CallableStatement cs, List updateCountParameters, List resultSetParameters, int updateCount) + throws SQLException { + + Map returnedResults = new HashMap(); + int rsIndex = 0; + int updateIndex = 0; + boolean moreResults; + if (!skipResultsProcessing) { + do { + if (updateCount == -1) { + if (resultSetParameters != null && resultSetParameters.size() > rsIndex) { + SqlReturnResultSet declaredRsParam = (SqlReturnResultSet)resultSetParameters.get(rsIndex); + returnedResults.putAll(processResultSet(cs.getResultSet(), declaredRsParam)); + rsIndex++; + } + else { + if (!skipUndeclaredResults) { + String rsName = RETURN_RESULT_SET_PREFIX + (rsIndex + 1); + SqlReturnResultSet undeclaredRsParam = new SqlReturnResultSet(rsName, new ColumnMapRowMapper()); + logger.info("Added default SqlReturnResultSet parameter named " + rsName); + returnedResults.putAll(processResultSet(cs.getResultSet(), undeclaredRsParam)); + rsIndex++; + } + } + } + else { + if (updateCountParameters != null && updateCountParameters.size() > updateIndex) { + SqlReturnUpdateCount ucParam = (SqlReturnUpdateCount)updateCountParameters.get(updateIndex); + String declaredUcName = ucParam.getName(); + returnedResults.put(declaredUcName, new Integer(updateCount)); + updateIndex++; + } + else { + if (!skipUndeclaredResults) { + String undeclaredUcName = RETURN_UPDATE_COUNT_PREFIX + (updateIndex + 1); + logger.info("Added default SqlReturnUpdateCount parameter named " + undeclaredUcName); + returnedResults.put(undeclaredUcName, new Integer(updateCount)); + updateIndex++; + } + } + } + moreResults = cs.getMoreResults(); + updateCount = cs.getUpdateCount(); + if (logger.isDebugEnabled()) { + logger.debug("CallableStatement.getUpdateCount() returned " + updateCount); + } + } + while (moreResults || updateCount != -1); + } + return returnedResults; + } + + /** + * Extract output parameters from the completed stored procedure. + * @param cs JDBC wrapper for the stored procedure + * @param parameters parameter list for the stored procedure + * @return Map that contains returned results + */ + protected Map extractOutputParameters(CallableStatement cs, List parameters) throws SQLException { + Map returnedResults = new HashMap(); + int sqlColIndex = 1; + for (int i = 0; i < parameters.size(); i++) { + SqlParameter param = (SqlParameter) parameters.get(i); + if (param instanceof SqlOutParameter) { + SqlOutParameter outParam = (SqlOutParameter) param; + if (outParam.isReturnTypeSupported()) { + Object out = outParam.getSqlReturnType().getTypeValue( + cs, sqlColIndex, outParam.getSqlType(), outParam.getTypeName()); + returnedResults.put(outParam.getName(), out); + } + else { + Object out = cs.getObject(sqlColIndex); + if (out instanceof ResultSet) { + if (outParam.isResultSetSupported()) { + returnedResults.putAll(processResultSet((ResultSet) out, outParam)); + } + else { + String rsName = outParam.getName(); + SqlReturnResultSet rsParam = new SqlReturnResultSet(rsName, new ColumnMapRowMapper()); + returnedResults.putAll(processResultSet(cs.getResultSet(), rsParam)); + logger.info("Added default SqlReturnResultSet parameter named " + rsName); + } + } + else { + returnedResults.put(outParam.getName(), out); + } + } + } + if (!(param.isResultsParameter())) { + sqlColIndex++; + } + } + return returnedResults; + } + + /** + * Process the given ResultSet from a stored procedure. + * @param rs the ResultSet to process + * @param param the corresponding stored procedure parameter + * @return Map that contains returned results + */ + protected Map processResultSet(ResultSet rs, ResultSetSupportingSqlParameter param) throws SQLException { + if (rs == null) { + return Collections.EMPTY_MAP; + } + Map returnedResults = new HashMap(); + try { + ResultSet rsToUse = rs; + if (this.nativeJdbcExtractor != null) { + rsToUse = this.nativeJdbcExtractor.getNativeResultSet(rs); + } + if (param.getRowMapper() != null) { + RowMapper rowMapper = param.getRowMapper(); + Object result = (new RowMapperResultSetExtractor(rowMapper)).extractData(rsToUse); + returnedResults.put(param.getName(), result); + } + else if (param.getRowCallbackHandler() != null) { + RowCallbackHandler rch = param.getRowCallbackHandler(); + (new RowCallbackHandlerResultSetExtractor(rch)).extractData(rsToUse); + returnedResults.put(param.getName(), "ResultSet returned from stored procedure was processed"); + } + else if (param.getResultSetExtractor() != null) { + Object result = param.getResultSetExtractor().extractData(rsToUse); + returnedResults.put(param.getName(), result); + } + } + finally { + JdbcUtils.closeResultSet(rs); + } + return returnedResults; + } + + + //------------------------------------------------------------------------- + // Implementation hooks and helper methods + //------------------------------------------------------------------------- + + /** + * Create a new RowMapper for reading columns as key-value pairs. + * @return the RowMapper to use + * @see ColumnMapRowMapper + */ + protected RowMapper getColumnMapRowMapper() { + return new ColumnMapRowMapper(); + } + + /** + * Create a new RowMapper for reading result objects from a single column. + * @param requiredType the type that each result object is expected to match + * @return the RowMapper to use + * @see SingleColumnRowMapper + */ + protected RowMapper getSingleColumnRowMapper(Class requiredType) { + return new SingleColumnRowMapper(requiredType); + } + + /** + * Create a Map instance to be used as results map. + *

If "isResultsMapCaseInsensitive" has been set to true, a linked case-insensitive Map + * will be created if possible, else a plain HashMap (see Spring's CollectionFactory). + * @return the results Map instance + * @see #setResultsMapCaseInsensitive + * @see org.springframework.core.CollectionFactory#createLinkedCaseInsensitiveMapIfPossible + */ + protected Map createResultsMap() { + if (isResultsMapCaseInsensitive()) { + return CollectionFactory.createLinkedCaseInsensitiveMapIfPossible(10); + } + else { + return new LinkedHashMap(); + } + } + + /** + * Prepare the given JDBC Statement (or PreparedStatement or CallableStatement), + * applying statement settings such as fetch size, max rows, and query timeout. + * @param stmt the JDBC Statement to prepare + * @throws SQLException if thrown by JDBC API + * @see #setFetchSize + * @see #setMaxRows + * @see #setQueryTimeout + * @see org.springframework.jdbc.datasource.DataSourceUtils#applyTransactionTimeout + */ + protected void applyStatementSettings(Statement stmt) throws SQLException { + int fetchSize = getFetchSize(); + if (fetchSize > 0) { + stmt.setFetchSize(fetchSize); + } + int maxRows = getMaxRows(); + if (maxRows > 0) { + stmt.setMaxRows(maxRows); + } + DataSourceUtils.applyTimeout(stmt, getDataSource(), getQueryTimeout()); + } + + /** + * Throw an SQLWarningException if we're not ignoring warnings, + * else log the warnings (at debug level). + * @param stmt the current JDBC statement + * @throws SQLWarningException if not ignoring warnings + * @see org.springframework.jdbc.SQLWarningException + */ + protected void handleWarnings(Statement stmt) throws SQLException { + if (isIgnoreWarnings()) { + if (logger.isDebugEnabled()) { + SQLWarning warningToLog = stmt.getWarnings(); + while (warningToLog != null) { + logger.debug("SQLWarning ignored: SQL state '" + warningToLog.getSQLState() + "', error code '" + + warningToLog.getErrorCode() + "', message [" + warningToLog.getMessage() + "]"); + warningToLog = warningToLog.getNextWarning(); + } + } + } + else { + handleWarnings(stmt.getWarnings()); + } + } + + /** + * Throw an SQLWarningException if encountering an actual warning. + * @param warning the warnings object from the current statement. + * May be null, in which case this method does nothing. + * @throws SQLWarningException in case of an actual warning to be raised + */ + protected void handleWarnings(SQLWarning warning) throws SQLWarningException { + if (warning != null) { + throw new SQLWarningException("Warning not ignored", warning); + } + } + + /** + * Determine SQL from potential provider object. + * @param sqlProvider object that's potentially a SqlProvider + * @return the SQL string, or null + * @see SqlProvider + */ + private static String getSql(Object sqlProvider) { + if (sqlProvider instanceof SqlProvider) { + return ((SqlProvider) sqlProvider).getSql(); + } + else { + return null; + } + } + + + /** + * Invocation handler that suppresses close calls on JDBC COnnections. + * Also prepares returned Statement (Prepared/CallbackStatement) objects. + * @see java.sql.Connection#close() + */ + private class CloseSuppressingInvocationHandler implements InvocationHandler { + + private final Connection target; + + public CloseSuppressingInvocationHandler(Connection target) { + this.target = target; + } + + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + // Invocation on ConnectionProxy interface coming in... + + if (method.getName().equals("getTargetConnection")) { + // Handle getTargetConnection method: return underlying Connection. + return this.target; + } + else if (method.getName().equals("equals")) { + // Only consider equal when proxies are identical. + return (proxy == args[0] ? Boolean.TRUE : Boolean.FALSE); + } + else if (method.getName().equals("hashCode")) { + // Use hashCode of PersistenceManager proxy. + return new Integer(System.identityHashCode(proxy)); + } + else if (method.getName().equals("close")) { + // Handle close method: suppress, not valid. + return null; + } + + // Invoke method on target Connection. + try { + Object retVal = method.invoke(this.target, args); + + // If return value is a JDBC Statement, apply statement settings + // (fetch size, max rows, transaction timeout). + if (retVal instanceof Statement) { + applyStatementSettings(((Statement) retVal)); + } + + return retVal; + } + catch (InvocationTargetException ex) { + throw ex.getTargetException(); + } + } + } + + + /** + * Simple adapter for PreparedStatementCreator, allowing to use a plain SQL statement. + */ + private static class SimplePreparedStatementCreator implements PreparedStatementCreator, SqlProvider { + + private final String sql; + + public SimplePreparedStatementCreator(String sql) { + Assert.notNull(sql, "SQL must not be null"); + this.sql = sql; + } + + public PreparedStatement createPreparedStatement(Connection con) throws SQLException { + return con.prepareStatement(this.sql); + } + + public String getSql() { + return this.sql; + } + } + + + /** + * Simple adapter for CallableStatementCreator, allowing to use a plain SQL statement. + */ + private static class SimpleCallableStatementCreator implements CallableStatementCreator, SqlProvider { + + private final String callString; + + public SimpleCallableStatementCreator(String callString) { + Assert.notNull(callString, "Call string must not be null"); + this.callString = callString; + } + + public CallableStatement createCallableStatement(Connection con) throws SQLException { + return con.prepareCall(this.callString); + } + + public String getSql() { + return this.callString; + } + } + + + /** + * Adapter to enable use of a RowCallbackHandler inside a ResultSetExtractor. + *

Uses a regular ResultSet, so we have to be careful when using it: + * We don't use it for navigating since this could lead to unpredictable consequences. + */ + private static class RowCallbackHandlerResultSetExtractor implements ResultSetExtractor { + + private final RowCallbackHandler rch; + + public RowCallbackHandlerResultSetExtractor(RowCallbackHandler rch) { + this.rch = rch; + } + + public Object extractData(ResultSet rs) throws SQLException { + while (rs.next()) { + this.rch.processRow(rs); + } + return null; + } + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/ParameterDisposer.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/ParameterDisposer.java new file mode 100644 index 0000000000..6687ec1688 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/ParameterDisposer.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2005 the original author or authors. + * + * Licensed 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.springframework.jdbc.core; + +/** + * Interface to be implemented by objects that can close resources + * allocated by parameters like SqlLobValues. + * + *

Typically implemented by PreparedStatementCreators and + * PreparedStatementSetters that support DisposableSqlTypeValue + * objects (e.g. SqlLobValue) as parameters. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @since 1.1 + * @see PreparedStatementCreator + * @see PreparedStatementSetter + * @see DisposableSqlTypeValue + * @see org.springframework.jdbc.core.support.SqlLobValue + */ +public interface ParameterDisposer { + + /** + * Close the resources allocated by parameters that the implementing + * object holds, for example in case of a DisposableSqlTypeValue + * (like a SqlLobValue). + * @see DisposableSqlTypeValue#cleanup + * @see org.springframework.jdbc.core.support.SqlLobValue#cleanup + */ + public void cleanupParameters(); + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/ParameterMapper.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/ParameterMapper.java new file mode 100644 index 0000000000..a7a9928dea --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/ParameterMapper.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed 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.springframework.jdbc.core; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Map; + +/** + * Implement this interface when parameters need to be customized based + * on the connection. We might need to do this to make use of proprietary + * features, available only with a specific Connection type. + * + * @author Rod Johnson + * @author Thomas Risberg + * @see CallableStatementCreatorFactory#newCallableStatementCreator(ParameterMapper) + * @see org.springframework.jdbc.object.StoredProcedure#execute(ParameterMapper) + */ +public interface ParameterMapper { + + /** + * Create a Map of input parameters, keyed by name. + * @param con JDBC connection. This is useful (and the purpose of this interface) + * if we need to do something RDBMS-specific with a proprietary Connection + * implementation class. This class conceals such proprietary details. However, + * it is best to avoid using such proprietary RDBMS features if possible. + * @throws SQLException if a SQLException is encountered setting + * parameter values (that is, there's no need to catch SQLException) + * @return Map of input parameters, keyed by name (never null) + */ + Map createMap(Connection con) throws SQLException; + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/PreparedStatementCallback.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/PreparedStatementCallback.java new file mode 100644 index 0000000000..0e528d85ce --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/PreparedStatementCallback.java @@ -0,0 +1,80 @@ +/* + * Copyright 2002-2005 the original author or authors. + * + * Licensed 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.springframework.jdbc.core; + +import java.sql.PreparedStatement; +import java.sql.SQLException; + +import org.springframework.dao.DataAccessException; + +/** + * Generic callback interface for code that operates on a PreparedStatement. + * Allows to execute any number of operations on a single PreparedStatement, + * for example a single executeUpdate call or repeated + * executeUpdate calls with varying parameters. + * + *

Used internally by JdbcTemplate, but also useful for application code. + * Note that the passed-in PreparedStatement can have been created by the + * framework or by a custom PreparedStatementCreator. However, the latter is + * hardly ever necessary, as most custom callback actions will perform updates + * in which case a standard PreparedStatement is fine. Custom actions will + * always set parameter values themselves, so that PreparedStatementCreator + * capability is not needed either. + * + * @author Juergen Hoeller + * @since 16.03.2004 + * @see JdbcTemplate#execute(String, PreparedStatementCallback) + * @see JdbcTemplate#execute(PreparedStatementCreator, PreparedStatementCallback) + */ +public interface PreparedStatementCallback { + + /** + * Gets called by JdbcTemplate.execute with an active JDBC + * PreparedStatement. Does not need to care about closing the Statement + * or the Connection, or about handling transactions: this will all be + * handled by Spring's JdbcTemplate. + * + *

NOTE: Any ResultSets opened should be closed in finally blocks + * within the callback implementation. Spring will close the Statement + * object after the callback returned, but this does not necessarily imply + * that the ResultSet resources will be closed: the Statement objects might + * get pooled by the connection pool, with close calls only + * returning the object to the pool but not physically closing the resources. + * + *

If called without a thread-bound JDBC transaction (initiated by + * DataSourceTransactionManager), the code will simply get executed on the + * JDBC connection with its transactional semantics. If JdbcTemplate is + * configured to use a JTA-aware DataSource, the JDBC connection and thus + * the callback code will be transactional if a JTA transaction is active. + * + *

Allows for returning a result object created within the callback, i.e. + * a domain object or a collection of domain objects. Note that there's + * special support for single step actions: see JdbcTemplate.queryForObject etc. + * A thrown RuntimeException is treated as application exception, it gets + * propagated to the caller of the template. + * + * @param ps active JDBC PreparedStatement + * @return a result object, or null if none + * @throws SQLException if thrown by a JDBC method, to be auto-converted + * to a DataAccessException by a SQLExceptionTranslator + * @throws DataAccessException in case of custom exceptions + * @see JdbcTemplate#queryForObject(String, Object[], Class) + * @see JdbcTemplate#queryForList(String, Object[]) + */ + Object doInPreparedStatement(PreparedStatement ps) throws SQLException, DataAccessException; + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/PreparedStatementCreator.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/PreparedStatementCreator.java new file mode 100644 index 0000000000..dc92215618 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/PreparedStatementCreator.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2005 the original author or authors. + * + * Licensed 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.springframework.jdbc.core; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +/** + * One of the two central callback interfaces used by the JdbcTemplate class. + * This interface creates a PreparedStatement given a connection, provided + * by the JdbcTemplate class. Implementations are responsible for providing + * SQL and any necessary parameters. + * + *

Implementations do not need to concern themselves with + * SQLExceptions that may be thrown from operations they attempt. + * The JdbcTemplate class will catch and handle SQLExceptions appropriately. + * + *

A PreparedStatementCreator should also implement the SqlProvider interface + * if it is able to provide the SQL it uses for PreparedStatement creation. + * This allows for better contextual information in case of exceptions. + * + * @author Rod Johnson + * @see JdbcTemplate#execute(PreparedStatementCreator, PreparedStatementCallback) + * @see JdbcTemplate#query(PreparedStatementCreator, RowCallbackHandler) + * @see JdbcTemplate#update(PreparedStatementCreator) + * @see SqlProvider + */ +public interface PreparedStatementCreator { + + /** + * Create a statement in this connection. Allows implementations to use + * PreparedStatements. The JdbcTemplate will close the created statement. + * @param con Connection to use to create statement + * @return a prepared statement + * @throws SQLException there is no need to catch SQLExceptions + * that may be thrown in the implementation of this method. + * The JdbcTemplate class will handle them. + */ + PreparedStatement createPreparedStatement(Connection con) throws SQLException; + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/PreparedStatementCreatorFactory.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/PreparedStatementCreatorFactory.java new file mode 100644 index 0000000000..401dcb0b69 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/PreparedStatementCreatorFactory.java @@ -0,0 +1,321 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.core; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.dao.InvalidDataAccessResourceUsageException; +import org.springframework.jdbc.support.nativejdbc.NativeJdbcExtractor; +import org.springframework.util.Assert; + +/** + * Helper class that efficiently creates multiple {@link PreparedStatementCreator} + * objects with different parameters based on a SQL statement and a single + * set of parameter declarations. + * + * @author Rod Johnson + * @author Thomas Risberg + * @author Juergen Hoeller + */ +public class PreparedStatementCreatorFactory { + + /** The SQL, which won't change when the parameters change */ + private final String sql; + + /** List of SqlParameter objects. May not be null. */ + private final List declaredParameters; + + private int resultSetType = ResultSet.TYPE_FORWARD_ONLY; + + private boolean updatableResults = false; + + private boolean returnGeneratedKeys = false; + + private String[] generatedKeysColumnNames = null; + + private NativeJdbcExtractor nativeJdbcExtractor; + + + /** + * Create a new factory. Will need to add parameters via the + * {@link #addParameter} method or have no parameters. + */ + public PreparedStatementCreatorFactory(String sql) { + this.sql = sql; + this.declaredParameters = new LinkedList(); + } + + /** + * Create a new factory with the given SQL and JDBC types. + * @param sql SQL to execute + * @param types int array of JDBC types + */ + public PreparedStatementCreatorFactory(String sql, int[] types) { + this.sql = sql; + this.declaredParameters = SqlParameter.sqlTypesToAnonymousParameterList(types); + } + + /** + * Create a new factory with the given SQL and parameters. + * @param sql SQL + * @param declaredParameters list of {@link SqlParameter} objects + * @see SqlParameter + */ + public PreparedStatementCreatorFactory(String sql, List declaredParameters) { + this.sql = sql; + this.declaredParameters = declaredParameters; + } + + + /** + * Add a new declared parameter. + *

Order of parameter addition is significant. + * @param param the parameter to add to the list of declared parameters + */ + public void addParameter(SqlParameter param) { + this.declaredParameters.add(param); + } + + /** + * Set whether to use prepared statements that return a specific type of ResultSet. + * @param resultSetType the ResultSet type + * @see java.sql.ResultSet#TYPE_FORWARD_ONLY + * @see java.sql.ResultSet#TYPE_SCROLL_INSENSITIVE + * @see java.sql.ResultSet#TYPE_SCROLL_SENSITIVE + */ + public void setResultSetType(int resultSetType) { + this.resultSetType = resultSetType; + } + + /** + * Set whether to use prepared statements capable of returning updatable ResultSets. + */ + public void setUpdatableResults(boolean updatableResults) { + this.updatableResults = updatableResults; + } + + /** + * Set whether prepared statements should be capable of returning auto-generated keys. + */ + public void setReturnGeneratedKeys(boolean returnGeneratedKeys) { + this.returnGeneratedKeys = returnGeneratedKeys; + } + + /** + * Set the column names of the auto-generated keys. + */ + public void setGeneratedKeysColumnNames(String[] names) { + this.generatedKeysColumnNames = names; + } + + /** + * Specify the NativeJdbcExtractor to use for unwrapping PreparedStatements, if any. + */ + public void setNativeJdbcExtractor(NativeJdbcExtractor nativeJdbcExtractor) { + this.nativeJdbcExtractor = nativeJdbcExtractor; + } + + + /** + * Return a new PreparedStatementSetter for the given parameters. + * @param params list of parameters (may be null) + */ + public PreparedStatementSetter newPreparedStatementSetter(List params) { + return new PreparedStatementCreatorImpl(params != null ? params : Collections.EMPTY_LIST); + } + + /** + * Return a new PreparedStatementSetter for the given parameters. + * @param params the parameter array (may be null) + */ + public PreparedStatementSetter newPreparedStatementSetter(Object[] params) { + return new PreparedStatementCreatorImpl(params != null ? Arrays.asList(params) : Collections.EMPTY_LIST); + } + + /** + * Return a new PreparedStatementCreator for the given parameters. + * @param params list of parameters (may be null) + */ + public PreparedStatementCreator newPreparedStatementCreator(List params) { + return new PreparedStatementCreatorImpl(params != null ? params : Collections.EMPTY_LIST); + } + + /** + * Return a new PreparedStatementCreator for the given parameters. + * @param params the parameter array (may be null) + */ + public PreparedStatementCreator newPreparedStatementCreator(Object[] params) { + return new PreparedStatementCreatorImpl(params != null ? Arrays.asList(params) : Collections.EMPTY_LIST); + } + + /** + * Return a new PreparedStatementCreator for the given parameters. + * @param sqlToUse the actual SQL statement to use (if different from + * the factory's, for example because of named parameter expanding) + * @param params the parameter array (may be null) + */ + public PreparedStatementCreator newPreparedStatementCreator(String sqlToUse, Object[] params) { + return new PreparedStatementCreatorImpl( + sqlToUse, params != null ? Arrays.asList(params) : Collections.EMPTY_LIST); + } + + + /** + * PreparedStatementCreator implementation returned by this class. + */ + private class PreparedStatementCreatorImpl + implements PreparedStatementCreator, PreparedStatementSetter, SqlProvider, ParameterDisposer { + + private final String actualSql; + + private final List parameters; + + public PreparedStatementCreatorImpl(List parameters) { + this(sql, parameters); + } + + public PreparedStatementCreatorImpl(String actualSql, List parameters) { + this.actualSql = actualSql; + Assert.notNull(parameters, "Parameters List must not be null"); + this.parameters = parameters; + if (this.parameters.size() != declaredParameters.size()) { + // account for named parameters being used multiple times + Set names = new HashSet(); + for (int i = 0; i < parameters.size(); i++) { + Object o = parameters.get(i); + if (o instanceof SqlParameterValue) { + names.add(((SqlParameterValue)o).getName()); + } + else { + names.add("Parameter #" + i); + } + } + if (names.size() != declaredParameters.size()) { + throw new InvalidDataAccessApiUsageException( + "SQL [" + sql + "]: given " + names.size() + + " parameters but expected " + declaredParameters.size()); + } + } + } + + public PreparedStatement createPreparedStatement(Connection con) throws SQLException { + PreparedStatement ps = null; + if (generatedKeysColumnNames != null || returnGeneratedKeys) { + try { + if (generatedKeysColumnNames != null) { + ps = con.prepareStatement(this.actualSql, generatedKeysColumnNames); + } + else { + ps = con.prepareStatement(this.actualSql, PreparedStatement.RETURN_GENERATED_KEYS); + } + } + catch (AbstractMethodError ex) { + throw new InvalidDataAccessResourceUsageException( + "The JDBC driver is not compliant to JDBC 3.0 and thus " + + "does not support retrieval of auto-generated keys", ex); + } + } + else if (resultSetType == ResultSet.TYPE_FORWARD_ONLY && !updatableResults) { + ps = con.prepareStatement(this.actualSql); + } + else { + ps = con.prepareStatement(this.actualSql, resultSetType, + updatableResults ? ResultSet.CONCUR_UPDATABLE : ResultSet.CONCUR_READ_ONLY); + } + setValues(ps); + return ps; + } + + public void setValues(PreparedStatement ps) throws SQLException { + // Determine PreparedStatement to pass to custom types. + PreparedStatement psToUse = ps; + if (nativeJdbcExtractor != null) { + psToUse = nativeJdbcExtractor.getNativePreparedStatement(ps); + } + + // Set arguments: Does nothing if there are no parameters. + int sqlColIndx = 1; + for (int i = 0; i < this.parameters.size(); i++) { + Object in = this.parameters.get(i); + SqlParameter declaredParameter = null; + // SqlParameterValue overrides declared parameter metadata, in particular for + // independence from the declared parameter position in case of named parameters. + if (in instanceof SqlParameterValue) { + SqlParameterValue paramValue = (SqlParameterValue) in; + in = paramValue.getValue(); + declaredParameter = paramValue; + } + else { + if (declaredParameters.size() <= i) { + throw new InvalidDataAccessApiUsageException( + "SQL [" + sql + "]: unable to access parameter number " + (i + 1) + + " given only " + declaredParameters.size() + " parameters"); + + } + declaredParameter = (SqlParameter) declaredParameters.get(i); + } + if (in instanceof Collection && declaredParameter.getSqlType() != Types.ARRAY) { + Collection entries = (Collection) in; + for (Iterator it = entries.iterator(); it.hasNext();) { + Object entry = it.next(); + if (entry instanceof Object[]) { + Object[] valueArray = ((Object[])entry); + for (int k = 0; k < valueArray.length; k++) { + Object argValue = valueArray[k]; + StatementCreatorUtils.setParameterValue(psToUse, sqlColIndx++, declaredParameter, argValue); + } + } + else { + StatementCreatorUtils.setParameterValue(psToUse, sqlColIndx++, declaredParameter, entry); + } + } + } + else { + StatementCreatorUtils.setParameterValue(psToUse, sqlColIndx++, declaredParameter, in); + } + } + } + + public String getSql() { + return sql; + } + + public void cleanupParameters() { + StatementCreatorUtils.cleanupParameters(this.parameters); + } + + public String toString() { + StringBuffer buf = new StringBuffer("PreparedStatementCreatorFactory.PreparedStatementCreatorImpl: sql=["); + buf.append(sql).append("]; parameters=").append(this.parameters); + return buf.toString(); + } + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/PreparedStatementSetter.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/PreparedStatementSetter.java new file mode 100644 index 0000000000..fb12a1b48d --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/PreparedStatementSetter.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc.core; + +import java.sql.PreparedStatement; +import java.sql.SQLException; + +/** + * General callback interface used by the {@link JdbcTemplate} class. + * + *

This interface sets values on a {@link java.sql.PreparedStatement} provided + * by the JdbcTemplate class, for each of a number of updates in a batch using the + * same SQL. Implementations are responsible for setting any necessary parameters. + * SQL with placeholders will already have been supplied. + * + *

It's easier to use this interface than {@link PreparedStatementCreator}: + * The JdbcTemplate will create the PreparedStatement, with the callback + * only being responsible for setting parameter values. + * + *

Implementations do not need to concern themselves with + * SQLExceptions that may be thrown from operations they attempt. + * The JdbcTemplate class will catch and handle SQLExceptions appropriately. + * + * @author Rod Johnson + * @since March 2, 2003 + * @see JdbcTemplate#update(String, PreparedStatementSetter) + * @see JdbcTemplate#query(String, PreparedStatementSetter, ResultSetExtractor) + */ +public interface PreparedStatementSetter { + + /** + * Set parameter values on the given PreparedStatement. + * @param ps the PreparedStatement to invoke setter methods on + * @throws SQLException if a SQLException is encountered + * (i.e. there is no need to catch SQLException) + */ + void setValues(PreparedStatement ps) throws SQLException; + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/ResultSetExtractor.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/ResultSetExtractor.java new file mode 100644 index 0000000000..118bc9d0b2 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/ResultSetExtractor.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc.core; + +import java.sql.ResultSet; +import java.sql.SQLException; + +import org.springframework.dao.DataAccessException; + +/** + * Callback interface used by {@link JdbcTemplate}'s query methods. + * Implementations of this interface perform the actual work of extracting + * results from a {@link java.sql.ResultSet}, but don't need to worry + * about exception handling. {@link java.sql.SQLException SQLExceptions} + * will be caught and handled by the calling JdbcTemplate. + * + *

This interface is mainly used within the JDBC framework itself. + * A {@link RowMapper} is usually a simpler choice for ResultSet processing, + * mapping one result object per row instead of one result object for + * the entire ResultSet. + * + *

Note: In contrast to a {@link RowCallbackHandler}, a ResultSetExtractor + * object is typically stateless and thus reusable, as long as it doesn't + * access stateful resources (such as output streams when streaming LOB + * contents) or keep result state within the object. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @since April 24, 2003 + * @see JdbcTemplate + * @see RowCallbackHandler + * @see RowMapper + * @see org.springframework.jdbc.core.support.AbstractLobStreamingResultSetExtractor + */ +public interface ResultSetExtractor { + + /** + * Implementations must implement this method to process the entire ResultSet. + * @param rs ResultSet to extract data from. Implementations should + * not close this: it will be closed by the calling JdbcTemplate. + * @return an arbitrary result object, or null if none + * (the extractor will typically be stateful in the latter case). + * @throws SQLException if a SQLException is encountered getting column + * values or navigating (that is, there's no need to catch SQLException) + * @throws DataAccessException in case of custom exceptions + */ + Object extractData(ResultSet rs) throws SQLException, DataAccessException; + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/ResultSetSupportingSqlParameter.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/ResultSetSupportingSqlParameter.java new file mode 100644 index 0000000000..003a7d6fa1 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/ResultSetSupportingSqlParameter.java @@ -0,0 +1,136 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc.core; + +/** + * Common base class for ResultSet-supporting SqlParameters like + * {@link SqlOutParameter} and {@link SqlReturnResultSet}. + * + * @author Juergen Hoeller + * @since 1.0.2 + */ +public class ResultSetSupportingSqlParameter extends SqlParameter { + + private ResultSetExtractor resultSetExtractor; + + private RowCallbackHandler rowCallbackHandler; + + private RowMapper rowMapper; + + + /** + * Create a new ResultSetSupportingSqlParameter. + * @param name name of the parameter, as used in input and output maps + * @param sqlType SQL type of the parameter according to java.sql.Types + */ + public ResultSetSupportingSqlParameter(String name, int sqlType) { + super(name, sqlType); + } + + /** + * Create a new ResultSetSupportingSqlParameter. + * @param name name of the parameter, as used in input and output maps + * @param sqlType SQL type of the parameter according to java.sql.Types + * @param scale the number of digits after the decimal point + * (for DECIMAL and NUMERIC types) + */ + public ResultSetSupportingSqlParameter(String name, int sqlType, int scale) { + super(name, sqlType, scale); + } + + /** + * Create a new ResultSetSupportingSqlParameter. + * @param name name of the parameter, as used in input and output maps + * @param sqlType SQL type of the parameter according to java.sql.Types + * @param typeName the type name of the parameter (optional) + */ + public ResultSetSupportingSqlParameter(String name, int sqlType, String typeName) { + super(name, sqlType, typeName); + } + + /** + * Create a new ResultSetSupportingSqlParameter. + * @param name name of the parameter, as used in input and output maps + * @param sqlType SQL type of the parameter according to java.sql.Types + * @param rse ResultSetExtractor to use for parsing the ResultSet + */ + public ResultSetSupportingSqlParameter(String name, int sqlType, ResultSetExtractor rse) { + super(name, sqlType); + this.resultSetExtractor = rse; + } + + /** + * Create a new ResultSetSupportingSqlParameter. + * @param name name of the parameter, as used in input and output maps + * @param sqlType SQL type of the parameter according to java.sql.Types + * @param rch RowCallbackHandler to use for parsing the ResultSet + */ + public ResultSetSupportingSqlParameter(String name, int sqlType, RowCallbackHandler rch) { + super(name, sqlType); + this.rowCallbackHandler = rch; + } + + /** + * Create a new ResultSetSupportingSqlParameter. + * @param name name of the parameter, as used in input and output maps + * @param sqlType SQL type of the parameter according to java.sql.Types + * @param rm RowMapper to use for parsing the ResultSet + */ + public ResultSetSupportingSqlParameter(String name, int sqlType, RowMapper rm) { + super(name, sqlType); + this.rowMapper = rm; + } + + + /** + * Does this parameter support a ResultSet, i.e. does it hold a + * ResultSetExtractor, RowCallbackHandler or RowMapper? + */ + public boolean isResultSetSupported() { + return (this.resultSetExtractor != null || this.rowCallbackHandler != null || this.rowMapper != null); + } + + /** + * Return the ResultSetExtractor held by this parameter, if any. + */ + public ResultSetExtractor getResultSetExtractor() { + return resultSetExtractor; + } + + /** + * Return the RowCallbackHandler held by this parameter, if any. + */ + public RowCallbackHandler getRowCallbackHandler() { + return this.rowCallbackHandler; + } + + /** + * Return the RowMapper held by this parameter, if any. + */ + public RowMapper getRowMapper() { + return this.rowMapper; + } + + + /** + *

This implementation always returns false. + */ + public boolean isInputValueProvided() { + return false; + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/RowCallbackHandler.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/RowCallbackHandler.java new file mode 100644 index 0000000000..4d34ccffe8 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/RowCallbackHandler.java @@ -0,0 +1,60 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc.core; + +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + * An interface used by {@link JdbcTemplate} for processing rows of a + * {@link java.sql.ResultSet} on a per-row basis. Implementations of + * this interface perform the actual work of processing each row + * but don't need to worry about exception handling. + * {@link java.sql.SQLException SQLExceptions} will be caught and handled + * by the calling JdbcTemplate. + * + *

In contrast to a {@link ResultSetExtractor}, a RowCallbackHandler + * object is typically stateful: It keeps the result state within the + * object, to be available for later inspection. See + * {@link RowCountCallbackHandler} for a usage example. + * + *

Consider using a {@link RowMapper} instead if you need to map + * exactly one result object per row, assembling them into a List. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see JdbcTemplate + * @see RowMapper + * @see ResultSetExtractor + * @see RowCountCallbackHandler + */ +public interface RowCallbackHandler { + + /** + * Implementations must implement this method to process each row of data + * in the ResultSet. This method should not call next() on + * the ResultSet; it is only supposed to extract values of the current row. + *

Exactly what the implementation chooses to do is up to it: + * A trivial implementation might simply count rows, while another + * implementation might build an XML document. + * @param rs the ResultSet to process (pre-initialized for the current row) + * @throws SQLException if a SQLException is encountered getting + * column values (that is, there's no need to catch SQLException) + */ + void processRow(ResultSet rs) throws SQLException; + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/RowCountCallbackHandler.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/RowCountCallbackHandler.java new file mode 100644 index 0000000000..88efe072ec --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/RowCountCallbackHandler.java @@ -0,0 +1,137 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed 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.springframework.jdbc.core; + +import org.springframework.jdbc.support.JdbcUtils; + +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; + +/** + * Implementation of RowCallbackHandler. Convenient superclass for callback handlers. + * An instance can only be used once. + * + *

We can either use this on its own (for example, in a test case, to ensure + * that our result sets have valid dimensions), or use it as a superclass + * for callback handlers that actually do something, and will benefit + * from the dimension information it provides. + * + *

A usage example with JdbcTemplate: + * + *

JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);  // reusable object
+ * 
+ * RowCountCallbackHandler countCallback = new RowCountCallbackHandler();  // not reusable
+ * jdbcTemplate.query("select * from user", countCallback);
+ * int rowCount = countCallback.getRowCount();
+ * + * @author Rod Johnson + * @since May 3, 2001 + */ +public class RowCountCallbackHandler implements RowCallbackHandler { + + /** Rows we've seen so far */ + private int rowCount; + + /** Columns we've seen so far */ + private int columnCount; + + /** + * Indexed from 0. Type (as in java.sql.Types) for the columns + * as returned by ResultSetMetaData object. + */ + private int[] columnTypes; + + /** + * Indexed from 0. Column name as returned by ResultSetMetaData object. + */ + private String[] columnNames; + + + /** + * Implementation of ResultSetCallbackHandler. + * Work out column size if this is the first row, otherwise just count rows. + *

Subclasses can perform custom extraction or processing + * by overriding the processRow(ResultSet, int) method. + * @see #processRow(java.sql.ResultSet, int) + */ + public final void processRow(ResultSet rs) throws SQLException { + if (this.rowCount == 0) { + ResultSetMetaData rsmd = rs.getMetaData(); + this.columnCount = rsmd.getColumnCount(); + this.columnTypes = new int[this.columnCount]; + this.columnNames = new String[this.columnCount]; + for (int i = 0; i < this.columnCount; i++) { + this.columnTypes[i] = rsmd.getColumnType(i + 1); + this.columnNames[i] = JdbcUtils.lookupColumnName(rsmd, i + 1); + } + // could also get column names + } + processRow(rs, this.rowCount++); + } + + /** + * Subclasses may override this to perform custom extraction + * or processing. This class's implementation does nothing. + * @param rs ResultSet to extract data from. This method is + * invoked for each row + * @param rowNum number of the current row (starting from 0) + */ + protected void processRow(ResultSet rs, int rowNum) throws SQLException { + } + + + /** + * Return the types of the columns as java.sql.Types constants + * Valid after processRow is invoked the first time. + * @return the types of the columns as java.sql.Types constants. + * Indexed from 0 to n-1. + */ + public final int[] getColumnTypes() { + return columnTypes; + } + + /** + * Return the names of the columns. + * Valid after processRow is invoked the first time. + * @return the names of the columns. + * Indexed from 0 to n-1. + */ + public final String[] getColumnNames() { + return columnNames; + } + + /** + * Return the row count of this ResultSet + * Only valid after processing is complete + * @return the number of rows in this ResultSet + */ + public final int getRowCount() { + return rowCount; + } + + /** + * Return the number of columns in this result set. + * Valid once we've seen the first row, + * so subclasses can use it during processing + * @return the number of columns in this result set + */ + public final int getColumnCount() { + return columnCount; + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/RowMapper.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/RowMapper.java new file mode 100644 index 0000000000..1b439c9048 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/RowMapper.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc.core; + +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + * An interface used by {@link JdbcTemplate} for mapping rows of a + * {@link java.sql.ResultSet} on a per-row basis. Implementations of this + * interface perform the actual work of mapping each row to a result object, + * but don't need to worry about exception handling. + * {@link java.sql.SQLException SQLExceptions} will be caught and handled + * by the calling JdbcTemplate. + * + *

Typically used either for {@link JdbcTemplate}'s query methods + * or for out parameters of stored procedures. RowMapper objects are + * typically stateless and thus reusable; they are an ideal choice for + * implementing row-mapping logic in a single place. + * + *

Alternatively, consider subclassing + * {@link org.springframework.jdbc.object.MappingSqlQuery} from the + * jdbc.object package: Instead of working with separate + * JdbcTemplate and RowMapper objects, you can build executable query + * objects (containing row-mapping logic) in that style. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @see JdbcTemplate + * @see RowCallbackHandler + * @see ResultSetExtractor + * @see org.springframework.jdbc.object.MappingSqlQuery + */ +public interface RowMapper { + + /** + * Implementations must implement this method to map each row of data + * in the ResultSet. This method should not call next() on + * the ResultSet; it is only supposed to map values of the current row. + * @param rs the ResultSet to map (pre-initialized for the current row) + * @param rowNum the number of the current row + * @return the result object for the current row + * @throws SQLException if a SQLException is encountered getting + * column values (that is, there's no need to catch SQLException) + */ + Object mapRow(ResultSet rs, int rowNum) throws SQLException; + +} + \ No newline at end of file diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/RowMapperResultSetExtractor.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/RowMapperResultSetExtractor.java new file mode 100644 index 0000000000..989fc5b57f --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/RowMapperResultSetExtractor.java @@ -0,0 +1,97 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed 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.springframework.jdbc.core; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +import org.springframework.util.Assert; + +/** + * Adapter implementation of the ResultSetExtractor interface that delegates + * to a RowMapper which is supposed to create an object for each row. + * Each object is added to the results List of this ResultSetExtractor. + * + *

Useful for the typical case of one object per row in the database table. + * The number of entries in the results list will match the number of rows. + * + *

Note that a RowMapper object is typically stateless and thus reusable; + * just the RowMapperResultSetExtractor adapter is stateful. + * + *

A usage example with JdbcTemplate: + * + *

JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);  // reusable object
+ * RowMapper rowMapper = new UserRowMapper();  // reusable object
+ *
+ * List allUsers = (List) jdbcTemplate.query(
+ *     "select * from user",
+ *     new RowMapperResultSetExtractor(rowMapper, 10));
+ *
+ * User user = (User) jdbcTemplate.queryForObject(
+ *     "select * from user where id=?", new Object[] {id},
+ *     new RowMapperResultSetExtractor(rowMapper, 1));
+ * + *

Alternatively, consider subclassing MappingSqlQuery from the jdbc.object + * package: Instead of working with separate JdbcTemplate and RowMapper objects, + * you can have executable query objects (containing row-mapping logic) there. + * + * @author Juergen Hoeller + * @since 1.0.2 + * @see RowMapper + * @see JdbcTemplate + * @see org.springframework.jdbc.object.MappingSqlQuery + */ +public class RowMapperResultSetExtractor implements ResultSetExtractor { + + private final RowMapper rowMapper; + + private final int rowsExpected; + + + /** + * Create a new RowMapperResultSetExtractor. + * @param rowMapper the RowMapper which creates an object for each row + */ + public RowMapperResultSetExtractor(RowMapper rowMapper) { + this(rowMapper, 0); + } + + /** + * Create a new RowMapperResultSetExtractor. + * @param rowMapper the RowMapper which creates an object for each row + * @param rowsExpected the number of expected rows + * (just used for optimized collection handling) + */ + public RowMapperResultSetExtractor(RowMapper rowMapper, int rowsExpected) { + Assert.notNull(rowMapper, "RowMapper is required"); + this.rowMapper = rowMapper; + this.rowsExpected = rowsExpected; + } + + + public Object extractData(ResultSet rs) throws SQLException { + List results = (this.rowsExpected > 0 ? new ArrayList(this.rowsExpected) : new ArrayList()); + int rowNum = 0; + while (rs.next()) { + results.add(this.rowMapper.mapRow(rs, rowNum++)); + } + return results; + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/SingleColumnRowMapper.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/SingleColumnRowMapper.java new file mode 100644 index 0000000000..87eab93d2a --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/SingleColumnRowMapper.java @@ -0,0 +1,185 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.core; + +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; + +import org.springframework.dao.TypeMismatchDataAccessException; +import org.springframework.jdbc.IncorrectResultSetColumnCountException; +import org.springframework.jdbc.support.JdbcUtils; +import org.springframework.util.NumberUtils; + +/** + * {@link RowMapper} implementation that converts a single column into a single + * result value per row. Expects to operate on a java.sql.ResultSet + * that just contains a single column. + * + *

The type of the result value for each row can be specified. The value + * for the single column will be extracted from the ResultSet + * and converted into the specified target type. + * + * @author Juergen Hoeller + * @since 1.2 + * @see JdbcTemplate#queryForList(String, Class) + * @see JdbcTemplate#queryForObject(String, Class) + */ +public class SingleColumnRowMapper implements RowMapper { + + private Class requiredType; + + + /** + * Create a new SingleColumnRowMapper. + * @see #setRequiredType + */ + public SingleColumnRowMapper() { + } + + /** + * Create a new SingleColumnRowMapper. + * @param requiredType the type that each result object is expected to match + */ + public SingleColumnRowMapper(Class requiredType) { + this.requiredType = requiredType; + } + + /** + * Set the type that each result object is expected to match. + *

If not specified, the column value will be exposed as + * returned by the JDBC driver. + */ + public void setRequiredType(Class requiredType) { + this.requiredType = requiredType; + } + + + /** + * Extract a value for the single column in the current row. + *

Validates that there is only one column selected, + * then delegates to getColumnValue() and also + * convertValueToRequiredType, if necessary. + * @see java.sql.ResultSetMetaData#getColumnCount() + * @see #getColumnValue(java.sql.ResultSet, int, Class) + * @see #convertValueToRequiredType(Object, Class) + */ + public Object mapRow(ResultSet rs, int rowNum) throws SQLException { + // Validate column count. + ResultSetMetaData rsmd = rs.getMetaData(); + int nrOfColumns = rsmd.getColumnCount(); + if (nrOfColumns != 1) { + throw new IncorrectResultSetColumnCountException(1, nrOfColumns); + } + + // Extract column value from JDBC ResultSet. + Object result = getColumnValue(rs, 1, this.requiredType); + if (result != null && this.requiredType != null && !this.requiredType.isInstance(result)) { + // Extracted value does not match already: try to convert it. + try { + return convertValueToRequiredType(result, this.requiredType); + } + catch (IllegalArgumentException ex) { + throw new TypeMismatchDataAccessException( + "Type mismatch affecting row number " + rowNum + " and column type '" + + rsmd.getColumnTypeName(1) + "': " + ex.getMessage()); + } + } + return result; + } + + /** + * Retrieve a JDBC object value for the specified column. + *

The default implementation calls + * {@link JdbcUtils#getResultSetValue(java.sql.ResultSet, int, Class)}. + * If no required type has been specified, this method delegates to + * getColumnValue(rs, index), which basically calls + * ResultSet.getObject(index) but applies some additional + * default conversion to appropriate value types. + * @param rs is the ResultSet holding the data + * @param index is the column index + * @param requiredType the type that each result object is expected to match + * (or null if none specified) + * @return the Object value + * @throws SQLException in case of extraction failure + * @see org.springframework.jdbc.support.JdbcUtils#getResultSetValue(java.sql.ResultSet, int, Class) + * @see #getColumnValue(java.sql.ResultSet, int) + */ + protected Object getColumnValue(ResultSet rs, int index, Class requiredType) throws SQLException { + if (requiredType != null) { + return JdbcUtils.getResultSetValue(rs, index, requiredType); + } + else { + // No required type specified -> perform default extraction. + return getColumnValue(rs, index); + } + } + + /** + * Retrieve a JDBC object value for the specified column, using the most + * appropriate value type. Called if no required type has been specified. + *

The default implementation delegates to JdbcUtils.getResultSetValue(), + * which uses the ResultSet.getObject(index) method. Additionally, + * it includes a "hack" to get around Oracle returning a non-standard object for + * their TIMESTAMP datatype. See the JdbcUtils#getResultSetValue() + * javadoc for details. + * @param rs is the ResultSet holding the data + * @param index is the column index + * @return the Object value + * @throws SQLException in case of extraction failure + * @see org.springframework.jdbc.support.JdbcUtils#getResultSetValue(java.sql.ResultSet, int) + */ + protected Object getColumnValue(ResultSet rs, int index) throws SQLException { + return JdbcUtils.getResultSetValue(rs, index); + } + + /** + * Convert the given column value to the specified required type. + * Only called if the extracted column value does not match already. + *

If the required type is String, the value will simply get stringified + * via toString(). In case of a Number, the value will be + * converted into a Number, either through number conversion or through + * String parsing (depending on the value type). + * @param value the column value as extracted from getColumnValue() + * (never null) + * @param requiredType the type that each result object is expected to match + * (never null) + * @return the converted value + * @see #getColumnValue(java.sql.ResultSet, int, Class) + */ + protected Object convertValueToRequiredType(Object value, Class requiredType) { + if (String.class.equals(requiredType)) { + return value.toString(); + } + else if (Number.class.isAssignableFrom(requiredType)) { + if (value instanceof Number) { + // Convert original Number to target Number class. + return NumberUtils.convertNumberToTargetClass(((Number) value), requiredType); + } + else { + // Convert stringified value to target Number class. + return NumberUtils.parseNumber(value.toString(), requiredType); + } + } + else { + throw new IllegalArgumentException( + "Value [" + value + "] is of type [" + value.getClass().getName() + + "] and cannot be converted to required type [" + requiredType.getName() + "]"); + } + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/SqlInOutParameter.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/SqlInOutParameter.java new file mode 100644 index 0000000000..c236e9f024 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/SqlInOutParameter.java @@ -0,0 +1,112 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc.core; + +/** + * Subclass of {@link SqlOutParameter} to represent an INOUT parameter. + * Will return true for SqlParameter's {@link #isInputValueProvided} + * test, in contrast to a standard SqlOutParameter. + * + *

Output parameters - like all stored procedure parameters - + * must have names. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @since 2.0 + */ +public class SqlInOutParameter extends SqlOutParameter { + + /** + * Create a new SqlInOutParameter. + * @param name name of the parameter, as used in input and output maps + * @param sqlType SQL type of the parameter according to java.sql.Types + */ + public SqlInOutParameter(String name, int sqlType) { + super(name, sqlType); + } + + /** + * Create a new SqlInOutParameter. + * @param name name of the parameter, as used in input and output maps + * @param sqlType SQL type of the parameter according to java.sql.Types + * @param scale the number of digits after the decimal point + * (for DECIMAL and NUMERIC types) + */ + public SqlInOutParameter(String name, int sqlType, int scale) { + super(name, sqlType, scale); + } + + /** + * Create a new SqlInOutParameter. + * @param name name of the parameter, as used in input and output maps + * @param sqlType SQL type of the parameter according to java.sql.Types + * @param typeName the type name of the parameter (optional) + */ + public SqlInOutParameter(String name, int sqlType, String typeName) { + super(name, sqlType, typeName); + } + + /** + * Create a new SqlInOutParameter. + * @param name name of the parameter, as used in input and output maps + * @param sqlType SQL type of the parameter according to java.sql.Types + * @param typeName the type name of the parameter (optional) + * @param sqlReturnType custom value handler for complex type (optional) + */ + public SqlInOutParameter(String name, int sqlType, String typeName, SqlReturnType sqlReturnType) { + super(name, sqlType, typeName, sqlReturnType); + } + + /** + * Create a new SqlInOutParameter. + * @param name name of the parameter, as used in input and output maps + * @param sqlType SQL type of the parameter according to java.sql.Types + * @param rse ResultSetExtractor to use for parsing the ResultSet + */ + public SqlInOutParameter(String name, int sqlType, ResultSetExtractor rse) { + super(name, sqlType, rse); + } + + /** + * Create a new SqlInOutParameter. + * @param name name of the parameter, as used in input and output maps + * @param sqlType SQL type of the parameter according to java.sql.Types + * @param rch RowCallbackHandler to use for parsing the ResultSet + */ + public SqlInOutParameter(String name, int sqlType, RowCallbackHandler rch) { + super(name, sqlType, rch); + } + + /** + * Create a new SqlInOutParameter. + * @param name name of the parameter, as used in input and output maps + * @param sqlType SQL type of the parameter according to java.sql.Types + * @param rm RowMapper to use for parsing the ResultSet + */ + public SqlInOutParameter(String name, int sqlType, RowMapper rm) { + super(name, sqlType, rm); + } + + + /** + * This implementation always returns true. + */ + public boolean isInputValueProvided() { + return true; + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/SqlOutParameter.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/SqlOutParameter.java new file mode 100644 index 0000000000..9454e2bdaf --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/SqlOutParameter.java @@ -0,0 +1,125 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc.core; + +/** + * Subclass of SqlParameter to represent an output parameter. + * No additional properties: instanceof will be used to check + * for such types. + * + *

Output parameters - like all stored procedure parameters - + * must have names. + * + * @author Rod Johnson + * @author Thomas Risberg + * @author Juergen Hoeller + * @see SqlReturnResultSet + * @see SqlInOutParameter + */ +public class SqlOutParameter extends ResultSetSupportingSqlParameter { + + private SqlReturnType sqlReturnType; + + + /** + * Create a new SqlOutParameter. + * @param name name of the parameter, as used in input and output maps + * @param sqlType SQL type of the parameter according to java.sql.Types + */ + public SqlOutParameter(String name, int sqlType) { + super(name, sqlType); + } + + /** + * Create a new SqlOutParameter. + * @param name name of the parameter, as used in input and output maps + * @param sqlType SQL type of the parameter according to java.sql.Types + * @param scale the number of digits after the decimal point + * (for DECIMAL and NUMERIC types) + */ + public SqlOutParameter(String name, int sqlType, int scale) { + super(name, sqlType, scale); + } + + /** + * Create a new SqlOutParameter. + * @param name name of the parameter, as used in input and output maps + * @param sqlType SQL type of the parameter according to java.sql.Types + * @param typeName the type name of the parameter (optional) + */ + public SqlOutParameter(String name, int sqlType, String typeName) { + super(name, sqlType, typeName); + } + + /** + * Create a new SqlOutParameter. + * @param name name of the parameter, as used in input and output maps + * @param sqlType SQL type of the parameter according to java.sql.Types + * @param typeName the type name of the parameter (optional) + * @param sqlReturnType custom value handler for complex type (optional) + */ + public SqlOutParameter(String name, int sqlType, String typeName, SqlReturnType sqlReturnType) { + super(name, sqlType, typeName); + this.sqlReturnType = sqlReturnType; + } + + /** + * Create a new SqlOutParameter. + * @param name name of the parameter, as used in input and output maps + * @param sqlType SQL type of the parameter according to java.sql.Types + * @param rse ResultSetExtractor to use for parsing the ResultSet + */ + public SqlOutParameter(String name, int sqlType, ResultSetExtractor rse) { + super(name, sqlType, rse); + } + + /** + * Create a new SqlOutParameter. + * @param name name of the parameter, as used in input and output maps + * @param sqlType SQL type of the parameter according to java.sql.Types + * @param rch RowCallbackHandler to use for parsing the ResultSet + */ + public SqlOutParameter(String name, int sqlType, RowCallbackHandler rch) { + super(name, sqlType, rch); + } + + /** + * Create a new SqlOutParameter. + * @param name name of the parameter, as used in input and output maps + * @param sqlType SQL type of the parameter according to java.sql.Types + * @param rm RowMapper to use for parsing the ResultSet + */ + public SqlOutParameter(String name, int sqlType, RowMapper rm) { + super(name, sqlType, rm); + } + + + /** + * Return the custom return type, if any. + */ + public SqlReturnType getSqlReturnType() { + return this.sqlReturnType; + } + + /** + * Return whether this parameter holds a custom return type. + */ + public boolean isReturnTypeSupported() { + return (this.sqlReturnType != null); + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/SqlParameter.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/SqlParameter.java new file mode 100644 index 0000000000..b2f86e37bd --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/SqlParameter.java @@ -0,0 +1,190 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc.core; + +import java.util.LinkedList; +import java.util.List; + +import org.springframework.util.Assert; + +/** + * Object to represent a SQL parameter definition. + * + *

Parameters may be anonymous, in which case "name" is null. + * However, all parameters must define a SQL type according to {@link java.sql.Types}. + * + * @author Rod Johnson + * @author Thomas Risberg + * @author Juergen Hoeller + * @see java.sql.Types + */ +public class SqlParameter { + + /** The name of the parameter, if any */ + private String name; + + /** SQL type constant from java.sql.Types */ + private final int sqlType; + + /** Used for types that are user-named like: STRUCT, DISTINCT, JAVA_OBJECT, named array types */ + private String typeName; + + + /** The scale to apply in case of a NUMERIC or DECIMAL type, if any */ + private Integer scale; + + + /** + * Create a new anonymous SqlParameter, supplying the SQL type. + * @param sqlType SQL type of the parameter according to java.sql.Types + */ + public SqlParameter(int sqlType) { + this.sqlType = sqlType; + } + + /** + * Create a new anonymous SqlParameter, supplying the SQL type. + * @param sqlType SQL type of the parameter according to java.sql.Types + * @param typeName the type name of the parameter (optional) + */ + public SqlParameter(int sqlType, String typeName) { + this.sqlType = sqlType; + this.typeName = typeName; + } + + /** + * Create a new anonymous SqlParameter, supplying the SQL type. + * @param sqlType SQL type of the parameter according to java.sql.Types + * @param scale the number of digits after the decimal point + * (for DECIMAL and NUMERIC types) + */ + public SqlParameter(int sqlType, int scale) { + this.sqlType = sqlType; + this.scale = new Integer(scale); + } + + /** + * Create a new SqlParameter, supplying name and SQL type. + * @param name name of the parameter, as used in input and output maps + * @param sqlType SQL type of the parameter according to java.sql.Types + */ + public SqlParameter(String name, int sqlType) { + this.name = name; + this.sqlType = sqlType; + } + + /** + * Create a new SqlParameter, supplying name and SQL type. + * @param name name of the parameter, as used in input and output maps + * @param sqlType SQL type of the parameter according to java.sql.Types + * @param typeName the type name of the parameter (optional) + */ + public SqlParameter(String name, int sqlType, String typeName) { + this.name = name; + this.sqlType = sqlType; + this.typeName = typeName; + } + + /** + * Create a new SqlParameter, supplying name and SQL type. + * @param name name of the parameter, as used in input and output maps + * @param sqlType SQL type of the parameter according to java.sql.Types + * @param scale the number of digits after the decimal point + * (for DECIMAL and NUMERIC types) + */ + public SqlParameter(String name, int sqlType, int scale) { + this.name = name; + this.sqlType = sqlType; + this.scale = new Integer(scale); + } + + /** + * Copy constructor. + * @param otherParam the SqlParameter object to copy from + */ + public SqlParameter(SqlParameter otherParam) { + Assert.notNull(otherParam, "SqlParameter object must not be null"); + this.name = otherParam.name; + this.sqlType = otherParam.sqlType; + this.typeName = otherParam.typeName; + this.scale = otherParam.scale; + } + + + /** + * Return the name of the parameter. + */ + public String getName() { + return this.name; + } + + /** + * Return the SQL type of the parameter. + */ + public int getSqlType() { + return this.sqlType; + } + + /** + * Return the type name of the parameter, if any. + */ + public String getTypeName() { + return this.typeName; + } + + /** + * Return the scale of the parameter, if any. + */ + public Integer getScale() { + return this.scale; + } + + + /** + * Return whether this parameter holds input values that should be set + * before execution even if they are null. + *

This implementation always returns true. + */ + public boolean isInputValueProvided() { + return true; + } + + /** + * Return whether this parameter is an implicit return parameter used during the + * results preocessing of the CallableStatement.getMoreResults/getUpdateCount. + *

This implementation always returns false. + */ + public boolean isResultsParameter() { + return false; + } + + + /** + * Convert a list of JDBC types, as defined in java.sql.Types, + * to a List of SqlParameter objects as used in this package. + */ + public static List sqlTypesToAnonymousParameterList(int[] types) { + List result = new LinkedList(); + if (types != null) { + for (int i = 0; i < types.length; i++) { + result.add(new SqlParameter(types[i])); + } + } + return result; + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/SqlParameterValue.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/SqlParameterValue.java new file mode 100644 index 0000000000..088b756e76 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/SqlParameterValue.java @@ -0,0 +1,93 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc.core; + +/** + * Object to represent a SQL parameter value, including parameter metadata + * such as the SQL type and the scale for numeric values. + * + *

Designed for use with {@link JdbcTemplate}'s operations that take an array of + * argument values: Each such argument value may be a SqlParameterValue, + * indicating the SQL type (and optionally the scale) instead of letting the + * template guess a default type. Note that this only applies to the operations with + * a 'plain' argument array, not to the overloaded variants with an explicit type array. + * + * @author Juergen Hoeller + * @since 2.0.5 + * @see java.sql.Types + * @see JdbcTemplate#query(String, Object[], ResultSetExtractor) + * @see JdbcTemplate#query(String, Object[], RowCallbackHandler) + * @see JdbcTemplate#query(String, Object[], RowMapper) + * @see JdbcTemplate#update(String, Object[]) + */ +public class SqlParameterValue extends SqlParameter { + + private final Object value; + + + /** + * Create a new SqlParameterValue, supplying the SQL type. + * @param sqlType SQL type of the parameter according to java.sql.Types + * @param value the value object + */ + public SqlParameterValue(int sqlType, Object value) { + super(sqlType); + this.value = value; + } + + /** + * Create a new SqlParameterValue, supplying the SQL type. + * @param sqlType SQL type of the parameter according to java.sql.Types + * @param typeName the type name of the parameter (optional) + * @param value the value object + */ + public SqlParameterValue(int sqlType, String typeName, Object value) { + super(sqlType, typeName); + this.value = value; + } + + /** + * Create a new SqlParameterValue, supplying the SQL type. + * @param sqlType SQL type of the parameter according to java.sql.Types + * @param scale the number of digits after the decimal point + * (for DECIMAL and NUMERIC types) + * @param value the value object + */ + public SqlParameterValue(int sqlType, int scale, Object value) { + super(sqlType, scale); + this.value = value; + } + + /** + * Create a new SqlParameterValue based on the given SqlParameter declaration. + * @param declaredParam the declared SqlParameter to define a value for + * @param value the value object + */ + public SqlParameterValue(SqlParameter declaredParam, Object value) { + super(declaredParam); + this.value = value; + } + + + /** + * Return the value object that this parameter value holds. + */ + public Object getValue() { + return this.value; + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/SqlProvider.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/SqlProvider.java new file mode 100644 index 0000000000..5dc642be86 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/SqlProvider.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2005 the original author or authors. + * + * Licensed 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.springframework.jdbc.core; + +/** + * Interface to be implemented by objects that can provide SQL strings. + * + *

Typically implemented by PreparedStatementCreators, CallableStatementCreators + * and StatementCallbacks that want to expose the SQL they use to create their + * statements, to allow for better contextual information in case of exceptions. + * + * @author Juergen Hoeller + * @since 16.03.2004 + * @see PreparedStatementCreator + * @see CallableStatementCreator + * @see StatementCallback + */ +public interface SqlProvider { + + /** + * Return the SQL string for this object, i.e. + * typically the SQL used for creating statements. + * @return the SQL string, or null + */ + String getSql(); + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/SqlReturnResultSet.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/SqlReturnResultSet.java new file mode 100644 index 0000000000..5cf7be27f8 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/SqlReturnResultSet.java @@ -0,0 +1,68 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed 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.springframework.jdbc.core; + +/** + * Represents a returned {@link java.sql.ResultSet} from a stored procedure call. + * + *

A {@link ResultSetExtractor}, {@link RowCallbackHandler} or {@link RowMapper} + * must be provided to handle any returned rows. + * + *

Returned {@link java.sql.ResultSet ResultSets} - like all stored procedure + * parameters - must have names. + * + * @author Thomas Risberg + * @author Juergen Hoeller + */ +public class SqlReturnResultSet extends ResultSetSupportingSqlParameter { + + /** + * Create a new instance of the {@link SqlReturnResultSet} class. + * @param name name of the parameter, as used in input and output maps + * @param extractor ResultSetExtractor to use for parsing the {@link java.sql.ResultSet} + */ + public SqlReturnResultSet(String name, ResultSetExtractor extractor) { + super(name, 0, extractor); + } + + /** + * Create a new instance of the {@link SqlReturnResultSet} class. + * @param name name of the parameter, as used in input and output maps + * @param handler RowCallbackHandler to use for parsing the {@link java.sql.ResultSet} + */ + public SqlReturnResultSet(String name, RowCallbackHandler handler) { + super(name, 0, handler); + } + + /** + * Create a new instance of the {@link SqlReturnResultSet} class. + * @param name name of the parameter, as used in input and output maps + * @param mapper RowMapper to use for parsing the {@link java.sql.ResultSet} + */ + public SqlReturnResultSet(String name, RowMapper mapper) { + super(name, 0, mapper); + } + + /** + * Return whether this parameter is an implicit return parameter used during the + * results preocessing of the CallableStatement.getMoreResults/getUpdateCount. + *

This implementation always returns true. + */ + public boolean isResultsParameter() { + return true; + } +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/SqlReturnType.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/SqlReturnType.java new file mode 100644 index 0000000000..f4d056a5ee --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/SqlReturnType.java @@ -0,0 +1,65 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc.core; + +import java.sql.CallableStatement; +import java.sql.SQLException; + +/** + * Interface to be implemented for retrieving values for more complex database-specific + * types not supported by the standard CallableStatement.getObject method. + * + *

Implementations perform the actual work of getting the actual values. They must + * implement the callback method getTypeValue which can throw SQLExceptions + * that will be caught and translated by the calling code. This callback method has + * access to the underlying Connection via the given CallableStatement object, if that + * should be needed to create any database-specific objects. + * + * @author Thomas Risberg + * @since 1.1 + * @see java.sql.Types + * @see java.sql.CallableStatement#getObject + * @see org.springframework.jdbc.object.StoredProcedure#execute(java.util.Map) + */ +public interface SqlReturnType { + + /** + * Constant that indicates an unknown (or unspecified) SQL type. + * Passed into setTypeValue if the original operation method does + * not specify a SQL type. + * @see java.sql.Types + * @see JdbcOperations#update(String, Object[]) + */ + int TYPE_UNKNOWN = Integer.MIN_VALUE; + + + /** + * Get the type value from the specific object. + * @param cs the CallableStatement to operate on + * @param paramIndex the index of the parameter for which we need to set the value + * @param sqlType SQL type of the parameter we are setting + * @param typeName the type name of the parameter + * @return the target value + * @throws SQLException if a SQLException is encountered setting parameter values + * (that is, there's no need to catch SQLException) + * @see java.sql.Types + * @see java.sql.CallableStatement#getObject + */ + Object getTypeValue(CallableStatement cs, int paramIndex, int sqlType, String typeName) + throws SQLException; + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/SqlReturnUpdateCount.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/SqlReturnUpdateCount.java new file mode 100644 index 0000000000..3a60e62e45 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/SqlReturnUpdateCount.java @@ -0,0 +1,41 @@ +package org.springframework.jdbc.core; + +import java.sql.Types; + +/** + * Represents a returned update count from a stored procedure call. + * + *

Returned update counts - like all stored procedure + * parameters - must have names. + * + * @author Thomas Risberg + */ +public class SqlReturnUpdateCount extends SqlParameter { + + /** + * Create a new instance of the {@link SqlReturnUpdateCount} class. + * @param name name of the parameter, as used in input and output maps + */ + public SqlReturnUpdateCount(String name) { + super(name, Types.INTEGER); + } + + + /** + * Return whether this parameter holds input values that should be set + * before execution even if they are null. + *

This implementation always returns false. + */ + public boolean isInputValueProvided() { + return false; + } + + /** + * Return whether this parameter is an implicit return parameter used during the + * results preocessing of the CallableStatement.getMoreResults/getUpdateCount. + *

This implementation always returns true. + */ + public boolean isResultsParameter() { + return true; + } +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/SqlRowSetResultSetExtractor.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/SqlRowSetResultSetExtractor.java new file mode 100644 index 0000000000..c17038027f --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/SqlRowSetResultSetExtractor.java @@ -0,0 +1,86 @@ +/* + * Copyright 2002-2005 the original author or authors. + * + * Licensed 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.springframework.jdbc.core; + +import java.sql.ResultSet; +import java.sql.SQLException; + +import javax.sql.rowset.CachedRowSet; + +import com.sun.rowset.CachedRowSetImpl; + +import org.springframework.jdbc.support.rowset.ResultSetWrappingSqlRowSet; +import org.springframework.jdbc.support.rowset.SqlRowSet; + +/** + * ResultSetExtractor implementation that returns a Spring SqlRowSet + * representation for each given ResultSet. + * + *

The default implementation uses a standard JDBC CachedRowSet underneath. + * This means that JDBC RowSet support needs to be available at runtime: + * by default, Sun's com.sun.rowset.CachedRowSetImpl class is + * used, which is part of JDK 1.5+ and also available separately as part of + * Sun's JDBC RowSet Implementations download (rowset.jar). + * + * @author Juergen Hoeller + * @since 1.2 + * @see #newCachedRowSet + * @see org.springframework.jdbc.support.rowset.SqlRowSet + * @see JdbcTemplate#queryForRowSet(String) + * @see javax.sql.rowset.CachedRowSet + */ +public class SqlRowSetResultSetExtractor implements ResultSetExtractor { + + public Object extractData(ResultSet rs) throws SQLException { + return createSqlRowSet(rs); + } + + /** + * Create a SqlRowSet that wraps the given ResultSet, + * representing its data in a disconnected fashion. + *

This implementation creates a Spring ResultSetWrappingSqlRowSet + * instance that wraps a standard JDBC CachedRowSet instance. + * Can be overridden to use a different implementation. + * @param rs the original ResultSet (connected) + * @return the disconnected SqlRowSet + * @throws SQLException if thrown by JDBC methods + * @see #newCachedRowSet + * @see org.springframework.jdbc.support.rowset.ResultSetWrappingSqlRowSet + */ + protected SqlRowSet createSqlRowSet(ResultSet rs) throws SQLException { + CachedRowSet rowSet = newCachedRowSet(); + rowSet.populate(rs); + return new ResultSetWrappingSqlRowSet(rowSet); + } + + /** + * Create a new CachedRowSet instance, to be populated by + * the createSqlRowSet implementation. + *

The default implementation creates a new instance of + * Sun's com.sun.rowset.CachedRowSetImpl class, + * which is part of JDK 1.5+ and also available separately + * as part of Sun's JDBC RowSet Implementations download. + * @return a new CachedRowSet instance + * @throws SQLException if thrown by JDBC methods + * @see #createSqlRowSet + * @see com.sun.rowset.CachedRowSetImpl + */ + protected CachedRowSet newCachedRowSet() throws SQLException { + return new CachedRowSetImpl(); + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/SqlTypeValue.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/SqlTypeValue.java new file mode 100644 index 0000000000..6a55f386fb --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/SqlTypeValue.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.core; + +import java.sql.PreparedStatement; +import java.sql.SQLException; + +import org.springframework.jdbc.support.JdbcUtils; + +/** + * Interface to be implemented for setting values for more complex database-specific + * types not supported by the standard setObject method. This is + * effectively an extended variant of {@link org.springframework.jdbc.support.SqlValue}. + * + *

Implementations perform the actual work of setting the actual values. They must + * implement the callback method setTypeValue which can throw SQLExceptions + * that will be caught and translated by the calling code. This callback method has + * access to the underlying Connection via the given PreparedStatement object, if that + * should be needed to create any database-specific objects. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @since 1.1 + * @see java.sql.Types + * @see java.sql.PreparedStatement#setObject + * @see JdbcOperations#update(String, Object[], int[]) + * @see org.springframework.jdbc.support.SqlValue + */ +public interface SqlTypeValue { + + /** + * Constant that indicates an unknown (or unspecified) SQL type. + * Passed into setTypeValue if the original operation method + * does not specify a SQL type. + * @see java.sql.Types + * @see JdbcOperations#update(String, Object[]) + */ + int TYPE_UNKNOWN = JdbcUtils.TYPE_UNKNOWN; + + + /** + * Set the type value on the given PreparedStatement. + * @param ps the PreparedStatement to work on + * @param paramIndex the index of the parameter for which we need to set the value + * @param sqlType SQL type of the parameter we are setting + * @param typeName the type name of the parameter (optional) + * @throws SQLException if a SQLException is encountered while setting parameter values + * @see java.sql.Types + * @see java.sql.PreparedStatement#setObject + */ + void setTypeValue(PreparedStatement ps, int paramIndex, int sqlType, String typeName) throws SQLException; + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/StatementCallback.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/StatementCallback.java new file mode 100644 index 0000000000..bcda002ec2 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/StatementCallback.java @@ -0,0 +1,73 @@ +/* + * Copyright 2002-2005 the original author or authors. + * + * Licensed 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.springframework.jdbc.core; + +import java.sql.SQLException; +import java.sql.Statement; + +import org.springframework.dao.DataAccessException; + +/** + * Generic callback interface for code that operates on a JDBC Statement. + * Allows to execute any number of operations on a single Statement, + * for example a single executeUpdate call or repeated + * executeUpdate calls with varying SQL. + * + *

Used internally by JdbcTemplate, but also useful for application code. + * + * @author Juergen Hoeller + * @since 16.03.2004 + * @see JdbcTemplate#execute(StatementCallback) + */ +public interface StatementCallback { + + /** + * Gets called by JdbcTemplate.execute with an active JDBC + * Statement. Does not need to care about closing the Statement or the + * Connection, or about handling transactions: this will all be handled + * by Spring's JdbcTemplate. + * + *

NOTE: Any ResultSets opened should be closed in finally blocks + * within the callback implementation. Spring will close the Statement + * object after the callback returned, but this does not necessarily imply + * that the ResultSet resources will be closed: the Statement objects might + * get pooled by the connection pool, with close calls only + * returning the object to the pool but not physically closing the resources. + * + *

If called without a thread-bound JDBC transaction (initiated by + * DataSourceTransactionManager), the code will simply get executed on the + * JDBC connection with its transactional semantics. If JdbcTemplate is + * configured to use a JTA-aware DataSource, the JDBC connection and thus + * the callback code will be transactional if a JTA transaction is active. + * + *

Allows for returning a result object created within the callback, i.e. + * a domain object or a collection of domain objects. Note that there's + * special support for single step actions: see JdbcTemplate.queryForObject etc. + * A thrown RuntimeException is treated as application exception, it gets + * propagated to the caller of the template. + * + * @param stmt active JDBC Statement + * @return a result object, or null if none + * @throws SQLException if thrown by a JDBC method, to be auto-converted + * to a DataAccessException by a SQLExceptionTranslator + * @throws DataAccessException in case of custom exceptions + * @see JdbcTemplate#queryForObject(String, Class) + * @see JdbcTemplate#queryForRowSet(String) + */ + Object doInStatement(Statement stmt) throws SQLException, DataAccessException; + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/StatementCreatorUtils.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/StatementCreatorUtils.java new file mode 100644 index 0000000000..8c2ee40322 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/StatementCreatorUtils.java @@ -0,0 +1,409 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.core; + +import java.io.StringWriter; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.sql.Blob; +import java.sql.Clob; +import java.sql.DatabaseMetaData; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Types; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.jdbc.support.SqlValue; + +/** + * Utility methods for PreparedStatementSetter/Creator and CallableStatementCreator + * implementations, providing sophisticated parameter management (including support + * for LOB values). + * + *

Used by PreparedStatementCreatorFactory and CallableStatementCreatorFactory, + * but also available for direct use in custom setter/creator implementations. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @since 1.1 + * @see PreparedStatementSetter + * @see PreparedStatementCreator + * @see CallableStatementCreator + * @see PreparedStatementCreatorFactory + * @see CallableStatementCreatorFactory + * @see SqlParameter + * @see SqlTypeValue + * @see org.springframework.jdbc.core.support.SqlLobValue + */ +public abstract class StatementCreatorUtils { + + private static final Log logger = LogFactory.getLog(StatementCreatorUtils.class); + + private static Map javaTypeToSqlTypeMap = new HashMap(32); + + static { + /* JDBC 3.0 only - not compatible with e.g. MySQL at present + javaTypeToSqlTypeMap.put(boolean.class, new Integer(Types.BOOLEAN)); + javaTypeToSqlTypeMap.put(Boolean.class, new Integer(Types.BOOLEAN)); + */ + javaTypeToSqlTypeMap.put(byte.class, new Integer(Types.TINYINT)); + javaTypeToSqlTypeMap.put(Byte.class, new Integer(Types.TINYINT)); + javaTypeToSqlTypeMap.put(short.class, new Integer(Types.SMALLINT)); + javaTypeToSqlTypeMap.put(Short.class, new Integer(Types.SMALLINT)); + javaTypeToSqlTypeMap.put(int.class, new Integer(Types.INTEGER)); + javaTypeToSqlTypeMap.put(Integer.class, new Integer(Types.INTEGER)); + javaTypeToSqlTypeMap.put(long.class, new Integer(Types.BIGINT)); + javaTypeToSqlTypeMap.put(Long.class, new Integer(Types.BIGINT)); + javaTypeToSqlTypeMap.put(BigInteger.class, new Integer(Types.BIGINT)); + javaTypeToSqlTypeMap.put(float.class, new Integer(Types.FLOAT)); + javaTypeToSqlTypeMap.put(Float.class, new Integer(Types.FLOAT)); + javaTypeToSqlTypeMap.put(double.class, new Integer(Types.DOUBLE)); + javaTypeToSqlTypeMap.put(Double.class, new Integer(Types.DOUBLE)); + javaTypeToSqlTypeMap.put(BigDecimal.class, new Integer(Types.DECIMAL)); + javaTypeToSqlTypeMap.put(java.sql.Date.class, new Integer(Types.DATE)); + javaTypeToSqlTypeMap.put(java.sql.Time.class, new Integer(Types.TIME)); + javaTypeToSqlTypeMap.put(java.sql.Timestamp.class, new Integer(Types.TIMESTAMP)); + javaTypeToSqlTypeMap.put(Blob.class, new Integer(Types.BLOB)); + javaTypeToSqlTypeMap.put(Clob.class, new Integer(Types.CLOB)); + } + + + /** + * Derive a default SQL type from the given Java type. + * @param javaType the Java type to translate + * @return the corresponding SQL type, or null if none found + */ + public static int javaTypeToSqlParameterType(Class javaType) { + Integer sqlType = (Integer) javaTypeToSqlTypeMap.get(javaType); + if (sqlType != null) { + return sqlType.intValue(); + } + if (Number.class.isAssignableFrom(javaType)) { + return Types.NUMERIC; + } + if (isStringValue(javaType)) { + return Types.VARCHAR; + } + if (isDateValue(javaType) || Calendar.class.isAssignableFrom(javaType)) { + return Types.TIMESTAMP; + } + return SqlTypeValue.TYPE_UNKNOWN; + } + + /** + * Set the value for a parameter. The method used is based on the SQL type + * of the parameter and we can handle complex types like arrays and LOBs. + * @param ps the prepared statement or callable statement + * @param paramIndex index of the parameter we are setting + * @param param the parameter as it is declared including type + * @param inValue the value to set + * @throws SQLException if thrown by PreparedStatement methods + */ + public static void setParameterValue( + PreparedStatement ps, int paramIndex, SqlParameter param, Object inValue) + throws SQLException { + + setParameterValueInternal(ps, paramIndex, param.getSqlType(), param.getTypeName(), param.getScale(), inValue); + } + + /** + * Set the value for a parameter. The method used is based on the SQL type + * of the parameter and we can handle complex types like arrays and LOBs. + * @param ps the prepared statement or callable statement + * @param paramIndex index of the parameter we are setting + * @param sqlType the SQL type of the parameter + * @param inValue the value to set (plain value or a SqlTypeValue) + * @throws SQLException if thrown by PreparedStatement methods + * @see SqlTypeValue + */ + public static void setParameterValue( + PreparedStatement ps, int paramIndex, int sqlType, Object inValue) + throws SQLException { + + setParameterValueInternal(ps, paramIndex, sqlType, null, null, inValue); + } + + /** + * Set the value for a parameter. The method used is based on the SQL type + * of the parameter and we can handle complex types like arrays and LOBs. + * @param ps the prepared statement or callable statement + * @param paramIndex index of the parameter we are setting + * @param sqlType the SQL type of the parameter + * @param typeName the type name of the parameter + * (optional, only used for SQL NULL and SqlTypeValue) + * @param inValue the value to set (plain value or a SqlTypeValue) + * @throws SQLException if thrown by PreparedStatement methods + * @see SqlTypeValue + */ + public static void setParameterValue( + PreparedStatement ps, int paramIndex, int sqlType, String typeName, Object inValue) + throws SQLException { + + setParameterValueInternal(ps, paramIndex, sqlType, typeName, null, inValue); + } + + /** + * Set the value for a parameter. The method used is based on the SQL type + * of the parameter and we can handle complex types like arrays and LOBs. + * @param ps the prepared statement or callable statement + * @param paramIndex index of the parameter we are setting + * @param sqlType the SQL type of the parameter + * @param typeName the type name of the parameter + * (optional, only used for SQL NULL and SqlTypeValue) + * @param scale the number of digits after the decimal point + * (for DECIMAL and NUMERIC types) + * @param inValue the value to set (plain value or a SqlTypeValue) + * @throws SQLException if thrown by PreparedStatement methods + * @see SqlTypeValue + */ + private static void setParameterValueInternal( + PreparedStatement ps, int paramIndex, int sqlType, String typeName, Integer scale, Object inValue) + throws SQLException { + + String typeNameToUse = typeName; + int sqlTypeToUse = sqlType; + Object inValueToUse = inValue; + + // override type info? + if (inValue instanceof SqlParameterValue) { + SqlParameterValue parameterValue = (SqlParameterValue) inValue; + if (logger.isDebugEnabled()) { + logger.debug("Overriding typeinfo with runtime info from SqlParameterValue: column index " + paramIndex + + ", SQL type " + parameterValue.getSqlType() + + ", Type name " + parameterValue.getTypeName()); + } + if (parameterValue.getSqlType() != SqlTypeValue.TYPE_UNKNOWN) { + sqlTypeToUse = parameterValue.getSqlType(); + } + if (parameterValue.getTypeName() != null) { + typeNameToUse = parameterValue.getTypeName(); + } + inValueToUse = parameterValue.getValue(); + } + + if (logger.isTraceEnabled()) { + logger.trace("Setting SQL statement parameter value: column index " + paramIndex + + ", parameter value [" + inValueToUse + + "], value class [" + (inValueToUse != null ? inValueToUse.getClass().getName() : "null") + + "], SQL type " + (sqlTypeToUse == SqlTypeValue.TYPE_UNKNOWN ? "unknown" : Integer.toString(sqlTypeToUse))); + } + + if (inValueToUse == null) { + setNull(ps, paramIndex, sqlTypeToUse, typeNameToUse); + } + else { + setValue(ps, paramIndex, sqlTypeToUse, typeNameToUse, scale, inValueToUse); + } + } + + private static void setNull(PreparedStatement ps, int paramIndex, int sqlType, String typeName) throws SQLException { + if (sqlType == SqlTypeValue.TYPE_UNKNOWN) { + boolean useSetObject = false; + sqlType = Types.NULL; + try { + DatabaseMetaData dbmd = ps.getConnection().getMetaData(); + String databaseProductName = dbmd.getDatabaseProductName(); + String jdbcDriverName = dbmd.getDriverName(); + if (databaseProductName.startsWith("Informix") || + jdbcDriverName.startsWith("Microsoft SQL Server")) { + useSetObject = true; + } + else if (databaseProductName.startsWith("DB2") || + jdbcDriverName.startsWith("jConnect") || + jdbcDriverName.startsWith("SQLServer")|| + jdbcDriverName.startsWith("Apache Derby Embedded")) { + sqlType = Types.VARCHAR; + } + } + catch (Throwable ex) { + logger.debug("Could not check database or driver name", ex); + } + if (useSetObject) { + ps.setObject(paramIndex, null); + } + else { + ps.setNull(paramIndex, sqlType); + } + } + else if (typeName != null) { + ps.setNull(paramIndex, sqlType, typeName); + } + else { + ps.setNull(paramIndex, sqlType); + } + } + + private static void setValue(PreparedStatement ps, int paramIndex, int sqlType, String typeName, + Integer scale, Object inValue) throws SQLException { + + if (inValue instanceof SqlTypeValue) { + ((SqlTypeValue) inValue).setTypeValue(ps, paramIndex, sqlType, typeName); + } + else if (inValue instanceof SqlValue) { + ((SqlValue) inValue).setValue(ps, paramIndex); + } + else if (sqlType == Types.VARCHAR || sqlType == Types.LONGVARCHAR || + (sqlType == Types.CLOB && isStringValue(inValue.getClass()))) { + ps.setString(paramIndex, inValue.toString()); + } + else if (sqlType == Types.DECIMAL || sqlType == Types.NUMERIC) { + if (inValue instanceof BigDecimal) { + ps.setBigDecimal(paramIndex, (BigDecimal) inValue); + } + else if (scale != null) { + ps.setObject(paramIndex, inValue, sqlType, scale.intValue()); + } + else { + ps.setObject(paramIndex, inValue, sqlType); + } + } + else if (sqlType == Types.DATE) { + if (inValue instanceof java.util.Date) { + if (inValue instanceof java.sql.Date) { + ps.setDate(paramIndex, (java.sql.Date) inValue); + } + else { + ps.setDate(paramIndex, new java.sql.Date(((java.util.Date) inValue).getTime())); + } + } + else if (inValue instanceof Calendar) { + Calendar cal = (Calendar) inValue; + ps.setDate(paramIndex, new java.sql.Date(cal.getTime().getTime()), cal); + } + else { + ps.setObject(paramIndex, inValue, Types.DATE); + } + } + else if (sqlType == Types.TIME) { + if (inValue instanceof java.util.Date) { + if (inValue instanceof java.sql.Time) { + ps.setTime(paramIndex, (java.sql.Time) inValue); + } + else { + ps.setTime(paramIndex, new java.sql.Time(((java.util.Date) inValue).getTime())); + } + } + else if (inValue instanceof Calendar) { + Calendar cal = (Calendar) inValue; + ps.setTime(paramIndex, new java.sql.Time(cal.getTime().getTime()), cal); + } + else { + ps.setObject(paramIndex, inValue, Types.TIME); + } + } + else if (sqlType == Types.TIMESTAMP) { + if (inValue instanceof java.util.Date) { + if (inValue instanceof java.sql.Timestamp) { + ps.setTimestamp(paramIndex, (java.sql.Timestamp) inValue); + } + else { + ps.setTimestamp(paramIndex, new java.sql.Timestamp(((java.util.Date) inValue).getTime())); + } + } + else if (inValue instanceof Calendar) { + Calendar cal = (Calendar) inValue; + ps.setTimestamp(paramIndex, new java.sql.Timestamp(cal.getTime().getTime()), cal); + } + else { + ps.setObject(paramIndex, inValue, Types.TIMESTAMP); + } + } + else if (sqlType == SqlTypeValue.TYPE_UNKNOWN) { + if (isStringValue(inValue.getClass())) { + ps.setString(paramIndex, inValue.toString()); + } + else if (isDateValue(inValue.getClass())) { + ps.setTimestamp(paramIndex, new java.sql.Timestamp(((java.util.Date) inValue).getTime())); + } + else if (inValue instanceof Calendar) { + Calendar cal = (Calendar) inValue; + ps.setTimestamp(paramIndex, new java.sql.Timestamp(cal.getTime().getTime()), cal); + } + else { + // Fall back to generic setObject call without SQL type specified. + ps.setObject(paramIndex, inValue); + } + } + else { + // Fall back to generic setObject call with SQL type specified. + ps.setObject(paramIndex, inValue, sqlType); + } + } + + /** + * Check whether the given value can be treated as a String value. + */ + private static boolean isStringValue(Class inValueType) { + // Consider any CharSequence (including JDK 1.5's StringBuilder) as String. + return (CharSequence.class.isAssignableFrom(inValueType) || + StringWriter.class.isAssignableFrom(inValueType)); + } + + /** + * Check whether the given value is a java.util.Date + * (but not one of the JDBC-specific subclasses). + */ + private static boolean isDateValue(Class inValueType) { + return (java.util.Date.class.isAssignableFrom(inValueType) && + !(java.sql.Date.class.isAssignableFrom(inValueType) || + java.sql.Time.class.isAssignableFrom(inValueType) || + java.sql.Timestamp.class.isAssignableFrom(inValueType))); + } + + /** + * Clean up all resources held by parameter values which were passed to an + * execute method. This is for example important for closing LOB values. + * @param paramValues parameter values supplied. May be null. + * @see DisposableSqlTypeValue#cleanup() + * @see org.springframework.jdbc.core.support.SqlLobValue#cleanup() + */ + public static void cleanupParameters(Object[] paramValues) { + if (paramValues != null) { + cleanupParameters(Arrays.asList(paramValues)); + } + } + + /** + * Clean up all resources held by parameter values which were passed to an + * execute method. This is for example important for closing LOB values. + * @param paramValues parameter values supplied. May be null. + * @see DisposableSqlTypeValue#cleanup() + * @see org.springframework.jdbc.core.support.SqlLobValue#cleanup() + */ + public static void cleanupParameters(Collection paramValues) { + if (paramValues != null) { + for (Iterator it = paramValues.iterator(); it.hasNext();) { + Object inValue = it.next(); + if (inValue instanceof DisposableSqlTypeValue) { + ((DisposableSqlTypeValue) inValue).cleanup(); + } + else if (inValue instanceof SqlValue) { + ((SqlValue) inValue).cleanup(); + } + } + } + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallMetaDataContext.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallMetaDataContext.java new file mode 100644 index 0000000000..f0ac6d9eb6 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallMetaDataContext.java @@ -0,0 +1,589 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.core.metadata; + +import java.sql.DatabaseMetaData; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.sql.DataSource; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.SqlOutParameter; +import org.springframework.jdbc.core.SqlParameter; +import org.springframework.jdbc.core.SqlReturnResultSet; +import org.springframework.jdbc.core.SqlParameterValue; +import org.springframework.jdbc.core.namedparam.SqlParameterSource; +import org.springframework.jdbc.core.namedparam.SqlParameterSourceUtils; +import org.springframework.jdbc.support.JdbcUtils; + +/** + * Class to manage context metadata used for the configuration and execution of the call. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @since 2.5 + */ +public class CallMetaDataContext { + + /** Logger available to subclasses */ + protected final Log logger = LogFactory.getLog(getClass()); + + /** name of procedure to call **/ + private String procedureName; + + /** name of catalog for call **/ + private String catalogName; + + /** name of schema for call **/ + private String schemaName; + + /** List of SqlParameter objects to be used in call execution */ + private List callParameters = new ArrayList(); + + /** name to use for the return value in the output map */ + private String functionReturnName = "return"; + + /** Set of in parameter names to exclude use for any not listed */ + private Set limitedInParameterNames = new HashSet(); + + /** List of SqlParameter names for out parameters */ + private List outParameterNames = new ArrayList(); + + /** should we access call parameter meta data info or not */ + private boolean accessCallParameterMetaData = true; + + /** indicates whether this is a procedure or a function **/ + private boolean function; + + /** indicates whether this procedure's return value should be included **/ + private boolean returnValueRequired; + + /** the provider of call meta data */ + private CallMetaDataProvider metaDataProvider; + + + /** + * Specify the name used for the return value of the function. + */ + public void setFunctionReturnName(String functionReturnName) { + this.functionReturnName = functionReturnName; + } + + /** + * Get the name used for the return value of the function. + */ + public String getFunctionReturnName() { + return this.functionReturnName; + } + + /** + * Specify a limited set of in parameters to be used. + */ + public void setLimitedInParameterNames(Set limitedInParameterNames) { + this.limitedInParameterNames = limitedInParameterNames; + } + + /** + * Get a limited set of in parameters to be used. + */ + public Set getLimitedInParameterNames() { + return this.limitedInParameterNames; + } + + /** + * Specify the names of the out parameters. + */ + public void setOutParameterNames(List outParameterNames) { + this.outParameterNames = outParameterNames; + } + + /** + * Get a list of the out parameter names. + */ + public List getOutParameterNames() { + return this.outParameterNames; + } + + /** + * Specify the name of the procedure. + */ + public void setProcedureName(String procedureName) { + this.procedureName = procedureName; + } + + /** + * Get the name of the procedure. + */ + public String getProcedureName() { + return this.procedureName; + } + + /** + * Specify the name of the catalog. + */ + public void setCatalogName(String catalogName) { + this.catalogName = catalogName; + } + + /** + * Get the name of the catalog. + */ + public String getCatalogName() { + return this.catalogName; + } + + /** + * Secify the name of the schema. + */ + public void setSchemaName(String schemaName) { + this.schemaName = schemaName; + } + + /** + * Get the name of the schema. + */ + public String getSchemaName() { + return this.schemaName; + } + + /** + * Specify whether this call is a function call. + */ + public void setFunction(boolean function) { + this.function = function; + } + + /** + * Check whether this call is a function call. + */ + public boolean isFunction() { + return this.function; + } + + /** + * Specify whether a return value is required. + */ + public void setReturnValueRequired(boolean returnValueRequired) { + this.returnValueRequired = returnValueRequired; + } + + /** + * Check whether a return value is required. + */ + public boolean isReturnValueRequired() { + return this.returnValueRequired; + } + + /** + * Specify whether call parameter metadata should be accessed. + */ + public void setAccessCallParameterMetaData(boolean accessCallParameterMetaData) { + this.accessCallParameterMetaData = accessCallParameterMetaData; + } + + /** + * Check whether call parameter metadata should be accessed. + */ + public boolean isAccessCallParameterMetaData() { + return this.accessCallParameterMetaData; + } + + + /** + * Create a ReturnResultSetParameter/SqlOutParameter depending on the support provided + * by the JDBC driver used for the database in use. + * @param parameterName the name of the parameter (also used as the name of the List returned in the output) + * @param rowMapper a RowMapper implementation used to map the data retuned in the result set + * @return the appropriate SqlParameter + */ + public SqlParameter createReturnResultSetParameter(String parameterName, RowMapper rowMapper) { + if (this.metaDataProvider.isReturnResultSetSupported()) { + return new SqlReturnResultSet(parameterName, rowMapper); + } + else { + if (this.metaDataProvider.isRefCursorSupported()) { + return new SqlOutParameter(parameterName, this.metaDataProvider.getRefCursorSqlType(), rowMapper); + } + else { + throw new InvalidDataAccessApiUsageException("Return of a ResultSet from a stored procedure is not supported."); + } + } + } + + /** + * Get the name of the single out parameter for this call. If there are multiple parameters then the name of + * the first one is returned. + */ + public String getScalarOutParameterName() { + if (isFunction()) { + return this.functionReturnName; + } + else { + if (this.outParameterNames.size() > 1) { + logger.warn("Accessing single output value when procedure has more than one output parameter"); + } + return (this.outParameterNames.size() > 0 ? this.outParameterNames.get(0) : null); + } + } + + /** + * Get the List of SqlParameter objects to be used in call execution + */ + public List getCallParameters() { + return this.callParameters; + } + + /** + * Initialize this class with metadata from the database + * @param dataSource the DataSource used to retrieve metadata + */ + public void initializeMetaData(DataSource dataSource) { + this.metaDataProvider = CallMetaDataProviderFactory.createMetaDataProvider(dataSource, this); + } + + /** + * Process the list of parameters provided and if procedure column metedata is used the + * parameters will be matched against the metadata information and any missing ones will + * be automatically included + * @param parameters the list of parameters to use as a base + */ + public void processParameters(List parameters) { + this.callParameters = reconcileParameters(parameters); + } + + /** + * Reconcile the provided parameters with available metadata and add new ones where appropriate + */ + private List reconcileParameters(List parameters) { + final List declaredReturnParameters = new ArrayList(); + final Map declaredParameters = new LinkedHashMap(); + boolean returnDeclared = false; + List outParameterNames = new ArrayList(); + List metaDataParameterNames = new ArrayList(); + + // get the names of the meta data parameters + for (CallParameterMetaData meta : metaDataProvider.getCallParameterMetaData()) { + if (meta.getParameterType() != DatabaseMetaData.procedureColumnReturn) { + metaDataParameterNames.add(meta.getParameterName().toLowerCase()); + } + } + + // Separate implicit return parameters from explicit parameters... + for (SqlParameter parameter : parameters) { + if (parameter.isResultsParameter()) { + declaredReturnParameters.add(parameter); + } + else { + String parameterNameToMatch = this.metaDataProvider.parameterNameToUse(parameter.getName()).toLowerCase(); + declaredParameters.put(parameterNameToMatch, parameter); + if (parameter instanceof SqlOutParameter) { + outParameterNames.add(parameter.getName()); + if (this.isFunction() && !metaDataParameterNames.contains(parameterNameToMatch)) { + if (!returnDeclared) { + if (logger.isDebugEnabled()) { + logger.debug("Using declared out parameter '" + parameter.getName() + "' for function return value"); + } + this.setFunctionReturnName(parameter.getName()); + returnDeclared = true; + } + } + } + } + } + this.setOutParameterNames(outParameterNames); + + final List workParameters = new ArrayList(); + workParameters.addAll(declaredReturnParameters); + + if (!this.metaDataProvider.isProcedureColumnMetaDataUsed()) { + workParameters.addAll(declaredParameters.values()); + return workParameters; + } + + Map limitedInParamNamesMap = new HashMap(this.limitedInParameterNames.size()); + for (String limitedParameterName : this.limitedInParameterNames) { + limitedInParamNamesMap.put( + this.metaDataProvider.parameterNameToUse(limitedParameterName).toLowerCase(), limitedParameterName); + } + + for (CallParameterMetaData meta : metaDataProvider.getCallParameterMetaData()) { + String parNameToCheck = null; + if (meta.getParameterName() != null) { + parNameToCheck = this.metaDataProvider.parameterNameToUse(meta.getParameterName()).toLowerCase(); + } + String parNameToUse = this.metaDataProvider.parameterNameToUse(meta.getParameterName()); + if (declaredParameters.containsKey(parNameToCheck) || + (meta.getParameterType() == DatabaseMetaData.procedureColumnReturn && returnDeclared)) { + SqlParameter parameter; + if (meta.getParameterType() == DatabaseMetaData.procedureColumnReturn) { + parameter = declaredParameters.get(this.getFunctionReturnName()); + if (parameter == null && this.getOutParameterNames().size() > 0) { + parameter = declaredParameters.get(this.getOutParameterNames().get(0).toLowerCase()); + } + if (parameter == null) { + throw new InvalidDataAccessApiUsageException( + "Unable to locate declared parameter for function return value - " + + " add an SqlOutParameter with name \"" + getFunctionReturnName() +"\""); + } + else { + this.setFunctionReturnName(parameter.getName()); + } + } + else { + parameter = declaredParameters.get(parNameToCheck); + } + if (parameter != null) { + workParameters.add(parameter); + if (logger.isDebugEnabled()) { + logger.debug("Using declared parameter for: " + + (parNameToUse == null ? getFunctionReturnName() : parNameToUse)); + } + } + } + else { + if (meta.getParameterType() == DatabaseMetaData.procedureColumnReturn) { + if (!isFunction() && !isReturnValueRequired() && + this.metaDataProvider.byPassReturnParameter(meta.getParameterName())) { + if (logger.isDebugEnabled()) { + logger.debug("Bypassing metadata return parameter for: " + meta.getParameterName()); + } + } + else { + String returnNameToUse = + ( meta.getParameterName() == null || meta.getParameterName().length() < 1 ) ? + this.getFunctionReturnName() : parNameToUse; + workParameters.add(new SqlOutParameter(returnNameToUse, meta.getSqlType())); + if (this.isFunction()) + outParameterNames.add(returnNameToUse); + if (logger.isDebugEnabled()) { + logger.debug("Added metadata return parameter for: " + returnNameToUse); + } + } + } + else { + if (meta.getParameterType() == DatabaseMetaData.procedureColumnOut) { + workParameters.add(this.metaDataProvider.createDefaultOutParameter(parNameToUse, meta)); + outParameterNames.add(parNameToUse); + if (logger.isDebugEnabled()) { + logger.debug("Added metadata out parameter for: " + parNameToUse); + } + } + else if (meta.getParameterType() == DatabaseMetaData.procedureColumnInOut) { + workParameters.add(this.metaDataProvider.createDefaultInOutParameter(parNameToUse, meta)); + outParameterNames.add(parNameToUse); + if (logger.isDebugEnabled()) { + logger.debug("Added metadata in out parameter for: " + parNameToUse); + } + } + else { + if (this.limitedInParameterNames.size() == 0 || + limitedInParamNamesMap.containsKey(parNameToUse.toLowerCase())) { + workParameters.add(this.metaDataProvider.createDefaultInParameter(parNameToUse, meta)); + if (logger.isDebugEnabled()) { + logger.debug("Added metadata in parameter for: " + parNameToUse); + } + } + else { + if (logger.isDebugEnabled()) { + logger.debug("Limited set of parameters " + limitedInParamNamesMap.keySet() + + " skipped parameter for: " + parNameToUse); + } + } + } + } + } + } + + return workParameters; + + } + + /** + * Match input parameter values with the parameters declared to be used in the call. + * @param parameterSource the input values + * @return a Map containing the matched parameter names with the value taken from the input + */ + public Map matchInParameterValuesWithCallParameters(SqlParameterSource parameterSource) { + // For parameter source lookups we need to provide case-insensitive lookup support + // since the database metadata is not necessarily providing case sensitive parameter names. + Map caseInsensitiveParameterNames = + SqlParameterSourceUtils.extractCaseInsensitiveParameterNames(parameterSource); + + Map callParameterNames = new HashMap(this.callParameters.size()); + Map matchedParameters = new HashMap(this.callParameters.size()); + for (SqlParameter parameter : this.callParameters) { + if (parameter.isInputValueProvided()) { + String parameterName = parameter.getName(); + String parameterNameToMatch = this.metaDataProvider.parameterNameToUse(parameterName); + if (parameterNameToMatch != null) { + callParameterNames.put(parameterNameToMatch.toLowerCase(), parameterName); + } + if (parameterName != null) { + if (parameterSource.hasValue(parameterName)) { + matchedParameters.put(parameterName, SqlParameterSourceUtils.getTypedValue(parameterSource, parameterName)); + } + else { + String lowerCaseName = parameterName.toLowerCase(); + if (parameterSource.hasValue(lowerCaseName)) { + matchedParameters.put(parameterName, SqlParameterSourceUtils.getTypedValue(parameterSource, lowerCaseName)); + } + else { + String propertyName = JdbcUtils.convertUnderscoreNameToPropertyName(parameterName); + if (parameterSource.hasValue(propertyName)) { + matchedParameters.put(parameterName, SqlParameterSourceUtils.getTypedValue(parameterSource, propertyName)); + } + else { + if (caseInsensitiveParameterNames.containsKey(lowerCaseName)) { + String sourceName = (String) caseInsensitiveParameterNames.get(lowerCaseName); + matchedParameters.put(parameterName, SqlParameterSourceUtils.getTypedValue(parameterSource, sourceName)); + } + else { + logger.warn("Unable to locate the corresponding parameter value for '" + parameterName + + "' within the parameter values provided: " + caseInsensitiveParameterNames.values()); + } + } + } + } + } + } + } + + if (logger.isDebugEnabled()) { + logger.debug("Matching " + caseInsensitiveParameterNames.values() + " with " + callParameterNames.values()); + logger.debug("Found match for " + matchedParameters.keySet()); + } + return matchedParameters; + } + + /** + * Match input parameter values with the parameters declared to be used in the call. + * @param inParameters the input values + * @return a Map containing the matched parameter names with the value taken from the input + */ + public Map matchInParameterValuesWithCallParameters(Map inParameters) { + if (!this.metaDataProvider.isProcedureColumnMetaDataUsed()) { + return inParameters; + } + Map callParameterNames = new HashMap(this.callParameters.size()); + for (SqlParameter parameter : this.callParameters) { + if (parameter.isInputValueProvided()) { + String parameterName = parameter.getName(); + String parameterNameToMatch = this.metaDataProvider.parameterNameToUse(parameterName); + if (parameterNameToMatch != null) { + callParameterNames.put(parameterNameToMatch.toLowerCase(), parameterName); + } + } + } + Map matchedParameters = new HashMap(inParameters.size()); + for (String parameterName : inParameters.keySet()) { + String parameterNameToMatch = this.metaDataProvider.parameterNameToUse(parameterName); + String callParameterName = callParameterNames.get(parameterNameToMatch.toLowerCase()); + if (callParameterName == null) { + if (logger.isDebugEnabled()) { + Object value = inParameters.get(parameterName); + if (value instanceof SqlParameterValue) { + value = ((SqlParameterValue)value).getValue(); + } + if (value != null) { + logger.debug("Unable to locate the corresponding IN or IN-OUT parameter for \"" + parameterName + + "\" in the parameters used: " + callParameterNames.keySet()); + } + } + } + else { + matchedParameters.put(callParameterName, inParameters.get(parameterName)); + } + } + if (matchedParameters.size() < callParameterNames.size()) { + for (String parameterName : callParameterNames.keySet()) { + String parameterNameToMatch = this.metaDataProvider.parameterNameToUse(parameterName); + String callParameterName = callParameterNames.get(parameterNameToMatch.toLowerCase()); + if (!matchedParameters.containsKey(callParameterName)) { + logger.warn("Unable to locate the corresponding parameter value for '" + parameterName + + "' within the parameter values provided: " + inParameters.keySet()); + } + } + } + if (logger.isDebugEnabled()) { + logger.debug("Matching " + inParameters.keySet() + " with " + callParameterNames.values()); + logger.debug("Found match for " + matchedParameters.keySet()); + } + return matchedParameters; + } + + /** + * Build the call string based on configuration and metadata information. + * @return the call string to be used + */ + public String createCallString() { + String callString; + int parameterCount = 0; + String catalogNameToUse = null; + String schemaNameToUse = null; + + // For Oracle where catalogs are not supported we need to reverse the schema name + // and the catalog name since the cataog is used for the package name + if (this.metaDataProvider.isSupportsSchemasInProcedureCalls() && + !this.metaDataProvider.isSupportsCatalogsInProcedureCalls()) { + schemaNameToUse = this.metaDataProvider.catalogNameToUse(this.getCatalogName()); + catalogNameToUse = this.metaDataProvider.schemaNameToUse(this.getSchemaName()); + } + else { + catalogNameToUse = this.metaDataProvider.catalogNameToUse(this.getCatalogName()); + schemaNameToUse = this.metaDataProvider.schemaNameToUse(this.getSchemaName()); + } + String procedureNameToUse = this.metaDataProvider.procedureNameToUse(this.getProcedureName()); + if (this.isFunction() || this.isReturnValueRequired()) { + callString = "{? = call " + + (catalogNameToUse != null && catalogNameToUse.length() > 0 ? catalogNameToUse + "." : "") + + (schemaNameToUse != null && schemaNameToUse.length() > 0 ? schemaNameToUse + "." : "") + + procedureNameToUse + "("; + parameterCount = -1; + } + else { + callString = "{call " + + (catalogNameToUse != null && catalogNameToUse.length() > 0 ? catalogNameToUse + "." : "") + + (schemaNameToUse != null && schemaNameToUse.length() > 0 ? schemaNameToUse + "." : "") + + procedureNameToUse + "("; + } + for (SqlParameter parameter : this.callParameters) { + if (!(parameter.isResultsParameter())) { + if (parameterCount > 0) { + callString += ", "; + } + if (parameterCount >= 0) { + callString += "?"; + } + parameterCount++; + } + } + callString += ")}"; + + return callString; + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallMetaDataProvider.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallMetaDataProvider.java new file mode 100644 index 0000000000..9bc9258e9a --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallMetaDataProvider.java @@ -0,0 +1,175 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc.core.metadata; + +import java.sql.DatabaseMetaData; +import java.sql.SQLException; +import java.util.List; + +import org.springframework.jdbc.core.SqlParameter; + +/** + * Interface specifying the API to be implemented by a class providing call metadata. + * This is intended for internal use by Spring's + * {@link org.springframework.jdbc.core.simple.SimpleJdbcTemplate}. + * + * @author Thomas Risberg + * @since 2.5 + */ +public interface CallMetaDataProvider { + + /** + * Initialize using the provided DatabaseMetData. + * @param databaseMetaData used to retrieve database specific information + * @throws SQLException in case of initialization failure + */ + void initializeWithMetaData(DatabaseMetaData databaseMetaData) throws SQLException; + + /** + * Initialize the database specific management of procedure column meta data. + * This is only called for databases that are supported. This initalization + * can be turned off by specifying that column meta data should not be used. + * @param databaseMetaData used to retreive database specific information + * @param catalogName name of catalog to use or null + * @param schemaName name of schema name to use or null + * @param procedureName name of the stored procedure + * @throws SQLException in case of initialization failure + * @see org.springframework.jdbc.core.simple.SimpleJdbcCall#withoutProcedureColumnMetaDataAccess() + */ + void initializeWithProcedureColumnMetaData( + DatabaseMetaData databaseMetaData, String catalogName, String schemaName, String procedureName) + throws SQLException; + + /** + * Provide any modification of the procedure name passed in to match the meta data currently used. + * This could include alterig the case. + */ + String procedureNameToUse(String procedureName); + + /** + * Provide any modification of the catalog name passed in to match the meta data currently used. + * This could include alterig the case. + */ + String catalogNameToUse(String catalogName); + + /** + * Provide any modification of the schema name passed in to match the meta data currently used. + * This could include alterig the case. + */ + String schemaNameToUse(String schemaName); + + /** + * Provide any modification of the catalog name passed in to match the meta data currently used. + * The reyurned value will be used for meta data lookups. This could include alterig the case used or + * providing a base catalog if mone provided. + */ + String metaDataCatalogNameToUse(String catalogName) ; + + /** + * Provide any modification of the schema name passed in to match the meta data currently used. + * The reyurned value will be used for meta data lookups. This could include alterig the case used or + * providing a base schema if mone provided. + */ + String metaDataSchemaNameToUse(String schemaName) ; + + /** + * Provide any modification of the column name passed in to match the meta data currently used. + * This could include alterig the case. + * @param parameterName name of the parameter of column + */ + String parameterNameToUse(String parameterName); + + /** + * Create a default out parameter based on the provided meta data. This is used when no expicit + * parameter declaration has been made. + * @param parameterName the name of the parameter + * @param meta meta data used for this call + * @return the configured SqlOutParameter + */ + SqlParameter createDefaultOutParameter(String parameterName, CallParameterMetaData meta); + + /** + * Create a default inout parameter based on the provided meta data. This is used when no expicit + * parameter declaration has been made. + * @param parameterName the name of the parameter + * @param meta meta data used for this call + * @return the configured SqlInOutParameter + */ + SqlParameter createDefaultInOutParameter(String parameterName, CallParameterMetaData meta); + + /** + * Create a default in parameter based on the provided meta data. This is used when no expicit + * parameter declaration has been made. + * @param parameterName the name of the parameter + * @param meta meta data used for this call + * @return the configured SqlParameter + */ + SqlParameter createDefaultInParameter(String parameterName, CallParameterMetaData meta); + + /** + * Get the name of the current user. Useful for meta data lookups etc. + * @return current user name from database connection + */ + String getUserName(); + + /** + * Does this database support returning resultsets that should be retreived with the JDBC call + * {@link java.sql.Statement#getResultSet()} + */ + boolean isReturnResultSetSupported(); + + /** + * Does this database support returning resultsets as ref cursors to be retreived with + * {@link java.sql.CallableStatement#getObject(int)} for the specified column. + */ + boolean isRefCursorSupported(); + + /** + * Get the {@link java.sql.Types} type for columns that return resultsets as ref cursors if this feature + * is supported. + */ + int getRefCursorSqlType(); + + /** + * Are we using the meta data for the procedure columns? + */ + boolean isProcedureColumnMetaDataUsed(); + + /** + * Should we bypass the return parameter with the specified name. + * This allows the database specific implementation to skip the processing + * for specific results returned by the database call. + */ + boolean byPassReturnParameter(String parameterName); + + /** + * Get the call parameter metadata that is currently used. + * @return List of {@link CallParameterMetaData} + */ + List getCallParameterMetaData(); + + /** + * Does the database support the use of catalog name in procedure calls + */ + boolean isSupportsCatalogsInProcedureCalls(); + + /** + * Does the database support the use of schema name in procedure calls + */ + boolean isSupportsSchemasInProcedureCalls(); + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallMetaDataProviderFactory.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallMetaDataProviderFactory.java new file mode 100644 index 0000000000..38d47301c3 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallMetaDataProviderFactory.java @@ -0,0 +1,140 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc.core.metadata; + +import java.sql.DatabaseMetaData; +import java.sql.SQLException; +import java.util.Arrays; +import java.util.List; + +import javax.sql.DataSource; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.jdbc.support.DatabaseMetaDataCallback; +import org.springframework.jdbc.support.JdbcUtils; +import org.springframework.jdbc.support.MetaDataAccessException; + +/** + * Factory used to create a {@link CallMetaDataProvider} implementation based on the type of databse being used. + * + * @author Thomas Risberg + * @since 2.5 + */ +public class CallMetaDataProviderFactory { + + /** Logger */ + private static final Log logger = LogFactory.getLog(CallMetaDataProviderFactory.class); + + /** List of supported database products for procedure calls */ + public static final List supportedDatabaseProductsForProcedures = Arrays.asList( + "Apache Derby", + "DB2", + "MySQL", + "Microsoft SQL Server", + "Oracle", + "PostgreSQL", + "Sybase" + ); + /** List of supported database products for function calls */ + public static final List supportedDatabaseProductsForFunctions = Arrays.asList( + "MySQL", + "Microsoft SQL Server", + "Oracle", + "PostgreSQL" + ); + + /** + * Create a CallMetaDataProvider based on the database metedata + * @param dataSource used to retrieve metedata + * @param context the class that holds configuration and metedata + * @return instance of the CallMetaDataProvider implementation to be used + */ + static public CallMetaDataProvider createMetaDataProvider(DataSource dataSource, final CallMetaDataContext context) { + try { + return (CallMetaDataProvider) JdbcUtils.extractDatabaseMetaData(dataSource, new DatabaseMetaDataCallback() { + public Object processMetaData(DatabaseMetaData databaseMetaData) throws SQLException, MetaDataAccessException { + String databaseProductName = JdbcUtils.commonDatabaseName(databaseMetaData.getDatabaseProductName()); + boolean accessProcedureColumnMetaData = context.isAccessCallParameterMetaData(); + if (context.isFunction()) { + if (!supportedDatabaseProductsForFunctions.contains(databaseProductName)) { + if (logger.isWarnEnabled()) { + logger.warn(databaseProductName + " is not one of the databases fully supported for function calls " + + "-- supported are: " + supportedDatabaseProductsForFunctions); + } + if (accessProcedureColumnMetaData) { + logger.warn("Metadata processing disabled - you must specify all parameters explicitly"); + accessProcedureColumnMetaData = false; + } + } + } + else { + if (!supportedDatabaseProductsForProcedures.contains(databaseProductName)) { + if (logger.isWarnEnabled()) { + logger.warn(databaseProductName + " is not one of the databases fully supported for procedure calls " + + "-- supported are: " + supportedDatabaseProductsForProcedures); + } + if (accessProcedureColumnMetaData) { + logger.warn("Metadata processing disabled - you must specify all parameters explicitly"); + accessProcedureColumnMetaData = false; + } + } + } + + CallMetaDataProvider provider; + if ("Oracle".equals(databaseProductName)) { + provider = new OracleCallMetaDataProvider(databaseMetaData); + } + else if ("DB2".equals(databaseProductName)) { + provider = new Db2CallMetaDataProvider((databaseMetaData)); + } + else if ("Apache Derby".equals(databaseProductName)) { + provider = new DerbyCallMetaDataProvider((databaseMetaData)); + } + else if ("PostgreSQL".equals(databaseProductName)) { + provider = new PostgresCallMetaDataProvider((databaseMetaData)); + } + else if ("Sybase".equals(databaseProductName)) { + provider = new SybaseCallMetaDataProvider((databaseMetaData)); + } + else if ("Microsoft SQL Server".equals(databaseProductName)) { + provider = new SqlServerCallMetaDataProvider((databaseMetaData)); + } + else { + provider = new GenericCallMetaDataProvider(databaseMetaData); + } + if (logger.isDebugEnabled()) { + logger.debug("Using " + provider.getClass().getName()); + } + provider.initializeWithMetaData(databaseMetaData); + if (accessProcedureColumnMetaData) { + provider.initializeWithProcedureColumnMetaData( + databaseMetaData, context.getCatalogName(), context.getSchemaName(), context.getProcedureName()); + } + return provider; + } + }); + } + catch (MetaDataAccessException ex) { + throw new DataAccessResourceFailureException("Error retreiving database metadata", ex); + } + + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallParameterMetaData.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallParameterMetaData.java new file mode 100644 index 0000000000..4c1393640d --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallParameterMetaData.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc.core.metadata; + +/** + * Holder of metadata for a specific parameter that is used for call processing. + * + * @author Thomas Risberg + * @since 2.5 + */ +public class CallParameterMetaData { + private String parameterName; + private int parameterType; + private int sqlType; + private String typeName; + private boolean nullable; + + /** + * Constructor taking all the properties + */ + public CallParameterMetaData(String columnName, int columnType, int sqlType, String typeName, boolean nullable) { + this.parameterName = columnName; + this.parameterType = columnType; + this.sqlType = sqlType; + this.typeName = typeName; + this.nullable = nullable; + } + + + /** + * Get the parameter name. + */ + public String getParameterName() { + return parameterName; + } + + /** + * Get the parameter type. + */ + public int getParameterType() { + return parameterType; + } + + /** + * Get the parameter SQL type. + */ + public int getSqlType() { + return sqlType; + } + + /** + * Get the parameter type name. + */ + public String getTypeName() { + return typeName; + } + + /** + * Get whether the parameter is nullable. + */ + public boolean isNullable() { + return nullable; + } +} \ No newline at end of file diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/Db2CallMetaDataProvider.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/Db2CallMetaDataProvider.java new file mode 100644 index 0000000000..24fcfcc00f --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/Db2CallMetaDataProvider.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc.core.metadata; + +import java.sql.DatabaseMetaData; +import java.sql.SQLException; + +/** + * DB2 specific implementation for the {@link CallMetaDataProvider} interface. + * This class is intended for internal use by the Simple JDBC classes. + * + * @author Thomas Risberg + * @since 2.5 + */ +public class Db2CallMetaDataProvider extends GenericCallMetaDataProvider { + + public Db2CallMetaDataProvider(DatabaseMetaData databaseMetaData) throws SQLException { + super(databaseMetaData); + } + + + @Override + public void initializeWithMetaData(DatabaseMetaData databaseMetaData) throws SQLException { + try { + setSupportsCatalogsInProcedureCalls(databaseMetaData.supportsCatalogsInProcedureCalls()); + } + catch (SQLException se) { + logger.debug("Error retrieving 'DatabaseMetaData.supportsCatalogsInProcedureCalls' - " + se.getMessage()); + } + try { + setSupportsSchemasInProcedureCalls(databaseMetaData.supportsSchemasInProcedureCalls()); + } + catch (SQLException se) { + logger.debug("Error retrieving 'DatabaseMetaData.supportsSchemasInProcedureCalls' - " + se.getMessage()); + } + try { + setStoresUpperCaseIdentifiers(databaseMetaData.storesUpperCaseIdentifiers()); + } + catch (SQLException se) { + logger.debug("Error retrieving 'DatabaseMetaData.storesUpperCaseIdentifiers' - " + se.getMessage()); + } + try { + setStoresLowerCaseIdentifiers(databaseMetaData.storesLowerCaseIdentifiers()); + } + catch (SQLException se) { + logger.debug("Error retrieving 'DatabaseMetaData.storesLowerCaseIdentifiers' - " + se.getMessage()); + } + } + + @Override + public String metaDataSchemaNameToUse(String schemaName) { + // Use current user schema if no schema specified + return schemaName == null ? getUserName().toUpperCase() : super.metaDataSchemaNameToUse(schemaName); + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/DerbyCallMetaDataProvider.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/DerbyCallMetaDataProvider.java new file mode 100644 index 0000000000..6d22dee275 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/DerbyCallMetaDataProvider.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc.core.metadata; + +import java.sql.DatabaseMetaData; +import java.sql.SQLException; + +/** + * Derby specific implementation for the {@link CallMetaDataProvider} interface. + * This class is intended for internal use by the Simple JDBC classes. + * + * @author Thomas Risberg + * @since 2.5 + */ +public class DerbyCallMetaDataProvider extends GenericCallMetaDataProvider { + + public DerbyCallMetaDataProvider(DatabaseMetaData databaseMetaData) throws SQLException { + super(databaseMetaData); + } + + + @Override + public String metaDataSchemaNameToUse(String schemaName) { + // Use current user schema if no schema specified + return schemaName == null ? getUserName().toUpperCase() : super.metaDataSchemaNameToUse(schemaName); + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/GenericCallMetaDataProvider.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/GenericCallMetaDataProvider.java new file mode 100644 index 0000000000..424c573c18 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/GenericCallMetaDataProvider.java @@ -0,0 +1,364 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc.core.metadata; + +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.jdbc.core.SqlOutParameter; +import org.springframework.jdbc.core.SqlParameter; +import org.springframework.jdbc.core.SqlInOutParameter; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.util.StringUtils; + +/** + * Generic implementation for the {@link CallMetaDataProvider} interface. + * This class can be extended to provide database specific behavior. + * + * @author Thomas Risberg + * @since 2.5 + */ +public class GenericCallMetaDataProvider implements CallMetaDataProvider { + + /** Logger available to subclasses */ + protected static final Log logger = LogFactory.getLog(CallMetaDataProvider.class); + + private boolean procedureColumnMetaDataUsed = false; + + private String userName; + + private boolean supportsCatalogsInProcedureCalls = true; + + private boolean supportsSchemasInProcedureCalls = true; + + private boolean storesUpperCaseIdentifiers = true; + + private boolean storesLowerCaseIdentifiers = false; + + private List callParameterMetaData = new ArrayList(); + + + /** + * Constructor used to initialize with provided database meta data. + * @param databaseMetaData meta data to be used + */ + protected GenericCallMetaDataProvider(DatabaseMetaData databaseMetaData) throws SQLException { + this.userName = databaseMetaData.getUserName(); + } + + + public void initializeWithMetaData(DatabaseMetaData databaseMetaData) throws SQLException { + try { + setSupportsCatalogsInProcedureCalls(databaseMetaData.supportsCatalogsInProcedureCalls()); + } + catch (SQLException se) { + logger.warn("Error retrieving 'DatabaseMetaData.supportsCatalogsInProcedureCalls' - " + se.getMessage()); + } + try { + setSupportsSchemasInProcedureCalls(databaseMetaData.supportsSchemasInProcedureCalls()); + } + catch (SQLException se) { + logger.warn("Error retrieving 'DatabaseMetaData.supportsSchemasInProcedureCalls' - " + se.getMessage()); + } + try { + setStoresUpperCaseIdentifiers(databaseMetaData.storesUpperCaseIdentifiers()); + } + catch (SQLException se) { + logger.warn("Error retrieving 'DatabaseMetaData.storesUpperCaseIdentifiers' - " + se.getMessage()); + } + try { + setStoresLowerCaseIdentifiers(databaseMetaData.storesLowerCaseIdentifiers()); + } + catch (SQLException se) { + logger.warn("Error retrieving 'DatabaseMetaData.storesLowerCaseIdentifiers' - " + se.getMessage()); + } + } + + public void initializeWithProcedureColumnMetaData(DatabaseMetaData databaseMetaData, String catalogName, String schemaName, String procedureName) + throws SQLException { + + this.procedureColumnMetaDataUsed = true; + processProcedureColumns(databaseMetaData, catalogName, schemaName, procedureName); + } + + public List getCallParameterMetaData() { + return callParameterMetaData; + } + + public String procedureNameToUse(String procedureName) { + if (procedureName == null) + return null; + else if (isStoresUpperCaseIdentifiers()) + return procedureName.toUpperCase(); + else if(isStoresLowerCaseIdentifiers()) + return procedureName.toLowerCase(); + else + return procedureName; + } + + public String catalogNameToUse(String catalogName) { + if (catalogName == null) + return null; + else if (isStoresUpperCaseIdentifiers()) + return catalogName.toUpperCase(); + else if(isStoresLowerCaseIdentifiers()) + return catalogName.toLowerCase(); + else + return catalogName; + } + + public String schemaNameToUse(String schemaName) { + if (schemaName == null) + return null; + else if (isStoresUpperCaseIdentifiers()) + return schemaName.toUpperCase(); + else if(isStoresLowerCaseIdentifiers()) + return schemaName.toLowerCase(); + else + return schemaName; + } + + public String metaDataCatalogNameToUse(String catalogName) { + if (isSupportsCatalogsInProcedureCalls()) { + return catalogNameToUse(catalogName); + } + else { + return null; + } + } + + public String metaDataSchemaNameToUse(String schemaName) { + if (isSupportsSchemasInProcedureCalls()) { + return schemaNameToUse(schemaName); + } + else { + return null; + } + } + + public String parameterNameToUse(String parameterName) { + if (parameterName == null) { + return null; + } + else if (isStoresUpperCaseIdentifiers()) { + return parameterName.toUpperCase(); + } + else if(isStoresLowerCaseIdentifiers()) { + return parameterName.toLowerCase(); + } + else { + return parameterName; + } + } + + public boolean byPassReturnParameter(String parameterName) { + return false; + } + + public SqlParameter createDefaultOutParameter(String parameterName, CallParameterMetaData meta) { + return new SqlOutParameter(parameterName, meta.getSqlType()); + } + + public SqlParameter createDefaultInOutParameter(String parameterName, CallParameterMetaData meta) { + return new SqlInOutParameter(parameterName, meta.getSqlType()); + } + + public SqlParameter createDefaultInParameter(String parameterName, CallParameterMetaData meta) { + return new SqlParameter(parameterName, meta.getSqlType()); + } + + public String getUserName() { + return this.userName; + } + + public boolean isReturnResultSetSupported() { + return true; + } + + public boolean isRefCursorSupported() { + return false; + } + + public int getRefCursorSqlType() { + return Types.OTHER; + } + + public boolean isProcedureColumnMetaDataUsed() { + return this.procedureColumnMetaDataUsed; + } + + + /** + * Specify whether the database supports the use of catalog name in procedure calls + */ + protected void setSupportsCatalogsInProcedureCalls(boolean supportsCatalogsInProcedureCalls) { + this.supportsCatalogsInProcedureCalls = supportsCatalogsInProcedureCalls; + } + + /** + * Does the database support the use of catalog name in procedure calls + */ + public boolean isSupportsCatalogsInProcedureCalls() { + return this.supportsCatalogsInProcedureCalls; + } + + /** + * Specify whether the database supports the use of schema name in procedure calls + */ + protected void setSupportsSchemasInProcedureCalls(boolean supportsSchemasInProcedureCalls) { + this.supportsSchemasInProcedureCalls = supportsSchemasInProcedureCalls; + } + + /** + * Does the database support the use of schema name in procedure calls + */ + public boolean isSupportsSchemasInProcedureCalls() { + return this.supportsSchemasInProcedureCalls; + } + + /** + * Specify whether the database uses upper case for identifiers + */ + protected void setStoresUpperCaseIdentifiers(boolean storesUpperCaseIdentifiers) { + this.storesUpperCaseIdentifiers = storesUpperCaseIdentifiers; + } + + /** + * Does the database use upper case for identifiers + */ + protected boolean isStoresUpperCaseIdentifiers() { + return this.storesUpperCaseIdentifiers; + } + + /** + * Specify whether the database uses lower case for identifiers + */ + protected void setStoresLowerCaseIdentifiers(boolean storesLowerCaseIdentifiers) { + this.storesLowerCaseIdentifiers = storesLowerCaseIdentifiers; + } + + /** + * Does the database use lower case for identifiers + */ + protected boolean isStoresLowerCaseIdentifiers() { + return this.storesLowerCaseIdentifiers; + } + + + /** + * Process the procedure column metadata + */ + private void processProcedureColumns(DatabaseMetaData databaseMetaData, String catalogName, String schemaName, String procedureName) { + ResultSet procs = null; + String metaDataCatalogName = metaDataCatalogNameToUse(catalogName); + String metaDataSchemaName = metaDataSchemaNameToUse(schemaName); + String metaDataProcedureName = procedureNameToUse(procedureName); + if (logger.isDebugEnabled()) { + logger.debug("Retrieving metadata for " + metaDataCatalogName + "/" + + metaDataSchemaName + "/" + metaDataProcedureName); + } + try { + procs = databaseMetaData.getProcedures( + metaDataCatalogName, + metaDataSchemaName, + metaDataProcedureName); + List found = new ArrayList(); + while (procs.next()) { + found.add(procs.getString("PROCEDURE_CAT") + + "."+procs.getString("PROCEDURE_SCHEM") + + "."+procs.getString("PROCEDURE_NAME")); + } + procs.close(); + if (found.size() > 1) { + throw new InvalidDataAccessApiUsageException("Unable to determine the correct call signature - " + + "multiple procedures/functions/signatures for " + metaDataProcedureName + " found " + found); + } + if (found.size() < 1) { + if (metaDataProcedureName.contains(".") && !StringUtils.hasText(metaDataCatalogName)) { + String packageName = metaDataProcedureName.substring(0, metaDataProcedureName.indexOf(".")); + throw new InvalidDataAccessApiUsageException("Unable to determine the correct call signature for " + + metaDataProcedureName + " - package name should be specified separately using " + + "'.withCatalogName(\"" + packageName + "\")'"); + } + } + + procs = databaseMetaData.getProcedureColumns( + metaDataCatalogName, + metaDataSchemaName, + metaDataProcedureName, + null); + while (procs.next()) { + String columnName = procs.getString("COLUMN_NAME"); + int columnType = procs.getInt("COLUMN_TYPE"); + if (columnName == null && ( + columnType == DatabaseMetaData.procedureColumnIn || + columnType == DatabaseMetaData.procedureColumnInOut || + columnType == DatabaseMetaData.procedureColumnOut)) { + if (logger.isDebugEnabled()) { + logger.debug("Skipping metadata for: " + + columnName + + " " + columnType + + " " + procs.getInt("DATA_TYPE") + + " " + procs.getString("TYPE_NAME") + + " " + procs.getBoolean("NULLABLE") + + " (probably a member of a collection)" + ); + } + } + else { + CallParameterMetaData meta = new CallParameterMetaData( + columnName, + columnType, + procs.getInt("DATA_TYPE"), + procs.getString("TYPE_NAME"), + procs.getBoolean("NULLABLE") + ); + callParameterMetaData.add(meta); + if (logger.isDebugEnabled()) { + logger.debug("Retrieved metadata: " + + meta.getParameterName() + + " " + meta.getParameterType() + + " " + meta.getSqlType() + + " " + meta.getTypeName() + + " " + meta.isNullable() + ); + } + } + } + } + catch (SQLException se) { + logger.warn("Error while retreiving metadata for procedure columns: " + se.getMessage()); + } + finally { + try { + if (procs != null) + procs.close(); + } + catch (SQLException se) { + logger.warn("Problem closing resultset for procedure column metadata " + se.getMessage()); + } + } + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/GenericTableMetaDataProvider.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/GenericTableMetaDataProvider.java new file mode 100644 index 0000000000..a1f48d648b --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/GenericTableMetaDataProvider.java @@ -0,0 +1,421 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc.core.metadata; + +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.dao.DataAccessResourceFailureException; + +/** + * A generic implementation of the {@link TableMetaDataProvider} that should provide enough features for all supported + * databases. + * + * @author Thomas Risberg + * @since 2.5 + */ +public class GenericTableMetaDataProvider implements TableMetaDataProvider { + + /** Logger available to subclasses */ + protected static final Log logger = LogFactory.getLog(TableMetaDataProvider.class); + + /** indicator whether column metadata should be used */ + private boolean tableColumnMetaDataUsed = false; + + /** the version of the database */ + private String databaseVersion; + + /** the name of the user currently connected */ + private String userName; + + /** indicates whether the identifiers are uppercased */ + private boolean storesUpperCaseIdentifiers = true; + + /** indicates whether the identifiers are lowercased */ + private boolean storesLowerCaseIdentifiers = false; + + /** indicates whether generated keys retrieval is supported */ + private boolean getGeneratedKeysSupported = true; + + /** indicates whether the use of a String[] for generated keys is supported */ + private boolean generatedKeysColumnNameArraySupported = true; + + /** database product we know that don't support the use of a String[] for generated keys */ + private List productsNotSupportingGeneratedKeysColumnNameArray = Arrays.asList(new String[] {"Apache Derby"}); + + /** Collection of TableParameterMetaData objects */ + private List insertParameterMetaData = new ArrayList(); + + + /** + * Constructor used to initialize with provided database meta data. + * @param databaseMetaData meta data to be used + * @throws SQLException + */ + protected GenericTableMetaDataProvider(DatabaseMetaData databaseMetaData) throws SQLException { + userName = databaseMetaData.getUserName(); + } + + /** + * Get whether identifiers use upper case + */ + public boolean isStoresUpperCaseIdentifiers() { + return storesUpperCaseIdentifiers; + } + + /** + * Specify whether identifiers use upper case + */ + public void setStoresUpperCaseIdentifiers(boolean storesUpperCaseIdentifiers) { + this.storesUpperCaseIdentifiers = storesUpperCaseIdentifiers; + } + + /** + * Get whether identifiers use lower case + */ + public boolean isStoresLowerCaseIdentifiers() { + return storesLowerCaseIdentifiers; + } + + /** + * Specify whether identifiers use lower case + */ + public void setStoresLowerCaseIdentifiers(boolean storesLowerCaseIdentifiers) { + this.storesLowerCaseIdentifiers = storesLowerCaseIdentifiers; + } + + public boolean isTableColumnMetaDataUsed() { + return tableColumnMetaDataUsed; + } + + public List getTableParameterMetaData() { + return insertParameterMetaData; + } + + public boolean isGetGeneratedKeysSupported() { + return getGeneratedKeysSupported; + } + + public boolean isGetGeneratedKeysSimulated(){ + return false; + } + + public String getSimpleQueryForGetGeneratedKey(String tableName, String keyColumnName) { + return null; + } + + /** + * Specify whether a column name array is supported for generated keys + */ + public void setGetGeneratedKeysSupported(boolean getGeneratedKeysSupported) { + this.getGeneratedKeysSupported = getGeneratedKeysSupported; + } + + public boolean isGeneratedKeysColumnNameArraySupported() { + return generatedKeysColumnNameArraySupported; + } + + /** + * Specify whether a column name array is supported for generated keys + */ + public void setGeneratedKeysColumnNameArraySupported(boolean generatedKeysColumnNameArraySupported) { + this.generatedKeysColumnNameArraySupported = generatedKeysColumnNameArraySupported; + } + + + public void initializeWithMetaData(DatabaseMetaData databaseMetaData) throws SQLException { + + try { + if (databaseMetaData.supportsGetGeneratedKeys()) { + logger.debug("GetGeneratedKeys is supported"); + setGetGeneratedKeysSupported(true); + } + else { + logger.debug("GetGeneratedKeys is not supported"); + setGetGeneratedKeysSupported(false); + } + } + catch (SQLException se) { + logger.warn("Error retrieving 'DatabaseMetaData.getGeneratedKeys' - " + se.getMessage()); + } + try { + String databaseProductName = databaseMetaData.getDatabaseProductName(); + if (productsNotSupportingGeneratedKeysColumnNameArray.contains(databaseProductName)) { + logger.debug("GeneratedKeysColumnNameArray is not supported for " + databaseProductName); + setGeneratedKeysColumnNameArraySupported(false); + } + else { + logger.debug("GeneratedKeysColumnNameArray is supported for " + databaseProductName); + setGeneratedKeysColumnNameArraySupported(true); + } + } + catch (SQLException se) { + logger.warn("Error retrieving 'DatabaseMetaData.getDatabaseProductName' - " + se.getMessage()); + } + try { + databaseVersion = databaseMetaData.getDatabaseProductVersion(); + } + catch (SQLException se) { + logger.warn("Error retrieving 'DatabaseMetaData.getDatabaseProductVersion' - " + se.getMessage()); + } + try { + setStoresUpperCaseIdentifiers(databaseMetaData.storesUpperCaseIdentifiers()); + } + catch (SQLException se) { + logger.warn("Error retrieving 'DatabaseMetaData.storesUpperCaseIdentifiers' - " + se.getMessage()); + } + try { + setStoresLowerCaseIdentifiers(databaseMetaData.storesLowerCaseIdentifiers()); + } + catch (SQLException se) { + logger.warn("Error retrieving 'DatabaseMetaData.storesLowerCaseIdentifiers' - " + se.getMessage()); + } + + } + + public void initializeWithTableColumnMetaData(DatabaseMetaData databaseMetaData, String catalogName, String schemaName, String tableName) + throws SQLException { + + tableColumnMetaDataUsed = true; + + locateTableAndProcessMetaData(databaseMetaData, catalogName, schemaName, tableName); + + + } + + public String tableNameToUse(String tableName) { + if (tableName == null) + return null; + else if (isStoresUpperCaseIdentifiers()) + return tableName.toUpperCase(); + else if(isStoresLowerCaseIdentifiers()) + return tableName.toLowerCase(); + else + return tableName; + } + + public String catalogNameToUse(String catalogName) { + if (catalogName == null) + return null; + else if (isStoresUpperCaseIdentifiers()) + return catalogName.toUpperCase(); + else if(isStoresLowerCaseIdentifiers()) + return catalogName.toLowerCase(); + else + return catalogName; + } + + public String schemaNameToUse(String schemaName) { + if (schemaName == null) + return null; + else if (isStoresUpperCaseIdentifiers()) + return schemaName.toUpperCase(); + else if(isStoresLowerCaseIdentifiers()) + return schemaName.toLowerCase(); + else + return schemaName; + } + + public String metaDataCatalogNameToUse(String catalogName) { + return catalogNameToUse(catalogName); + } + + public String metaDataSchemaNameToUse(String schemaName) { + if (schemaName == null) { + return schemaNameToUse(userName); + } + return schemaNameToUse(schemaName); + } + + + /** + * Provide access to version info for subclasses + */ + protected String getDatabaseVersion() { + return databaseVersion; + } + + /** + * Method supporting the metedata processing for a table + */ + private void locateTableAndProcessMetaData(DatabaseMetaData databaseMetaData, String catalogName, String schemaName, String tableName) { + Map tableMeta = new HashMap(); + ResultSet tables = null; + + try { + tables = databaseMetaData.getTables( + catalogNameToUse(catalogName), + schemaNameToUse(schemaName), + tableNameToUse(tableName), + null); + while (tables != null && tables.next()) { + TableMetaData tmd = new TableMetaData(); + tmd.setCatalogName(tables.getString("TABLE_CAT")); + tmd.setSchemaName(tables.getString("TABLE_SCHEM")); + tmd.setTableName(tables.getString("TABLE_NAME")); + tmd.setType(tables.getString("TABLE_TYPE")); + if (tmd.getSchemaName() == null) { + tableMeta.put(userName.toUpperCase(), tmd); + } + else { + tableMeta.put(tmd.getSchemaName().toUpperCase(), tmd); + } + } + } + catch (SQLException se) { + logger.warn("Error while accessing table meta data results" + se.getMessage()); + } + finally { + if (tables != null) { + try { + tables.close(); + } catch (SQLException e) { + logger.warn("Error while closing table meta data reults" + e.getMessage()); + } + } + } + + if (tableMeta.size() < 1) { + logger.warn("Unable to locate table meta data for '" + tableName +"' -- column names must be provided"); + } + else { + TableMetaData tmd = null; + if (schemaName == null) { + tmd = tableMeta.get(userName.toUpperCase()); + if (tmd == null) { + tmd = tableMeta.get("PUBLIC"); + if (tmd == null) { + tmd = tableMeta.get("DBO"); + } + if (tmd == null) { + throw new DataAccessResourceFailureException("Unable to locate table meta data for '" + tableName + "' in the default schema"); + } + } + } + else { + tmd = tableMeta.get(schemaName.toUpperCase()); + if (tmd == null) { + throw new DataAccessResourceFailureException("Unable to locate table meta data for '" + tableName + "' in the '" + schemaName + "' schema"); + } + } + + processTableColumns(databaseMetaData, tmd); + } + } + + /** + * Method supporting the metedata processing for a table's columns + */ + private void processTableColumns(DatabaseMetaData databaseMetaData, TableMetaData tmd) { + ResultSet tableColumns = null; + String metaDataCatalogName = metaDataCatalogNameToUse(tmd.getCatalogName()); + String metaDataSchemaName = metaDataSchemaNameToUse(tmd.getSchemaName()); + String metaDataTableName = tableNameToUse(tmd.getTableName()); + if (logger.isDebugEnabled()) { + logger.debug("Retrieving metadata for " + metaDataCatalogName + "/" + + metaDataSchemaName + "/" + metaDataTableName); + } + try { + tableColumns = databaseMetaData.getColumns( + metaDataCatalogName, + metaDataSchemaName, + metaDataTableName, + null); + while (tableColumns.next()) { + TableParameterMetaData meta = new TableParameterMetaData( + tableColumns.getString("COLUMN_NAME"), + tableColumns.getInt("DATA_TYPE"), + tableColumns.getBoolean("NULLABLE") + ); + insertParameterMetaData.add(meta); + if (logger.isDebugEnabled()) { + logger.debug("Retrieved metadata: " + + meta.getParameterName() + + " " + meta.getSqlType() + + " " + meta.isNullable() + ); + } + } + } + catch (SQLException se) { + logger.warn("Error while retreiving metadata for procedure columns: " + se.getMessage()); + } + finally { + try { + if (tableColumns != null) + tableColumns.close(); + } + catch (SQLException se) { + logger.warn("Problem closing resultset for procedure column metadata " + se.getMessage()); + } + } + + } + + + /** + * Class representing table meta data + */ + private class TableMetaData { + private String catalogName; + private String schemaName; + private String tableName; + private String type; + + + public String getCatalogName() { + return catalogName; + } + + public void setCatalogName(String catalogName) { + this.catalogName = catalogName; + } + + public String getSchemaName() { + return schemaName; + } + + public void setSchemaName(String schemaName) { + this.schemaName = schemaName; + } + + public String getTableName() { + return tableName; + } + + public void setTableName(String tableName) { + this.tableName = tableName; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + } +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/HsqlTableMetaDataProvider.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/HsqlTableMetaDataProvider.java new file mode 100644 index 0000000000..d1dee3465a --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/HsqlTableMetaDataProvider.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc.core.metadata; + +import java.sql.DatabaseMetaData; +import java.sql.SQLException; + +/** + * The HSQL specific implementation of the {@link TableMetaDataProvider}. Suports a feature for + * retreiving generated keys without the JDBC 3.0 getGeneratedKeys support. + * + * @author Thomas Risberg + * @since 2.5 + */ +public class HsqlTableMetaDataProvider extends GenericTableMetaDataProvider { + + public HsqlTableMetaDataProvider(DatabaseMetaData databaseMetaData) throws SQLException { + super(databaseMetaData); + } + + + public boolean isGetGeneratedKeysSimulated() { + return true; + } + + + public String getSimpleQueryForGetGeneratedKey(String tableName, String keyColumnName) { + return "select max(identity()) from " + tableName; + } +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/OracleCallMetaDataProvider.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/OracleCallMetaDataProvider.java new file mode 100644 index 0000000000..de063d5f8a --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/OracleCallMetaDataProvider.java @@ -0,0 +1,82 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc.core.metadata; + +import java.sql.DatabaseMetaData; +import java.sql.SQLException; +import java.sql.Types; + +import org.springframework.jdbc.core.ColumnMapRowMapper; +import org.springframework.jdbc.core.SqlOutParameter; +import org.springframework.jdbc.core.SqlParameter; +import org.springframework.jdbc.core.SqlInOutParameter; + +/** + * Oracle specific implementation for the {@link CallMetaDataProvider} interface. + * This class is intended for internal use by the Simple JDBC classes. + * + * @author Thomas Risberg + * @since 2.5 + */ +public class OracleCallMetaDataProvider extends GenericCallMetaDataProvider { + + private static final String REF_CURSOR_NAME = "REF CURSOR"; + + + public OracleCallMetaDataProvider(DatabaseMetaData databaseMetaData) throws SQLException { + super(databaseMetaData); + } + + + @Override + public boolean isReturnResultSetSupported() { + return false; + } + + @Override + public boolean isRefCursorSupported() { + return true; + } + + @Override + public int getRefCursorSqlType() { + return -10; + } + + @Override + public String metaDataCatalogNameToUse(String catalogName) { + // Oracle uses catalog name for package name or an empty string if no package + return catalogName == null ? "" : catalogNameToUse(catalogName); + } + + @Override + public String metaDataSchemaNameToUse(String schemaName) { + // Use current user schema if no schema specified + return schemaName == null ? getUserName() : super.metaDataSchemaNameToUse(schemaName); + } + + @Override + public SqlParameter createDefaultOutParameter(String parameterName, CallParameterMetaData meta) { + if (meta.getSqlType() == Types.OTHER && REF_CURSOR_NAME.equals(meta.getTypeName())) { + return new SqlOutParameter(parameterName, getRefCursorSqlType(), new ColumnMapRowMapper()); + } + else { + return super.createDefaultOutParameter(parameterName, meta); + } + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/PostgresCallMetaDataProvider.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/PostgresCallMetaDataProvider.java new file mode 100644 index 0000000000..d23db80d46 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/PostgresCallMetaDataProvider.java @@ -0,0 +1,64 @@ +package org.springframework.jdbc.core.metadata; + +import org.springframework.jdbc.core.SqlParameter; +import org.springframework.jdbc.core.SqlOutParameter; +import org.springframework.jdbc.core.ColumnMapRowMapper; + +import java.sql.DatabaseMetaData; +import java.sql.SQLException; +import java.sql.Types; + +/** + * Oracle specific implementation for the {@link org.springframework.jdbc.core.metadata.CallMetaDataProvider} interface. + * This class is intended for internal use by the Simple JDBC classes. + * + * @author Thomas Risberg + * @since 2.5 + */ +public class PostgresCallMetaDataProvider extends GenericCallMetaDataProvider { + + private static final String RETURN_VALUE_NAME = "returnValue"; + + + public PostgresCallMetaDataProvider(DatabaseMetaData databaseMetaData) throws SQLException { + super(databaseMetaData); + } + + + @Override + public boolean isReturnResultSetSupported() { + return false; + } + + @Override + public boolean isRefCursorSupported() { + return true; + } + + @Override + public int getRefCursorSqlType() { + return Types.OTHER; + } + + @Override + public String metaDataSchemaNameToUse(String schemaName) { + // Use public schema if no schema specified + return schemaName == null ? "public" : super.metaDataSchemaNameToUse(schemaName); + } + + @Override + public SqlParameter createDefaultOutParameter(String parameterName, CallParameterMetaData meta) { + if (meta.getSqlType() == Types.OTHER && "refcursor".equals(meta.getTypeName())) { + return new SqlOutParameter(parameterName, getRefCursorSqlType(), new ColumnMapRowMapper()); + } + else { + return super.createDefaultOutParameter(parameterName, meta); + } + } + + @Override + public boolean byPassReturnParameter(String parameterName) { + return (RETURN_VALUE_NAME.equals(parameterName) || + super.byPassReturnParameter(parameterName)); + } +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/PostgresTableMetaDataProvider.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/PostgresTableMetaDataProvider.java new file mode 100644 index 0000000000..7a60b7a2ed --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/PostgresTableMetaDataProvider.java @@ -0,0 +1,34 @@ +package org.springframework.jdbc.core.metadata; + +import java.sql.DatabaseMetaData; +import java.sql.SQLException; + +/** + * The HSQL specific implementation of the {@link org.springframework.jdbc.core.metadata.TableMetaDataProvider}. Suports a feature for + * retreiving generated keys without the JDBC 3.0 getGeneratedKeys support. + * + * @author Thomas Risberg + * @since 2.5 + */ +public class PostgresTableMetaDataProvider extends GenericTableMetaDataProvider { + + public PostgresTableMetaDataProvider(DatabaseMetaData databaseMetaData) throws SQLException { + super(databaseMetaData); + } + + + public boolean isGetGeneratedKeysSimulated() { + if (getDatabaseVersion().compareTo("8.2.0") >= 0) { + return true; + } + else { + logger.warn("PostgreSQL does not support getGeneratedKeys or INSERT ... RETURNING in version " + getDatabaseVersion()); + return false; + } + } + + + public String getSimpleQueryForGetGeneratedKey(String tableName, String keyColumnName) { + return "RETURNING " + keyColumnName; + } +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/SqlServerCallMetaDataProvider.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/SqlServerCallMetaDataProvider.java new file mode 100644 index 0000000000..93725b0d73 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/SqlServerCallMetaDataProvider.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc.core.metadata; + +import java.sql.DatabaseMetaData; +import java.sql.SQLException; + +/** + * SQL Server specific implementation for the {@link CallMetaDataProvider} interface. + * This class is intended for internal use by the Simple JDBC classes. + * + * @author Thomas Risberg + * @since 2.5 + */ +public class SqlServerCallMetaDataProvider extends GenericCallMetaDataProvider { + + private static final String REMOVABLE_COLUMN_PREFIX = "@"; + + private static final String RETURN_VALUE_NAME = "@RETURN_VALUE"; + + + public SqlServerCallMetaDataProvider(DatabaseMetaData databaseMetaData) throws SQLException { + super(databaseMetaData); + } + + + @Override + public String parameterNameToUse(String parameterName) { + if (parameterName == null) { + return null; + } + else if (parameterName.length() > 1 && parameterName.startsWith(REMOVABLE_COLUMN_PREFIX)) { + return super.parameterNameToUse(parameterName.substring(1)); + } + else { + return super.parameterNameToUse(parameterName); + } + } + + @Override + public boolean byPassReturnParameter(String parameterName) { + return (RETURN_VALUE_NAME.equals(parameterName) || super.byPassReturnParameter(parameterName)); + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/SybaseCallMetaDataProvider.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/SybaseCallMetaDataProvider.java new file mode 100644 index 0000000000..7f0757366e --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/SybaseCallMetaDataProvider.java @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc.core.metadata; + +import java.sql.DatabaseMetaData; +import java.sql.SQLException; + +/** + * Sybase specific implementation for the {@link CallMetaDataProvider} interface. + * This class is intended for internal use by the Simple JDBC classes. + * + * @author Thomas Risberg + * @since 2.5 + */ +public class SybaseCallMetaDataProvider extends GenericCallMetaDataProvider { + + private static final String REMOVABLE_COLUMN_PREFIX = "@"; + + private static final String RETURN_VALUE_NAME = "RETURN_VALUE"; + + + public SybaseCallMetaDataProvider(DatabaseMetaData databaseMetaData) throws SQLException { + super(databaseMetaData); + } + + + @Override + public String parameterNameToUse(String parameterName) { + if (parameterName == null) { + return null; + } + else if (parameterName.length() > 1 && parameterName.startsWith(REMOVABLE_COLUMN_PREFIX)) { + return super.parameterNameToUse(parameterName.substring(1)); + } + else { + return super.parameterNameToUse(parameterName); + } + } + + @Override + public boolean byPassReturnParameter(String parameterName) { + return (RETURN_VALUE_NAME.equals(parameterName) || + RETURN_VALUE_NAME.equals(parameterNameToUse(parameterName)) || + super.byPassReturnParameter(parameterName)); + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/TableMetaDataContext.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/TableMetaDataContext.java new file mode 100644 index 0000000000..d62e0d9ec7 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/TableMetaDataContext.java @@ -0,0 +1,347 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc.core.metadata; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.sql.DataSource; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.jdbc.core.SqlTypeValue; +import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.SqlParameterSource; +import org.springframework.jdbc.core.namedparam.SqlParameterSourceUtils; +import org.springframework.jdbc.support.JdbcUtils; + +/** + * Class to manage context metadata used for the configuration + * and execution of operations on a database table. + * + * @author Thomas Risberg + * @since 2.5 + */ +public class TableMetaDataContext { + + /** Logger available to subclasses */ + protected final Log logger = LogFactory.getLog(getClass()); + + /** name of procedure to call **/ + private String tableName; + + /** name of catalog for call **/ + private String catalogName; + + /** name of schema for call **/ + private String schemaName; + + /** List of columns objects to be used in this context */ + private List tableColumns = new ArrayList(); + + /** should we access insert parameter meta data info or not */ + private boolean accessTableParameterMetaData = true; + + /** the provider of call meta data */ + private TableMetaDataProvider metaDataProvider; + + /** are we using generated key columns */ + private boolean generatedKeyColumnsUsed = false; + + + /** + * Set the name of the table for this context. + */ + public void setTableName(String tableName) { + this.tableName = tableName; + } + + /** + * Get the name of the table for this context. + */ + public String getTableName() { + return this.tableName; + } + + /** + * Set the name of the catalog for this context. + */ + public void setCatalogName(String catalogName) { + this.catalogName = catalogName; + } + + /** + * Get the name of the catalog for this context. + */ + public String getCatalogName() { + return this.catalogName; + } + + /** + * Set the name of the schema for this context. + */ + public void setSchemaName(String schemaName) { + this.schemaName = schemaName; + } + + /** + * Get the name of the schema for this context. + */ + public String getSchemaName() { + return this.schemaName; + } + + /** + * Specify whether we should access table column meta data. + */ + public void setAccessTableParameterMetaData(boolean accessTableParameterMetaData) { + this.accessTableParameterMetaData = accessTableParameterMetaData; + } + + /** + * Are we accessing table meta data? + */ + public boolean isAccessTableParameterMetaData() { + return this.accessTableParameterMetaData; + } + + + /** + * Get a List of the table column names. + */ + public List getTableColumns() { + return this.tableColumns; + } + + /** + * Does this database support the JDBC 3.0 feature of retreiving generated keys + * {@link java.sql.DatabaseMetaData#supportsGetGeneratedKeys()}? + */ + public boolean isGetGeneratedKeysSupported() { + return this.metaDataProvider.isGetGeneratedKeysSupported(); + } + + /** + * Does this database support simple query to retrieve generated keys + * when the JDBC 3.0 feature is not supported + * {@link java.sql.DatabaseMetaData#supportsGetGeneratedKeys()}? + */ + public boolean isGetGeneratedKeysSimulated() { + return this.metaDataProvider.isGetGeneratedKeysSimulated(); + } + + /** + * Does this database support simple query to retrieve generated keys + * when the JDBC 3.0 feature is not supported + * {@link java.sql.DatabaseMetaData#supportsGetGeneratedKeys()}? + */ + public String getSimulationQueryForGetGeneratedKey(String tableName, String keyColumnName) { + return this.metaDataProvider.getSimpleQueryForGetGeneratedKey(tableName, keyColumnName); + } + + /** + * Is a column name String array for retreiving generated keys supported + * {@link java.sql.Connection#createStruct(String, Object[])}? + */ + public boolean isGeneratedKeysColumnNameArraySupported() { + return this.metaDataProvider.isGeneratedKeysColumnNameArraySupported(); + } + + + /** + * Process the current meta data with the provided configuration options + * @param dataSource the DataSource being used + * @param declaredColumns any coluns that are declared + * @param generatedKeyNames name of generated keys + */ + public void processMetaData(DataSource dataSource, List declaredColumns, String[] generatedKeyNames) { + this.metaDataProvider = TableMetaDataProviderFactory.createMetaDataProvider(dataSource, this); + this.tableColumns = reconcileColumnsToUse(declaredColumns, generatedKeyNames); + } + + /** + * Compare columns created from metadata with declared columns and return a reconciled list. + * @param declaredColumns declared column names + * @param generatedKeyNames names of generated key columns + */ + private List reconcileColumnsToUse(List declaredColumns, String[] generatedKeyNames) { + if (generatedKeyNames.length > 0) { + generatedKeyColumnsUsed = true; + } + if (declaredColumns.size() > 0) { + return new ArrayList(declaredColumns); + } + Set keys = new HashSet(generatedKeyNames.length); + for (String key : generatedKeyNames) { + keys.add(key.toUpperCase()); + } + List columns = new ArrayList(); + for (TableParameterMetaData meta : metaDataProvider.getTableParameterMetaData()) { + if (!keys.contains(meta.getParameterName().toUpperCase())) { + columns.add(meta.getParameterName()); + } + } + return columns; + } + + /** + * Match the provided column names and values with the list of columns used. + * @param parameterSource the parameter names and values + */ + public List matchInParameterValuesWithInsertColumns(SqlParameterSource parameterSource) { + List values = new ArrayList(); + // for parameter source lookups we need to provide caseinsensitive lookup support since the + // database metadata is not necessarily providing case sensitive column names + Map caseInsensitiveParameterNames = + SqlParameterSourceUtils.extractCaseInsensitiveParameterNames(parameterSource); + for (String column : tableColumns) { + if (parameterSource.hasValue(column)) { + values.add(SqlParameterSourceUtils.getTypedValue(parameterSource, column)); + } + else { + String lowerCaseName = column.toLowerCase(); + if (parameterSource.hasValue(lowerCaseName)) { + values.add(SqlParameterSourceUtils.getTypedValue(parameterSource, lowerCaseName)); + } + else { + String propertyName = JdbcUtils.convertUnderscoreNameToPropertyName(column); + if (parameterSource.hasValue(propertyName)) { + values.add(SqlParameterSourceUtils.getTypedValue(parameterSource, propertyName)); + } + else { + if (caseInsensitiveParameterNames.containsKey(lowerCaseName)) { + values.add( + SqlParameterSourceUtils.getTypedValue(parameterSource, + (String) caseInsensitiveParameterNames.get(lowerCaseName))); + } + else { + values.add(null); + } + } + } + } + } + return values; + } + + /** + * Match the provided column names and values with the list of columns used. + * @param inParameters the parameter names and values + */ + public List matchInParameterValuesWithInsertColumns(Map inParameters) { + List values = new ArrayList(); + Map source = new HashMap(); + for (String key : inParameters.keySet()) { + source.put(key.toLowerCase(), inParameters.get(key)); + } + for (String column : tableColumns) { + values.add(source.get(column.toLowerCase())); + } + return values; + } + + + /** + * Build the insert string based on configuration and metadata information + * @return the insert string to be used + */ + public String createInsertString(String[] generatedKeyNames) { + HashSet keys = new HashSet(generatedKeyNames.length); + for (String key : generatedKeyNames) { + keys.add(key.toUpperCase()); + } + StringBuilder insertStatement = new StringBuilder(); + insertStatement.append("INSERT INTO "); + if (this.getSchemaName() != null) { + insertStatement.append(this.getSchemaName()); + insertStatement.append("."); + } + insertStatement.append(this.getTableName()); + insertStatement.append(" ("); + int columnCount = 0; + for (String columnName : this.getTableColumns()) { + if (!keys.contains(columnName.toUpperCase())) { + columnCount++; + if (columnCount > 1) { + insertStatement.append(", "); + } + insertStatement.append(columnName); + } + } + insertStatement.append(") VALUES("); + if (columnCount < 1) { + if (generatedKeyColumnsUsed) { + logger.info("Unable to locate non-key columns for table '" + + this.getTableName() + "' so an empty insert statement is generated"); + } + else { + throw new InvalidDataAccessApiUsageException("Unable to locate columns for table '" + + this.getTableName() + "' so an insert statement can't be generated"); + } + } + for (int i = 0; i < columnCount; i++) { + if (i > 0) { + insertStatement.append(", "); + } + insertStatement.append("?"); + } + insertStatement.append(")"); + return insertStatement.toString(); + } + + /** + * Build the array of {@link java.sql.Types} based on configuration and metadata information + * @return the array of types to be used + */ + public int[] createInsertTypes() { + + int[] types = new int[this.getTableColumns().size()]; + + List parameters = this.metaDataProvider.getTableParameterMetaData(); + Map parameterMap = new HashMap(parameters.size()); + for (TableParameterMetaData tpmd : parameters) { + parameterMap.put(tpmd.getParameterName().toUpperCase(), tpmd); + } + + int typeIndx = 0; + for (String column : this.getTableColumns()) { + if (column == null) { + types[typeIndx] = SqlTypeValue.TYPE_UNKNOWN; + } + else { + TableParameterMetaData tpmd = parameterMap.get(column.toUpperCase()); + if (tpmd != null) { + types[typeIndx] = tpmd.getSqlType(); + } + else { + types[typeIndx] = SqlTypeValue.TYPE_UNKNOWN; + } + } + typeIndx++; + } + + return types; + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/TableMetaDataProvider.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/TableMetaDataProvider.java new file mode 100644 index 0000000000..af2f31e59f --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/TableMetaDataProvider.java @@ -0,0 +1,130 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc.core.metadata; + +import java.sql.DatabaseMetaData; +import java.sql.SQLException; +import java.util.List; + +/** + * Interface specifying the API to be implemented by a class providing table metedata. This is intended for internal use + * by the Simple JDBC classes. + * + * @author Thomas Risberg + * @since 2.5 + */ +public interface TableMetaDataProvider { + + /** + * Initialize using the database metedata provided + * @param databaseMetaData + * @throws SQLException + */ + void initializeWithMetaData(DatabaseMetaData databaseMetaData) throws SQLException; + + /** + * Initialize using provided database metadata, table and column information. This initalization can be + * turned off by specifying that column meta data should not be used. + * @param databaseMetaData used to retreive database specific information + * @param catalogName name of catalog to use or null + * @param schemaName name of schema name to use or null + * @param tableName name of the table + * @throws SQLException + */ + void initializeWithTableColumnMetaData(DatabaseMetaData databaseMetaData, String catalogName, String schemaName, String tableName) + throws SQLException; + + /** + * Get the table name formatted based on metadata information. This could include altering the case. + * + * @param tableName + * @return table name formatted + */ + String tableNameToUse(String tableName); + + /** + * Get the catalog name formatted based on metadata information. This could include altering the case. + * + * @param catalogName + * @return catalog name formatted + */ + String catalogNameToUse(String catalogName); + + /** + * Get the schema name formatted based on metadata information. This could include altering the case. + * + * @param schemaName + * @return schema name formatted + */ + String schemaNameToUse(String schemaName); + + /** + * Provide any modification of the catalog name passed in to match the meta data currently used. + * The reyurned value will be used for meta data lookups. This could include alterig the case used or + * providing a base catalog if mone provided. + * + * @param catalogName + * @return catalog name to use + */ + String metaDataCatalogNameToUse(String catalogName) ; + + /** + * Provide any modification of the schema name passed in to match the meta data currently used. + * The reyurned value will be used for meta data lookups. This could include alterig the case used or + * providing a base schema if mone provided. + * + * @param schemaName + * @return schema name to use + */ + String metaDataSchemaNameToUse(String schemaName) ; + + /** + * Are we using the meta data for the table columns? + */ + boolean isTableColumnMetaDataUsed(); + + /** + * Does this database support the JDBC 3.0 feature of retreiving generated keys + * {@link java.sql.DatabaseMetaData#supportsGetGeneratedKeys()} + */ + boolean isGetGeneratedKeysSupported(); + + /** + * Does this database support a simple quey to retreive the generated key whe the JDBC 3.0 feature + * of retreiving generated keys is not supported + * {@link java.sql.DatabaseMetaData#supportsGetGeneratedKeys()} + */ + boolean isGetGeneratedKeysSimulated(); + + /** + * Get the simple query to retreive a generated key + */ + String getSimpleQueryForGetGeneratedKey(String tableName, String keyColumnName); + + /** + * Does this database support a column name String array for retreiving generated keys + * {@link java.sql.Connection#createStruct(String, Object[])} + */ + boolean isGeneratedKeysColumnNameArraySupported(); + + /** + * Get the table parameter metadata that is currently used. + * @return List of {@link TableParameterMetaData} + */ + List getTableParameterMetaData(); + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/TableMetaDataProviderFactory.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/TableMetaDataProviderFactory.java new file mode 100644 index 0000000000..3084b3f97d --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/TableMetaDataProviderFactory.java @@ -0,0 +1,85 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc.core.metadata; + +import java.sql.DatabaseMetaData; +import java.sql.SQLException; + +import javax.sql.DataSource; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.jdbc.support.DatabaseMetaDataCallback; +import org.springframework.jdbc.support.JdbcUtils; +import org.springframework.jdbc.support.MetaDataAccessException; + +/** + * Factory used to create a {@link TableMetaDataProvider} implementation based on the type of databse being used. + * + * @author Thomas Risberg + * @since 2.5 + */ +public class TableMetaDataProviderFactory { + + /** Logger */ + private static final Log logger = LogFactory.getLog(TableMetaDataProviderFactory.class); + + /** + * Create a TableMetaDataProvider based on the database metedata + * @param dataSource used to retrieve metedata + * @param context the class that holds configuration and metedata + * @return instance of the TableMetaDataProvider implementation to be used + */ + static public TableMetaDataProvider createMetaDataProvider(DataSource dataSource, + final TableMetaDataContext context) { + try { + return (TableMetaDataProvider) JdbcUtils.extractDatabaseMetaData( + dataSource, new DatabaseMetaDataCallback() { + + public Object processMetaData(DatabaseMetaData databaseMetaData) + throws SQLException, MetaDataAccessException { + String databaseProductName = JdbcUtils.commonDatabaseName(databaseMetaData.getDatabaseProductName()); + boolean accessTableColumnMetaData = context.isAccessTableParameterMetaData(); + TableMetaDataProvider provider; + if ("HSQL Database Engine".equals(databaseProductName)) { + provider = new HsqlTableMetaDataProvider(databaseMetaData); + } + else if ("PostgreSQL".equals(databaseProductName)) { + provider = new PostgresTableMetaDataProvider(databaseMetaData); + } + else { + provider = new GenericTableMetaDataProvider(databaseMetaData); + } + if (logger.isDebugEnabled()) { + logger.debug("Using " + provider.getClass().getName()); + } + provider.initializeWithMetaData(databaseMetaData); + if (accessTableColumnMetaData) { + provider.initializeWithTableColumnMetaData(databaseMetaData, context.getCatalogName(), context.getSchemaName(), context.getTableName()); + } + return provider; + } + }); + } catch (MetaDataAccessException e) { + throw new DataAccessResourceFailureException("Error retreiving database metadata", e); + } + + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/TableParameterMetaData.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/TableParameterMetaData.java new file mode 100644 index 0000000000..4488063b70 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/TableParameterMetaData.java @@ -0,0 +1,65 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc.core.metadata; + +/** + * Holder of metadata for a specific parameter that is used for table processing. + * + * @author Thomas Risberg + * @since 2.5 + */ +public class TableParameterMetaData { + + private final String parameterName; + + private final int sqlType; + + private final boolean nullable; + + + /** + * Constructor taking all the properties. + */ + public TableParameterMetaData(String columnName, int sqlType, boolean nullable) { + this.parameterName = columnName; + this.sqlType = sqlType; + this.nullable = nullable; + } + + + /** + * Get the parameter name. + */ + public String getParameterName() { + return this.parameterName; + } + + /** + * Get the parameter SQL type. + */ + public int getSqlType() { + return this.sqlType; + } + + /** + * Get whether the parameter/column is nullable. + */ + public boolean isNullable() { + return this.nullable; + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/package.html b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/package.html new file mode 100644 index 0000000000..587f20302d --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/metadata/package.html @@ -0,0 +1,7 @@ + + + +Context metadata abstraction for the configuration and execution of a stored procedure call. + + + diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/namedparam/AbstractSqlParameterSource.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/namedparam/AbstractSqlParameterSource.java new file mode 100644 index 0000000000..514ed4483f --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/namedparam/AbstractSqlParameterSource.java @@ -0,0 +1,84 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.core.namedparam; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.util.Assert; + +/** + * Abstract base class for {@link SqlParameterSource} implementations. + * Provides registration of SQL types per parameter. + * + * @author Juergen Hoeller + * @since 2.0 + */ +public abstract class AbstractSqlParameterSource implements SqlParameterSource { + + private final Map sqlTypes = new HashMap(); + + private final Map typeNames = new HashMap(); + + + /** + * Register a SQL type for the given parameter. + * @param paramName the name of the parameter + * @param sqlType the SQL type of the parameter + */ + public void registerSqlType(String paramName, int sqlType) { + Assert.notNull(paramName, "Parameter name must not be null"); + this.sqlTypes.put(paramName, new Integer(sqlType)); + } + + /** + * Register a SQL type for the given parameter. + * @param paramName the name of the parameter + * @param typeName the type name of the parameter + */ + public void registerTypeName(String paramName, String typeName) { + Assert.notNull(paramName, "Parameter name must not be null"); + this.typeNames.put(paramName, typeName); + } + + /** + * Return the SQL type for the given parameter, if registered. + * @param paramName the name of the parameter + * @return the SQL type of the parameter, + * or TYPE_UNKNOWN if not registered + */ + public int getSqlType(String paramName) { + Assert.notNull(paramName, "Parameter name must not be null"); + Integer sqlType = (Integer) this.sqlTypes.get(paramName); + if (sqlType != null) { + return sqlType.intValue(); + } + return TYPE_UNKNOWN; + } + + /** + * Return the type name for the given parameter, if registered. + * @param paramName the name of the parameter + * @return the type name of the parameter, + * or null if not registered + */ + public String getTypeName(String paramName) { + Assert.notNull(paramName, "Parameter name must not be null"); + return (String) this.typeNames.get(paramName); + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/namedparam/BeanPropertySqlParameterSource.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/namedparam/BeanPropertySqlParameterSource.java new file mode 100644 index 0000000000..5640a77179 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/namedparam/BeanPropertySqlParameterSource.java @@ -0,0 +1,103 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.core.namedparam; + +import java.beans.PropertyDescriptor; +import java.util.ArrayList; +import java.util.List; + +import org.springframework.beans.BeanWrapper; +import org.springframework.beans.NotReadablePropertyException; +import org.springframework.beans.PropertyAccessor; +import org.springframework.beans.PropertyAccessorFactory; +import org.springframework.jdbc.core.StatementCreatorUtils; + +/** + * {@link SqlParameterSource} implementation that obtains parameter values + * from bean properties of a given JavaBean object. The names of the bean + * properties have to match the parameter names. + * + *

Uses a Spring BeanWrapper for bean property access underneath. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @since 2.0 + * @see NamedParameterJdbcTemplate + * @see org.springframework.beans.BeanWrapper + */ +public class BeanPropertySqlParameterSource extends AbstractSqlParameterSource { + + private final BeanWrapper beanWrapper; + + private String[] propertyNames; + + + /** + * Create a new BeanPropertySqlParameterSource for the given bean. + * @param object the bean instance to wrap + */ + public BeanPropertySqlParameterSource(Object object) { + this.beanWrapper = PropertyAccessorFactory.forBeanPropertyAccess(object); + } + + + public boolean hasValue(String paramName) { + return this.beanWrapper.isReadableProperty(paramName); + } + + public Object getValue(String paramName) throws IllegalArgumentException { + try { + return this.beanWrapper.getPropertyValue(paramName); + } + catch (NotReadablePropertyException ex) { + throw new IllegalArgumentException(ex.getMessage()); + } + } + + /** + * Provide access to the property names of the wrapped bean. + * Uses support provided in the {@link PropertyAccessor} interface. + * @return an array containing all the known property names + */ + public String[] getReadablePropertyNames() { + if (this.propertyNames == null) { + List names = new ArrayList(); + PropertyDescriptor[] props = this.beanWrapper.getPropertyDescriptors(); + for (int i = 0; i < props.length; i++) { + if (this.beanWrapper.isReadableProperty(props[i].getName())) { + names.add(props[i].getName()); + } + } + this.propertyNames = (String[]) names.toArray(new String[names.size()]); + } + return this.propertyNames; + } + + /** + * Derives a default SQL type from the corresponding property type. + * @see org.springframework.jdbc.core.StatementCreatorUtils#javaTypeToSqlParameterType + */ + public int getSqlType(String paramName) { + int sqlType = super.getSqlType(paramName); + if (sqlType != TYPE_UNKNOWN) { + return sqlType; + } + Class propType = this.beanWrapper.getPropertyType(paramName); + return StatementCreatorUtils.javaTypeToSqlParameterType(propType); + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/namedparam/MapSqlParameterSource.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/namedparam/MapSqlParameterSource.java new file mode 100644 index 0000000000..9e86ac3fc1 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/namedparam/MapSqlParameterSource.java @@ -0,0 +1,165 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed 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.springframework.jdbc.core.namedparam; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Iterator; + +import org.springframework.util.Assert; +import org.springframework.jdbc.core.SqlParameterValue; + +/** + * {@link SqlParameterSource} implementation that holds a given Map of parameters. + * + *

This class is intended for passing in a simple Map of parameter values + * to the methods of the {@link NamedParameterJdbcTemplate} class. + * + *

The addValue methods on this class will make adding several + * values easier. The methods return a reference to the {@link MapSqlParameterSource} + * itself, so you can chain several method calls together within a single statement. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @since 2.0 + * @see #addValue(String, Object) + * @see #addValue(String, Object, int) + * @see #registerSqlType + * @see NamedParameterJdbcTemplate + */ +public class MapSqlParameterSource extends AbstractSqlParameterSource { + + private final Map values = new HashMap(); + + + /** + * Create an empty MapSqlParameterSource, + * with values to be added via addValue. + * @see #addValue(String, Object) + */ + public MapSqlParameterSource() { + } + + /** + * Create a new MapSqlParameterSource, with one value + * comprised of the supplied arguments. + * @param paramName the name of the parameter + * @param value the value of the parameter + * @see #addValue(String, Object) + */ + public MapSqlParameterSource(String paramName, Object value) { + addValue(paramName, value); + } + + /** + * Create a new MapSqlParameterSource based on a Map. + * @param values a Map holding existing parameter values (can be null) + */ + public MapSqlParameterSource(Map values) { + addValues(values); + } + + + /** + * Add a parameter to this parameter source. + * @param paramName the name of the parameter + * @param value the value of the parameter + * @return a reference to this parameter source, + * so it's possible to chain several calls together + */ + public MapSqlParameterSource addValue(String paramName, Object value) { + Assert.notNull(paramName, "Parameter name must not be null"); + this.values.put(paramName, value); + if (value != null && value instanceof SqlParameterValue) { + registerSqlType(paramName, ((SqlParameterValue)value).getSqlType()); + } + return this; + } + + /** + * Add a parameter to this parameter source. + * @param paramName the name of the parameter + * @param value the value of the parameter + * @param sqlType the SQL type of the parameter + * @return a reference to this parameter source, + * so it's possible to chain several calls together + */ + public MapSqlParameterSource addValue(String paramName, Object value, int sqlType) { + Assert.notNull(paramName, "Parameter name must not be null"); + this.values.put(paramName, value); + registerSqlType(paramName, sqlType); + return this; + } + + /** + * Add a parameter to this parameter source. + * @param paramName the name of the parameter + * @param value the value of the parameter + * @param sqlType the SQL type of the parameter + * @param typeName the type name of the parameter + * @return a reference to this parameter source, + * so it's possible to chain several calls together + */ + public MapSqlParameterSource addValue(String paramName, Object value, int sqlType, String typeName) { + Assert.notNull(paramName, "Parameter name must not be null"); + this.values.put(paramName, value); + registerSqlType(paramName, sqlType); + registerTypeName(paramName, typeName); + return this; + } + + /** + * Add a Map of parameters to this parameter source. + * @param values a Map holding existing parameter values (can be null) + * @return a reference to this parameter source, + * so it's possible to chain several calls together + */ + public MapSqlParameterSource addValues(Map values) { + if (values != null) { + this.values.putAll(values); + for (Iterator iter = values.keySet().iterator(); iter.hasNext();) { + Object k = iter.next(); + Object o = values.get(k); + if (o != null && k instanceof String && o instanceof SqlParameterValue) { + registerSqlType((String)k, ((SqlParameterValue)o).getSqlType()); + } + } + } + return this; + } + + /** + * Expose the current parameter values as read-only Map. + */ + public Map getValues() { + return Collections.unmodifiableMap(this.values); + } + + + public boolean hasValue(String paramName) { + return this.values.containsKey(paramName); + } + + public Object getValue(String paramName) { + if (!hasValue(paramName)) { + throw new IllegalArgumentException("No value registered for key '" + paramName + "'"); + } + return this.values.get(paramName); + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcDaoSupport.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcDaoSupport.java new file mode 100644 index 0000000000..0639716f22 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcDaoSupport.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed 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.springframework.jdbc.core.namedparam; + +import org.springframework.jdbc.core.support.JdbcDaoSupport; + +/** + * Extension of JdbcDaoSupport that exposes a NamedParameterJdbcTemplate as well. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @since 2.0 + * @see NamedParameterJdbcTemplate + */ +public class NamedParameterJdbcDaoSupport extends JdbcDaoSupport { + + private NamedParameterJdbcTemplate namedParameterJdbcTemplate; + + + /** + * Create a NamedParameterJdbcTemplate based on the configured JdbcTemplate. + */ + protected void initTemplateConfig() { + this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(getJdbcTemplate()); + } + + /** + * Return a NamedParameterJdbcTemplate wrapping the configured JdbcTemplate. + */ + public NamedParameterJdbcTemplate getNamedParameterJdbcTemplate() { + return namedParameterJdbcTemplate; + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcOperations.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcOperations.java new file mode 100644 index 0000000000..5d897a3e19 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcOperations.java @@ -0,0 +1,492 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc.core.namedparam; + +import java.util.List; +import java.util.Map; + +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.core.JdbcOperations; +import org.springframework.jdbc.core.PreparedStatementCallback; +import org.springframework.jdbc.core.ResultSetExtractor; +import org.springframework.jdbc.core.RowCallbackHandler; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.support.KeyHolder; +import org.springframework.jdbc.support.rowset.SqlRowSet; + +/** + * Interface specifying a basic set of JDBC operations allowing the use + * of named parameters rather than the traditional '?' placeholders. + * + *

This is an alternative to the classic + * {@link org.springframework.jdbc.core.JdbcOperations} interface, + * implemented by {@link NamedParameterJdbcTemplate}. This interface is not + * often used directly, but provides a useful option to enhance testability, + * as it can easily be mocked or stubbed. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @since 2.0 + * @see NamedParameterJdbcTemplate + * @see org.springframework.jdbc.core.JdbcOperations + */ +public interface NamedParameterJdbcOperations { + + /** + * Expose the classic Spring JdbcTemplate to allow invocation of + * classic JDBC operations. + */ + JdbcOperations getJdbcOperations(); + + + /** + * Execute a JDBC data access operation, implemented as callback action + * working on a JDBC PreparedStatement. This allows for implementing arbitrary + * data access operations on a single Statement, within Spring's managed + * JDBC environment: that is, participating in Spring-managed transactions + * and converting JDBC SQLExceptions into Spring's DataAccessException hierarchy. + *

The callback action can return a result object, for example a + * domain object or a collection of domain objects. + * @param sql SQL to execute + * @param paramSource container of arguments to bind to the query + * @param action callback object that specifies the action + * @return a result object returned by the action, or null + * @throws DataAccessException if there is any problem + */ + Object execute(String sql, SqlParameterSource paramSource, PreparedStatementCallback action) + throws DataAccessException; + + /** + * Execute a JDBC data access operation, implemented as callback action + * working on a JDBC PreparedStatement. This allows for implementing arbitrary + * data access operations on a single Statement, within Spring's managed + * JDBC environment: that is, participating in Spring-managed transactions + * and converting JDBC SQLExceptions into Spring's DataAccessException hierarchy. + *

The callback action can return a result object, for example a + * domain object or a collection of domain objects. + * @param sql SQL to execute + * @param paramMap map of parameters to bind to the query + * (leaving it to the PreparedStatement to guess the corresponding SQL type) + * @param action callback object that specifies the action + * @return a result object returned by the action, or null + * @throws DataAccessException if there is any problem + */ + Object execute(String sql, Map paramMap, PreparedStatementCallback action) + throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a list + * of arguments to bind to the query, reading the ResultSet with a + * ResultSetExtractor. + * @param sql SQL query to execute + * @param paramSource container of arguments to bind to the query + * @param rse object that will extract results + * @return an arbitrary result object, as returned by the ResultSetExtractor + * @throws DataAccessException if the query fails + */ + Object query(String sql, SqlParameterSource paramSource, ResultSetExtractor rse) + throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a list + * of arguments to bind to the query, reading the ResultSet with a + * ResultSetExtractor. + * @param sql SQL query to execute + * @param paramMap map of parameters to bind to the query + * (leaving it to the PreparedStatement to guess the corresponding SQL type) + * @param rse object that will extract results + * @return an arbitrary result object, as returned by the ResultSetExtractor + * @throws org.springframework.dao.DataAccessException if the query fails + */ + Object query(String sql, Map paramMap, ResultSetExtractor rse) throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a list of + * arguments to bind to the query, reading the ResultSet on a per-row basis + * with a RowCallbackHandler. + * @param sql SQL query to execute + * @param paramSource container of arguments to bind to the query + * @param rch object that will extract results, one row at a time + * @throws DataAccessException if the query fails + */ + void query(String sql, SqlParameterSource paramSource, RowCallbackHandler rch) + throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a list of + * arguments to bind to the query, reading the ResultSet on a per-row basis + * with a RowCallbackHandler. + * @param sql SQL query to execute + * @param paramMap map of parameters to bind to the query + * (leaving it to the PreparedStatement to guess the corresponding SQL type) + * @param rch object that will extract results, one row at a time + * @throws org.springframework.dao.DataAccessException if the query fails + */ + void query(String sql, Map paramMap, RowCallbackHandler rch) throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a list + * of arguments to bind to the query, mapping each row to a Java object + * via a RowMapper. + * @param sql SQL query to execute + * @param paramSource container of arguments to bind to the query + * @param rowMapper object that will map one object per row + * @return the result List, containing mapped objects + * @throws org.springframework.dao.DataAccessException if the query fails + */ + List query(String sql, SqlParameterSource paramSource, RowMapper rowMapper) + throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a list + * of arguments to bind to the query, mapping each row to a Java object + * via a RowMapper. + * @param sql SQL query to execute + * @param paramMap map of parameters to bind to the query + * (leaving it to the PreparedStatement to guess the corresponding SQL type) + * @param rowMapper object that will map one object per row + * @return the result List, containing mapped objects + * @throws org.springframework.dao.DataAccessException if the query fails + */ + List query(String sql, Map paramMap, RowMapper rowMapper) throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a list + * of arguments to bind to the query, mapping a single result row to a + * Java object via a RowMapper. + * @param sql SQL query to execute + * @param paramSource container of arguments to bind to the query + * @param rowMapper object that will map one object per row + * @return the single mapped object + * @throws org.springframework.dao.IncorrectResultSizeDataAccessException + * if the query does not return exactly one row, or does not return exactly + * one column in that row + * @throws org.springframework.dao.DataAccessException if the query fails + */ + Object queryForObject(String sql, SqlParameterSource paramSource, RowMapper rowMapper) + throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a list + * of arguments to bind to the query, mapping a single result row to a + * Java object via a RowMapper. + * @param sql SQL query to execute + * @param paramMap map of parameters to bind to the query + * (leaving it to the PreparedStatement to guess the corresponding SQL type) + * @param rowMapper object that will map one object per row + * @return the single mapped object + * @throws org.springframework.dao.IncorrectResultSizeDataAccessException + * if the query does not return exactly one row, or does not return exactly + * one column in that row + * @throws org.springframework.dao.DataAccessException if the query fails + */ + Object queryForObject(String sql, Map paramMap, RowMapper rowMapper) throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a + * list of arguments to bind to the query, expecting a result object. + *

The query is expected to be a single row/single column query; the returned + * result will be directly mapped to the corresponding object type. + * @param sql SQL query to execute + * @param paramSource container of arguments to bind to the query + * @param requiredType the type that the result object is expected to match + * @return the result object of the required type, or null in case of SQL NULL + * @throws org.springframework.dao.IncorrectResultSizeDataAccessException + * if the query does not return exactly one row, or does not return exactly + * one column in that row + * @throws org.springframework.dao.DataAccessException if the query fails + * @see org.springframework.jdbc.core.JdbcTemplate#queryForObject(String, Class) + */ + Object queryForObject(String sql, SqlParameterSource paramSource, Class requiredType) + throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a + * list of arguments to bind to the query, expecting a result object. + *

The query is expected to be a single row/single column query; the returned + * result will be directly mapped to the corresponding object type. + * @param sql SQL query to execute + * @param paramMap map of parameters to bind to the query + * (leaving it to the PreparedStatement to guess the corresponding SQL type) + * @param requiredType the type that the result object is expected to match + * @return the result object of the required type, or null in case of SQL NULL + * @throws org.springframework.dao.IncorrectResultSizeDataAccessException + * if the query does not return exactly one row, or does not return exactly + * one column in that row + * @throws org.springframework.dao.DataAccessException if the query fails + * @see org.springframework.jdbc.core.JdbcTemplate#queryForObject(String, Class) + */ + Object queryForObject(String sql, Map paramMap, Class requiredType) throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a + * list of arguments to bind to the query, expecting a result Map. + *

The query is expected to be a single row query; the result row will be + * mapped to a Map (one entry for each column, using the column name as the key). + * @param sql SQL query to execute + * @param paramSource container of arguments to bind to the query + * @return the result Map (one entry for each column, using the column name as the key) + * @throws org.springframework.dao.IncorrectResultSizeDataAccessException + * if the query does not return exactly one row, or does not return exactly + * one column in that row + * @throws org.springframework.dao.DataAccessException if the query fails + * @see org.springframework.jdbc.core.JdbcTemplate#queryForMap(String) + * @see org.springframework.jdbc.core.ColumnMapRowMapper + */ + Map queryForMap(String sql, SqlParameterSource paramSource) throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a + * list of arguments to bind to the query, expecting a result Map. + * The queryForMap() methods defined by this interface are appropriate + * when you don't have a domain model. Otherwise, consider using + * one of the queryForObject() methods. + *

The query is expected to be a single row query; the result row will be + * mapped to a Map (one entry for each column, using the column name as the key). + * @param sql SQL query to execute + * @param paramMap map of parameters to bind to the query + * (leaving it to the PreparedStatement to guess the corresponding SQL type) + * @return the result Map (one entry for each column, using the column name as the key) + * @throws org.springframework.dao.IncorrectResultSizeDataAccessException + * if the query does not return exactly one row, or does not return exactly + * one column in that row + * @throws org.springframework.dao.DataAccessException if the query fails + * @see org.springframework.jdbc.core.JdbcTemplate#queryForMap(String) + * @see org.springframework.jdbc.core.ColumnMapRowMapper + */ + Map queryForMap(String sql, Map paramMap) throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a + * list of arguments to bind to the query, resulting in a long value. + *

The query is expected to be a single row/single column query that + * results in a long value. + * @param sql SQL query to execute + * @param paramSource container of arguments to bind to the query + * @return the long value, or 0 in case of SQL NULL + * @throws org.springframework.dao.IncorrectResultSizeDataAccessException + * if the query does not return exactly one row, or does not return exactly + * one column in that row + * @throws org.springframework.dao.DataAccessException if the query fails + * @see org.springframework.jdbc.core.JdbcTemplate#queryForLong(String) + */ + long queryForLong(String sql, SqlParameterSource paramSource) throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a + * list of arguments to bind to the query, resulting in a long value. + *

The query is expected to be a single row/single column query that + * results in a long value. + * @param sql SQL query to execute + * @param paramMap map of parameters to bind to the query + * (leaving it to the PreparedStatement to guess the corresponding SQL type) + * @return the long value, or 0 in case of SQL NULL + * @throws org.springframework.dao.IncorrectResultSizeDataAccessException + * if the query does not return exactly one row, or does not return exactly + * one column in that row + * @throws org.springframework.dao.DataAccessException if the query fails + * @see org.springframework.jdbc.core.JdbcTemplate#queryForLong(String) + */ + long queryForLong(String sql, Map paramMap) throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a + * list of arguments to bind to the query, resulting in an int value. + *

The query is expected to be a single row/single column query that + * results in an int value. + * @param sql SQL query to execute + * @param paramSource container of arguments to bind to the query + * @return the int value, or 0 in case of SQL NULL + * @throws org.springframework.dao.IncorrectResultSizeDataAccessException if the query does not return + * exactly one row, or does not return exactly one column in that row + * @throws org.springframework.dao.DataAccessException if the query fails + * @see org.springframework.jdbc.core.JdbcTemplate#queryForInt(String) + */ + int queryForInt(String sql, SqlParameterSource paramSource) throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a + * list of arguments to bind to the query, resulting in an int value. + *

The query is expected to be a single row/single column query that + * results in an int value. + * @param sql SQL query to execute + * @param paramMap map of parameters to bind to the query + * (leaving it to the PreparedStatement to guess the corresponding SQL type) + * @return the int value, or 0 in case of SQL NULL + * @throws org.springframework.dao.IncorrectResultSizeDataAccessException if the query does not return + * exactly one row, or does not return exactly one column in that row + * @throws org.springframework.dao.DataAccessException if the query fails + * @see org.springframework.jdbc.core.JdbcTemplate#queryForInt(String) + */ + int queryForInt(String sql, Map paramMap) throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a + * list of arguments to bind to the query, expecting a result list. + *

The results will be mapped to a List (one entry for each row) of + * result objects, each of them matching the specified element type. + * @param sql SQL query to execute + * @param paramSource container of arguments to bind to the query + * @param elementType the required type of element in the result list + * (for example, Integer.class) + * @return a List of objects that match the specified element type + * @throws org.springframework.dao.DataAccessException if the query fails + * @see org.springframework.jdbc.core.JdbcTemplate#queryForList(String, Class) + * @see org.springframework.jdbc.core.SingleColumnRowMapper + */ + List queryForList(String sql, SqlParameterSource paramSource, Class elementType) + throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a + * list of arguments to bind to the query, expecting a result list. + *

The results will be mapped to a List (one entry for each row) of + * result objects, each of them matching the specified element type. + * @param sql SQL query to execute + * @param paramMap map of parameters to bind to the query + * (leaving it to the PreparedStatement to guess the corresponding SQL type) + * @param elementType the required type of element in the result list + * (for example, Integer.class) + * @return a List of objects that match the specified element type + * @throws org.springframework.dao.DataAccessException if the query fails + * @see org.springframework.jdbc.core.JdbcTemplate#queryForList(String, Class) + * @see org.springframework.jdbc.core.SingleColumnRowMapper + */ + List queryForList(String sql, Map paramMap, Class elementType) throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a + * list of arguments to bind to the query, expecting a result list. + *

The results will be mapped to a List (one entry for each row) of + * Maps (one entry for each column, using the column name as the key). + * Thus Each element in the list will be of the form returned by this interface's + * queryForMap() methods. + * @param sql SQL query to execute + * @param paramSource container of arguments to bind to the query + * @return a List that contains a Map per row + * @throws org.springframework.dao.DataAccessException if the query fails + * @see org.springframework.jdbc.core.JdbcTemplate#queryForList(String) + */ + List queryForList(String sql, SqlParameterSource paramSource) throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a + * list of arguments to bind to the query, expecting a result list. + *

The results will be mapped to a List (one entry for each row) of + * Maps (one entry for each column, using the column name as the key). + * Each element in the list will be of the form returned by this interface's + * queryForMap() methods. + * @param sql SQL query to execute + * @param paramMap map of parameters to bind to the query + * (leaving it to the PreparedStatement to guess the corresponding SQL type) + * @return a List that contains a Map per row + * @throws org.springframework.dao.DataAccessException if the query fails + * @see org.springframework.jdbc.core.JdbcTemplate#queryForList(String) + */ + List queryForList(String sql, Map paramMap) throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a + * list of arguments to bind to the query, expecting a SqlRowSet. + *

The results will be mapped to an SqlRowSet which holds the data in a + * disconnected fashion. This wrapper will translate any SQLExceptions thrown. + *

Note that that, for the default implementation, JDBC RowSet support needs to + * be available at runtime: by default, Sun's com.sun.rowset.CachedRowSetImpl + * class is used, which is part of JDK 1.5+ and also available separately as part of + * Sun's JDBC RowSet Implementations download (rowset.jar). + * @param sql SQL query to execute + * @param paramSource container of arguments to bind to the query + * @return a SqlRowSet representation (possibly a wrapper around a + * javax.sql.rowset.CachedRowSet) + * @throws org.springframework.dao.DataAccessException if there is any problem executing the query + * @see org.springframework.jdbc.core.JdbcTemplate#queryForRowSet(String) + * @see org.springframework.jdbc.core.SqlRowSetResultSetExtractor + * @see javax.sql.rowset.CachedRowSet + */ + SqlRowSet queryForRowSet(String sql, SqlParameterSource paramSource) throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a + * list of arguments to bind to the query, expecting a SqlRowSet. + *

The results will be mapped to an SqlRowSet which holds the data in a + * disconnected fashion. This wrapper will translate any SQLExceptions thrown. + *

Note that that, for the default implementation, JDBC RowSet support needs to + * be available at runtime: by default, Sun's com.sun.rowset.CachedRowSetImpl + * class is used, which is part of JDK 1.5+ and also available separately as part of + * Sun's JDBC RowSet Implementations download (rowset.jar). + * @param sql SQL query to execute + * @param paramMap map of parameters to bind to the query + * (leaving it to the PreparedStatement to guess the corresponding SQL type) + * @return a SqlRowSet representation (possibly a wrapper around a + * javax.sql.rowset.CachedRowSet) + * @throws org.springframework.dao.DataAccessException if there is any problem executing the query + * @see org.springframework.jdbc.core.JdbcTemplate#queryForRowSet(String) + * @see org.springframework.jdbc.core.SqlRowSetResultSetExtractor + * @see javax.sql.rowset.CachedRowSet + */ + SqlRowSet queryForRowSet(String sql, Map paramMap) throws DataAccessException; + + /** + * Issue an update via a prepared statement, binding the given arguments. + * @param sql SQL containing named parameters + * @param paramSource container of arguments and SQL types to bind to the query + * @return the number of rows affected + * @throws org.springframework.dao.DataAccessException if there is any problem issuing the update + */ + int update(String sql, SqlParameterSource paramSource) throws DataAccessException; + + /** + * Issue an update via a prepared statement, binding the given arguments. + * @param sql SQL containing named parameters + * @param paramMap map of parameters to bind to the query + * (leaving it to the PreparedStatement to guess the corresponding SQL type) + * @return the number of rows affected + * @throws org.springframework.dao.DataAccessException if there is any problem issuing the update + */ + int update(String sql, Map paramMap) throws DataAccessException; + + /** + * Issue an update via a prepared statement, binding the given arguments, + * returning generated keys. + * @param sql SQL containing named parameters + * @param paramSource container of arguments and SQL types to bind to the query + * @param generatedKeyHolder KeyHolder that will hold the generated keys + * @return the number of rows affected + * @throws org.springframework.dao.DataAccessException if there is any problem issuing the update + * @see MapSqlParameterSource + * @see org.springframework.jdbc.support.GeneratedKeyHolder + */ + int update(String sql, SqlParameterSource paramSource, KeyHolder generatedKeyHolder) + throws DataAccessException; + + /** + * Issue an update via a prepared statement, binding the given arguments, + * returning generated keys. + * @param sql SQL containing named parameters + * @param paramSource container of arguments and SQL types to bind to the query + * @param generatedKeyHolder KeyHolder that will hold the generated keys + * @param keyColumnNames names of the columns that will have keys generated for them + * @return the number of rows affected + * @throws org.springframework.dao.DataAccessException if there is any problem issuing the update + * @see MapSqlParameterSource + * @see org.springframework.jdbc.support.GeneratedKeyHolder + */ + int update( + String sql, SqlParameterSource paramSource, KeyHolder generatedKeyHolder, String[] keyColumnNames) + throws DataAccessException; + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcTemplate.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcTemplate.java new file mode 100644 index 0000000000..bd93fce0d8 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcTemplate.java @@ -0,0 +1,278 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc.core.namedparam; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.sql.DataSource; + +import org.springframework.dao.DataAccessException; +import org.springframework.dao.support.DataAccessUtils; +import org.springframework.jdbc.core.ColumnMapRowMapper; +import org.springframework.jdbc.core.JdbcOperations; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.PreparedStatementCallback; +import org.springframework.jdbc.core.PreparedStatementCreator; +import org.springframework.jdbc.core.PreparedStatementCreatorFactory; +import org.springframework.jdbc.core.ResultSetExtractor; +import org.springframework.jdbc.core.RowCallbackHandler; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.SingleColumnRowMapper; +import org.springframework.jdbc.core.SqlRowSetResultSetExtractor; +import org.springframework.jdbc.support.KeyHolder; +import org.springframework.jdbc.support.rowset.SqlRowSet; +import org.springframework.util.Assert; + +/** + * Template class with a basic set of JDBC operations, allowing the use + * of named parameters rather than traditional '?' placeholders. + * + *

This class delegates to a wrapped {@link #getJdbcOperations() JdbcTemplate} + * once the substitution from named parameters to JDBC style '?' placeholders is + * done at execution time. It also allows for expanding a {@link java.util.List} + * of values to the appropriate number of placeholders. + * + *

The underlying {@link org.springframework.jdbc.core.JdbcTemplate} is + * exposed to allow for convenient access to the traditional + * {@link org.springframework.jdbc.core.JdbcTemplate} methods. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @since 2.0 + * @see NamedParameterJdbcOperations + * @see org.springframework.jdbc.core.JdbcTemplate + */ +public class NamedParameterJdbcTemplate implements NamedParameterJdbcOperations { + + /** The JdbcTemplate we are wrapping */ + private final JdbcOperations classicJdbcTemplate; + + /** Map of original SQL String to ParsedSql representation */ + private final Map parsedSqlCache = new HashMap(); + + + /** + * Create a new NamedParameterJdbcTemplate for the given {@link DataSource}. + *

Creates a classic Spring {@link org.springframework.jdbc.core.JdbcTemplate} and wraps it. + * @param dataSource the JDBC DataSource to access + */ + public NamedParameterJdbcTemplate(DataSource dataSource) { + Assert.notNull(dataSource, "The [dataSource] argument cannot be null."); + this.classicJdbcTemplate = new JdbcTemplate(dataSource); + } + + /** + * Create a new NamedParameterJdbcTemplate for the given classic + * Spring {@link org.springframework.jdbc.core.JdbcTemplate}. + * @param classicJdbcTemplate the classic Spring JdbcTemplate to wrap + */ + public NamedParameterJdbcTemplate(JdbcOperations classicJdbcTemplate) { + Assert.notNull(classicJdbcTemplate, "JdbcTemplate must not be null"); + this.classicJdbcTemplate = classicJdbcTemplate; + } + + + /** + * Expose the classic Spring JdbcTemplate to allow invocation of + * less commonly used methods. + */ + public JdbcOperations getJdbcOperations() { + return this.classicJdbcTemplate; + } + + + public Object execute(String sql, SqlParameterSource paramSource, PreparedStatementCallback action) + throws DataAccessException { + + return getJdbcOperations().execute(getPreparedStatementCreator(sql, paramSource), action); + } + + public Object execute(String sql, Map paramMap, PreparedStatementCallback action) throws DataAccessException { + return execute(sql, new MapSqlParameterSource(paramMap), action); + } + + public Object query(String sql, SqlParameterSource paramSource, ResultSetExtractor rse) + throws DataAccessException { + + return getJdbcOperations().query(getPreparedStatementCreator(sql, paramSource), rse); + } + + public Object query(String sql, Map paramMap, ResultSetExtractor rse) throws DataAccessException { + return query(sql, new MapSqlParameterSource(paramMap), rse); + } + + public void query(String sql, SqlParameterSource paramSource, RowCallbackHandler rch) + throws DataAccessException { + + getJdbcOperations().query(getPreparedStatementCreator(sql, paramSource), rch); + } + + public void query(String sql, Map paramMap, RowCallbackHandler rch) throws DataAccessException { + query(sql, new MapSqlParameterSource(paramMap), rch); + } + + public List query(String sql, SqlParameterSource paramSource, RowMapper rowMapper) + throws DataAccessException { + + return getJdbcOperations().query(getPreparedStatementCreator(sql, paramSource), rowMapper); + } + + public List query(String sql, Map paramMap, RowMapper rowMapper) throws DataAccessException { + return query(sql, new MapSqlParameterSource(paramMap), rowMapper); + } + + public Object queryForObject(String sql, SqlParameterSource paramSource, RowMapper rowMapper) + throws DataAccessException { + + List results = getJdbcOperations().query(getPreparedStatementCreator(sql, paramSource), rowMapper); + return DataAccessUtils.requiredSingleResult(results); + } + + public Object queryForObject(String sql, Map paramMap, RowMapper rowMapper) throws DataAccessException { + return queryForObject(sql, new MapSqlParameterSource(paramMap), rowMapper); + } + + public Object queryForObject(String sql, SqlParameterSource paramSource, Class requiredType) + throws DataAccessException { + + return queryForObject(sql, paramSource, new SingleColumnRowMapper(requiredType)); + } + + public Object queryForObject(String sql, Map paramMap, Class requiredType) throws DataAccessException { + return queryForObject(sql, paramMap, new SingleColumnRowMapper(requiredType)); + } + + public Map queryForMap(String sql, SqlParameterSource paramSource) throws DataAccessException { + return (Map) queryForObject(sql, paramSource, new ColumnMapRowMapper()); + } + + public Map queryForMap(String sql, Map paramMap) throws DataAccessException { + return (Map) queryForObject(sql, paramMap, new ColumnMapRowMapper()); + } + + public long queryForLong(String sql, SqlParameterSource paramSource) throws DataAccessException { + Number number = (Number) queryForObject(sql, paramSource, Number.class); + return (number != null ? number.longValue() : 0); + } + + public long queryForLong(String sql, Map paramMap) throws DataAccessException { + return queryForLong(sql, new MapSqlParameterSource(paramMap)); + } + + public int queryForInt(String sql, SqlParameterSource paramSource) throws DataAccessException { + Number number = (Number) queryForObject(sql, paramSource, Number.class); + return (number != null ? number.intValue() : 0); + } + + public int queryForInt(String sql, Map paramMap) throws DataAccessException { + return queryForInt(sql, new MapSqlParameterSource(paramMap)); + } + + public List queryForList(String sql, SqlParameterSource paramSource, Class elementType) + throws DataAccessException { + return query(sql, paramSource, new SingleColumnRowMapper(elementType)); + } + + public List queryForList(String sql, Map paramMap, Class elementType) throws DataAccessException { + return queryForList(sql, new MapSqlParameterSource(paramMap), elementType); + } + + public List queryForList(String sql, SqlParameterSource paramSource) throws DataAccessException { + return query(sql, paramSource, new ColumnMapRowMapper()); + } + + public List queryForList(String sql, Map paramMap) throws DataAccessException { + return queryForList(sql, new MapSqlParameterSource(paramMap)); + } + + public SqlRowSet queryForRowSet(String sql, SqlParameterSource paramSource) throws DataAccessException { + return (SqlRowSet) getJdbcOperations().query( + getPreparedStatementCreator(sql, paramSource), new SqlRowSetResultSetExtractor()); + } + + public SqlRowSet queryForRowSet(String sql, Map paramMap) throws DataAccessException { + return queryForRowSet(sql, new MapSqlParameterSource(paramMap)); + } + + public int update(String sql, SqlParameterSource paramSource) throws DataAccessException { + return getJdbcOperations().update(getPreparedStatementCreator(sql, paramSource)); + } + + public int update(String sql, Map paramMap) throws DataAccessException { + return update(sql, new MapSqlParameterSource(paramMap)); + } + + public int update(String sql, SqlParameterSource paramSource, KeyHolder generatedKeyHolder) + throws DataAccessException { + + return update(sql, paramSource, generatedKeyHolder, null); + } + + public int update( + String sql, SqlParameterSource paramSource, KeyHolder generatedKeyHolder, String[] keyColumnNames) + throws DataAccessException { + + ParsedSql parsedSql = getParsedSql(sql); + String sqlToUse = NamedParameterUtils.substituteNamedParameters(parsedSql, paramSource); + Object[] params = NamedParameterUtils.buildValueArray(parsedSql, paramSource, null); + int[] paramTypes = NamedParameterUtils.buildSqlTypeArray(parsedSql, paramSource); + PreparedStatementCreatorFactory pscf = new PreparedStatementCreatorFactory(sqlToUse, paramTypes); + if (keyColumnNames != null) { + pscf.setGeneratedKeysColumnNames(keyColumnNames); + } + else { + pscf.setReturnGeneratedKeys(true); + } + return getJdbcOperations().update(pscf.newPreparedStatementCreator(params), generatedKeyHolder); + } + + + /** + * Build a PreparedStatementCreator based on the given SQL and named parameters. + *

Note: Not used for the update variant with generated key handling. + * @param sql SQL to execute + * @param paramSource container of arguments to bind + * @return the corresponding PreparedStatementCreator + */ + protected PreparedStatementCreator getPreparedStatementCreator(String sql, SqlParameterSource paramSource) { + ParsedSql parsedSql = getParsedSql(sql); + String sqlToUse = NamedParameterUtils.substituteNamedParameters(parsedSql, paramSource); + Object[] params = NamedParameterUtils.buildValueArray(parsedSql, paramSource, null); + int[] paramTypes = NamedParameterUtils.buildSqlTypeArray(parsedSql, paramSource); + PreparedStatementCreatorFactory pscf = new PreparedStatementCreatorFactory(sqlToUse, paramTypes); + return pscf.newPreparedStatementCreator(params); + } + + /** + * Obtain a parsed representation of the given SQL statement. + * @param sql the original SQL + * @return a representation of the parsed SQL statement + */ + protected ParsedSql getParsedSql(String sql) { + synchronized (this.parsedSqlCache) { + ParsedSql parsedSql = (ParsedSql) this.parsedSqlCache.get(sql); + if (parsedSql == null) { + parsedSql = NamedParameterUtils.parseSqlStatement(sql); + this.parsedSqlCache.put(sql, parsedSql); + } + return parsedSql; + } + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterUtils.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterUtils.java new file mode 100644 index 0000000000..60b813963f --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterUtils.java @@ -0,0 +1,386 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.core.namedparam; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.jdbc.core.SqlParameter; +import org.springframework.jdbc.core.SqlParameterValue; +import org.springframework.util.Assert; + +/** + * Helper methods for named parameter parsing. + * Only intended for internal use within Spring's JDBC framework. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @since 2.0 + */ +public abstract class NamedParameterUtils { + + /** + * Set of characters that qualify as parameter separators, + * indicating that a parameter name in a SQL String has ended. + */ + private static final char[] PARAMETER_SEPARATORS = + new char[] {'"', '\'', ':', '&', ',', ';', '(', ')', '|', '=', '+', '-', '*', '%', '/', '\\', '<', '>', '^'}; + + /** + * Set of characters that qualify as comment or quotes starting characters. + */ + private static final String[] START_SKIP = + new String[] {"'", "\"", "--", "/*"}; + + /** + * Set of characters that at are the corresponding comment or quotes ending characters. + */ + private static final String[] STOP_SKIP = + new String[] {"'", "\"", "\n", "*/"}; + + + //------------------------------------------------------------------------- + // Core methods used by NamedParameterJdbcTemplate and SqlQuery/SqlUpdate + //------------------------------------------------------------------------- + + /** + * Parse the SQL statement and locate any placeholders or named parameters. + * Named parameters are substituted for a JDBC placeholder. + * @param sql the SQL statement + * @return the parsed statement, represented as ParsedSql instance + */ + public static ParsedSql parseSqlStatement(String sql) { + Assert.notNull(sql, "SQL must not be null"); + + Set namedParameters = new HashSet(); + ParsedSql parsedSql = new ParsedSql(sql); + + char[] statement = sql.toCharArray(); + int namedParameterCount = 0; + int unnamedParameterCount = 0; + int totalParameterCount = 0; + + int i = 0; + while (i < statement.length) { + int skipToPosition = skipCommentsAndQuotes(statement, i); + if (i != skipToPosition) { + if (skipToPosition >= statement.length) { + break; + } + i = skipToPosition; + } + char c = statement[i]; + if (c == ':' || c == '&') { + int j = i + 1; + if (j < statement.length && statement[j] == ':' && c == ':') { + // Postgres-style "::" casting operator - to be skipped. + i = i + 2; + continue; + } + while (j < statement.length && !isParameterSeparator(statement[j])) { + j++; + } + if (j - i > 1) { + String parameter = sql.substring(i + 1, j); + if (!namedParameters.contains(parameter)) { + namedParameters.add(parameter); + namedParameterCount++; + } + parsedSql.addNamedParameter(parameter, i, j); + totalParameterCount++; + } + i = j - 1; + } + else { + if (c == '?') { + unnamedParameterCount++; + totalParameterCount++; + } + } + i++; + } + parsedSql.setNamedParameterCount(namedParameterCount); + parsedSql.setUnnamedParameterCount(unnamedParameterCount); + parsedSql.setTotalParameterCount(totalParameterCount); + return parsedSql; + } + + /** + * Skip over comments and quoted names present in an SQL statement + * @param statement character array containing SQL statement + * @param position current position of statement + * @return next position to process after any comments or quotes are skipped + */ + private static int skipCommentsAndQuotes(char[] statement, int position) { + for (int i = 0; i < START_SKIP.length; i++) { + if (statement[position] == START_SKIP[i].charAt(0)) { + boolean match = true; + for (int j = 1; j < START_SKIP[i].length(); j++) { + if (!(statement[position + j] == START_SKIP[i].charAt(j))) { + match = false; + break; + } + } + if (match) { + int offset = START_SKIP[i].length(); + for (int m = position + offset; m < statement.length; m++) { + if (statement[m] == STOP_SKIP[i].charAt(0)) { + boolean endMatch = true; + int endPos = m; + for (int n = 1; n < STOP_SKIP[i].length(); n++) { + if (m + n >= statement.length) { + // last comment not closed properly + return statement.length; + } + if (!(statement[m + n] == STOP_SKIP[i].charAt(n))) { + endMatch = false; + break; + } + endPos = m + n; + } + if (endMatch) { + // found character sequence ending comment or quote + return endPos + 1; + } + } + } + // character sequence ending comment or quote not found + return statement.length; + } + + } + } + return position; + } + + /** + * Parse the SQL statement and locate any placeholders or named parameters. + * Named parameters are substituted for a JDBC placeholder and any select list + * is expanded to the required number of placeholders. Select lists may contain + * an array of objects and in that case the placeholders will be grouped and + * enclosed with parantheses. This allows for the use of "expression lists" in + * the SQL statement like:
+ * select id, name, state from table where (name, age) in (('John', 35), ('Ann', 50)) + *

The parameter values passed in are used to determine the number of + * placeholder to be used for a select list. Select lists should be limited + * to 100 or fewer elements. A larger number of elements is not guaramteed to + * be supported by the database and is strictly vendor-dependent. + * @param parsedSql the parsed represenation of the SQL statement + * @param paramSource the source for named parameters + * @return the SQL statement with substituted parameters + * @see #parseSqlStatement + */ + public static String substituteNamedParameters(ParsedSql parsedSql, SqlParameterSource paramSource) { + String originalSql = parsedSql.getOriginalSql(); + StringBuffer actualSql = new StringBuffer(); + List paramNames = parsedSql.getParameterNames(); + int lastIndex = 0; + for (int i = 0; i < paramNames.size(); i++) { + String paramName = (String) paramNames.get(i); + int[] indexes = parsedSql.getParameterIndexes(i); + int startIndex = indexes[0]; + int endIndex = indexes[1]; + actualSql.append(originalSql.substring(lastIndex, startIndex)); + if (paramSource != null && paramSource.hasValue(paramName)) { + Object value = paramSource.getValue(paramName); + if (value instanceof Collection) { + Iterator entryIter = ((Collection) value).iterator(); + int k = 0; + while (entryIter.hasNext()) { + if (k > 0) { + actualSql.append(", "); + } + k++; + Object entryItem = entryIter.next(); + if (entryItem instanceof Object[]) { + Object[] expressionList = (Object[]) entryItem; + actualSql.append("("); + for (int m = 0; m < expressionList.length; m++) { + if (m > 0) { + actualSql.append(", "); + } + actualSql.append("?"); + } + actualSql.append(")"); + } + else { + actualSql.append("?"); + } + } + } + else { + actualSql.append("?"); + } + } + else { + actualSql.append("?"); + } + lastIndex = endIndex; + } + actualSql.append(originalSql.substring(lastIndex, originalSql.length())); + return actualSql.toString(); + } + + /** + * Convert a Map of named parameter values to a corresponding array. + * @param parsedSql the parsed SQL statement + * @param paramSource the source for named parameters + * @param declaredParams the List of declared SqlParameter objects + * (may be null). If specified, the parameter metadata will + * be built into the value array in the form of SqlParameterValue objects. + * @return the array of values + */ + public static Object[] buildValueArray(ParsedSql parsedSql, SqlParameterSource paramSource, List declaredParams) { + Object[] paramArray = new Object[parsedSql.getTotalParameterCount()]; + if (parsedSql.getNamedParameterCount() > 0 && parsedSql.getUnnamedParameterCount() > 0) { + throw new InvalidDataAccessApiUsageException( + "You can't mix named and traditional ? placeholders. You have " + + parsedSql.getNamedParameterCount() + " named parameter(s) and " + + parsedSql.getUnnamedParameterCount() + " traditonal placeholder(s) in [" + + parsedSql.getOriginalSql() + "]"); + } + List paramNames = parsedSql.getParameterNames(); + for (int i = 0; i < paramNames.size(); i++) { + String paramName = (String) paramNames.get(i); + try { + Object value = paramSource.getValue(paramName); + SqlParameter param = findParameter(declaredParams, paramName, i); + paramArray[i] = (param != null ? new SqlParameterValue(param, value) : value); + } + catch (IllegalArgumentException ex) { + throw new InvalidDataAccessApiUsageException( + "No value supplied for the SQL parameter '" + paramName + "': " + ex.getMessage()); + } + } + return paramArray; + } + + /** + * Find a matching parameter in the given list of declared parameters. + * @param declaredParams the declared SqlParameter objects + * @param paramName the name of the desired parameter + * @param paramIndex the index of the desired parameter + * @return the declared SqlParameter, or null if none found + */ + private static SqlParameter findParameter(List declaredParams, String paramName, int paramIndex) { + if (declaredParams != null) { + // First pass: Look for named parameter match. + for (Iterator it = declaredParams.iterator(); it.hasNext();) { + SqlParameter declaredParam = (SqlParameter) it.next(); + if (paramName.equals(declaredParam.getName())) { + return declaredParam; + } + } + // Second pass: Look for parameter index match. + if (paramIndex < declaredParams.size()) { + SqlParameter declaredParam = (SqlParameter) declaredParams.get(paramIndex); + // Only accept unnamed parameters for index matches. + if (declaredParam.getName() == null) { + return declaredParam; + } + } + } + return null; + } + + /** + * Determine whether a parameter name ends at the current position, + * that is, whether the given character qualifies as a separator. + */ + private static boolean isParameterSeparator(char c) { + if (Character.isWhitespace(c)) { + return true; + } + for (int i = 0; i < PARAMETER_SEPARATORS.length; i++) { + if (c == PARAMETER_SEPARATORS[i]) { + return true; + } + } + return false; + } + + /** + * Convert a Map of parameter types to a corresponding int array. + * This is necessary in order to reuse existing methods on JdbcTemplate. + * Any named parameter types are placed in the correct position in the + * Object array based on the parsed SQL statement info. + * @param parsedSql the parsed SQL statement + * @param paramSource the source for named parameters + */ + public static int[] buildSqlTypeArray(ParsedSql parsedSql, SqlParameterSource paramSource) { + int[] sqlTypes = new int[parsedSql.getTotalParameterCount()]; + List paramNames = parsedSql.getParameterNames(); + for (int i = 0; i < paramNames.size(); i++) { + String paramName = (String) paramNames.get(i); + sqlTypes[i] = paramSource.getSqlType(paramName); + } + return sqlTypes; + } + + + //------------------------------------------------------------------------- + // Convenience methods operating on a plain SQL String + //------------------------------------------------------------------------- + + /** + * Parse the SQL statement and locate any placeholders or named parameters. + * Named parameters are substituted for a JDBC placeholder. + *

This is a shortcut version of + * {@link #parseSqlStatement(String)} in combination with + * {@link #substituteNamedParameters(ParsedSql, SqlParameterSource)}. + * @param sql the SQL statement + * @return the actual (parsed) SQL statement + */ + public static String parseSqlStatementIntoString(String sql) { + ParsedSql parsedSql = parseSqlStatement(sql); + return substituteNamedParameters(parsedSql, null); + } + + /** + * Parse the SQL statement and locate any placeholders or named parameters. + * Named parameters are substituted for a JDBC placeholder and any select list + * is expanded to the required number of placeholders. + *

This is a shortcut version of + * {@link #substituteNamedParameters(ParsedSql, SqlParameterSource)}. + * @param sql the SQL statement + * @param paramSource the source for named parameters + * @return the SQL statement with substituted parameters + */ + public static String substituteNamedParameters(String sql, SqlParameterSource paramSource) { + ParsedSql parsedSql = parseSqlStatement(sql); + return substituteNamedParameters(parsedSql, paramSource); + } + + /** + * Convert a Map of named parameter values to a corresponding array. + *

This is a shortcut version of + * {@link #buildValueArray(ParsedSql, SqlParameterSource, java.util.List)}. + * @param sql the SQL statement + * @param paramMap the Map of parameters + * @return the array of values + */ + public static Object[] buildValueArray(String sql, Map paramMap) { + ParsedSql parsedSql = parseSqlStatement(sql); + return buildValueArray(parsedSql, new MapSqlParameterSource(paramMap), null); + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/namedparam/ParsedSql.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/namedparam/ParsedSql.java new file mode 100644 index 0000000000..641875e90c --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/namedparam/ParsedSql.java @@ -0,0 +1,144 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc.core.namedparam; + +import java.util.ArrayList; +import java.util.List; + +/** + * Holds information about a parsed SQL statement. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @since 2.0 + */ +public class ParsedSql { + + private String originalSql; + + private List parameterNames = new ArrayList(); + + private List parameterIndexes = new ArrayList(); + + private int namedParameterCount; + + private int unnamedParameterCount; + + private int totalParameterCount; + + + /** + * Create a new instance of the {@link ParsedSql} class. + * @param originalSql the SQL statement that is being (or is to be) parsed + */ + ParsedSql(String originalSql) { + this.originalSql = originalSql; + } + + /** + * Return the SQL statement that is being parsed. + */ + String getOriginalSql() { + return this.originalSql; + } + + + /** + * Add a named parameter parsed from this SQL statement. + * @param parameterName the name of the parameter + * @param startIndex the start index in the original SQL String + * @param endIndex the end index in the original SQL String + */ + void addNamedParameter(String parameterName, int startIndex, int endIndex) { + this.parameterNames.add(parameterName); + this.parameterIndexes.add(new int[] {startIndex, endIndex}); + } + + /** + * Return all of the parameters (bind variables) in the parsed SQL statement. + * Repeated occurences of the same parameter name are included here. + */ + List getParameterNames() { + return this.parameterNames; + } + + /** + * Return the parameter indexes for the specified parameter. + * @param parameterPosition the position of the parameter + * (as index in the parameter names List) + * @return the start index and end index, combined into + * a int array of length 2 + */ + int[] getParameterIndexes(int parameterPosition) { + return (int[]) this.parameterIndexes.get(parameterPosition); + } + + /** + * Set the count of named parameters in the SQL statement. + * Each parameter name counts once; repeated occurences do not count here. + */ + void setNamedParameterCount(int namedParameterCount) { + this.namedParameterCount = namedParameterCount; + } + + /** + * Return the count of named parameters in the SQL statement. + * Each parameter name counts once; repeated occurences do not count here. + */ + int getNamedParameterCount() { + return this.namedParameterCount; + } + + /** + * Set the count of all of the unnamed parameters in the SQL statement. + */ + void setUnnamedParameterCount(int unnamedParameterCount) { + this.unnamedParameterCount = unnamedParameterCount; + } + + /** + * Return the count of all of the unnamed parameters in the SQL statement. + */ + int getUnnamedParameterCount() { + return this.unnamedParameterCount; + } + + /** + * Set the total count of all of the parameters in the SQL statement. + * Repeated occurences of the same parameter name do count here. + */ + void setTotalParameterCount(int totalParameterCount) { + this.totalParameterCount = totalParameterCount; + } + + /** + * Return the total count of all of the parameters in the SQL statement. + * Repeated occurences of the same parameter name do count here. + */ + int getTotalParameterCount() { + return this.totalParameterCount; + } + + + /** + * Exposes the original SQL String. + */ + public String toString() { + return this.originalSql; + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/namedparam/SqlParameterSource.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/namedparam/SqlParameterSource.java new file mode 100644 index 0000000000..5d984f6aff --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/namedparam/SqlParameterSource.java @@ -0,0 +1,84 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc.core.namedparam; + +import org.springframework.jdbc.support.JdbcUtils; + +/** + * Interface that defines common functionality for objects that can + * offer parameter values for named SQL parameters, serving as argument + * for {@link NamedParameterJdbcTemplate} operations. + * + *

This interface allows for the specification of SQL type in addition + * to parameter values. All parameter values and types are identified by + * specifying the name of the parameter. + * + *

Intended to wrap various implementations like a Map or a JavaBean + * with a consistent interface. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @since 2.0 + * @see NamedParameterJdbcOperations + * @see NamedParameterJdbcTemplate + * @see MapSqlParameterSource + * @see BeanPropertySqlParameterSource + */ +public interface SqlParameterSource { + + /** + * Constant that indicates an unknown (or unspecified) SQL type. + * To be returned from getType when no specific SQL type known. + * @see #getSqlType + * @see java.sql.Types + */ + int TYPE_UNKNOWN = JdbcUtils.TYPE_UNKNOWN; + + + /** + * Determine whether there is a value for the specified named parameter. + * @param paramName the name of the parameter + * @return whether there is a value defined + */ + boolean hasValue(String paramName); + + /** + * Return the parameter value for the requested named parameter. + * @param paramName the name of the parameter + * @return the value of the specified parameter + * @throws IllegalArgumentException if there is no value for the requested parameter + */ + Object getValue(String paramName) throws IllegalArgumentException; + + /** + * Determine the SQL type for the specified named parameter. + * @param paramName the name of the parameter + * @return the SQL type of the specified parameter, + * or TYPE_UNKNOWN if not known + * @see #TYPE_UNKNOWN + */ + int getSqlType(String paramName); + + /** + * Determine the type ane for the specified named parameter. + * @param paramName the name of the parameter + * @return the type name of the specified parameter, + * or null if not known + */ + String getTypeName(String paramName); + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/namedparam/SqlParameterSourceUtils.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/namedparam/SqlParameterSourceUtils.java new file mode 100644 index 0000000000..f89e2a62e6 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/namedparam/SqlParameterSourceUtils.java @@ -0,0 +1,110 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc.core.namedparam; + +import java.util.HashMap; +import java.util.Map; +import java.util.Iterator; + +import org.springframework.jdbc.core.SqlParameterValue; + +/** + * Class that provides helper methods for the use of {@link SqlParameterSource} + * with SimpleJdbc classes. + * + * @author Thomas Risberg + * @since 2.5 + * @see org.springframework.jdbc.core.simple.SimpleJdbcTemplate + */ +public class SqlParameterSourceUtils { + + /** + * Create an array of MapSqlParameterSource objects populated with data from the + * values passed in. This will define what is included in a batch operation. + * @param valueMaps array of Maps containing the values to be used + * @return an array of SqlParameterSource + */ + public static SqlParameterSource[] createBatch(Map[] valueMaps) { + MapSqlParameterSource[] batch = new MapSqlParameterSource[valueMaps.length]; + for (int i = 0; i < valueMaps.length; i++) { + Map valueMap = valueMaps[i]; + batch[i] = new MapSqlParameterSource(valueMap); + } + return batch; + } + + /** + * Create an array of BeanPropertySqlParameterSource objects populated with data + * from the values passed in. This will define what is included in a batch operation. + * @param beans object array of beans containing the values to be used + * @return an array of SqlParameterSource + */ + public static SqlParameterSource[] createBatch(Object[] beans) { + BeanPropertySqlParameterSource[] batch = new BeanPropertySqlParameterSource[beans.length]; + for (int i = 0; i < beans.length; i++) { + Object bean = beans[i]; + batch[i] = new BeanPropertySqlParameterSource(bean); + } + return batch; + } + + /** + * Create a wrapped value if parameter has type information, plain object if not. + * @param source the source of paramer values and type information + * @param parameterName the name of the parameter + * @return the value object + */ + public static Object getTypedValue(SqlParameterSource source, String parameterName) { + int sqlType = source.getSqlType(parameterName); + if (sqlType != SqlParameterSource.TYPE_UNKNOWN) { + if (source.getTypeName(parameterName) != null) { + return new SqlParameterValue(sqlType, source.getTypeName(parameterName), source.getValue(parameterName)); + } + else { + return new SqlParameterValue(sqlType, source.getValue(parameterName)); + } + } + else { + return source.getValue(parameterName); + } + } + +/** + * Create a Map of case insensitive parameter names together with the original name. + * @param parameterSource the source of paramer names + * @return the Map that can be used for case insensitive matching of parameter names + */ + public static Map extractCaseInsensitiveParameterNames(SqlParameterSource parameterSource) { + Map caseInsensitiveParameterNames = new HashMap(); + if (parameterSource instanceof BeanPropertySqlParameterSource) { + String[] propertyNames = ((BeanPropertySqlParameterSource)parameterSource).getReadablePropertyNames(); + for (int i = 0; i < propertyNames.length; i++) { + String name = propertyNames[i]; + caseInsensitiveParameterNames.put(name.toLowerCase(), name); + } + } + else if (parameterSource instanceof MapSqlParameterSource) { + for (Iterator it = ((MapSqlParameterSource) parameterSource).getValues().keySet().iterator(); it.hasNext();) + { + String name = (String) it.next(); + caseInsensitiveParameterNames.put(name.toLowerCase(), name); + } + } + return caseInsensitiveParameterNames; + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/namedparam/package.html b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/namedparam/package.html new file mode 100644 index 0000000000..25eb2221ba --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/namedparam/package.html @@ -0,0 +1,16 @@ + + + +JdbcTemplate variant with named parameter support. + +

NamedParameterJdbcTemplate is a wrapper around JdbcTemplate that adds +support for named parameter parsing. It does not implement the JdbcOperations +interface or extend JdbcTemplate, but implements the dedicated +NamedParameterJdbcOperations interface. + +

If you need the full power of Spring JDBC for less common operations, use +the getJdbcOperations() method of NamedParameterJdbcTemplate and +work with the returned classic template, or use a JdbcTemplate instance directly. + + + diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/package.html b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/package.html new file mode 100644 index 0000000000..3c178a6257 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/package.html @@ -0,0 +1,8 @@ + + + +Provides the core JDBC framework, based on JdbcTemplate +and its associated callback interfaces and helper objects. + + + diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/simple/AbstractJdbcCall.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/simple/AbstractJdbcCall.java new file mode 100644 index 0000000000..0a1c4c129f --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/simple/AbstractJdbcCall.java @@ -0,0 +1,407 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.core.simple; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.sql.DataSource; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.jdbc.core.CallableStatementCreator; +import org.springframework.jdbc.core.CallableStatementCreatorFactory; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.SqlParameter; +import org.springframework.jdbc.core.metadata.CallMetaDataContext; +import org.springframework.jdbc.core.namedparam.SqlParameterSource; +import org.springframework.util.StringUtils; + +/** + * Abstract class to provide base functionality for easy stored procedure calls + * based on configuration options and database metadata. + * This class provides the base SPI for {@link SimpleJdbcCall}. + * + * @author Thomas Risberg + * @since 2.5 + */ +public abstract class AbstractJdbcCall { + + /** Logger available to subclasses */ + protected final Log logger = LogFactory.getLog(getClass()); + + /** Lower-level class used to execute SQL */ + private JdbcTemplate jdbcTemplate = new JdbcTemplate(); + + /** List of SqlParameter objects */ + private final List declaredParameters = new ArrayList(); + + /** List of RefCursor/ResultSet RowMapper objects */ + private final Map declaredRowMappers = new LinkedHashMap(); + + /** + * Has this operation been compiled? Compilation means at + * least checking that a DataSource and sql have been provided, + * but subclasses may also implement their own custom validation. + */ + private boolean compiled = false; + + /** the generated string used for call statement */ + private String callString; + + /** context used to retrieve and manage database metadata */ + private CallMetaDataContext callMetaDataContext = new CallMetaDataContext(); + + /** + * Object enabling us to create CallableStatementCreators + * efficiently, based on this class's declared parameters. + */ + private CallableStatementCreatorFactory callableStatementFactory; + + + /** + * Constructor to be used when initializing using a {@link DataSource}. + * @param dataSource the DataSource to be used + */ + protected AbstractJdbcCall(DataSource dataSource) { + this.jdbcTemplate = new JdbcTemplate(dataSource); + } + + /** + * Constructor to be used when initializing using a {@link JdbcTemplate}. + * @param jdbcTemplate the JdbcTemplate to use + */ + protected AbstractJdbcCall(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + + /** + * Get the configured {@link JdbcTemplate} + */ + public JdbcTemplate getJdbcTemplate() { + return this.jdbcTemplate; + } + + /** + * Get the {@link CallableStatementCreatorFactory} being used + */ + protected CallableStatementCreatorFactory getCallableStatementFactory() { + return this.callableStatementFactory; + } + + + /** + * Set the name of the stored procedure. + */ + public void setProcedureName(String procedureName) { + this.callMetaDataContext.setProcedureName(procedureName); + } + + /** + * Get the name of the stored procedure. + */ + public String getProcedureName() { + return this.callMetaDataContext.getProcedureName(); + } + + /** + * Set the names of in parameters to be used. + */ + public void setInParameterNames(Set inParameterNames) { + this.callMetaDataContext.setLimitedInParameterNames(inParameterNames); + } + + /** + * Get the names of in parameters to be used. + */ + public Set getInParameterNames() { + return this.callMetaDataContext.getLimitedInParameterNames(); + } + + /** + * Set the catalog name to use. + */ + public void setCatalogName(String catalogName) { + this.callMetaDataContext.setCatalogName(catalogName); + } + + /** + * Get the catalog name used. + */ + public String getCatalogName() { + return this.callMetaDataContext.getCatalogName(); + } + + /** + * Set the schema name to use, + */ + public void setSchemaName(String schemaName) { + this.callMetaDataContext.setSchemaName(schemaName); + } + + /** + * Get the schema name used. + */ + public String getSchemaName() { + return this.callMetaDataContext.getSchemaName(); + } + + /** + * Specify whether this call is a function call. + */ + public void setFunction(boolean function) { + this.callMetaDataContext.setFunction(function); + } + + /** + * Is this call a function call? + */ + public boolean isFunction() { + return this.callMetaDataContext.isFunction(); + } + + /** + * Specify whether the call requires a rerurn value. + */ + public void setReturnValueRequired(boolean b) { + this.callMetaDataContext.setReturnValueRequired(b); + } + + /** + * Does the call require a return value? + */ + public boolean isReturnValueRequired() { + return this.callMetaDataContext.isReturnValueRequired(); + } + + /** + * Add a declared parameter to the list of parameters for the call. + * Only parameters declared as SqlParameter and SqlInOutParameter + * will be used to provide input values. This is different from the StoredProcedure class + * which for backwards compatibility reasons allows input values to be provided for parameters declared + * as SqlOutParameter. + * @param parameter the {@link SqlParameter} to add + */ + public void addDeclaredParameter(SqlParameter parameter) { + if (!StringUtils.hasText(parameter.getName())) { + throw new InvalidDataAccessApiUsageException("You must specify a parameter name when declaring parameters for \"" + getProcedureName() + "\""); + } + this.declaredParameters.add(parameter); + if (logger.isDebugEnabled()) { + logger.debug("Added declared parameter for [" + getProcedureName() + "]: " + parameter.getName()); + } + } + + /** + * Add a {@link org.springframework.jdbc.core.RowMapper} for the specified parameter or column + * @param parameterName name of parameter or column + * @param rowMapper the RowMapper implementation to use + */ + public void addDeclaredRowMapper(String parameterName, ParameterizedRowMapper rowMapper) { + this.declaredRowMappers.put(parameterName, rowMapper); + if (logger.isDebugEnabled()) { + logger.debug("Added row mapper for [" + getProcedureName() + "]: " + parameterName); + } + } + + /** + * Get the call string that should be used based on parameters and meta data + */ + public String getCallString() { + return this.callString; + } + + /** + * Specify whether the parameter metadata for the call should be used. The default is true. + */ + public void setAccessCallParameterMetaData(boolean accessCallParameterMetaData) { + this.callMetaDataContext.setAccessCallParameterMetaData(accessCallParameterMetaData); + } + + + //------------------------------------------------------------------------- + // Methods handling compilation issues + //------------------------------------------------------------------------- + + /** + * Compile this JdbcCall using provided parameters and meta data plus other settings. This + * finalizes the configuration for this object and subsequent attempts to compile are ignored. + * This will be implicitly called the first time an un-compiled call is executed. + * @throws org.springframework.dao.InvalidDataAccessApiUsageException if the object hasn't + * been correctly initialized, for example if no DataSource has been provided + */ + public final void compile() throws InvalidDataAccessApiUsageException { + if (!isCompiled()) { + if (getProcedureName() == null) { + throw new InvalidDataAccessApiUsageException("Procedure or Function name is required"); + } + + try { + this.jdbcTemplate.afterPropertiesSet(); + } + catch (IllegalArgumentException ex) { + throw new InvalidDataAccessApiUsageException(ex.getMessage()); + } + + compileInternal(); + this.compiled = true; + + if (logger.isDebugEnabled()) { + logger.debug("SqlCall for " + (isFunction() ? "function" : "procedure") + " [" + getProcedureName() + "] compiled"); + } + } + } + + /** + * Method to perform the actual compilation. Subclasses can override this template method to perform + * their own compilation. Invoked after this base class's compilation is complete. + */ + protected void compileInternal() { + this.callMetaDataContext.initializeMetaData(getJdbcTemplate().getDataSource()); + + // iterate over the declared RowMappers and register the corresponding SqlParameter + for (Map.Entry entry : this.declaredRowMappers.entrySet()) { + SqlParameter resultSetParameter = + this.callMetaDataContext.createReturnResultSetParameter(entry.getKey(), entry.getValue()); + this.declaredParameters.add(resultSetParameter); + } + callMetaDataContext.processParameters(this.declaredParameters); + + this.callString = this.callMetaDataContext.createCallString(); + if (logger.isDebugEnabled()) { + logger.debug("Compiled stored procedure. Call string is [" + this.callString + "]"); + } + + this.callableStatementFactory = + new CallableStatementCreatorFactory(getCallString(), this.callMetaDataContext.getCallParameters()); + this.callableStatementFactory.setNativeJdbcExtractor(getJdbcTemplate().getNativeJdbcExtractor()); + + onCompileInternal(); + } + + /** + * Hook method that subclasses may override to react to compilation. + * This implementation does nothing. + */ + protected void onCompileInternal() { + } + + /** + * Is this operation "compiled"? + * @return whether this operation is compiled, and ready to use. + */ + public boolean isCompiled() { + return this.compiled; + } + + /** + * Check whether this operation has been compiled already; + * lazily compile it if not already compiled. + *

Automatically called by doExecute. + */ + protected void checkCompiled() { + if (!isCompiled()) { + logger.debug("JdbcCall call not compiled before execution - invoking compile"); + compile(); + } + } + + + //------------------------------------------------------------------------- + // Methods handling execution + //------------------------------------------------------------------------- + + /** + * Method that provides execution of the call using the passed in {@link SqlParameterSource} + * @param parameterSource parameter names and values to be used in call + * @return Map of out parameters + */ + protected Map doExecute(SqlParameterSource parameterSource) { + checkCompiled(); + Map params = matchInParameterValuesWithCallParameters(parameterSource); + return executeCallInternal(params); + } + + /** + * Method that provides execution of the call using the passed in Map of parameters + * @param args Map of parameter name and values + * @return Map of out parameters + */ + protected Map doExecute(Map args) { + checkCompiled(); + Map params = matchInParameterValuesWithCallParameters(args); + return executeCallInternal(params); + } + + /** + * Method to perform the actual call processing + */ + private Map executeCallInternal(Map params) { + CallableStatementCreator csc = getCallableStatementFactory().newCallableStatementCreator(params); + if (logger.isDebugEnabled()) { + logger.debug("The following parameters are used for call " + getCallString() + " with: " + params); + int i = 1; + for (SqlParameter p : getCallParameters()) { + logger.debug(i++ + ": " + p.getName() + " SQL Type "+ p.getSqlType() + " Type Name " + p.getTypeName() + " " + p.getClass().getName()); + } + } + return getJdbcTemplate().call(csc, getCallParameters()); + } + + /** + * Get the name of a single out parameter or return value. + * Used for functions or procedures with one out parameter. + */ + protected String getScalarOutParameterName() { + return this.callMetaDataContext.getScalarOutParameterName(); + } + + /** + * Match the provided in parameter values with registered parameters and + * parameters defined via metadata processing. + * @param parameterSource the parameter vakues provided as a {@link SqlParameterSource} + * @return Map with parameter names and values + */ + protected Map matchInParameterValuesWithCallParameters(SqlParameterSource parameterSource) { + return this.callMetaDataContext.matchInParameterValuesWithCallParameters(parameterSource); + } + + /** + * Match the provided in parameter values with registered parameters and + * parameters defined via metadata processing. + * @param args the parameter values provided in a Map + * @return Map with parameter names and values + */ + protected Map matchInParameterValuesWithCallParameters(Map args) { + return this.callMetaDataContext.matchInParameterValuesWithCallParameters(args); + } + + /** + * Get a List of all the call parameters to be used for call. This includes any parameters added + * based on meta data processing. + */ + protected List getCallParameters() { + return this.callMetaDataContext.getCallParameters(); + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/simple/AbstractJdbcInsert.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/simple/AbstractJdbcInsert.java new file mode 100644 index 0000000000..c6d9323068 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/simple/AbstractJdbcInsert.java @@ -0,0 +1,625 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc.core.simple; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Statement; +import java.sql.ResultSet; +import java.util.Arrays; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.sql.DataSource; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.dao.DataAccessException; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.dao.InvalidDataAccessResourceUsageException; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.jdbc.core.BatchPreparedStatementSetter; +import org.springframework.jdbc.core.ConnectionCallback; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.PreparedStatementCreator; +import org.springframework.jdbc.core.SqlTypeValue; +import org.springframework.jdbc.core.StatementCreatorUtils; +import org.springframework.jdbc.core.metadata.TableMetaDataContext; +import org.springframework.jdbc.core.namedparam.SqlParameterSource; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.jdbc.support.JdbcUtils; +import org.springframework.jdbc.support.KeyHolder; +import org.springframework.util.Assert; + +/** + * Abstract class to provide base functionality for easy inserts + * based on configuration options and database metadata. + * This class provides the base SPI for {@link SimpleJdbcInsert}. + * + * @author Thomas Risberg + * @since 2.5 + */ +public abstract class AbstractJdbcInsert { + + /** Logger available to subclasses */ + protected final Log logger = LogFactory.getLog(getClass()); + + /** Lower-level class used to execute SQL */ + private JdbcTemplate jdbcTemplate = new JdbcTemplate(); + + /** List of columns objects to be used in insert statement */ + private List declaredColumns = new ArrayList(); + + /** + * Has this operation been compiled? Compilation means at + * least checking that a DataSource or JdbcTemplate has been provided, + * but subclasses may also implement their own custom validation. + */ + private boolean compiled = false; + + /** the generated string used for insert statement */ + private String insertString; + + /** the SQL Type information for the insert columns */ + private int[] insertTypes; + + /** the names of the columns holding the generated key */ + private String[] generatedKeyNames = new String[] {}; + + /** context used to retrieve and manage database metadata */ + private TableMetaDataContext tableMetaDataContext = new TableMetaDataContext(); + + + /** + * Constructor for sublasses to delegate to for setting the DataSource. + */ + protected AbstractJdbcInsert(DataSource dataSource) { + jdbcTemplate = new JdbcTemplate(dataSource); + } + + /** + * Constructor for sublasses to delegate to for setting the JdbcTemplate. + */ + protected AbstractJdbcInsert(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + + //------------------------------------------------------------------------- + // Methods dealing with configuaration properties + //------------------------------------------------------------------------- + + /** + * Get the name of the table for this insert + */ + public String getTableName() { + return tableMetaDataContext.getTableName(); + } + + /** + * Set the name of the table for this insert + */ + public void setTableName(String tableName) { + checkIfConfigurationModificationIsAllowed(); + tableMetaDataContext.setTableName(tableName); + } + + /** + * Get the name of the schema for this insert + */ + public String getSchemaName() { + return tableMetaDataContext.getSchemaName(); + } + + /** + * Set the name of the schema for this insert + */ + public void setSchemaName(String schemaName) { + checkIfConfigurationModificationIsAllowed(); + tableMetaDataContext.setSchemaName(schemaName); + } + + /** + * Get the name of the catalog for this insert + */ + public String getCatalogName() { + return tableMetaDataContext.getCatalogName(); + } + + /** + * Set the name of the catalog for this insert + */ + public void setCatalogName(String catalogName) { + checkIfConfigurationModificationIsAllowed(); + tableMetaDataContext.setCatalogName(catalogName); + } + + /** + * Set the names of the columns to be used + */ + public void setColumnNames(List columnNames) { + checkIfConfigurationModificationIsAllowed(); + declaredColumns.clear(); + declaredColumns.addAll(columnNames); + } + + /** + * Get the names of the columns used + */ + public List getColumnNames() { + return Collections.unmodifiableList(declaredColumns); + } + + /** + * Get the names of any generated keys + */ + public String[] getGeneratedKeyNames() { + return generatedKeyNames; + } + + /** + * Set the names of any generated keys + */ + public void setGeneratedKeyNames(String[] generatedKeyNames) { + checkIfConfigurationModificationIsAllowed(); + this.generatedKeyNames = generatedKeyNames; + } + + /** + * Specify the name of a single generated key column + */ + public void setGeneratedKeyName(String generatedKeyName) { + checkIfConfigurationModificationIsAllowed(); + this.generatedKeyNames = new String[] {generatedKeyName}; + } + + /** + * Get the insert string to be used + */ + public String getInsertString() { + return insertString; + } + + /** + * Get the array of {@link java.sql.Types} to be used for insert + */ + public int[] getInsertTypes() { + return insertTypes; + } + + /** + * Get the {@link JdbcTemplate} that is configured to be used + */ + protected JdbcTemplate getJdbcTemplate() { + return jdbcTemplate; + } + + + //------------------------------------------------------------------------- + // Methods handling compilation issues + //------------------------------------------------------------------------- + + /** + * Compile this JdbcInsert using provided parameters and meta data plus other settings. This + * finalizes the configuration for this object and subsequent attempts to compile are ignored. + * This will be implicitly called the first time an un-compiled insert is executed. + * @throws org.springframework.dao.InvalidDataAccessApiUsageException if the object hasn't + * been correctly initialized, for example if no DataSource has been provided + */ + public final void compile() throws InvalidDataAccessApiUsageException { + if (!isCompiled()) { + if (getTableName() == null) { + throw new InvalidDataAccessApiUsageException("Table name is required"); + } + + try { + this.jdbcTemplate.afterPropertiesSet(); + } + catch (IllegalArgumentException ex) { + throw new InvalidDataAccessApiUsageException(ex.getMessage()); + } + + compileInternal(); + this.compiled = true; + + if (logger.isDebugEnabled()) { + logger.debug("JdbcInsert for table [" + getTableName() + "] compiled"); + } + } + } + + /** + * Method to perform the actual compilation. Subclasses can override this template method to perform + * their own compilation. Invoked after this base class's compilation is complete. + */ + protected void compileInternal() { + + tableMetaDataContext.processMetaData(getJdbcTemplate().getDataSource(), getColumnNames(), getGeneratedKeyNames()); + + insertString = tableMetaDataContext.createInsertString(getGeneratedKeyNames()); + + insertTypes = tableMetaDataContext.createInsertTypes(); + + if (logger.isDebugEnabled()) { + logger.debug("Compiled JdbcInsert. Insert string is [" + getInsertString() + "]"); + } + + onCompileInternal(); + } + + /** + * Hook method that subclasses may override to react to compilation. + * This implementation does nothing. + */ + protected void onCompileInternal() { + } + + /** + * Is this operation "compiled"? + * @return whether this operation is compiled, and ready to use. + */ + public boolean isCompiled() { + return this.compiled; + } + + /** + * Check whether this operation has been compiled already; + * lazily compile it if not already compiled. + *

Automatically called by validateParameters. + */ + protected void checkCompiled() { + if (!isCompiled()) { + logger.debug("JdbcInsert not compiled before execution - invoking compile"); + compile(); + } + } + + /** + * Method to check whether we are allowd to make any configuration changes at this time. If the class has been + * compiled, then no further changes to the configuration are allowed. + */ + protected void checkIfConfigurationModificationIsAllowed() { + if (isCompiled()) { + throw new InvalidDataAccessApiUsageException("Configuration can't be altered once the class has been compiled or used."); + } + } + + + //------------------------------------------------------------------------- + // Methods handling execution + //------------------------------------------------------------------------- + + /** + * Method that provides execution of the insert using the passed in Map of parameters + * + * @param args Map with parameter names and values to be used in insert + * @return number of rows affected + */ + protected int doExecute(Map args) { + checkCompiled(); + List values = matchInParameterValuesWithInsertColumns(args); + return executeInsertInternal(values); + } + + /** + * Method that provides execution of the insert using the passed in {@link SqlParameterSource} + * + * @param parameterSource parameter names and values to be used in insert + * @return number of rows affected + */ + protected int doExecute(SqlParameterSource parameterSource) { + checkCompiled(); + List values = matchInParameterValuesWithInsertColumns(parameterSource); + return executeInsertInternal(values); + } + + /** + * Method to execute the insert + */ + private int executeInsertInternal(List values) { + if (logger.isDebugEnabled()) { + logger.debug("The following parameters are used for insert " + getInsertString() + " with: " + values); + } + int updateCount = jdbcTemplate.update(getInsertString(), values.toArray()); + return updateCount; + } + + /** + * Method that provides execution of the insert using the passed in Map of parameters + * and returning a generated key + * + * @param args Map with parameter names and values to be used in insert + * @return the key generated by the insert + */ + protected Number doExecuteAndReturnKey(Map args) { + checkCompiled(); + List values = matchInParameterValuesWithInsertColumns(args); + return executeInsertAndReturnKeyInternal(values); + } + + /** + * Method that provides execution of the insert using the passed in {@link SqlParameterSource} + * and returning a generated key + * + * @param parameterSource parameter names and values to be used in insert + * @return the key generated by the insert + */ + protected Number doExecuteAndReturnKey(SqlParameterSource parameterSource) { + checkCompiled(); + List values = matchInParameterValuesWithInsertColumns(parameterSource); + return executeInsertAndReturnKeyInternal(values); + } + + /** + * Method that provides execution of the insert using the passed in Map of parameters + * and returning all generated keys + * + * @param args Map with parameter names and values to be used in insert + * @return the KeyHolder containing keys generated by the insert + */ + protected KeyHolder doExecuteAndReturnKeyHolder(Map args) { + checkCompiled(); + List values = matchInParameterValuesWithInsertColumns(args); + return executeInsertAndReturnKeyHolderInternal(values); + } + + /** + * Method that provides execution of the insert using the passed in {@link SqlParameterSource} + * and returning all generated keys + * + * @param parameterSource parameter names and values to be used in insert + * @return the KeyHolder containing keys generated by the insert + */ + protected KeyHolder doExecuteAndReturnKeyHolder(SqlParameterSource parameterSource) { + checkCompiled(); + List values = matchInParameterValuesWithInsertColumns(parameterSource); + return executeInsertAndReturnKeyHolderInternal(values); + } + + /** + * Method to execute the insert generating single key + */ + private Number executeInsertAndReturnKeyInternal(final List values) { + KeyHolder kh = executeInsertAndReturnKeyHolderInternal(values); + if (kh != null && kh.getKey() != null) { + return kh.getKey(); + } + else { + throw new DataIntegrityViolationException("Unable to retreive the generated key for the insert: " + + getInsertString()); + } + } + + /** + * Method to execute the insert generating any number of keys + */ + private KeyHolder executeInsertAndReturnKeyHolderInternal(final List values) { + if (logger.isDebugEnabled()) { + logger.debug("The following parameters are used for call " + getInsertString() + " with: " + values); + } + final KeyHolder keyHolder = new GeneratedKeyHolder(); + if (this.tableMetaDataContext.isGetGeneratedKeysSupported()) { + jdbcTemplate.update( + new PreparedStatementCreator() { + public PreparedStatement createPreparedStatement(Connection con) throws SQLException { + PreparedStatement ps = prepareStatementForGeneratedKeys(con); + setParameterValues(ps, values, null); + return ps; + } + }, + keyHolder); + } + else { + if (!this.tableMetaDataContext.isGetGeneratedKeysSimulated()) { + throw new InvalidDataAccessResourceUsageException( + "The getGeneratedKeys feature is not supported by this database"); + } + if (getGeneratedKeyNames().length < 1) { + throw new InvalidDataAccessApiUsageException("Generated Key Name(s) not specificed. " + + "Using the generated keys features requires specifying the name(s) of the generated column(s)"); + } + if (getGeneratedKeyNames().length > 1) { + throw new InvalidDataAccessApiUsageException( + "Current database only supports retreiving the key for a single column. There are " + + getGeneratedKeyNames().length + " columns specified: " + Arrays.asList(getGeneratedKeyNames())); + } + // This is a hack to be able to get the generated key from a database that doesn't support + // get generated keys feature. HSQL is one, PostgreSQL is another. Postgres uses a RETURNING + // clause while HSQL uses a second query that has to be executed with the same connection. + final String keyQuery = tableMetaDataContext.getSimulationQueryForGetGeneratedKey( + tableMetaDataContext.getTableName(), + getGeneratedKeyNames()[0]); + Assert.notNull(keyQuery, "Query for simulating get generated keys can't be null"); + if (keyQuery.toUpperCase().startsWith("RETURNING")) { + Long key = jdbcTemplate.queryForLong( + getInsertString() + " " + keyQuery, + values.toArray(new Object[values.size()])); + HashMap keys = new HashMap(1); + keys.put(getGeneratedKeyNames()[0], key); + keyHolder.getKeyList().add(keys); + } + else { + jdbcTemplate.execute(new ConnectionCallback() { + public Object doInConnection(Connection con) throws SQLException, DataAccessException { + // Do the insert + PreparedStatement ps = null; + try { + ps = con.prepareStatement(getInsertString()); + setParameterValues(ps, values, null); + ps.executeUpdate(); + } finally { + JdbcUtils.closeStatement(ps); + } + //Get the key + Statement keyStmt = null; + ResultSet rs = null; + HashMap keys = new HashMap(1); + try { + keyStmt = con.createStatement(); + rs = keyStmt.executeQuery(keyQuery); + if (rs.next()) { + long key = rs.getLong(1); + keys.put(getGeneratedKeyNames()[0], key); + keyHolder.getKeyList().add(keys); + } + } finally { + JdbcUtils.closeResultSet(rs); + JdbcUtils.closeStatement(keyStmt); + } + return null; + } + }); + } + return keyHolder; + } + return keyHolder; + } + + /** + * Create the PreparedStatement to be used for insert that have generated keys + * + * @param con the connection used + * @return PreparedStatement to use + * @throws SQLException + */ + private PreparedStatement prepareStatementForGeneratedKeys(Connection con) throws SQLException { + if (getGeneratedKeyNames().length < 1) { + throw new InvalidDataAccessApiUsageException("Generated Key Name(s) not specificed. " + + "Using the generated keys features requires specifying the name(s) of the generated column(s)"); + } + PreparedStatement ps; + if (this.tableMetaDataContext.isGeneratedKeysColumnNameArraySupported()) { + if (logger.isDebugEnabled()) { + logger.debug("Using generated keys support with array of column names."); + } + ps = con.prepareStatement(getInsertString(), getGeneratedKeyNames()); + } + else { + if (logger.isDebugEnabled()) { + logger.debug("Using generated keys support with Statement.RETURN_GENERATED_KEYS."); + } + ps = con.prepareStatement(getInsertString(), Statement.RETURN_GENERATED_KEYS); + } + return ps; + } + + /** + * Method that provides execution of a batch insert using the passed in Maps of parameters + * + * @param batch array of Maps with parameter names and values to be used in batch insert + * @return array of number of rows affected + */ + protected int[] doExecuteBatch(Map[] batch) { + checkCompiled(); + List[] batchValues = new ArrayList[batch.length]; + int i = 0; + for (Map args : batch) { + List values = matchInParameterValuesWithInsertColumns(args); + batchValues[i++] = values; + } + return executeBatchInternal(batchValues); + } + + /** + * Method that provides execution of a batch insert using the passed in array of {@link SqlParameterSource} + * + * @param batch array of SqlParameterSource with parameter names and values to be used in insert + * @return array of number of rows affected + */ + protected int[] doExecuteBatch(SqlParameterSource[] batch) { + checkCompiled(); + List[] batchValues = new ArrayList[batch.length]; + int i = 0; + for (SqlParameterSource parameterSource : batch) { + List values = matchInParameterValuesWithInsertColumns(parameterSource); + batchValues[i++] = values; + } + return executeBatchInternal(batchValues); + } + + /** + * Method to execute the batch insert + */ + //TODO synchronize parameter setters with the SimpleJdbcTemplate + private int[] executeBatchInternal(final List[] batchValues) { + if (logger.isDebugEnabled()) { + logger.debug("Executing statement " + getInsertString() + " with batch of size: " + batchValues.length); + } + final int[] columnTypes = getInsertTypes(); + int[] updateCounts = jdbcTemplate.batchUpdate( + getInsertString(), + new BatchPreparedStatementSetter() { + + public void setValues(PreparedStatement ps, int i) throws SQLException { + List values = batchValues[i]; + setParameterValues(ps, values, columnTypes); + } + + public int getBatchSize() { + return batchValues.length; + } + }); + return updateCounts; + } + + /** + * Internal implementation for setting parameter values + * @param preparedStatement the PreparedStatement + * @param values the values to be set + */ + private void setParameterValues(PreparedStatement preparedStatement, List values, int[] columnTypes) + throws SQLException { + int colIndex = 0; + for (Object value : values) { + colIndex++; + if (columnTypes == null || colIndex < columnTypes.length) { + StatementCreatorUtils.setParameterValue(preparedStatement, colIndex, SqlTypeValue.TYPE_UNKNOWN, value); + } + else { + StatementCreatorUtils.setParameterValue(preparedStatement, colIndex, columnTypes[colIndex - 1], value); + } + } + } + + /** + * Match the provided in parameter values with regitered parameters and parameters defined via metedata + * processing. + * + * @param parameterSource the parameter vakues provided as a {@link SqlParameterSource} + * @return Map with parameter names and values + */ + protected List matchInParameterValuesWithInsertColumns(SqlParameterSource parameterSource) { + return tableMetaDataContext.matchInParameterValuesWithInsertColumns(parameterSource); + } + + /** + * Match the provided in parameter values with regitered parameters and parameters defined via metedata + * processing. + * + * @param args the parameter values provided in a Map + * @return Map with parameter names and values + */ + protected List matchInParameterValuesWithInsertColumns(Map args) { + return tableMetaDataContext.matchInParameterValuesWithInsertColumns(args); + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/simple/ParameterizedBeanPropertyRowMapper.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/simple/ParameterizedBeanPropertyRowMapper.java new file mode 100644 index 0000000000..4e818b64e7 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/simple/ParameterizedBeanPropertyRowMapper.java @@ -0,0 +1,81 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.core.simple; + +import java.sql.ResultSet; +import java.sql.SQLException; + +import org.springframework.jdbc.core.BeanPropertyRowMapper; + +/** + * {@link ParameterizedRowMapper} implementation that converts a row into a new instance + * of the specified mapped target class. The mapped target class must be a top-level class + * and it must have a default or no-arg constructor. + * + *

Uses Java 5 covariant return types to override the return type of the {@link #mapRow} + * method to be the type parameter T. + * + *

Column values are mapped based on matching the column name as obtained from result set + * metadata to public setters for the corresponding properties. The names are matched either + * directly or by transforming a name separating the parts with underscores to the same name + * using "camel" case. + * + *

Mapping is provided for fields in the target class for many common types, e.g.: + * String, boolean, Boolean, byte, Byte, short, Short, int, Integer, long, Long, + * float, Float, double, Double, BigDecimal, java.util.Date, etc. + * + *

To facilitate mapping between columns and fields that don't have matching names, + * try using column aliases in the SQL statement like "select fname as first_name from customer". + * + *

Please note that this class is designed to provide convenience rather than high performance. + * For best performance consider using a custom RowMapper. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @since 2.5 + * @see ParameterizedRowMapper + */ +public class ParameterizedBeanPropertyRowMapper extends BeanPropertyRowMapper + implements ParameterizedRowMapper { + + /** + * Create a new ParameterizedBeanPropertyRowMapper. + *

Generally prefer the {@link #newInstance(Class)} method instead, + * which avoids the need for specifying the mapped type twice. + * @see #setMappedClass + */ + public ParameterizedBeanPropertyRowMapper() { + } + + @SuppressWarnings("unchecked") + public T mapRow(ResultSet rs, int rowNumber) throws SQLException { + return (T) super.mapRow(rs, rowNumber); + } + + + /** + * Static factory method to create a new ParameterizedBeanPropertyRowMapper + * (with the mapped class specified only once). + * @param mappedClass the class that each row should be mapped to + */ + public static ParameterizedBeanPropertyRowMapper newInstance(Class mappedClass) { + ParameterizedBeanPropertyRowMapper newInstance = new ParameterizedBeanPropertyRowMapper(); + newInstance.setMappedClass(mappedClass); + return newInstance; + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/simple/ParameterizedRowMapper.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/simple/ParameterizedRowMapper.java new file mode 100644 index 0000000000..fa9d6ee8a2 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/simple/ParameterizedRowMapper.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2005 the original author or authors. + * + * Licensed 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.springframework.jdbc.core.simple; + +import java.sql.ResultSet; +import java.sql.SQLException; + +import org.springframework.jdbc.core.RowMapper; + +/** + * Extension of the {@link org.springframework.jdbc.core.RowMapper} interface, + * adding type parameterization. Uses Java 5 covariant return types to override + * the return type of the {@link #mapRow} method to be the type parameter + * T. + * + * @author Rob Harrop + * @since 2.0 + * @see org.springframework.jdbc.core.simple.SimpleJdbcOperations + */ +public interface ParameterizedRowMapper extends RowMapper { + + /** + * Implementations should return the object representation of + * the current row in the supplied {@link ResultSet}. + * @see org.springframework.jdbc.core.RowMapper#mapRow + */ + T mapRow(ResultSet rs, int rowNum) throws SQLException; + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/simple/ParameterizedSingleColumnRowMapper.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/simple/ParameterizedSingleColumnRowMapper.java new file mode 100644 index 0000000000..195e7fb3a1 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/simple/ParameterizedSingleColumnRowMapper.java @@ -0,0 +1,68 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.core.simple; + +import java.sql.ResultSet; +import java.sql.SQLException; + +import org.springframework.jdbc.core.SingleColumnRowMapper; + +/** + * {@link ParameterizedRowMapper} implementation that converts a single column + * into a single result value per row. Expects to operate on a + * java.sql.ResultSet that just contains a single column. + * + *

The type of the result value for each row can be specified. The value + * for the single column will be extracted from the ResultSet + * and converted into the specified target type. + * + *

Uses Java 5 covariant return types to override the return type of the + * {@link #mapRow} method to be the type parameter T. + * + * @author Juergen Hoeller + * @since 2.5.2 + */ +public class ParameterizedSingleColumnRowMapper extends SingleColumnRowMapper + implements ParameterizedRowMapper { + + /** + * Create a new ParameterizedSingleColumnRowMapper. + *

Generally prefer the {@link #newInstance(Class)} method instead, + * which avoids the need for specifying the mapped type twice. + * @see #setRequiredType + */ + public ParameterizedSingleColumnRowMapper() { + } + + @SuppressWarnings("unchecked") + public T mapRow(ResultSet rs, int rowNumber) throws SQLException { + return (T) super.mapRow(rs, rowNumber); + } + + + /** + * Static factory method to create a new ParameterizedSingleColumnRowMapper + * (with the required type specified only once). + * @param requiredType the type that each result object is expected to match + */ + public static ParameterizedSingleColumnRowMapper newInstance(Class requiredType) { + ParameterizedSingleColumnRowMapper rm = new ParameterizedSingleColumnRowMapper(); + rm.setRequiredType(requiredType); + return rm; + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcCall.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcCall.java new file mode 100644 index 0000000000..c9f7b40a75 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcCall.java @@ -0,0 +1,167 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.core.simple; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; + +import javax.sql.DataSource; + +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.SqlParameter; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.SqlParameterSource; + +/** + * A SimpleJdbcCall is a multi-threaded, reusable object representing a call + * to a stored procedure or a stored function. It provides meta data processing + * to simplify the code needed to access basic stored procedures/functions. + * All you need to provide is the name of the procedure/function and a Map + * containing the parameters when you execute the call. The names of the + * supplied parameters will be matched up with in and out parameters declared + * when the stored procedure was created. + * + *

The meta data processing is based on the DatabaseMetaData provided by + * the JDBC driver. Since we rely on the JDBC driver this "auto-detection" + * can only be used for databases that are known to provide accurate meta data. + * These currently include Derby, MySQL, Microsoft SQL Server, Oracle and DB2. + * For any other databases you are required to declare all parameters explicitly. + * You can of course declare all parameters explicitly even if the database provides + * the necessary meta data. In that case your declared parameters will take precedence. + * You can also turn off any mete data processing if you want to use parameter names + * that do not match what is declared during the stored procedure compilation. + * + *

The actual insert is being handled using Spring's + * {@link org.springframework.jdbc.core.JdbcTemplate}. + * + *

Many of the configuration methods return the current instance of the SimpleJdbcCall + * to provide the ability to string multiple ones together in a "fluid" interface style. + * + * @author Thomas Risberg + * @since 2.5 + * @see java.sql.DatabaseMetaData + * @see org.springframework.jdbc.core.JdbcTemplate + */ +public class SimpleJdbcCall extends AbstractJdbcCall implements SimpleJdbcCallOperations { + + /** + * Constructor that takes one parameter with the JDBC DataSource to use when creating the + * JdbcTemplate. + * @param dataSource the DataSource to use + * @see org.springframework.jdbc.core.JdbcTemplate#setDataSource + */ + public SimpleJdbcCall(DataSource dataSource) { + super(dataSource); + } + + /** + * Alternative Constructor that takes one parameter with the JdbcTemplate to be used. + * @param jdbcTemplate the JdbcTemplate to use + * @see org.springframework.jdbc.core.JdbcTemplate#setDataSource + */ + public SimpleJdbcCall(JdbcTemplate jdbcTemplate) { + super(jdbcTemplate); + } + + + public SimpleJdbcCall withProcedureName(String procedureName) { + setProcedureName(procedureName); + setFunction(false); + return this; + } + + public SimpleJdbcCall withFunctionName(String functionName) { + setProcedureName(functionName); + setFunction(true); + return this; + } + + public SimpleJdbcCall withSchemaName(String schemaName) { + setSchemaName(schemaName); + return this; + } + + public SimpleJdbcCall withCatalogName(String catalogName) { + setCatalogName(catalogName); + return this; + } + + public SimpleJdbcCall withReturnValue() { + setReturnValueRequired(true); + return this; + } + + public SimpleJdbcCall declareParameters(SqlParameter... sqlParameters) { + for (SqlParameter sqlParameter : sqlParameters) { + if (sqlParameter != null) { + addDeclaredParameter(sqlParameter); + } + } + return this; + } + + public SimpleJdbcCall useInParameterNames(String... inParameterNames) { + setInParameterNames(new HashSet(Arrays.asList(inParameterNames))); + return this; + } + + public SimpleJdbcCall returningResultSet(String parameterName, ParameterizedRowMapper rowMapper) { + addDeclaredRowMapper(parameterName, rowMapper); + return this; + } + + public SimpleJdbcCall withoutProcedureColumnMetaDataAccess() { + setAccessCallParameterMetaData(false); + return this; + } + + + @SuppressWarnings("unchecked") + public T executeFunction(Class returnType, Map args) { + return (T) doExecute(args).get(getScalarOutParameterName()); + } + + @SuppressWarnings("unchecked") + public T executeFunction(Class returnType, MapSqlParameterSource args) { + return (T) doExecute(args).get(getScalarOutParameterName()); + } + + @SuppressWarnings("unchecked") + public T executeObject(Class returnType, Map args) { + return (T) doExecute(args).get(getScalarOutParameterName()); + } + + @SuppressWarnings("unchecked") + public T executeObject(Class returnType, MapSqlParameterSource args) { + return (T) doExecute(args).get(getScalarOutParameterName()); + } + + public Map execute() { + return doExecute(new HashMap()); + } + + public Map execute(Map args) { + return doExecute(args); + } + + public Map execute(SqlParameterSource parameterSource) { + return doExecute(parameterSource); + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcCallOperations.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcCallOperations.java new file mode 100644 index 0000000000..769b50befa --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcCallOperations.java @@ -0,0 +1,157 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc.core.simple; + +import java.util.Map; + +import org.springframework.jdbc.core.SqlParameter; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.SqlParameterSource; + +/** + * Interface specifying the API for a Simple JDBC Call implemented by {@link SimpleJdbcCall}. + * This interface is not often used directly, but provides the + * option to enhance testability, as it can easily be mocked or stubbed. + * + * @author Thomas Risberg + * @since 2.5 + */ +public interface SimpleJdbcCallOperations { + + /** + * Specify the procedure name to be used - this implies that we will be calling a stored procedure. + * @param procedureName the name of the stored procedure + * @return the instance of this SimpleJdbcCall + */ + SimpleJdbcCallOperations withProcedureName(String procedureName); + + /** + * Specify the procedure name to be used - this implies that we will be calling a stored function. + * @param functionName the name of the stored function + * @return the instance of this SimpleJdbcCall + */ + SimpleJdbcCallOperations withFunctionName(String functionName); + + /** + * Optionally, specify the name of the schema that contins the stored procedure. + * @param schemaName the name of the schema + * @return the instance of this SimpleJdbcCall + */ + SimpleJdbcCallOperations withSchemaName(String schemaName); + + /** + * Optionally, specify the name of the catalog that contins the stored procedure. + * To provide consistency with the Oracle DatabaseMetaData, this is used to specify the package name if + * the procedure is declared as part of a package. + * @param catalogName the catalog or package name + * @return the instance of this SimpleJdbcCall + */ + SimpleJdbcCallOperations withCatalogName(String catalogName); + + /** + * Indicates the procedure's return value should be included in the results returned. + * @return the instance of this SimpleJdbcCall + */ + SimpleJdbcCallOperations withReturnValue(); + + /** + * Specify one or more parameters if desired. These parameters will be supplemented with any + * parameter information retrieved from the database meta data. + * Note that only parameters declared as SqlParameter and SqlInOutParameter + * will be used to provide input values. This is different from the StoredProcedure class + * which for backwards compatibility reasons allows input values to be provided for parameters declared + * as SqlOutParameter. + * + * @param sqlParameters the parameters to use + * @return the instance of this SimpleJdbcCall + */ + SimpleJdbcCallOperations declareParameters(SqlParameter... sqlParameters); + + /** Not used yet */ + SimpleJdbcCallOperations useInParameterNames(String... inParameterNames); + + /** + * Used to specify when a ResultSet is returned by the stored procedure and you want it mapped + * by a RowMapper. The results will be returned using the parameter name specified. Multiple + * ResultSets must be declared in the correct order. If the database you are using uses ref cursors + * then the name specified must match the name of the parameter declared for the procedure in the + * database. + * @param parameterName the name of the returned results and/or the name of the ref cursor parameter + * @param rowMapper the RowMapper implementation that will map the data returned for each row + * */ + SimpleJdbcCallOperations returningResultSet(String parameterName, ParameterizedRowMapper rowMapper); + + /** + * Turn off any processing of parameter meta data information obtained via JDBC. + * @return the instance of this SimpleJdbcCall + */ + SimpleJdbcCallOperations withoutProcedureColumnMetaDataAccess(); + + + /** + * Execute the stored function and return the results obtained as an Object of the specified return type. + * @param returnType the type of the value to return + * @param args Map containing the parameter values to be used in the call. + */ + T executeFunction(Class returnType, Map args); + + /** + * Execute the stored function and return the results obtained as an Object of the specified return type. + * @param returnType the type of the value to return + * @param args MapSqlParameterSource containing the parameter values to be used in the call. + */ + T executeFunction(Class returnType, MapSqlParameterSource args); + + /** + * Execute the stored procedure and return the single out parameter as an Object of the specified return type. + * In the case where there are multiple out parameters, the first one is returned and additional out parameters + * are ignored. + * @param returnType the type of the value to return + * @param args Map containing the parameter values to be used in the call. + */ + T executeObject(Class returnType, Map args); + + /** + * Execute the stored procedure and return the single out parameter as an Object of the specified return type. + * In the case where there are multiple out parameters, the first one is returned and additional out parameters + * are ignored. + * @param returnType the type of the value to return + * @param args MapSqlParameterSource containing the parameter values to be used in the call. + */ + T executeObject(Class returnType, MapSqlParameterSource args); + + /** + * Execute the stored procedure and return a map of output params, keyed by name as in parameter declarations.. + * @return map of output params. + */ + Map execute(); + + /** + * Execute the stored procedure and return a map of output params, keyed by name as in parameter declarations.. + * @param args Map containing the parameter values to be used in the call. + * @return map of output params. + */ + Map execute(Map args); + + /** + * Execute the stored procedure and return a map of output params, keyed by name as in parameter declarations.. + * @param args SqlParameterSource containing the parameter values to be used in the call. + * @return map of output params. + */ + Map execute(SqlParameterSource args); + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcDaoSupport.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcDaoSupport.java new file mode 100644 index 0000000000..c26eefce95 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcDaoSupport.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc.core.simple; + +import org.springframework.jdbc.core.support.JdbcDaoSupport; + +/** + * Extension of {@link org.springframework.jdbc.core.support.JdbcDaoSupport} + * that exposes a {@link #getSimpleJdbcTemplate() SimpleJdbcTemplate} as well. + * Only usable on Java 5 and above. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @since 2.0 + * @see SimpleJdbcTemplate + */ +public class SimpleJdbcDaoSupport extends JdbcDaoSupport { + + private SimpleJdbcTemplate simpleJdbcTemplate; + + + /** + * Create a SimpleJdbcTemplate based on the configured JdbcTemplate. + */ + protected void initTemplateConfig() { + this.simpleJdbcTemplate = new SimpleJdbcTemplate(getJdbcTemplate()); + } + + /** + * Return a SimpleJdbcTemplate wrapping the configured JdbcTemplate. + */ + public SimpleJdbcTemplate getSimpleJdbcTemplate() { + return this.simpleJdbcTemplate; + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcInsert.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcInsert.java new file mode 100644 index 0000000000..cd57b26d76 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcInsert.java @@ -0,0 +1,129 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.core.simple; + +import java.util.Arrays; +import java.util.Map; + +import javax.sql.DataSource; + +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.namedparam.SqlParameterSource; +import org.springframework.jdbc.support.KeyHolder; + +/** + * A SimpleJdbcInsert is a multi-threaded, reusable object providing easy insert + * capabilities for a table. It provides meta data processing to simplify the code + * needed to construct a basic insert statement. All you need to provide is the + * name of the table and a Map containing the column names and the column values. + * + *

The meta data processing is based on the DatabaseMetaData provided by the + * JDBC driver. As long as the JBDC driver can provide the names of the columns + * for a specified table than we can rely on this auto-detection feature. If that + * is not the case then the column names must be specified explicitly. + * + *

The actual insert is being handled using Spring's + * {@link org.springframework.jdbc.core.JdbcTemplate}. + * + *

Many of the configuration methods return the current instance of the SimpleJdbcInsert + * to provide the ability to string multiple ones together in a "fluid" interface style. + * + * @author Thomas Risberg + * @since 2.5 + * @see java.sql.DatabaseMetaData + * @see org.springframework.jdbc.core.JdbcTemplate + */ +public class SimpleJdbcInsert extends AbstractJdbcInsert implements SimpleJdbcInsertOperations { + + /** + * Constructor that takes one parameter with the JDBC DataSource to use when creating the + * JdbcTemplate. + * @param dataSource the DataSource to use + * @see org.springframework.jdbc.core.JdbcTemplate#setDataSource + */ + public SimpleJdbcInsert(DataSource dataSource) { + super(dataSource); + } + + /** + * Alternative Constructor that takes one parameter with the JdbcTemplate to be used. + * @param jdbcTemplate the JdbcTemplate to use + * @see org.springframework.jdbc.core.JdbcTemplate#setDataSource + */ + public SimpleJdbcInsert(JdbcTemplate jdbcTemplate) { + super(jdbcTemplate); + } + + + public SimpleJdbcInsert withTableName(String tableName) { + setTableName(tableName); + return this; + } + + public SimpleJdbcInsert withSchemaName(String schemaName) { + setSchemaName(schemaName); + return this; + } + + public SimpleJdbcInsert withCatalogName(String catalogName) { + setCatalogName(catalogName); + return this; + } + + public SimpleJdbcInsert usingColumns(String... columnNames) { + setColumnNames(Arrays.asList(columnNames)); + return this; + } + + public SimpleJdbcInsert usingGeneratedKeyColumns(String... columnNames) { + setGeneratedKeyNames(columnNames); + return this; + } + + public int execute(Map args) { + return doExecute(args); + } + + public int execute(SqlParameterSource parameterSource) { + return doExecute(parameterSource); + } + + public Number executeAndReturnKey(Map args) { + return doExecuteAndReturnKey(args); + } + + public Number executeAndReturnKey(SqlParameterSource parameterSource) { + return doExecuteAndReturnKey(parameterSource); + } + + public KeyHolder executeAndReturnKeyHolder(Map args) { + return doExecuteAndReturnKeyHolder(args); + } + + public KeyHolder executeAndReturnKeyHolder(SqlParameterSource parameterSource) { + return doExecuteAndReturnKeyHolder(parameterSource); + } + + public int[] executeBatch(Map[] batch) { + return doExecuteBatch(batch); + } + + public int[] executeBatch(SqlParameterSource[] batch) { + return doExecuteBatch(batch); + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcInsertOperations.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcInsertOperations.java new file mode 100644 index 0000000000..4a3dcc47bb --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcInsertOperations.java @@ -0,0 +1,134 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc.core.simple; + +import java.util.Map; + +import org.springframework.jdbc.core.namedparam.SqlParameterSource; +import org.springframework.jdbc.support.KeyHolder; + +/** + * Interface specifying the API for a Simple JDBC Insert implemented by {@link SimpleJdbcInsert}. + * This interface is not often used directly, but provides the + * option to enhance testability, as it can easily be mocked or stubbed. + * + * @author Thomas Risberg + * @since 2.5 + */ +public interface SimpleJdbcInsertOperations { + + /** + * Specify the table name to be used for the insert. + * @param tableName the name of the stored table + * @return the instance of this SimpleJdbcInsert + */ + SimpleJdbcInsertOperations withTableName(String tableName); + + /** + * Specify the shema name, if any, to be used for the insert. + * @param schemaName the name of the schema + * @return the instance of this SimpleJdbcInsert + */ + SimpleJdbcInsertOperations withSchemaName(String schemaName); + + /** + * Specify the catalog name, if any, to be used for the insert. + * @param catalogName the name of the catalog + * @return the instance of this SimpleJdbcInsert + */ + SimpleJdbcInsertOperations withCatalogName(String catalogName); + + /** + * Specify the column names that the insert statement should be limited to use. + * @param columnNames one or more column names + * @return the instance of this SimpleJdbcInsert + */ + SimpleJdbcInsertOperations usingColumns(String... columnNames); + + /** + * Specify the name sof any columns that have auto generated keys. + * @param columnNames one or more column names + * @return the instance of this SimpleJdbcInsert + */ + SimpleJdbcInsertOperations usingGeneratedKeyColumns(String... columnNames); + + + /** + * Execute the insert using the values passed in. + * @param args Map containing column names and corresponding value + * @return the number of rows affected as returned by the JDBC driver + */ + int execute(Map args); + + /** + * Execute the insert using the values passed in. + * @param parameterSource SqlParameterSource containing values to use for insert + * @return the number of rows affected as returned by the JDBC driver + */ + int execute(SqlParameterSource parameterSource); + + /** + * Execute the insert using the values passed in and return the generated key. This requires that + * the name of the columns with auto generated keys have been specified. This method will always + * return a key or throw an exception if a key was not returned. + * @param args Map containing column names and corresponding value + * @return the generated key value + */ + Number executeAndReturnKey(Map args); + + /** + * Execute the insert using the values passed in and return the generated key. This requires that + * the name of the columns with auto generated keys have been specified. This method will always + * return a key or throw an exception if a key was not returned. + * @param parameterSource SqlParameterSource containing values to use for insert + * @return the generated key value. + */ + Number executeAndReturnKey(SqlParameterSource parameterSource); + + /** + * Execute the insert using the values passed in and return the generated keys. This requires that + * the name of the columns with auto generated keys have been specified. This method will always return + * a KeyHolder but the caller must verify that it actually contains the generated keys. + * @param args Map containing column names and corresponding value + * @return the KeyHolder containing all generated keys + */ + KeyHolder executeAndReturnKeyHolder(Map args); + + /** + * Execute the insert using the values passed in and return the generated keys. This requires that + * the name of the columns with auto generated keys have been specified. This method will always return + * a KeyHolder but the caller must verify that it actually contains the generated keys. + * @param parameterSource SqlParameterSource containing values to use for insert + * @return the KeyHolder containing all generated keys + */ + KeyHolder executeAndReturnKeyHolder(SqlParameterSource parameterSource); + + /** + * Execute a batch insert using the batch of values passed in. + * @param batch an array of Maps containing a batch of column names and corresponding value + * @return the array of number of rows affected as returned by the JDBC driver + */ + int[] executeBatch(Map[] batch); + + /** + * Execute a batch insert using the batch of values passed in. + * @param batch an array of SqlParameterSource containing values for the batch + * @return the array of number of rows affected as returned by the JDBC driver + */ + int[] executeBatch(SqlParameterSource[] batch); + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcOperations.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcOperations.java new file mode 100644 index 0000000000..dbd22909a5 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcOperations.java @@ -0,0 +1,386 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc.core.simple; + +import java.util.List; +import java.util.Map; + +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.core.JdbcOperations; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; +import org.springframework.jdbc.core.namedparam.SqlParameterSource; + +/** + * JDBC operations interface usable on Java 5 and above, exposing a + * set of common JDBC operations, whose interface is simplified + * through the use of varargs and autoboxing. + * + * @author Rod Johnson + * @author Rob Harrop + * @author Thomas Risberg + * @since 2.0 + * @see org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate + * @see SimpleJdbcTemplate + * @see org.springframework.jdbc.core.JdbcOperations + */ +public interface SimpleJdbcOperations { + + /** + * Expose the classic Spring JdbcTemplate to allow invocation of less + * commonly used methods. + */ + JdbcOperations getJdbcOperations(); + + /** + * Expose the Spring NamedParameterJdbcTemplate to allow invocation of less + * commonly used methods. + */ + NamedParameterJdbcOperations getNamedParameterJdbcOperations(); + + + /** + * Query for an int passing in a SQL query + * using the named parameter support provided by the + * {@link org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate} + * and a map containing the arguments. + * @param sql the SQL query to run. + * @param args the map containing the arguments for the query. + */ + int queryForInt(String sql, Map args) throws DataAccessException; + + /** + * Query for an int passing in a SQL query + * using the named parameter support provided by the + * {@link org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate} + * and a SqlParameterSource containing the arguments. + * @param sql the SQL query to run. + * @param args the SqlParameterSource containing the arguments for the query. + */ + int queryForInt(String sql, SqlParameterSource args) throws DataAccessException; + + /** + * Query for an int passing in a SQL query + * using the standard '?' placeholders for parameters + * and a variable number of arguments. + * @param sql the SQL query to run. + * @param args the variable number of arguments for the query. + */ + int queryForInt(String sql, Object... args) throws DataAccessException; + + /** + * Query for an long passing in a SQL query + * using the named parameter support provided by the + * {@link org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate} + * and a map containing the arguments. + * @param sql the SQL query to run. + * @param args the map containing the arguments for the query. + */ + long queryForLong(String sql, Map args) throws DataAccessException; + + /** + * Query for an long passing in a SQL query + * using the named parameter support provided by the + * {@link org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate} + * and a SqlParameterSource containing the arguments. + * @param sql the SQL query to run. + * @param args the SqlParameterSource containing the arguments for the query. + */ + long queryForLong(String sql, SqlParameterSource args) throws DataAccessException; + + /** + * Query for an long passing in a SQL query + * using the standard '?' placeholders for parameters + * and a variable number of arguments. + * @param sql the SQL query to run. + * @param args the variable number of arguments for the query. + */ + long queryForLong(String sql, Object... args) throws DataAccessException; + + /** + * Query for an object of type T identified by the supplied @{@link Class}. + * Uses sql with the named parameter support provided by the + * {@link org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate} + * @param sql the SQL query to run. + * @param requiredType the required type of the return value. + * @param args the map containing the arguments for the query. + * @see JdbcOperations#queryForObject(String, Class) + * @see JdbcOperations#queryForObject(String, Object[], Class) + */ + T queryForObject(String sql, Class requiredType, Map args) + throws DataAccessException; + + /** + * Query for an object of type T identified by the supplied @{@link Class}. + * Uses sql with the named parameter support provided by the + * {@link org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate} + * @param sql the SQL query to run. + * @param requiredType the required type of the return value. + * @param args the SqlParameterSource containing the arguments for the query. + * @see JdbcOperations#queryForObject(String, Class) + * @see JdbcOperations#queryForObject(String, Object[], Class) + */ + T queryForObject(String sql, Class requiredType, SqlParameterSource args) + throws DataAccessException; + + /** + * Query for an object of type T identified by the supplied @{@link Class}. + * Uses sql with the standard '?' placeholders for parameters + * @param sql the SQL query to run. + * @param requiredType the required type of the return value. + * @param args the variable number of arguments for the query. + * @see JdbcOperations#queryForObject(String, Class) + * @see JdbcOperations#queryForObject(String, Object[], Class) + */ + T queryForObject(String sql, Class requiredType, Object... args) + throws DataAccessException; + + /** + * Query for an object of type T using the supplied + * {@link ParameterizedRowMapper} to the query results to the object. + * Uses sql with the named parameter support provided by the + * {@link org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate} + * @param sql the SQL query to run. + * @param rm the @{@link ParameterizedRowMapper} to use for result mapping + * @param args the map containing the arguments for the query. + * @see JdbcOperations#queryForObject(String, org.springframework.jdbc.core.RowMapper) + * @see JdbcOperations#queryForObject(String, Object[], org.springframework.jdbc.core.RowMapper) + */ + T queryForObject(String sql, ParameterizedRowMapper rm, Map args) + throws DataAccessException; + + /** + * Query for an object of type T using the supplied + * {@link ParameterizedRowMapper} to the query results to the object. + * Uses sql with the named parameter support provided by the + * {@link org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate} + * @param sql the SQL query to run. + * @param rm the @{@link ParameterizedRowMapper} to use for result mapping + * @param args the SqlParameterSource containing the arguments for the query. + * @see JdbcOperations#queryForObject(String, org.springframework.jdbc.core.RowMapper) + * @see JdbcOperations#queryForObject(String, Object[], org.springframework.jdbc.core.RowMapper) + */ + T queryForObject(String sql, ParameterizedRowMapper rm, SqlParameterSource args) + throws DataAccessException; + + /** + * Query for an object of type T using the supplied + * {@link ParameterizedRowMapper} to the query results to the object. + * Uses sql with the standard '?' placeholders for parameters + * @param sql the SQL query to run. + * @param rm the @{@link ParameterizedRowMapper} to use for result mapping + * @param args the variable number of arguments for the query. + * @see JdbcOperations#queryForObject(String, org.springframework.jdbc.core.RowMapper) + * @see JdbcOperations#queryForObject(String, Object[], org.springframework.jdbc.core.RowMapper) + */ + T queryForObject(String sql, ParameterizedRowMapper rm, Object... args) + throws DataAccessException; + + /** + * Query for a {@link List} of Objects of type T using + * the supplied {@link ParameterizedRowMapper} to the query results to the object. + * Uses sql with the named parameter support provided by the + * {@link org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate} + * @param sql the SQL query to run. + * @param rm the @{@link ParameterizedRowMapper} to use for result mapping + * @param args the map containing the arguments for the query. + * @see JdbcOperations#queryForObject(String, org.springframework.jdbc.core.RowMapper) + * @see JdbcOperations#queryForObject(String, Object[], org.springframework.jdbc.core.RowMapper) + */ + List query(String sql, ParameterizedRowMapper rm, Map args) + throws DataAccessException; + + /** + * Query for a {@link List} of Objects of type T using + * the supplied {@link ParameterizedRowMapper} to the query results to the object. + * Uses sql with the named parameter support provided by the + * {@link org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate} + * @param sql the SQL query to run. + * @param rm the @{@link ParameterizedRowMapper} to use for result mapping + * @param args the SqlParameterSource containing the arguments for the query. + * @see JdbcOperations#queryForObject(String, org.springframework.jdbc.core.RowMapper) + * @see JdbcOperations#queryForObject(String, Object[], org.springframework.jdbc.core.RowMapper) + */ + List query(String sql, ParameterizedRowMapper rm, SqlParameterSource args) + throws DataAccessException; + + /** + * Query for a {@link List} of Objects of type T using + * the supplied {@link ParameterizedRowMapper} to the query results to the object. + * Uses sql with the standard '?' placeholders for parameters + * @param sql the SQL query to run. + * @param rm the @{@link ParameterizedRowMapper} to use for result mapping + * @param args the variable number of arguments for the query. + * @see JdbcOperations#queryForObject(String, org.springframework.jdbc.core.RowMapper) + * @see JdbcOperations#queryForObject(String, Object[], org.springframework.jdbc.core.RowMapper) + */ + List query(String sql, ParameterizedRowMapper rm, Object... args) + throws DataAccessException; + + /** + * Execute the supplied query with the supplied arguments. + *

The query is expected to be a single row query; the result row will be + * mapped to a Map (one entry for each column, using the column name as the key). + * Uses sql with the named parameter support provided by the + * {@link org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate} + * @param sql the SQL query to run. + * @param args the map containing the arguments for the query. + * @see JdbcOperations#queryForMap(String) + * @see JdbcOperations#queryForMap(String, Object[]) + */ + Map queryForMap(String sql, Map args) + throws DataAccessException; + + /** + * Execute the supplied query with the supplied arguments. + *

The query is expected to be a single row query; the result row will be + * mapped to a Map (one entry for each column, using the column name as the key). + * Uses sql with the named parameter support provided by the + * {@link org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate} + * @param sql the SQL query to run. + * @param args the SqlParameterSource containing the arguments for the query. + * @see JdbcOperations#queryForMap(String) + * @see JdbcOperations#queryForMap(String, Object[]) + */ + Map queryForMap(String sql, SqlParameterSource args) + throws DataAccessException; + + /** + * Execute the supplied query with the (optional) supplied arguments. + *

The query is expected to be a single row query; the result row will be + * mapped to a Map (one entry for each column, using the column name as the key). + * Uses sql with the standard '?' placeholders for parameters + * @param sql the SQL query to run. + * @param args the variable number of arguments for the query. + * @see JdbcOperations#queryForMap(String) + * @see JdbcOperations#queryForMap(String, Object[]) + */ + Map queryForMap(String sql, Object... args) + throws DataAccessException; + + /** + * Execute the supplied query with the supplied arguments. + *

Each element in the returned {@link List} is constructed as a {@link Map} + * as described in {@link #queryForMap} + * Uses sql with the named parameter support provided by the + * {@link org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate} + * @param sql the SQL query to run. + * @param args the map containing the arguments for the query. + * @see JdbcOperations#queryForList(String) + * @see JdbcOperations#queryForList(String, Object[]) + */ + List> queryForList(String sql, Map args) + throws DataAccessException; + + /** + * Execute the supplied query with the supplied arguments. + *

Each element in the returned {@link List} is constructed as a {@link Map} + * as described in {@link #queryForMap} + * Uses sql with the named parameter support provided by the + * {@link org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate} + * @param sql the SQL query to run. + * @param args the SqlParameterSource containing the arguments for the query. + * @see JdbcOperations#queryForList(String) + * @see JdbcOperations#queryForList(String, Object[]) + */ + List> queryForList(String sql, SqlParameterSource args) + throws DataAccessException; + + /** + * Execute the supplied query with the (optional) supplied arguments. + *

Each element in the returned {@link List} is constructed as a {@link Map} + * as described in {@link #queryForMap} + * Uses sql with the standard '?' placeholders for parameters + * @param sql the SQL query to run. + * @param args the variable number of arguments for the query. + * @see JdbcOperations#queryForList(String) + * @see JdbcOperations#queryForList(String, Object[]) + */ + List> queryForList(String sql, Object... args) + throws DataAccessException; + + /** + * Execute the supplied SQL statement with (optional) supplied arguments. + * Uses sql with the named parameter support provided by the + * {@link org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate} + * @param sql the SQL statement to execute. + * @param args the map containing the arguments for the query. + * @return the numbers of rows affected by the update. + * @see NamedParameterJdbcOperations#update(String, Map) + */ + int update(String sql, Map args) throws DataAccessException; + + /** + * Execute the supplied SQL statement with supplied arguments. + * Uses sql with the named parameter support provided by the + * {@link org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate} + * @param sql the SQL statement to execute. + * @param args the SqlParameterSource containing the arguments for the statement. + * @return the numbers of rows affected by the update. + * @see NamedParameterJdbcOperations#update(String, SqlParameterSource) + */ + int update(String sql, SqlParameterSource args) throws DataAccessException; + + /** + * Execute the supplied SQL statement with supplied arguments. + * Uses sql with the standard '?' placeholders for parameters + * @param sql the SQL statement to execute. + * @param args the variable number of arguments for the query. + * @return the numbers of rows affected by the update. + * @see JdbcOperations#update(String) + * @see JdbcOperations#update(String, Object[]) + */ + int update(String sql, Object... args) throws DataAccessException; + + /** + * Executes a batch using the supplied SQL statement with the batch of supplied arguments. + * Uses sql with the named parameter support. + * @param sql the SQL statement to execute. + * @param batchValues the array of Maps containing the batch of arguments for the query. + * @return an array containing the numbers of rows affected by each update in the batch. + */ + public int[] batchUpdate(String sql, Map[] batchValues); + + /** + * Execute a batch using the supplied SQL statement with the batch of supplied arguments. + * Uses sql with the named parameter support. + * @param sql the SQL statement to execute. + * @param batchArgs the array of {@link SqlParameterSource} containing the batch of arguments for the query. + * @return an array containing the numbers of rows affected by each update in the batch. + */ + public int[] batchUpdate(String sql, SqlParameterSource[] batchArgs); + + /** + * Execute a batch using the supplied SQL statement with the batch of supplied arguments. + * Uses sql with the standard '?' placeholders for parameters + * @param sql the SQL statement to execute. + * @param batchArgs the List of Object arrays containing the batch of arguments for the query. + * @return an array containing the numbers of rows affected by each update in the batch. + */ + public int[] batchUpdate(String sql, List batchArgs); + + /** + * Execute a batch using the supplied SQL statement with the batch of supplied arguments. + * Uses sql with the standard '?' placeholders for parameters + * @param sql the SQL statement to execute. + * @param batchArgs the List of Object arrays containing the batch of arguments for the query. + * @param argTypes SQL types of the arguments + * (constants from java.sql.Types) + * @return an array containing the numbers of rows affected by each update in the batch. + */ + public int[] batchUpdate(String sql, List batchArgs, int[] argTypes); + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcTemplate.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcTemplate.java new file mode 100644 index 0000000000..0688de0d67 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcTemplate.java @@ -0,0 +1,338 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc.core.simple; + +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.List; +import java.util.Map; + +import javax.sql.DataSource; + +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.core.BatchPreparedStatementSetter; +import org.springframework.jdbc.core.JdbcOperations; +import org.springframework.jdbc.core.SqlParameterValue; +import org.springframework.jdbc.core.SqlTypeValue; +import org.springframework.jdbc.core.StatementCreatorUtils; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.core.namedparam.NamedParameterUtils; +import org.springframework.jdbc.core.namedparam.ParsedSql; +import org.springframework.jdbc.core.namedparam.SqlParameterSource; +import org.springframework.util.ObjectUtils; + +/** + * Java-5-based convenience wrapper for the classic Spring + * {@link org.springframework.jdbc.core.JdbcTemplate}, + * taking advantage of varargs and autoboxing, and exposing only the most + * commonly required operations in order to simplify JdbcTemplate usage. + * + *

Use the {@link #getJdbcOperations()} method (or a straight JdbcTemplate) + * if you need to invoke less commonly used template methods. This includes + * any methods specifying SQL types, methods using less commonly used callbacks + * such as RowCallbackHandler, updates with PreparedStatementSetters rather than + * argument arrays, and stored procedures as well as batch operations. + * + * @author Rod Johnson + * @author Rob Harrop + * @author Juergen Hoeller + * @author Thomas Risberg + * @since 2.0 + * @see ParameterizedRowMapper + * @see SimpleJdbcDaoSupport + * @see org.springframework.jdbc.core.JdbcTemplate + */ +public class SimpleJdbcTemplate implements SimpleJdbcOperations { + + /** The NamedParameterJdbcTemplate that we are wrapping */ + private final NamedParameterJdbcOperations namedParameterJdbcOperations; + + + /** + * Create a new SimpleJdbcTemplate for the given DataSource. + *

Creates a classic Spring JdbcTemplate and wraps it. + * @param dataSource the JDBC DataSource to access + */ + public SimpleJdbcTemplate(DataSource dataSource) { + this.namedParameterJdbcOperations = new NamedParameterJdbcTemplate(dataSource); + } + + /** + * Create a new SimpleJdbcTemplate for the given classic Spring JdbcTemplate. + * @param classicJdbcTemplate the classic Spring JdbcTemplate to wrap + */ + public SimpleJdbcTemplate(JdbcOperations classicJdbcTemplate) { + this.namedParameterJdbcOperations = new NamedParameterJdbcTemplate(classicJdbcTemplate); + } + + /** + * Create a new SimpleJdbcTemplate for the given Spring NamedParameterJdbcTemplate. + * @param namedParameterJdbcTemplate the Spring NamedParameterJdbcTemplate to wrap + */ + public SimpleJdbcTemplate(NamedParameterJdbcOperations namedParameterJdbcTemplate) { + this.namedParameterJdbcOperations = namedParameterJdbcTemplate; + } + + + /** + * Expose the classic Spring JdbcTemplate to allow invocation of + * less commonly used methods. + */ + public JdbcOperations getJdbcOperations() { + return this.namedParameterJdbcOperations.getJdbcOperations(); + } + + /** + * Expose the Spring NamedParameterJdbcTemplate to allow invocation of + * less commonly used methods. + */ + public NamedParameterJdbcOperations getNamedParameterJdbcOperations() { + return this.namedParameterJdbcOperations; + } + + + public int queryForInt(String sql, Map args) throws DataAccessException { + return getNamedParameterJdbcOperations().queryForInt(sql, args); + } + + public int queryForInt(String sql, SqlParameterSource args) throws DataAccessException { + return getNamedParameterJdbcOperations().queryForInt(sql, args); + } + + public int queryForInt(String sql, Object... args) throws DataAccessException { + return (ObjectUtils.isEmpty(args) ? + getJdbcOperations().queryForInt(sql) : + getJdbcOperations().queryForInt(sql, getArguments(args))); + } + + public long queryForLong(String sql, Map args) throws DataAccessException { + return getNamedParameterJdbcOperations().queryForLong(sql, args); + } + + public long queryForLong(String sql, SqlParameterSource args) throws DataAccessException { + return getNamedParameterJdbcOperations().queryForLong(sql, args); + } + + public long queryForLong(String sql, Object... args) throws DataAccessException { + return (ObjectUtils.isEmpty(args) ? + getJdbcOperations().queryForLong(sql) : + getJdbcOperations().queryForLong(sql, getArguments(args))); + } + + @SuppressWarnings("unchecked") + public T queryForObject(String sql, Class requiredType, Map args) throws DataAccessException { + return (T) getNamedParameterJdbcOperations().queryForObject(sql, args, requiredType); + } + + @SuppressWarnings("unchecked") + public T queryForObject(String sql, Class requiredType, SqlParameterSource args) + throws DataAccessException { + return (T) getNamedParameterJdbcOperations().queryForObject(sql, args, requiredType); + } + + @SuppressWarnings("unchecked") + public T queryForObject(String sql, Class requiredType, Object... args) throws DataAccessException { + return (T) (ObjectUtils.isEmpty(args) ? + getJdbcOperations().queryForObject(sql, requiredType) : + getJdbcOperations().queryForObject(sql, getArguments(args), requiredType)); + } + + @SuppressWarnings("unchecked") + public T queryForObject(String sql, ParameterizedRowMapper rm, Map args) throws DataAccessException { + return (T) getNamedParameterJdbcOperations().queryForObject(sql, args, rm); + } + + @SuppressWarnings("unchecked") + public T queryForObject(String sql, ParameterizedRowMapper rm, SqlParameterSource args) + throws DataAccessException { + return (T) getNamedParameterJdbcOperations().queryForObject(sql, args, rm); + } + + @SuppressWarnings("unchecked") + public T queryForObject(String sql, ParameterizedRowMapper rm, Object... args) throws DataAccessException { + return (T) (ObjectUtils.isEmpty(args) ? + getJdbcOperations().queryForObject(sql, rm): + getJdbcOperations().queryForObject(sql, getArguments(args), rm)); + } + + @SuppressWarnings("unchecked") + public List query(String sql, ParameterizedRowMapper rm, Map args) throws DataAccessException { + return (List) getNamedParameterJdbcOperations().query(sql, args, rm); + } + + @SuppressWarnings("unchecked") + public List query(String sql, ParameterizedRowMapper rm, SqlParameterSource args) + throws DataAccessException { + return (List) getNamedParameterJdbcOperations().query(sql, args, rm); + } + + @SuppressWarnings("unchecked") + public List query(String sql, ParameterizedRowMapper rm, Object... args) throws DataAccessException { + return (List) (ObjectUtils.isEmpty(args) ? + getJdbcOperations().query(sql, rm) : + getJdbcOperations().query(sql, getArguments(args), rm)); + } + + @SuppressWarnings("unchecked") + public Map queryForMap(String sql, Map args) throws DataAccessException { + return getNamedParameterJdbcOperations().queryForMap(sql, args); + } + + @SuppressWarnings("unchecked") + public Map queryForMap(String sql, SqlParameterSource args) + throws DataAccessException { + return getNamedParameterJdbcOperations().queryForMap(sql, args); + } + + @SuppressWarnings("unchecked") + public Map queryForMap(String sql, Object... args) throws DataAccessException { + return (ObjectUtils.isEmpty(args) ? + getJdbcOperations().queryForMap(sql) : + getJdbcOperations().queryForMap(sql, getArguments(args))); + } + + @SuppressWarnings("unchecked") + public List> queryForList(String sql, Map args) throws DataAccessException { + return getNamedParameterJdbcOperations().queryForList(sql, args); + } + + @SuppressWarnings("unchecked") + public List> queryForList(String sql, SqlParameterSource args) + throws DataAccessException { + return getNamedParameterJdbcOperations().queryForList(sql, args); + } + + @SuppressWarnings("unchecked") + public List> queryForList(String sql, Object... args) throws DataAccessException { + return (ObjectUtils.isEmpty(args) ? + getJdbcOperations().queryForList(sql) : + getJdbcOperations().queryForList(sql, getArguments(args))); + } + + public int update(String sql, Map args) throws DataAccessException { + return getNamedParameterJdbcOperations().update(sql, args); + } + + public int update(String sql, SqlParameterSource args) throws DataAccessException { + return getNamedParameterJdbcOperations().update(sql, args); + } + + public int update(String sql, Object ... args) throws DataAccessException { + return (ObjectUtils.isEmpty(args) ? + getJdbcOperations().update(sql) : + getJdbcOperations().update(sql, getArguments(args))); + } + + public int[] batchUpdate(String sql, List batchArgs) { + return doExecuteBatchUpdate(sql, batchArgs, new int[0]); + } + + public int[] batchUpdate(String sql, List batchArgs, int[] argTypes) { + return doExecuteBatchUpdate(sql, batchArgs, argTypes); + } + + public int[] batchUpdate(String sql, Map[] batchValues) { + SqlParameterSource[] batchArgs = new SqlParameterSource[batchValues.length]; + int i = 0; + for (Map values : batchValues) { + batchArgs[i] = new MapSqlParameterSource(values); + i++; + } + return doExecuteBatchUpdateWithNamedParameters(sql, batchArgs); + } + + public int[] batchUpdate(String sql, SqlParameterSource[] batchArgs) { + return doExecuteBatchUpdateWithNamedParameters(sql, batchArgs); + } + + + private int[] doExecuteBatchUpdate(String sql, final List batchValues, final int[] columnTypes) { + return getJdbcOperations().batchUpdate( + sql, + new BatchPreparedStatementSetter() { + + public void setValues(PreparedStatement ps, int i) throws SQLException { + Object[] values = batchValues.get(i); + doSetStatementParameters(values, ps, columnTypes); + } + + public int getBatchSize() { + return batchValues.size(); + } + }); + } + + private int[] doExecuteBatchUpdateWithNamedParameters(String sql, final SqlParameterSource[] batchArgs) { + if (batchArgs.length <= 0) { + return new int[] {0}; + } + final ParsedSql parsedSql = NamedParameterUtils.parseSqlStatement(sql); + String sqlToUse = NamedParameterUtils.substituteNamedParameters(parsedSql, batchArgs[0]); + return getJdbcOperations().batchUpdate( + sqlToUse, + new BatchPreparedStatementSetter() { + + public void setValues(PreparedStatement ps, int i) throws SQLException { + Object[] values = NamedParameterUtils.buildValueArray(parsedSql, batchArgs[i], null); + int[] columnTypes = NamedParameterUtils.buildSqlTypeArray(parsedSql, batchArgs[i]); + doSetStatementParameters(values, ps, columnTypes); + } + + public int getBatchSize() { + return batchArgs.length; + } + }); + } + + private void doSetStatementParameters(Object[] values, PreparedStatement ps, int[] columnTypes) throws SQLException { + int colIndex = 0; + for (Object value : values) { + colIndex++; + if (value instanceof SqlParameterValue) { + SqlParameterValue paramValue = (SqlParameterValue) value; + StatementCreatorUtils.setParameterValue(ps, colIndex, paramValue, paramValue.getValue()); + } + else { + int colType; + if (columnTypes == null || columnTypes.length < colIndex) { + colType = SqlTypeValue.TYPE_UNKNOWN; + } + else { + colType = columnTypes[colIndex - 1]; + } + StatementCreatorUtils.setParameterValue(ps, colIndex, colType, value); + } + } + } + + + /** + * Considers an Object array passed into a varargs parameter as + * collection of arguments rather than as single argument. + */ + private Object[] getArguments(Object[] varArgs) { + if (varArgs.length == 1 && varArgs[0] instanceof Object[]) { + return (Object[]) varArgs[0]; + } + else { + return varArgs; + } + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/simple/package.html b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/simple/package.html new file mode 100644 index 0000000000..8c4bfb4bbb --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/simple/package.html @@ -0,0 +1,17 @@ + + + +Simplification layer over JdbcTemplate for Java 5 and above. + +

SimpleJdbcTemplate is a wrapper around JdbcTemplate that takes advantage +of varargs and autoboxing. It also offers only a subset of the methods +available on JdbcTemplate: Hence, it does not implement the JdbcOperations +interface or extend JdbcTemplate, but implements the dedicated +SimpleJdbcOperations interface. + +

If you need the full power of Spring JDBC for less common operations, +use the getJdbcOperations() method of SimpleJdbcTemplate and work +with the returned classic template, or use a JdbcTemplate instance directly. + + + diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/support/AbstractInterruptibleBatchPreparedStatementSetter.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/support/AbstractInterruptibleBatchPreparedStatementSetter.java new file mode 100644 index 0000000000..64b115d3fd --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/support/AbstractInterruptibleBatchPreparedStatementSetter.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc.core.support; + +import java.sql.PreparedStatement; +import java.sql.SQLException; + +import org.springframework.jdbc.core.InterruptibleBatchPreparedStatementSetter; + +/** + * Abstract implementation of the {@link InterruptibleBatchPreparedStatementSetter} + * interface, combining the check for available values and setting of those + * into a single callback method {@link #setValuesIfAvailable}. + * + * @author Juergen Hoeller + * @since 2.0 + * @see #setValuesIfAvailable + */ +public abstract class AbstractInterruptibleBatchPreparedStatementSetter + implements InterruptibleBatchPreparedStatementSetter { + + private boolean exhausted; + + + /** + * This implementation calls {@link #setValuesIfAvailable} + * and sets this instance's exhaustion flag accordingly. + */ + public final void setValues(PreparedStatement ps, int i) throws SQLException { + this.exhausted = !setValuesIfAvailable(ps, i); + } + + /** + * This implementation return this instance's current exhaustion flag. + */ + public final boolean isBatchExhausted(int i) { + return this.exhausted; + } + + /** + * This implementation returns Integer.MAX_VALUE. + * Can be overridden in subclasses to lower the maximum batch size. + */ + public int getBatchSize() { + return Integer.MAX_VALUE; + } + + + /** + * Check for available values and set them on the given PreparedStatement. + * If no values are available anymore, return false. + * @param ps PreparedStatement we'll invoke setter methods on + * @param i index of the statement we're issuing in the batch, starting from 0 + * @return whether there were values to apply (that is, whether the applied + * parameters should be added to the batch and this method should be called + * for a further iteration) + * @throws SQLException if a SQLException is encountered + * (i.e. there is no need to catch SQLException) + */ + protected abstract boolean setValuesIfAvailable(PreparedStatement ps, int i) throws SQLException; + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/support/AbstractLobCreatingPreparedStatementCallback.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/support/AbstractLobCreatingPreparedStatementCallback.java new file mode 100644 index 0000000000..0ad480049f --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/support/AbstractLobCreatingPreparedStatementCallback.java @@ -0,0 +1,89 @@ +/* + * Copyright 2002-2005 the original author or authors. + * + * Licensed 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.springframework.jdbc.core.support; + +import java.sql.PreparedStatement; +import java.sql.SQLException; + +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.core.PreparedStatementCallback; +import org.springframework.jdbc.support.lob.LobCreator; +import org.springframework.jdbc.support.lob.LobHandler; + +/** + * Abstract PreparedStatementCallback implementation that manages a LobCreator. + * Typically used as inner class, with access to surrounding method arguments. + * + *

Delegates to the setValues template method for setting values + * on the PreparedStatement, using a given LobCreator for BLOB/CLOB arguments. + * + *

A usage example with JdbcTemplate: + * + *

JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);  // reusable object
+ * LobHandler lobHandler = new DefaultLobHandler();  // reusable object
+ *
+ * jdbcTemplate.execute(
+ *     "INSERT INTO imagedb (image_name, content, description) VALUES (?, ?, ?)",
+ *     new AbstractLobCreatingPreparedStatementCallback(lobHandler) {
+ *       protected void setValues(PreparedStatement ps, LobCreator lobCreator) throws SQLException {
+ *         ps.setString(1, name);
+ *         lobCreator.setBlobAsBinaryStream(ps, 2, contentStream, contentLength);
+ *         lobCreator.setClobAsString(ps, 3, description);
+ *       }
+ *     }
+ * );
+ * + * @author Juergen Hoeller + * @since 1.0.2 + * @see org.springframework.jdbc.support.lob.LobCreator + */ +public abstract class AbstractLobCreatingPreparedStatementCallback implements PreparedStatementCallback { + + private final LobHandler lobHandler; + + /** + * Create a new AbstractLobCreatingPreparedStatementCallback for the + * given LobHandler. + * @param lobHandler the LobHandler to create LobCreators with + */ + public AbstractLobCreatingPreparedStatementCallback(LobHandler lobHandler) { + this.lobHandler = lobHandler; + } + + public final Object doInPreparedStatement(PreparedStatement ps) throws SQLException, DataAccessException { + LobCreator lobCreator = this.lobHandler.getLobCreator(); + try { + setValues(ps, lobCreator); + return new Integer(ps.executeUpdate()); + } + finally { + lobCreator.close(); + } + } + + /** + * Set values on the given PreparedStatement, using the given + * LobCreator for BLOB/CLOB arguments. + * @param ps the PreparedStatement to use + * @param lobCreator the LobCreator to use + * @throws SQLException if thrown by JDBC methods + * @throws DataAccessException in case of custom exceptions + */ + protected abstract void setValues(PreparedStatement ps, LobCreator lobCreator) + throws SQLException, DataAccessException; + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/support/AbstractLobStreamingResultSetExtractor.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/support/AbstractLobStreamingResultSetExtractor.java new file mode 100644 index 0000000000..df22236518 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/support/AbstractLobStreamingResultSetExtractor.java @@ -0,0 +1,120 @@ +/* + * Copyright 2002-2005 the original author or authors. + * + * Licensed 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.springframework.jdbc.core.support; + +import java.io.IOException; +import java.sql.ResultSet; +import java.sql.SQLException; + +import org.springframework.dao.DataAccessException; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.dao.IncorrectResultSizeDataAccessException; +import org.springframework.jdbc.LobRetrievalFailureException; +import org.springframework.jdbc.core.ResultSetExtractor; + +/** + * Abstract ResultSetExtractor implementation that assumes streaming of LOB data. + * Typically used as inner class, with access to surrounding method arguments. + * + *

Delegates to the streamData template method for streaming LOB + * content to some OutputStream, typically using a LobHandler. Converts an + * IOException thrown during streaming to a LobRetrievalFailureException. + * + *

A usage example with JdbcTemplate: + * + *

JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);  // reusable object
+ * final LobHandler lobHandler = new DefaultLobHandler();  // reusable object
+ *
+ * jdbcTemplate.query(
+ *		 "SELECT content FROM imagedb WHERE image_name=?", new Object[] {name},
+ *		 new AbstractLobStreamingResultSetExtractor() {
+ *			 public void streamData(ResultSet rs) throws SQLException, IOException {
+ *				 FileCopyUtils.copy(lobHandler.getBlobAsBinaryStream(rs, 1), contentStream);
+ *			 }
+ *		 }
+ * );
+ * + * @author Juergen Hoeller + * @since 1.0.2 + * @see org.springframework.jdbc.support.lob.LobHandler + * @see org.springframework.jdbc.LobRetrievalFailureException + */ +public abstract class AbstractLobStreamingResultSetExtractor implements ResultSetExtractor { + + /** + * Delegates to handleNoRowFound, handleMultipleRowsFound and streamData, + * according to the ResultSet state. Converts an IOException thrown by + * streamData to a LobRetrievalFailureException. + * @see #handleNoRowFound + * @see #handleMultipleRowsFound + * @see #streamData + * @see org.springframework.jdbc.LobRetrievalFailureException + */ + public final Object extractData(ResultSet rs) throws SQLException, DataAccessException { + if (!rs.next()) { + handleNoRowFound(); + } + else { + try { + streamData(rs); + if (rs.next()) { + handleMultipleRowsFound(); + } + } + catch (IOException ex) { + throw new LobRetrievalFailureException("Couldn't stream LOB content", ex); + } + } + return null; + } + + /** + * Handle the case where the ResultSet does not contain a row. + * @throws DataAccessException a corresponding exception, + * by default an EmptyResultDataAccessException + * @see org.springframework.dao.EmptyResultDataAccessException + */ + protected void handleNoRowFound() throws DataAccessException { + throw new EmptyResultDataAccessException( + "LobStreamingResultSetExtractor did not find row in database", 1); + } + + /** + * Handle the case where the ResultSet contains multiple rows. + * @throws DataAccessException a corresponding exception, + * by default an IncorrectResultSizeDataAccessException + * @see org.springframework.dao.IncorrectResultSizeDataAccessException + */ + protected void handleMultipleRowsFound() throws DataAccessException { + throw new IncorrectResultSizeDataAccessException( + "LobStreamingResultSetExtractor found multiple rows in database", 1); + } + + /** + * Stream LOB content from the given ResultSet to some OutputStream. + *

Typically used as inner class, with access to surrounding method arguments + * and to a LobHandler instance variable of the surrounding class. + * @param rs the ResultSet to take the LOB content from + * @throws SQLException if thrown by JDBC methods + * @throws IOException if thrown by stream access methods + * @throws DataAccessException in case of custom exceptions + * @see org.springframework.jdbc.support.lob.LobHandler#getBlobAsBinaryStream + * @see org.springframework.util.FileCopyUtils + */ + protected abstract void streamData(ResultSet rs) throws SQLException, IOException, DataAccessException; + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/support/AbstractSqlTypeValue.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/support/AbstractSqlTypeValue.java new file mode 100644 index 0000000000..0f5a808ad9 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/support/AbstractSqlTypeValue.java @@ -0,0 +1,79 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed 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.springframework.jdbc.core.support; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +import org.springframework.jdbc.core.SqlTypeValue; + +/** + * Abstract implementation of the SqlTypeValue interface, for convenient + * creation of type values that are supposed to be passed into the + * PreparedStatement.setObject method. The createTypeValue + * callback method has access to the underlying Connection, if that should + * be needed to create any database-specific objects. + * + *

A usage example from a StoredProcedure (compare this to the plain + * SqlTypeValue version in the superclass javadoc): + * + *

proc.declareParameter(new SqlParameter("myarray", Types.ARRAY, "NUMBERS"));
+ * ...
+ *
+ * Map in = new HashMap();
+ * in.put("myarray", new AbstractSqlTypeValue() {
+ *   public Object createTypeValue(Connection con, int sqlType, String typeName) throws SQLException {
+ *	   oracle.sql.ArrayDescriptor desc = new oracle.sql.ArrayDescriptor(typeName, con);
+ *	   return new oracle.sql.ARRAY(desc, con, seats);
+ *   }
+ * });
+ * Map out = execute(in);
+ * 
+ * + * @author Juergen Hoeller + * @since 1.1 + * @see java.sql.PreparedStatement#setObject(int, Object, int) + * @see org.springframework.jdbc.object.StoredProcedure + */ +public abstract class AbstractSqlTypeValue implements SqlTypeValue { + + public final void setTypeValue(PreparedStatement ps, int paramIndex, int sqlType, String typeName) + throws SQLException { + + Object value = createTypeValue(ps.getConnection(), sqlType, typeName); + if (sqlType == TYPE_UNKNOWN) { + ps.setObject(paramIndex, value); + } + else { + ps.setObject(paramIndex, value, sqlType); + } + } + + /** + * Create the type value to be passed into PreparedStatement.setObject. + * @param con the JDBC Connection, if needed to create any database-specific objects + * @param sqlType SQL type of the parameter we are setting + * @param typeName the type name of the parameter + * @return the type value + * @throws SQLException if a SQLException is encountered setting + * parameter values (that is, there's no need to catch SQLException) + * @see java.sql.PreparedStatement#setObject(int, Object, int) + */ + protected abstract Object createTypeValue(Connection con, int sqlType, String typeName) throws SQLException; + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/support/JdbcBeanDefinitionReader.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/support/JdbcBeanDefinitionReader.java new file mode 100644 index 0000000000..9eb0fa9b93 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/support/JdbcBeanDefinitionReader.java @@ -0,0 +1,121 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed 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.springframework.jdbc.core.support; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Properties; + +import javax.sql.DataSource; + +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.PropertiesBeanDefinitionReader; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowCallbackHandler; +import org.springframework.util.Assert; + +/** + * Bean definition reader that reads values from a database table, + * based on a given SQL statement. + * + *

Expects columns for bean name, property name and value as String. + * Formats for each are identical to the properties format recognized + * by PropertiesBeanDefinitionReader. + * + *

NOTE: This is mainly intended as an example for a custom + * JDBC-based bean definition reader. It does not aim to offer + * comprehensive functionality. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see #loadBeanDefinitions + * @see org.springframework.beans.factory.support.PropertiesBeanDefinitionReader + */ +public class JdbcBeanDefinitionReader { + + private final PropertiesBeanDefinitionReader propReader; + + private JdbcTemplate jdbcTemplate; + + + /** + * Create a new JdbcBeanDefinitionReader for the given bean factory, + * using a default PropertiesBeanDefinitionReader underneath. + *

DataSource or JdbcTemplate still need to be set. + * @see #setDataSource + * @see #setJdbcTemplate + */ + public JdbcBeanDefinitionReader(BeanDefinitionRegistry beanFactory) { + this.propReader = new PropertiesBeanDefinitionReader(beanFactory); + } + + /** + * Create a new JdbcBeanDefinitionReader that delegates to the + * given PropertiesBeanDefinitionReader underneath. + *

DataSource or JdbcTemplate still need to be set. + * @see #setDataSource + * @see #setJdbcTemplate + */ + public JdbcBeanDefinitionReader(PropertiesBeanDefinitionReader beanDefinitionReader) { + Assert.notNull(beanDefinitionReader, "Bean definition reader must not be null"); + this.propReader = beanDefinitionReader; + } + + + /** + * Set the DataSource to use to obtain database connections. + * Will implicitly create a new JdbcTemplate with the given DataSource. + */ + public void setDataSource(DataSource dataSource) { + this.jdbcTemplate = new JdbcTemplate(dataSource); + } + + /** + * Set the JdbcTemplate to be used by this bean factory. + * Contains settings for DataSource, SQLExceptionTranslator, NativeJdbcExtractor, etc. + */ + public void setJdbcTemplate(JdbcTemplate jdbcTemplate) { + Assert.notNull(jdbcTemplate, "JdbcTemplate must not be null"); + this.jdbcTemplate = jdbcTemplate; + } + + + /** + * Load bean definitions from the database via the given SQL string. + * @param sql SQL query to use for loading bean definitions. + * The first three columns must be bean name, property name and value. + * Any join and any other columns are permitted: e.g. + * SELECT BEAN_NAME, PROPERTY, VALUE FROM CONFIG WHERE CONFIG.APP_ID = 1 + * It's also possible to perform a join. Column names are not significant -- + * only the ordering of these first three columns. + */ + public void loadBeanDefinitions(String sql) { + Assert.notNull(this.jdbcTemplate, "Not fully configured - specify DataSource or JdbcTemplate"); + final Properties props = new Properties(); + this.jdbcTemplate.query(sql, new RowCallbackHandler() { + public void processRow(ResultSet rs) throws SQLException { + String beanName = rs.getString(1); + String property = rs.getString(2); + String value = rs.getString(3); + // Make a properties entry by combining bean name and property. + props.setProperty(beanName + "." + property, value); + } + }); + this.propReader.registerBeanDefinitions(props); + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/support/JdbcDaoSupport.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/support/JdbcDaoSupport.java new file mode 100644 index 0000000000..16f2846a98 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/support/JdbcDaoSupport.java @@ -0,0 +1,145 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.core.support; + +import java.sql.Connection; + +import javax.sql.DataSource; + +import org.springframework.dao.support.DaoSupport; +import org.springframework.jdbc.CannotGetJdbcConnectionException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.datasource.DataSourceUtils; +import org.springframework.jdbc.support.SQLExceptionTranslator; + +/** + * Convenient super class for JDBC-based data access objects. + * + *

Requires a {@link javax.sql.DataSource} to be set, providing a + * {@link org.springframework.jdbc.core.JdbcTemplate} based on it to + * subclasses through the {@link #getJdbcTemplate()} method. + * + *

This base class is mainly intended for JdbcTemplate usage but can + * also be used when working with a Connection directly or when using + * org.springframework.jdbc.object operation objects. + * + * @author Juergen Hoeller + * @since 28.07.2003 + * @see #setDataSource + * @see #getJdbcTemplate + * @see org.springframework.jdbc.core.JdbcTemplate + */ +public abstract class JdbcDaoSupport extends DaoSupport { + + private JdbcTemplate jdbcTemplate; + + + /** + * Set the JDBC DataSource to be used by this DAO. + */ + public final void setDataSource(DataSource dataSource) { + if (this.jdbcTemplate == null || dataSource != this.jdbcTemplate.getDataSource()) { + this.jdbcTemplate = createJdbcTemplate(dataSource); + initTemplateConfig(); + } + } + + /** + * Create a JdbcTemplate for the given DataSource. + * Only invoked if populating the DAO with a DataSource reference! + *

Can be overridden in subclasses to provide a JdbcTemplate instance + * with different configuration, or a custom JdbcTemplate subclass. + * @param dataSource the JDBC DataSource to create a JdbcTemplate for + * @return the new JdbcTemplate instance + * @see #setDataSource + */ + protected JdbcTemplate createJdbcTemplate(DataSource dataSource) { + return new JdbcTemplate(dataSource); + } + + /** + * Return the JDBC DataSource used by this DAO. + */ + public final DataSource getDataSource() { + return (this.jdbcTemplate != null ? this.jdbcTemplate.getDataSource() : null); + } + + /** + * Set the JdbcTemplate for this DAO explicitly, + * as an alternative to specifying a DataSource. + */ + public final void setJdbcTemplate(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + initTemplateConfig(); + } + + /** + * Return the JdbcTemplate for this DAO, + * pre-initialized with the DataSource or set explicitly. + */ + public final JdbcTemplate getJdbcTemplate() { + return this.jdbcTemplate; + } + + /** + * Initialize the template-based configuration of this DAO. + * Called after a new JdbcTemplate has been set, either directly + * or through a DataSource. + *

This implementation is empty. Subclasses may override this + * to configure further objects based on the JdbcTemplate. + * @see #getJdbcTemplate() + */ + protected void initTemplateConfig() { + } + + protected void checkDaoConfig() { + if (this.jdbcTemplate == null) { + throw new IllegalArgumentException("'dataSource' or 'jdbcTemplate' is required"); + } + } + + + /** + * Return the SQLExceptionTranslator of this DAO's JdbcTemplate, + * for translating SQLExceptions in custom JDBC access code. + * @see org.springframework.jdbc.core.JdbcTemplate#getExceptionTranslator() + */ + protected final SQLExceptionTranslator getExceptionTranslator() { + return getJdbcTemplate().getExceptionTranslator(); + } + + /** + * Get a JDBC Connection, either from the current transaction or a new one. + * @return the JDBC Connection + * @throws CannotGetJdbcConnectionException if the attempt to get a Connection failed + * @see org.springframework.jdbc.datasource.DataSourceUtils#getConnection(javax.sql.DataSource) + */ + protected final Connection getConnection() throws CannotGetJdbcConnectionException { + return DataSourceUtils.getConnection(getDataSource()); + } + + /** + * Close the given JDBC Connection, created via this DAO's DataSource, + * if it isn't bound to the thread. + * @param con Connection to close + * @see org.springframework.jdbc.datasource.DataSourceUtils#releaseConnection + */ + protected final void releaseConnection(Connection con) { + DataSourceUtils.releaseConnection(con, getDataSource()); + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/support/SqlLobValue.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/support/SqlLobValue.java new file mode 100644 index 0000000000..4921a84b6d --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/support/SqlLobValue.java @@ -0,0 +1,215 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed 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.springframework.jdbc.core.support; + +import java.io.InputStream; +import java.io.Reader; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Types; + +import org.springframework.jdbc.core.DisposableSqlTypeValue; +import org.springframework.jdbc.support.lob.DefaultLobHandler; +import org.springframework.jdbc.support.lob.LobCreator; +import org.springframework.jdbc.support.lob.LobHandler; + +/** + * Object to represent an SQL BLOB/CLOB value parameter. BLOBs can either be an + * InputStream or a byte array. CLOBs can be in the form of a Reader, InputStream + * or String. Each CLOB/BLOB value will be stored together with its length. + * The type is based on which constructor is used. Objects of this class are + * immutable except for the LobCreator reference. Use them and discard them. + * + *

This class holds a reference to a LocCreator that must be closed after the + * update has completed. This is done via a call to the closeLobCreator method. + * All handling of the LobCreator is done by the framework classes that use it - + * no need to set or close the LobCreator for end users of this class. + * + *

A usage example: + * + *

JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);  // reusable object
+ * LobHandler lobHandler = new DefaultLobHandler();  // reusable object
+ *
+ * jdbcTemplate.update(
+ *     "INSERT INTO imagedb (image_name, content, description) VALUES (?, ?, ?)",
+ *     new Object[] {
+ *       name,
+ *       new SqlLobValue(contentStream, contentLength, lobHandler),
+ *       new SqlLobValue(description, lobHandler)
+ *     },
+ *     new int[] {Types.VARCHAR, Types.BLOB, Types.CLOB});
+ * 
+ * + * @author Thomas Risberg + * @author Juergen Hoeller + * @since 1.1 + * @see org.springframework.jdbc.support.lob.LobHandler + * @see org.springframework.jdbc.support.lob.LobCreator + * @see org.springframework.jdbc.core.JdbcTemplate#update(String, Object[], int[]) + * @see org.springframework.jdbc.object.SqlUpdate#update(Object[]) + * @see org.springframework.jdbc.object.StoredProcedure#execute(java.util.Map) + */ +public class SqlLobValue implements DisposableSqlTypeValue { + + private final Object content; + + private final int length; + + /** + * This contains a reference to the LobCreator - so we can close it + * once the update is done. + */ + private final LobCreator lobCreator; + + + /** + * Create a new BLOB value with the given byte array, + * using a DefaultLobHandler. + * @param bytes the byte array containing the BLOB value + * @see org.springframework.jdbc.support.lob.DefaultLobHandler + */ + public SqlLobValue(byte[] bytes) { + this(bytes, new DefaultLobHandler()); + } + + /** + * Create a new BLOB value with the given byte array. + * @param bytes the byte array containing the BLOB value + * @param lobHandler the LobHandler to be used + */ + public SqlLobValue(byte[] bytes, LobHandler lobHandler) { + this.content = bytes; + this.length = (bytes != null ? bytes.length : 0); + this.lobCreator = lobHandler.getLobCreator(); + } + + /** + * Create a new CLOB value with the given content string, + * using a DefaultLobHandler. + * @param content the String containing the CLOB value + * @see org.springframework.jdbc.support.lob.DefaultLobHandler + */ + public SqlLobValue(String content) { + this(content, new DefaultLobHandler()); + } + + /** + * Create a new CLOB value with the given content string. + * @param content the String containing the CLOB value + * @param lobHandler the LobHandler to be used + */ + public SqlLobValue(String content, LobHandler lobHandler) { + this.content = content; + this.length = (content != null ? content.length() : 0); + this.lobCreator = lobHandler.getLobCreator(); + } + + /** + * Create a new BLOB/CLOB value with the given stream, + * using a DefaultLobHandler. + * @param stream the stream containing the LOB value + * @param length the length of the LOB value + * @see org.springframework.jdbc.support.lob.DefaultLobHandler + */ + public SqlLobValue(InputStream stream, int length) { + this(stream, length, new DefaultLobHandler()); + } + + /** + * Create a new BLOB/CLOB value with the given stream. + * @param stream the stream containing the LOB value + * @param length the length of the LOB value + * @param lobHandler the LobHandler to be used + */ + public SqlLobValue(InputStream stream, int length, LobHandler lobHandler) { + this.content = stream; + this.length = length; + this.lobCreator = lobHandler.getLobCreator(); + } + + /** + * Create a new CLOB value with the given character stream, + * using a DefaultLobHandler. + * @param reader the character stream containing the CLOB value + * @param length the length of the CLOB value + * @see org.springframework.jdbc.support.lob.DefaultLobHandler + */ + public SqlLobValue(Reader reader, int length) { + this(reader, length, new DefaultLobHandler()); + } + + /** + * Create a new CLOB value with the given character stream. + * @param reader the character stream containing the CLOB value + * @param length the length of the CLOB value + * @param lobHandler the LobHandler to be used + */ + public SqlLobValue(Reader reader, int length, LobHandler lobHandler) { + this.content = reader; + this.length = length; + this.lobCreator = lobHandler.getLobCreator(); + } + + + /** + * Set the specified content via the LobCreator. + */ + public void setTypeValue(PreparedStatement ps, int paramIndex, int sqlType, String typeName) + throws SQLException { + if (sqlType == Types.BLOB) { + if (this.content instanceof byte[] || this.content == null) { + this.lobCreator.setBlobAsBytes(ps, paramIndex, (byte[]) this.content); + } + else if (this.content instanceof String) { + this.lobCreator.setBlobAsBytes(ps, paramIndex, ((String) this.content).getBytes()); + } + else if (this.content instanceof InputStream) { + this.lobCreator.setBlobAsBinaryStream(ps, paramIndex, (InputStream) this.content, this.length); + } + else { + throw new IllegalArgumentException( + "Content type [" + this.content.getClass().getName() + "] not supported for BLOB columns"); + } + } + else if (sqlType == Types.CLOB) { + if (this.content instanceof String || this.content == null) { + this.lobCreator.setClobAsString(ps, paramIndex, (String) this.content); + } + else if (this.content instanceof InputStream) { + this.lobCreator.setClobAsAsciiStream(ps, paramIndex, (InputStream) this.content, this.length); + } + else if (this.content instanceof Reader) { + this.lobCreator.setClobAsCharacterStream(ps, paramIndex, (Reader) this.content, this.length); + } + else { + throw new IllegalArgumentException( + "Content type [" + this.content.getClass().getName() + "] not supported for CLOB columns"); + } + } + else { + throw new IllegalArgumentException("SqlLobValue only supports SQL types BLOB and CLOB"); + } + } + + /** + * Close the LobCreator, if any. + */ + public void cleanup() { + this.lobCreator.close(); + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/support/package.html b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/support/package.html new file mode 100644 index 0000000000..f478757b22 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/core/support/package.html @@ -0,0 +1,8 @@ + + + +Classes supporting the org.springframework.jdbc.core package. +Contains a DAO base class for JdbcTemplate usage. + + + diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/AbstractDataSource.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/AbstractDataSource.java new file mode 100644 index 0000000000..813b6ea0c6 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/AbstractDataSource.java @@ -0,0 +1,93 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc.datasource; + +import java.io.PrintWriter; +import java.sql.SQLException; + +import javax.sql.DataSource; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.util.Assert; + +/** + * Abstract base class for Spring's {@link javax.sql.DataSource} + * implementations, taking care of the padding. + * + *

'Padding' in the context of this class means default implementations + * for certain methods from the DataSource interface, such as + * {@link #getLoginTimeout()}, {@link #setLoginTimeout(int)}, and so forth. + * + * @author Juergen Hoeller + * @since 07.05.2003 + * @see DriverManagerDataSource + */ +public abstract class AbstractDataSource implements DataSource { + + /** Logger available to subclasses */ + protected final Log logger = LogFactory.getLog(getClass()); + + + /** + * Returns 0, indicating the default system timeout is to be used. + */ + public int getLoginTimeout() throws SQLException { + return 0; + } + + /** + * Setting a login timeout is not supported. + */ + public void setLoginTimeout(int timeout) throws SQLException { + throw new UnsupportedOperationException("setLoginTimeout"); + } + + /** + * LogWriter methods are not supported. + */ + public PrintWriter getLogWriter() { + throw new UnsupportedOperationException("getLogWriter"); + } + + /** + * LogWriter methods are not supported. + */ + public void setLogWriter(PrintWriter pw) throws SQLException { + throw new UnsupportedOperationException("setLogWriter"); + } + + + //--------------------------------------------------------------------- + // Implementation of JDBC 4.0's Wrapper interface + //--------------------------------------------------------------------- + + public Object unwrap(Class iface) throws SQLException { + Assert.notNull(iface, "Interface argument must not be null"); + if (!DataSource.class.equals(iface)) { + throw new SQLException("DataSource of type [" + getClass().getName() + + "] can only be unwrapped as [javax.sql.DataSource], not as [" + iface.getName()); + } + return this; + } + + public boolean isWrapperFor(Class iface) throws SQLException { + return DataSource.class.equals(iface); + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/AbstractDriverBasedDataSource.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/AbstractDriverBasedDataSource.java new file mode 100644 index 0000000000..06b142cc79 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/AbstractDriverBasedDataSource.java @@ -0,0 +1,161 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.datasource; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Properties; + +import org.springframework.util.Assert; + +/** + * Abstract base class for JDBC {@link javax.sql.DataSource} implementations + * that operate on a JDBC {@link java.sql.Driver}. + * + * @author Juergen Hoeller + * @since 2.5.5 + * @see SimpleDriverDataSource + * @see DriverManagerDataSource + */ +public abstract class AbstractDriverBasedDataSource extends AbstractDataSource { + + private String url; + + private String username; + + private String password; + + private Properties connectionProperties; + + + /** + * Set the JDBC URL to use for connecting through the Driver. + * @see java.sql.Driver#connect(String, java.util.Properties) + */ + public void setUrl(String url) { + Assert.hasText(url, "Property 'url' must not be empty"); + this.url = url.trim(); + } + + /** + * Return the JDBC URL to use for connecting through the Driver. + */ + public String getUrl() { + return this.url; + } + + /** + * Set the JDBC username to use for connecting through the Driver. + * @see java.sql.Driver#connect(String, java.util.Properties) + */ + public void setUsername(String username) { + this.username = username; + } + + /** + * Return the JDBC username to use for connecting through the Driver. + */ + public String getUsername() { + return this.username; + } + + /** + * Set the JDBC password to use for connecting through the Driver. + * @see java.sql.Driver#connect(String, java.util.Properties) + */ + public void setPassword(String password) { + this.password = password; + } + + /** + * Return the JDBC password to use for connecting through the Driver. + */ + public String getPassword() { + return this.password; + } + + /** + * Specify arbitrary connection properties as key/value pairs, + * to be passed to the Driver. + *

Can also contain "user" and "password" properties. However, + * any "username" and "password" bean properties specified on this + * DataSource will override the corresponding connection properties. + * @see java.sql.Driver#connect(String, java.util.Properties) + */ + public void setConnectionProperties(Properties connectionProperties) { + this.connectionProperties = connectionProperties; + } + + /** + * Return the connection properties to be passed to the Driver, if any. + */ + public Properties getConnectionProperties() { + return this.connectionProperties; + } + + + /** + * This implementation delegates to getConnectionFromDriver, + * using the default username and password of this DataSource. + * @see #getConnectionFromDriver(String, String) + * @see #setUsername + * @see #setPassword + */ + public Connection getConnection() throws SQLException { + return getConnectionFromDriver(getUsername(), getPassword()); + } + + /** + * This implementation delegates to getConnectionFromDriver, + * using the given username and password. + * @see #getConnectionFromDriver(String, String) + */ + public Connection getConnection(String username, String password) throws SQLException { + return getConnectionFromDriver(username, password); + } + + + /** + * Build properties for the Driver, including the given username and password (if any), + * and obtain a corresponding Connection. + * @param username the name of the user + * @param password the password to use + * @return the obtained Connection + * @throws SQLException in case of failure + * @see java.sql.Driver#connect(String, java.util.Properties) + */ + protected Connection getConnectionFromDriver(String username, String password) throws SQLException { + Properties props = new Properties(getConnectionProperties()); + if (username != null) { + props.setProperty("user", username); + } + if (password != null) { + props.setProperty("password", password); + } + return getConnectionFromDriver(props); + } + + /** + * Obtain a Connection using the given properties. + *

Template method to be implemented by subclasses. + * @param props the merged connection properties + * @return the obtained Connection + * @throws SQLException in case of failure + */ + protected abstract Connection getConnectionFromDriver(Properties props) throws SQLException; + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/ConnectionHandle.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/ConnectionHandle.java new file mode 100644 index 0000000000..4223dfed1a --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/ConnectionHandle.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2005 the original author or authors. + * + * Licensed 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.springframework.jdbc.datasource; + +import java.sql.Connection; + +/** + * Simple interface to be implemented by handles for a JDBC Connection. + * Used by JdoDialect, for example. + * + * @author Juergen Hoeller + * @since 1.1 + * @see SimpleConnectionHandle + * @see ConnectionHolder + * @see org.springframework.orm.jdo.JdoDialect#getJdbcConnection + */ +public interface ConnectionHandle { + + /** + * Fetch the JDBC Connection that this handle refers to. + */ + Connection getConnection(); + + /** + * Release the JDBC Connection that this handle refers to. + * @param con the JDBC Connection to release + */ + void releaseConnection(Connection con); + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/ConnectionHolder.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/ConnectionHolder.java new file mode 100644 index 0000000000..36fd35ec40 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/ConnectionHolder.java @@ -0,0 +1,205 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc.datasource; + +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Savepoint; + +import org.springframework.transaction.support.ResourceHolderSupport; +import org.springframework.util.Assert; + +/** + * Connection holder, wrapping a JDBC Connection. + * {@link DataSourceTransactionManager} binds instances of this class + * to the thread, for a specific DataSource. + * + *

Inherits rollback-only support for nested JDBC transactions + * and reference count functionality from the base class. + * + *

Note: This is an SPI class, not intended to be used by applications. + * + * @author Juergen Hoeller + * @since 06.05.2003 + * @see DataSourceTransactionManager + * @see DataSourceUtils + */ +public class ConnectionHolder extends ResourceHolderSupport { + + public static final String SAVEPOINT_NAME_PREFIX = "SAVEPOINT_"; + + + private ConnectionHandle connectionHandle; + + private Connection currentConnection; + + private boolean transactionActive = false; + + private Boolean savepointsSupported; + + private int savepointCounter = 0; + + + /** + * Create a new ConnectionHolder for the given ConnectionHandle. + * @param connectionHandle the ConnectionHandle to hold + */ + public ConnectionHolder(ConnectionHandle connectionHandle) { + Assert.notNull(connectionHandle, "ConnectionHandle must not be null"); + this.connectionHandle = connectionHandle; + } + + /** + * Create a new ConnectionHolder for the given JDBC Connection, + * wrapping it with a {@link SimpleConnectionHandle}, + * assuming that there is no ongoing transaction. + * @param connection the JDBC Connection to hold + * @see SimpleConnectionHandle + * @see #ConnectionHolder(java.sql.Connection, boolean) + */ + public ConnectionHolder(Connection connection) { + this.connectionHandle = new SimpleConnectionHandle(connection); + } + + /** + * Create a new ConnectionHolder for the given JDBC Connection, + * wrapping it with a {@link SimpleConnectionHandle}. + * @param connection the JDBC Connection to hold + * @param transactionActive whether the given Connection is involved + * in an ongoing transaction + * @see SimpleConnectionHandle + */ + public ConnectionHolder(Connection connection, boolean transactionActive) { + this(connection); + this.transactionActive = transactionActive; + } + + + /** + * Return the ConnectionHandle held by this ConnectionHolder. + */ + public ConnectionHandle getConnectionHandle() { + return this.connectionHandle; + } + + /** + * Return whether this holder currently has a Connection. + */ + protected boolean hasConnection() { + return (this.connectionHandle != null); + } + + /** + * Set whether this holder represents an active, JDBC-managed transaction. + * @see DataSourceTransactionManager + */ + protected void setTransactionActive(boolean transactionActive) { + this.transactionActive = transactionActive; + } + + /** + * Return whether this holder represents an active, JDBC-managed transaction. + */ + protected boolean isTransactionActive() { + return this.transactionActive; + } + + + /** + * Override the existing Connection handle with the given Connection. + * Reset the handle if given null. + *

Used for releasing the Connection on suspend (with a null + * argument) and setting a fresh Connection on resume. + */ + protected void setConnection(Connection connection) { + if (this.currentConnection != null) { + this.connectionHandle.releaseConnection(this.currentConnection); + this.currentConnection = null; + } + if (connection != null) { + this.connectionHandle = new SimpleConnectionHandle(connection); + } + else { + this.connectionHandle = null; + } + } + + /** + * Return the current Connection held by this ConnectionHolder. + *

This will be the same Connection until released + * gets called on the ConnectionHolder, which will reset the + * held Connection, fetching a new Connection on demand. + * @see ConnectionHandle#getConnection() + * @see #released() + */ + public Connection getConnection() { + Assert.notNull(this.connectionHandle, "Active Connection is required"); + if (this.currentConnection == null) { + this.currentConnection = this.connectionHandle.getConnection(); + } + return this.currentConnection; + } + + /** + * Return whether JDBC 3.0 Savepoints are supported. + * Caches the flag for the lifetime of this ConnectionHolder. + * @throws SQLException if thrown by the JDBC driver + */ + public boolean supportsSavepoints() throws SQLException { + if (this.savepointsSupported == null) { + this.savepointsSupported = new Boolean(getConnection().getMetaData().supportsSavepoints()); + } + return this.savepointsSupported.booleanValue(); + } + + /** + * Create a new JDBC 3.0 Savepoint for the current Connection, + * using generated savepoint names that are unique for the Connection. + * @return the new Savepoint + * @throws SQLException if thrown by the JDBC driver + */ + public Savepoint createSavepoint() throws SQLException { + this.savepointCounter++; + return getConnection().setSavepoint(SAVEPOINT_NAME_PREFIX + this.savepointCounter); + } + + /** + * Releases the current Connection held by this ConnectionHolder. + *

This is necessary for ConnectionHandles that expect "Connection borrowing", + * where each returned Connection is only temporarily leased and needs to be + * returned once the data operation is done, to make the Connection available + * for other operations within the same transaction. This is the case with + * JDO 2.0 DataStoreConnections, for example. + * @see org.springframework.orm.jdo.DefaultJdoDialect#getJdbcConnection + */ + public void released() { + super.released(); + if (this.currentConnection != null) { + this.connectionHandle.releaseConnection(this.currentConnection); + this.currentConnection = null; + } + } + + + public void clear() { + super.clear(); + this.transactionActive = false; + this.savepointsSupported = null; + this.savepointCounter = 0; + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/ConnectionProxy.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/ConnectionProxy.java new file mode 100644 index 0000000000..c18b51a783 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/ConnectionProxy.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.datasource; + +import java.sql.Connection; + +/** + * Subinterface of {@link java.sql.Connection} to be implemented by + * Connection proxies. Allows access to the underlying target Connection. + * + *

This interface can be checked when there is a need to cast to a + * native JDBC Connection such as Oracle's OracleConnection. Spring's + * {@link org.springframework.jdbc.support.nativejdbc.NativeJdbcExtractorAdapter} + * automatically detects such proxies before delegating to the actual + * unwrapping for a specific connection pool. + * + * @author Juergen Hoeller + * @since 1.1 + * @see TransactionAwareDataSourceProxy + * @see LazyConnectionDataSourceProxy + * @see DataSourceUtils#getTargetConnection(java.sql.Connection) + */ +public interface ConnectionProxy extends Connection { + + /** + * Return the target Connection of this proxy. + *

This will typically be the native driver Connection + * or a wrapper from a connection pool. + * @return the underlying Connection (never null) + */ + Connection getTargetConnection(); + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceTransactionManager.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceTransactionManager.java new file mode 100644 index 0000000000..7a6f045db6 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceTransactionManager.java @@ -0,0 +1,363 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc.datasource; + +import java.sql.Connection; +import java.sql.SQLException; + +import javax.sql.DataSource; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.transaction.CannotCreateTransactionException; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionSystemException; +import org.springframework.transaction.support.AbstractPlatformTransactionManager; +import org.springframework.transaction.support.DefaultTransactionStatus; +import org.springframework.transaction.support.ResourceTransactionManager; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +/** + * {@link org.springframework.transaction.PlatformTransactionManager} + * implementation for a single JDBC {@link javax.sql.DataSource}. This class is + * capable of working in any environment with any JDBC driver, as long as the setup + * uses a JDBC 2.0 Standard Extensions / JDBC 3.0 javax.sql.DataSource + * as its Connection factory mechanism. Binds a JDBC Connection from the specified + * DataSource to the current thread, potentially allowing for one thread-bound + * Connection per DataSource. + * + *

Note: The DataSource that this transaction manager operates on needs + * to return independent Connections. The Connections may come from a pool + * (the typical case), but the DataSource must not return thread-scoped / + * request-scoped Connections or the like. This transaction manager will + * associate Connections with thread-bound transactions itself, according + * to the specified propagation behavior. It assumes that a separate, + * independent Connection can be obtained even during an ongoing transaction. + * + *

Application code is required to retrieve the JDBC Connection via + * {@link DataSourceUtils#getConnection(DataSource)} instead of a standard + * J2EE-style {@link DataSource#getConnection()} call. Spring classes such as + * {@link org.springframework.jdbc.core.JdbcTemplate} use this strategy implicitly. + * If not used in combination with this transaction manager, the + * {@link DataSourceUtils} lookup strategy behaves exactly like the native + * DataSource lookup; it can thus be used in a portable fashion. + * + *

Alternatively, you can allow application code to work with the standard + * J2EE-style lookup pattern {@link DataSource#getConnection()}, for example for + * legacy code that is not aware of Spring at all. In that case, define a + * {@link TransactionAwareDataSourceProxy} for your target DataSource, and pass + * that proxy DataSource to your DAOs, which will automatically participate in + * Spring-managed transactions when accessing it. + * + *

Supports custom isolation levels, and timeouts which get applied as + * appropriate JDBC statement timeouts. To support the latter, application code + * must either use {@link org.springframework.jdbc.core.JdbcTemplate}, call + * {@link DataSourceUtils#applyTransactionTimeout} for each created JDBC Statement, + * or go through a {@link TransactionAwareDataSourceProxy} which will create + * timeout-aware JDBC Connections and Statements automatically. + * + *

Consider defining a {@link LazyConnectionDataSourceProxy} for your target + * DataSource, pointing both this transaction manager and your DAOs to it. + * This will lead to optimized handling of "empty" transactions, i.e. of transactions + * without any JDBC statements executed. A LazyConnectionDataSourceProxy will not fetch + * an actual JDBC Connection from the target DataSource until a Statement gets executed, + * lazily applying the specified transaction settings to the target Connection. + * + *

On JDBC 3.0, this transaction manager supports nested transactions via the + * JDBC 3.0 {@link java.sql.Savepoint} mechanism. The + * {@link #setNestedTransactionAllowed "nestedTransactionAllowed"} flag defaults + * to "true", since nested transactions will work without restrictions on JDBC + * drivers that support savepoints (such as the Oracle JDBC driver). + * + *

This transaction manager can be used as a replacement for the + * {@link org.springframework.transaction.jta.JtaTransactionManager} in the single + * resource case, as it does not require a container that supports JTA, typically + * in combination with a locally defined JDBC DataSource (e.g. a Jakarta Commons + * DBCP connection pool). Switching between this local strategy and a JTA + * environment is just a matter of configuration! + * + * @author Juergen Hoeller + * @since 02.05.2003 + * @see #setNestedTransactionAllowed + * @see java.sql.Savepoint + * @see DataSourceUtils#getConnection(javax.sql.DataSource) + * @see DataSourceUtils#applyTransactionTimeout + * @see DataSourceUtils#releaseConnection + * @see TransactionAwareDataSourceProxy + * @see LazyConnectionDataSourceProxy + * @see org.springframework.jdbc.core.JdbcTemplate + */ +public class DataSourceTransactionManager extends AbstractPlatformTransactionManager + implements ResourceTransactionManager, InitializingBean { + + private DataSource dataSource; + + + /** + * Create a new DataSourceTransactionManager instance. + * A DataSource has to be set to be able to use it. + * @see #setDataSource + */ + public DataSourceTransactionManager() { + setNestedTransactionAllowed(true); + } + + /** + * Create a new DataSourceTransactionManager instance. + * @param dataSource JDBC DataSource to manage transactions for + */ + public DataSourceTransactionManager(DataSource dataSource) { + this(); + setDataSource(dataSource); + afterPropertiesSet(); + } + + /** + * Set the JDBC DataSource that this instance should manage transactions for. + *

This will typically be a locally defined DataSource, for example a + * Jakarta Commons DBCP connection pool. Alternatively, you can also drive + * transactions for a non-XA J2EE DataSource fetched from JNDI. For an XA + * DataSource, use JtaTransactionManager. + *

The DataSource specified here should be the target DataSource to manage + * transactions for, not a TransactionAwareDataSourceProxy. Only data access + * code may work with TransactionAwareDataSourceProxy, while the transaction + * manager needs to work on the underlying target DataSource. If there's + * nevertheless a TransactionAwareDataSourceProxy passed in, it will be + * unwrapped to extract its target DataSource. + *

The DataSource passed in here needs to return independent Connections. + * The Connections may come from a pool (the typical case), but the DataSource + * must not return thread-scoped / request-scoped Connections or the like. + * @see TransactionAwareDataSourceProxy + * @see org.springframework.transaction.jta.JtaTransactionManager + */ + public void setDataSource(DataSource dataSource) { + if (dataSource instanceof TransactionAwareDataSourceProxy) { + // If we got a TransactionAwareDataSourceProxy, we need to perform transactions + // for its underlying target DataSource, else data access code won't see + // properly exposed transactions (i.e. transactions for the target DataSource). + this.dataSource = ((TransactionAwareDataSourceProxy) dataSource).getTargetDataSource(); + } + else { + this.dataSource = dataSource; + } + } + + /** + * Return the JDBC DataSource that this instance manages transactions for. + */ + public DataSource getDataSource() { + return this.dataSource; + } + + public void afterPropertiesSet() { + if (getDataSource() == null) { + throw new IllegalArgumentException("Property 'dataSource' is required"); + } + } + + + public Object getResourceFactory() { + return getDataSource(); + } + + protected Object doGetTransaction() { + DataSourceTransactionObject txObject = new DataSourceTransactionObject(); + txObject.setSavepointAllowed(isNestedTransactionAllowed()); + ConnectionHolder conHolder = + (ConnectionHolder) TransactionSynchronizationManager.getResource(this.dataSource); + txObject.setConnectionHolder(conHolder, false); + return txObject; + } + + protected boolean isExistingTransaction(Object transaction) { + DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction; + return (txObject.getConnectionHolder() != null && txObject.getConnectionHolder().isTransactionActive()); + } + + /** + * This implementation sets the isolation level but ignores the timeout. + */ + protected void doBegin(Object transaction, TransactionDefinition definition) { + DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction; + Connection con = null; + + try { + if (txObject.getConnectionHolder() == null || + txObject.getConnectionHolder().isSynchronizedWithTransaction()) { + Connection newCon = this.dataSource.getConnection(); + if (logger.isDebugEnabled()) { + logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction"); + } + txObject.setConnectionHolder(new ConnectionHolder(newCon), true); + } + + txObject.getConnectionHolder().setSynchronizedWithTransaction(true); + con = txObject.getConnectionHolder().getConnection(); + + Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition); + txObject.setPreviousIsolationLevel(previousIsolationLevel); + + // Switch to manual commit if necessary. This is very expensive in some JDBC drivers, + // so we don't want to do it unnecessarily (for example if we've explicitly + // configured the connection pool to set it already). + if (con.getAutoCommit()) { + txObject.setMustRestoreAutoCommit(true); + if (logger.isDebugEnabled()) { + logger.debug("Switching JDBC Connection [" + con + "] to manual commit"); + } + con.setAutoCommit(false); + } + txObject.getConnectionHolder().setTransactionActive(true); + + int timeout = determineTimeout(definition); + if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) { + txObject.getConnectionHolder().setTimeoutInSeconds(timeout); + } + + // Bind the session holder to the thread. + if (txObject.isNewConnectionHolder()) { + TransactionSynchronizationManager.bindResource(getDataSource(), txObject.getConnectionHolder()); + } + } + + catch (SQLException ex) { + DataSourceUtils.releaseConnection(con, this.dataSource); + throw new CannotCreateTransactionException("Could not open JDBC Connection for transaction", ex); + } + } + + protected Object doSuspend(Object transaction) { + DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction; + txObject.setConnectionHolder(null); + ConnectionHolder conHolder = (ConnectionHolder) + TransactionSynchronizationManager.unbindResource(this.dataSource); + return conHolder; + } + + protected void doResume(Object transaction, Object suspendedResources) { + ConnectionHolder conHolder = (ConnectionHolder) suspendedResources; + TransactionSynchronizationManager.bindResource(this.dataSource, conHolder); + } + + protected void doCommit(DefaultTransactionStatus status) { + DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction(); + Connection con = txObject.getConnectionHolder().getConnection(); + if (status.isDebug()) { + logger.debug("Committing JDBC transaction on Connection [" + con + "]"); + } + try { + con.commit(); + } + catch (SQLException ex) { + throw new TransactionSystemException("Could not commit JDBC transaction", ex); + } + } + + protected void doRollback(DefaultTransactionStatus status) { + DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction(); + Connection con = txObject.getConnectionHolder().getConnection(); + if (status.isDebug()) { + logger.debug("Rolling back JDBC transaction on Connection [" + con + "]"); + } + try { + con.rollback(); + } + catch (SQLException ex) { + throw new TransactionSystemException("Could not roll back JDBC transaction", ex); + } + } + + protected void doSetRollbackOnly(DefaultTransactionStatus status) { + DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction(); + if (status.isDebug()) { + logger.debug("Setting JDBC transaction [" + txObject.getConnectionHolder().getConnection() + + "] rollback-only"); + } + txObject.setRollbackOnly(); + } + + protected void doCleanupAfterCompletion(Object transaction) { + DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction; + + // Remove the connection holder from the thread, if exposed. + if (txObject.isNewConnectionHolder()) { + TransactionSynchronizationManager.unbindResource(this.dataSource); + } + + // Reset connection. + Connection con = txObject.getConnectionHolder().getConnection(); + try { + if (txObject.isMustRestoreAutoCommit()) { + con.setAutoCommit(true); + } + DataSourceUtils.resetConnectionAfterTransaction(con, txObject.getPreviousIsolationLevel()); + } + catch (Throwable ex) { + logger.debug("Could not reset JDBC Connection after transaction", ex); + } + + if (txObject.isNewConnectionHolder()) { + if (logger.isDebugEnabled()) { + logger.debug("Releasing JDBC Connection [" + con + "] after transaction"); + } + DataSourceUtils.releaseConnection(con, this.dataSource); + } + + txObject.getConnectionHolder().clear(); + } + + + /** + * DataSource transaction object, representing a ConnectionHolder. + * Used as transaction object by DataSourceTransactionManager. + */ + private static class DataSourceTransactionObject extends JdbcTransactionObjectSupport { + + private boolean newConnectionHolder; + + private boolean mustRestoreAutoCommit; + + public void setConnectionHolder(ConnectionHolder connectionHolder, boolean newConnectionHolder) { + super.setConnectionHolder(connectionHolder); + this.newConnectionHolder = newConnectionHolder; + } + + public boolean isNewConnectionHolder() { + return newConnectionHolder; + } + + public boolean hasTransaction() { + return (getConnectionHolder() != null && getConnectionHolder().isTransactionActive()); + } + + public void setMustRestoreAutoCommit(boolean mustRestoreAutoCommit) { + this.mustRestoreAutoCommit = mustRestoreAutoCommit; + } + + public boolean isMustRestoreAutoCommit() { + return mustRestoreAutoCommit; + } + + public void setRollbackOnly() { + getConnectionHolder().setRollbackOnly(); + } + + public boolean isRollbackOnly() { + return getConnectionHolder().isRollbackOnly(); + } + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java new file mode 100644 index 0000000000..c5016a4f14 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java @@ -0,0 +1,452 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.datasource; + +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; + +import javax.sql.DataSource; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.jdbc.CannotGetJdbcConnectionException; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.support.TransactionSynchronizationAdapter; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.util.Assert; + +/** + * Helper class that provides static methods for obtaining JDBC Connections from + * a {@link javax.sql.DataSource}. Includes special support for Spring-managed + * transactional Connections, e.g. managed by {@link DataSourceTransactionManager} + * or {@link org.springframework.transaction.jta.JtaTransactionManager}. + * + *

Used internally by Spring's {@link org.springframework.jdbc.core.JdbcTemplate}, + * Spring's JDBC operation objects and the JDBC {@link DataSourceTransactionManager}. + * Can also be used directly in application code. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see #getConnection + * @see #releaseConnection + * @see DataSourceTransactionManager + * @see org.springframework.transaction.jta.JtaTransactionManager + * @see org.springframework.transaction.support.TransactionSynchronizationManager + */ +public abstract class DataSourceUtils { + + /** + * Order value for TransactionSynchronization objects that clean up + * JDBC Connections. + */ + public static final int CONNECTION_SYNCHRONIZATION_ORDER = 1000; + + private static final Log logger = LogFactory.getLog(DataSourceUtils.class); + + + /** + * Obtain a Connection from the given DataSource. Translates SQLExceptions into + * the Spring hierarchy of unchecked generic data access exceptions, simplifying + * calling code and making any exception that is thrown more meaningful. + *

Is aware of a corresponding Connection bound to the current thread, for example + * when using {@link DataSourceTransactionManager}. Will bind a Connection to the + * thread if transaction synchronization is active, e.g. when running within a + * {@link org.springframework.transaction.jta.JtaTransactionManager JTA} transaction). + * @param dataSource the DataSource to obtain Connections from + * @return a JDBC Connection from the given DataSource + * @throws org.springframework.jdbc.CannotGetJdbcConnectionException + * if the attempt to get a Connection failed + * @see #releaseConnection + */ + public static Connection getConnection(DataSource dataSource) throws CannotGetJdbcConnectionException { + try { + return doGetConnection(dataSource); + } + catch (SQLException ex) { + throw new CannotGetJdbcConnectionException("Could not get JDBC Connection", ex); + } + } + + /** + * Actually obtain a JDBC Connection from the given DataSource. + * Same as {@link #getConnection}, but throwing the original SQLException. + *

Is aware of a corresponding Connection bound to the current thread, for example + * when using {@link DataSourceTransactionManager}. Will bind a Connection to the thread + * if transaction synchronization is active (e.g. if in a JTA transaction). + *

Directly accessed by {@link TransactionAwareDataSourceProxy}. + * @param dataSource the DataSource to obtain Connections from + * @return a JDBC Connection from the given DataSource + * @throws SQLException if thrown by JDBC methods + * @see #doReleaseConnection + */ + public static Connection doGetConnection(DataSource dataSource) throws SQLException { + Assert.notNull(dataSource, "No DataSource specified"); + + ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource); + if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) { + conHolder.requested(); + if (!conHolder.hasConnection()) { + logger.debug("Fetching resumed JDBC Connection from DataSource"); + conHolder.setConnection(dataSource.getConnection()); + } + return conHolder.getConnection(); + } + // Else we either got no holder or an empty thread-bound holder here. + + logger.debug("Fetching JDBC Connection from DataSource"); + Connection con = dataSource.getConnection(); + + if (TransactionSynchronizationManager.isSynchronizationActive()) { + logger.debug("Registering transaction synchronization for JDBC Connection"); + // Use same Connection for further JDBC actions within the transaction. + // Thread-bound object will get removed by synchronization at transaction completion. + ConnectionHolder holderToUse = conHolder; + if (holderToUse == null) { + holderToUse = new ConnectionHolder(con); + } + else { + holderToUse.setConnection(con); + } + holderToUse.requested(); + TransactionSynchronizationManager.registerSynchronization( + new ConnectionSynchronization(holderToUse, dataSource)); + holderToUse.setSynchronizedWithTransaction(true); + if (holderToUse != conHolder) { + TransactionSynchronizationManager.bindResource(dataSource, holderToUse); + } + } + + return con; + } + + /** + * Prepare the given Connection with the given transaction semantics. + * @param con the Connection to prepare + * @param definition the transaction definition to apply + * @return the previous isolation level, if any + * @throws SQLException if thrown by JDBC methods + * @see #resetConnectionAfterTransaction + */ + public static Integer prepareConnectionForTransaction(Connection con, TransactionDefinition definition) + throws SQLException { + + Assert.notNull(con, "No Connection specified"); + + // Set read-only flag. + if (definition != null && definition.isReadOnly()) { + try { + if (logger.isDebugEnabled()) { + logger.debug("Setting JDBC Connection [" + con + "] read-only"); + } + con.setReadOnly(true); + } + catch (Throwable ex) { + // SQLException or UnsupportedOperationException + // -> ignore, it's just a hint anyway. + logger.debug("Could not set JDBC Connection read-only", ex); + } + } + + // Apply specific isolation level, if any. + Integer previousIsolationLevel = null; + if (definition != null && definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT) { + if (logger.isDebugEnabled()) { + logger.debug("Changing isolation level of JDBC Connection [" + con + "] to " + + definition.getIsolationLevel()); + } + previousIsolationLevel = new Integer(con.getTransactionIsolation()); + con.setTransactionIsolation(definition.getIsolationLevel()); + } + + return previousIsolationLevel; + } + + /** + * Reset the given Connection after a transaction, + * regarding read-only flag and isolation level. + * @param con the Connection to reset + * @param previousIsolationLevel the isolation level to restore, if any + * @see #prepareConnectionForTransaction + */ + public static void resetConnectionAfterTransaction(Connection con, Integer previousIsolationLevel) { + Assert.notNull(con, "No Connection specified"); + try { + // Reset transaction isolation to previous value, if changed for the transaction. + if (previousIsolationLevel != null) { + if (logger.isDebugEnabled()) { + logger.debug("Resetting isolation level of JDBC Connection [" + + con + "] to " + previousIsolationLevel); + } + con.setTransactionIsolation(previousIsolationLevel.intValue()); + } + + // Reset read-only flag. + if (con.isReadOnly()) { + if (logger.isDebugEnabled()) { + logger.debug("Resetting read-only flag of JDBC Connection [" + con + "]"); + } + con.setReadOnly(false); + } + } + catch (Throwable ex) { + logger.debug("Could not reset JDBC Connection after transaction", ex); + } + } + + /** + * Determine whether the given JDBC Connection is transactional, that is, + * bound to the current thread by Spring's transaction facilities. + * @param con the Connection to check + * @param dataSource the DataSource that the Connection was obtained from + * (may be null) + * @return whether the Connection is transactional + */ + public static boolean isConnectionTransactional(Connection con, DataSource dataSource) { + if (dataSource == null) { + return false; + } + ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource); + return (conHolder != null && connectionEquals(conHolder, con)); + } + + /** + * Apply the current transaction timeout, if any, + * to the given JDBC Statement object. + * @param stmt the JDBC Statement object + * @param dataSource the DataSource that the Connection was obtained from + * @throws SQLException if thrown by JDBC methods + * @see java.sql.Statement#setQueryTimeout + */ + public static void applyTransactionTimeout(Statement stmt, DataSource dataSource) throws SQLException { + applyTimeout(stmt, dataSource, 0); + } + + /** + * Apply the specified timeout - overridden by the current transaction timeout, + * if any - to the given JDBC Statement object. + * @param stmt the JDBC Statement object + * @param dataSource the DataSource that the Connection was obtained from + * @param timeout the timeout to apply (or 0 for no timeout outside of a transaction) + * @throws SQLException if thrown by JDBC methods + * @see java.sql.Statement#setQueryTimeout + */ + public static void applyTimeout(Statement stmt, DataSource dataSource, int timeout) throws SQLException { + Assert.notNull(stmt, "No Statement specified"); + Assert.notNull(dataSource, "No DataSource specified"); + ConnectionHolder holder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource); + if (holder != null && holder.hasTimeout()) { + // Remaining transaction timeout overrides specified value. + stmt.setQueryTimeout(holder.getTimeToLiveInSeconds()); + } + else if (timeout > 0) { + // No current transaction timeout -> apply specified value. + stmt.setQueryTimeout(timeout); + } + } + + /** + * Close the given Connection, obtained from the given DataSource, + * if it is not managed externally (that is, not bound to the thread). + * @param con the Connection to close if necessary + * (if this is null, the call will be ignored) + * @param dataSource the DataSource that the Connection was obtained from + * (may be null) + * @see #getConnection + */ + public static void releaseConnection(Connection con, DataSource dataSource) { + try { + doReleaseConnection(con, dataSource); + } + catch (SQLException ex) { + logger.debug("Could not close JDBC Connection", ex); + } + catch (Throwable ex) { + logger.debug("Unexpected exception on closing JDBC Connection", ex); + } + } + + /** + * Actually close the given Connection, obtained from the given DataSource. + * Same as {@link #releaseConnection}, but throwing the original SQLException. + *

Directly accessed by {@link TransactionAwareDataSourceProxy}. + * @param con the Connection to close if necessary + * (if this is null, the call will be ignored) + * @param dataSource the DataSource that the Connection was obtained from + * (may be null) + * @throws SQLException if thrown by JDBC methods + * @see #doGetConnection + */ + public static void doReleaseConnection(Connection con, DataSource dataSource) throws SQLException { + if (con == null) { + return; + } + + if (dataSource != null) { + ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource); + if (conHolder != null && connectionEquals(conHolder, con)) { + // It's the transactional Connection: Don't close it. + conHolder.released(); + return; + } + } + + // Leave the Connection open only if the DataSource is our + // special SmartDataSoruce and it wants the Connection left open. + if (!(dataSource instanceof SmartDataSource) || ((SmartDataSource) dataSource).shouldClose(con)) { + logger.debug("Returning JDBC Connection to DataSource"); + con.close(); + } + } + + /** + * Determine whether the given two Connections are equal, asking the target + * Connection in case of a proxy. Used to detect equality even if the + * user passed in a raw target Connection while the held one is a proxy. + * @param conHolder the ConnectionHolder for the held Connection (potentially a proxy) + * @param passedInCon the Connection passed-in by the user + * (potentially a target Connection without proxy) + * @return whether the given Connections are equal + * @see #getTargetConnection + */ + private static boolean connectionEquals(ConnectionHolder conHolder, Connection passedInCon) { + if (!conHolder.hasConnection()) { + return false; + } + Connection heldCon = conHolder.getConnection(); + // Explicitly check for identity too: for Connection handles that do not implement + // "equals" properly, such as the ones Commons DBCP exposes). + return (heldCon == passedInCon || heldCon.equals(passedInCon) || + getTargetConnection(heldCon).equals(passedInCon)); + } + + /** + * Return the innermost target Connection of the given Connection. If the given + * Connection is a proxy, it will be unwrapped until a non-proxy Connection is + * found. Otherwise, the passed-in Connection will be returned as-is. + * @param con the Connection proxy to unwrap + * @return the innermost target Connection, or the passed-in one if no proxy + * @see ConnectionProxy#getTargetConnection() + */ + public static Connection getTargetConnection(Connection con) { + Connection conToUse = con; + while (conToUse instanceof ConnectionProxy) { + conToUse = ((ConnectionProxy) conToUse).getTargetConnection(); + } + return conToUse; + } + + /** + * Determine the connection synchronization order to use for the given + * DataSource. Decreased for every level of nesting that a DataSource + * has, checked through the level of DelegatingDataSource nesting. + * @param dataSource the DataSource to check + * @return the connection synchronization order to use + * @see #CONNECTION_SYNCHRONIZATION_ORDER + */ + private static int getConnectionSynchronizationOrder(DataSource dataSource) { + int order = CONNECTION_SYNCHRONIZATION_ORDER; + DataSource currDs = dataSource; + while (currDs instanceof DelegatingDataSource) { + order--; + currDs = ((DelegatingDataSource) currDs).getTargetDataSource(); + } + return order; + } + + + /** + * Callback for resource cleanup at the end of a non-native JDBC transaction + * (e.g. when participating in a JtaTransactionManager transaction). + * @see org.springframework.transaction.jta.JtaTransactionManager + */ + private static class ConnectionSynchronization extends TransactionSynchronizationAdapter { + + private final ConnectionHolder connectionHolder; + + private final DataSource dataSource; + + private int order; + + private boolean holderActive = true; + + public ConnectionSynchronization(ConnectionHolder connectionHolder, DataSource dataSource) { + this.connectionHolder = connectionHolder; + this.dataSource = dataSource; + this.order = getConnectionSynchronizationOrder(dataSource); + } + + public int getOrder() { + return this.order; + } + + public void suspend() { + if (this.holderActive) { + TransactionSynchronizationManager.unbindResource(this.dataSource); + if (this.connectionHolder.hasConnection() && !this.connectionHolder.isOpen()) { + // Release Connection on suspend if the application doesn't keep + // a handle to it anymore. We will fetch a fresh Connection if the + // application accesses the ConnectionHolder again after resume, + // assuming that it will participate in the same transaction. + releaseConnection(this.connectionHolder.getConnection(), this.dataSource); + this.connectionHolder.setConnection(null); + } + } + } + + public void resume() { + if (this.holderActive) { + TransactionSynchronizationManager.bindResource(this.dataSource, this.connectionHolder); + } + } + + public void beforeCompletion() { + // Release Connection early if the holder is not open anymore + // (that is, not used by another resource like a Hibernate Session + // that has its own cleanup via transaction synchronization), + // to avoid issues with strict JTA implementations that expect + // the close call before transaction completion. + if (!this.connectionHolder.isOpen()) { + TransactionSynchronizationManager.unbindResource(this.dataSource); + this.holderActive = false; + if (this.connectionHolder.hasConnection()) { + releaseConnection(this.connectionHolder.getConnection(), this.dataSource); + } + } + } + + public void afterCompletion(int status) { + // If we haven't closed the Connection in beforeCompletion, + // close it now. The holder might have been used for other + // cleanup in the meantime, for example by a Hibernate Session. + if (this.holderActive) { + // The thread-bound ConnectionHolder might not be available anymore, + // since afterCompletion might get called from a different thread. + TransactionSynchronizationManager.unbindResourceIfPossible(this.dataSource); + this.holderActive = false; + if (this.connectionHolder.hasConnection()) { + releaseConnection(this.connectionHolder.getConnection(), this.dataSource); + // Reset the ConnectionHolder: It might remain bound to the thread. + this.connectionHolder.setConnection(null); + } + } + this.connectionHolder.reset(); + } + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/DelegatingDataSource.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/DelegatingDataSource.java new file mode 100644 index 0000000000..5f2b77210a --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/DelegatingDataSource.java @@ -0,0 +1,120 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc.datasource; + +import java.io.PrintWriter; +import java.sql.Connection; +import java.sql.SQLException; + +import javax.sql.DataSource; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.util.Assert; + +/** + * JDBC {@link javax.sql.DataSource} implementation that delegates all calls + * to a given target {@link javax.sql.DataSource}. + * + *

This class is meant to be subclassed, with subclasses overriding only + * those methods (such as {@link #getConnection()}) that should not simply + * delegate to the target DataSource. + * + * @author Juergen Hoeller + * @since 1.1 + * @see #getConnection + */ +public class DelegatingDataSource implements DataSource, InitializingBean { + + private DataSource targetDataSource; + + + /** + * Create a new DelegatingDataSource. + * @see #setTargetDataSource + */ + public DelegatingDataSource() { + } + + /** + * Create a new DelegatingDataSource. + * @param targetDataSource the target DataSource + */ + public DelegatingDataSource(DataSource targetDataSource) { + setTargetDataSource(targetDataSource); + } + + + /** + * Set the target DataSource that this DataSource should delegate to. + */ + public void setTargetDataSource(DataSource targetDataSource) { + Assert.notNull(targetDataSource, "'targetDataSource' must not be null"); + this.targetDataSource = targetDataSource; + } + + /** + * Return the target DataSource that this DataSource should delegate to. + */ + public DataSource getTargetDataSource() { + return this.targetDataSource; + } + + public void afterPropertiesSet() { + if (getTargetDataSource() == null) { + throw new IllegalArgumentException("Property 'targetDataSource' is required"); + } + } + + + public Connection getConnection() throws SQLException { + return getTargetDataSource().getConnection(); + } + + public Connection getConnection(String username, String password) throws SQLException { + return getTargetDataSource().getConnection(username, password); + } + + public PrintWriter getLogWriter() throws SQLException { + return getTargetDataSource().getLogWriter(); + } + + public void setLogWriter(PrintWriter out) throws SQLException { + getTargetDataSource().setLogWriter(out); + } + + public int getLoginTimeout() throws SQLException { + return getTargetDataSource().getLoginTimeout(); + } + + public void setLoginTimeout(int seconds) throws SQLException { + getTargetDataSource().setLoginTimeout(seconds); + } + + + //--------------------------------------------------------------------- + // Implementation of JDBC 4.0's Wrapper interface + //--------------------------------------------------------------------- + + public Object unwrap(Class iface) throws SQLException { + return getTargetDataSource().unwrap(iface); + } + + public boolean isWrapperFor(Class iface) throws SQLException { + return getTargetDataSource().isWrapperFor(iface); + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/DriverManagerDataSource.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/DriverManagerDataSource.java new file mode 100644 index 0000000000..2e80e28f76 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/DriverManagerDataSource.java @@ -0,0 +1,177 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.datasource; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.Properties; + +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * Simple implementation of the standard JDBC {@link javax.sql.DataSource} interface, + * configuring the plain old JDBC {@link java.sql.DriverManager} via bean properties, and + * returning a new {@link java.sql.Connection} from every getConnection call. + * + *

NOTE: This class is not an actual connection pool; it does not actually + * pool Connections. It just serves as simple replacement for a full-blown + * connection pool, implementing the same standard interface, but creating new + * Connections on every call. + * + *

Useful for test or standalone environments outside of a J2EE container, either + * as a DataSource bean in a corresponding ApplicationContext or in conjunction with + * a simple JNDI environment. Pool-assuming Connection.close() calls will + * simply close the Connection, so any DataSource-aware persistence code should work. + * + *

NOTE: Within special class loading environments such as OSGi, this class + * is effectively superseded by {@link SimpleDriverDataSource} due to general class + * loading issues with the JDBC DriverManager that be resolved through direct Driver + * usage (which is exactly what SimpleDriverDataSource does). + * + *

In a J2EE container, it is recommended to use a JNDI DataSource provided by + * the container. Such a DataSource can be exposed as a DataSource bean in a Spring + * ApplicationContext via {@link org.springframework.jndi.JndiObjectFactoryBean}, + * for seamless switching to and from a local DataSource bean like this class. + * For tests, you can then either set up a mock JNDI environment through Spring's + * {@link org.springframework.mock.jndi.SimpleNamingContextBuilder}, or switch the + * bean definition to a local DataSource (which is simpler and thus recommended). + * + *

If you need a "real" connection pool outside of a J2EE container, consider + * Apache's Jakarta Commons DBCP + * or C3P0. + * Commons DBCP's BasicDataSource and C3P0's ComboPooledDataSource are full + * connection pool beans, supporting the same basic properties as this class + * plus specific settings (such as minimal/maximal pool size etc). + * + * @author Juergen Hoeller + * @since 14.03.2003 + * @see SimpleDriverDataSource + */ +public class DriverManagerDataSource extends AbstractDriverBasedDataSource { + + /** + * Constructor for bean-style configuration. + */ + public DriverManagerDataSource() { + } + + /** + * Create a new DriverManagerDataSource with the given JDBC URL, + * not specifying a username or password for JDBC access. + * @param url the JDBC URL to use for accessing the DriverManager + * @see java.sql.DriverManager#getConnection(String) + */ + public DriverManagerDataSource(String url) { + setUrl(url); + } + + /** + * Create a new DriverManagerDataSource with the given standard + * DriverManager parameters. + * @param url the JDBC URL to use for accessing the DriverManager + * @param username the JDBC username to use for accessing the DriverManager + * @param password the JDBC password to use for accessing the DriverManager + * @see java.sql.DriverManager#getConnection(String, String, String) + */ + public DriverManagerDataSource(String url, String username, String password) { + setUrl(url); + setUsername(username); + setPassword(password); + } + + /** + * Create a new DriverManagerDataSource with the given JDBC URL, + * not specifying a username or password for JDBC access. + * @param url the JDBC URL to use for accessing the DriverManager + * @param conProps JDBC connection properties + * @see java.sql.DriverManager#getConnection(String) + */ + public DriverManagerDataSource(String url, Properties conProps) { + setUrl(url); + setConnectionProperties(conProps); + } + + /** + * Create a new DriverManagerDataSource with the given standard + * DriverManager parameters. + * @param driverClassName the JDBC driver class name + * @param url the JDBC URL to use for accessing the DriverManager + * @param username the JDBC username to use for accessing the DriverManager + * @param password the JDBC password to use for accessing the DriverManager + * @deprecated since Spring 2.5. DriverManagerDataSource is primarily + * intended for accessing pre-registered JDBC drivers. + * If you need to register a new driver, consider using + * {@link SimpleDriverDataSource} instead. + */ + public DriverManagerDataSource(String driverClassName, String url, String username, String password) { + setDriverClassName(driverClassName); + setUrl(url); + setUsername(username); + setPassword(password); + } + + + /** + * Set the JDBC driver class name. This driver will get initialized + * on startup, registering itself with the JDK's DriverManager. + *

NOTE: DriverManagerDataSource is primarily intended for accessing + * pre-registered JDBC drivers. If you need to register a new driver, + * consider using {@link SimpleDriverDataSource} instead. Alternatively, consider + * initializing the JDBC driver yourself before instantiating this DataSource. + * The "driverClassName" property is mainly preserved for backwards compatibility, + * as well as for migrating between Commons DBCP and this DataSource. + * @see java.sql.DriverManager#registerDriver(java.sql.Driver) + * @see SimpleDriverDataSource + */ + public void setDriverClassName(String driverClassName) { + Assert.hasText(driverClassName, "Property 'driverClassName' must not be empty"); + String driverClassNameToUse = driverClassName.trim(); + try { + Class.forName(driverClassNameToUse, true, ClassUtils.getDefaultClassLoader()); + } + catch (ClassNotFoundException ex) { + IllegalStateException ise = + new IllegalStateException("Could not load JDBC driver class [" + driverClassNameToUse + "]"); + ise.initCause(ex); + throw ise; + } + if (logger.isInfoEnabled()) { + logger.info("Loaded JDBC driver: " + driverClassNameToUse); + } + } + + + protected Connection getConnectionFromDriver(Properties props) throws SQLException { + String url = getUrl(); + if (logger.isDebugEnabled()) { + logger.debug("Creating new JDBC DriverManager Connection to [" + url + "]"); + } + return getConnectionFromDriverManager(url, props); + } + + /** + * Getting a Connection using the nasty static from DriverManager is extracted + * into a protected method to allow for easy unit testing. + * @see java.sql.DriverManager#getConnection(String, java.util.Properties) + */ + protected Connection getConnectionFromDriverManager(String url, Properties props) throws SQLException { + return DriverManager.getConnection(url, props); + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/IsolationLevelDataSourceAdapter.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/IsolationLevelDataSourceAdapter.java new file mode 100644 index 0000000000..3d75f0536b --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/IsolationLevelDataSourceAdapter.java @@ -0,0 +1,165 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc.datasource; + +import java.sql.Connection; +import java.sql.SQLException; + +import org.springframework.core.Constants; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.support.DefaultTransactionDefinition; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +/** + * An adapter for a target {@link javax.sql.DataSource}, applying the current + * Spring transaction's isolation level (and potentially specified user credentials) + * to every getConnection call. Also applies the read-only flag, + * if specified. + * + *

Can be used to proxy a target JNDI DataSource that does not have the + * desired isolation level (and user credentials) configured. Client code + * can work with this DataSource as usual, not worrying about such settings. + * + *

Inherits the capability to apply specific user credentials from its superclass + * {@link UserCredentialsDataSourceAdapter}; see the latter's javadoc for details + * on that functionality (e.g. {@link #setCredentialsForCurrentThread}). + * + *

WARNING: This adapter simply calls + * {@link java.sql.Connection#setTransactionIsolation} and/or + * {@link java.sql.Connection#setReadOnly} for every Connection obtained from it. + * It does, however, not reset those settings; it rather expects the target + * DataSource to perform such resetting as part of its connection pool handling. + * Make sure that the target DataSource properly cleans up such transaction state. + * + * @author Juergen Hoeller + * @since 2.0.3 + * @see #setIsolationLevel + * @see #setIsolationLevelName + * @see #setUsername + * @see #setPassword + */ +public class IsolationLevelDataSourceAdapter extends UserCredentialsDataSourceAdapter { + + /** Constants instance for TransactionDefinition */ + private static final Constants constants = new Constants(TransactionDefinition.class); + + private Integer isolationLevel; + + + /** + * Set the default isolation level by the name of the corresponding constant + * in {@link org.springframework.transaction.TransactionDefinition}, e.g. + * "ISOLATION_SERIALIZABLE". + *

If not specified, the target DataSource's default will be used. + * Note that a transaction-specific isolation value will always override + * any isolation setting specified at the DataSource level. + * @param constantName name of the constant + * @see org.springframework.transaction.TransactionDefinition#ISOLATION_READ_UNCOMMITTED + * @see org.springframework.transaction.TransactionDefinition#ISOLATION_READ_COMMITTED + * @see org.springframework.transaction.TransactionDefinition#ISOLATION_REPEATABLE_READ + * @see org.springframework.transaction.TransactionDefinition#ISOLATION_SERIALIZABLE + * @see #setIsolationLevel + */ + public final void setIsolationLevelName(String constantName) throws IllegalArgumentException { + if (constantName == null || !constantName.startsWith(DefaultTransactionDefinition.PREFIX_ISOLATION)) { + throw new IllegalArgumentException("Only isolation constants allowed"); + } + setIsolationLevel(constants.asNumber(constantName).intValue()); + } + + /** + * Specify the default isolation level to use for Connection retrieval, + * according to the JDBC {@link java.sql.Connection} constants + * (equivalent to the corresponding Spring + * {@link org.springframework.transaction.TransactionDefinition} constants). + *

If not specified, the target DataSource's default will be used. + * Note that a transaction-specific isolation value will always override + * any isolation setting specified at the DataSource level. + * @see java.sql.Connection#TRANSACTION_READ_UNCOMMITTED + * @see java.sql.Connection#TRANSACTION_READ_COMMITTED + * @see java.sql.Connection#TRANSACTION_REPEATABLE_READ + * @see java.sql.Connection#TRANSACTION_SERIALIZABLE + * @see org.springframework.transaction.TransactionDefinition#ISOLATION_READ_UNCOMMITTED + * @see org.springframework.transaction.TransactionDefinition#ISOLATION_READ_COMMITTED + * @see org.springframework.transaction.TransactionDefinition#ISOLATION_REPEATABLE_READ + * @see org.springframework.transaction.TransactionDefinition#ISOLATION_SERIALIZABLE + * @see org.springframework.transaction.TransactionDefinition#getIsolationLevel() + * @see org.springframework.transaction.support.TransactionSynchronizationManager#getCurrentTransactionIsolationLevel() + */ + public void setIsolationLevel(int isolationLevel) { + if (!constants.getValues(DefaultTransactionDefinition.PREFIX_ISOLATION).contains(new Integer(isolationLevel))) { + throw new IllegalArgumentException("Only values of isolation constants allowed"); + } + this.isolationLevel = + (isolationLevel != TransactionDefinition.ISOLATION_DEFAULT ? new Integer(isolationLevel) : null); + } + + /** + * Return the statically specified isolation level, + * or null if none. + */ + protected Integer getIsolationLevel() { + return this.isolationLevel; + } + + + /** + * Applies the current isolation level value and read-only flag + * to the returned Connection. + * @see #getCurrentIsolationLevel() + * @see #getCurrentReadOnlyFlag() + */ + protected Connection doGetConnection(String username, String password) throws SQLException { + Connection con = super.doGetConnection(username, password); + Boolean readOnlyToUse = getCurrentReadOnlyFlag(); + if (readOnlyToUse != null) { + con.setReadOnly(readOnlyToUse.booleanValue()); + } + Integer isolationLevelToUse = getCurrentIsolationLevel(); + if (isolationLevelToUse != null) { + con.setTransactionIsolation(isolationLevelToUse.intValue()); + } + return con; + } + + /** + * Determine the current isolation level: either the transaction's + * isolation level or a statically defined isolation level. + * @return the current isolation level, or null if none + * @see org.springframework.transaction.support.TransactionSynchronizationManager#getCurrentTransactionIsolationLevel() + * @see #setIsolationLevel + */ + protected Integer getCurrentIsolationLevel() { + Integer isolationLevelToUse = TransactionSynchronizationManager.getCurrentTransactionIsolationLevel(); + if (isolationLevelToUse == null) { + isolationLevelToUse = getIsolationLevel(); + } + return isolationLevelToUse; + } + + /** + * Determine the current read-only flag: by default, + * the transaction's read-only hint. + * @return whether there is a read-only hint for the current scope + * @see org.springframework.transaction.support.TransactionSynchronizationManager#isCurrentTransactionReadOnly() + */ + protected Boolean getCurrentReadOnlyFlag() { + boolean txReadOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly(); + return (txReadOnly ? Boolean.TRUE : null); + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/JdbcTransactionObjectSupport.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/JdbcTransactionObjectSupport.java new file mode 100644 index 0000000000..5b4f5782fd --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/JdbcTransactionObjectSupport.java @@ -0,0 +1,157 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc.datasource; + +import java.sql.Savepoint; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.transaction.CannotCreateTransactionException; +import org.springframework.transaction.NestedTransactionNotSupportedException; +import org.springframework.transaction.SavepointManager; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.TransactionSystemException; +import org.springframework.transaction.TransactionUsageException; +import org.springframework.transaction.support.SmartTransactionObject; + +/** + * Convenient base class for JDBC-aware transaction objects. + * Can contain a {@link ConnectionHolder}, and implements the + * {@link org.springframework.transaction.SavepointManager} + * interface based on that ConnectionHolder. + * + *

Allows for programmatic management of JDBC 3.0 + * {@link java.sql.Savepoint Savepoints}. Spring's + * {@link org.springframework.transaction.support.DefaultTransactionStatus} + * will automatically delegate to this, as it autodetects transaction + * objects that implement the SavepointManager interface. + * + *

Note that savepoints are only supported for drivers which + * support JDBC 3.0 or higher. + * + * @author Juergen Hoeller + * @since 1.1 + */ +public abstract class JdbcTransactionObjectSupport implements SavepointManager, SmartTransactionObject { + + private static final Log logger = LogFactory.getLog(JdbcTransactionObjectSupport.class); + + + private ConnectionHolder connectionHolder; + + private Integer previousIsolationLevel; + + private boolean savepointAllowed = false; + + + public void setConnectionHolder(ConnectionHolder connectionHolder) { + this.connectionHolder = connectionHolder; + } + + public ConnectionHolder getConnectionHolder() { + return this.connectionHolder; + } + + public boolean hasConnectionHolder() { + return (this.connectionHolder != null); + } + + public void setPreviousIsolationLevel(Integer previousIsolationLevel) { + this.previousIsolationLevel = previousIsolationLevel; + } + + public Integer getPreviousIsolationLevel() { + return this.previousIsolationLevel; + } + + public void setSavepointAllowed(boolean savepointAllowed) { + this.savepointAllowed = savepointAllowed; + } + + public boolean isSavepointAllowed() { + return this.savepointAllowed; + } + + + //--------------------------------------------------------------------- + // Implementation of SavepointManager + //--------------------------------------------------------------------- + + /** + * This implementation creates a JDBC 3.0 Savepoint and returns it. + * @see java.sql.Connection#setSavepoint + */ + public Object createSavepoint() throws TransactionException { + ConnectionHolder conHolder = getConnectionHolderForSavepoint(); + try { + if (!conHolder.supportsSavepoints()) { + throw new NestedTransactionNotSupportedException( + "Cannot create a nested transaction because savepoints are not supported by your JDBC driver"); + } + } + catch (Throwable ex) { + throw new NestedTransactionNotSupportedException( + "Cannot create a nested transaction because your JDBC driver is not a JDBC 3.0 driver", ex); + } + try { + return conHolder.createSavepoint(); + } + catch (Throwable ex) { + throw new CannotCreateTransactionException("Could not create JDBC savepoint", ex); + } + } + + /** + * This implementation rolls back to the given JDBC 3.0 Savepoint. + * @see java.sql.Connection#rollback(java.sql.Savepoint) + */ + public void rollbackToSavepoint(Object savepoint) throws TransactionException { + try { + getConnectionHolderForSavepoint().getConnection().rollback((Savepoint) savepoint); + } + catch (Throwable ex) { + throw new TransactionSystemException("Could not roll back to JDBC savepoint", ex); + } + } + + /** + * This implementation releases the given JDBC 3.0 Savepoint. + * @see java.sql.Connection#releaseSavepoint + */ + public void releaseSavepoint(Object savepoint) throws TransactionException { + try { + getConnectionHolderForSavepoint().getConnection().releaseSavepoint((Savepoint) savepoint); + } + catch (Throwable ex) { + logger.debug("Could not explicitly release JDBC savepoint", ex); + } + } + + protected ConnectionHolder getConnectionHolderForSavepoint() throws TransactionException { + if (!isSavepointAllowed()) { + throw new NestedTransactionNotSupportedException( + "Transaction manager does not allow nested transactions"); + } + if (!hasConnectionHolder()) { + throw new TransactionUsageException( + "Cannot create nested transaction if not exposing a JDBC transaction"); + } + return getConnectionHolder(); + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/LazyConnectionDataSourceProxy.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/LazyConnectionDataSourceProxy.java new file mode 100644 index 0000000000..9214962520 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/LazyConnectionDataSourceProxy.java @@ -0,0 +1,420 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.datasource; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.sql.Connection; +import java.sql.SQLException; + +import javax.sql.DataSource; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.core.Constants; + +/** + * Proxy for a target DataSource, fetching actual JDBC Connections lazily, + * i.e. not until first creation of a Statement. Connection initialization + * properties like auto-commit mode, transaction isolation and read-only mode + * will be kept and applied to the actual JDBC Connection as soon as an + * actual Connection is fetched (if ever). Consequently, commit and rollback + * calls will be ignored if no Statements have been created. + * + *

This DataSource proxy allows to avoid fetching JDBC Connections from + * a pool unless actually necessary. JDBC transaction control can happen + * without fetching a Connection from the pool or communicating with the + * database; this will be done lazily on first creation of a JDBC Statement. + * + *

If you configure both a LazyConnectionDataSourceProxy and a + * TransactionAwareDataSourceProxy, make sure that the latter is the outermost + * DataSource. In such a scenario, data access code will talk to the + * transaction-aware DataSource, which will in turn work with the + * LazyConnectionDataSourceProxy. + * + *

Lazy fetching of physical JDBC Connections is particularly beneficial + * in a generic transaction demarcation environment. It allows you to demarcate + * transactions on all methods that could potentially perform data access, + * without paying a performance penalty if no actual data access happens. + * + *

This DataSource proxy gives you behavior analogous to JTA and a + * transactional JNDI DataSource (as provided by the J2EE server), even + * with a local transaction strategy like DataSourceTransactionManager or + * HibernateTransactionManager. It does not add value with Spring's + * JtaTransactionManager as transaction strategy. + * + *

Lazy fetching of JDBC Connections is also recommended for read-only + * operations with Hibernate, in particular if the chances of resolving the + * result in the second-level cache are high. This avoids the need to + * communicate with the database at all for such read-only operations. + * You will get the same effect with non-transactional reads, but lazy fetching + * of JDBC Connections allows you to still perform reads in transactions. + * + *

NOTE: This DataSource proxy needs to return wrapped Connections to + * handle lazy fetching of an actual JDBC Connection. Therefore, the returned + * Connections cannot be cast to a native JDBC Connection type like OracleConnection, + * or to a connection pool implementation type. Use a corresponding + * NativeJdbcExtractor to retrieve the native JDBC Connection. + * + * @author Juergen Hoeller + * @since 1.1.4 + * @see ConnectionProxy + * @see DataSourceTransactionManager + * @see org.springframework.orm.hibernate3.HibernateTransactionManager + * @see org.springframework.jdbc.support.nativejdbc.NativeJdbcExtractor + */ +public class LazyConnectionDataSourceProxy extends DelegatingDataSource { + + /** Constants instance for TransactionDefinition */ + private static final Constants constants = new Constants(Connection.class); + + private static final Log logger = LogFactory.getLog(LazyConnectionDataSourceProxy.class); + + private Boolean defaultAutoCommit; + + private Integer defaultTransactionIsolation; + + + /** + * Create a new LazyConnectionDataSourceProxy. + * @see #setTargetDataSource + */ + public LazyConnectionDataSourceProxy() { + } + + /** + * Create a new LazyConnectionDataSourceProxy. + * @param targetDataSource the target DataSource + */ + public LazyConnectionDataSourceProxy(DataSource targetDataSource) { + setTargetDataSource(targetDataSource); + afterPropertiesSet(); + } + + + /** + * Set the default auto-commit mode to expose when no target Connection + * has been fetched yet (-> actual JDBC Connection default not known yet). + *

If not specified, the default gets determined by checking a target + * Connection on startup. If that check fails, the default will be determined + * lazily on first access of a Connection. + * @see java.sql.Connection#setAutoCommit + */ + public void setDefaultAutoCommit(boolean defaultAutoCommit) { + this.defaultAutoCommit = new Boolean(defaultAutoCommit); + } + + /** + * Set the default transaction isolation level to expose when no target Connection + * has been fetched yet (-> actual JDBC Connection default not known yet). + *

This property accepts the int constant value (e.g. 8) as defined in the + * {@link java.sql.Connection} interface; it is mainly intended for programmatic + * use. Consider using the "defaultTransactionIsolationName" property for setting + * the value by name (e.g. "TRANSACTION_SERIALIZABLE"). + *

If not specified, the default gets determined by checking a target + * Connection on startup. If that check fails, the default will be determined + * lazily on first access of a Connection. + * @see #setDefaultTransactionIsolationName + * @see java.sql.Connection#setTransactionIsolation + */ + public void setDefaultTransactionIsolation(int defaultTransactionIsolation) { + this.defaultTransactionIsolation = new Integer(defaultTransactionIsolation); + } + + /** + * Set the default transaction isolation level by the name of the corresponding + * constant in {@link java.sql.Connection}, e.g. "TRANSACTION_SERIALIZABLE". + * @param constantName name of the constant + * @see #setDefaultTransactionIsolation + * @see java.sql.Connection#TRANSACTION_READ_UNCOMMITTED + * @see java.sql.Connection#TRANSACTION_READ_COMMITTED + * @see java.sql.Connection#TRANSACTION_REPEATABLE_READ + * @see java.sql.Connection#TRANSACTION_SERIALIZABLE + */ + public void setDefaultTransactionIsolationName(String constantName) { + setDefaultTransactionIsolation(constants.asNumber(constantName).intValue()); + } + + + public void afterPropertiesSet() { + super.afterPropertiesSet(); + + // Determine default auto-commit and transaction isolation + // via a Connection from the target DataSource, if possible. + if (this.defaultAutoCommit == null || this.defaultTransactionIsolation == null) { + try { + Connection con = getTargetDataSource().getConnection(); + try { + checkDefaultConnectionProperties(con); + } + finally { + con.close(); + } + } + catch (SQLException ex) { + logger.warn("Could not retrieve default auto-commit and transaction isolation settings", ex); + } + } + } + + /** + * Check the default connection properties (auto-commit, transaction isolation), + * keeping them to be able to expose them correctly without fetching an actual + * JDBC Connection from the target DataSource. + *

This will be invoked once on startup, but also for each retrieval of a + * target Connection. If the check failed on startup (because the database was + * down), we'll lazily retrieve those settings. + * @param con the Connection to use for checking + * @throws SQLException if thrown by Connection methods + */ + protected synchronized void checkDefaultConnectionProperties(Connection con) throws SQLException { + if (this.defaultAutoCommit == null) { + this.defaultAutoCommit = new Boolean(con.getAutoCommit()); + } + if (this.defaultTransactionIsolation == null) { + this.defaultTransactionIsolation = new Integer(con.getTransactionIsolation()); + } + } + + /** + * Expose the default auto-commit value. + */ + protected Boolean defaultAutoCommit() { + return this.defaultAutoCommit; + } + + /** + * Expose the default transaction isolation value. + */ + protected Integer defaultTransactionIsolation() { + return this.defaultTransactionIsolation; + } + + + /** + * Return a Connection handle that lazily fetches an actual JDBC Connection + * when asked for a Statement (or PreparedStatement or CallableStatement). + *

The returned Connection handle implements the ConnectionProxy interface, + * allowing to retrieve the underlying target Connection. + * @return a lazy Connection handle + * @see ConnectionProxy#getTargetConnection() + */ + public Connection getConnection() throws SQLException { + return (Connection) Proxy.newProxyInstance( + ConnectionProxy.class.getClassLoader(), + new Class[] {ConnectionProxy.class}, + new LazyConnectionInvocationHandler()); + } + + /** + * Return a Connection handle that lazily fetches an actual JDBC Connection + * when asked for a Statement (or PreparedStatement or CallableStatement). + *

The returned Connection handle implements the ConnectionProxy interface, + * allowing to retrieve the underlying target Connection. + * @param username the per-Connection username + * @param password the per-Connection password + * @return a lazy Connection handle + * @see ConnectionProxy#getTargetConnection() + */ + public Connection getConnection(String username, String password) throws SQLException { + return (Connection) Proxy.newProxyInstance( + ConnectionProxy.class.getClassLoader(), + new Class[] {ConnectionProxy.class}, + new LazyConnectionInvocationHandler(username, password)); + } + + + /** + * Invocation handler that defers fetching an actual JDBC Connection + * until first creation of a Statement. + */ + private class LazyConnectionInvocationHandler implements InvocationHandler { + + private String username; + + private String password; + + private Boolean readOnly = Boolean.FALSE; + + private Integer transactionIsolation; + + private Boolean autoCommit; + + private boolean closed = false; + + private Connection target; + + public LazyConnectionInvocationHandler() { + this.autoCommit = defaultAutoCommit(); + this.transactionIsolation = defaultTransactionIsolation(); + } + + public LazyConnectionInvocationHandler(String username, String password) { + this(); + this.username = username; + this.password = password; + } + + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + // Invocation on ConnectionProxy interface coming in... + + if (method.getName().equals("equals")) { + // We must avoid fetching a target Connection for "equals". + // Only consider equal when proxies are identical. + return (proxy == args[0] ? Boolean.TRUE : Boolean.FALSE); + } + else if (method.getName().equals("hashCode")) { + // We must avoid fetching a target Connection for "hashCode", + // and we must return the same hash code even when the target + // Connection has been fetched: use hashCode of Connection proxy. + return new Integer(System.identityHashCode(proxy)); + } + else if (method.getName().equals("getTargetConnection")) { + // Handle getTargetConnection method: return underlying connection. + return getTargetConnection(method); + } + + if (!hasTargetConnection()) { + // No physical target Connection kept yet -> + // resolve transaction demarcation methods without fetching + // a physical JDBC Connection until absolutely necessary. + + if (method.getName().equals("toString")) { + return "Lazy Connection proxy for target DataSource [" + getTargetDataSource() + "]"; + } + else if (method.getName().equals("isReadOnly")) { + return this.readOnly; + } + else if (method.getName().equals("setReadOnly")) { + this.readOnly = (Boolean) args[0]; + return null; + } + else if (method.getName().equals("getTransactionIsolation")) { + if (this.transactionIsolation != null) { + return this.transactionIsolation; + } + // Else fetch actual Connection and check there, + // because we didn't have a default specified. + } + else if (method.getName().equals("setTransactionIsolation")) { + this.transactionIsolation = (Integer) args[0]; + return null; + } + else if (method.getName().equals("getAutoCommit")) { + if (this.autoCommit != null) { + return this.autoCommit; + } + // Else fetch actual Connection and check there, + // because we didn't have a default specified. + } + else if (method.getName().equals("setAutoCommit")) { + this.autoCommit = (Boolean) args[0]; + return null; + } + else if (method.getName().equals("commit")) { + // Ignore: no statements created yet. + return null; + } + else if (method.getName().equals("rollback")) { + // Ignore: no statements created yet. + return null; + } + else if (method.getName().equals("getWarnings")) { + return null; + } + else if (method.getName().equals("clearWarnings")) { + return null; + } + else if (method.getName().equals("isClosed")) { + return (this.closed ? Boolean.TRUE : Boolean.FALSE); + } + else if (method.getName().equals("close")) { + // Ignore: no target connection yet. + this.closed = true; + return null; + } + else if (this.closed) { + // Connection proxy closed, without ever having fetched a + // physical JDBC Connection: throw corresponding SQLException. + throw new SQLException("Illegal operation: connection is closed"); + } + } + + // Target Connection already fetched, + // or target Connection necessary for current operation -> + // invoke method on target connection. + try { + return method.invoke(getTargetConnection(method), args); + } + catch (InvocationTargetException ex) { + throw ex.getTargetException(); + } + } + + /** + * Return whether the proxy currently holds a target Connection. + */ + private boolean hasTargetConnection() { + return (this.target != null); + } + + /** + * Return the target Connection, fetching it and initializing it if necessary. + */ + private Connection getTargetConnection(Method operation) throws SQLException { + if (this.target == null) { + // No target Connection held -> fetch one. + if (logger.isDebugEnabled()) { + logger.debug("Connecting to database for operation '" + operation.getName() + "'"); + } + + // Fetch physical Connection from DataSource. + this.target = (this.username != null) ? + getTargetDataSource().getConnection(this.username, this.password) : + getTargetDataSource().getConnection(); + + // If we still lack default connection properties, check them now. + checkDefaultConnectionProperties(this.target); + + // Apply kept transaction settings, if any. + if (this.readOnly.booleanValue()) { + this.target.setReadOnly(this.readOnly.booleanValue()); + } + if (this.transactionIsolation != null && + !this.transactionIsolation.equals(defaultTransactionIsolation())) { + this.target.setTransactionIsolation(this.transactionIsolation.intValue()); + } + if (this.autoCommit != null && this.autoCommit.booleanValue() != this.target.getAutoCommit()) { + this.target.setAutoCommit(this.autoCommit.booleanValue()); + } + } + + else { + // Target Connection already held -> return it. + if (logger.isDebugEnabled()) { + logger.debug("Using existing database connection for operation '" + operation.getName() + "'"); + } + } + + return this.target; + } + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/SimpleConnectionHandle.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/SimpleConnectionHandle.java new file mode 100644 index 0000000000..801e25a121 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/SimpleConnectionHandle.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc.datasource; + +import java.sql.Connection; + +import org.springframework.util.Assert; + +/** + * Simple implementation of the {@link ConnectionHandle} interface, + * containing a given JDBC Connection. + * + * @author Juergen Hoeller + * @since 1.1 + */ +public class SimpleConnectionHandle implements ConnectionHandle { + + private final Connection connection; + + + /** + * Create a new SimpleConnectionHandle for the given Connection. + * @param connection the JDBC Connection + */ + public SimpleConnectionHandle(Connection connection) { + Assert.notNull(connection, "Connection must not be null"); + this.connection = connection; + } + + /** + * Return the specified Connection as-is. + */ + public Connection getConnection() { + return this.connection; + } + + /** + * This implementation is empty, as we're using a standard + * Connection handle that does not have to be released. + */ + public void releaseConnection(Connection con) { + } + + + public String toString() { + return "SimpleConnectionHandle: " + this.connection; + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/SimpleDriverDataSource.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/SimpleDriverDataSource.java new file mode 100644 index 0000000000..982fba1952 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/SimpleDriverDataSource.java @@ -0,0 +1,142 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.datasource; + +import java.sql.Connection; +import java.sql.Driver; +import java.sql.SQLException; +import java.util.Properties; + +import org.springframework.beans.BeanUtils; +import org.springframework.util.Assert; + +/** + * Simple implementation of the standard JDBC {@link javax.sql.DataSource} interface, + * configuring a plain old JDBC {@link java.sql.Driver} via bean properties, and returning + * a new {@link java.sql.Connection} from every getConnection call. + * + *

NOTE: This class is not an actual connection pool; it does not actually + * pool Connections. It just serves as simple replacement for a full-blown + * connection pool, implementing the same standard interface, but creating new + * Connections on every call. + * + *

In a J2EE container, it is recommended to use a JNDI DataSource provided by + * the container. Such a DataSource can be exposed as a DataSource bean in a Spring + * ApplicationContext via {@link org.springframework.jndi.JndiObjectFactoryBean}, + * for seamless switching to and from a local DataSource bean like this class. + * + *

If you need a "real" connection pool outside of a J2EE container, consider + * Apache's Jakarta Commons DBCP + * or C3P0. + * Commons DBCP's BasicDataSource and C3P0's ComboPooledDataSource are full + * connection pool beans, supporting the same basic properties as this class + * plus specific settings (such as minimal/maximal pool size etc). + * + * @author Juergen Hoeller + * @since 2.5.5 + * @see DriverManagerDataSource + */ +public class SimpleDriverDataSource extends AbstractDriverBasedDataSource { + + private Driver driver; + + + /** + * Constructor for bean-style configuration. + */ + public SimpleDriverDataSource() { + } + + /** + * Create a new DriverManagerDataSource with the given standard Driver parameters. + * @param driver the JDBC Driver object + * @param url the JDBC URL to use for accessing the DriverManager + * @see java.sql.Driver#connect(String, java.util.Properties) + */ + public SimpleDriverDataSource(Driver driver, String url) { + setDriver(driver); + setUrl(url); + } + + /** + * Create a new DriverManagerDataSource with the given standard Driver parameters. + * @param driver the JDBC Driver object + * @param url the JDBC URL to use for accessing the DriverManager + * @param username the JDBC username to use for accessing the DriverManager + * @param password the JDBC password to use for accessing the DriverManager + * @see java.sql.Driver#connect(String, java.util.Properties) + */ + public SimpleDriverDataSource(Driver driver, String url, String username, String password) { + setDriver(driver); + setUrl(url); + setUsername(username); + setPassword(password); + } + + /** + * Create a new DriverManagerDataSource with the given standard Driver parameters. + * @param driver the JDBC Driver object + * @param url the JDBC URL to use for accessing the DriverManager + * @param conProps JDBC connection properties + * @see java.sql.Driver#connect(String, java.util.Properties) + */ + public SimpleDriverDataSource(Driver driver, String url, Properties conProps) { + setDriver(driver); + setUrl(url); + setConnectionProperties(conProps); + } + + + /** + * Specify the JDBC Driver implementation class to use. + *

An instance of this Driver class will be created and held + * within the SimpleDriverDataSource. + * @see #setDriver + */ + public void setDriverClass(Class driverClass) { + this.driver = (Driver) BeanUtils.instantiateClass(driverClass); + } + + /** + * Specify the JDBC Driver instance to use. + *

This allows for passing in a shared, possibly pre-configured + * Driver instance. + * @see #setDriverClass + */ + public void setDriver(Driver driver) { + this.driver = driver; + } + + /** + * Return the JDBC Driver instance to use. + */ + public Driver getDriver() { + return this.driver; + } + + + protected Connection getConnectionFromDriver(Properties props) throws SQLException { + Driver driver = getDriver(); + String url = getUrl(); + Assert.notNull(driver, "Driver must not be null"); + if (logger.isDebugEnabled()) { + logger.debug("Creating new JDBC Driver Connection to [" + url + "]"); + } + return driver.connect(url, props); + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/SingleConnectionDataSource.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/SingleConnectionDataSource.java new file mode 100644 index 0000000000..312f90a6e1 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/SingleConnectionDataSource.java @@ -0,0 +1,344 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.datasource; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.sql.Connection; +import java.sql.SQLException; + +import org.springframework.beans.factory.DisposableBean; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * Implementation of {@link SmartDataSource} that wraps a single JDBC Connection + * which is not closed after use. Obviously, this is not multi-threading capable. + * + *

Note that at shutdown, someone should close the underlying Connection + * via the close() method. Client code will never call close + * on the Connection handle if it is SmartDataSource-aware (e.g. uses + * DataSourceUtils.releaseConnection). + * + *

If client code will call close() in the assumption of a pooled + * Connection, like when using persistence tools, set "suppressClose" to "true". + * This will return a close-suppressing proxy instead of the physical Connection. + * Be aware that you will not be able to cast this to a native + * OracleConnection or the like anymore; you need to use a + * {@link org.springframework.jdbc.support.nativejdbc.NativeJdbcExtractor} then. + * + *

This is primarily intended for testing. For example, it enables easy testing + * outside an application server, for code that expects to work on a DataSource. + * In contrast to {@link DriverManagerDataSource}, it reuses the same Connection + * all the time, avoiding excessive creation of physical Connections. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see #getConnection() + * @see java.sql.Connection#close() + * @see DataSourceUtils#releaseConnection + * @see org.springframework.jdbc.support.nativejdbc.NativeJdbcExtractor + */ +public class SingleConnectionDataSource extends DriverManagerDataSource + implements SmartDataSource, DisposableBean { + + /** Create a close-suppressing proxy? */ + private boolean suppressClose; + + /** Override AutoCommit? */ + private Boolean autoCommit; + + /** Wrapped Connection */ + private Connection target; + + /** Proxy Connection */ + private Connection connection; + + /** Synchronization monitor for the shared Connection */ + private final Object connectionMonitor = new Object(); + + + /** + * Constructor for bean-style configuration. + */ + public SingleConnectionDataSource() { + } + + /** + * Create a new SingleConnectionDataSource with the given standard + * DriverManager parameters. + * @param driverClassName the JDBC driver class name + * @param url the JDBC URL to use for accessing the DriverManager + * @param username the JDBC username to use for accessing the DriverManager + * @param password the JDBC password to use for accessing the DriverManager + * @param suppressClose if the returned Connection should be a + * close-suppressing proxy or the physical Connection + * @deprecated since Spring 2.5. Driver parameter usage is generally not recommended + * for a SingleConnectionDataSource. If you insist on using driver parameters + * directly, set up the Driver class manually before invoking this DataSource. + * @see java.sql.DriverManager#getConnection(String, String, String) + */ + public SingleConnectionDataSource( + String driverClassName, String url, String username, String password, boolean suppressClose) { + + super(driverClassName, url, username, password); + this.suppressClose = suppressClose; + } + + /** + * Create a new SingleConnectionDataSource with the given standard + * DriverManager parameters. + * @param url the JDBC URL to use for accessing the DriverManager + * @param username the JDBC username to use for accessing the DriverManager + * @param password the JDBC password to use for accessing the DriverManager + * @param suppressClose if the returned Connection should be a + * close-suppressing proxy or the physical Connection + * @see java.sql.DriverManager#getConnection(String, String, String) + */ + public SingleConnectionDataSource(String url, String username, String password, boolean suppressClose) { + super(url, username, password); + this.suppressClose = suppressClose; + } + + /** + * Create a new SingleConnectionDataSource with the given standard + * DriverManager parameters. + * @param url the JDBC URL to use for accessing the DriverManager + * @param suppressClose if the returned Connection should be a + * close-suppressing proxy or the physical Connection + * @see java.sql.DriverManager#getConnection(String, String, String) + */ + public SingleConnectionDataSource(String url, boolean suppressClose) { + super(url); + this.suppressClose = suppressClose; + } + + /** + * Create a new SingleConnectionDataSource with a given Connection. + * @param target underlying target Connection + * @param suppressClose if the Connection should be wrapped with a Connection that + * suppresses close() calls (to allow for normal close() + * usage in applications that expect a pooled Connection but do not know our + * SmartDataSource interface) + */ + public SingleConnectionDataSource(Connection target, boolean suppressClose) { + Assert.notNull(target, "Connection must not be null"); + this.target = target; + this.suppressClose = suppressClose; + this.connection = (suppressClose ? getCloseSuppressingConnectionProxy(target) : target); + } + + + /** + * Set whether the returned Connection should be a close-suppressing proxy + * or the physical Connection. + */ + public void setSuppressClose(boolean suppressClose) { + this.suppressClose = suppressClose; + } + + /** + * Return whether the returned Connection will be a close-suppressing proxy + * or the physical Connection. + */ + protected boolean isSuppressClose() { + return this.suppressClose; + } + + /** + * Set whether the returned Connection's "autoCommit" setting should be overridden. + */ + public void setAutoCommit(boolean autoCommit) { + this.autoCommit = (autoCommit ? Boolean.TRUE : Boolean.FALSE); + } + + /** + * Return whether the returned Connection's "autoCommit" setting should be overridden. + * @return the "autoCommit" value, or null if none to be applied + */ + protected Boolean getAutoCommitValue() { + return this.autoCommit; + } + + + public Connection getConnection() throws SQLException { + synchronized (this.connectionMonitor) { + if (this.connection == null) { + // No underlying Connection -> lazy init via DriverManager. + initConnection(); + } + if (this.connection.isClosed()) { + throw new SQLException( + "Connection was closed in SingleConnectionDataSource. Check that user code checks " + + "shouldClose() before closing Connections, or set 'suppressClose' to 'true'"); + } + return this.connection; + } + } + + /** + * Specifying a custom username and password doesn't make sense + * with a single Connection. Returns the single Connection if given + * the same username and password; throws a SQLException else. + */ + public Connection getConnection(String username, String password) throws SQLException { + if (ObjectUtils.nullSafeEquals(username, getUsername()) && + ObjectUtils.nullSafeEquals(password, getPassword())) { + return getConnection(); + } + else { + throw new SQLException("SingleConnectionDataSource does not support custom username and password"); + } + } + + /** + * This is a single Connection: Do not close it when returning to the "pool". + */ + public boolean shouldClose(Connection con) { + synchronized (this.connectionMonitor) { + return (con != this.connection && con != this.target); + } + } + + /** + * Close the underlying Connection. + * The provider of this DataSource needs to care for proper shutdown. + *

As this bean implements DisposableBean, a bean factory will + * automatically invoke this on destruction of its cached singletons. + */ + public void destroy() { + synchronized (this.connectionMonitor) { + closeConnection(); + } + } + + + /** + * Initialize the underlying Connection via the DriverManager. + */ + public void initConnection() throws SQLException { + if (getUrl() == null) { + throw new IllegalStateException("'url' property is required for lazily initializing a Connection"); + } + synchronized (this.connectionMonitor) { + closeConnection(); + this.target = getConnectionFromDriver(getUsername(), getPassword()); + prepareConnection(this.target); + if (logger.isInfoEnabled()) { + logger.info("Established shared JDBC Connection: " + this.target); + } + this.connection = (isSuppressClose() ? getCloseSuppressingConnectionProxy(this.target) : this.target); + } + } + + /** + * Reset the underlying shared Connection, to be reinitialized on next access. + */ + public void resetConnection() { + synchronized (this.connectionMonitor) { + closeConnection(); + this.target = null; + this.connection = null; + } + } + + /** + * Prepare the given Connection before it is exposed. + *

The default implementation applies the auto-commit flag, if necessary. + * Can be overridden in subclasses. + * @param con the Connection to prepare + * @see #setAutoCommit + */ + protected void prepareConnection(Connection con) throws SQLException { + Boolean autoCommit = getAutoCommitValue(); + if (autoCommit != null && con.getAutoCommit() != autoCommit.booleanValue()) { + con.setAutoCommit(autoCommit.booleanValue()); + } + } + + /** + * Close the underlying shared Connection. + */ + private void closeConnection() { + if (this.target != null) { + try { + this.target.close(); + } + catch (Throwable ex) { + logger.warn("Could not close shared JDBC Connection", ex); + } + } + } + + /** + * Wrap the given Connection with a proxy that delegates every method call to it + * but suppresses close calls. + * @param target the original Connection to wrap + * @return the wrapped Connection + */ + protected Connection getCloseSuppressingConnectionProxy(Connection target) { + return (Connection) Proxy.newProxyInstance( + ConnectionProxy.class.getClassLoader(), + new Class[] {ConnectionProxy.class}, + new CloseSuppressingInvocationHandler(target)); + } + + + /** + * Invocation handler that suppresses close calls on JDBC Connections. + */ + private static class CloseSuppressingInvocationHandler implements InvocationHandler { + + private final Connection target; + + public CloseSuppressingInvocationHandler(Connection target) { + this.target = target; + } + + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + // Invocation on ConnectionProxy interface coming in... + + if (method.getName().equals("equals")) { + // Only consider equal when proxies are identical. + return (proxy == args[0] ? Boolean.TRUE : Boolean.FALSE); + } + else if (method.getName().equals("hashCode")) { + // Use hashCode of Connection proxy. + return new Integer(System.identityHashCode(proxy)); + } + else if (method.getName().equals("close")) { + // Handle close method: don't pass the call on. + return null; + } + else if (method.getName().equals("getTargetConnection")) { + // Handle getTargetConnection method: return underlying Connection. + return this.target; + } + + // Invoke method on target Connection. + try { + return method.invoke(this.target, args); + } + catch (InvocationTargetException ex) { + throw ex.getTargetException(); + } + } + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/SmartDataSource.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/SmartDataSource.java new file mode 100644 index 0000000000..3c0e1377b1 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/SmartDataSource.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc.datasource; + +import java.sql.Connection; + +import javax.sql.DataSource; + +/** + * Extension of the javax.sql.DataSource interface, to be + * implemented by special DataSources that return JDBC Connections + * in an unwrapped fashion. + * + *

Classes using this interface can query whether or not the Connection + * should be closed after an operation. Spring's DataSourceUtils and + * JdbcTemplate classes automatically perform such a check. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see SingleConnectionDataSource#shouldClose + * @see DataSourceUtils#releaseConnection + * @see org.springframework.jdbc.core.JdbcTemplate + */ +public interface SmartDataSource extends DataSource { + + /** + * Should we close this Connection, obtained from this DataSource? + *

Code that uses Connections from a SmartDataSource should always + * perform a check via this method before invoking close(). + *

Note that the JdbcTemplate class in the 'jdbc.core' package takes care of + * releasing JDBC Connections, freeing application code of this responsibility. + * @param con the Connection to check + * @return whether the given Connection should be closed + * @see java.sql.Connection#close() + */ + boolean shouldClose(Connection con); + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/TransactionAwareDataSourceProxy.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/TransactionAwareDataSourceProxy.java new file mode 100644 index 0000000000..218830d5d3 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/TransactionAwareDataSourceProxy.java @@ -0,0 +1,246 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc.datasource; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; + +import javax.sql.DataSource; + +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.util.Assert; + +/** + * Proxy for a target JDBC {@link javax.sql.DataSource}, adding awareness of + * Spring-managed transactions. Similar to a transactional JNDI DataSource + * as provided by a J2EE server. + * + *

Data access code that should remain unaware of Spring's data access support + * can work with this proxy to seamlessly participate in Spring-managed transactions. + * Note that the transaction manager, for example {@link DataSourceTransactionManager}, + * still needs to work with the underlying DataSource, not with this proxy. + * + *

Make sure that TransactionAwareDataSourceProxy is the outermost DataSource + * of a chain of DataSource proxies/adapters. TransactionAwareDataSourceProxy + * can delegate either directly to the target connection pool or to some + * intermediary proxy/adapter like {@link LazyConnectionDataSourceProxy} or + * {@link UserCredentialsDataSourceAdapter}. + * + *

Delegates to {@link DataSourceUtils} for automatically participating in + * thread-bound transactions, for example managed by {@link DataSourceTransactionManager}. + * getConnection calls and close calls on returned Connections + * will behave properly within a transaction, i.e. always operate on the transactional + * Connection. If not within a transaction, normal DataSource behavior applies. + * + *

This proxy allows data access code to work with the plain JDBC API and still + * participate in Spring-managed transactions, similar to JDBC code in a J2EE/JTA + * environment. However, if possible, use Spring's DataSourceUtils, JdbcTemplate or + * JDBC operation objects to get transaction participation even without a proxy for + * the target DataSource, avoiding the need to define such a proxy in the first place. + * + *

As a further effect, using a transaction-aware DataSource will apply remaining + * transaction timeouts to all created JDBC (Prepared/Callable)Statement. This means + * that all operations performed through standard JDBC will automatically participate + * in Spring-managed transaction timeouts. + * + *

NOTE: This DataSource proxy needs to return wrapped Connections + * (which implement the {@link ConnectionProxy} interface) in order to handle + * close calls properly. Therefore, the returned Connections cannot be cast + * to a native JDBC Connection type like OracleConnection or to a connection + * pool implementation type. Use a corresponding + * {@link org.springframework.jdbc.support.nativejdbc.NativeJdbcExtractor} + * to retrieve the native JDBC Connection. + * + * @author Juergen Hoeller + * @since 1.1 + * @see javax.sql.DataSource#getConnection() + * @see java.sql.Connection#close() + * @see DataSourceUtils#doGetConnection + * @see DataSourceUtils#applyTransactionTimeout + * @see DataSourceUtils#doReleaseConnection + */ +public class TransactionAwareDataSourceProxy extends DelegatingDataSource { + + private boolean reobtainTransactionalConnections = false; + + + /** + * Create a new TransactionAwareDataSourceProxy. + * @see #setTargetDataSource + */ + public TransactionAwareDataSourceProxy() { + } + + /** + * Create a new TransactionAwareDataSourceProxy. + * @param targetDataSource the target DataSource + */ + public TransactionAwareDataSourceProxy(DataSource targetDataSource) { + super(targetDataSource); + } + + /** + * Specify whether to reobtain the target Connection for each operation + * performed within a transaction. + *

The default is "false". Specify "true" to reobtain transactional + * Connections for every call on the Connection proxy; this is advisable + * on JBoss if you hold on to a Connection handle across transaction boundaries. + *

The effect of this setting is similar to the + * "hibernate.connection.release_mode" value "after_statement". + */ + public void setReobtainTransactionalConnections(boolean reobtainTransactionalConnections) { + this.reobtainTransactionalConnections = reobtainTransactionalConnections; + } + + + /** + * Delegates to DataSourceUtils for automatically participating in Spring-managed + * transactions. Throws the original SQLException, if any. + *

The returned Connection handle implements the ConnectionProxy interface, + * allowing to retrieve the underlying target Connection. + * @return a transactional Connection if any, a new one else + * @see DataSourceUtils#doGetConnection + * @see ConnectionProxy#getTargetConnection + */ + public Connection getConnection() throws SQLException { + DataSource ds = getTargetDataSource(); + Assert.state(ds != null, "'targetDataSource' is required"); + return getTransactionAwareConnectionProxy(ds); + } + + /** + * Wraps the given Connection with a proxy that delegates every method call to it + * but delegates close() calls to DataSourceUtils. + * @param targetDataSource DataSource that the Connection came from + * @return the wrapped Connection + * @see java.sql.Connection#close() + * @see DataSourceUtils#doReleaseConnection + */ + protected Connection getTransactionAwareConnectionProxy(DataSource targetDataSource) { + return (Connection) Proxy.newProxyInstance( + ConnectionProxy.class.getClassLoader(), + new Class[] {ConnectionProxy.class}, + new TransactionAwareInvocationHandler(targetDataSource)); + } + + /** + * Determine whether to obtain a fixed target Connection for the proxy + * or to reobtain the target Connection for each operation. + *

The default implementation returns true for all + * standard cases. This can be overridden through the + * {@link #setReobtainTransactionalConnections "reobtainTransactionalConnections"} + * flag, which enforces a non-fixed target Connection within an active transaction. + * Note that non-transactional access will always use a fixed Connection. + * @param targetDataSource the target DataSource + */ + protected boolean shouldObtainFixedConnection(DataSource targetDataSource) { + return (!TransactionSynchronizationManager.isSynchronizationActive() || + !this.reobtainTransactionalConnections); + } + + + /** + * Invocation handler that delegates close calls on JDBC Connections + * to DataSourceUtils for being aware of thread-bound transactions. + */ + private class TransactionAwareInvocationHandler implements InvocationHandler { + + private final DataSource targetDataSource; + + private Connection target; + + private boolean closed = false; + + public TransactionAwareInvocationHandler(DataSource targetDataSource) { + this.targetDataSource = targetDataSource; + } + + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + // Invocation on ConnectionProxy interface coming in... + + if (method.getName().equals("equals")) { + // Only considered as equal when proxies are identical. + return (proxy == args[0] ? Boolean.TRUE : Boolean.FALSE); + } + else if (method.getName().equals("hashCode")) { + // Use hashCode of Connection proxy. + return new Integer(System.identityHashCode(proxy)); + } + else if (method.getName().equals("toString")) { + // Allow for differentiating between the proxy and the raw Connection. + StringBuffer buf = new StringBuffer("Transaction-aware proxy for target Connection "); + if (this.target != null) { + buf.append("[").append(this.target.toString()).append("]"); + } + else { + buf.append(" from DataSource [").append(this.targetDataSource).append("]"); + } + } + else if (method.getName().equals("close")) { + // Handle close method: only close if not within a transaction. + DataSourceUtils.doReleaseConnection(this.target, this.targetDataSource); + this.closed = true; + return null; + } + + if (this.target == null) { + if (this.closed) { + throw new SQLException("Connection handle already closed"); + } + if (shouldObtainFixedConnection(this.targetDataSource)) { + this.target = DataSourceUtils.doGetConnection(this.targetDataSource); + } + } + Connection actualTarget = this.target; + if (actualTarget == null) { + actualTarget = DataSourceUtils.doGetConnection(this.targetDataSource); + } + + if (method.getName().equals("getTargetConnection")) { + // Handle getTargetConnection method: return underlying Connection. + return actualTarget; + } + + // Invoke method on target Connection. + try { + Object retVal = method.invoke(actualTarget, args); + + // If return value is a Statement, apply transaction timeout. + // Applies to createStatement, prepareStatement, prepareCall. + if (retVal instanceof Statement) { + DataSourceUtils.applyTransactionTimeout((Statement) retVal, this.targetDataSource); + } + + return retVal; + } + catch (InvocationTargetException ex) { + throw ex.getTargetException(); + } + finally { + if (actualTarget != this.target) { + DataSourceUtils.doReleaseConnection(actualTarget, this.targetDataSource); + } + } + } + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/UserCredentialsDataSourceAdapter.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/UserCredentialsDataSourceAdapter.java new file mode 100644 index 0000000000..695acbb0f9 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/UserCredentialsDataSourceAdapter.java @@ -0,0 +1,186 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.datasource; + +import java.sql.Connection; +import java.sql.SQLException; + +import org.springframework.core.NamedThreadLocal; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * An adapter for a target JDBC {@link javax.sql.DataSource}, applying the specified + * user credentials to every standard getConnection() call, implicitly + * invoking getConnection(username, password) on the target. + * All other methods simply delegate to the corresponding methods of the + * target DataSource. + * + *

Can be used to proxy a target JNDI DataSource that does not have user + * credentials configured. Client code can work with this DataSource as usual, + * using the standard getConnection() call. + * + *

In the following example, client code can simply transparently work with + * the preconfigured "myDataSource", implicitly accessing "myTargetDataSource" + * with the specified user credentials. + * + *

+ * <bean id="myTargetDataSource" class="org.springframework.jndi.JndiObjectFactoryBean">
+ *   <property name="jndiName" value="java:comp/env/jdbc/myds"/>
+ * </bean>
+ *
+ * <bean id="myDataSource" class="org.springframework.jdbc.datasource.UserCredentialsDataSourceAdapter">
+ *   <property name="targetDataSource" ref="myTargetDataSource"/>
+ *   <property name="username" value="myusername"/>
+ *   <property name="password" value="mypassword"/>
+ * </bean>
+ * + *

If the "username" is empty, this proxy will simply delegate to the + * standard getConnection() method of the target DataSource. + * This can be used to keep a UserCredentialsDataSourceAdapter bean definition + * just for the option of implicitly passing in user credentials if + * the particular target DataSource requires it. + * + * @author Juergen Hoeller + * @since 1.0.2 + * @see #getConnection + */ +public class UserCredentialsDataSourceAdapter extends DelegatingDataSource { + + private String username; + + private String password; + + private final ThreadLocal threadBoundCredentials = new NamedThreadLocal("Current JDBC user credentials"); + + + /** + * Set the default username that this adapter should use for retrieving Connections. + *

Default is no specific user. Note that an explicitly specified username + * will always override any username/password specified at the DataSource level. + * @see #setPassword + * @see #setCredentialsForCurrentThread(String, String) + * @see #getConnection(String, String) + */ + public void setUsername(String username) { + this.username = username; + } + + /** + * Set the default user's password that this adapter should use for retrieving Connections. + *

Default is no specific password. Note that an explicitly specified username + * will always override any username/password specified at the DataSource level. + * @see #setUsername + * @see #setCredentialsForCurrentThread(String, String) + * @see #getConnection(String, String) + */ + public void setPassword(String password) { + this.password = password; + } + + + /** + * Set user credententials for this proxy and the current thread. + * The given username and password will be applied to all subsequent + * getConnection() calls on this DataSource proxy. + *

This will override any statically specified user credentials, + * that is, values of the "username" and "password" bean properties. + * @param username the username to apply + * @param password the password to apply + * @see #removeCredentialsFromCurrentThread + */ + public void setCredentialsForCurrentThread(String username, String password) { + this.threadBoundCredentials.set(new JdbcUserCredentials(username, password)); + } + + /** + * Remove any user credentials for this proxy from the current thread. + * Statically specified user credentials apply again afterwards. + * @see #setCredentialsForCurrentThread + */ + public void removeCredentialsFromCurrentThread() { + this.threadBoundCredentials.set(null); + } + + + /** + * Determine whether there are currently thread-bound credentials, + * using them if available, falling back to the statically specified + * username and password (i.e. values of the bean properties) else. + *

Delegates to {@link #doGetConnection(String, String)} with the + * determined credentials as parameters. + */ + public Connection getConnection() throws SQLException { + JdbcUserCredentials threadCredentials = (JdbcUserCredentials) this.threadBoundCredentials.get(); + if (threadCredentials != null) { + return doGetConnection(threadCredentials.username, threadCredentials.password); + } + else { + return doGetConnection(this.username, this.password); + } + } + + /** + * Simply delegates to {@link #doGetConnection(String, String)}, + * keeping the given user credentials as-is. + */ + public Connection getConnection(String username, String password) throws SQLException { + return doGetConnection(username, password); + } + + /** + * This implementation delegates to the getConnection(username, password) + * method of the target DataSource, passing in the specified user credentials. + * If the specified username is empty, it will simply delegate to the standard + * getConnection() method of the target DataSource. + * @param username the username to use + * @param password the password to use + * @return the Connection + * @see javax.sql.DataSource#getConnection(String, String) + * @see javax.sql.DataSource#getConnection() + */ + protected Connection doGetConnection(String username, String password) throws SQLException { + Assert.state(getTargetDataSource() != null, "'targetDataSource' is required"); + if (StringUtils.hasLength(username)) { + return getTargetDataSource().getConnection(username, password); + } + else { + return getTargetDataSource().getConnection(); + } + } + + + /** + * Inner class used as ThreadLocal value. + */ + private static class JdbcUserCredentials { + + public final String username; + + public final String password; + + private JdbcUserCredentials(String username, String password) { + this.username = username; + this.password = password; + } + + public String toString() { + return "JdbcUserCredentials[username='" + this.username + "',password='" + this.password + "']"; + } + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/WebSphereDataSourceAdapter.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/WebSphereDataSourceAdapter.java new file mode 100644 index 0000000000..cd9657632e --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/WebSphereDataSourceAdapter.java @@ -0,0 +1,177 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.datasource; + +import java.lang.reflect.Method; +import java.sql.Connection; +import java.sql.SQLException; + +import javax.sql.DataSource; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; + +/** + * {@link DataSource} implementation that delegates all calls to a WebSphere + * target {@link DataSource}, typically obtained from JNDI, applying a current + * isolation level and/or current user credentials to every Connection obtained + * from it. + * + *

Uses IBM-specific API to get a JDBC Connection with a specific isolation + * level (and read-only flag) from a WebSphere DataSource + * (IBM code example). + * Supports the transaction-specific isolation level exposed by + * {@link org.springframework.transaction.support.TransactionSynchronizationManager#getCurrentTransactionIsolationLevel()}. + * It's also possible to specify a default isolation level, to be applied when the + * current Spring-managed transaction does not define a specific isolation level. + * + *

Usage example, defining the target DataSource as an inner-bean JNDI lookup + * (of course, you can link to any WebSphere DataSource through a bean reference): + * + *

+ * <bean id="myDataSource" class="org.springframework.jdbc.datasource.WebSphereDataSourceAdapter">
+ *   <property name="targetDataSource">
+ *     <bean class="org.springframework.jndi.JndiObjectFactoryBean">
+ *       <property name="jndiName" value="jdbc/myds"/>
+ *     </bean>
+ *   </property>
+ * </bean>
+ * + * Thanks to Ricardo Olivieri for submitting the original implementation + * of this approach! + * + * @author Juergen Hoeller + * @author Lari Hotari + * @author Ricardo N. Olivieri + * @since 2.0.3 + * @see com.ibm.websphere.rsadapter.JDBCConnectionSpec + * @see com.ibm.websphere.rsadapter.WSDataSource#getConnection(com.ibm.websphere.rsadapter.JDBCConnectionSpec) + * @see org.springframework.transaction.support.TransactionSynchronizationManager#getCurrentTransactionIsolationLevel() + * @see org.springframework.transaction.support.TransactionSynchronizationManager#isCurrentTransactionReadOnly() + */ +public class WebSphereDataSourceAdapter extends IsolationLevelDataSourceAdapter { + + protected final Log logger = LogFactory.getLog(getClass()); + + private Class wsDataSourceClass; + + private Method newJdbcConnSpecMethod; + + private Method wsDataSourceGetConnectionMethod; + + private Method setTransactionIsolationMethod; + + private Method setReadOnlyMethod; + + private Method setUserNameMethod; + + private Method setPasswordMethod; + + + /** + * This constructor retrieves the WebSphere JDBC connection spec API, + * so we can get obtain specific WebSphere Connections using reflection. + */ + public WebSphereDataSourceAdapter() { + try { + this.wsDataSourceClass = getClass().getClassLoader().loadClass("com.ibm.websphere.rsadapter.WSDataSource"); + Class jdbcConnSpecClass = getClass().getClassLoader().loadClass("com.ibm.websphere.rsadapter.JDBCConnectionSpec"); + Class wsrraFactoryClass = getClass().getClassLoader().loadClass("com.ibm.websphere.rsadapter.WSRRAFactory"); + this.newJdbcConnSpecMethod = wsrraFactoryClass.getMethod("createJDBCConnectionSpec", (Class[]) null); + this.wsDataSourceGetConnectionMethod = + this.wsDataSourceClass.getMethod("getConnection", new Class[] {jdbcConnSpecClass}); + this.setTransactionIsolationMethod = + jdbcConnSpecClass.getMethod("setTransactionIsolation", new Class[] {int.class}); + this.setReadOnlyMethod = jdbcConnSpecClass.getMethod("setReadOnly", new Class[] {Boolean.class}); + this.setUserNameMethod = jdbcConnSpecClass.getMethod("setUserName", new Class[] {String.class}); + this.setPasswordMethod = jdbcConnSpecClass.getMethod("setPassword", new Class[] {String.class}); + } + catch (Exception ex) { + throw new IllegalStateException( + "Could not initialize WebSphereDataSourceAdapter because WebSphere API classes are not available: " + ex); + } + } + + /** + * Checks that the specified 'targetDataSource' actually is + * a WebSphere WSDataSource. + */ + public void afterPropertiesSet() { + super.afterPropertiesSet(); + + if (!this.wsDataSourceClass.isInstance(getTargetDataSource())) { + throw new IllegalStateException( + "Specified 'targetDataSource' is not a WebSphere WSDataSource: " + getTargetDataSource()); + } + } + + + /** + * Builds a WebSphere JDBCConnectionSpec object for the current settings + * and calls WSDataSource.getConnection(JDBCConnectionSpec). + * @see #createConnectionSpec + * @see com.ibm.websphere.rsadapter.WSDataSource#getConnection(com.ibm.websphere.rsadapter.JDBCConnectionSpec) + */ + protected Connection doGetConnection(String username, String password) throws SQLException { + // Create JDBCConnectionSpec using current isolation level value and read-only flag. + Object connSpec = createConnectionSpec( + getCurrentIsolationLevel(), getCurrentReadOnlyFlag(), username, password); + if (logger.isDebugEnabled()) { + logger.debug("Obtaining JDBC Connection from WebSphere DataSource [" + + getTargetDataSource() + "], using ConnectionSpec [" + connSpec + "]"); + } + // Create Connection through invoking WSDataSource.getConnection(JDBCConnectionSpec) + return (Connection) ReflectionUtils.invokeJdbcMethod( + this.wsDataSourceGetConnectionMethod, getTargetDataSource(), new Object[] {connSpec}); + } + + /** + * Create a WebSphere JDBCConnectionSpec object for the given charateristics. + *

The default implementation uses reflection to apply the given settings. + * Can be overridden in subclasses to customize the JDBCConnectionSpec object + * (JDBCConnectionSpec javadoc; + * IBM developerWorks article). + * @param isolationLevel the isolation level to apply (or null if none) + * @param readOnlyFlag the read-only flag to apply (or null if none) + * @param username the username to apply (null or empty indicates the default) + * @param password the password to apply (may be null or empty) + * @throws SQLException if thrown by JDBCConnectionSpec API methods + * @see com.ibm.websphere.rsadapter.JDBCConnectionSpec + */ + protected Object createConnectionSpec( + Integer isolationLevel, Boolean readOnlyFlag, String username, String password) throws SQLException { + + Object connSpec = ReflectionUtils.invokeJdbcMethod(this.newJdbcConnSpecMethod, null); + if (isolationLevel != null) { + ReflectionUtils.invokeJdbcMethod(this.setTransactionIsolationMethod, connSpec, new Object[] {isolationLevel}); + } + if (readOnlyFlag != null) { + ReflectionUtils.invokeJdbcMethod(this.setReadOnlyMethod, connSpec, new Object[] {readOnlyFlag}); + } + // If the username is empty, we'll simply let the target DataSource + // use its default credentials. + if (StringUtils.hasLength(username)) { + ReflectionUtils.invokeJdbcMethod(this.setUserNameMethod, connSpec, new Object[] {username}); + ReflectionUtils.invokeJdbcMethod(this.setPasswordMethod, connSpec, new Object[] {password}); + } + return connSpec; + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/AbstractRoutingDataSource.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/AbstractRoutingDataSource.java new file mode 100644 index 0000000000..10267f2618 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/AbstractRoutingDataSource.java @@ -0,0 +1,184 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.datasource.lookup; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import javax.sql.DataSource; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.jdbc.datasource.AbstractDataSource; +import org.springframework.util.Assert; + +/** + * Abstract {@link javax.sql.DataSource} implementation that routes {@link #getConnection()} + * calls to one of various target DataSources based on a lookup key. The latter is usually + * (but not necessarily) determined through some thread-bound transaction context. + * + * @author Juergen Hoeller + * @since 2.0.1 + * @see #setTargetDataSources + * @see #setDefaultTargetDataSource + * @see #determineCurrentLookupKey() + */ +public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean { + + private Map targetDataSources; + + private Object defaultTargetDataSource; + + private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup(); + + private Map resolvedDataSources; + + private DataSource resolvedDefaultDataSource; + + + /** + * Specify the map of target DataSources, with the lookup key as key. + * The mapped value can either be a corresponding {@link javax.sql.DataSource} + * instance or a data source name String (to be resolved via a + * {@link #setDataSourceLookup DataSourceLookup}). + *

The key can be of arbitrary type; this class implements the + * generic lookup process only. The concrete key representation will + * be handled by {@link #resolveSpecifiedLookupKey(Object)} and + * {@link #determineCurrentLookupKey()}. + */ + public void setTargetDataSources(Map targetDataSources) { + this.targetDataSources = targetDataSources; + } + + /** + * Specify the default target DataSource, if any. + * The mapped value can either be a corresponding {@link javax.sql.DataSource} + * instance or a data source name String (to be resolved via a + * {@link #setDataSourceLookup DataSourceLookup}). + *

This DataSource will be used as target if none of the keyed + * {@link #setTargetDataSources targetDataSources} match the + * {@link #determineCurrentLookupKey()} current lookup key. + */ + public void setDefaultTargetDataSource(Object defaultTargetDataSource) { + this.defaultTargetDataSource = defaultTargetDataSource; + } + + /** + * Set the DataSourceLookup implementation to use for resolving data source + * name Strings in the {@link #setTargetDataSources targetDataSources} map. + *

Default is a {@link JndiDataSourceLookup}, allowing the JNDI names + * of application server DataSources to be specified directly. + */ + public void setDataSourceLookup(DataSourceLookup dataSourceLookup) { + this.dataSourceLookup = (dataSourceLookup != null ? dataSourceLookup : new JndiDataSourceLookup()); + } + + + public void afterPropertiesSet() { + if (this.targetDataSources == null) { + throw new IllegalArgumentException("targetDataSources is required"); + } + this.resolvedDataSources = new HashMap(this.targetDataSources.size()); + for (Iterator it = this.targetDataSources.entrySet().iterator(); it.hasNext();) { + Map.Entry entry = (Map.Entry) it.next(); + Object lookupKey = resolveSpecifiedLookupKey(entry.getKey()); + DataSource dataSource = resolveSpecifiedDataSource(entry.getValue()); + this.resolvedDataSources.put(lookupKey, dataSource); + } + if (this.defaultTargetDataSource != null) { + this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource); + } + } + + /** + * Resolve the specified data source object into a DataSource instance. + *

The default implementation handles DataSource instances and data source + * names (to be resolved via a {@link #setDataSourceLookup DataSourceLookup}). + * @param dataSource the data source value object as specified in the + * {@link #setTargetDataSources targetDataSources} map + * @return the resolved DataSource (never null) + * @throws IllegalArgumentException in case of an unsupported value type + */ + protected DataSource resolveSpecifiedDataSource(Object dataSource) throws IllegalArgumentException { + if (dataSource instanceof DataSource) { + return (DataSource) dataSource; + } + else if (dataSource instanceof String) { + return this.dataSourceLookup.getDataSource((String) dataSource); + } + else { + throw new IllegalArgumentException( + "Illegal data source value - only [javax.sql.DataSource] and String supported: " + dataSource); + } + } + + + public Connection getConnection() throws SQLException { + return determineTargetDataSource().getConnection(); + } + + public Connection getConnection(String username, String password) throws SQLException { + return determineTargetDataSource().getConnection(username, password); + } + + /** + * Retrieve the current target DataSource. Determines the + * {@link #determineCurrentLookupKey() current lookup key}, performs + * a lookup in the {@link #setTargetDataSources targetDataSources} map, + * falls back to the specified + * {@link #setDefaultTargetDataSource default target DataSource} if necessary. + * @see #determineCurrentLookupKey() + */ + protected DataSource determineTargetDataSource() { + Assert.notNull(this.resolvedDataSources, "DataSource router not initialized"); + Object lookupKey = determineCurrentLookupKey(); + DataSource dataSource = (DataSource) this.resolvedDataSources.get(lookupKey); + if (dataSource == null) { + dataSource = this.resolvedDefaultDataSource; + } + if (dataSource == null) { + throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]"); + } + return dataSource; + } + + + /** + * Resolve the given lookup key object, as specified in the + * {@link #setTargetDataSources targetDataSources} map, into + * the actual lookup key to be used for matching with the + * {@link #determineCurrentLookupKey() current lookup key}. + *

The default implementation simply returns the given key as-is. + * @param lookupKey the lookup key object as specified by the user + * @return the lookup key as needed for matching + */ + protected Object resolveSpecifiedLookupKey(Object lookupKey) { + return lookupKey; + } + + /** + * Determine the current lookup key. This will typically be + * implemented to check a thread-bound transaction context. + *

Allows for arbitrary keys. The returned key needs + * to match the stored lookup key type, as resolved by the + * {@link #resolveSpecifiedLookupKey} method. + */ + protected abstract Object determineCurrentLookupKey(); + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/BeanFactoryDataSourceLookup.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/BeanFactoryDataSourceLookup.java new file mode 100644 index 0000000000..49919dc26e --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/BeanFactoryDataSourceLookup.java @@ -0,0 +1,81 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc.datasource.lookup; + +import javax.sql.DataSource; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.util.Assert; + +/** + * {@link DataSourceLookup} implementation based on a Spring {@link BeanFactory}. + * + *

Will lookup Spring managed beans identified by bean name, + * expecting them to be of type javax.sql.DataSource. + * + * @author Costin Leau + * @author Juergen Hoeller + * @since 2.0 + * @see org.springframework.beans.factory.BeanFactory + */ +public class BeanFactoryDataSourceLookup implements DataSourceLookup, BeanFactoryAware { + + private BeanFactory beanFactory; + + + /** + * Create a new instance of the {@link BeanFactoryDataSourceLookup} class. + *

The BeanFactory to access must be set via setBeanFactory. + * @see #setBeanFactory + */ + public BeanFactoryDataSourceLookup() { + } + + /** + * Create a new instance of the {@link BeanFactoryDataSourceLookup} class. + *

Use of this constructor is redundant if this object is being created + * by a Spring IoC container, as the supplied {@link BeanFactory} will be + * replaced by the {@link BeanFactory} that creates it (c.f. the + * {@link BeanFactoryAware} contract). So only use this constructor if you + * are using this class outside the context of a Spring IoC container. + * @param beanFactory the bean factory to be used to lookup {@link DataSource DataSources} + */ + public BeanFactoryDataSourceLookup(BeanFactory beanFactory) { + Assert.notNull(beanFactory, "BeanFactory is required"); + this.beanFactory = beanFactory; + } + + + public void setBeanFactory(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + + public DataSource getDataSource(String dataSourceName) throws DataSourceLookupFailureException { + Assert.state(this.beanFactory != null, "BeanFactory is required"); + try { + return (DataSource) this.beanFactory.getBean(dataSourceName, DataSource.class); + } + catch (BeansException ex) { + throw new DataSourceLookupFailureException( + "Failed to look up DataSource bean with name '" + dataSourceName + "'", ex); + } + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/DataSourceLookup.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/DataSourceLookup.java new file mode 100644 index 0000000000..dd10476a02 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/DataSourceLookup.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed 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.springframework.jdbc.datasource.lookup; + +import javax.sql.DataSource; + +/** + * Strategy interface for looking up DataSources by name. + * + *

Used, for example, to resolve data source names in JPA + * persistence.xml files. + * + * @author Costin Leau + * @author Juergen Hoeller + * @since 2.0 + * @see org.springframework.orm.jpa.persistenceunit.DefaultPersistenceUnitManager#setDataSourceLookup + */ +public interface DataSourceLookup { + + /** + * Retrieve the DataSource identified by the given name. + * @param dataSourceName the name of the DataSource + * @return the DataSource (never null) + * @throws DataSourceLookupFailureException if the lookup failed + */ + DataSource getDataSource(String dataSourceName) throws DataSourceLookupFailureException; + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/DataSourceLookupFailureException.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/DataSourceLookupFailureException.java new file mode 100644 index 0000000000..cc62b9b999 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/DataSourceLookupFailureException.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed 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.springframework.jdbc.datasource.lookup; + +import org.springframework.dao.NonTransientDataAccessException; + +/** + * Exception to be thrown by a DataSourceLookup implementation, + * indicating that the specified DataSource could not be obtained. + * + * @author Juergen Hoeller + * @since 2.0 + */ +public class DataSourceLookupFailureException extends NonTransientDataAccessException { + + /** + * Constructor for DataSourceLookupFailureException. + * @param msg the detail message + */ + public DataSourceLookupFailureException(String msg) { + super(msg); + } + + /** + * Constructor for DataSourceLookupFailureException. + * @param msg the detail message + * @param cause the root cause (usually from using a underlying + * lookup API such as JNDI) + */ + public DataSourceLookupFailureException(String msg, Throwable cause) { + super(msg, cause); + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/IsolationLevelDataSourceRouter.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/IsolationLevelDataSourceRouter.java new file mode 100644 index 0000000000..1080b728b3 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/IsolationLevelDataSourceRouter.java @@ -0,0 +1,124 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed 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.springframework.jdbc.datasource.lookup; + +import org.springframework.core.Constants; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.support.DefaultTransactionDefinition; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +/** + * DataSource that routes to one of various target DataSources based on the + * current transaction isolation level. The target DataSources need to be + * configured with the isolation level name as key, as defined on the + * {@link org.springframework.transaction.TransactionDefinition TransactionDefinition interface}. + * + *

This is particularly useful in combination with JTA transaction management + * (typically through Spring's {@link org.springframework.transaction.jta.JtaTransactionManager}). + * Standard JTA does not support transaction-specific isolation levels. Some JTA + * providers support isolation levels as a vendor-specific extension (e.g. WebLogic), + * which is the preferred way of addressing this. As alternative (e.g. on WebSphere), + * the target database can be represented through multiple JNDI DataSources, each + * configured with a different isolation level (for the entire DataSource). + * The present DataSource router allows to transparently switch to the + * appropriate DataSource based on the current transaction's isolation level. + * + *

The configuration can for example look like this, assuming that the target + * DataSources are defined as individual Spring beans with names + * "myRepeatableReadDataSource", "mySerializableDataSource" and "myDefaultDataSource": + * + *

+ * <bean id="dataSourceRouter" class="org.springframework.jdbc.datasource.lookup.IsolationLevelDataSourceRouter">
+ *   <property name="targetDataSources">
+ *     <map>
+ *       <entry key="ISOLATION_REPEATABLE_READ" value-ref="myRepeatableReadDataSource"/>
+ *       <entry key="ISOLATION_SERIALIZABLE" value-ref="mySerializableDataSource"/>
+ *     </map>
+ *   </property>
+ *   <property name="defaultTargetDataSource" ref="myDefaultDataSource"/>
+ * </bean>
+ * + * Alternatively, the keyed values can also be data source names, to be resolved + * through a {@link #setDataSourceLookup DataSourceLookup}: by default, JNDI + * names for a standard JNDI lookup. This allows for a single concise definition + * without the need for separate DataSource bean definitions. + * + *
+ * <bean id="dataSourceRouter" class="org.springframework.jdbc.datasource.lookup.IsolationLevelDataSourceRouter">
+ *   <property name="targetDataSources">
+ *     <map>
+ *       <entry key="ISOLATION_REPEATABLE_READ" value="java:comp/env/jdbc/myrrds"/>
+ *       <entry key="ISOLATION_SERIALIZABLE" value="java:comp/env/jdbc/myserds"/>
+ *     </map>
+ *   </property>
+ *   <property name="defaultTargetDataSource" value="java:comp/env/jdbc/mydefds"/>
+ * </bean>
+ * + * Note: If you are using this router in combination with Spring's + * {@link org.springframework.transaction.jta.JtaTransactionManager}, + * don't forget to switch the "allowCustomIsolationLevels" flag to "true". + * (By default, JtaTransactionManager will only accept a default isolation level + * because of the lack of isolation level support in standard JTA itself.) + * + *
+ * <bean id="transactionManager" class="org.springframework.transaction.jta.JtaTransactionManager">
+ *   <property name="allowCustomIsolationLevels" value="true"/>
+ * </bean>
+ * + * @author Juergen Hoeller + * @since 2.0.1 + * @see #setTargetDataSources + * @see #setDefaultTargetDataSource + * @see org.springframework.transaction.TransactionDefinition#ISOLATION_READ_UNCOMMITTED + * @see org.springframework.transaction.TransactionDefinition#ISOLATION_READ_COMMITTED + * @see org.springframework.transaction.TransactionDefinition#ISOLATION_REPEATABLE_READ + * @see org.springframework.transaction.TransactionDefinition#ISOLATION_SERIALIZABLE + * @see org.springframework.transaction.jta.JtaTransactionManager + */ +public class IsolationLevelDataSourceRouter extends AbstractRoutingDataSource { + + /** Constants instance for TransactionDefinition */ + private static final Constants constants = new Constants(TransactionDefinition.class); + + + /** + * Supports Integer values for the isolation level constants + * as well as isolation level names as defined on the + * {@link org.springframework.transaction.TransactionDefinition TransactionDefinition interface}. + */ + protected Object resolveSpecifiedLookupKey(Object lookupKey) { + if (lookupKey instanceof Integer) { + return (Integer) lookupKey; + } + else if (lookupKey instanceof String) { + String constantName = (String) lookupKey; + if (constantName == null || !constantName.startsWith(DefaultTransactionDefinition.PREFIX_ISOLATION)) { + throw new IllegalArgumentException("Only isolation constants allowed"); + } + return constants.asNumber(constantName); + } + else { + throw new IllegalArgumentException( + "Invalid lookup key - needs to be isolation level Integer or isolation level name String: " + lookupKey); + } + } + + protected Object determineCurrentLookupKey() { + return TransactionSynchronizationManager.getCurrentTransactionIsolationLevel(); + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/JndiDataSourceLookup.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/JndiDataSourceLookup.java new file mode 100644 index 0000000000..327e6a06f1 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/JndiDataSourceLookup.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.datasource.lookup; + +import javax.naming.NamingException; +import javax.sql.DataSource; + +import org.springframework.jndi.JndiLocatorSupport; + +/** + * JNDI-based {@link DataSourceLookup} implementation. + * + *

For specific JNDI configuration, it is recommended to configure + * the "jndiEnvironment"/"jndiTemplate" properties. + * + * @author Costin Leau + * @author Juergen Hoeller + * @since 2.0 + * @see #setJndiEnvironment + * @see #setJndiTemplate + */ +public class JndiDataSourceLookup extends JndiLocatorSupport implements DataSourceLookup { + + public JndiDataSourceLookup() { + setResourceRef(true); + } + + public DataSource getDataSource(String dataSourceName) throws DataSourceLookupFailureException { + try { + return (DataSource) lookup(dataSourceName, DataSource.class); + } + catch (NamingException ex) { + throw new DataSourceLookupFailureException( + "Failed to look up JNDI DataSource with name '" + dataSourceName + "'", ex); + } + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/MapDataSourceLookup.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/MapDataSourceLookup.java new file mode 100644 index 0000000000..e91852651b --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/MapDataSourceLookup.java @@ -0,0 +1,117 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed 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.springframework.jdbc.datasource.lookup; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import javax.sql.DataSource; + +import org.springframework.util.Assert; + +/** + * Simple {@link DataSourceLookup} implementation that relies on a map for doing lookups. + * + *

Useful for testing environments or applications that need to match arbitrary + * {@link String} names to target {@link DataSource} objects. + * + * @author Costin Leau + * @author Juergen Hoeller + * @author Rick Evans + * @since 2.0 + */ +public class MapDataSourceLookup implements DataSourceLookup { + + private final Map dataSources = new HashMap(4); + + + /** + * Create a new instance of the {@link MapDataSourceLookup} class. + */ + public MapDataSourceLookup() { + } + + /** + * Create a new instance of the {@link MapDataSourceLookup} class. + * @param dataSources the {@link Map} of {@link DataSource DataSources}; the keys + * are {@link String Strings}, the values are actual {@link DataSource} instances. + */ + public MapDataSourceLookup(Map dataSources) { + setDataSources(dataSources); + } + + /** + * Create a new instance of the {@link MapDataSourceLookup} class. + * @param dataSourceName the name under which the supplied {@link DataSource} is to be added + * @param dataSource the {@link DataSource} to be added + */ + public MapDataSourceLookup(String dataSourceName, DataSource dataSource) { + addDataSource(dataSourceName, dataSource); + } + + + /** + * Set the {@link Map} of {@link DataSource DataSources}; the keys + * are {@link String Strings}, the values are actual {@link DataSource} instances. + *

If the supplied {@link Map} is null, then this method + * call effectively has no effect. + * @param dataSources said {@link Map} of {@link DataSource DataSources} + */ + public void setDataSources(Map dataSources) { + if (dataSources != null) { + this.dataSources.putAll(dataSources); + } + } + + /** + * Get the {@link Map} of {@link DataSource DataSources} maintained by this object. + *

The returned {@link Map} is {@link Collections#unmodifiableMap(java.util.Map) unmodifiable}. + * @return said {@link Map} of {@link DataSource DataSources} (never null) + */ + public Map getDataSources() { + return Collections.unmodifiableMap(this.dataSources); + } + + /** + * Add the supplied {@link DataSource} to the map of {@link DataSource DataSources} + * maintained by this object. + * @param dataSourceName the name under which the supplied {@link DataSource} is to be added + * @param dataSource the {@link DataSource} to be so added + */ + public void addDataSource(String dataSourceName, DataSource dataSource) { + Assert.notNull(dataSourceName, "DataSource name must not be null"); + Assert.notNull(dataSource, "DataSource must not be null"); + this.dataSources.put(dataSourceName, dataSource); + } + + public DataSource getDataSource(String dataSourceName) throws DataSourceLookupFailureException { + Assert.notNull(dataSourceName, "DataSource name must not be null"); + Object value = this.dataSources.get(dataSourceName); + if (value == null) { + throw new DataSourceLookupFailureException( + "No DataSource with name '" + dataSourceName + "' registered"); + } + if (!(value instanceof DataSource)) { + throw new DataSourceLookupFailureException( + "The object [" + value + "] with name '" + dataSourceName + + "' in the DataSource map is not a [javax.sql.DataSource]"); + } + return (DataSource) value; + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/SingleDataSourceLookup.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/SingleDataSourceLookup.java new file mode 100644 index 0000000000..3443a713a8 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/SingleDataSourceLookup.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed 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.springframework.jdbc.datasource.lookup; + +import javax.sql.DataSource; + +import org.springframework.util.Assert; + +/** + * An implementation of the DataSourceLookup that simply wraps a + * single given DataSource, returned for any data source name. + * + * @author Juergen Hoeller + * @since 2.0 + */ +public class SingleDataSourceLookup implements DataSourceLookup { + + private final DataSource dataSource; + + + /** + * Create a new instance of the {@link SingleDataSourceLookup} class. + * @param dataSource the single {@link DataSource} to wrap + */ + public SingleDataSourceLookup(DataSource dataSource) { + Assert.notNull(dataSource, "DataSource must not be null"); + this.dataSource = dataSource; + } + + + public DataSource getDataSource(String dataSourceName) { + return this.dataSource; + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/package.html b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/package.html new file mode 100644 index 0000000000..094d2aa71d --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/package.html @@ -0,0 +1,7 @@ + + + +Provides a strategy for looking up JDBC DataSources by name. + + + diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/package.html b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/package.html new file mode 100644 index 0000000000..1e72155619 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/package.html @@ -0,0 +1,9 @@ + + + +Provides a utility class for easy DataSource access, +a PlatformTransactionManager for a single DataSource, +and various simple DataSource implementations. + + + diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/object/BatchSqlUpdate.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/object/BatchSqlUpdate.java new file mode 100644 index 0000000000..ed1977f19e --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/object/BatchSqlUpdate.java @@ -0,0 +1,247 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed 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.springframework.jdbc.object; + +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +import javax.sql.DataSource; + +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.core.BatchPreparedStatementSetter; + +/** + * SqlUpdate subclass that performs batch update operations. Encapsulates + * queuing up records to be updated, and adds them as a single batch once + * flush is called or the given batch size has been met. + * + *

Note that this class is a non-thread-safe object, in contrast + * to all other JDBC operations objects in this package. You need to create + * a new instance of it for each use, or call reset before + * reuse within the same thread. + * + * @author Keith Donald + * @author Juergen Hoeller + * @since 1.1 + * @see #flush + * @see #reset + */ +public class BatchSqlUpdate extends SqlUpdate { + + /** + * Default number of inserts to accumulate before commiting a batch (5000). + */ + public static int DEFAULT_BATCH_SIZE = 5000; + + + private int batchSize = DEFAULT_BATCH_SIZE; + + private boolean trackRowsAffected = true; + + private final LinkedList parameterQueue = new LinkedList(); + + private final List rowsAffected = new ArrayList(); + + + /** + * Constructor to allow use as a JavaBean. DataSource and SQL + * must be supplied before compilation and use. + * @see #setDataSource + * @see #setSql + */ + public BatchSqlUpdate() { + super(); + } + + /** + * Construct an update object with a given DataSource and SQL. + * @param ds DataSource to use to obtain connections + * @param sql SQL statement to execute + */ + public BatchSqlUpdate(DataSource ds, String sql) { + super(ds, sql); + } + + /** + * Construct an update object with a given DataSource, SQL + * and anonymous parameters. + * @param ds DataSource to use to obtain connections + * @param sql SQL statement to execute + * @param types SQL types of the parameters, as defined in the + * java.sql.Types class + * @see java.sql.Types + */ + public BatchSqlUpdate(DataSource ds, String sql, int[] types) { + super(ds, sql, types); + } + + /** + * Construct an update object with a given DataSource, SQL, + * anonymous parameters and specifying the maximum number of rows + * that may be affected. + * @param ds DataSource to use to obtain connections + * @param sql SQL statement to execute + * @param types SQL types of the parameters, as defined in the + * java.sql.Types class + * @param batchSize the number of statements that will trigger + * an automatic intermediate flush + * @see java.sql.Types + */ + public BatchSqlUpdate(DataSource ds, String sql, int[] types, int batchSize) { + super(ds, sql, types); + setBatchSize(batchSize); + } + + + /** + * Set the number of statements that will trigger an automatic intermediate + * flush. update calls or the given statement parameters will + * be queued until the batch size is met, at which point it will empty the + * queue and execute the batch. + *

You can also flush already queued statements with an explicit + * flush call. Note that you need to this after queueing + * all parameters to guarantee that all statements have been flushed. + */ + public void setBatchSize(int batchSize) { + this.batchSize = batchSize; + } + + /** + * Set whether to track the rows affected by batch updates performed + * by this operation object. + *

Default is "true". Turn this off to save the memory needed for + * the list of row counts. + * @see #getRowsAffected() + */ + public void setTrackRowsAffected(boolean trackRowsAffected) { + this.trackRowsAffected = trackRowsAffected; + } + + /** + * BatchSqlUpdate does not support BLOB or CLOB parameters. + */ + protected boolean supportsLobParameters() { + return false; + } + + + /** + * Overridden version of update that adds the given statement + * parameters to the queue rather than executing them immediately. + * All other update methods of the SqlUpdate base class go + * through this method and will thus behave similarly. + *

You need to call flush to actually execute the batch. + * If the specified batch size is reached, an implicit flush will happen; + * you still need to finally call flush to flush all statements. + * @param params array of parameter objects + * @return the number of rows affected by the update (always -1, + * meaning "not applicable", as the statement is not actually + * executed by this method) + * @see #flush + */ + public int update(Object[] params) throws DataAccessException { + validateParameters(params); + this.parameterQueue.add(params.clone()); + + if (this.parameterQueue.size() == this.batchSize) { + if (logger.isDebugEnabled()) { + logger.debug("Triggering auto-flush because queue reached batch size of " + this.batchSize); + } + flush(); + } + + return -1; + } + + /** + * Trigger any queued update operations to be added as a final batch. + * @return an array of the number of rows affected by each statement + */ + public int[] flush() { + if (this.parameterQueue.isEmpty()) { + return new int[0]; + } + + int[] rowsAffected = getJdbcTemplate().batchUpdate( + getSql(), + new BatchPreparedStatementSetter() { + public int getBatchSize() { + return parameterQueue.size(); + } + public void setValues(PreparedStatement ps, int index) throws SQLException { + Object[] params = (Object[]) parameterQueue.removeFirst(); + newPreparedStatementSetter(params).setValues(ps); + } + }); + + if (this.trackRowsAffected) { + for (int i = 0; i < rowsAffected.length; i++) { + this.rowsAffected.add(new Integer(rowsAffected[i])); + } + } + for (int i = 0; i < rowsAffected.length; i++) { + checkRowsAffected(rowsAffected[i]); + } + return rowsAffected; + } + + /** + * Return the current number of statements or statement parameters + * in the queue. + */ + public int getQueueCount() { + return this.parameterQueue.size(); + } + + /** + * Return the number of already executed statements. + */ + public int getExecutionCount() { + return this.rowsAffected.size(); + } + + /** + * Return the number of affected rows for all already executed statements. + * Accumulates all of flush's return values until + * reset is invoked. + * @return an array of the number of rows affected by each statement + * @see #reset + */ + public int[] getRowsAffected() { + int[] result = new int[this.rowsAffected.size()]; + int i = 0; + for (Iterator it = this.rowsAffected.iterator(); it.hasNext(); i++) { + Integer rowCount = (Integer) it.next(); + result[i] = rowCount.intValue(); + } + return result; + } + + /** + * Reset the statement parameter queue, the rows affected cache, + * and the execution count. + */ + public void reset() { + this.parameterQueue.clear(); + this.rowsAffected.clear(); + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/object/MappingSqlQuery.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/object/MappingSqlQuery.java new file mode 100644 index 0000000000..9e8a60793b --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/object/MappingSqlQuery.java @@ -0,0 +1,82 @@ +/* + * Copyright 2002-2005 the original author or authors. + * + * Licensed 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.springframework.jdbc.object; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Map; + +import javax.sql.DataSource; + +/** + * Reusable query in which concrete subclasses must implement the abstract + * mapRow(ResultSet, int) method to convert each row of the JDBC ResultSet + * into an object. + * + *

Simplifies MappingSqlQueryWithParameters API by dropping parameters and + * context. Most subclasses won't care about parameters. If you don't use + * contextual information, subclass this instead of MappingSqlQueryWithParameters. + * + * @author Rod Johnson + * @author Thomas Risberg + * @author Jean-Pierre Pawlak + * @see MappingSqlQueryWithParameters + */ +public abstract class MappingSqlQuery extends MappingSqlQueryWithParameters { + + /** + * Constructor that allows use as a JavaBean. + */ + public MappingSqlQuery() { + } + + /** + * Convenient constructor with DataSource and SQL string. + * @param ds DataSource to use to obtain connections + * @param sql SQL to run + */ + public MappingSqlQuery(DataSource ds, String sql) { + super(ds, sql); + } + + /** + * This method is implemented to invoke the simpler mapRow + * template method, ignoring parameters. + * @see #mapRow(ResultSet, int) + */ + protected final Object mapRow(ResultSet rs, int rowNum, Object[] parameters, Map context) + throws SQLException { + + return mapRow(rs, rowNum); + } + + /** + * Subclasses must implement this method to convert each row of the + * ResultSet into an object of the result type. + *

Subclasses of this class, as opposed to direct subclasses of + * MappingSqlQueryWithParameters, don't need to concern themselves + * with the parameters to the execute method of the query object. + * @param rs ResultSet we're working through + * @param rowNum row number (from 0) we're up to + * @return an object of the result type + * @throws SQLException if there's an error extracting data. + * Subclasses can simply not catch SQLExceptions, relying on the + * framework to clean up. + */ + protected abstract Object mapRow(ResultSet rs, int rowNum) throws SQLException; + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/object/MappingSqlQueryWithParameters.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/object/MappingSqlQueryWithParameters.java new file mode 100644 index 0000000000..8a8c9192ef --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/object/MappingSqlQueryWithParameters.java @@ -0,0 +1,119 @@ +/* + * Copyright 2002-2005 the original author or authors. + * + * Licensed 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.springframework.jdbc.object; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Map; + +import javax.sql.DataSource; + +import org.springframework.jdbc.core.RowMapper; + +/** + * Reusable RDBMS query in which concrete subclasses must implement + * the abstract mapRow(ResultSet, int) method to map each row of + * the JDBC ResultSet into an object. + * + *

Such manual mapping is usually preferable to "automatic" + * mapping using reflection, which can become complex in non-trivial + * cases. For example, the present class allows different objects + * to be used for different rows (for example, if a subclass is indicated). + * It allows computed fields to be set. And there's no need for + * ResultSet columns to have the same names as bean properties. + * The Pareto Principle in action: going the extra mile to automate + * the extraction process makes the framework much more complex + * and delivers little real benefit. + * + *

Subclasses can be constructed providing SQL, parameter types + * and a DataSource. SQL will often vary between subclasses. + * + * @author Rod Johnson + * @author Thomas Risberg + * @author Jean-Pierre Pawlak + * @see org.springframework.jdbc.object.MappingSqlQuery + * @see org.springframework.jdbc.object.SqlQuery + */ +public abstract class MappingSqlQueryWithParameters extends SqlQuery { + + /** + * Constructor to allow use as a JavaBean + */ + public MappingSqlQueryWithParameters() { + } + + /** + * Convenient constructor with DataSource and SQL string. + * @param ds DataSource to use to get connections + * @param sql SQL to run + */ + public MappingSqlQueryWithParameters(DataSource ds, String sql) { + super(ds, sql); + } + + + /** + * Implementation of protected abstract method. This invokes the subclass's + * implementation of the mapRow() method. + */ + protected RowMapper newRowMapper(Object[] parameters, Map context) { + return new RowMapperImpl(parameters, context); + } + + /** + * Subclasses must implement this method to convert each row + * of the ResultSet into an object of the result type. + * @param rs ResultSet we're working through + * @param rowNum row number (from 0) we're up to + * @param parameters to the query (passed to the execute() method). + * Subclasses are rarely interested in these. + * It can be null if there are no parameters. + * @param context passed to the execute() method. + * It can be null if no contextual information is need. + * @return an object of the result type + * @throws SQLException if there's an error extracting data. + * Subclasses can simply not catch SQLExceptions, relying on the + * framework to clean up. + */ + protected abstract Object mapRow(ResultSet rs, int rowNum, Object[] parameters, Map context) + throws SQLException; + + + /** + * Implementation of RowMapper that calls the enclosing + * class's mapRow method for each row. + */ + protected class RowMapperImpl implements RowMapper { + + private final Object[] params; + + private final Map context; + + /** + * Use an array results. More efficient if we know how many results to expect. + */ + public RowMapperImpl(Object[] parameters, Map context) { + this.params = parameters; + this.context = context; + } + + public Object mapRow(ResultSet rs, int rowNum) throws SQLException { + return MappingSqlQueryWithParameters.this.mapRow(rs, rowNum, this.params, this.context); + } + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/object/RdbmsOperation.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/object/RdbmsOperation.java new file mode 100644 index 0000000000..dab50f9c40 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/object/RdbmsOperation.java @@ -0,0 +1,473 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc.object; + +import java.sql.ResultSet; +import java.sql.Types; +import java.util.*; + +import javax.sql.DataSource; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.SqlParameter; + +/** + * An "RDBMS operation" is a multi-threaded, reusable object representing a query, + * update, or stored procedure call. An RDBMS operation is not a command, + * as a command is not reusable. However, execute methods may take commands as + * arguments. Subclasses should be JavaBeans, allowing easy configuration. + * + *

This class and subclasses throw runtime exceptions, defined in the + * (and as thrown by the + * org.springframework.jdbc.core package, which the classes + * in this package use under the hood to perform raw JDBC operations). + * + *

Subclasses should set SQL and add parameters before invoking the + * {@link #compile()} method. The order in which parameters are added is + * significant. The appropriate execute or update + * method can then be invoked. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see SqlQuery + * @see SqlUpdate + * @see StoredProcedure + * @see org.springframework.jdbc.core.JdbcTemplate + */ +public abstract class RdbmsOperation implements InitializingBean { + + /** Logger available to subclasses */ + protected final Log logger = LogFactory.getLog(getClass()); + + /** Lower-level class used to execute SQL */ + private JdbcTemplate jdbcTemplate = new JdbcTemplate(); + + private int resultSetType = ResultSet.TYPE_FORWARD_ONLY; + + private boolean updatableResults = false; + + private boolean returnGeneratedKeys = false; + + private String[] generatedKeysColumnNames = null; + + /** SQL statement */ + private String sql; + + /** List of SqlParameter objects */ + private final List declaredParameters = new LinkedList(); + + /** + * Has this operation been compiled? Compilation means at + * least checking that a DataSource and sql have been provided, + * but subclasses may also implement their own custom validation. + */ + private boolean compiled; + + + /** + * An alternative to the more commonly used setDataSource() when you want to + * use the same JdbcTemplate in multiple RdbmsOperations. This is appropriate if the + * JdbcTemplate has special configuration such as a SQLExceptionTranslator that should + * apply to multiple RdbmsOperation objects. + */ + public void setJdbcTemplate(JdbcTemplate jdbcTemplate) { + if (jdbcTemplate == null) { + throw new IllegalArgumentException("jdbcTemplate must not be null"); + } + this.jdbcTemplate = jdbcTemplate; + } + + /** + * Return the JdbcTemplate object used by this object. + */ + public JdbcTemplate getJdbcTemplate() { + return this.jdbcTemplate; + } + + /** + * Set the JDBC DataSource to obtain connections from. + * @see org.springframework.jdbc.core.JdbcTemplate#setDataSource + */ + public void setDataSource(DataSource dataSource) { + this.jdbcTemplate.setDataSource(dataSource); + } + + /** + * Set the fetch size for this RDBMS operation. This is important for processing + * large result sets: Setting this higher than the default value will increase + * processing speed at the cost of memory consumption; setting this lower can + * avoid transferring row data that will never be read by the application. + *

Default is 0, indicating to use the driver's default. + * @see org.springframework.jdbc.core.JdbcTemplate#setFetchSize + */ + public void setFetchSize(int fetchSize) { + this.jdbcTemplate.setFetchSize(fetchSize); + } + + /** + * Set the maximum number of rows for this RDBMS operation. This is important + * for processing subsets of large result sets, avoiding to read and hold + * the entire result set in the database or in the JDBC driver. + *

Default is 0, indicating to use the driver's default. + * @see org.springframework.jdbc.core.JdbcTemplate#setMaxRows + */ + public void setMaxRows(int maxRows) { + this.jdbcTemplate.setMaxRows(maxRows); + } + + /** + * Set the query timeout for statements that this RDBMS operation executes. + *

Default is 0, indicating to use the JDBC driver's default. + *

Note: Any timeout specified here will be overridden by the remaining + * transaction timeout when executing within a transaction that has a + * timeout specified at the transaction level. + */ + public void setQueryTimeout(int queryTimeout) { + this.jdbcTemplate.setQueryTimeout(queryTimeout); + } + + /** + * Set whether to use statements that return a specific type of ResultSet. + * @param resultSetType the ResultSet type + * @see java.sql.ResultSet#TYPE_FORWARD_ONLY + * @see java.sql.ResultSet#TYPE_SCROLL_INSENSITIVE + * @see java.sql.ResultSet#TYPE_SCROLL_SENSITIVE + * @see java.sql.Connection#prepareStatement(String, int, int) + */ + public void setResultSetType(int resultSetType) { + this.resultSetType = resultSetType; + } + + /** + * Return whether statements will return a specific type of ResultSet. + */ + public int getResultSetType() { + return this.resultSetType; + } + + /** + * Set whether to use statements that are capable of returning + * updatable ResultSets. + * @see java.sql.Connection#prepareStatement(String, int, int) + */ + public void setUpdatableResults(boolean updatableResults) { + if (isCompiled()) { + throw new InvalidDataAccessApiUsageException( + "The updateableResults flag must be set before the operation is compiled"); + } + this.updatableResults = updatableResults; + } + + /** + * Return whether statements will return updatable ResultSets. + */ + public boolean isUpdatableResults() { + return this.updatableResults; + } + + /** + * Set whether prepared statements should be capable of returning + * auto-generated keys. + * @see java.sql.Connection#prepareStatement(String, int) + */ + public void setReturnGeneratedKeys(boolean returnGeneratedKeys) { + if (isCompiled()) { + throw new InvalidDataAccessApiUsageException( + "The returnGeneratedKeys flag must be set before the operation is compiled"); + } + this.returnGeneratedKeys = returnGeneratedKeys; + } + + /** + * Return whether statements should be capable of returning + * auto-generated keys. + */ + public boolean isReturnGeneratedKeys() { + return this.returnGeneratedKeys; + } + + /** + * Set the column names of the auto-generated keys. + * @see java.sql.Connection#prepareStatement(String, String[]) + */ + public void setGeneratedKeysColumnNames(String[] names) { + if (isCompiled()) { + throw new InvalidDataAccessApiUsageException( + "The column names for the generated keys must be set before the operation is compiled"); + } + this.generatedKeysColumnNames = names; + } + + /** + * Return the column names of the auto generated keys. + */ + public String[] getGeneratedKeysColumnNames() { + return this.generatedKeysColumnNames; + } + + /** + * Set the SQL executed by this operation. + */ + public void setSql(String sql) { + this.sql = sql; + } + + /** + * Subclasses can override this to supply dynamic SQL if they wish, + * but SQL is normally set by calling the setSql() method + * or in a subclass constructor. + */ + public String getSql() { + return this.sql; + } + + /** + * Add anonymous parameters, specifying only their SQL types + * as defined in the java.sql.Types class. + *

Parameter ordering is significant. This method is an alternative + * to the {@link #declareParameter} method, which should normally be preferred. + * @param types array of SQL types as defined in the + * java.sql.Types class + * @throws InvalidDataAccessApiUsageException if the operation is already compiled + */ + public void setTypes(int[] types) throws InvalidDataAccessApiUsageException { + if (isCompiled()) { + throw new InvalidDataAccessApiUsageException("Cannot add parameters once query is compiled"); + } + if (types != null) { + for (int i = 0; i < types.length; i++) { + declareParameter(new SqlParameter(types[i])); + } + } + } + + /** + * Declare a parameter for this operation. + *

The order in which this method is called is significant when using + * positional parameters. It is not significant when using named parameters + * with named SqlParameter objects here; it remains significant when using + * named parameters in combination with unnamed SqlParameter objects here. + * @param param the SqlParameter to add. This will specify SQL type and (optionally) + * the parameter's name. Note that you typically use the {@link SqlParameter} class + * itself here, not any of its subclasses. + * @throws InvalidDataAccessApiUsageException if the operation is already compiled, + * and hence cannot be configured further + */ + public void declareParameter(SqlParameter param) throws InvalidDataAccessApiUsageException { + if (isCompiled()) { + throw new InvalidDataAccessApiUsageException("Cannot add parameters once the query is compiled"); + } + this.declaredParameters.add(param); + } + + /** + * Add one or more declared parameters. Used for configuring this operation + * when used in a bean factory. Each parameter will specify SQL type and (optionally) + * the parameter's name. + * @param parameters Array containing the declared {@link SqlParameter} objects + * @see #declaredParameters + */ + public void setParameters(SqlParameter[] parameters) { + if (isCompiled()) { + throw new InvalidDataAccessApiUsageException("Cannot add parameters once the query is compiled"); + } + for (int i = 0; i < parameters.length; i++) { + if (parameters[i] != null) { + this.declaredParameters.add(parameters[i]); + } + else { + throw new InvalidDataAccessApiUsageException("Cannot add parameter at index " + i + " from " + + Arrays.asList(parameters) + " since it is 'null'"); + } + } + } + + /** + * Return a list of the declared {@link SqlParameter} objects. + */ + protected List getDeclaredParameters() { + return this.declaredParameters; + } + + + /** + * Ensures compilation if used in a bean factory. + */ + public void afterPropertiesSet() { + compile(); + } + + /** + * Compile this query. + * Ignores subsequent attempts to compile. + * @throws InvalidDataAccessApiUsageException if the object hasn't + * been correctly initialized, for example if no DataSource has been provided + */ + public final void compile() throws InvalidDataAccessApiUsageException { + if (!isCompiled()) { + if (getSql() == null) { + throw new InvalidDataAccessApiUsageException("Property 'sql' is required"); + } + + try { + this.jdbcTemplate.afterPropertiesSet(); + } + catch (IllegalArgumentException ex) { + throw new InvalidDataAccessApiUsageException(ex.getMessage()); + } + + compileInternal(); + this.compiled = true; + + if (logger.isDebugEnabled()) { + logger.debug("RdbmsOperation with SQL [" + getSql() + "] compiled"); + } + } + } + + /** + * Is this operation "compiled"? Compilation, as in JDO, + * means that the operation is fully configured, and ready to use. + * The exact meaning of compilation will vary between subclasses. + * @return whether this operation is compiled, and ready to use. + */ + public boolean isCompiled() { + return this.compiled; + } + + /** + * Check whether this operation has been compiled already; + * lazily compile it if not already compiled. + *

Automatically called by validateParameters. + * @see #validateParameters + */ + protected void checkCompiled() { + if (!isCompiled()) { + logger.debug("SQL operation not compiled before execution - invoking compile"); + compile(); + } + } + + /** + * Validate the parameters passed to an execute method based on declared parameters. + * Subclasses should invoke this method before every executeQuery() + * or update() method. + * @param parameters parameters supplied (may be null) + * @throws InvalidDataAccessApiUsageException if the parameters are invalid + */ + protected void validateParameters(Object[] parameters) throws InvalidDataAccessApiUsageException { + checkCompiled(); + + int declaredInParameters = 0; + Iterator it = this.declaredParameters.iterator(); + while (it.hasNext()) { + SqlParameter param = (SqlParameter) it.next(); + if (param.isInputValueProvided()) { + if (!supportsLobParameters() && + (param.getSqlType() == Types.BLOB || param.getSqlType() == Types.CLOB)) { + throw new InvalidDataAccessApiUsageException( + "BLOB or CLOB parameters are not allowed for this kind of operation"); + } + declaredInParameters++; + } + } + + validateParameterCount((parameters != null ? parameters.length : 0), declaredInParameters); + } + + /** + * Validate the named parameters passed to an execute method based on declared parameters. + * Subclasses should invoke this method before every executeQuery() or + * update() method. + * @param parameters parameter Map supplied. May be null. + * @throws InvalidDataAccessApiUsageException if the parameters are invalid + */ + protected void validateNamedParameters(Map parameters) throws InvalidDataAccessApiUsageException { + checkCompiled(); + Map paramsToUse = (parameters != null ? parameters : Collections.EMPTY_MAP); + + int declaredInParameters = 0; + Iterator it = this.declaredParameters.iterator(); + while (it.hasNext()) { + SqlParameter param = (SqlParameter) it.next(); + if (param.isInputValueProvided()) { + if (!supportsLobParameters() && + (param.getSqlType() == Types.BLOB || param.getSqlType() == Types.CLOB)) { + throw new InvalidDataAccessApiUsageException( + "BLOB or CLOB parameters are not allowed for this kind of operation"); + } + if (param.getName() != null && !paramsToUse.containsKey(param.getName())) { + throw new InvalidDataAccessApiUsageException("The parameter named '" + param.getName() + + "' was not among the parameters supplied: " + paramsToUse.keySet()); + } + declaredInParameters++; + } + } + + validateParameterCount(paramsToUse.size(), declaredInParameters); + } + + /** + * Validate the given parameter count against the given declared parameters. + * @param suppliedParamCount the number of actual parameters given + * @param declaredInParamCount the number of input parameters declared + */ + private void validateParameterCount(int suppliedParamCount, int declaredInParamCount) { + if (suppliedParamCount < declaredInParamCount) { + throw new InvalidDataAccessApiUsageException(suppliedParamCount + " parameters were supplied, but " + + declaredInParamCount + " in parameters were declared in class [" + getClass().getName() + "]"); + } + if (suppliedParamCount > this.declaredParameters.size() && !allowsUnusedParameters()) { + throw new InvalidDataAccessApiUsageException(suppliedParamCount + " parameters were supplied, but " + + declaredInParamCount + " parameters were declared in class [" + getClass().getName() + "]"); + } + } + + + /** + * Subclasses must implement this template method to perform their own compilation. + * Invoked after this base class's compilation is complete. + *

Subclasses can assume that SQL and a DataSource have been supplied. + * @throws InvalidDataAccessApiUsageException if the subclass hasn't been + * properly configured + */ + protected abstract void compileInternal() throws InvalidDataAccessApiUsageException; + + /** + * Return whether BLOB/CLOB parameters are supported for this kind of operation. + *

The default is true. + */ + protected boolean supportsLobParameters() { + return true; + } + + /** + * Return whether this operation accepts additional parameters that are + * given but not actually used. Applies in particular to parameter Maps. + *

The default is false. + * @see StoredProcedure + */ + protected boolean allowsUnusedParameters() { + return false; + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/object/SqlCall.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/object/SqlCall.java new file mode 100644 index 0000000000..b0b29970ae --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/object/SqlCall.java @@ -0,0 +1,199 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed 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.springframework.jdbc.object; + +import java.util.List; +import java.util.Map; + +import javax.sql.DataSource; + +import org.springframework.jdbc.core.CallableStatementCreator; +import org.springframework.jdbc.core.CallableStatementCreatorFactory; +import org.springframework.jdbc.core.ParameterMapper; +import org.springframework.jdbc.core.SqlParameter; +import org.springframework.jdbc.core.SqlReturnResultSet; + +/** + * RdbmsOperation using a JdbcTemplate and representing a SQL-based + * call such as a stored procedure or a stored function. + * + *

Configures a CallableStatementCreatorFactory based on the declared + * parameters. + * + * @author Rod Johnson + * @author Thomas Risberg + * @see CallableStatementCreatorFactory + */ +public abstract class SqlCall extends RdbmsOperation { + + /** + * Object enabling us to create CallableStatementCreators + * efficiently, based on this class's declared parameters. + */ + private CallableStatementCreatorFactory callableStatementFactory; + + /** + * Flag used to indicate that this call is for a function and to + * use the {? = call get_invoice_count(?)} syntax. + */ + private boolean function = false; + + /** + * Flag used to indicate that the sql for this call should be used exactly as it is + * defined. No need to add the escape syntax and parameter place holders. + */ + private boolean sqlReadyForUse = false; + + /** + * Call string as defined in java.sql.CallableStatement. + * String of form {call add_invoice(?, ?, ?)} + * or {? = call get_invoice_count(?)} if isFunction is set to true + * Updated after each parameter is added. + */ + private String callString; + + + /** + * Constructor to allow use as a JavaBean. + * A DataSource, SQL and any parameters must be supplied before + * invoking the compile method and using this object. + * @see #setDataSource + * @see #setSql + * @see #compile + */ + public SqlCall() { + } + + /** + * Create a new SqlCall object with SQL, but without parameters. + * Must add parameters or settle with none. + * @param ds DataSource to obtain connections from + * @param sql SQL to execute + */ + public SqlCall(DataSource ds, String sql) { + setDataSource(ds); + setSql(sql); + } + + + /** + * Set whether this call is for a function. + */ + public void setFunction(boolean function) { + this.function = function; + } + + /** + * Return whether this call is for a function. + */ + public boolean isFunction() { + return function; + } + + /** + * Set whether the SQL can be used as is. + */ + public void setSqlReadyForUse(boolean sqlReadyForUse) { + this.sqlReadyForUse = sqlReadyForUse; + } + + /** + * Return whether the SQL can be used as is. + */ + public boolean isSqlReadyForUse() { + return sqlReadyForUse; + } + + + /** + * Overridden method to configure the CallableStatementCreatorFactory + * based on our declared parameters. + * @see RdbmsOperation#compileInternal() + */ + protected final void compileInternal() { + if (isSqlReadyForUse()) { + this.callString = getSql(); + } + else { + List parameters = getDeclaredParameters(); + int parameterCount = 0; + if (isFunction()) { + this.callString = "{? = call " + getSql() + "("; + parameterCount = -1; + } + else { + this.callString = "{call " + getSql() + "("; + } + for (int i = 0; i < parameters.size(); i++) { + SqlParameter parameter = (SqlParameter) parameters.get(i); + if (!(parameter.isResultsParameter())) { + if (parameterCount > 0) { + this.callString += ", "; + } + if (parameterCount >= 0) { + this.callString += "?"; + } + parameterCount++; + } + } + this.callString += ")}"; + } + if (logger.isDebugEnabled()) { + logger.debug("Compiled stored procedure. Call string is [" + getCallString() + "]"); + } + + this.callableStatementFactory = new CallableStatementCreatorFactory(getCallString(), getDeclaredParameters()); + this.callableStatementFactory.setResultSetType(getResultSetType()); + this.callableStatementFactory.setUpdatableResults(isUpdatableResults()); + this.callableStatementFactory.setNativeJdbcExtractor(getJdbcTemplate().getNativeJdbcExtractor()); + + onCompileInternal(); + } + + /** + * Hook method that subclasses may override to react to compilation. + * This implementation does nothing. + */ + protected void onCompileInternal() { + } + + /** + * Get the call string. + */ + public String getCallString() { + return this.callString; + } + + /** + * Return a CallableStatementCreator to perform an operation + * with this parameters. + * @param inParams parameters. May be null. + */ + protected CallableStatementCreator newCallableStatementCreator(Map inParams) { + return this.callableStatementFactory.newCallableStatementCreator(inParams); + } + + /** + * Return a CallableStatementCreator to perform an operation + * with the parameters returned from this ParameterMapper. + * @param inParamMapper parametermapper. May not be null. + */ + protected CallableStatementCreator newCallableStatementCreator(ParameterMapper inParamMapper) { + return this.callableStatementFactory.newCallableStatementCreator(inParamMapper); + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/object/SqlFunction.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/object/SqlFunction.java new file mode 100644 index 0000000000..ec9b2b8cba --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/object/SqlFunction.java @@ -0,0 +1,198 @@ +/* + * Copyright 2002-2005 the original author or authors. + * + * Licensed 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.springframework.jdbc.object; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; + +import javax.sql.DataSource; + +import org.springframework.dao.TypeMismatchDataAccessException; +import org.springframework.jdbc.core.SingleColumnRowMapper; +import org.springframework.jdbc.support.JdbcUtils; + +/** + * SQL "function" wrapper for a query that returns a single row of results. + * The default behavior is to return an int, but that can be overridden by + * using the constructor with an extra return type parameter. + * + *

Intended to use to call SQL functions that return a single result using a + * query like "select user()" or "select sysdate from dual". It is not intended + * for calling more complex stored functions or for using a CallableStatement to + * invoke a stored procedure or stored function. Use StoredProcedure or SqlCall + * for this type of processing. + * + *

This is a concrete class, which there is often no need to subclass. + * Code using this package can create an object of this type, declaring SQL + * and parameters, and then invoke the appropriate run method + * repeatedly to execute the function. Subclasses are only supposed to add + * specialized run methods for specific parameter and return types. + * + *

Like all RdbmsOperation objects, SqlFunction objects are thread-safe. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Jean-Pierre Pawlak + * @see org.springframework.jdbc.object.StoredProcedure + */ +public class SqlFunction extends MappingSqlQuery { + + private final SingleColumnRowMapper rowMapper = new SingleColumnRowMapper(); + + + /** + * Constructor to allow use as a JavaBean. + * A DataSource, SQL and any parameters must be supplied before + * invoking the compile method and using this object. + * @see #setDataSource + * @see #setSql + * @see #compile + */ + public SqlFunction() { + setRowsExpected(1); + } + + /** + * Create a new SqlFunction object with SQL, but without parameters. + * Must add parameters or settle with none. + * @param ds DataSource to obtain connections from + * @param sql SQL to execute + */ + public SqlFunction(DataSource ds, String sql) { + setRowsExpected(1); + setDataSource(ds); + setSql(sql); + } + + /** + * Create a new SqlFunction object with SQL and parameters. + * @param ds DataSource to obtain connections from + * @param sql SQL to execute + * @param types SQL types of the parameters, as defined in the + * java.sql.Types class + * @see java.sql.Types + */ + public SqlFunction(DataSource ds, String sql, int[] types) { + setRowsExpected(1); + setDataSource(ds); + setSql(sql); + setTypes(types); + } + + /** + * Create a new SqlFunction object with SQL, parameters and a result type. + * @param ds DataSource to obtain connections from + * @param sql SQL to execute + * @param types SQL types of the parameters, as defined in the + * java.sql.Types class + * @param resultType the type that the result object is required to match + * @see #setResultType(Class) + * @see java.sql.Types + */ + public SqlFunction(DataSource ds, String sql, int[] types, Class resultType) { + setRowsExpected(1); + setDataSource(ds); + setSql(sql); + setTypes(types); + setResultType(resultType); + } + + + /** + * Specify the type that the result object is required to match. + *

If not specified, the result value will be exposed as + * returned by the JDBC driver. + */ + public void setResultType(Class resultType) { + this.rowMapper.setRequiredType(resultType); + } + + + /** + * This implementation of this method extracts a single value from the + * single row returned by the function. If there are a different number + * of rows returned, this is treated as an error. + */ + protected Object mapRow(ResultSet rs, int rowNum) throws SQLException { + return this.rowMapper.mapRow(rs, rowNum); + } + + + /** + * Convenient method to run the function without arguments. + * @return the value of the function + */ + public int run() { + return run(null); + } + + /** + * Convenient method to run the function with a single int argument. + * @param parameter single int parameter + * @return the value of the function + */ + public int run(int parameter) { + return run(new Object[] {new Integer(parameter)}); + } + + /** + * Analogous to the SqlQuery.execute([]) method. This is a + * generic method to execute a query, taken a number of arguments. + * @param parameters array of parameters. These will be objects or + * object wrapper types for primitives. + * @return the value of the function + */ + public int run(Object[] parameters) { + Object obj = super.findObject(parameters); + if (!(obj instanceof Number)) { + throw new TypeMismatchDataAccessException("Couldn't convert result object [" + obj + "] to int"); + } + return ((Number) obj).intValue(); + } + + /** + * Convenient method to run the function without arguments, + * returning the value as an object. + * @return the value of the function + */ + public Object runGeneric() { + return findObject((Object[]) null); + } + + /** + * Convenient method to run the function with a single int argument. + * @param parameter single int parameter + * @return the value of the function as an Object + */ + public Object runGeneric(int parameter) { + return findObject(parameter); + } + + /** + * Analogous to the SqlQuery.findObject(Object[]) method. + * This is a generic method to execute a query, taken a number of arguments. + * @param parameters array of parameters. These will be objects or + * object wrapper types for primitives. + * @return the value of the function, as an Object + * @see #execute(Object[]) + */ + public Object runGeneric(Object[] parameters) { + return findObject(parameters); + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/object/SqlOperation.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/object/SqlOperation.java new file mode 100644 index 0000000000..3c9a742edf --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/object/SqlOperation.java @@ -0,0 +1,118 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc.object; + +import org.springframework.jdbc.core.PreparedStatementCreator; +import org.springframework.jdbc.core.PreparedStatementCreatorFactory; +import org.springframework.jdbc.core.PreparedStatementSetter; +import org.springframework.jdbc.core.namedparam.NamedParameterUtils; +import org.springframework.jdbc.core.namedparam.ParsedSql; + +/** + * Operation object representing a SQL-based operation such as a query or update, + * as opposed to a stored procedure. + * + *

Configures a {@link org.springframework.jdbc.core.PreparedStatementCreatorFactory} + * based on the declared parameters. + * + * @author Rod Johnson + * @author Juergen Hoeller + */ +public abstract class SqlOperation extends RdbmsOperation { + + /** + * Object enabling us to create PreparedStatementCreators efficiently, + * based on this class's declared parameters. + */ + private PreparedStatementCreatorFactory preparedStatementFactory; + + /** Parsed representation of the SQL statement */ + private ParsedSql cachedSql; + + /** Monitor for locking the cached representation of the parsed SQL statement */ + private final Object parsedSqlMonitor = new Object(); + + + /** + * Overridden method to configure the PreparedStatementCreatorFactory + * based on our declared parameters. + */ + protected final void compileInternal() { + this.preparedStatementFactory = new PreparedStatementCreatorFactory(getSql(), getDeclaredParameters()); + this.preparedStatementFactory.setResultSetType(getResultSetType()); + this.preparedStatementFactory.setUpdatableResults(isUpdatableResults()); + this.preparedStatementFactory.setReturnGeneratedKeys(isReturnGeneratedKeys()); + if (getGeneratedKeysColumnNames() != null) { + this.preparedStatementFactory.setGeneratedKeysColumnNames(getGeneratedKeysColumnNames()); + } + this.preparedStatementFactory.setNativeJdbcExtractor(getJdbcTemplate().getNativeJdbcExtractor()); + + onCompileInternal(); + } + + /** + * Hook method that subclasses may override to post-process compilation. + * This implementation does nothing. + * @see #compileInternal + */ + protected void onCompileInternal() { + } + + /** + * Obtain a parsed representation of this operation's SQL statement. + *

Typically used for named parameter parsing. + */ + protected ParsedSql getParsedSql() { + synchronized (this.parsedSqlMonitor) { + if (this.cachedSql == null) { + this.cachedSql = NamedParameterUtils.parseSqlStatement(getSql()); + } + return this.cachedSql; + } + } + + + /** + * Return a PreparedStatementSetter to perform an operation + * with the given parameters. + * @param params the parameter array (may be null) + */ + protected final PreparedStatementSetter newPreparedStatementSetter(Object[] params) { + return this.preparedStatementFactory.newPreparedStatementSetter(params); + } + + /** + * Return a PreparedStatementCreator to perform an operation + * with the given parameters. + * @param params the parameter array (may be null) + */ + protected final PreparedStatementCreator newPreparedStatementCreator(Object[] params) { + return this.preparedStatementFactory.newPreparedStatementCreator(params); + } + + /** + * Return a PreparedStatementCreator to perform an operation + * with the given parameters. + * @param sqlToUse the actual SQL statement to use (if different from + * the factory's, for example because of named parameter expanding) + * @param params the parameter array (may be null) + */ + protected final PreparedStatementCreator newPreparedStatementCreator(String sqlToUse, Object[] params) { + return this.preparedStatementFactory.newPreparedStatementCreator(sqlToUse, params); + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/object/SqlQuery.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/object/SqlQuery.java new file mode 100644 index 0000000000..260214a4d3 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/object/SqlQuery.java @@ -0,0 +1,363 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc.object; + +import java.util.List; +import java.util.Map; + +import javax.sql.DataSource; + +import org.springframework.dao.DataAccessException; +import org.springframework.dao.support.DataAccessUtils; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterUtils; +import org.springframework.jdbc.core.namedparam.ParsedSql; + +/** + * Reusable operation object representing a SQL query. + * + *

Subclasses must implement the {@link #newRowMapper} method to provide + * an object that can extract the results of iterating over the + * ResultSet created during the execution of the query. + * + *

This class provides a number of public execute methods that are + * analogous to the different convenient JDO query execute methods. Subclasses + * can either rely on one of these inherited methods, or can add their own + * custom execution methods, with meaningful names and typed parameters + * (definitely a best practice). Each custom query method will invoke one of + * this class's untyped query methods. + * + *

Like all RdbmsOperation classes that ship with the Spring + * Framework, SqlQuery instances are thread-safe after their + * initialization is complete. That is, after they are constructed and configured + * via their setter methods, they can be used safely from multiple threads. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Thomas Risberg + * @see SqlUpdate + */ +public abstract class SqlQuery extends SqlOperation { + + /** The number of rows to expect; if 0, unknown. */ + private int rowsExpected = 0; + + + /** + * Constructor to allow use as a JavaBean. + *

The DataSource and SQL must be supplied before + * compilation and use. + */ + public SqlQuery() { + } + + /** + * Convenient constructor with a DataSource and SQL string. + * @param ds the DataSource to use to get connections + * @param sql the SQL to execute; SQL can also be supplied at runtime + * by overriding the {@link #getSql()} method. + */ + public SqlQuery(DataSource ds, String sql) { + setDataSource(ds); + setSql(sql); + } + + + /** + * Set the number of rows expected. + *

This can be used to ensure efficient storage of results. The + * default behavior is not to expect any specific number of rows. + */ + public void setRowsExpected(int rowsExpected) { + this.rowsExpected = rowsExpected; + } + + /** + * Get the number of rows expected. + */ + public int getRowsExpected() { + return this.rowsExpected; + } + + + /** + * Central execution method. All un-named parameter execution goes through this method. + * @param params parameters, similar to JDO query parameters. + * Primitive parameters must be represented by their Object wrapper type. + * The ordering of parameters is significant. + * @param context contextual information passed to the mapRow + * callback method. The JDBC operation itself doesn't rely on this parameter, + * but it can be useful for creating the objects of the result list. + * @return a List of objects, one per row of the ResultSet. Normally all these + * will be of the same class, although it is possible to use different types. + */ + public List execute(Object[] params, Map context) throws DataAccessException { + validateParameters(params); + RowMapper rowMapper = newRowMapper(params, context); + return getJdbcTemplate().query(newPreparedStatementCreator(params), rowMapper); + } + + /** + * Convenient method to execute without context. + * @param params parameters for the query. Primitive parameters must + * be represented by their Object wrapper type. The ordering of parameters is + * significant. + */ + public List execute(Object[] params) throws DataAccessException { + return execute(params, null); + } + + /** + * Convenient method to execute without parameters. + * @param context the contextual information for object creation + */ + public List execute(Map context) throws DataAccessException { + return execute((Object[]) null, context); + } + + /** + * Convenient method to execute without parameters nor context. + */ + public List execute() throws DataAccessException { + return execute((Object[]) null); + } + + /** + * Convenient method to execute with a single int parameter and context. + * @param p1 single int parameter + * @param context the contextual information for object creation + */ + public List execute(int p1, Map context) throws DataAccessException { + return execute(new Object[] {new Integer(p1)}, context); + } + + /** + * Convenient method to execute with a single int parameter. + * @param p1 single int parameter + */ + public List execute(int p1) throws DataAccessException { + return execute(p1, null); + } + + /** + * Convenient method to execute with two int parameters and context. + * @param p1 first int parameter + * @param p2 second int parameter + * @param context the contextual information for object creation + */ + public List execute(int p1, int p2, Map context) throws DataAccessException { + return execute(new Object[] {new Integer(p1), new Integer(p2)}, context); + } + + /** + * Convenient method to execute with two int parameters. + * @param p1 first int parameter + * @param p2 second int parameter + */ + public List execute(int p1, int p2) throws DataAccessException { + return execute(p1, p2, null); + } + + /** + * Convenient method to execute with a single long parameter and context. + * @param p1 single long parameter + * @param context the contextual information for object creation + */ + public List execute(long p1, Map context) throws DataAccessException { + return execute(new Object[] {new Long(p1)}, context); + } + + /** + * Convenient method to execute with a single long parameter. + * @param p1 single long parameter + */ + public List execute(long p1) throws DataAccessException { + return execute(p1, null); + } + + /** + * Convenient method to execute with a single String parameter and context. + * @param p1 single String parameter + * @param context the contextual information for object creation + */ + public List execute(String p1, Map context) throws DataAccessException { + return execute(new Object[] {p1}, context); + } + + /** + * Convenient method to execute with a single String parameter. + * @param p1 single String parameter + */ + public List execute(String p1) throws DataAccessException { + return execute(p1, null); + } + + /** + * Central execution method. All named parameter execution goes through this method. + * @param paramMap parameters associated with the name specified while declaring + * the SqlParameters. Primitive parameters must be represented by their Object wrapper + * type. The ordering of parameters is not significant since they are supplied in a + * SqlParameterMap which is an implementation of the Map interface. + * @param context contextual information passed to the mapRow + * callback method. The JDBC operation itself doesn't rely on this parameter, + * but it can be useful for creating the objects of the result list. + * @return a List of objects, one per row of the ResultSet. Normally all these + * will be of the same class, although it is possible to use different types. + */ + public List executeByNamedParam(Map paramMap, Map context) throws DataAccessException { + validateNamedParameters(paramMap); + ParsedSql parsedSql = getParsedSql(); + MapSqlParameterSource paramSource = new MapSqlParameterSource(paramMap); + String sqlToUse = NamedParameterUtils.substituteNamedParameters(parsedSql, paramSource); + Object[] params = NamedParameterUtils.buildValueArray(parsedSql, paramSource, getDeclaredParameters()); + RowMapper rowMapper = newRowMapper(params, context); + return getJdbcTemplate().query(newPreparedStatementCreator(sqlToUse, params), rowMapper); + } + + /** + * Convenient method to execute without context. + * @param paramMap parameters associated with the name specified while declaring + * the SqlParameters. Primitive parameters must be represented by their Object wrapper + * type. The ordering of parameters is not significant. + */ + public List executeByNamedParam(Map paramMap) throws DataAccessException { + return executeByNamedParam(paramMap, null); + } + + + /** + * Generic object finder method, used by all other findObject methods. + * Object finder methods are like EJB entity bean finders, in that it is + * considered an error if they return more than one result. + * @return the result object, or null if not found. Subclasses may + * choose to treat this as an error and throw an exception. + * @see org.springframework.dao.support.DataAccessUtils#singleResult + */ + public Object findObject(Object[] params, Map context) throws DataAccessException { + List results = execute(params, context); + return DataAccessUtils.singleResult(results); + } + + /** + * Convenient method to find a single object without context. + */ + public Object findObject(Object[] params) throws DataAccessException { + return findObject(params, null); + } + + /** + * Convenient method to find a single object given a single int parameter + * and a context. + */ + public Object findObject(int p1, Map context) throws DataAccessException { + return findObject(new Object[] {new Integer(p1)}, context); + } + + /** + * Convenient method to find a single object given a single int parameter. + */ + public Object findObject(int p1) throws DataAccessException { + return findObject(p1, null); + } + + /** + * Convenient method to find a single object given two int parameters + * and a context. + */ + public Object findObject(int p1, int p2, Map context) throws DataAccessException { + return findObject(new Object[] {new Integer(p1), new Integer(p2)}, context); + } + + /** + * Convenient method to find a single object given two int parameters. + */ + public Object findObject(int p1, int p2) throws DataAccessException { + return findObject(p1, p2, null); + } + + /** + * Convenient method to find a single object given a single long parameter + * and a context. + */ + public Object findObject(long p1, Map context) throws DataAccessException { + return findObject(new Object[] {new Long(p1)}, context); + } + + /** + * Convenient method to find a single object given a single long parameter. + */ + public Object findObject(long p1) throws DataAccessException { + return findObject(p1, null); + } + + /** + * Convenient method to find a single object given a single String parameter + * and a context. + */ + public Object findObject(String p1, Map context) throws DataAccessException { + return findObject(new Object[] {p1}, context); + } + + /** + * Convenient method to find a single object given a single String parameter. + */ + public Object findObject(String p1) throws DataAccessException { + return findObject(p1, null); + } + + /** + * Generic object finder method for named parameters. + * @param paramMap Map of parameter name to parameter object, + * matching named parameters specified in the SQL statement. + * Ordering is not significant. + * @param context contextual information passed to the mapRow + * callback method. The JDBC operation itself doesn't rely on this parameter, + * but it can be useful for creating the objects of the result list. + * @return a List of objects, one per row of the ResultSet. Normally all these + * will be of the same class, although it is possible to use different types. + */ + public Object findObjectByNamedParam(Map paramMap, Map context) throws DataAccessException { + List results = executeByNamedParam(paramMap, context); + return DataAccessUtils.singleResult(results); + } + + /** + * Convenient method to execute without context. + * @param paramMap Map of parameter name to parameter object, + * matching named parameters specified in the SQL statement. + * Ordering is not significant. + */ + public Object findObjectByNamedParam(Map paramMap) throws DataAccessException { + return findObjectByNamedParam(paramMap, null); + } + + + /** + * Subclasses must implement this method to extract an object per row, to be + * returned by the execute method as an aggregated {@link List}. + * @param parameters the parameters to the execute() method, + * in case subclass is interested; may be null if there + * were no parameters. + * @param context contextual information passed to the mapRow + * callback method. The JDBC operation itself doesn't rely on this parameter, + * but it can be useful for creating the objects of the result list. + * @see #execute + */ + protected abstract RowMapper newRowMapper(Object[] parameters, Map context); + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/object/SqlUpdate.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/object/SqlUpdate.java new file mode 100644 index 0000000000..e67dee6390 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/object/SqlUpdate.java @@ -0,0 +1,273 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc.object; + +import java.util.Map; + +import javax.sql.DataSource; + +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.JdbcUpdateAffectedIncorrectNumberOfRowsException; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterUtils; +import org.springframework.jdbc.core.namedparam.ParsedSql; +import org.springframework.jdbc.support.KeyHolder; + +/** + * Reusable operation object representing a SQL update. + * + *

This class provides a number of update methods, + * analogous to the execute methods of query objects. + * + *

This class is concrete. Although it can be subclassed (for example + * to add a custom update method) it can easily be parameterized by setting + * SQL and declaring parameters. + * + *

Like all RdbmsOperation classes that ship with the Spring + * Framework, SqlQuery instances are thread-safe after their + * initialization is complete. That is, after they are constructed and configured + * via their setter methods, they can be used safely from multiple threads. + * + * @author Rod Johnson + * @author Thomas Risberg + * @author Juergen Hoeller + * @see SqlQuery + */ +public class SqlUpdate extends SqlOperation { + + /** + * Maximum number of rows the update may affect. If more are + * affected, an exception will be thrown. Ignored if 0. + */ + private int maxRowsAffected = 0; + + /** + * An exact number of rows that must be affected. + * Ignored if 0. + */ + private int requiredRowsAffected = 0; + + + /** + * Constructor to allow use as a JavaBean. DataSource and SQL + * must be supplied before compilation and use. + * @see #setDataSource + * @see #setSql + */ + public SqlUpdate() { + } + + /** + * Constructs an update object with a given DataSource and SQL. + * @param ds DataSource to use to obtain connections + * @param sql SQL statement to execute + */ + public SqlUpdate(DataSource ds, String sql) { + setDataSource(ds); + setSql(sql); + } + + /** + * Construct an update object with a given DataSource, SQL + * and anonymous parameters. + * @param ds DataSource to use to obtain connections + * @param sql SQL statement to execute + * @param types SQL types of the parameters, as defined in the + * java.sql.Types class + * @see java.sql.Types + */ + public SqlUpdate(DataSource ds, String sql, int[] types) { + setDataSource(ds); + setSql(sql); + setTypes(types); + } + + /** + * Construct an update object with a given DataSource, SQL, + * anonymous parameters and specifying the maximum number of rows + * that may be affected. + * @param ds DataSource to use to obtain connections + * @param sql SQL statement to execute + * @param types SQL types of the parameters, as defined in the + * java.sql.Types class + * @param maxRowsAffected the maximum number of rows that may + * be affected by the update + * @see java.sql.Types + */ + public SqlUpdate(DataSource ds, String sql, int[] types, int maxRowsAffected) { + setDataSource(ds); + setSql(sql); + setTypes(types); + this.maxRowsAffected = maxRowsAffected; + } + + + /** + * Set the maximum number of rows that may be affected by this update. + * The default value is 0, which does not limit the number of rows affected. + * @param maxRowsAffected the maximum number of rows that can be affected by + * this update without this class's update method considering it an error + */ + public void setMaxRowsAffected(int maxRowsAffected) { + this.maxRowsAffected = maxRowsAffected; + } + + /** + * Set the exact number of rows that must be affected by this update. + * The default value is 0, which allows any number of rows to be affected. + *

This is an alternative to setting the maximum number of rows + * that may be affected. + * @param requiredRowsAffected the exact number of rows that must be affected + * by this update without this class's update method considering it an error + */ + public void setRequiredRowsAffected(int requiredRowsAffected) { + this.requiredRowsAffected = requiredRowsAffected; + } + + /** + * Check the given number of affected rows against the + * specified maximum number or required number. + * @param rowsAffected the number of affected rows + * @throws JdbcUpdateAffectedIncorrectNumberOfRowsException + * if the actually affected rows are out of bounds + * @see #setMaxRowsAffected + * @see #setRequiredRowsAffected + */ + protected void checkRowsAffected(int rowsAffected) throws JdbcUpdateAffectedIncorrectNumberOfRowsException { + if (this.maxRowsAffected > 0 && rowsAffected > this.maxRowsAffected) { + throw new JdbcUpdateAffectedIncorrectNumberOfRowsException(getSql(), this.maxRowsAffected, rowsAffected); + } + if (this.requiredRowsAffected > 0 && rowsAffected != this.requiredRowsAffected) { + throw new JdbcUpdateAffectedIncorrectNumberOfRowsException(getSql(), this.requiredRowsAffected, rowsAffected); + } + } + + + /** + * Generic method to execute the update given parameters. + * All other update methods invoke this method. + * @param params array of parameters objects + * @return the number of rows affected by the update + */ + public int update(Object[] params) throws DataAccessException { + validateParameters(params); + int rowsAffected = getJdbcTemplate().update(newPreparedStatementCreator(params)); + checkRowsAffected(rowsAffected); + return rowsAffected; + } + + /** + * Method to execute the update given arguments and + * retrieve the generated keys using a KeyHolder. + * @param params array of parameter objects + * @param generatedKeyHolder KeyHolder that will hold the generated keys + * @return the number of rows affected by the update + */ + public int update(Object[] params, KeyHolder generatedKeyHolder) throws DataAccessException { + validateParameters(params); + int rowsAffected = getJdbcTemplate().update(newPreparedStatementCreator(params), generatedKeyHolder); + checkRowsAffected(rowsAffected); + return rowsAffected; + } + + /** + * Convenience method to execute an update with no parameters. + */ + public int update() throws DataAccessException { + return update((Object[]) null); + } + + /** + * Convenient method to execute an update given one int arg. + */ + public int update(int p1) throws DataAccessException { + return update(new Object[] {new Integer(p1)}); + } + + /** + * Convenient method to execute an update given two int args. + */ + public int update(int p1, int p2) throws DataAccessException { + return update(new Object[] {new Integer(p1), new Integer(p2)}); + } + + /** + * Convenient method to execute an update given one long arg. + */ + public int update(long p1) throws DataAccessException { + return update(new Object[] {new Long(p1)}); + } + + /** + * Convenient method to execute an update given two long args. + */ + public int update(long p1, long p2) throws DataAccessException { + return update(new Object[] {new Long(p1), new Long(p2)}); + } + + /** + * Convenient method to execute an update given one String arg. + */ + public int update(String p) throws DataAccessException { + return update(new Object[] {p}); + } + + /** + * Convenient method to execute an update given two String args. + */ + public int update(String p1, String p2) throws DataAccessException { + return update(new Object[] {p1, p2}); + } + + /** + * Generic method to execute the update given named parameters. + * All other update methods invoke this method. + * @param paramMap Map of parameter name to parameter object, + * matching named parameters specified in the SQL statement + * @return the number of rows affected by the update + */ + public int updateByNamedParam(Map paramMap) throws DataAccessException { + validateNamedParameters(paramMap); + ParsedSql parsedSql = getParsedSql(); + MapSqlParameterSource paramSource = new MapSqlParameterSource(paramMap); + String sqlToUse = NamedParameterUtils.substituteNamedParameters(parsedSql, paramSource); + Object[] params = NamedParameterUtils.buildValueArray(parsedSql, paramSource, getDeclaredParameters()); + int rowsAffected = getJdbcTemplate().update(newPreparedStatementCreator(sqlToUse, params)); + checkRowsAffected(rowsAffected); + return rowsAffected; + } + + /** + * Method to execute the update given arguments and + * retrieve the generated keys using a KeyHolder. + * @param paramMap Map of parameter name to parameter object, + * matching named parameters specified in the SQL statement + * @param generatedKeyHolder KeyHolder that will hold the generated keys + * @return the number of rows affected by the update + */ + public int updateByNamedParam(Map paramMap, KeyHolder generatedKeyHolder) throws DataAccessException { + validateNamedParameters(paramMap); + ParsedSql parsedSql = getParsedSql(); + MapSqlParameterSource paramSource = new MapSqlParameterSource(paramMap); + String sqlToUse = NamedParameterUtils.substituteNamedParameters(parsedSql, paramSource); + Object[] params = NamedParameterUtils.buildValueArray(parsedSql, paramSource, getDeclaredParameters()); + int rowsAffected = getJdbcTemplate().update(newPreparedStatementCreator(sqlToUse, params), generatedKeyHolder); + checkRowsAffected(rowsAffected); + return rowsAffected; + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/object/StoredProcedure.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/object/StoredProcedure.java new file mode 100644 index 0000000000..fbbdc59866 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/object/StoredProcedure.java @@ -0,0 +1,141 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed 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.springframework.jdbc.object; + +import java.util.Map; + +import javax.sql.DataSource; + +import org.springframework.dao.DataAccessException; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.ParameterMapper; +import org.springframework.jdbc.core.SqlParameter; + +/** + * Superclass for object abstractions of RDBMS stored procedures. + * This class is abstract and it is intended that subclasses will provide + * a typed method for invocation that delegates to the supplied + * {@link #execute} method. + * + *

The inherited sql property is the name of the stored + * procedure in the RDBMS. Note that JDBC 3.0 introduces named parameters, + * although the other features provided by this class are still necessary + * in JDBC 3.0. + * + * @author Rod Johnson + * @author Thomas Risberg + * @see #setSql + */ +public abstract class StoredProcedure extends SqlCall { + + /** + * Allow use as a bean. + */ + protected StoredProcedure() { + } + + /** + * Create a new object wrapper for a stored procedure. + * @param ds DataSource to use throughout the lifetime + * of this object to obtain connections + * @param name name of the stored procedure in the database + */ + protected StoredProcedure(DataSource ds, String name) { + setDataSource(ds); + setSql(name); + } + + /** + * Create a new object wrapper for a stored procedure. + * @param jdbcTemplate JdbcTemplate which wraps DataSource + * @param name name of the stored procedure in the database + */ + protected StoredProcedure(JdbcTemplate jdbcTemplate, String name) { + setJdbcTemplate(jdbcTemplate); + setSql(name); + } + + + /** + * StoredProcedure parameter Maps are by default allowed to contain + * additional entries that are not actually used as parameters. + */ + protected boolean allowsUnusedParameters() { + return true; + } + + /** + * Declare a parameter. Overridden method. + * Parameters declared as SqlParameter and SqlInOutParameter + * will always be used to provide input values. In addition to this any parameter declared + * as SqlOutParameter where an non-null input value is provided will also be used + * as an input paraneter. + * Note: Calls to declareParameter must be made in the same order as + * they appear in the database's stored procedure parameter list. + * Names are purely used to help mapping. + * @param param parameter object + */ + public void declareParameter(SqlParameter param) throws InvalidDataAccessApiUsageException { + if (param.getName() == null) { + throw new InvalidDataAccessApiUsageException("Parameters to stored procedures must have names as well as types"); + } + super.declareParameter(param); + } + + + /** + * Execute the stored procedure. Subclasses should define a strongly typed + * execute method (with a meaningful name) that invokes this method, populating + * the input map and extracting typed values from the output map. Subclass + * execute methods will often take domain objects as arguments and return values. + * Alternatively, they can return void. + * @param inParams map of input parameters, keyed by name as in parameter + * declarations. Output parameters need not (but can be) included in this map. + * It is legal for map entries to be null, and this will produce the + * correct behavior using a NULL argument to the stored procedure. + * @return map of output params, keyed by name as in parameter declarations. + * Output parameters will appear here, with their values after the + * stored procedure has been called. + */ + public Map execute(Map inParams) throws DataAccessException { + validateParameters(inParams.values().toArray()); + return getJdbcTemplate().call(newCallableStatementCreator(inParams), getDeclaredParameters()); + } + + /** + * Execute the stored procedure. Subclasses should define a strongly typed + * execute method (with a meaningful name) that invokes this method, passing in + * a ParameterMapper that will populate the input map. This allows mapping database + * specific features since the ParameterMapper has access to the Connection object. + * The execute method is also responsible for extracting typed values from the output map. + * Subclass execute methods will often take domain objects as arguments and return values. + * Alternatively, they can return void. + * @param inParamMapper map of input parameters, keyed by name as in parameter + * declarations. Output parameters need not (but can be) included in this map. + * It is legal for map entries to be null, and this will produce the correct + * behavior using a NULL argument to the stored procedure. + * @return map of output params, keyed by name as in parameter declarations. + * Output parameters will appear here, with their values after the + * stored procedure has been called. + */ + public Map execute(ParameterMapper inParamMapper) throws DataAccessException { + checkCompiled(); + return getJdbcTemplate().call(newCallableStatementCreator(inParamMapper), getDeclaredParameters()); + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/object/UpdatableSqlQuery.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/object/UpdatableSqlQuery.java new file mode 100644 index 0000000000..113c03971d --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/object/UpdatableSqlQuery.java @@ -0,0 +1,102 @@ +/* + * Copyright 2002-2005 the original author or authors. + * + * Licensed 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.springframework.jdbc.object; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Map; + +import javax.sql.DataSource; + +import org.springframework.jdbc.core.RowMapper; + +/** + * Reusable RDBMS query in which concrete subclasses must implement + * the abstract updateRow(ResultSet, int, context) method to update each + * row of the JDBC ResultSet and optionally map contents into an object. + * + *

Subclasses can be constructed providing SQL, parameter types + * and a DataSource. SQL will often vary between subclasses. + * + * @author Thomas Risberg + * @see org.springframework.jdbc.object.SqlQuery + */ +public abstract class UpdatableSqlQuery extends SqlQuery { + + /** + * Constructor to allow use as a JavaBean + */ + public UpdatableSqlQuery() { + setUpdatableResults(true); + } + + /** + * Convenient constructor with DataSource and SQL string. + * @param ds DataSource to use to get connections + * @param sql SQL to run + */ + public UpdatableSqlQuery(DataSource ds, String sql) { + super(ds, sql); + setUpdatableResults(true); + } + + /** + * Implementation of the superclass template method. This invokes the subclass's + * implementation of the updateRow() method. + */ + protected RowMapper newRowMapper(Object[] parameters, Map context) { + return new RowMapperImpl(context); + } + + /** + * Subclasses must implement this method to update each row of the + * ResultSet and optionally create object of the result type. + * @param rs ResultSet we're working through + * @param rowNum row number (from 0) we're up to + * @param context passed to the execute() method. + * It can be null if no contextual information is need. If you + * need to pass in data for each row, you can pass in a HashMap with + * the primary key of the row being the key for the HashMap. That way + * it is easy to locate the updates for each row + * @return an object of the result type + * @throws SQLException if there's an error updateing data. + * Subclasses can simply not catch SQLExceptions, relying on the + * framework to clean up. + */ + protected abstract Object updateRow(ResultSet rs, int rowNum, Map context) throws SQLException; + + + /** + * Implementation of RowMapper that calls the enclosing + * class's updateRow() method for each row. + */ + protected class RowMapperImpl implements RowMapper { + + private final Map context; + + public RowMapperImpl(Map context) { + this.context = context; + } + + public Object mapRow(ResultSet rs, int rowNum) throws SQLException { + Object result = updateRow(rs, rowNum, this.context); + rs.updateRow(); + return result; + } + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/object/package.html b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/object/package.html new file mode 100644 index 0000000000..c268dce22e --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/object/package.html @@ -0,0 +1,20 @@ + + + +The classes in this package represent RDBMS queries, updates, +and stored procedures as threadsafe, reusable objects. This approach +is modelled by JDO, although of course objects returned by queries +are "disconnected" from the database. + +

This higher level of JDBC abstraction depends on the lower-level +abstraction in the org.springframework.jdbc.core package. +Exceptions thrown are as in the org.springframework.dao package, +meaning that code using this package does not need to implement JDBC or +RDBMS-specific error handling. + +

This package and related packages are discussed in Chapter 9 of +Expert One-On-One J2EE Design and Development +by Rod Johnson (Wrox, 2002). + + + diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/package.html b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/package.html new file mode 100644 index 0000000000..15224f3ef0 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/package.html @@ -0,0 +1,23 @@ + + + +The classes in this package make JDBC easier to use and +reduce the likelihood of common errors. In particular, they: +

    +
  • Simplify error handling, avoiding the need for try/catch/final +blocks in application code. +
  • Present exceptions to application code in a generic hierarchy of +unchecked exceptions, enabling applications to catch data access +exceptions without being dependent on JDBC, and to ignore fatal +exceptions there is no value in catching. +
  • Allow the implementation of error handling to be modified +to target different RDBMSes without introducing proprietary +dependencies into application code. +
+ +

This package and related packages are discussed in Chapter 9 of +Expert One-On-One J2EE Design and Development +by Rod Johnson (Wrox, 2002). + + + diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/AbstractFallbackSQLExceptionTranslator.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/AbstractFallbackSQLExceptionTranslator.java new file mode 100644 index 0000000000..bea2aa7461 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/AbstractFallbackSQLExceptionTranslator.java @@ -0,0 +1,113 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.support; + +import java.sql.SQLException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.UncategorizedSQLException; +import org.springframework.util.Assert; + +/** + * Base class for {@link SQLExceptionTranslator} implementations that allow for + * fallback to some other {@link SQLExceptionTranslator}. + * + * @author Juergen Hoeller + * @since 2.5.6 + */ +public abstract class AbstractFallbackSQLExceptionTranslator implements SQLExceptionTranslator { + + /** Logger available to subclasses */ + protected final Log logger = LogFactory.getLog(getClass()); + + private SQLExceptionTranslator fallbackTranslator; + + + /** + * Override the default SQL state fallback translator + * (typically a {@link SQLStateSQLExceptionTranslator}). + */ + public void setFallbackTranslator(SQLExceptionTranslator fallback) { + this.fallbackTranslator = fallback; + } + + /** + * Return the fallback exception translator, if any. + */ + public SQLExceptionTranslator getFallbackTranslator() { + return this.fallbackTranslator; + } + + + /** + * Pre-checks the arguments, calls {@link #doTranslate}, and invokes the + * {@link #getFallbackTranslator() fallback translator} if necessary. + */ + public DataAccessException translate(String task, String sql, SQLException ex) { + Assert.notNull(ex, "Cannot translate a null SQLException"); + if (task == null) { + task = ""; + } + if (sql == null) { + sql = ""; + } + + DataAccessException dex = doTranslate(task, sql, ex); + if (dex != null) { + // Specific exception match found. + return dex; + } + // Looking for a fallback... + SQLExceptionTranslator fallback = getFallbackTranslator(); + if (fallback != null) { + return fallback.translate(task, sql, ex); + } + // We couldn't identify it more precisely. + return new UncategorizedSQLException(task, sql, ex); + } + + /** + * Template method for actually translating the given exception. + *

The passed-in arguments will have been pre-checked. Furthermore, this method + * is allowed to return null to indicate that no exception match has + * been found and that fallback translation should kick in. + * @param task readable text describing the task being attempted + * @param sql SQL query or update that caused the problem (may be null) + * @param ex the offending SQLException + * @return the DataAccessException, wrapping the SQLException; + * or null if no exception match found + */ + protected abstract DataAccessException doTranslate(String task, String sql, SQLException ex); + + + /** + * Build a message String for the given {@link java.sql.SQLException}. + *

To be called by translator subclasses when creating an instance of a generic + * {@link org.springframework.dao.DataAccessException} class. + * @param task readable text describing the task being attempted + * @param sql the SQL statement that caused the problem (may be null) + * @param ex the offending SQLException + * @return the message String to use + */ + protected String buildMessage(String task, String sql, SQLException ex) { + return task + "; SQL [" + sql + "]; " + ex.getMessage(); + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/CustomSQLErrorCodesTranslation.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/CustomSQLErrorCodesTranslation.java new file mode 100644 index 0000000000..82954fd435 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/CustomSQLErrorCodesTranslation.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.support; + +import org.springframework.dao.DataAccessException; +import org.springframework.util.StringUtils; + +/** + * JavaBean for holding custom JDBC error codes translation for a particular + * database. The "exceptionClass" property defines which exception will be + * thrown for the list of error codes specified in the errorCodes property. + * + * @author Thomas Risberg + * @since 1.1 + * @see SQLErrorCodeSQLExceptionTranslator + */ +public class CustomSQLErrorCodesTranslation { + + private String[] errorCodes = new String[0]; + + private Class exceptionClass; + + + /** + * Set the SQL error codes to match. + */ + public void setErrorCodes(String[] errorCodes) { + this.errorCodes = StringUtils.sortStringArray(errorCodes); + } + + /** + * Return the SQL error codes to match. + */ + public String[] getErrorCodes() { + return this.errorCodes; + } + + /** + * Set the exception class for the specified error codes. + */ + public void setExceptionClass(Class exceptionClass) { + if (!DataAccessException.class.isAssignableFrom(exceptionClass)) { + throw new IllegalArgumentException("Invalid exception class [" + exceptionClass + + "]: needs to be a subclass of [org.springframework.dao.DataAccessException]"); + } + this.exceptionClass = exceptionClass; + } + + /** + * Return the exception class for the specified error codes. + */ + public Class getExceptionClass() { + return this.exceptionClass; + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/DatabaseMetaDataCallback.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/DatabaseMetaDataCallback.java new file mode 100644 index 0000000000..ebaf984a2d --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/DatabaseMetaDataCallback.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2005 the original author or authors. + * + * Licensed 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.springframework.jdbc.support; + +import java.sql.DatabaseMetaData; +import java.sql.SQLException; + +/** + * A callback interface used by the JdbcUtils class. Implementations of this + * interface perform the actual work of extracting database meta data, but + * don't need to worry about exception handling. SQLExceptions will be caught + * and handled correctly by the JdbcUtils class. + * + * @author Thomas Risberg + * @see JdbcUtils#extractDatabaseMetaData + */ +public interface DatabaseMetaDataCallback { + + /** + * Implementations must implement this method to process the meta data + * passed in. Exactly what the implementation chooses to do is up to it. + * @param dbmd the DatabaseMetaData to process + * @return a result object extracted from the meta data + * (can be an arbitrary object, as needed by the implementation) + * @throws SQLException if a SQLException is encountered getting + * column values (that is, there's no need to catch SQLException) + * @throws MetaDataAccessException in case of other failures while + * extracting meta data (for example, reflection failure) + */ + Object processMetaData(DatabaseMetaData dbmd) throws SQLException, MetaDataAccessException; + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/DatabaseStartupValidator.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/DatabaseStartupValidator.java new file mode 100644 index 0000000000..acc95ccac7 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/DatabaseStartupValidator.java @@ -0,0 +1,154 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.support; + +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; + +import javax.sql.DataSource; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.jdbc.CannotGetJdbcConnectionException; + +/** + * Bean that checks if a database has already started up. To be referenced + * via "depends-on" from beans that depend on database startup, like a Hibernate + * SessionFactory or custom data access objects that access a DataSource directly. + * + *

Useful to defer application initialization until a database has started up. + * Particularly appropriate for waiting on a slowly starting Oracle database. + * + * @author Juergen Hoeller + * @since 18.12.2003 + */ +public class DatabaseStartupValidator implements InitializingBean { + + public static final int DEFAULT_INTERVAL = 1; + + public static final int DEFAULT_TIMEOUT = 60; + + + protected final Log logger = LogFactory.getLog(getClass()); + + private DataSource dataSource; + + private String validationQuery; + + private int interval = DEFAULT_INTERVAL; + + private int timeout = DEFAULT_TIMEOUT; + + + /** + * Set the DataSource to validate. + */ + public void setDataSource(DataSource dataSource) { + this.dataSource = dataSource; + } + + /** + * Set the SQL query string to use for validation. + */ + public void setValidationQuery(String validationQuery) { + this.validationQuery = validationQuery; + } + + /** + * Set the interval between validation runs (in seconds). + * Default is 1. + */ + public void setInterval(int interval) { + this.interval = interval; + } + + /** + * Set the timeout (in seconds) after which a fatal exception + * will be thrown. Default is 60. + */ + public void setTimeout(int timeout) { + this.timeout = timeout; + } + + + /** + * Check whether the validation query can be executed on a Connection + * from the specified DataSource, with the specified interval between + * checks, until the specified timeout. + */ + public void afterPropertiesSet() { + if (this.dataSource == null) { + throw new IllegalArgumentException("dataSource is required"); + } + if (this.validationQuery == null) { + throw new IllegalArgumentException("validationQuery is required"); + } + + try { + boolean validated = false; + long beginTime = System.currentTimeMillis(); + long deadLine = beginTime + this.timeout * 1000; + SQLException latestEx = null; + + while (!validated && System.currentTimeMillis() < deadLine) { + Connection con = null; + Statement stmt = null; + try { + con = this.dataSource.getConnection(); + stmt = con.createStatement(); + stmt.execute(this.validationQuery); + validated = true; + } + catch (SQLException ex) { + latestEx = ex; + logger.debug("Validation query [" + this.validationQuery + "] threw exception", ex); + float rest = ((float) (deadLine - System.currentTimeMillis())) / 1000; + if (rest > this.interval) { + logger.warn("Database has not started up yet - retrying in " + this.interval + + " seconds (timeout in " + rest + " seconds)"); + } + } + finally { + JdbcUtils.closeStatement(stmt); + JdbcUtils.closeConnection(con); + } + + if (!validated) { + Thread.sleep(this.interval * 1000); + } + } + + if (!validated) { + throw new CannotGetJdbcConnectionException( + "Database has not started up within " + this.timeout + " seconds", latestEx); + } + + float duration = (System.currentTimeMillis() - beginTime) / 1000; + if (logger.isInfoEnabled()) { + logger.info("Database startup detected after " + duration + " seconds"); + } + } + catch (InterruptedException ex) { + // Re-interrupt current thread, to allow other threads to react. + Thread.currentThread().interrupt(); + } + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/GeneratedKeyHolder.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/GeneratedKeyHolder.java new file mode 100644 index 0000000000..cc0f52a5bb --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/GeneratedKeyHolder.java @@ -0,0 +1,101 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc.support; + +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import org.springframework.dao.DataRetrievalFailureException; +import org.springframework.dao.InvalidDataAccessApiUsageException; + +/** + * Default implementation of the {@link KeyHolder} interface, to be used for + * holding auto-generated keys (as potentially returned by JDBC insert statements). + * + *

Create an instance of this class for each insert operation, and pass + * it to the corresponding {@link org.springframework.jdbc.core.JdbcTemplate} + * or {org.springframework.jdbc.object.SqlUpdate} methods. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @since 1.1 + */ +public class GeneratedKeyHolder implements KeyHolder { + + private final List keyList; + + + /** + * Create a new GeneratedKeyHolder with a default list. + */ + public GeneratedKeyHolder() { + this.keyList = new LinkedList(); + } + + /** + * Create a new GeneratedKeyHolder with a given list. + * @param keyList a list to hold maps of keys + */ + public GeneratedKeyHolder(List keyList) { + this.keyList = keyList; + } + + + public Number getKey() throws InvalidDataAccessApiUsageException, DataRetrievalFailureException { + if (this.keyList.size() == 0) { + return null; + } + if (this.keyList.size() > 1 || ((Map) this.keyList.get(0)).size() > 1) { + throw new InvalidDataAccessApiUsageException( + "The getKey method should only be used when a single key is returned. " + + "The current key entry contains multiple keys: " + this.keyList); + } + Iterator keyIter = ((Map) this.keyList.get(0)).values().iterator(); + if (keyIter.hasNext()) { + Object key = keyIter.next(); + if (!(key instanceof Number)) { + throw new DataRetrievalFailureException( + "The generated key is not of a supported numeric type. " + + "Unable to cast [" + (key != null ? key.getClass().getName() : null) + + "] to [" + Number.class.getName() + "]"); + } + return (Number) key; + } + else { + throw new DataRetrievalFailureException("Unable to retrieve the generated key. " + + "Check that the table has an identity column enabled."); + } + } + + public Map getKeys() throws InvalidDataAccessApiUsageException { + if (this.keyList.size() == 0) { + return null; + } + if (this.keyList.size() > 1) + throw new InvalidDataAccessApiUsageException( + "The getKeys method should only be used when keys for a single row are returned. " + + "The current key list contains keys for multiple rows: " + this.keyList); + return (Map) this.keyList.get(0); + } + + public List getKeyList() { + return this.keyList; + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/JdbcAccessor.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/JdbcAccessor.java new file mode 100644 index 0000000000..a81b36db81 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/JdbcAccessor.java @@ -0,0 +1,141 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc.support; + +import javax.sql.DataSource; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.InitializingBean; + +/** + * Base class for {@link org.springframework.jdbc.core.JdbcTemplate} and + * other JDBC-accessing DAO helpers, defining common properties such as + * DataSource and exception translator. + * + *

Not intended to be used directly. + * See {@link org.springframework.jdbc.core.JdbcTemplate}. + * + * @author Juergen Hoeller + * @since 28.11.2003 + * @see org.springframework.jdbc.core.JdbcTemplate + */ +public abstract class JdbcAccessor implements InitializingBean { + + /** Logger available to subclasses */ + protected final Log logger = LogFactory.getLog(getClass()); + + private DataSource dataSource; + + private SQLExceptionTranslator exceptionTranslator; + + private boolean lazyInit = true; + + + /** + * Set the JDBC DataSource to obtain connections from. + */ + public void setDataSource(DataSource dataSource) { + this.dataSource = dataSource; + } + + /** + * Return the DataSource used by this template. + */ + public DataSource getDataSource() { + return this.dataSource; + } + + /** + * Specify the database product name for the DataSource that this accessor uses. + * This allows to initialize a SQLErrorCodeSQLExceptionTranslator without + * obtaining a Connection from the DataSource to get the metadata. + * @param dbName the database product name that identifies the error codes entry + * @see SQLErrorCodeSQLExceptionTranslator#setDatabaseProductName + * @see java.sql.DatabaseMetaData#getDatabaseProductName() + */ + public void setDatabaseProductName(String dbName) { + this.exceptionTranslator = new SQLErrorCodeSQLExceptionTranslator(dbName); + } + + /** + * Set the exception translator for this instance. + *

If no custom translator is provided, a default + * {@link SQLErrorCodeSQLExceptionTranslator} is used + * which examines the SQLException's vendor-specific error code. + * @see org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator + * @see org.springframework.jdbc.support.SQLStateSQLExceptionTranslator + */ + public void setExceptionTranslator(SQLExceptionTranslator exceptionTranslator) { + this.exceptionTranslator = exceptionTranslator; + } + + /** + * Return the exception translator for this instance. + *

Creates a default {@link SQLErrorCodeSQLExceptionTranslator} + * for the specified DataSource if none set, or a + * {@link SQLStateSQLExceptionTranslator} in case of no DataSource. + * @see #getDataSource() + */ + public synchronized SQLExceptionTranslator getExceptionTranslator() { + if (this.exceptionTranslator == null) { + DataSource dataSource = getDataSource(); + if (dataSource != null) { + this.exceptionTranslator = new SQLErrorCodeSQLExceptionTranslator(dataSource); + } + else { + this.exceptionTranslator = new SQLStateSQLExceptionTranslator(); + } + } + return this.exceptionTranslator; + } + + /** + * Set whether to lazily initialize the SQLExceptionTranslator for this accessor, + * on first encounter of a SQLException. Default is "true"; can be switched to + * "false" for initialization on startup. + *

Early initialization just applies if afterPropertiesSet() is called. + * @see #getExceptionTranslator() + * @see #afterPropertiesSet() + */ + public void setLazyInit(boolean lazyInit) { + this.lazyInit = lazyInit; + } + + /** + * Return whether to lazily initialize the SQLExceptionTranslator for this accessor. + * @see #getExceptionTranslator() + */ + public boolean isLazyInit() { + return this.lazyInit; + } + + /** + * Eagerly initialize the exception translator, if demanded, + * creating a default one for the specified DataSource if none set. + */ + public void afterPropertiesSet() { + if (getDataSource() == null) { + throw new IllegalArgumentException("Property 'dataSource' is required"); + } + if (!isLazyInit()) { + getExceptionTranslator(); + } + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/JdbcUtils.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/JdbcUtils.java new file mode 100644 index 0000000000..33947f9629 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/JdbcUtils.java @@ -0,0 +1,463 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.support; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.math.BigDecimal; +import java.sql.Blob; +import java.sql.Clob; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.sql.Statement; +import java.sql.Types; + +import javax.sql.DataSource; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.jdbc.CannotGetJdbcConnectionException; +import org.springframework.jdbc.datasource.DataSourceUtils; + +/** + * Generic utility methods for working with JDBC. Mainly for internal use + * within the framework, but also useful for custom JDBC access code. + * + * @author Thomas Risberg + * @author Juergen Hoeller + */ +public abstract class JdbcUtils { + + /** + * Constant that indicates an unknown (or unspecified) SQL type. + * @see java.sql.Types + */ + public static final int TYPE_UNKNOWN = Integer.MIN_VALUE; + + + private static final Log logger = LogFactory.getLog(JdbcUtils.class); + + + /** + * Close the given JDBC Connection and ignore any thrown exception. + * This is useful for typical finally blocks in manual JDBC code. + * @param con the JDBC Connection to close (may be null) + */ + public static void closeConnection(Connection con) { + if (con != null) { + try { + con.close(); + } + catch (SQLException ex) { + logger.debug("Could not close JDBC Connection", ex); + } + catch (Throwable ex) { + // We don't trust the JDBC driver: It might throw RuntimeException or Error. + logger.debug("Unexpected exception on closing JDBC Connection", ex); + } + } + } + + /** + * Close the given JDBC Statement and ignore any thrown exception. + * This is useful for typical finally blocks in manual JDBC code. + * @param stmt the JDBC Statement to close (may be null) + */ + public static void closeStatement(Statement stmt) { + if (stmt != null) { + try { + stmt.close(); + } + catch (SQLException ex) { + logger.trace("Could not close JDBC Statement", ex); + } + catch (Throwable ex) { + // We don't trust the JDBC driver: It might throw RuntimeException or Error. + logger.trace("Unexpected exception on closing JDBC Statement", ex); + } + } + } + + /** + * Close the given JDBC ResultSet and ignore any thrown exception. + * This is useful for typical finally blocks in manual JDBC code. + * @param rs the JDBC ResultSet to close (may be null) + */ + public static void closeResultSet(ResultSet rs) { + if (rs != null) { + try { + rs.close(); + } + catch (SQLException ex) { + logger.trace("Could not close JDBC ResultSet", ex); + } + catch (Throwable ex) { + // We don't trust the JDBC driver: It might throw RuntimeException or Error. + logger.trace("Unexpected exception on closing JDBC ResultSet", ex); + } + } + } + + /** + * Retrieve a JDBC column value from a ResultSet, using the specified value type. + *

Uses the specifically typed ResultSet accessor methods, falling back to + * {@link #getResultSetValue(java.sql.ResultSet, int)} for unknown types. + *

Note that the returned value may not be assignable to the specified + * required type, in case of an unknown type. Calling code needs to deal + * with this case appropriately, e.g. throwing a corresponding exception. + * @param rs is the ResultSet holding the data + * @param index is the column index + * @param requiredType the required value type (may be null) + * @return the value object + * @throws SQLException if thrown by the JDBC API + */ + public static Object getResultSetValue(ResultSet rs, int index, Class requiredType) throws SQLException { + if (requiredType == null) { + return getResultSetValue(rs, index); + } + + Object value = null; + boolean wasNullCheck = false; + + // Explicitly extract typed value, as far as possible. + if (String.class.equals(requiredType)) { + value = rs.getString(index); + } + else if (boolean.class.equals(requiredType) || Boolean.class.equals(requiredType)) { + value = Boolean.valueOf(rs.getBoolean(index)); + wasNullCheck = true; + } + else if (byte.class.equals(requiredType) || Byte.class.equals(requiredType)) { + value = new Byte(rs.getByte(index)); + wasNullCheck = true; + } + else if (short.class.equals(requiredType) || Short.class.equals(requiredType)) { + value = new Short(rs.getShort(index)); + wasNullCheck = true; + } + else if (int.class.equals(requiredType) || Integer.class.equals(requiredType)) { + value = new Integer(rs.getInt(index)); + wasNullCheck = true; + } + else if (long.class.equals(requiredType) || Long.class.equals(requiredType)) { + value = new Long(rs.getLong(index)); + wasNullCheck = true; + } + else if (float.class.equals(requiredType) || Float.class.equals(requiredType)) { + value = new Float(rs.getFloat(index)); + wasNullCheck = true; + } + else if (double.class.equals(requiredType) || Double.class.equals(requiredType) || + Number.class.equals(requiredType)) { + value = new Double(rs.getDouble(index)); + wasNullCheck = true; + } + else if (byte[].class.equals(requiredType)) { + value = rs.getBytes(index); + } + else if (java.sql.Date.class.equals(requiredType)) { + value = rs.getDate(index); + } + else if (java.sql.Time.class.equals(requiredType)) { + value = rs.getTime(index); + } + else if (java.sql.Timestamp.class.equals(requiredType) || java.util.Date.class.equals(requiredType)) { + value = rs.getTimestamp(index); + } + else if (BigDecimal.class.equals(requiredType)) { + value = rs.getBigDecimal(index); + } + else if (Blob.class.equals(requiredType)) { + value = rs.getBlob(index); + } + else if (Clob.class.equals(requiredType)) { + value = rs.getClob(index); + } + else { + // Some unknown type desired -> rely on getObject. + value = getResultSetValue(rs, index); + } + + // Perform was-null check if demanded (for results that the + // JDBC driver returns as primitives). + if (wasNullCheck && value != null && rs.wasNull()) { + value = null; + } + return value; + } + + /** + * Retrieve a JDBC column value from a ResultSet, using the most appropriate + * value type. The returned value should be a detached value object, not having + * any ties to the active ResultSet: in particular, it should not be a Blob or + * Clob object but rather a byte array respectively String representation. + *

Uses the getObject(index) method, but includes additional "hacks" + * to get around Oracle 10g returning a non-standard object for its TIMESTAMP + * datatype and a java.sql.Date for DATE columns leaving out the + * time portion: These columns will explicitly be extracted as standard + * java.sql.Timestamp object. + * @param rs is the ResultSet holding the data + * @param index is the column index + * @return the value object + * @throws SQLException if thrown by the JDBC API + * @see java.sql.Blob + * @see java.sql.Clob + * @see java.sql.Timestamp + */ + public static Object getResultSetValue(ResultSet rs, int index) throws SQLException { + Object obj = rs.getObject(index); + String className = null; + if (obj != null) { + className = obj.getClass().getName(); + } + if (obj instanceof Blob) { + obj = rs.getBytes(index); + } + else if (obj instanceof Clob) { + obj = rs.getString(index); + } + else if (className != null && + ("oracle.sql.TIMESTAMP".equals(className) || + "oracle.sql.TIMESTAMPTZ".equals(className))) { + obj = rs.getTimestamp(index); + } + else if (className != null && className.startsWith("oracle.sql.DATE")) { + String metaDataClassName = rs.getMetaData().getColumnClassName(index); + if ("java.sql.Timestamp".equals(metaDataClassName) || + "oracle.sql.TIMESTAMP".equals(metaDataClassName)) { + obj = rs.getTimestamp(index); + } + else { + obj = rs.getDate(index); + } + } + else if (obj != null && obj instanceof java.sql.Date) { + if ("java.sql.Timestamp".equals(rs.getMetaData().getColumnClassName(index))) { + obj = rs.getTimestamp(index); + } + } + return obj; + } + + /** + * Extract database meta data via the given DatabaseMetaDataCallback. + *

This method will open a connection to the database and retrieve the database metadata. + * Since this method is called before the exception translation feature is configured for + * a datasource, this method can not rely on the SQLException translation functionality. + *

Any exceptions will be wrapped in a MetaDataAccessException. This is a checked exception + * and any calling code should catch and handle this exception. You can just log the + * error and hope for the best, but there is probably a more serious error that will + * reappear when you try to access the database again. + * @param dataSource the DataSource to extract metadata for + * @param action callback that will do the actual work + * @return object containing the extracted information, as returned by + * the DatabaseMetaDataCallback's processMetaData method + * @throws MetaDataAccessException if meta data access failed + */ + public static Object extractDatabaseMetaData(DataSource dataSource, DatabaseMetaDataCallback action) + throws MetaDataAccessException { + + Connection con = null; + try { + con = DataSourceUtils.getConnection(dataSource); + if (con == null) { + // should only happen in test environments + throw new MetaDataAccessException("Connection returned by DataSource [" + dataSource + "] was null"); + } + DatabaseMetaData metaData = con.getMetaData(); + if (metaData == null) { + // should only happen in test environments + throw new MetaDataAccessException("DatabaseMetaData returned by Connection [" + con + "] was null"); + } + return action.processMetaData(metaData); + } + catch (CannotGetJdbcConnectionException ex) { + throw new MetaDataAccessException("Could not get Connection for extracting meta data", ex); + } + catch (SQLException ex) { + throw new MetaDataAccessException("Error while extracting DatabaseMetaData", ex); + } + catch (AbstractMethodError err) { + throw new MetaDataAccessException( + "JDBC DatabaseMetaData method not implemented by JDBC driver - upgrade your driver", err); + } + finally { + DataSourceUtils.releaseConnection(con, dataSource); + } + } + + /** + * Call the specified method on DatabaseMetaData for the given DataSource, + * and extract the invocation result. + * @param dataSource the DataSource to extract meta data for + * @param metaDataMethodName the name of the DatabaseMetaData method to call + * @return the object returned by the specified DatabaseMetaData method + * @throws MetaDataAccessException if we couldn't access the DatabaseMetaData + * or failed to invoke the specified method + * @see java.sql.DatabaseMetaData + */ + public static Object extractDatabaseMetaData(DataSource dataSource, final String metaDataMethodName) + throws MetaDataAccessException { + + return extractDatabaseMetaData(dataSource, + new DatabaseMetaDataCallback() { + public Object processMetaData(DatabaseMetaData dbmd) throws SQLException, MetaDataAccessException { + try { + Method method = DatabaseMetaData.class.getMethod(metaDataMethodName, (Class[]) null); + return method.invoke(dbmd, (Object[]) null); + } + catch (NoSuchMethodException ex) { + throw new MetaDataAccessException("No method named '" + metaDataMethodName + + "' found on DatabaseMetaData instance [" + dbmd + "]", ex); + } + catch (IllegalAccessException ex) { + throw new MetaDataAccessException( + "Could not access DatabaseMetaData method '" + metaDataMethodName + "'", ex); + } + catch (InvocationTargetException ex) { + if (ex.getTargetException() instanceof SQLException) { + throw (SQLException) ex.getTargetException(); + } + throw new MetaDataAccessException( + "Invocation of DatabaseMetaData method '" + metaDataMethodName + "' failed", ex); + } + } + }); + } + + /** + * Return whether the given JDBC driver supports JDBC 2.0 batch updates. + *

Typically invoked right before execution of a given set of statements: + * to decide whether the set of SQL statements should be executed through + * the JDBC 2.0 batch mechanism or simply in a traditional one-by-one fashion. + *

Logs a warning if the "supportsBatchUpdates" methods throws an exception + * and simply returns false in that case. + * @param con the Connection to check + * @return whether JDBC 2.0 batch updates are supported + * @see java.sql.DatabaseMetaData#supportsBatchUpdates() + */ + public static boolean supportsBatchUpdates(Connection con) { + try { + DatabaseMetaData dbmd = con.getMetaData(); + if (dbmd != null) { + if (dbmd.supportsBatchUpdates()) { + logger.debug("JDBC driver supports batch updates"); + return true; + } + else { + logger.debug("JDBC driver does not support batch updates"); + } + } + } + catch (SQLException ex) { + logger.debug("JDBC driver 'supportsBatchUpdates' method threw exception", ex); + } + catch (AbstractMethodError err) { + logger.debug("JDBC driver does not support JDBC 2.0 'supportsBatchUpdates' method", err); + } + return false; + } + + /** + * Extract a common name for the database in use even if various drivers/platforms provide varying names. + * @param source the name as provided in database metedata + * @return the common name to be used + */ + public static String commonDatabaseName(String source) { + String name = source; + if (source != null && source.startsWith("DB2")) { + name = "DB2"; + } + else if ("Sybase SQL Server".equals(source) || + "Adaptive Server Enterprise".equals(source) || "sql server".equals(source) ) { + name = "Sybase"; + } + return name; + } + + /** + * Check whether the given SQL type is numeric. + * @param sqlType the SQL type to be checked + * @return whether the type is numeric + */ + public static boolean isNumeric(int sqlType) { + return Types.BIT == sqlType || Types.BIGINT == sqlType || Types.DECIMAL == sqlType || + Types.DOUBLE == sqlType || Types.FLOAT == sqlType || Types.INTEGER == sqlType || + Types.NUMERIC == sqlType || Types.REAL == sqlType || Types.SMALLINT == sqlType || + Types.TINYINT == sqlType; + } + + /** + * Determine the column name to use. The column name is determined based on a + * lookup using ResultSetMetaData. + *

This method implementation takes into account recent clarifications + * expressed in the JDBC 4.0 specification: + *

columnLabel - the label for the column specified with the SQL AS clause. + * If the SQL AS clause was not specified, then the label is the name of the column. + * @return the column name to use + * @param resultSetMetaData the current meta data to use + * @param columnIndex the index of the column for the look up + * @throws SQLException in case of lookup failure + */ + public static String lookupColumnName(ResultSetMetaData resultSetMetaData, int columnIndex) throws SQLException { + String name = resultSetMetaData.getColumnLabel(columnIndex); + if (name == null || name.length() < 1) { + name = resultSetMetaData.getColumnName(columnIndex); + } + return name; + } + + /** + * Convert a column name with underscores to the corresponding property name using "camel case". A name + * like "customer_number" would match a "customerNumber" property name. + * @param name the column name to be converted + * @return the name using "camel case" + */ + public static String convertUnderscoreNameToPropertyName(String name) { + StringBuffer result = new StringBuffer(); + boolean nextIsUpper = false; + if (name != null && name.length() > 0) { + if (name.length() > 1 && name.substring(1,2).equals("_")) { + result.append(name.substring(0, 1).toUpperCase()); + } + else { + result.append(name.substring(0, 1).toLowerCase()); + } + for (int i = 1; i < name.length(); i++) { + String s = name.substring(i, i + 1); + if (s.equals("_")) { + nextIsUpper = true; + } + else { + if (nextIsUpper) { + result.append(s.toUpperCase()); + nextIsUpper = false; + } + else { + result.append(s.toLowerCase()); + } + } + } + } + return result.toString(); + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/KeyHolder.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/KeyHolder.java new file mode 100644 index 0000000000..a2b8d4c6cf --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/KeyHolder.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2005 the original author or authors. + * + * Licensed 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.springframework.jdbc.support; + +import java.util.List; +import java.util.Map; + +import org.springframework.dao.InvalidDataAccessApiUsageException; + +/** + * Interface for retrieving keys, typically used for auto-generated keys + * as potentially returned by JDBC insert statements. + * + *

Implementations of this interface can hold any number of keys. + * In the general case, the keys are returned as a List containing one Map + * for each row of keys. + * + *

Most applications only use on key per row and process only one row at a + * time in an insert statement. In these cases, just call getKey + * to retrieve the key. The returned value is a Number here, which is the + * usual type for auto-generated keys. + * + * @author Thomas Risberg + * @since 1.1 + * @see org.springframework.jdbc.core.JdbcTemplate + * @see org.springframework.jdbc.object.SqlUpdate + */ +public interface KeyHolder { + + /** + * Retrieve the first item from the first map, assuming that there is just + * one item and just one map, and that the item is a number. + * This is the typical case: a single, numeric generated key. + *

Keys are held in a List of Maps, where each item in the list represents + * the keys for each row. If there are multiple columns, then the Map will have + * multiple entries as well. If this method encounters multiple entries in + * either the map or the list meaning that multiple keys were returned, + * then an InvalidDataAccessApiUsageException is thrown. + * @return the generated key + * @throws InvalidDataAccessApiUsageException if multiple keys are encountered. + */ + Number getKey() throws InvalidDataAccessApiUsageException; + + /** + * Retrieve the first map of keys. If there are multiple entries in the list + * (meaning that multiple rows had keys returned), then an + * InvalidDataAccessApiUsageException is thrown. + * @return the Map of generated keys + * @throws InvalidDataAccessApiUsageException if keys for multiple rows are encountered + */ + Map getKeys() throws InvalidDataAccessApiUsageException; + + /** + * Return a reference to the List that contains the keys. + * Can be used for extracting keys for multiple rows (an unusual case), + * and also for adding new maps of keys. + * @return the List for the generated keys, with each entry being a Map + * of column names and key values + */ + List getKeyList(); + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/MetaDataAccessException.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/MetaDataAccessException.java new file mode 100644 index 0000000000..e4870e97c7 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/MetaDataAccessException.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed 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.springframework.jdbc.support; + +import org.springframework.core.NestedCheckedException; + +/** + * Exception indicating that something went wrong during JDBC metadata lookup. + * + *

This is a checked exception since we want it to be caught, logged and + * handled rather than cause the application to fail. Failure to read JDBC + * metadata is usually not a fatal problem. + * + * @author Thomas Risberg + * @since 1.0.1 + */ +public class MetaDataAccessException extends NestedCheckedException { + + /** + * Constructor for MetaDataAccessException. + * @param msg the detail message + */ + public MetaDataAccessException(String msg) { + super(msg); + } + + /** + * Constructor for MetaDataAccessException. + * @param msg the detail message + * @param cause the root cause from the data access API in use + */ + public MetaDataAccessException(String msg, Throwable cause) { + super(msg, cause); + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodeSQLExceptionTranslator.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodeSQLExceptionTranslator.java new file mode 100644 index 0000000000..b9c66fdfc4 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodeSQLExceptionTranslator.java @@ -0,0 +1,388 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.support; + +import java.lang.reflect.Constructor; +import java.sql.BatchUpdateException; +import java.sql.SQLException; +import java.util.Arrays; + +import javax.sql.DataSource; + +import org.springframework.core.JdkVersion; +import org.springframework.dao.CannotAcquireLockException; +import org.springframework.dao.CannotSerializeTransactionException; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.dao.DeadlockLoserDataAccessException; +import org.springframework.dao.PermissionDeniedDataAccessException; +import org.springframework.dao.TransientDataAccessResourceException; +import org.springframework.jdbc.BadSqlGrammarException; +import org.springframework.jdbc.InvalidResultSetAccessException; + +/** + * Implementation of {@link SQLExceptionTranslator} that analyzes vendor-specific error codes. + * More precise than an implementation based on SQL state, but heavily vendor-specific. + * + *

This class applies the following matching rules: + *

    + *
  • Try custom translation implemented by any subclass. Note that this class is + * concrete and is typically used itself, in which case this rule doesn't apply. + *
  • Apply error code matching. Error codes are obtained from the SQLErrorCodesFactory + * by default. This factory loads a "sql-error-codes.xml" file from the class path, + * defining error code mappings for database names from database metadata. + *
  • Fallback to a fallback translator. {@link SQLStateSQLExceptionTranslator} is the + * default fallback translator, analyzing the exception's SQL state only. On Java 6 + * which introduces its own SQLException subclass hierarchy, we will + * use {@link SQLExceptionSubclassTranslator} by default, which in turns falls back + * to Spring's own SQL state translation when not encountering specific subclasses. + *
+ * + *

The configuration file named "sql-error-codes.xml" is by default read from + * this package. It can be overridden through a file of the same name in the root + * of the class path (e.g. in the "/WEB-INF/classes" directory), as long as the + * Spring JDBC package is loaded from the same ClassLoader. + * + * @author Rod Johnson + * @author Thomas Risberg + * @author Juergen Hoeller + * @see SQLErrorCodesFactory + * @see SQLStateSQLExceptionTranslator + */ +public class SQLErrorCodeSQLExceptionTranslator extends AbstractFallbackSQLExceptionTranslator { + + private static final int MESSAGE_ONLY_CONSTRUCTOR = 1; + private static final int MESSAGE_THROWABLE_CONSTRUCTOR = 2; + private static final int MESSAGE_SQLEX_CONSTRUCTOR = 3; + private static final int MESSAGE_SQL_THROWABLE_CONSTRUCTOR = 4; + private static final int MESSAGE_SQL_SQLEX_CONSTRUCTOR = 5; + + + /** Error codes used by this translator */ + private SQLErrorCodes sqlErrorCodes; + + + /** + * Constructor for use as a JavaBean. + * The SqlErrorCodes or DataSource property must be set. + */ + public SQLErrorCodeSQLExceptionTranslator() { + if (JdkVersion.getMajorJavaVersion() >= JdkVersion.JAVA_16) { + setFallbackTranslator(new SQLExceptionSubclassTranslator()); + } + else { + setFallbackTranslator(new SQLStateSQLExceptionTranslator()); + } + } + + /** + * Create a SQL error code translator for the given DataSource. + * Invoking this constructor will cause a Connection to be obtained + * from the DataSource to get the metadata. + * @param dataSource DataSource to use to find metadata and establish + * which error codes are usable + * @see SQLErrorCodesFactory + */ + public SQLErrorCodeSQLExceptionTranslator(DataSource dataSource) { + this(); + setDataSource(dataSource); + } + + /** + * Create a SQL error code translator for the given database product name. + * Invoking this constructor will avoid obtaining a Connection from the + * DataSource to get the metadata. + * @param dbName the database product name that identifies the error codes entry + * @see SQLErrorCodesFactory + * @see java.sql.DatabaseMetaData#getDatabaseProductName() + */ + public SQLErrorCodeSQLExceptionTranslator(String dbName) { + this(); + setDatabaseProductName(dbName); + } + + /** + * Create a SQLErrorCode translator given these error codes. + * Does not require a database metadata lookup to be performed using a connection. + * @param sec error codes + */ + public SQLErrorCodeSQLExceptionTranslator(SQLErrorCodes sec) { + this(); + this.sqlErrorCodes = sec; + } + + + /** + * Set the DataSource for this translator. + *

Setting this property will cause a Connection to be obtained from + * the DataSource to get the metadata. + * @param dataSource DataSource to use to find metadata and establish + * which error codes are usable + * @see SQLErrorCodesFactory#getErrorCodes(javax.sql.DataSource) + * @see java.sql.DatabaseMetaData#getDatabaseProductName() + */ + public void setDataSource(DataSource dataSource) { + this.sqlErrorCodes = SQLErrorCodesFactory.getInstance().getErrorCodes(dataSource); + } + + /** + * Set the database product name for this translator. + *

Setting this property will avoid obtaining a Connection from the DataSource + * to get the metadata. + * @param dbName the database product name that identifies the error codes entry + * @see SQLErrorCodesFactory#getErrorCodes(String) + * @see java.sql.DatabaseMetaData#getDatabaseProductName() + */ + public void setDatabaseProductName(String dbName) { + this.sqlErrorCodes = SQLErrorCodesFactory.getInstance().getErrorCodes(dbName); + } + + /** + * Set custom error codes to be used for translation. + * @param sec custom error codes to use + */ + public void setSqlErrorCodes(SQLErrorCodes sec) { + this.sqlErrorCodes = sec; + } + + /** + * Return the error codes used by this translator. + * Usually determined via a DataSource. + * @see #setDataSource + */ + public SQLErrorCodes getSqlErrorCodes() { + return this.sqlErrorCodes; + } + + + protected DataAccessException doTranslate(String task, String sql, SQLException ex) { + SQLException sqlEx = ex; + if (sqlEx instanceof BatchUpdateException && sqlEx.getNextException() != null) { + SQLException nestedSqlEx = sqlEx.getNextException(); + if (nestedSqlEx.getErrorCode() > 0 || nestedSqlEx.getSQLState() != null) { + logger.debug("Using nested SQLException from the BatchUpdateException"); + sqlEx = nestedSqlEx; + } + } + + // First, try custom translation from overridden method. + DataAccessException dex = customTranslate(task, sql, sqlEx); + if (dex != null) { + return dex; + } + + // Check SQLErrorCodes with corresponding error code, if available. + if (this.sqlErrorCodes != null) { + String errorCode = null; + if (this.sqlErrorCodes.isUseSqlStateForTranslation()) { + errorCode = sqlEx.getSQLState(); + } + else { + errorCode = Integer.toString(sqlEx.getErrorCode()); + } + + if (errorCode != null) { + // Look for defined custom translations first. + CustomSQLErrorCodesTranslation[] customTranslations = this.sqlErrorCodes.getCustomTranslations(); + if (customTranslations != null) { + for (int i = 0; i < customTranslations.length; i++) { + CustomSQLErrorCodesTranslation customTranslation = customTranslations[i]; + if (Arrays.binarySearch(customTranslation.getErrorCodes(), errorCode) >= 0) { + if (customTranslation.getExceptionClass() != null) { + DataAccessException customException = createCustomException( + task, sql, sqlEx, customTranslation.getExceptionClass()); + if (customException != null) { + logTranslation(task, sql, sqlEx, true); + return customException; + } + } + } + } + } + // Next, look for grouped error codes. + if (Arrays.binarySearch(this.sqlErrorCodes.getBadSqlGrammarCodes(), errorCode) >= 0) { + logTranslation(task, sql, sqlEx, false); + return new BadSqlGrammarException(task, sql, sqlEx); + } + else if (Arrays.binarySearch(this.sqlErrorCodes.getInvalidResultSetAccessCodes(), errorCode) >= 0) { + logTranslation(task, sql, sqlEx, false); + return new InvalidResultSetAccessException(task, sql, sqlEx); + } + else if (Arrays.binarySearch(this.sqlErrorCodes.getDataIntegrityViolationCodes(), errorCode) >= 0) { + logTranslation(task, sql, sqlEx, false); + return new DataIntegrityViolationException(buildMessage(task, sql, sqlEx), sqlEx); + } + else if (Arrays.binarySearch(this.sqlErrorCodes.getPermissionDeniedCodes(), errorCode) >= 0) { + logTranslation(task, sql, sqlEx, false); + return new PermissionDeniedDataAccessException(buildMessage(task, sql, sqlEx), sqlEx); + } + else if (Arrays.binarySearch(this.sqlErrorCodes.getDataAccessResourceFailureCodes(), errorCode) >= 0) { + logTranslation(task, sql, sqlEx, false); + return new DataAccessResourceFailureException(buildMessage(task, sql, sqlEx), sqlEx); + } + else if (Arrays.binarySearch(this.sqlErrorCodes.getTransientDataAccessResourceCodes(), errorCode) >= 0) { + logTranslation(task, sql, sqlEx, false); + return new TransientDataAccessResourceException(buildMessage(task, sql, sqlEx), sqlEx); + } + else if (Arrays.binarySearch(this.sqlErrorCodes.getCannotAcquireLockCodes(), errorCode) >= 0) { + logTranslation(task, sql, sqlEx, false); + return new CannotAcquireLockException(buildMessage(task, sql, sqlEx), sqlEx); + } + else if (Arrays.binarySearch(this.sqlErrorCodes.getDeadlockLoserCodes(), errorCode) >= 0) { + logTranslation(task, sql, sqlEx, false); + return new DeadlockLoserDataAccessException(buildMessage(task, sql, sqlEx), sqlEx); + } + else if (Arrays.binarySearch(this.sqlErrorCodes.getCannotSerializeTransactionCodes(), errorCode) >= 0) { + logTranslation(task, sql, sqlEx, false); + return new CannotSerializeTransactionException(buildMessage(task, sql, sqlEx), sqlEx); + } + } + } + + // We couldn't identify it more precisely - let's hand it over to the SQLState fallback translator. + if (logger.isDebugEnabled()) { + String codes = null; + if (this.sqlErrorCodes != null && this.sqlErrorCodes.isUseSqlStateForTranslation()) { + codes = "SQL state '" + sqlEx.getSQLState() + "', error code '" + sqlEx.getErrorCode(); + } + else { + codes = "Error code '" + sqlEx.getErrorCode() + "'"; + } + logger.debug("Unable to translate SQLException with " + codes + ", will now try the fallback translator"); + } + + return null; + } + + /** + * Subclasses can override this method to attempt a custom mapping from SQLException + * to DataAccessException. + * @param task readable text describing the task being attempted + * @param sql SQL query or update that caused the problem. May be null. + * @param sqlEx the offending SQLException + * @return null if no custom translation was possible, otherwise a DataAccessException + * resulting from custom translation. This exception should include the sqlEx parameter + * as a nested root cause. This implementation always returns null, meaning that + * the translator always falls back to the default error codes. + */ + protected DataAccessException customTranslate(String task, String sql, SQLException sqlEx) { + return null; + } + + /** + * Create a custom DataAccessException, based on a given exception + * class from a CustomSQLErrorCodesTranslation definition. + * @param task readable text describing the task being attempted + * @param sql SQL query or update that caused the problem. May be null. + * @param sqlEx the offending SQLException + * @param exceptionClass the exception class to use, as defined in the + * CustomSQLErrorCodesTranslation definition + * @return null if the custom exception could not be created, otherwise + * the resulting DataAccessException. This exception should include the + * sqlEx parameter as a nested root cause. + * @see CustomSQLErrorCodesTranslation#setExceptionClass + */ + protected DataAccessException createCustomException( + String task, String sql, SQLException sqlEx, Class exceptionClass) { + + // find appropriate constructor + try { + int constructorType = 0; + Constructor[] constructors = exceptionClass.getConstructors(); + for (int i = 0; i < constructors.length; i++) { + Class[] parameterTypes = constructors[i].getParameterTypes(); + if (parameterTypes.length == 1 && parameterTypes[0].equals(String.class)) { + if (constructorType < MESSAGE_ONLY_CONSTRUCTOR) + constructorType = MESSAGE_ONLY_CONSTRUCTOR; + } + if (parameterTypes.length == 2 && parameterTypes[0].equals(String.class) && + parameterTypes[1].equals(Throwable.class)) { + if (constructorType < MESSAGE_THROWABLE_CONSTRUCTOR) + constructorType = MESSAGE_THROWABLE_CONSTRUCTOR; + } + if (parameterTypes.length == 2 && parameterTypes[0].equals(String.class) && + parameterTypes[1].equals(SQLException.class)) { + if (constructorType < MESSAGE_SQLEX_CONSTRUCTOR) + constructorType = MESSAGE_SQLEX_CONSTRUCTOR; + } + if (parameterTypes.length == 3 && parameterTypes[0].equals(String.class) && + parameterTypes[1].equals(String.class) && parameterTypes[2].equals(Throwable.class)) { + if (constructorType < MESSAGE_SQL_THROWABLE_CONSTRUCTOR) + constructorType = MESSAGE_SQL_THROWABLE_CONSTRUCTOR; + } + if (parameterTypes.length == 3 && parameterTypes[0].equals(String.class) && + parameterTypes[1].equals(String.class) && parameterTypes[2].equals(SQLException.class)) { + if (constructorType < MESSAGE_SQL_SQLEX_CONSTRUCTOR) + constructorType = MESSAGE_SQL_SQLEX_CONSTRUCTOR; + } + } + + // invoke constructor + Constructor exceptionConstructor = null; + switch (constructorType) { + case MESSAGE_SQL_SQLEX_CONSTRUCTOR: + Class[] messageAndSqlAndSqlExArgsClass = new Class[] {String.class, String.class, SQLException.class}; + Object[] messageAndSqlAndSqlExArgs = new Object[] {task, sql, sqlEx}; + exceptionConstructor = exceptionClass.getConstructor(messageAndSqlAndSqlExArgsClass); + return (DataAccessException) exceptionConstructor.newInstance(messageAndSqlAndSqlExArgs); + case MESSAGE_SQL_THROWABLE_CONSTRUCTOR: + Class[] messageAndSqlAndThrowableArgsClass = new Class[] {String.class, String.class, Throwable.class}; + Object[] messageAndSqlAndThrowableArgs = new Object[] {task, sql, sqlEx}; + exceptionConstructor = exceptionClass.getConstructor(messageAndSqlAndThrowableArgsClass); + return (DataAccessException) exceptionConstructor.newInstance(messageAndSqlAndThrowableArgs); + case MESSAGE_SQLEX_CONSTRUCTOR: + Class[] messageAndSqlExArgsClass = new Class[] {String.class, SQLException.class}; + Object[] messageAndSqlExArgs = new Object[] {task + ": " + sqlEx.getMessage(), sqlEx}; + exceptionConstructor = exceptionClass.getConstructor(messageAndSqlExArgsClass); + return (DataAccessException) exceptionConstructor.newInstance(messageAndSqlExArgs); + case MESSAGE_THROWABLE_CONSTRUCTOR: + Class[] messageAndThrowableArgsClass = new Class[] {String.class, Throwable.class}; + Object[] messageAndThrowableArgs = new Object[] {task + ": " + sqlEx.getMessage(), sqlEx}; + exceptionConstructor = exceptionClass.getConstructor(messageAndThrowableArgsClass); + return (DataAccessException)exceptionConstructor.newInstance(messageAndThrowableArgs); + case MESSAGE_ONLY_CONSTRUCTOR: + Class[] messageOnlyArgsClass = new Class[] {String.class}; + Object[] messageOnlyArgs = new Object[] {task + ": " + sqlEx.getMessage()}; + exceptionConstructor = exceptionClass.getConstructor(messageOnlyArgsClass); + return (DataAccessException) exceptionConstructor.newInstance(messageOnlyArgs); + default: + if (logger.isWarnEnabled()) { + logger.warn("Unable to find appropriate constructor of custom exception class [" + + exceptionClass.getName() + "]"); + } + return null; + } + } + catch (Throwable ex) { + if (logger.isWarnEnabled()) { + logger.warn("Unable to instantiate custom exception class [" + exceptionClass.getName() + "]", ex); + } + return null; + } + } + + private void logTranslation(String task, String sql, SQLException sqlEx, boolean custom) { + if (logger.isDebugEnabled()) { + String intro = custom ? "Custom translation of" : "Translating"; + logger.debug(intro + " SQLException with SQL state '" + sqlEx.getSQLState() + + "', error code '" + sqlEx.getErrorCode() + "', message [" + sqlEx.getMessage() + + "]; SQL was [" + sql + "] for task [" + task + "]"); + } + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodes.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodes.java new file mode 100644 index 0000000000..6ffc31aa68 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodes.java @@ -0,0 +1,179 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.support; + +import org.springframework.util.StringUtils; + +/** + * JavaBean for holding JDBC error codes for a particular database. + * Instances of this class are normally loaded through a bean factory. + * + *

Used by Spring's {@link SQLErrorCodeSQLExceptionTranslator}. + * The file "sql-error-codes.xml" in this package contains default + * SQLErrorCodes instances for various databases. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @see SQLErrorCodesFactory + * @see SQLErrorCodeSQLExceptionTranslator + */ +public class SQLErrorCodes { + + private String[] databaseProductNames; + + private boolean useSqlStateForTranslation = false; + + private String[] badSqlGrammarCodes = new String[0]; + + private String[] invalidResultSetAccessCodes = new String[0]; + + private String[] dataIntegrityViolationCodes = new String[0]; + + private String[] permissionDeniedCodes = new String[0]; + + private String[] dataAccessResourceFailureCodes = new String[0]; + + private String[] transientDataAccessResourceCodes = new String[0]; + + private String[] cannotAcquireLockCodes = new String[0]; + + private String[] deadlockLoserCodes = new String[0]; + + private String[] cannotSerializeTransactionCodes = new String[0]; + + private CustomSQLErrorCodesTranslation[] customTranslations; + + + /** + * Set this property if the database name contains spaces, + * in which case we can not use the bean name for lookup. + */ + public void setDatabaseProductName(String databaseProductName) { + this.databaseProductNames = new String[] {databaseProductName}; + } + + public String getDatabaseProductName() { + return (this.databaseProductNames != null && this.databaseProductNames.length > 0 ? + this.databaseProductNames[0] : null); + } + + /** + * Set this property to specify multiple database names that contains spaces, + * in which case we can not use bean names for lookup. + */ + public void setDatabaseProductNames(String[] databaseProductNames) { + this.databaseProductNames = databaseProductNames; + } + + public String[] getDatabaseProductNames() { + return this.databaseProductNames; + } + + /** + * Set this property to true for databases that do not provide an error code + * but that do provide SQL State (this includes PostgreSQL). + */ + public void setUseSqlStateForTranslation(boolean useStateCodeForTranslation) { + this.useSqlStateForTranslation = useStateCodeForTranslation; + } + + public boolean isUseSqlStateForTranslation() { + return this.useSqlStateForTranslation; + } + + + public void setBadSqlGrammarCodes(String[] badSqlGrammarCodes) { + this.badSqlGrammarCodes = StringUtils.sortStringArray(badSqlGrammarCodes); + } + + public String[] getBadSqlGrammarCodes() { + return this.badSqlGrammarCodes; + } + + public void setInvalidResultSetAccessCodes(String[] invalidResultSetAccessCodes) { + this.invalidResultSetAccessCodes = invalidResultSetAccessCodes; + } + + public String[] getInvalidResultSetAccessCodes() { + return this.invalidResultSetAccessCodes; + } + + public void setDataIntegrityViolationCodes(String[] dataIntegrityViolationCodes) { + this.dataIntegrityViolationCodes = StringUtils.sortStringArray(dataIntegrityViolationCodes); + } + + public String[] getDataIntegrityViolationCodes() { + return this.dataIntegrityViolationCodes; + } + + public void setPermissionDeniedCodes(String[] permissionDeniedCodes) { + this.permissionDeniedCodes = StringUtils.sortStringArray(permissionDeniedCodes); + } + + public String[] getPermissionDeniedCodes() { + return this.permissionDeniedCodes; + } + + public void setDataAccessResourceFailureCodes(String[] dataAccessResourceFailureCodes) { + this.dataAccessResourceFailureCodes = dataAccessResourceFailureCodes; + } + + public String[] getDataAccessResourceFailureCodes() { + return this.dataAccessResourceFailureCodes; + } + + public void setTransientDataAccessResourceCodes(String[] transientDataAccessResourceCodes) { + this.transientDataAccessResourceCodes = transientDataAccessResourceCodes; + } + + public String[] getTransientDataAccessResourceCodes() { + return this.transientDataAccessResourceCodes; + } + + public void setCannotAcquireLockCodes(String[] cannotAcquireLockCodes) { + this.cannotAcquireLockCodes = StringUtils.sortStringArray(cannotAcquireLockCodes); + } + + public String[] getCannotAcquireLockCodes() { + return this.cannotAcquireLockCodes; + } + + public void setDeadlockLoserCodes(String[] deadlockLoserCodes) { + this.deadlockLoserCodes = deadlockLoserCodes; + } + + public String[] getDeadlockLoserCodes() { + return this.deadlockLoserCodes; + } + + public void setCannotSerializeTransactionCodes(String[] cannotSerializeTransactionCodes) { + this.cannotSerializeTransactionCodes = cannotSerializeTransactionCodes; + } + + public String[] getCannotSerializeTransactionCodes() { + return this.cannotSerializeTransactionCodes; + } + + public void setCustomTranslations(CustomSQLErrorCodesTranslation[] customTranslations) { + this.customTranslations = customTranslations; + } + + public CustomSQLErrorCodesTranslation[] getCustomTranslations() { + return this.customTranslations; + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodesFactory.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodesFactory.java new file mode 100644 index 0000000000..ac60f164b0 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodesFactory.java @@ -0,0 +1,253 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.support; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import javax.sql.DataSource; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.util.Assert; +import org.springframework.util.PatternMatchUtils; + +/** + * Factory for creating {@link SQLErrorCodes} based on the + * "databaseProductName" taken from the {@link java.sql.DatabaseMetaData}. + * + *

Returns SQLErrorCodes populated with vendor codes + * defined in a configuration file named "sql-error-codes.xml". + * Reads the default file in this package if not overridden by a file in + * the root of the class path (for example in the "/WEB-INF/classes" directory). + * + * @author Thomas Risberg + * @author Rod Johnson + * @author Juergen Hoeller + * @see java.sql.DatabaseMetaData#getDatabaseProductName() + */ +public class SQLErrorCodesFactory { + + /** + * The name of custom SQL error codes file, loading from the root + * of the class path (e.g. from the "/WEB-INF/classes" directory). + */ + public static final String SQL_ERROR_CODE_OVERRIDE_PATH = "sql-error-codes.xml"; + + /** + * The name of default SQL error code files, loading from the class path. + */ + public static final String SQL_ERROR_CODE_DEFAULT_PATH = "org/springframework/jdbc/support/sql-error-codes.xml"; + + + private static final Log logger = LogFactory.getLog(SQLErrorCodesFactory.class); + + /** + * Keep track of a single instance so we can return it to classes that request it. + */ + private static final SQLErrorCodesFactory instance = new SQLErrorCodesFactory(); + + + /** + * Return the singleton instance. + */ + public static SQLErrorCodesFactory getInstance() { + return instance; + } + + + /** + * Map to hold error codes for all databases defined in the config file. + * Key is the database product name, value is the SQLErrorCodes instance. + */ + private final Map errorCodesMap; + + /** + * Map to cache the SQLErrorCodes instance per DataSource. + * Key is the DataSource, value is the SQLErrorCodes instance. + */ + private final Map dataSourceCache = new HashMap(16); + + + /** + * Create a new instance of the {@link SQLErrorCodesFactory} class. + *

Not public to enforce Singleton design pattern. Would be private + * except to allow testing via overriding the + * {@link #loadResource(String)} method. + *

Do not subclass in application code. + * @see #loadResource(String) + */ + protected SQLErrorCodesFactory() { + Map errorCodes = null; + + try { + DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(); + XmlBeanDefinitionReader bdr = new XmlBeanDefinitionReader(lbf); + + // Load default SQL error codes. + Resource resource = loadResource(SQL_ERROR_CODE_DEFAULT_PATH); + if (resource != null && resource.exists()) { + bdr.loadBeanDefinitions(resource); + } + else { + logger.warn("Default sql-error-codes.xml not found (should be included in spring.jar)"); + } + + // Load custom SQL error codes, overriding defaults. + resource = loadResource(SQL_ERROR_CODE_OVERRIDE_PATH); + if (resource != null && resource.exists()) { + bdr.loadBeanDefinitions(resource); + logger.info("Found custom sql-error-codes.xml file at the root of the classpath"); + } + + // Check all beans of type SQLErrorCodes. + errorCodes = lbf.getBeansOfType(SQLErrorCodes.class, true, false); + if (logger.isInfoEnabled()) { + logger.info("SQLErrorCodes loaded: " + errorCodes.keySet()); + } + } + catch (BeansException ex) { + logger.warn("Error loading SQL error codes from config file", ex); + errorCodes = Collections.EMPTY_MAP; + } + + this.errorCodesMap = errorCodes; + } + + /** + * Load the given resource from the class path. + *

Not to be overridden by application developers, who should obtain + * instances of this class from the static {@link #getInstance()} method. + *

Protected for testability. + * @param path resource path; either a custom path or one of either + * {@link #SQL_ERROR_CODE_DEFAULT_PATH} or + * {@link #SQL_ERROR_CODE_OVERRIDE_PATH}. + * @return the resource, or null if the resource wasn't found + * @see #getInstance + */ + protected Resource loadResource(String path) { + return new ClassPathResource(path, getClass().getClassLoader()); + } + + + /** + * Return the {@link SQLErrorCodes} instance for the given database. + *

No need for a database metadata lookup. + * @param dbName the database name (must not be null) + * @return the SQLErrorCodes instance for the given database + * @throws IllegalArgumentException if the supplied database name is null + */ + public SQLErrorCodes getErrorCodes(String dbName) { + Assert.notNull(dbName, "Database product name must not be null"); + + SQLErrorCodes sec = (SQLErrorCodes) this.errorCodesMap.get(dbName); + if (sec == null) { + for (Iterator it = this.errorCodesMap.values().iterator(); it.hasNext();) { + SQLErrorCodes candidate = (SQLErrorCodes) it.next(); + if (PatternMatchUtils.simpleMatch(candidate.getDatabaseProductNames(), dbName)) { + sec = candidate; + break; + } + } + } + if (sec != null) { + if (logger.isDebugEnabled()) { + logger.debug("SQL error codes for '" + dbName + "' found"); + } + return sec; + } + + // Could not find the database among the defined ones. + if (logger.isDebugEnabled()) { + logger.debug("SQL error codes for '" + dbName + "' not found"); + } + return new SQLErrorCodes(); + } + + /** + * Return {@link SQLErrorCodes} for the given {@link DataSource}, + * evaluating "databaseProductName" from the + * {@link java.sql.DatabaseMetaData}, or an empty error codes + * instance if no SQLErrorCodes were found. + * @param dataSource the DataSource identifying the database + * @return the corresponding SQLErrorCodes object + * @see java.sql.DatabaseMetaData#getDatabaseProductName() + */ + public SQLErrorCodes getErrorCodes(DataSource dataSource) { + Assert.notNull(dataSource, "DataSource must not be null"); + if (logger.isDebugEnabled()) { + logger.debug("Looking up default SQLErrorCodes for DataSource [" + dataSource + "]"); + } + + synchronized (this.dataSourceCache) { + // Let's avoid looking up database product info if we can. + SQLErrorCodes sec = (SQLErrorCodes) this.dataSourceCache.get(dataSource); + if (sec != null) { + if (logger.isDebugEnabled()) { + logger.debug("SQLErrorCodes found in cache for DataSource [" + + dataSource.getClass().getName() + '@' + Integer.toHexString(dataSource.hashCode()) + "]"); + } + return sec; + } + // We could not find it - got to look it up. + try { + String dbName = (String) + JdbcUtils.extractDatabaseMetaData(dataSource, "getDatabaseProductName"); + if (dbName != null) { + if (logger.isDebugEnabled()) { + logger.debug("Database product name cached for DataSource [" + + dataSource.getClass().getName() + '@' + Integer.toHexString(dataSource.hashCode()) + + "]: name is '" + dbName + "'"); + } + sec = getErrorCodes(dbName); + this.dataSourceCache.put(dataSource, sec); + return sec; + } + } + catch (MetaDataAccessException ex) { + logger.warn("Error while extracting database product name - falling back to empty error codes", ex); + } + } + + // Fallback is to return an empty SQLErrorCodes instance. + return new SQLErrorCodes(); + } + + /** + * Associate the specified database name with the given {@link DataSource}. + * @param dataSource the DataSource identifying the database + * @param dbName the corresponding database name as stated in the error codes + * definition file (must not be null) + * @return the corresponding SQLErrorCodes object + */ + public SQLErrorCodes registerDatabase(DataSource dataSource, String dbName) { + synchronized (this.dataSourceCache) { + SQLErrorCodes sec = getErrorCodes(dbName); + this.dataSourceCache.put(dataSource, sec); + return sec; + } + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/SQLExceptionSubclassTranslator.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/SQLExceptionSubclassTranslator.java new file mode 100644 index 0000000000..79c5f96653 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/SQLExceptionSubclassTranslator.java @@ -0,0 +1,104 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.support; + +import java.sql.SQLDataException; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.sql.SQLIntegrityConstraintViolationException; +import java.sql.SQLInvalidAuthorizationSpecException; +import java.sql.SQLNonTransientConnectionException; +import java.sql.SQLNonTransientException; +import java.sql.SQLRecoverableException; +import java.sql.SQLSyntaxErrorException; +import java.sql.SQLTimeoutException; +import java.sql.SQLTransactionRollbackException; +import java.sql.SQLTransientConnectionException; +import java.sql.SQLTransientException; + +import org.springframework.dao.ConcurrencyFailureException; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.dao.PermissionDeniedDataAccessException; +import org.springframework.dao.RecoverableDataAccessException; +import org.springframework.dao.TransientDataAccessResourceException; +import org.springframework.jdbc.BadSqlGrammarException; + +/** + * {@link SQLExceptionTranslator} implementation which analyzes the specific + * {@link java.sql.SQLException} subclass thrown by the JDBC driver. + * + *

This is only available with JDBC 4.0 and later drivers when using Java 6 or later. + * Falls back to a standard {@link SQLStateSQLExceptionTranslator} if the JDBC driver + * does not actually expose JDBC 4 compliant SQLException subclasses. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @since 2.5 + * @see java.sql.SQLTransientException + * @see java.sql.SQLTransientException + * @see java.sql.SQLRecoverableException + */ +public class SQLExceptionSubclassTranslator extends AbstractFallbackSQLExceptionTranslator { + + public SQLExceptionSubclassTranslator() { + setFallbackTranslator(new SQLStateSQLExceptionTranslator()); + } + + protected DataAccessException doTranslate(String task, String sql, SQLException ex) { + if (ex instanceof SQLTransientException) { + if (ex instanceof SQLTransactionRollbackException) { + return new ConcurrencyFailureException(buildMessage(task, sql, ex), ex); + } + if (ex instanceof SQLTransientConnectionException) { + return new TransientDataAccessResourceException(buildMessage(task, sql, ex), ex); + } + if (ex instanceof SQLTimeoutException) { + return new TransientDataAccessResourceException(buildMessage(task, sql, ex), ex); + } + } + else if (ex instanceof SQLNonTransientException) { + if (ex instanceof SQLDataException) { + return new DataIntegrityViolationException(buildMessage(task, sql, ex), ex); + } + else if (ex instanceof SQLFeatureNotSupportedException) { + return new InvalidDataAccessApiUsageException(buildMessage(task, sql, ex), ex); + } + else if (ex instanceof SQLIntegrityConstraintViolationException) { + return new DataIntegrityViolationException(buildMessage(task, sql, ex), ex); + } + else if (ex instanceof SQLInvalidAuthorizationSpecException) { + return new PermissionDeniedDataAccessException(buildMessage(task, sql, ex), ex); + } + else if (ex instanceof SQLNonTransientConnectionException) { + return new DataAccessResourceFailureException(buildMessage(task, sql, ex), ex); + } + else if (ex instanceof SQLSyntaxErrorException) { + return new BadSqlGrammarException(task, sql, ex); + } + } + else if (ex instanceof SQLRecoverableException) { + return new RecoverableDataAccessException(buildMessage(task, sql, ex), ex); + } + + // Fallback to Spring's own SQL state translation... + return null; + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/SQLExceptionTranslator.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/SQLExceptionTranslator.java new file mode 100644 index 0000000000..a4f61535fd --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/SQLExceptionTranslator.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc.support; + +import java.sql.SQLException; + +import org.springframework.dao.DataAccessException; + +/** + * Strategy interface for translating between {@link SQLException SQLExceptions} + * and Spring's data access strategy-agnostic {@link DataAccessException} + * hierarchy. + * + *

Implementations can be generic (for example, using + * {@link java.sql.SQLException#getSQLState() SQLState} codes for JDBC) or wholly + * proprietary (for example, using Oracle error codes) for greater precision. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see org.springframework.dao.DataAccessException + */ +public interface SQLExceptionTranslator { + + /** + * Translate the given {@link SQLException} into a generic {@link DataAccessException}. + *

The returned DataAccessException is supposed to contain the original + * SQLException as root cause. However, client code may not generally + * rely on this due to DataAccessExceptions possibly being caused by other resource + * APIs as well. That said, a getRootCause() instanceof SQLException + * check (and subsequent cast) is considered reliable when expecting JDBC-based + * access to have happened. + * @param task readable text describing the task being attempted + * @param sql SQL query or update that caused the problem (may be null) + * @param ex the offending SQLException + * @return the DataAccessException, wrapping the SQLException + * @see org.springframework.dao.DataAccessException#getRootCause() + */ + DataAccessException translate(String task, String sql, SQLException ex); + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/SQLStateSQLExceptionTranslator.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/SQLStateSQLExceptionTranslator.java new file mode 100644 index 0000000000..a8c57e190e --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/SQLStateSQLExceptionTranslator.java @@ -0,0 +1,134 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.support; + +import java.sql.SQLException; +import java.util.HashSet; +import java.util.Set; + +import org.springframework.dao.ConcurrencyFailureException; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.dao.TransientDataAccessResourceException; +import org.springframework.jdbc.BadSqlGrammarException; + +/** + * {@link SQLExceptionTranslator} implementation that analyzes the SQL state in + * the {@link SQLException} based on the first two digits (the SQL state "class"). + * Detects standard SQL state values and well-known vendor-specific SQL states. + * + *

Not able to diagnose all problems, but is portable between databases and + * does not require special initialization (no database vendor detection, etc.). + * For more precise translation, consider {@link SQLErrorCodeSQLExceptionTranslator}. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Thomas Risberg + * @see java.sql.SQLException#getSQLState() + * @see SQLErrorCodeSQLExceptionTranslator + */ +public class SQLStateSQLExceptionTranslator extends AbstractFallbackSQLExceptionTranslator { + + private static final Set BAD_SQL_GRAMMAR_CODES = new HashSet(8); + + private static final Set DATA_INTEGRITY_VIOLATION_CODES = new HashSet(8); + + private static final Set DATA_ACCESS_RESOURCE_FAILURE_CODES = new HashSet(8); + + private static final Set TRANSIENT_DATA_ACCESS_RESOURCE_CODES = new HashSet(8); + + private static final Set CONCURRENCY_FAILURE_CODES = new HashSet(4); + + + static { + BAD_SQL_GRAMMAR_CODES.add("07"); // Dynamic SQL error + BAD_SQL_GRAMMAR_CODES.add("21"); // Cardinality violation + BAD_SQL_GRAMMAR_CODES.add("2A"); // Syntax error direct SQL + BAD_SQL_GRAMMAR_CODES.add("37"); // Syntax error dynamic SQL + BAD_SQL_GRAMMAR_CODES.add("42"); // General SQL syntax error + BAD_SQL_GRAMMAR_CODES.add("65"); // Oracle: unknown identifier + BAD_SQL_GRAMMAR_CODES.add("S0"); // MySQL uses this - from ODBC error codes? + + DATA_INTEGRITY_VIOLATION_CODES.add("01"); // Data truncation + DATA_INTEGRITY_VIOLATION_CODES.add("02"); // No data found + DATA_INTEGRITY_VIOLATION_CODES.add("22"); // Value out of range + DATA_INTEGRITY_VIOLATION_CODES.add("23"); // Integrity constraint violation + DATA_INTEGRITY_VIOLATION_CODES.add("27"); // Triggered data change violation + DATA_INTEGRITY_VIOLATION_CODES.add("44"); // With check violation + + DATA_ACCESS_RESOURCE_FAILURE_CODES.add("08"); // Connection exception + DATA_ACCESS_RESOURCE_FAILURE_CODES.add("53"); // PostgreSQL: insufficient resources (e.g. disk full) + DATA_ACCESS_RESOURCE_FAILURE_CODES.add("54"); // PostgreSQL: program limit exceeded (e.g. statement too complex) + DATA_ACCESS_RESOURCE_FAILURE_CODES.add("57"); // DB2: out-of-memory exception / database not started + DATA_ACCESS_RESOURCE_FAILURE_CODES.add("58"); // DB2: unexpected system error + + TRANSIENT_DATA_ACCESS_RESOURCE_CODES.add("JW"); // Sybase: internal I/O error + TRANSIENT_DATA_ACCESS_RESOURCE_CODES.add("JZ"); // Sybase: unexpected I/O error + TRANSIENT_DATA_ACCESS_RESOURCE_CODES.add("S1"); // DB2: communication failure + + CONCURRENCY_FAILURE_CODES.add("40"); // Transaction rollback + CONCURRENCY_FAILURE_CODES.add("61"); // Oracle: deadlock + } + + + protected DataAccessException doTranslate(String task, String sql, SQLException ex) { + String sqlState = getSqlState(ex); + if (sqlState != null && sqlState.length() >= 2) { + String classCode = sqlState.substring(0, 2); + if (logger.isDebugEnabled()) { + logger.debug("Extracted SQL state class '" + classCode + "' from value '" + sqlState + "'"); + } + if (BAD_SQL_GRAMMAR_CODES.contains(classCode)) { + return new BadSqlGrammarException(task, sql, ex); + } + else if (DATA_INTEGRITY_VIOLATION_CODES.contains(classCode)) { + return new DataIntegrityViolationException(buildMessage(task, sql, ex), ex); + } + else if (DATA_ACCESS_RESOURCE_FAILURE_CODES.contains(classCode)) { + return new DataAccessResourceFailureException(buildMessage(task, sql, ex), ex); + } + else if (TRANSIENT_DATA_ACCESS_RESOURCE_CODES.contains(classCode)) { + return new TransientDataAccessResourceException(buildMessage(task, sql, ex), ex); + } + else if (CONCURRENCY_FAILURE_CODES.contains(classCode)) { + return new ConcurrencyFailureException(buildMessage(task, sql, ex), ex); + } + } + return null; + } + + /** + * Gets the SQL state code from the supplied {@link SQLException exception}. + *

Some JDBC drivers nest the actual exception from a batched update, so we + * might need to dig down into the nested exception. + * @param ex the exception from which the {@link SQLException#getSQLState() SQL state} + * is to be extracted + * @return the SQL state code + */ + private String getSqlState(SQLException ex) { + String sqlState = ex.getSQLState(); + if (sqlState == null) { + SQLException nestedEx = ex.getNextException(); + if (nestedEx != null) { + sqlState = nestedEx.getSQLState(); + } + } + return sqlState; + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/SqlValue.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/SqlValue.java new file mode 100644 index 0000000000..6397e6761e --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/SqlValue.java @@ -0,0 +1,51 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.support; + +import java.sql.PreparedStatement; +import java.sql.SQLException; + +/** + * Simple interface for complex types to be set as statement parameters. + * + *

Implementations perform the actual work of setting the actual values. They must + * implement the callback method setValue which can throw SQLExceptions + * that will be caught and translated by the calling code. This callback method has + * access to the underlying Connection via the given PreparedStatement object, if that + * should be needed to create any database-specific objects. + * + * @author Juergen Hoeller + * @since 2.5.6 + * @see org.springframework.jdbc.core.SqlTypeValue + * @see org.springframework.jdbc.core.DisposableSqlTypeValue + */ +public interface SqlValue { + + /** + * Set the value on the given PreparedStatement. + * @param ps the PreparedStatement to work on + * @param paramIndex the index of the parameter for which we need to set the value + * @throws SQLException if a SQLException is encountered while setting parameter values + */ + void setValue(PreparedStatement ps, int paramIndex) throws SQLException; + + /** + * Clean up resources held by this value object. + */ + void cleanup(); + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/incrementer/AbstractColumnMaxValueIncrementer.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/incrementer/AbstractColumnMaxValueIncrementer.java new file mode 100644 index 0000000000..069dec794b --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/incrementer/AbstractColumnMaxValueIncrementer.java @@ -0,0 +1,97 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.support.incrementer; + +import javax.sql.DataSource; + +import org.springframework.util.Assert; + +/** + * Abstract base class for {@link DataFieldMaxValueIncrementer} implementations that use + * a column in a custom sequence table. Subclasses need to provide the specific handling + * of that table in their {@link #getNextKey()} implementation.. + * + * @author Juergen Hoeller + * @since 2.5.3 + */ +public abstract class AbstractColumnMaxValueIncrementer extends AbstractDataFieldMaxValueIncrementer { + + /** The name of the column for this sequence */ + private String columnName; + + /** The number of keys buffered in a cache */ + private int cacheSize = 1; + + + /** + * Default constructor for bean property style usage. + * @see #setDataSource + * @see #setIncrementerName + * @see #setColumnName + */ + public AbstractColumnMaxValueIncrementer() { + } + + /** + * Convenience constructor. + * @param dataSource the DataSource to use + * @param incrementerName the name of the sequence/table to use + * @param columnName the name of the column in the sequence table to use + */ + public AbstractColumnMaxValueIncrementer(DataSource dataSource, String incrementerName, String columnName) { + super(dataSource, incrementerName); + Assert.notNull(columnName, "Column name must not be null"); + this.columnName = columnName; + } + + + /** + * Set the name of the column in the sequence table. + */ + public void setColumnName(String columnName) { + this.columnName = columnName; + } + + /** + * Return the name of the column in the sequence table. + */ + public String getColumnName() { + return this.columnName; + } + + /** + * Set the number of buffered keys. + */ + public void setCacheSize(int cacheSize) { + this.cacheSize = cacheSize; + } + + /** + * Return the number of buffered keys. + */ + public int getCacheSize() { + return this.cacheSize; + } + + public void afterPropertiesSet() { + super.afterPropertiesSet(); + if (this.columnName == null) { + throw new IllegalArgumentException("Property 'columnName' is required"); + } + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/incrementer/AbstractDataFieldMaxValueIncrementer.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/incrementer/AbstractDataFieldMaxValueIncrementer.java new file mode 100644 index 0000000000..6e3a109050 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/incrementer/AbstractDataFieldMaxValueIncrementer.java @@ -0,0 +1,150 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.support.incrementer; + +import javax.sql.DataSource; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.dao.DataAccessException; +import org.springframework.util.Assert; + +/** + * Base implementation of {@link DataFieldMaxValueIncrementer} that delegates + * to a single {@link #getNextKey} template method that returns a long. + * Uses longs for String values, padding with zeroes if required. + * + * @author Dmitriy Kopylenko + * @author Juergen Hoeller + * @author Jean-Pierre Pawlak + * @author Juergen Hoeller + */ +public abstract class AbstractDataFieldMaxValueIncrementer implements DataFieldMaxValueIncrementer, InitializingBean { + + private DataSource dataSource; + + /** The name of the sequence/table containing the sequence */ + private String incrementerName; + + /** The length to which a string result should be pre-pended with zeroes */ + protected int paddingLength = 0; + + + /** + * Default constructor for bean property style usage. + * @see #setDataSource + * @see #setIncrementerName + */ + public AbstractDataFieldMaxValueIncrementer() { + } + + /** + * Convenience constructor. + * @param dataSource the DataSource to use + * @param incrementerName the name of the sequence/table to use + */ + public AbstractDataFieldMaxValueIncrementer(DataSource dataSource, String incrementerName) { + Assert.notNull(dataSource, "DataSource must not be null"); + Assert.notNull(incrementerName, "Incrementer name must not be null"); + this.dataSource = dataSource; + this.incrementerName = incrementerName; + } + + + /** + * Set the data source to retrieve the value from. + */ + public void setDataSource(DataSource dataSource) { + this.dataSource = dataSource; + } + + /** + * Return the data source to retrieve the value from. + */ + public DataSource getDataSource() { + return this.dataSource; + } + + /** + * Set the name of the sequence/table. + */ + public void setIncrementerName(String incrementerName) { + this.incrementerName = incrementerName; + } + + /** + * Return the name of the sequence/table. + */ + public String getIncrementerName() { + return this.incrementerName; + } + + /** + * Set the padding length, i.e. the length to which a string result + * should be pre-pended with zeroes. + */ + public void setPaddingLength(int paddingLength) { + this.paddingLength = paddingLength; + } + + /** + * Return the padding length for String values. + */ + public int getPaddingLength() { + return this.paddingLength; + } + + public void afterPropertiesSet() { + if (this.dataSource == null) { + throw new IllegalArgumentException("Property 'dataSource' is required"); + } + if (this.incrementerName == null) { + throw new IllegalArgumentException("Property 'incrementerName' is required"); + } + } + + + public int nextIntValue() throws DataAccessException { + return (int) getNextKey(); + } + + public long nextLongValue() throws DataAccessException { + return getNextKey(); + } + + public String nextStringValue() throws DataAccessException { + String s = Long.toString(getNextKey()); + int len = s.length(); + if (len < this.paddingLength) { + StringBuffer buf = new StringBuffer(this.paddingLength); + for (int i = 0; i < this.paddingLength - len; i++) { + buf.append('0'); + } + buf.append(s); + s = buf.toString(); + } + return s; + } + + + /** + * Determine the next key to use, as a long. + * @return the key to use as a long. It will eventually be converted later + * in another format by the public concrete methods of this class. + */ + protected abstract long getNextKey(); + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/incrementer/AbstractSequenceMaxValueIncrementer.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/incrementer/AbstractSequenceMaxValueIncrementer.java new file mode 100644 index 0000000000..0d4ec6c691 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/incrementer/AbstractSequenceMaxValueIncrementer.java @@ -0,0 +1,94 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.support.incrementer; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import javax.sql.DataSource; + +import org.springframework.dao.DataAccessException; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.jdbc.datasource.DataSourceUtils; +import org.springframework.jdbc.support.JdbcUtils; + +/** + * Abstract base class for {@link DataFieldMaxValueIncrementer} implementations that use + * a database sequence. Subclasses need to provide the database-specific SQL to use. + * + * @author Juergen Hoeller + * @since 26.02.2004 + * @see #getSequenceQuery + */ +public abstract class AbstractSequenceMaxValueIncrementer extends AbstractDataFieldMaxValueIncrementer { + + /** + * Default constructor for bean property style usage. + * @see #setDataSource + * @see #setIncrementerName + */ + public AbstractSequenceMaxValueIncrementer() { + } + + /** + * Convenience constructor. + * @param dataSource the DataSource to use + * @param incrementerName the name of the sequence/table to use + */ + public AbstractSequenceMaxValueIncrementer(DataSource dataSource, String incrementerName) { + super(dataSource, incrementerName); + } + + + /** + * Executes the SQL as specified by {@link #getSequenceQuery()}. + */ + protected long getNextKey() throws DataAccessException { + Connection con = DataSourceUtils.getConnection(getDataSource()); + Statement stmt = null; + ResultSet rs = null; + try { + stmt = con.createStatement(); + DataSourceUtils.applyTransactionTimeout(stmt, getDataSource()); + rs = stmt.executeQuery(getSequenceQuery()); + if (rs.next()) { + return rs.getLong(1); + } + else { + throw new DataAccessResourceFailureException("Sequence query did not return a result"); + } + } + catch (SQLException ex) { + throw new DataAccessResourceFailureException("Could not obtain sequence value", ex); + } + finally { + JdbcUtils.closeResultSet(rs); + JdbcUtils.closeStatement(stmt); + DataSourceUtils.releaseConnection(con, getDataSource()); + } + } + + /** + * Return the database-specific query to use for retrieving a sequence value. + *

The provided SQL is supposed to result in a single row with a single + * column that allows for extracting a long value. + */ + protected abstract String getSequenceQuery(); + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/incrementer/DB2MainframeSequenceMaxValueIncrementer.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/incrementer/DB2MainframeSequenceMaxValueIncrementer.java new file mode 100644 index 0000000000..2d9bb84323 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/incrementer/DB2MainframeSequenceMaxValueIncrementer.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.support.incrementer; + +import javax.sql.DataSource; + +/** + * {@link DataFieldMaxValueIncrementer} that retrieves the next value of a given sequence + * on DB2/390 or DB2/400. Thanks to Jens Eickmeyer for the suggestion! + * + * @author Juergen Hoeller + * @since 2.5.3 + * @see DB2SequenceMaxValueIncrementer + */ +public class DB2MainframeSequenceMaxValueIncrementer extends AbstractSequenceMaxValueIncrementer { + + /** + * Default constructor for bean property style usage. + * @see #setDataSource + * @see #setIncrementerName + */ + public DB2MainframeSequenceMaxValueIncrementer() { + } + + /** + * Convenience constructor. + * @param dataSource the DataSource to use + * @param incrementerName the name of the sequence/table to use + */ + public DB2MainframeSequenceMaxValueIncrementer(DataSource dataSource, String incrementerName) { + super(dataSource, incrementerName); + } + + + protected String getSequenceQuery() { + return "select next value for " + getIncrementerName() + " from sysibm.sysdummy1"; + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/incrementer/DB2SequenceMaxValueIncrementer.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/incrementer/DB2SequenceMaxValueIncrementer.java new file mode 100644 index 0000000000..d08526648b --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/incrementer/DB2SequenceMaxValueIncrementer.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.support.incrementer; + +import javax.sql.DataSource; + +/** + * {@link DataFieldMaxValueIncrementer} that retrieves the next value of a given sequence + * on DB2 UDB (for Unix and Windows). Thanks to Mark MacMahon for the suggestion! + * + * @author Juergen Hoeller + * @since 1.1.3 + * @see DB2MainframeSequenceMaxValueIncrementer + */ +public class DB2SequenceMaxValueIncrementer extends AbstractSequenceMaxValueIncrementer { + + /** + * Default constructor for bean property style usage. + * @see #setDataSource + * @see #setIncrementerName + */ + public DB2SequenceMaxValueIncrementer() { + } + + /** + * Convenience constructor. + * @param dataSource the DataSource to use + * @param incrementerName the name of the sequence/table to use + */ + public DB2SequenceMaxValueIncrementer(DataSource dataSource, String incrementerName) { + super(dataSource, incrementerName); + } + + + protected String getSequenceQuery() { + return "values nextval for " + getIncrementerName(); + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/incrementer/DataFieldMaxValueIncrementer.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/incrementer/DataFieldMaxValueIncrementer.java new file mode 100644 index 0000000000..825815c887 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/incrementer/DataFieldMaxValueIncrementer.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc.support.incrementer; + +import org.springframework.dao.DataAccessException; + +/** + * Interface that defines contract of incrementing any data store field's + * maximum value. Works much like a sequence number generator. + * + *

Typical implementations may use standard SQL, native RDBMS sequences + * or Stored Procedures to do the job. + * + * @author Dmitriy Kopylenko + * @author Jean-Pierre Pawlak + * @author Juergen Hoeller + */ +public interface DataFieldMaxValueIncrementer { + + /** + * Increment the data store field's max value as int. + * @return int next data store value such as max + 1 + * @throws org.springframework.dao.DataAccessException in case of errors + */ + int nextIntValue() throws DataAccessException; + + /** + * Increment the data store field's max value as long. + * @return int next data store value such as max + 1 + * @throws org.springframework.dao.DataAccessException in case of errors + */ + long nextLongValue() throws DataAccessException; + + /** + * Increment the data store field's max value as String. + * @return next data store value such as max + 1 + * @throws org.springframework.dao.DataAccessException in case of errors + */ + String nextStringValue() throws DataAccessException; + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/incrementer/DerbyMaxValueIncrementer.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/incrementer/DerbyMaxValueIncrementer.java new file mode 100644 index 0000000000..1577255c14 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/incrementer/DerbyMaxValueIncrementer.java @@ -0,0 +1,170 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.support.incrementer; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import javax.sql.DataSource; + +import org.springframework.dao.DataAccessException; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.jdbc.datasource.DataSourceUtils; +import org.springframework.jdbc.support.JdbcUtils; + +/** + * {@link DataFieldMaxValueIncrementer} that increments the maximum value of a given Derby table + * with the equivalent of an auto-increment column. Note: If you use this class, your Derby key + * column should NOT be defined as an IDENTITY column, as the sequence table does the job. + * + *

The sequence is kept in a table. There should be one sequence table per + * table that needs an auto-generated key. + * + *

Derby requires an additional column to be used for the insert since it is impossible + * to insert a null into the identity column and have the value generated. This is solved by + * providing the name of a dummy column that also must be created in the sequence table. + * + *

Example: + * + *

create table tab (id int not null primary key, text varchar(100));
+ * create table tab_sequence (value int generated always as identity, dummy char(1));
+ * insert into tab_sequence (dummy) values(null);
+ * + * If "cacheSize" is set, the intermediate values are served without querying the + * database. If the server or your application is stopped or crashes or a transaction + * is rolled back, the unused values will never be served. The maximum hole size in + * numbering is consequently the value of cacheSize. + * + * HINT: Since Derby supports the JDBC 3.0 getGeneratedKeys method, + * it is recommended to use IDENTITY columns directly in the tables and then utilizing + * a {@link org.springframework.jdbc.support.KeyHolder} when calling the with the + * update(PreparedStatementCreator psc, KeyHolder generatedKeyHolder) + * method of the {@link org.springframework.jdbc.core.JdbcTemplate}. + * + *

Thanks to Endre Stolsvik for the suggestion! + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @since 2.5 + */ +public class DerbyMaxValueIncrementer extends AbstractColumnMaxValueIncrementer { + + /** The default for dummy name */ + private static final String DEFAULT_DUMMY_NAME = "dummy"; + + /** The name of the dummy column used for inserts */ + private String dummyName = DEFAULT_DUMMY_NAME; + + /** The current cache of values */ + private long[] valueCache; + + /** The next id to serve from the value cache */ + private int nextValueIndex = -1; + + + /** + * Default constructor for bean property style usage. + * @see #setDataSource + * @see #setIncrementerName + * @see #setColumnName + */ + public DerbyMaxValueIncrementer() { + } + + /** + * Convenience constructor. + * @param dataSource the DataSource to use + * @param incrementerName the name of the sequence/table to use + * @param columnName the name of the column in the sequence table to use + */ + public DerbyMaxValueIncrementer(DataSource dataSource, String incrementerName, String columnName) { + super(dataSource, incrementerName, columnName); + this.dummyName = DEFAULT_DUMMY_NAME; + } + + /** + * Convenience constructor. + * @param dataSource the DataSource to use + * @param incrementerName the name of the sequence/table to use + * @param columnName the name of the column in the sequence table to use + * @param dummyName the name of the dummy column used for inserts + */ + public DerbyMaxValueIncrementer(DataSource dataSource, String incrementerName, String columnName, String dummyName) { + super(dataSource, incrementerName, columnName); + this.dummyName = dummyName; + } + + + /** + * Set the name of the dummy column. + */ + public void setDummyName(String dummyName) { + this.dummyName = dummyName; + } + + /** + * Return the name of the dummy column. + */ + public String getDummyName() { + return this.dummyName; + } + + + protected synchronized long getNextKey() throws DataAccessException { + if (this.nextValueIndex < 0 || this.nextValueIndex >= getCacheSize()) { + /* + * Need to use straight JDBC code because we need to make sure that the insert and select + * are performed on the same connection (otherwise we can't be sure that last_insert_id() + * returned the correct value) + */ + Connection con = DataSourceUtils.getConnection(getDataSource()); + Statement stmt = null; + try { + stmt = con.createStatement(); + DataSourceUtils.applyTransactionTimeout(stmt, getDataSource()); + this.valueCache = new long[getCacheSize()]; + this.nextValueIndex = 0; + for (int i = 0; i < getCacheSize(); i++) { + stmt.executeUpdate("insert into " + getIncrementerName() + " (" + getDummyName() + ") values(null)"); + ResultSet rs = stmt.executeQuery("select IDENTITY_VAL_LOCAL() from " + getIncrementerName()); + try { + if (!rs.next()) { + throw new DataAccessResourceFailureException("IDENTITY_VAL_LOCAL() failed after executing an update"); + } + this.valueCache[i] = rs.getLong(1); + } + finally { + JdbcUtils.closeResultSet(rs); + } + } + long maxValue = this.valueCache[(this.valueCache.length - 1)]; + stmt.executeUpdate("delete from " + getIncrementerName() + " where " + getColumnName() + " < " + maxValue); + } + catch (SQLException ex) { + throw new DataAccessResourceFailureException("Could not obtain IDENTITY value", ex); + } + finally { + JdbcUtils.closeStatement(stmt); + DataSourceUtils.releaseConnection(con, getDataSource()); + } + } + return this.valueCache[this.nextValueIndex++]; + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/incrementer/H2SequenceMaxValueIncrementer.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/incrementer/H2SequenceMaxValueIncrementer.java new file mode 100644 index 0000000000..271bc64447 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/incrementer/H2SequenceMaxValueIncrementer.java @@ -0,0 +1,51 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.support.incrementer; + +import javax.sql.DataSource; + +/** + * {@link DataFieldMaxValueIncrementer} that retrieves the next value of a given H2 Database sequence. + * + * @author Thomas Risberg + * @since 2.5 + */ +public class H2SequenceMaxValueIncrementer extends AbstractSequenceMaxValueIncrementer { + + /** + * Default constructor for bean property style usage. + * @see #setDataSource + * @see #setIncrementerName + */ + public H2SequenceMaxValueIncrementer() { + } + + /** + * Convenience constructor. + * @param dataSource the DataSource to use + * @param incrementerName the name of the sequence/table to use + */ + public H2SequenceMaxValueIncrementer(DataSource dataSource, String incrementerName) { + super(dataSource, incrementerName); + } + + + protected String getSequenceQuery() { + return "select " + getIncrementerName() + ".nextval from dual"; + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/incrementer/HsqlMaxValueIncrementer.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/incrementer/HsqlMaxValueIncrementer.java new file mode 100644 index 0000000000..3d1d5745ef --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/incrementer/HsqlMaxValueIncrementer.java @@ -0,0 +1,128 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.support.incrementer; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import javax.sql.DataSource; + +import org.springframework.dao.DataAccessException; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.jdbc.datasource.DataSourceUtils; +import org.springframework.jdbc.support.JdbcUtils; + +/** + * {@link DataFieldMaxValueIncrementer} that increments the maximum value of a given HSQL table + * with the equivalent of an auto-increment column. Note: If you use this class, your HSQL + * key column should NOT be auto-increment, as the sequence table does the job. + * + *

The sequence is kept in a table. There should be one sequence table per + * table that needs an auto-generated key. + * + *

Example: + * + *

create table tab (id int not null primary key, text varchar(100));
+ * create table tab_sequence (value identity);
+ * insert into tab_sequence values(0);
+ * + * If "cacheSize" is set, the intermediate values are served without querying the + * database. If the server or your application is stopped or crashes or a transaction + * is rolled back, the unused values will never be served. The maximum hole size in + * numbering is consequently the value of cacheSize. + * + *

NOTE: HSQL now supports sequences and you should consider using them instead: + * {@link HsqlSequenceMaxValueIncrementer} + * + * @author Jean-Pierre Pawlak + * @author Thomas Risberg + * @author Juergen Hoeller + * @see HsqlSequenceMaxValueIncrementer + */ +public class HsqlMaxValueIncrementer extends AbstractColumnMaxValueIncrementer { + + /** The current cache of values */ + private long[] valueCache; + + /** The next id to serve from the value cache */ + private int nextValueIndex = -1; + + + /** + * Default constructor for bean property style usage. + * @see #setDataSource + * @see #setIncrementerName + * @see #setColumnName + */ + public HsqlMaxValueIncrementer() { + } + + /** + * Convenience constructor. + * @param dataSource the DataSource to use + * @param incrementerName the name of the sequence/table to use + * @param columnName the name of the column in the sequence table to use + */ + public HsqlMaxValueIncrementer(DataSource dataSource, String incrementerName, String columnName) { + super(dataSource, incrementerName, columnName); + } + + + protected synchronized long getNextKey() throws DataAccessException { + if (this.nextValueIndex < 0 || this.nextValueIndex >= getCacheSize()) { + /* + * Need to use straight JDBC code because we need to make sure that the insert and select + * are performed on the same Connection. Otherwise we can't be sure that last_insert_id() + * returned the correct value. + */ + Connection con = DataSourceUtils.getConnection(getDataSource()); + Statement stmt = null; + try { + stmt = con.createStatement(); + DataSourceUtils.applyTransactionTimeout(stmt, getDataSource()); + this.valueCache = new long[getCacheSize()]; + this.nextValueIndex = 0; + for (int i = 0; i < getCacheSize(); i++) { + stmt.executeUpdate("insert into " + getIncrementerName() + " values(null)"); + ResultSet rs = stmt.executeQuery("select max(identity()) from " + getIncrementerName()); + try { + if (!rs.next()) { + throw new DataAccessResourceFailureException("identity() failed after executing an update"); + } + this.valueCache[i] = rs.getLong(1); + } + finally { + JdbcUtils.closeResultSet(rs); + } + } + long maxValue = this.valueCache[(this.valueCache.length - 1)]; + stmt.executeUpdate("delete from " + getIncrementerName() + " where " + getColumnName() + " < " + maxValue); + } + catch (SQLException ex) { + throw new DataAccessResourceFailureException("Could not obtain identity()", ex); + } + finally { + JdbcUtils.closeStatement(stmt); + DataSourceUtils.releaseConnection(con, getDataSource()); + } + } + return this.valueCache[this.nextValueIndex++]; + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/incrementer/HsqlSequenceMaxValueIncrementer.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/incrementer/HsqlSequenceMaxValueIncrementer.java new file mode 100644 index 0000000000..28a56e2ddc --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/incrementer/HsqlSequenceMaxValueIncrementer.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.support.incrementer; + +import javax.sql.DataSource; + +/** + * {@link DataFieldMaxValueIncrementer} that retrieves the next value of a given HSQL sequence. + * Thanks to Guillaume Bilodeau for the suggestion! + * + *

NOTE: This is an alternative to using a regular table to support generating + * unique keys that was necessary in previous versions of HSQL. + * + * @author Thomas Risberg + * @since 2.5 + * @see HsqlMaxValueIncrementer + */ +public class HsqlSequenceMaxValueIncrementer extends AbstractSequenceMaxValueIncrementer { + + /** + * Default constructor for bean property style usage. + * @see #setDataSource + * @see #setIncrementerName + */ + public HsqlSequenceMaxValueIncrementer() { + } + + /** + * Convenience constructor. + * @param dataSource the DataSource to use + * @param incrementerName the name of the sequence/table to use + */ + public HsqlSequenceMaxValueIncrementer(DataSource dataSource, String incrementerName) { + super(dataSource, incrementerName); + } + + + protected String getSequenceQuery() { + return "call next value for " + getIncrementerName(); + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/incrementer/MySQLMaxValueIncrementer.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/incrementer/MySQLMaxValueIncrementer.java new file mode 100644 index 0000000000..ac4c2fb4d1 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/incrementer/MySQLMaxValueIncrementer.java @@ -0,0 +1,131 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.support.incrementer; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import javax.sql.DataSource; + +import org.springframework.dao.DataAccessException; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.jdbc.datasource.DataSourceUtils; +import org.springframework.jdbc.support.JdbcUtils; + +/** + * {@link DataFieldMaxValueIncrementer} that increments the maximum value of a given MySQL table + * with the equivalent of an auto-increment column. Note: If you use this class, your MySQL + * key column should NOT be auto-increment, as the sequence table does the job. + * + *

The sequence is kept in a table; there should be one sequence table per + * table that needs an auto-generated key. The table type of the sequence table + * should be MyISAM so the sequences are allocated without regard to any + * transactions that might be in progress. + * + *

Example: + * + *

create table tab (id int unsigned not null primary key, text varchar(100));
+ * create table tab_sequence (value int not null) type=MYISAM;
+ * insert into tab_sequence values(0);
+ * + * If "cacheSize" is set, the intermediate values are served without querying the + * database. If the server or your application is stopped or crashes or a transaction + * is rolled back, the unused values will never be served. The maximum hole size in + * numbering is consequently the value of cacheSize. + * + * @author Jean-Pierre Pawlak + * @author Thomas Risberg + * @author Juergen Hoeller + */ +public class MySQLMaxValueIncrementer extends AbstractColumnMaxValueIncrementer { + + /** The SQL string for retrieving the new sequence value */ + private static final String VALUE_SQL = "select last_insert_id()"; + + /** The next id to serve */ + private long nextId = 0; + + /** The max id to serve */ + private long maxId = 0; + + + /** + * Default constructor for bean property style usage. + * @see #setDataSource + * @see #setIncrementerName + * @see #setColumnName + */ + public MySQLMaxValueIncrementer() { + } + + /** + * Convenience constructor. + * @param dataSource the DataSource to use + * @param incrementerName the name of the sequence/table to use + * @param columnName the name of the column in the sequence table to use + */ + public MySQLMaxValueIncrementer(DataSource dataSource, String incrementerName, String columnName) { + super(dataSource, incrementerName, columnName); + } + + + protected synchronized long getNextKey() throws DataAccessException { + if (this.maxId == this.nextId) { + /* + * Need to use straight JDBC code because we need to make sure that the insert and select + * are performed on the same connection (otherwise we can't be sure that last_insert_id() + * returned the correct value) + */ + Connection con = DataSourceUtils.getConnection(getDataSource()); + Statement stmt = null; + try { + stmt = con.createStatement(); + DataSourceUtils.applyTransactionTimeout(stmt, getDataSource()); + // Increment the sequence column... + String columnName = getColumnName(); + stmt.executeUpdate("update "+ getIncrementerName() + " set " + columnName + + " = last_insert_id(" + columnName + " + " + getCacheSize() + ")"); + // Retrieve the new max of the sequence column... + ResultSet rs = stmt.executeQuery(VALUE_SQL); + try { + if (!rs.next()) { + throw new DataAccessResourceFailureException("last_insert_id() failed after executing an update"); + } + this.maxId = rs.getLong(1); + } + finally { + JdbcUtils.closeResultSet(rs); + } + this.nextId = this.maxId - getCacheSize() + 1; + } + catch (SQLException ex) { + throw new DataAccessResourceFailureException("Could not obtain last_insert_id()", ex); + } + finally { + JdbcUtils.closeStatement(stmt); + DataSourceUtils.releaseConnection(con, getDataSource()); + } + } + else { + this.nextId++; + } + return this.nextId; + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/incrementer/OracleSequenceMaxValueIncrementer.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/incrementer/OracleSequenceMaxValueIncrementer.java new file mode 100644 index 0000000000..4760c03aff --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/incrementer/OracleSequenceMaxValueIncrementer.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.support.incrementer; + +import javax.sql.DataSource; + +/** + * {@link DataFieldMaxValueIncrementer} that retrieves the next value of a given Oracle sequence. + * + * @author Dmitriy Kopylenko + * @author Thomas Risberg + * @author Juergen Hoeller + */ +public class OracleSequenceMaxValueIncrementer extends AbstractSequenceMaxValueIncrementer { + + /** + * Default constructor for bean property style usage. + * @see #setDataSource + * @see #setIncrementerName + */ + public OracleSequenceMaxValueIncrementer() { + } + + /** + * Convenience constructor. + * @param dataSource the DataSource to use + * @param incrementerName the name of the sequence/table to use + */ + public OracleSequenceMaxValueIncrementer(DataSource dataSource, String incrementerName) { + super(dataSource, incrementerName); + } + + + protected String getSequenceQuery() { + return "select " + getIncrementerName() + ".nextval from dual"; + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/incrementer/PostgreSQLSequenceMaxValueIncrementer.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/incrementer/PostgreSQLSequenceMaxValueIncrementer.java new file mode 100644 index 0000000000..05f021539b --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/incrementer/PostgreSQLSequenceMaxValueIncrementer.java @@ -0,0 +1,51 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.support.incrementer; + +import javax.sql.DataSource; + +/** + * {@link DataFieldMaxValueIncrementer} that retrieves the next value of a given PostgreSQL sequence. + * Thanks to Tomislav Urban for the suggestion! + * + * @author Juergen Hoeller + */ +public class PostgreSQLSequenceMaxValueIncrementer extends AbstractSequenceMaxValueIncrementer { + + /** + * Default constructor for bean property style usage. + * @see #setDataSource + * @see #setIncrementerName + */ + public PostgreSQLSequenceMaxValueIncrementer() { + } + + /** + * Convenience constructor. + * @param dataSource the DataSource to use + * @param incrementerName the name of the sequence/table to use + */ + public PostgreSQLSequenceMaxValueIncrementer(DataSource dataSource, String incrementerName) { + super(dataSource, incrementerName); + } + + + protected String getSequenceQuery() { + return "select nextval('" + getIncrementerName() + "')"; + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/incrementer/SqlServerMaxValueIncrementer.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/incrementer/SqlServerMaxValueIncrementer.java new file mode 100644 index 0000000000..f24030a6ff --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/incrementer/SqlServerMaxValueIncrementer.java @@ -0,0 +1,117 @@ +package org.springframework.jdbc.support.incrementer; + +import org.springframework.dao.DataAccessException; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.jdbc.datasource.DataSourceUtils; +import org.springframework.jdbc.support.JdbcUtils; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.Statement; +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + * {@link DataFieldMaxValueIncrementer} that increments the maximum value of a given SQL Server table + * with the equivalent of an auto-increment column. Note: If you use this class, your Derby key + * column should NOT be defined as an IDENTITY column, as the sequence table does the job. + * + *

This class is inteded to be used with Microsoft SQL Server. + * + *

The sequence is kept in a table. There should be one sequence table per + * table that needs an auto-generated key. + * + *

Example: + * + *

 create table tab (id int not null primary key, text varchar(100))
+ * create table tab_sequence (id bigint identity)
+ * insert into tab_sequence default values
+ * + * If "cacheSize" is set, the intermediate values are served without querying the + * database. If the server or your application is stopped or crashes or a transaction + * is rolled back, the unused values will never be served. The maximum hole size in + * numbering is consequently the value of cacheSize. + * + * HINT: Since Microsoft SQL Server supports the JDBC 3.0 getGeneratedKeys method, + * it is recommended to use IDENTITY columns directly in the tables and then using a + * {@link org.springframework.jdbc.core.simple.SimpleJdbcInsert} or utilizing + * a {@link org.springframework.jdbc.support.KeyHolder} when calling the with the + * update(PreparedStatementCreator psc, KeyHolder generatedKeyHolder) + * method of the {@link org.springframework.jdbc.core.JdbcTemplate}. + * + *

Thanks to Preben Nilsson for the suggestion! + * + * @author Thomas Risberg + * @since 2.5.5 + */ +public class SqlServerMaxValueIncrementer extends AbstractColumnMaxValueIncrementer { + + /** The current cache of values */ + private long[] valueCache; + + /** The next id to serve from the value cache */ + private int nextValueIndex = -1; + + + /** + * Default constructor for bean property style usage. + * @see #setDataSource + * @see #setIncrementerName + * @see #setColumnName + */ + public SqlServerMaxValueIncrementer() { + } + + /** + * Convenience constructor. + * @param dataSource the DataSource to use + * @param incrementerName the name of the sequence/table to use + * @param columnName the name of the column in the sequence table to use + */ + public SqlServerMaxValueIncrementer(DataSource dataSource, String incrementerName, String columnName) { + super(dataSource, incrementerName, columnName); + } + + + protected synchronized long getNextKey() throws DataAccessException { + if (this.nextValueIndex < 0 || this.nextValueIndex >= getCacheSize()) { + /* + * Need to use straight JDBC code because we need to make sure that the insert and select + * are performed on the same connection (otherwise we can't be sure that @@identity + * returnes the correct value) + */ + Connection con = DataSourceUtils.getConnection(getDataSource()); + Statement stmt = null; + try { + stmt = con.createStatement(); + DataSourceUtils.applyTransactionTimeout(stmt, getDataSource()); + this.valueCache = new long[getCacheSize()]; + this.nextValueIndex = 0; + for (int i = 0; i < getCacheSize(); i++) { + stmt.executeUpdate("insert into " + getIncrementerName() + " default values"); + ResultSet rs = stmt.executeQuery("select @@identity"); + try { + if (!rs.next()) { + throw new DataAccessResourceFailureException("@@identity failed after executing an update"); + } + this.valueCache[i] = rs.getLong(1); + } + finally { + JdbcUtils.closeResultSet(rs); + } + } + long maxValue = this.valueCache[(this.valueCache.length - 1)]; + stmt.executeUpdate("delete from " + getIncrementerName() + " where " + getColumnName() + " < " + maxValue); + } + catch (SQLException ex) { + throw new DataAccessResourceFailureException("Could not increment identity", ex); + } + finally { + JdbcUtils.closeStatement(stmt); + DataSourceUtils.releaseConnection(con, getDataSource()); + } + } + return this.valueCache[this.nextValueIndex++]; + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/incrementer/SybaseMaxValueIncrementer.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/incrementer/SybaseMaxValueIncrementer.java new file mode 100644 index 0000000000..a19ec82df4 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/incrementer/SybaseMaxValueIncrementer.java @@ -0,0 +1,117 @@ +package org.springframework.jdbc.support.incrementer; + +import org.springframework.dao.DataAccessException; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.jdbc.datasource.DataSourceUtils; +import org.springframework.jdbc.support.JdbcUtils; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.Statement; +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + * {@link org.springframework.jdbc.support.incrementer.DataFieldMaxValueIncrementer} that increments the maximum value of a given SQL Server table + * with the equivalent of an auto-increment column. Note: If you use this class, your Derby key + * column should NOT be defined as an IDENTITY column, as the sequence table does the job. + * + *

This class is inteded to be used with Sybase Adaptive Server. + * + *

The sequence is kept in a table. There should be one sequence table per + * table that needs an auto-generated key. + * + *

Example: + * + *

 create table tab (id int not null primary key, text varchar(100))
+ * create table tab_sequence (id bigint identity)
+ * insert into tab_sequence values()
+ * + * If "cacheSize" is set, the intermediate values are served without querying the + * database. If the server or your application is stopped or crashes or a transaction + * is rolled back, the unused values will never be served. The maximum hole size in + * numbering is consequently the value of cacheSize. + * + * HINT: Since Sybase supports the JDBC 3.0 getGeneratedKeys method, + * it is recommended to use IDENTITY columns directly in the tables and then using a + * {@link org.springframework.jdbc.core.simple.SimpleJdbcInsert} or utilizing + * a {@link org.springframework.jdbc.support.KeyHolder} when calling the with the + * update(PreparedStatementCreator psc, KeyHolder generatedKeyHolder) + * method of the {@link org.springframework.jdbc.core.JdbcTemplate}. + * + *

Thanks to Yinwei Liu for the suggestion! + * + * @author Thomas Risberg + * @since 2.5.5 + */ +public class SybaseMaxValueIncrementer extends AbstractColumnMaxValueIncrementer { + + /** The current cache of values */ + private long[] valueCache; + + /** The next id to serve from the value cache */ + private int nextValueIndex = -1; + + + /** + * Default constructor for bean property style usage. + * @see #setDataSource + * @see #setIncrementerName + * @see #setColumnName + */ + public SybaseMaxValueIncrementer() { + } + + /** + * Convenience constructor. + * @param dataSource the DataSource to use + * @param incrementerName the name of the sequence/table to use + * @param columnName the name of the column in the sequence table to use + */ + public SybaseMaxValueIncrementer(DataSource dataSource, String incrementerName, String columnName) { + super(dataSource, incrementerName, columnName); + } + + + protected synchronized long getNextKey() throws DataAccessException { + if (this.nextValueIndex < 0 || this.nextValueIndex >= getCacheSize()) { + /* + * Need to use straight JDBC code because we need to make sure that the insert and select + * are performed on the same connection (otherwise we can't be sure that @@identity + * returnes the correct value) + */ + Connection con = DataSourceUtils.getConnection(getDataSource()); + Statement stmt = null; + try { + stmt = con.createStatement(); + DataSourceUtils.applyTransactionTimeout(stmt, getDataSource()); + this.valueCache = new long[getCacheSize()]; + this.nextValueIndex = 0; + for (int i = 0; i < getCacheSize(); i++) { + stmt.executeUpdate("insert into " + getIncrementerName() + " values()"); + ResultSet rs = stmt.executeQuery("select @@identity"); + try { + if (!rs.next()) { + throw new DataAccessResourceFailureException("@@identity failed after executing an update"); + } + this.valueCache[i] = rs.getLong(1); + } + finally { + JdbcUtils.closeResultSet(rs); + } + } + long maxValue = this.valueCache[(this.valueCache.length - 1)]; + stmt.executeUpdate("delete from " + getIncrementerName() + " where " + getColumnName() + " < " + maxValue); + } + catch (SQLException ex) { + throw new DataAccessResourceFailureException("Could not increment identity", ex); + } + finally { + JdbcUtils.closeStatement(stmt); + DataSourceUtils.releaseConnection(con, getDataSource()); + } + } + return this.valueCache[this.nextValueIndex++]; + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/incrementer/package.html b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/incrementer/package.html new file mode 100644 index 0000000000..16a67b197e --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/incrementer/package.html @@ -0,0 +1,10 @@ + + + +Provides a support framework for incrementing database table values +via sequences, with implementations for various databases. + +

Can be used independently, for example in custom JDBC access code. + + + diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/lob/AbstractLobHandler.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/lob/AbstractLobHandler.java new file mode 100644 index 0000000000..737a582459 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/lob/AbstractLobHandler.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2005 the original author or authors. + * + * Licensed 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.springframework.jdbc.support.lob; + +import java.io.InputStream; +import java.io.Reader; +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + * Abstract base class for LobHandler implementations. + * + *

Implements all accessor methods for column names through a column lookup + * and delegating to the corresponding accessor that takes a column index. + * + * @author Juergen Hoeller + * @since 1.2 + * @see java.sql.ResultSet#findColumn + */ +public abstract class AbstractLobHandler implements LobHandler { + + public byte[] getBlobAsBytes(ResultSet rs, String columnName) throws SQLException { + return getBlobAsBytes(rs, rs.findColumn(columnName)); + } + + public InputStream getBlobAsBinaryStream(ResultSet rs, String columnName) throws SQLException { + return getBlobAsBinaryStream(rs, rs.findColumn(columnName)); + } + + public String getClobAsString(ResultSet rs, String columnName) throws SQLException { + return getClobAsString(rs, rs.findColumn(columnName)); + } + + public InputStream getClobAsAsciiStream(ResultSet rs, String columnName) throws SQLException { + return getClobAsAsciiStream(rs, rs.findColumn(columnName)); + } + + public Reader getClobAsCharacterStream(ResultSet rs, String columnName) throws SQLException { + return getClobAsCharacterStream(rs, rs.findColumn(columnName)); + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/lob/DefaultLobHandler.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/lob/DefaultLobHandler.java new file mode 100644 index 0000000000..308890c7c3 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/lob/DefaultLobHandler.java @@ -0,0 +1,303 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.support.lob; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.StringReader; +import java.io.UnsupportedEncodingException; +import java.sql.Blob; +import java.sql.Clob; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Default implementation of the {@link LobHandler} interface. Invokes + * the direct accessor methods that java.sql.ResultSet + * and java.sql.PreparedStatement offer. + * + *

This LobHandler should work for any JDBC driver that is JDBC compliant + * in terms of the spec's suggestions regarding simple BLOB and CLOB handling. + * This does not apply to Oracle 9i, and only to a limited degree to Oracle 10g! + * As a consequence, use {@link OracleLobHandler} for accessing Oracle BLOBs/CLOBs. + * + *

Some JDBC drivers require values with a BLOB/CLOB target column to be + * explicitly set through the JDBC setBlob / setClob + * API: for example, PostgreSQL's driver. Switch the {@link #setWrapAsLob "wrapAsLob"} + * property to "true" when operating against such a driver. + * + *

On JDBC 4.0, this LobHandler also supports streaming the BLOB/CLOB content + * via the setBlob / setClob variants that take a stream + * argument directly. Consider switching the {@link #setStreamAsLob "streamAsLob"} + * property to "true" when operating against a fully compliant JDBC 4.0 driver. + * + *

See the {@link LobHandler} javadoc for a summary of recommendations. + * + * @author Juergen Hoeller + * @since 04.12.2003 + * @see #setStreamAsLob + * @see java.sql.ResultSet#getBytes + * @see java.sql.ResultSet#getBinaryStream + * @see java.sql.ResultSet#getString + * @see java.sql.ResultSet#getAsciiStream + * @see java.sql.ResultSet#getCharacterStream + * @see java.sql.PreparedStatement#setBytes + * @see java.sql.PreparedStatement#setBinaryStream + * @see java.sql.PreparedStatement#setString + * @see java.sql.PreparedStatement#setAsciiStream + * @see java.sql.PreparedStatement#setCharacterStream + */ +public class DefaultLobHandler extends AbstractLobHandler { + + protected final Log logger = LogFactory.getLog(getClass()); + + private boolean wrapAsLob = false; + + private boolean streamAsLob = false; + + + /** + * Specify whether to submit a byte array / String to the JDBC driver + * wrapped in a JDBC Blob / Clob object, using the JDBC setBlob / + * setClob method with a Blob / Clob argument. + *

Default is "false", using the common JDBC 2.0 setBinaryStream + * / setCharacterStream method for setting the content. + * Switch this to "true" for explicit Blob / Clob wrapping against + * JDBC drivers that are known to require such wrapping (e.g. PostgreSQL's). + *

This setting affects byte array / String arguments as well as stream + * arguments, unless {@link #setStreamAsLob "streamAsLob"} overrides this + * handling to use JDBC 4.0's new explicit streaming support (if available). + * @see java.sql.PreparedStatement#setBlob(int, java.sql.Blob) + * @see java.sql.PreparedStatement#setClob(int, java.sql.Clob) + */ + public void setWrapAsLob(boolean wrapAsLob) { + this.wrapAsLob = wrapAsLob; + } + + /** + * Specify whether to submit a binary stream / character stream to the JDBC + * driver as explicit LOB content, using the JDBC 4.0 setBlob / + * setClob method with a stream argument. + *

Default is "false", using the common JDBC 2.0 setBinaryStream + * / setCharacterStream method for setting the content. + * Switch this to "true" for explicit JDBC 4.0 usage, provided that your + * JDBC driver actually supports those JDBC 4.0 operations (e.g. Derby's). + *

This setting affects stream arguments as well as byte array / String + * arguments, requiring JDBC 4.0 support. For supporting LOB content against + * JDBC 3.0, check out the {@link #setWrapAsLob "wrapAsLob"} setting. + * @see java.sql.PreparedStatement#setBlob(int, java.io.InputStream, long) + * @see java.sql.PreparedStatement#setClob(int, java.io.Reader, long) + */ + public void setStreamAsLob(boolean streamAsLob) { + this.streamAsLob = streamAsLob; + } + + + public byte[] getBlobAsBytes(ResultSet rs, int columnIndex) throws SQLException { + logger.debug("Returning BLOB as bytes"); + return rs.getBytes(columnIndex); + } + + public InputStream getBlobAsBinaryStream(ResultSet rs, int columnIndex) throws SQLException { + logger.debug("Returning BLOB as binary stream"); + return rs.getBinaryStream(columnIndex); + } + + public String getClobAsString(ResultSet rs, int columnIndex) throws SQLException { + logger.debug("Returning CLOB as string"); + return rs.getString(columnIndex); + } + + public InputStream getClobAsAsciiStream(ResultSet rs, int columnIndex) throws SQLException { + logger.debug("Returning CLOB as ASCII stream"); + return rs.getAsciiStream(columnIndex); + } + + public Reader getClobAsCharacterStream(ResultSet rs, int columnIndex) throws SQLException { + logger.debug("Returning CLOB as character stream"); + return rs.getCharacterStream(columnIndex); + } + + public LobCreator getLobCreator() { + return new DefaultLobCreator(); + } + + + /** + * Default LobCreator implementation as inner class. + * Can be subclassed in DefaultLobHandler extensions. + */ + protected class DefaultLobCreator implements LobCreator { + + public void setBlobAsBytes(PreparedStatement ps, int paramIndex, byte[] content) + throws SQLException { + + if (streamAsLob) { + if (content != null) { + ps.setBlob(paramIndex, new ByteArrayInputStream(content), content.length); + } + else { + ps.setBlob(paramIndex, (Blob) null); + } + } + else if (wrapAsLob) { + if (content != null) { + ps.setBlob(paramIndex, new PassThroughBlob(content)); + } + else { + ps.setBlob(paramIndex, (Blob) null); + } + } + else { + ps.setBytes(paramIndex, content); + } + if (logger.isDebugEnabled()) { + logger.debug(content != null ? "Set bytes for BLOB with length " + content.length : + "Set BLOB to null"); + } + } + + public void setBlobAsBinaryStream( + PreparedStatement ps, int paramIndex, InputStream binaryStream, int contentLength) + throws SQLException { + + if (streamAsLob) { + if (binaryStream != null) { + ps.setBlob(paramIndex, binaryStream, contentLength); + } + else { + ps.setBlob(paramIndex, (Blob) null); + } + } + else if (wrapAsLob) { + if (binaryStream != null) { + ps.setBlob(paramIndex, new PassThroughBlob(binaryStream, contentLength)); + } + else { + ps.setBlob(paramIndex, (Blob) null); + } + } + else { + ps.setBinaryStream(paramIndex, binaryStream, contentLength); + } + if (logger.isDebugEnabled()) { + logger.debug(binaryStream != null ? "Set binary stream for BLOB with length " + contentLength : + "Set BLOB to null"); + } + } + + public void setClobAsString(PreparedStatement ps, int paramIndex, String content) + throws SQLException { + + if (streamAsLob) { + if (content != null) { + ps.setClob(paramIndex, new StringReader(content), content.length()); + } + else { + ps.setClob(paramIndex, (Clob) null); + } + } + else if (wrapAsLob) { + if (content != null) { + ps.setClob(paramIndex, new PassThroughClob(content)); + } + else { + ps.setClob(paramIndex, (Clob) null); + } + } + else { + ps.setString(paramIndex, content); + } + if (logger.isDebugEnabled()) { + logger.debug(content != null ? "Set string for CLOB with length " + content.length() : + "Set CLOB to null"); + } + } + + public void setClobAsAsciiStream( + PreparedStatement ps, int paramIndex, InputStream asciiStream, int contentLength) + throws SQLException { + + if (streamAsLob || wrapAsLob) { + if (asciiStream != null) { + try { + Reader reader = new InputStreamReader(asciiStream, "US-ASCII"); + if (streamAsLob) { + ps.setClob(paramIndex, reader, contentLength); + } + else { + ps.setClob(paramIndex, new PassThroughClob(reader, contentLength)); + } + } + catch (UnsupportedEncodingException ex) { + throw new SQLException("US-ASCII encoding not supported: " + ex); + } + } + else { + ps.setClob(paramIndex, (Clob) null); + } + } + else { + ps.setAsciiStream(paramIndex, asciiStream, contentLength); + } + if (logger.isDebugEnabled()) { + logger.debug(asciiStream != null ? "Set ASCII stream for CLOB with length " + contentLength : + "Set CLOB to null"); + } + } + + public void setClobAsCharacterStream( + PreparedStatement ps, int paramIndex, Reader characterStream, int contentLength) + throws SQLException { + + if (streamAsLob) { + if (characterStream != null) { + ps.setClob(paramIndex, characterStream, contentLength); + } + else { + ps.setClob(paramIndex, (Clob) null); + } + } + else if (wrapAsLob) { + if (characterStream != null) { + ps.setClob(paramIndex, new PassThroughClob(characterStream, contentLength)); + } + else { + ps.setClob(paramIndex, (Clob) null); + } + } + else { + ps.setCharacterStream(paramIndex, characterStream, contentLength); + } + if (logger.isDebugEnabled()) { + logger.debug(characterStream != null ? "Set character stream for CLOB with length " + contentLength : + "Set CLOB to null"); + } + } + + public void close() { + // nothing to do here + } + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/lob/JtaLobCreatorSynchronization.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/lob/JtaLobCreatorSynchronization.java new file mode 100644 index 0000000000..71ff069252 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/lob/JtaLobCreatorSynchronization.java @@ -0,0 +1,65 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed 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.springframework.jdbc.support.lob; + +import javax.transaction.Synchronization; + +import org.springframework.util.Assert; + +/** + * Callback for resource cleanup at the end of a JTA transaction. + * Invokes LobCreator.close() to clean up temporary LOBs + * that might have been created. + * + * @author Juergen Hoeller + * @since 2.0 + * @see org.springframework.jdbc.support.lob.LobCreator#close() + * @see javax.transaction.Transaction#registerSynchronization + */ +public class JtaLobCreatorSynchronization implements Synchronization { + + private final LobCreator lobCreator; + + private boolean beforeCompletionCalled = false; + + + /** + * Create a JtaLobCreatorSynchronization for the given LobCreator. + * @param lobCreator the LobCreator to close after transaction completion + */ + public JtaLobCreatorSynchronization(LobCreator lobCreator) { + Assert.notNull(lobCreator, "LobCreator must not be null"); + this.lobCreator = lobCreator; + } + + public void beforeCompletion() { + // Close the LobCreator early if possible, to avoid issues with strict JTA + // implementations that issue warnings when doing JDBC operations after + // transaction completion. + this.beforeCompletionCalled = true; + this.lobCreator.close(); + } + + public void afterCompletion(int status) { + if (!this.beforeCompletionCalled) { + // beforeCompletion not called before (probably because of JTA rollback). + // Close the LobCreator here. + this.lobCreator.close(); + } + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/lob/LobCreator.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/lob/LobCreator.java new file mode 100644 index 0000000000..1d667d41a9 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/lob/LobCreator.java @@ -0,0 +1,139 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.support.lob; + +import java.io.InputStream; +import java.io.Reader; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +/** + * Interface that abstracts potentially database-specific creation of large binary + * fields and large text fields. Does not work with java.sql.Blob + * and java.sql.Clob instances in the API, as some JDBC drivers + * do not support these types as such. + * + *

The LOB creation part is where {@link LobHandler} implementations usually + * differ. Possible strategies include usage of + * PreparedStatement.setBinaryStream/setCharacterStream but also + * PreparedStatement.setBlob/setClob with either a stream argument + * (requires JDBC 4.0) or java.sql.Blob/Clob wrapper objects. + * + *

A LobCreator represents a session for creating BLOBs: It is not + * thread-safe and needs to be instantiated for each statement execution or for + * each transaction. Each LobCreator needs to be closed after completion. + * + *

For convenient working with a PreparedStatement and a LobCreator, + * consider using {@link org.springframework.jdbc.core.JdbcTemplate} with an + *{@link org.springframework.jdbc.core.support.AbstractLobCreatingPreparedStatementCallback} + * implementation. See the latter's javadoc for details. + * + * @author Juergen Hoeller + * @since 04.12.2003 + * @see #close() + * @see LobHandler#getLobCreator() + * @see DefaultLobHandler.DefaultLobCreator + * @see OracleLobHandler.OracleLobCreator + * @see java.sql.PreparedStatement#setBlob + * @see java.sql.PreparedStatement#setClob + * @see java.sql.PreparedStatement#setBytes + * @see java.sql.PreparedStatement#setBinaryStream + * @see java.sql.PreparedStatement#setString + * @see java.sql.PreparedStatement#setAsciiStream + * @see java.sql.PreparedStatement#setCharacterStream + */ +public interface LobCreator { + + /** + * Set the given content as bytes on the given statement, using the given + * parameter index. Might simply invoke PreparedStatement.setBytes + * or create a Blob instance for it, depending on the database and driver. + * @param ps the PreparedStatement to the set the content on + * @param paramIndex the parameter index to use + * @param content the content as byte array, or null for SQL NULL + * @throws SQLException if thrown by JDBC methods + * @see java.sql.PreparedStatement#setBytes + */ + void setBlobAsBytes(PreparedStatement ps, int paramIndex, byte[] content) + throws SQLException; + + /** + * Set the given content as binary stream on the given statement, using the given + * parameter index. Might simply invoke PreparedStatement.setBinaryStream + * or create a Blob instance for it, depending on the database and driver. + * @param ps the PreparedStatement to the set the content on + * @param paramIndex the parameter index to use + * @param contentStream the content as binary stream, or null for SQL NULL + * @throws SQLException if thrown by JDBC methods + * @see java.sql.PreparedStatement#setBinaryStream + */ + void setBlobAsBinaryStream( + PreparedStatement ps, int paramIndex, InputStream contentStream, int contentLength) + throws SQLException; + + /** + * Set the given content as String on the given statement, using the given + * parameter index. Might simply invoke PreparedStatement.setString + * or create a Clob instance for it, depending on the database and driver. + * @param ps the PreparedStatement to the set the content on + * @param paramIndex the parameter index to use + * @param content the content as String, or null for SQL NULL + * @throws SQLException if thrown by JDBC methods + * @see java.sql.PreparedStatement#setBytes + */ + void setClobAsString(PreparedStatement ps, int paramIndex, String content) + throws SQLException; + + /** + * Set the given content as ASCII stream on the given statement, using the given + * parameter index. Might simply invoke PreparedStatement.setAsciiStream + * or create a Clob instance for it, depending on the database and driver. + * @param ps the PreparedStatement to the set the content on + * @param paramIndex the parameter index to use + * @param asciiStream the content as ASCII stream, or null for SQL NULL + * @throws SQLException if thrown by JDBC methods + * @see java.sql.PreparedStatement#setAsciiStream + */ + void setClobAsAsciiStream( + PreparedStatement ps, int paramIndex, InputStream asciiStream, int contentLength) + throws SQLException; + + /** + * Set the given content as character stream on the given statement, using the given + * parameter index. Might simply invoke PreparedStatement.setCharacterStream + * or create a Clob instance for it, depending on the database and driver. + * @param ps the PreparedStatement to the set the content on + * @param paramIndex the parameter index to use + * @param characterStream the content as character stream, or null for SQL NULL + * @throws SQLException if thrown by JDBC methods + * @see java.sql.PreparedStatement#setCharacterStream + */ + void setClobAsCharacterStream( + PreparedStatement ps, int paramIndex, Reader characterStream, int contentLength) + throws SQLException; + + /** + * Close this LobCreator session and free its temporarily created BLOBs and CLOBs. + * Will not need to do anything if using PreparedStatement's standard methods, + * but might be necessary to free database resources if using proprietary means. + *

NOTE: Needs to be invoked after the involved PreparedStatements have + * been executed or the affected O/R mapping sessions have been flushed. + * Otherwise, the database resources for the temporary BLOBs might stay allocated. + */ + void close(); + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/lob/LobCreatorUtils.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/lob/LobCreatorUtils.java new file mode 100644 index 0000000000..77afaba1d1 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/lob/LobCreatorUtils.java @@ -0,0 +1,84 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed 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.springframework.jdbc.support.lob; + +import javax.transaction.Status; +import javax.transaction.TransactionManager; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.transaction.TransactionSystemException; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +/** + * Helper class for registering a transaction synchronization for closing + * a LobCreator, preferring Spring transaction synchronization and falling + * back to plain JTA transaction synchronization. + * + * @author Juergen Hoeller + * @since 2.0 + * @see SpringLobCreatorSynchronization + * @see org.springframework.transaction.support.TransactionSynchronizationManager + * @see JtaLobCreatorSynchronization + * @see javax.transaction.Transaction#registerSynchronization + */ +public abstract class LobCreatorUtils { + + private static final Log logger = LogFactory.getLog(LobCreatorUtils.class); + + + /** + * Register a transaction synchronization for closing the given LobCreator, + * preferring Spring transaction synchronization and falling back to + * plain JTA transaction synchronization. + * @param lobCreator the LobCreator to close after transaction completion + * @param jtaTransactionManager the JTA TransactionManager to fall back to + * when no Spring transaction synchronization is active (may be null) + * @throws IllegalStateException if there is neither active Spring transaction + * synchronization nor active JTA transaction synchronization + */ + public static void registerTransactionSynchronization( + LobCreator lobCreator, TransactionManager jtaTransactionManager) throws IllegalStateException { + + if (TransactionSynchronizationManager.isSynchronizationActive()) { + logger.debug("Registering Spring transaction synchronization for LobCreator"); + TransactionSynchronizationManager.registerSynchronization( + new SpringLobCreatorSynchronization(lobCreator)); + } + else { + if (jtaTransactionManager != null) { + try { + int jtaStatus = jtaTransactionManager.getStatus(); + if (jtaStatus == Status.STATUS_ACTIVE || jtaStatus == Status.STATUS_MARKED_ROLLBACK) { + logger.debug("Registering JTA transaction synchronization for LobCreator"); + jtaTransactionManager.getTransaction().registerSynchronization( + new JtaLobCreatorSynchronization(lobCreator)); + return; + } + } + catch (Throwable ex) { + throw new TransactionSystemException( + "Could not register synchronization with JTA TransactionManager", ex); + } + } + throw new IllegalStateException("Active Spring transaction synchronization or active " + + "JTA transaction with specified [javax.transaction.TransactionManager] required"); + } + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/lob/LobHandler.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/lob/LobHandler.java new file mode 100644 index 0000000000..a994220678 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/lob/LobHandler.java @@ -0,0 +1,210 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.support.lob; + +import java.io.InputStream; +import java.io.Reader; +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + * Abstraction for handling large binary fields and large text fields in + * specific databases, no matter if represented as simple types or Large OBjects. + * Its main purpose is to isolate Oracle's peculiar handling of LOBs in + * {@link OracleLobHandler}; most other databases should be able to work + * with the provided {@link DefaultLobHandler}. + * + *

Provides accessor methods for BLOBs and CLOBs, and acts as factory for + * LobCreator instances, to be used as sessions for creating BLOBs or CLOBs. + * LobCreators are typically instantiated for each statement execution or for + * each transaction. They are not thread-safe because they might track + * allocated database resources to be able to free them after execution. + * + *

Most databases/drivers should be able to work with {@link DefaultLobHandler}, + * which by default delegates to JDBC's direct accessor methods, avoiding + * java.sql.Blob and java.sql.Clob completely. + * {@link DefaultLobHandler} can also be configured to populate LOBs using + * PreparedStatement.setBlob/setClob (e.g. for PostgreSQL). + * + *

Unfortunately, Oracle 9i just accepts Blob/Clob instances created via its own + * proprietary BLOB/CLOB API, and additionally doesn't accept large streams for + * PreparedStatement's corresponding setter methods. Therefore, you need to use + * {@link OracleLobHandler} there, which uses Oracle's BLOB/CLOB API for both all access. + * The Oracle 10g JDBC driver should basically work with {@link DefaultLobHandler} + * as well, with some limitations in terms of LOB sizes. + * + *

Of course, you need to declare different field types for each database. + * In Oracle, any binary content needs to go into a BLOB, and all character content + * beyond 4000 bytes needs to go into a CLOB. In MySQL, there is no notion of a + * CLOB type but rather a LONGTEXT type that behaves like a VARCHAR. For complete + * portability, use a LobHandler for fields that might typically require LOBs on + * some database because of the field size (take Oracle's numbers as a guideline). + * + *

Summarizing the recommended options (for actual LOB fields): + *

    + *
  • JDBC 4.0 driver: {@link DefaultLobHandler} with streamAsLob=true. + *
  • PostgreSQL: {@link DefaultLobHandler} with wrapAsLob=true. + *
  • Oracle 9i/10g: {@link OracleLobHandler} with a connection-pool-specific + * {@link OracleLobHandler#setNativeJdbcExtractor NativeJdbcExtractor}. + *
  • For all other database drivers (and for non-LOB fields that might potentially + * turn into LOBs on some databases): a plain {@link DefaultLobHandler}. + *
+ * + * @author Juergen Hoeller + * @since 23.12.2003 + * @see DefaultLobHandler + * @see OracleLobHandler + * @see java.sql.ResultSet#getBlob + * @see java.sql.ResultSet#getClob + * @see java.sql.ResultSet#getBytes + * @see java.sql.ResultSet#getBinaryStream + * @see java.sql.ResultSet#getString + * @see java.sql.ResultSet#getAsciiStream + * @see java.sql.ResultSet#getCharacterStream + */ +public interface LobHandler { + + /** + * Retrieve the given column as bytes from the given ResultSet. + * Might simply invoke ResultSet.getBytes or work with + * ResultSet.getBlob, depending on the database and driver. + * @param rs the ResultSet to retrieve the content from + * @param columnName the column name to use + * @return the content as byte array, or null in case of SQL NULL + * @throws SQLException if thrown by JDBC methods + * @see java.sql.ResultSet#getBytes + */ + byte[] getBlobAsBytes(ResultSet rs, String columnName) throws SQLException; + + /** + * Retrieve the given column as bytes from the given ResultSet. + * Might simply invoke ResultSet.getBytes or work with + * ResultSet.getBlob, depending on the database and driver. + * @param rs the ResultSet to retrieve the content from + * @param columnIndex the column index to use + * @return the content as byte array, or null in case of SQL NULL + * @throws SQLException if thrown by JDBC methods + * @see java.sql.ResultSet#getBytes + */ + byte[] getBlobAsBytes(ResultSet rs, int columnIndex) throws SQLException; + + /** + * Retrieve the given column as binary stream from the given ResultSet. + * Might simply invoke ResultSet.getBinaryStream or work with + * ResultSet.getBlob, depending on the database and driver. + * @param rs the ResultSet to retrieve the content from + * @param columnName the column name to use + * @return the content as binary stream, or null in case of SQL NULL + * @throws SQLException if thrown by JDBC methods + * @see java.sql.ResultSet#getBinaryStream + */ + InputStream getBlobAsBinaryStream(ResultSet rs, String columnName) throws SQLException; + + /** + * Retrieve the given column as binary stream from the given ResultSet. + * Might simply invoke ResultSet.getBinaryStream or work with + * ResultSet.getBlob, depending on the database and driver. + * @param rs the ResultSet to retrieve the content from + * @param columnIndex the column index to use + * @return the content as binary stream, or null in case of SQL NULL + * @throws SQLException if thrown by JDBC methods + * @see java.sql.ResultSet#getBinaryStream + */ + InputStream getBlobAsBinaryStream(ResultSet rs, int columnIndex) throws SQLException; + + /** + * Retrieve the given column as String from the given ResultSet. + * Might simply invoke ResultSet.getString or work with + * ResultSet.getClob, depending on the database and driver. + * @param rs the ResultSet to retrieve the content from + * @param columnName the column name to use + * @return the content as String, or null in case of SQL NULL + * @throws SQLException if thrown by JDBC methods + * @see java.sql.ResultSet#getString + */ + String getClobAsString(ResultSet rs, String columnName) throws SQLException; + + /** + * Retrieve the given column as String from the given ResultSet. + * Might simply invoke ResultSet.getString or work with + * ResultSet.getClob, depending on the database and driver. + * @param rs the ResultSet to retrieve the content from + * @param columnIndex the column index to use + * @return the content as String, or null in case of SQL NULL + * @throws SQLException if thrown by JDBC methods + * @see java.sql.ResultSet#getString + */ + String getClobAsString(ResultSet rs, int columnIndex) throws SQLException; + + /** + * Retrieve the given column as ASCII stream from the given ResultSet. + * Might simply invoke ResultSet.getAsciiStream or work with + * ResultSet.getClob, depending on the database and driver. + * @param rs the ResultSet to retrieve the content from + * @param columnName the column name to use + * @return the content as ASCII stream, or null in case of SQL NULL + * @throws SQLException if thrown by JDBC methods + * @see java.sql.ResultSet#getAsciiStream + */ + InputStream getClobAsAsciiStream(ResultSet rs, String columnName) throws SQLException; + + /** + * Retrieve the given column as ASCII stream from the given ResultSet. + * Might simply invoke ResultSet.getAsciiStream or work with + * ResultSet.getClob, depending on the database and driver. + * @param rs the ResultSet to retrieve the content from + * @param columnIndex the column index to use + * @return the content as ASCII stream, or null in case of SQL NULL + * @throws SQLException if thrown by JDBC methods + * @see java.sql.ResultSet#getAsciiStream + */ + InputStream getClobAsAsciiStream(ResultSet rs, int columnIndex) throws SQLException; + + /** + * Retrieve the given column as character stream from the given ResultSet. + * Might simply invoke ResultSet.getCharacterStream or work with + * ResultSet.getClob, depending on the database and driver. + * @param rs the ResultSet to retrieve the content from + * @param columnName the column name to use + * @return the content as character stream + * @throws SQLException if thrown by JDBC methods + * @see java.sql.ResultSet#getCharacterStream + */ + Reader getClobAsCharacterStream(ResultSet rs, String columnName) throws SQLException; + + /** + * Retrieve the given column as character stream from the given ResultSet. + * Might simply invoke ResultSet.getCharacterStream or work with + * ResultSet.getClob, depending on the database and driver. + * @param rs the ResultSet to retrieve the content from + * @param columnIndex the column index to use + * @return the content as character stream + * @throws SQLException if thrown by JDBC methods + * @see java.sql.ResultSet#getCharacterStream + */ + Reader getClobAsCharacterStream(ResultSet rs, int columnIndex) throws SQLException; + + /** + * Create a new {@link LobCreator} instance, i.e. a session for creating BLOBs + * and CLOBs. Needs to be closed after the created LOBs are not needed anymore - + * typically after statement execution or transaction completion. + * @return the new LobCreator instance + * @see LobCreator#close() + */ + LobCreator getLobCreator(); + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/lob/OracleLobHandler.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/lob/OracleLobHandler.java new file mode 100644 index 0000000000..bf54540361 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/lob/OracleLobHandler.java @@ -0,0 +1,438 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc.support.lob; + +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Reader; +import java.io.Writer; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.sql.Blob; +import java.sql.Clob; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.jdbc.support.nativejdbc.NativeJdbcExtractor; +import org.springframework.util.FileCopyUtils; + +/** + * {@link LobHandler} implementation for Oracle databases. Uses proprietary API + * to create oracle.sql.BLOB and oracle.sql.CLOB + * instances, as necessary when working with Oracle's JDBC driver. + * Note that this LobHandler requires Oracle JDBC driver 9i or higher! + * + *

While most databases are able to work with {@link DefaultLobHandler}, + * Oracle just accepts Blob/Clob instances created via its own proprietary + * BLOB/CLOB API, and additionally doesn't accept large streams for + * PreparedStatement's corresponding setter methods. Therefore, you need + * to use a strategy like this LobHandler implementation. + * + *

Needs to work on a native JDBC Connection, to be able to cast it to + * oracle.jdbc.OracleConnection. If you pass in Connections from a + * connection pool (the usual case in a J2EE environment), you need to set an + * appropriate {@link org.springframework.jdbc.support.nativejdbc.NativeJdbcExtractor} + * to allow for automatical retrieval of the underlying native JDBC Connection. + * LobHandler and NativeJdbcExtractor are separate concerns, therefore they + * are represented by separate strategy interfaces. + * + *

Coded via reflection to avoid dependencies on Oracle classes. + * Even reads in Oracle constants via reflection because of different Oracle + * drivers (classes12, ojdbc14) having different constant values! As this + * LobHandler initializes Oracle classes on instantiation, do not define this + * as eager-initializing singleton if you do not want to depend on the Oracle + * JAR being in the class path: use "lazy-init=true" to avoid this issue. + * + * @author Juergen Hoeller + * @since 04.12.2003 + * @see #setNativeJdbcExtractor + * @see oracle.sql.BLOB + * @see oracle.sql.CLOB + */ +public class OracleLobHandler extends AbstractLobHandler { + + private static final String BLOB_CLASS_NAME = "oracle.sql.BLOB"; + + private static final String CLOB_CLASS_NAME = "oracle.sql.CLOB"; + + private static final String DURATION_SESSION_FIELD_NAME = "DURATION_SESSION"; + + private static final String MODE_READWRITE_FIELD_NAME = "MODE_READWRITE"; + + + protected final Log logger = LogFactory.getLog(getClass()); + + private NativeJdbcExtractor nativeJdbcExtractor; + + private Boolean cache = Boolean.TRUE; + + private Class blobClass; + + private Class clobClass; + + private final Map durationSessionConstants = new HashMap(2); + + private final Map modeReadWriteConstants = new HashMap(2); + + + /** + * Set an appropriate NativeJdbcExtractor to be able to retrieve the underlying + * native oracle.jdbc.OracleConnection. This is necessary for + * DataSource-based connection pools, as those need to return wrapped JDBC + * Connection handles that cannot be cast to a native Connection implementation. + *

Effectively, this LobHandler just invokes a single NativeJdbcExtractor + * method, namely getNativeConnectionFromStatement with a + * PreparedStatement argument (falling back to a + * PreparedStatement.getConnection() call if no extractor is set). + *

A common choice is SimpleNativeJdbcExtractor, whose Connection unwrapping + * (which is what OracleLobHandler needs) will work with many connection pools. + * See SimpleNativeJdbcExtractor's javadoc for details. + * @see org.springframework.jdbc.support.nativejdbc.NativeJdbcExtractor#getNativeConnectionFromStatement + * @see org.springframework.jdbc.support.nativejdbc.SimpleNativeJdbcExtractor + * @see oracle.jdbc.OracleConnection + */ + public void setNativeJdbcExtractor(NativeJdbcExtractor nativeJdbcExtractor) { + this.nativeJdbcExtractor = nativeJdbcExtractor; + } + + /** + * Set whether to cache the temporary LOB in the buffer cache. + * This value will be passed into BLOB/CLOB.createTemporary. Default is "true". + * @see oracle.sql.BLOB#createTemporary + * @see oracle.sql.CLOB#createTemporary + */ + public void setCache(boolean cache) { + this.cache = new Boolean(cache); + } + + + /** + * Retrieve the oracle.sql.BLOB and oracle.sql.CLOB + * classes via reflection, and initialize the values for the + * DURATION_SESSION and MODE_READWRITE constants defined there. + * @param con the Oracle Connection, for using the exact same class loader + * that the Oracle driver was loaded with + * @see oracle.sql.BLOB#DURATION_SESSION + * @see oracle.sql.BLOB#MODE_READWRITE + * @see oracle.sql.CLOB#DURATION_SESSION + * @see oracle.sql.CLOB#MODE_READWRITE + */ + protected synchronized void initOracleDriverClasses(Connection con) { + if (this.blobClass == null) { + try { + // Initialize oracle.sql.BLOB class + this.blobClass = con.getClass().getClassLoader().loadClass(BLOB_CLASS_NAME); + this.durationSessionConstants.put( + this.blobClass, new Integer(this.blobClass.getField(DURATION_SESSION_FIELD_NAME).getInt(null))); + this.modeReadWriteConstants.put( + this.blobClass, new Integer(this.blobClass.getField(MODE_READWRITE_FIELD_NAME).getInt(null))); + + // Initialize oracle.sql.CLOB class + this.clobClass = con.getClass().getClassLoader().loadClass(CLOB_CLASS_NAME); + this.durationSessionConstants.put( + this.clobClass, new Integer(this.clobClass.getField(DURATION_SESSION_FIELD_NAME).getInt(null))); + this.modeReadWriteConstants.put( + this.clobClass, new Integer(this.clobClass.getField(MODE_READWRITE_FIELD_NAME).getInt(null))); + } + catch (Exception ex) { + throw new InvalidDataAccessApiUsageException( + "Couldn't initialize OracleLobHandler because Oracle driver classes are not available. " + + "Note that OracleLobHandler requires Oracle JDBC driver 9i or higher!", ex); + } + } + } + + + public byte[] getBlobAsBytes(ResultSet rs, int columnIndex) throws SQLException { + logger.debug("Returning Oracle BLOB as bytes"); + Blob blob = rs.getBlob(columnIndex); + return (blob != null ? blob.getBytes(1, (int) blob.length()) : null); + } + + public InputStream getBlobAsBinaryStream(ResultSet rs, int columnIndex) throws SQLException { + logger.debug("Returning Oracle BLOB as binary stream"); + Blob blob = rs.getBlob(columnIndex); + return (blob != null ? blob.getBinaryStream() : null); + } + + public String getClobAsString(ResultSet rs, int columnIndex) throws SQLException { + logger.debug("Returning Oracle CLOB as string"); + Clob clob = rs.getClob(columnIndex); + return (clob != null ? clob.getSubString(1, (int) clob.length()) : null); + } + + public InputStream getClobAsAsciiStream(ResultSet rs, int columnIndex) throws SQLException { + logger.debug("Returning Oracle CLOB as ASCII stream"); + Clob clob = rs.getClob(columnIndex); + return (clob != null ? clob.getAsciiStream() : null); + } + + public Reader getClobAsCharacterStream(ResultSet rs, int columnIndex) throws SQLException { + logger.debug("Returning Oracle CLOB as character stream"); + Clob clob = rs.getClob(columnIndex); + return (clob != null ? clob.getCharacterStream() : null); + } + + public LobCreator getLobCreator() { + return new OracleLobCreator(); + } + + + /** + * LobCreator implementation for Oracle databases. + * Creates Oracle-style temporary BLOBs and CLOBs that it frees on close. + * @see #close + */ + protected class OracleLobCreator implements LobCreator { + + private final List createdLobs = new LinkedList(); + + public void setBlobAsBytes(PreparedStatement ps, int paramIndex, final byte[] content) + throws SQLException { + + if (content != null) { + Blob blob = (Blob) createLob(ps, false, new LobCallback() { + public void populateLob(Object lob) throws Exception { + Method methodToInvoke = lob.getClass().getMethod("getBinaryOutputStream", new Class[0]); + OutputStream out = (OutputStream) methodToInvoke.invoke(lob, (Object[]) null); + FileCopyUtils.copy(content, out); + } + }); + ps.setBlob(paramIndex, blob); + if (logger.isDebugEnabled()) { + logger.debug("Set bytes for Oracle BLOB with length " + blob.length()); + } + } + else { + ps.setBlob(paramIndex, (Blob) null); + logger.debug("Set Oracle BLOB to null"); + } + } + + public void setBlobAsBinaryStream( + PreparedStatement ps, int paramIndex, final InputStream binaryStream, int contentLength) + throws SQLException { + + if (binaryStream != null) { + Blob blob = (Blob) createLob(ps, false, new LobCallback() { + public void populateLob(Object lob) throws Exception { + Method methodToInvoke = lob.getClass().getMethod("getBinaryOutputStream", (Class[]) null); + OutputStream out = (OutputStream) methodToInvoke.invoke(lob, (Object[]) null); + FileCopyUtils.copy(binaryStream, out); + } + }); + ps.setBlob(paramIndex, blob); + if (logger.isDebugEnabled()) { + logger.debug("Set binary stream for Oracle BLOB with length " + blob.length()); + } + } + else { + ps.setBlob(paramIndex, (Blob) null); + logger.debug("Set Oracle BLOB to null"); + } + } + + public void setClobAsString(PreparedStatement ps, int paramIndex, final String content) + throws SQLException { + + if (content != null) { + Clob clob = (Clob) createLob(ps, true, new LobCallback() { + public void populateLob(Object lob) throws Exception { + Method methodToInvoke = lob.getClass().getMethod("getCharacterOutputStream", (Class[]) null); + Writer writer = (Writer) methodToInvoke.invoke(lob, (Object[]) null); + FileCopyUtils.copy(content, writer); + } + }); + ps.setClob(paramIndex, clob); + if (logger.isDebugEnabled()) { + logger.debug("Set string for Oracle CLOB with length " + clob.length()); + } + } + else { + ps.setClob(paramIndex, (Clob) null); + logger.debug("Set Oracle CLOB to null"); + } + } + + public void setClobAsAsciiStream( + PreparedStatement ps, int paramIndex, final InputStream asciiStream, int contentLength) + throws SQLException { + + if (asciiStream != null) { + Clob clob = (Clob) createLob(ps, true, new LobCallback() { + public void populateLob(Object lob) throws Exception { + Method methodToInvoke = lob.getClass().getMethod("getAsciiOutputStream", (Class[]) null); + OutputStream out = (OutputStream) methodToInvoke.invoke(lob, (Object[]) null); + FileCopyUtils.copy(asciiStream, out); + } + }); + ps.setClob(paramIndex, clob); + if (logger.isDebugEnabled()) { + logger.debug("Set ASCII stream for Oracle CLOB with length " + clob.length()); + } + } + else { + ps.setClob(paramIndex, (Clob) null); + logger.debug("Set Oracle CLOB to null"); + } + } + + public void setClobAsCharacterStream( + PreparedStatement ps, int paramIndex, final Reader characterStream, int contentLength) + throws SQLException { + + if (characterStream != null) { + Clob clob = (Clob) createLob(ps, true, new LobCallback() { + public void populateLob(Object lob) throws Exception { + Method methodToInvoke = lob.getClass().getMethod("getCharacterOutputStream", (Class[]) null); + Writer writer = (Writer) methodToInvoke.invoke(lob, (Object[]) null); + FileCopyUtils.copy(characterStream, writer); + } + }); + ps.setClob(paramIndex, clob); + if (logger.isDebugEnabled()) { + logger.debug("Set character stream for Oracle CLOB with length " + clob.length()); + } + } + else { + ps.setClob(paramIndex, (Clob) null); + logger.debug("Set Oracle CLOB to null"); + } + } + + /** + * Create a LOB instance for the given PreparedStatement, + * populating it via the given callback. + */ + protected Object createLob(PreparedStatement ps, boolean clob, LobCallback callback) + throws SQLException { + + Connection con = null; + try { + con = getOracleConnection(ps); + initOracleDriverClasses(con); + Object lob = prepareLob(con, clob ? clobClass : blobClass); + callback.populateLob(lob); + lob.getClass().getMethod("close", (Class[]) null).invoke(lob, (Object[]) null); + this.createdLobs.add(lob); + if (logger.isDebugEnabled()) { + logger.debug("Created new Oracle " + (clob ? "CLOB" : "BLOB")); + } + return lob; + } + catch (SQLException ex) { + throw ex; + } + catch (InvocationTargetException ex) { + if (ex.getTargetException() instanceof SQLException) { + throw (SQLException) ex.getTargetException(); + } + else if (con != null && ex.getTargetException() instanceof ClassCastException) { + throw new InvalidDataAccessApiUsageException( + "OracleLobCreator needs to work on [oracle.jdbc.OracleConnection], not on [" + + con.getClass().getName() + "]: specify a corresponding NativeJdbcExtractor", + ex.getTargetException()); + } + else { + throw new DataAccessResourceFailureException("Could not create Oracle LOB", + ex.getTargetException()); + } + } + catch (Exception ex) { + throw new DataAccessResourceFailureException("Could not create Oracle LOB", ex); + } + } + + /** + * Retrieve the underlying OracleConnection, using a NativeJdbcExtractor if set. + */ + protected Connection getOracleConnection(PreparedStatement ps) + throws SQLException, ClassNotFoundException { + + return (nativeJdbcExtractor != null) ? + nativeJdbcExtractor.getNativeConnectionFromStatement(ps) : ps.getConnection(); + } + + /** + * Create and open an oracle.sql.BLOB/CLOB instance via reflection. + */ + protected Object prepareLob(Connection con, Class lobClass) throws Exception { + /* + BLOB blob = BLOB.createTemporary(con, false, BLOB.DURATION_SESSION); + blob.open(BLOB.MODE_READWRITE); + return blob; + */ + Method createTemporary = lobClass.getMethod( + "createTemporary", new Class[] {Connection.class, boolean.class, int.class}); + Object lob = createTemporary.invoke( + null, new Object[] {con, cache, durationSessionConstants.get(lobClass)}); + Method open = lobClass.getMethod("open", new Class[] {int.class}); + open.invoke(lob, new Object[] {modeReadWriteConstants.get(lobClass)}); + return lob; + } + + /** + * Free all temporary BLOBs and CLOBs created by this creator. + */ + public void close() { + try { + for (Iterator it = this.createdLobs.iterator(); it.hasNext();) { + /* + BLOB blob = (BLOB) it.next(); + blob.freeTemporary(); + */ + Object lob = it.next(); + Method freeTemporary = lob.getClass().getMethod("freeTemporary", new Class[0]); + freeTemporary.invoke(lob, new Object[0]); + it.remove(); + } + } + catch (InvocationTargetException ex) { + logger.error("Could not free Oracle LOB", ex.getTargetException()); + } + catch (Exception ex) { + throw new DataAccessResourceFailureException("Could not free Oracle LOB", ex); + } + } + } + + + /** + * Internal callback interface for use with createLob. + */ + protected static interface LobCallback { + + /** + * Populate the given BLOB or CLOB instance with content. + * @throws Exception any exception including InvocationTargetException + */ + void populateLob(Object lob) throws Exception; + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/lob/PassThroughBlob.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/lob/PassThroughBlob.java new file mode 100644 index 0000000000..d065bb6de9 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/lob/PassThroughBlob.java @@ -0,0 +1,97 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.support.lob; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.sql.Blob; +import java.sql.SQLException; + +/** + * Simple JDBC {@link Blob} adapter that exposes a given byte array or binary stream. + * Optionally used by {@link DefaultLobHandler}. + * + * @author Juergen Hoeller + * @since 2.5.3 + */ +class PassThroughBlob implements Blob { + + private byte[] content; + + private InputStream binaryStream; + + private long contentLength; + + + public PassThroughBlob(byte[] content) { + this.content = content; + this.contentLength = content.length; + } + + public PassThroughBlob(InputStream binaryStream, long contentLength) { + this.binaryStream = binaryStream; + this.contentLength = contentLength; + } + + + public long length() throws SQLException { + return this.contentLength; + } + + public InputStream getBinaryStream() throws SQLException { + return (this.content != null ? new ByteArrayInputStream(this.content) : this.binaryStream); + } + + + public InputStream getBinaryStream(long pos, long length) throws SQLException { + throw new UnsupportedOperationException(); + } + + public OutputStream setBinaryStream(long pos) throws SQLException { + throw new UnsupportedOperationException(); + } + + public byte[] getBytes(long pos, int length) throws SQLException { + throw new UnsupportedOperationException(); + } + + public int setBytes(long pos, byte[] bytes) throws SQLException { + throw new UnsupportedOperationException(); + } + + public int setBytes(long pos, byte[] bytes, int offset, int len) throws SQLException { + throw new UnsupportedOperationException(); + } + + public long position(byte pattern[], long start) throws SQLException { + throw new UnsupportedOperationException(); + } + + public long position(Blob pattern, long start) throws SQLException { + throw new UnsupportedOperationException(); + } + + public void truncate(long len) throws SQLException { + throw new UnsupportedOperationException(); + } + + public void free() throws SQLException { + throw new UnsupportedOperationException(); + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/lob/PassThroughClob.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/lob/PassThroughClob.java new file mode 100644 index 0000000000..57210b485a --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/lob/PassThroughClob.java @@ -0,0 +1,107 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.support.lob; + +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Reader; +import java.io.StringReader; +import java.io.Writer; +import java.sql.Clob; +import java.sql.SQLException; + +/** + * Simple JDBC {@link Clob} adapter that exposes a given String or character stream. + * Optionally used by {@link DefaultLobHandler}. + * + * @author Juergen Hoeller + * @since 2.5.3 + */ +class PassThroughClob implements Clob { + + private String content; + + private Reader characterStream; + + private long contentLength; + + + public PassThroughClob(String content) { + this.content = content; + this.contentLength = content.length(); + } + + public PassThroughClob(Reader characterStream, long contentLength) { + this.characterStream = characterStream; + this.contentLength = contentLength; + } + + + public long length() throws SQLException { + return this.contentLength; + } + + public Reader getCharacterStream() throws SQLException { + return (this.content != null ? new StringReader(this.content) : this.characterStream); + } + + + public Reader getCharacterStream(long pos, long length) throws SQLException { + throw new UnsupportedOperationException(); + } + + public Writer setCharacterStream(long pos) throws SQLException { + throw new UnsupportedOperationException(); + } + + public InputStream getAsciiStream() throws SQLException { + return null; + } + + public OutputStream setAsciiStream(long pos) throws SQLException { + throw new UnsupportedOperationException(); + } + + public String getSubString(long pos, int length) throws SQLException { + throw new UnsupportedOperationException(); + } + + public int setString(long pos, String str) throws SQLException { + throw new UnsupportedOperationException(); + } + + public int setString(long pos, String str, int offset, int len) throws SQLException { + throw new UnsupportedOperationException(); + } + + public long position(String searchstr, long start) throws SQLException { + throw new UnsupportedOperationException(); + } + + public long position(Clob searchstr, long start) throws SQLException { + throw new UnsupportedOperationException(); + } + + public void truncate(long len) throws SQLException { + throw new UnsupportedOperationException(); + } + + public void free() throws SQLException { + throw new UnsupportedOperationException(); + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/lob/SpringLobCreatorSynchronization.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/lob/SpringLobCreatorSynchronization.java new file mode 100644 index 0000000000..9f5fb4d5e8 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/lob/SpringLobCreatorSynchronization.java @@ -0,0 +1,80 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed 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.springframework.jdbc.support.lob; + +import org.springframework.jdbc.datasource.DataSourceUtils; +import org.springframework.transaction.support.TransactionSynchronizationAdapter; +import org.springframework.util.Assert; + +/** + * Callback for resource cleanup at the end of a Spring transaction. + * Invokes LobCreator.close() to clean up temporary LOBs + * that might have been created. + * + * @author Juergen Hoeller + * @since 2.0 + * @see org.springframework.jdbc.support.lob.LobCreator#close() + */ +public class SpringLobCreatorSynchronization extends TransactionSynchronizationAdapter { + + /** + * Order value for TransactionSynchronization objects that clean up LobCreators. + * Return CONNECTION_SYNCHRONIZATION_ORDER - 200 to execute LobCreator cleanup + * before Hibernate Session (- 100) and JDBC Connection cleanup, if any. + * @see org.springframework.jdbc.datasource.DataSourceUtils#CONNECTION_SYNCHRONIZATION_ORDER + */ + public static final int LOB_CREATOR_SYNCHRONIZATION_ORDER = + DataSourceUtils.CONNECTION_SYNCHRONIZATION_ORDER - 200; + + + private final LobCreator lobCreator; + + private boolean beforeCompletionCalled = false; + + + /** + * Create a SpringLobCreatorSynchronization for the given LobCreator. + * @param lobCreator the LobCreator to close after transaction completion + */ + public SpringLobCreatorSynchronization(LobCreator lobCreator) { + Assert.notNull(lobCreator, "LobCreator must not be null"); + this.lobCreator = lobCreator; + } + + public int getOrder() { + return LOB_CREATOR_SYNCHRONIZATION_ORDER; + } + + + public void beforeCompletion() { + // Close the LobCreator early if possible, to avoid issues with strict JTA + // implementations that issue warnings when doing JDBC operations after + // transaction completion. + this.beforeCompletionCalled = true; + this.lobCreator.close(); + } + + public void afterCompletion(int status) { + if (!this.beforeCompletionCalled) { + // beforeCompletion not called before (probably because of flushing on commit + // in the transaction manager, after the chain of beforeCompletion calls). + // Close the LobCreator here. + this.lobCreator.close(); + } + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/lob/package.html b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/lob/package.html new file mode 100644 index 0000000000..c549effcd3 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/lob/package.html @@ -0,0 +1,11 @@ + + + +Provides a stategy interface for Large OBject handling, +with implementations for various databases. + +

Can be used independently from jdbc.core and jdbc.object, +for example in custom JDBC access code. + + + diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/nativejdbc/C3P0NativeJdbcExtractor.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/nativejdbc/C3P0NativeJdbcExtractor.java new file mode 100644 index 0000000000..4995b01827 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/nativejdbc/C3P0NativeJdbcExtractor.java @@ -0,0 +1,109 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc.support.nativejdbc; + +import java.lang.reflect.Method; +import java.sql.Connection; +import java.sql.SQLException; + +import com.mchange.v2.c3p0.C3P0ProxyConnection; + +import org.springframework.util.ReflectionUtils; + +/** + * Implementation of the {@link NativeJdbcExtractor} interface for the + * C3P0 connection pool. + * + *

Returns underlying native Connections to application code instead of C3P0's + * wrapper implementations; unwraps the Connection for native Statements. + * The returned JDBC classes can then safely be cast, e.g. to + * oracle.jdbc.OracleConnection. + * + *

This NativeJdbcExtractor can be set just to allow working with + * a C3P0 DataSource: If a given object is not a C3P0 wrapper, it will be + * returned as-is. + * + *

Note that this class requires C3P0 0.8.5 or later; for earlier C3P0 versions, + * use SimpleNativeJdbcExtractor (which won't work for C3P0 0.8.5 or later). + * + * @author Juergen Hoeller + * @since 1.1.5 + * @see com.mchange.v2.c3p0.C3P0ProxyConnection#rawConnectionOperation + * @see SimpleNativeJdbcExtractor + */ +public class C3P0NativeJdbcExtractor extends NativeJdbcExtractorAdapter { + + private final Method getRawConnectionMethod; + + + /** + * This method is not meant to be used directly; it rather serves + * as callback method for C3P0's "rawConnectionOperation" API. + * @param con a native Connection handle + * @return the native Connection handle, as-is + */ + public static Connection getRawConnection(Connection con) { + return con; + } + + + public C3P0NativeJdbcExtractor() { + try { + this.getRawConnectionMethod = getClass().getMethod("getRawConnection", new Class[] {Connection.class}); + } + catch (NoSuchMethodException ex) { + throw new IllegalStateException("Internal error in C3P0NativeJdbcExtractor: " + ex.getMessage()); + } + } + + + public boolean isNativeConnectionNecessaryForNativeStatements() { + return true; + } + + public boolean isNativeConnectionNecessaryForNativePreparedStatements() { + return true; + } + + public boolean isNativeConnectionNecessaryForNativeCallableStatements() { + return true; + } + + /** + * Retrieve the Connection via C3P0's rawConnectionOperation API, + * using the getRawConnection as callback to get access to the + * raw Connection (which is otherwise not directly supported by C3P0). + * @see #getRawConnection + */ + protected Connection doGetNativeConnection(Connection con) throws SQLException { + if (con instanceof C3P0ProxyConnection) { + C3P0ProxyConnection cpCon = (C3P0ProxyConnection) con; + try { + return (Connection) cpCon.rawConnectionOperation( + this.getRawConnectionMethod, null, new Object[] {C3P0ProxyConnection.RAW_CONNECTION}); + } + catch (SQLException ex) { + throw ex; + } + catch (Exception ex) { + ReflectionUtils.handleReflectionException(ex); + } + } + return con; + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/nativejdbc/CommonsDbcpNativeJdbcExtractor.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/nativejdbc/CommonsDbcpNativeJdbcExtractor.java new file mode 100644 index 0000000000..3b3ec98d2b --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/nativejdbc/CommonsDbcpNativeJdbcExtractor.java @@ -0,0 +1,107 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.support.nativejdbc; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.sql.CallableStatement; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import org.springframework.util.ReflectionUtils; + +/** + * Implementation of the {@link NativeJdbcExtractor} interface for the + * Jakarta Commons DBCP connection pool, version 1.1 or higher. + * + *

Returns the underlying native Connection, Statement, etc to application + * code instead of DBCP's wrapper implementations. The returned JDBC classes + * can then safely be cast, e.g. to oracle.jdbc.OracleConnection. + * + *

This NativeJdbcExtractor can be set just to allow working with a + * Commons DBCP DataSource: If a given object is not a Commons DBCP wrapper, + * it will be returned as-is. + * + *

Note that this version of CommonsDbcpNativeJdbcExtractor will work + * against the original Commons DBCP in org.apache.commons.dbcp + * as well as against Tomcat 5.5's relocated Commons DBCP version in the + * org.apache.tomcat.dbcp.dbcp package. + * + * @author Juergen Hoeller + * @since 25.08.2003 + */ +public class CommonsDbcpNativeJdbcExtractor extends NativeJdbcExtractorAdapter { + + private static final String GET_INNERMOST_DELEGATE_METHOD_NAME = "getInnermostDelegate"; + + + /** + * Extracts the innermost delegate from the given Commons DBCP object. + * Falls back to the given object if no underlying object found. + * @param obj the Commons DBCP Connection/Statement/ResultSet + * @return the underlying native Connection/Statement/ResultSet + */ + private static Object getInnermostDelegate(Object obj) throws SQLException { + if (obj == null) { + return null; + } + try { + Class classToAnalyze = obj.getClass(); + while (!Modifier.isPublic(classToAnalyze.getModifiers())) { + classToAnalyze = classToAnalyze.getSuperclass(); + if (classToAnalyze == null) { + // No public provider class found -> fall back to given object. + return obj; + } + } + Method getInnermostDelegate = classToAnalyze.getMethod(GET_INNERMOST_DELEGATE_METHOD_NAME, (Class[]) null); + Object delegate = ReflectionUtils.invokeJdbcMethod(getInnermostDelegate, obj); + return (delegate != null ? delegate : obj); + } + catch (NoSuchMethodException ex) { + return obj; + } + catch (SecurityException ex) { + throw new IllegalStateException("Commons DBCP getInnermostDelegate method is not accessible: " + ex); + } + } + + + protected Connection doGetNativeConnection(Connection con) throws SQLException { + return (Connection) getInnermostDelegate(con); + } + + public Statement getNativeStatement(Statement stmt) throws SQLException { + return (Statement) getInnermostDelegate(stmt); + } + + public PreparedStatement getNativePreparedStatement(PreparedStatement ps) throws SQLException { + return (PreparedStatement) getNativeStatement(ps); + } + + public CallableStatement getNativeCallableStatement(CallableStatement cs) throws SQLException { + return (CallableStatement) getNativeStatement(cs); + } + + public ResultSet getNativeResultSet(ResultSet rs) throws SQLException { + return (ResultSet) getInnermostDelegate(rs); + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/nativejdbc/JBossNativeJdbcExtractor.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/nativejdbc/JBossNativeJdbcExtractor.java new file mode 100644 index 0000000000..db468a839f --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/nativejdbc/JBossNativeJdbcExtractor.java @@ -0,0 +1,137 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.support.nativejdbc; + +import java.lang.reflect.Method; +import java.sql.CallableStatement; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import org.springframework.util.ReflectionUtils; + +/** + * Implementation of the {@link NativeJdbcExtractor} interface for JBoss, + * supporting JBoss Application Server 3.2.4+. + * + *

Returns the underlying native Connection, Statement, etc to + * application code instead of JBoss' wrapper implementations. + * The returned JDBC classes can then safely be cast, e.g. to + * oracle.jdbc.OracleConnection. + * + *

This NativeJdbcExtractor can be set just to allow working with + * a JBoss connection pool: If a given object is not a JBoss wrapper, + * it will be returned as-is. + * + * @author Juergen Hoeller + * @since 03.01.2004 + * @see org.jboss.resource.adapter.jdbc.WrappedConnection#getUnderlyingConnection + * @see org.jboss.resource.adapter.jdbc.WrappedStatement#getUnderlyingStatement + * @see org.jboss.resource.adapter.jdbc.WrappedResultSet#getUnderlyingResultSet + */ +public class JBossNativeJdbcExtractor extends NativeJdbcExtractorAdapter { + + private static final String WRAPPED_CONNECTION_NAME = "org.jboss.resource.adapter.jdbc.WrappedConnection"; + + private static final String WRAPPED_STATEMENT_NAME = "org.jboss.resource.adapter.jdbc.WrappedStatement"; + + private static final String WRAPPED_RESULT_SET_NAME = "org.jboss.resource.adapter.jdbc.WrappedResultSet"; + + + private Class wrappedConnectionClass; + + private Class wrappedStatementClass; + + private Class wrappedResultSetClass; + + private Method getUnderlyingConnectionMethod; + + private Method getUnderlyingStatementMethod; + + private Method getUnderlyingResultSetMethod; + + + /** + * This constructor retrieves JBoss JDBC wrapper classes, + * so we can get the underlying vendor connection using reflection. + */ + public JBossNativeJdbcExtractor() { + try { + this.wrappedConnectionClass = getClass().getClassLoader().loadClass(WRAPPED_CONNECTION_NAME); + this.wrappedStatementClass = getClass().getClassLoader().loadClass(WRAPPED_STATEMENT_NAME); + this.wrappedResultSetClass = getClass().getClassLoader().loadClass(WRAPPED_RESULT_SET_NAME); + this.getUnderlyingConnectionMethod = + this.wrappedConnectionClass.getMethod("getUnderlyingConnection", (Class[]) null); + this.getUnderlyingStatementMethod = + this.wrappedStatementClass.getMethod("getUnderlyingStatement", (Class[]) null); + this.getUnderlyingResultSetMethod = + this.wrappedResultSetClass.getMethod("getUnderlyingResultSet", (Class[]) null); + } + catch (Exception ex) { + throw new IllegalStateException( + "Could not initialize JBossNativeJdbcExtractor because JBoss API classes are not available: " + ex); + } + } + + + /** + * Retrieve the Connection via JBoss' getUnderlyingConnection method. + */ + protected Connection doGetNativeConnection(Connection con) throws SQLException { + if (this.wrappedConnectionClass.isAssignableFrom(con.getClass())) { + return (Connection) ReflectionUtils.invokeJdbcMethod(this.getUnderlyingConnectionMethod, con); + } + return con; + } + + /** + * Retrieve the Connection via JBoss' getUnderlyingStatement method. + */ + public Statement getNativeStatement(Statement stmt) throws SQLException { + if (this.wrappedStatementClass.isAssignableFrom(stmt.getClass())) { + return (Statement) ReflectionUtils.invokeJdbcMethod(this.getUnderlyingStatementMethod, stmt); + } + return stmt; + } + + /** + * Retrieve the Connection via JBoss' getUnderlyingStatement method. + */ + public PreparedStatement getNativePreparedStatement(PreparedStatement ps) throws SQLException { + return (PreparedStatement) getNativeStatement(ps); + } + + /** + * Retrieve the Connection via JBoss' getUnderlyingStatement method. + */ + public CallableStatement getNativeCallableStatement(CallableStatement cs) throws SQLException { + return (CallableStatement) getNativeStatement(cs); + } + + /** + * Retrieve the Connection via JBoss' getUnderlyingResultSet method. + */ + public ResultSet getNativeResultSet(ResultSet rs) throws SQLException { + if (this.wrappedResultSetClass.isAssignableFrom(rs.getClass())) { + return (ResultSet) ReflectionUtils.invokeJdbcMethod(this.getUnderlyingResultSetMethod, rs); + } + return rs; + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/nativejdbc/Jdbc4NativeJdbcExtractor.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/nativejdbc/Jdbc4NativeJdbcExtractor.java new file mode 100644 index 0000000000..80cc76440e --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/nativejdbc/Jdbc4NativeJdbcExtractor.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc.support.nativejdbc; + +import java.sql.CallableStatement; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +/** + * {@link NativeJdbcExtractor} implementation that delegates to JDBC 4.0's + * unwrap method, as defined by {@link java.sql.Wrapper}. + * + *

Note: Only use this when actually running against a JDBC 4.0 driver, + * with a connection pool that supports the JDBC 4.0 API (i.e. at least accepts + * JDBC 4.0 API calls and passes them through to the underlying driver)! + * + * @author Juergen Hoeller + * @since 2.5 + * @see java.sql.Wrapper#unwrap + * @see SimpleNativeJdbcExtractor + * @see org.springframework.jdbc.core.JdbcTemplate#setNativeJdbcExtractor + * @see org.springframework.jdbc.support.lob.OracleLobHandler#setNativeJdbcExtractor + */ +public class Jdbc4NativeJdbcExtractor extends NativeJdbcExtractorAdapter { + + protected Connection doGetNativeConnection(Connection con) throws SQLException { + return (Connection) con.unwrap(Connection.class); + } + + public Statement getNativeStatement(Statement stmt) throws SQLException { + return (Statement) stmt.unwrap(Statement.class); + } + + public PreparedStatement getNativePreparedStatement(PreparedStatement ps) throws SQLException { + return (PreparedStatement) ps.unwrap(PreparedStatement.class); + } + + public CallableStatement getNativeCallableStatement(CallableStatement cs) throws SQLException { + return (CallableStatement) cs.unwrap(CallableStatement.class); + } + + public ResultSet getNativeResultSet(ResultSet rs) throws SQLException { + return (ResultSet) rs.unwrap(ResultSet.class); + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/nativejdbc/NativeJdbcExtractor.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/nativejdbc/NativeJdbcExtractor.java new file mode 100644 index 0000000000..08adb762c0 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/nativejdbc/NativeJdbcExtractor.java @@ -0,0 +1,169 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc.support.nativejdbc; + +import java.sql.CallableStatement; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +/** + * Interface for extracting native JDBC objects from wrapped objects coming from + * connection pools. This is necessary to allow for casting to native implementations + * like OracleConnection or OracleResultSet in application + * code, for example to create Blobs or to access vendor-specific features. + * + *

Note: Setting a custom NativeJdbcExtractor is just necessary + * if you intend to cast to database-specific implementations like + * OracleConnection or OracleResultSet. + * Otherwise, any wrapped JDBC object will be fine, with no need for unwrapping. + * + *

Note: To be able to support any pool's strategy of native ResultSet wrapping, + * it is advisable to get both the native Statement and the native ResultSet + * via this extractor. Some pools just allow to unwrap the Statement, some just to + * unwrap the ResultSet - the above strategy will cover both. It is typically + * not necessary to unwrap the Connection to retrieve a native ResultSet. + * + *

When working with a simple connection pool that wraps Connections but not + * Statements, a {@link SimpleNativeJdbcExtractor} is often sufficient. However, + * some pools (like Jakarta's Commons DBCP) wrap all JDBC objects that they + * return: Therefore, you need to use a specific NativeJdbcExtractor + * (like {@link CommonsDbcpNativeJdbcExtractor}) with them. + * + *

{@link org.springframework.jdbc.core.JdbcTemplate} can properly apply a + * NativeJdbcExtractor if specified, unwrapping all JDBC objects + * that it creates. Note that this is just necessary if you intend to cast to + * native implementations in your data access code. + * + *

{@link org.springframework.jdbc.support.lob.OracleLobHandler}, + * the Oracle-specific implementation of Spring's + * {@link org.springframework.jdbc.support.lob.LobHandler} interface, requires a + * NativeJdbcExtractor for obtaining the native OracleConnection. + * This is also necessary for other Oracle-specific features that you may want + * to leverage in your applications, such as Oracle InterMedia. + * + * @author Juergen Hoeller + * @since 25.08.2003 + * @see SimpleNativeJdbcExtractor + * @see CommonsDbcpNativeJdbcExtractor + * @see org.springframework.jdbc.core.JdbcTemplate#setNativeJdbcExtractor + * @see org.springframework.jdbc.support.lob.OracleLobHandler#setNativeJdbcExtractor + */ +public interface NativeJdbcExtractor { + + /** + * Return whether it is necessary to work on the native Connection to + * receive native Statements. + *

This should be true if the connection pool does not allow to extract + * the native JDBC objects from its Statement wrapper but supports a way + * to retrieve the native JDBC Connection. This way, applications can + * still receive native Statements and ResultSet via working on the + * native JDBC Connection. + */ + boolean isNativeConnectionNecessaryForNativeStatements(); + + /** + * Return whether it is necessary to work on the native Connection to + * receive native PreparedStatements. + *

This should be true if the connection pool does not allow to extract + * the native JDBC objects from its PreparedStatement wrappers but + * supports a way to retrieve the native JDBC Connection. This way, + * applications can still receive native Statements and ResultSet via + * working on the native JDBC Connection. + */ + boolean isNativeConnectionNecessaryForNativePreparedStatements(); + + /** + * Return whether it is necessary to work on the native Connection to + * receive native CallableStatements. + *

This should be true if the connection pool does not allow to extract + * the native JDBC objects from its CallableStatement wrappers but + * supports a way to retrieve the native JDBC Connection. This way, + * applications can still receive native Statements and ResultSet via + * working on the native JDBC Connection. + */ + boolean isNativeConnectionNecessaryForNativeCallableStatements(); + + /** + * Retrieve the underlying native JDBC Connection for the given Connection. + * Supposed to return the given Connection if not capable of unwrapping. + * @param con the Connection handle, potentially wrapped by a connection pool + * @return the underlying native JDBC Connection, if possible; + * else, the original Connection + * @throws SQLException if thrown by JDBC methods + */ + Connection getNativeConnection(Connection con) throws SQLException; + + /** + * Retrieve the underlying native JDBC Connection for the given Statement. + * Supposed to return the Statement.getConnection() if not + * capable of unwrapping. + *

Having this extra method allows for more efficient unwrapping if data + * access code already has a Statement. Statement.getConnection() + * often returns the native JDBC Connection even if the Statement itself + * is wrapped by a pool. + * @param stmt the Statement handle, potentially wrapped by a connection pool + * @return the underlying native JDBC Connection, if possible; + * else, the original Connection + * @throws SQLException if thrown by JDBC methods + * @see java.sql.Statement#getConnection() + */ + Connection getNativeConnectionFromStatement(Statement stmt) throws SQLException; + + /** + * Retrieve the underlying native JDBC Statement for the given Statement. + * Supposed to return the given Statement if not capable of unwrapping. + * @param stmt the Statement handle, potentially wrapped by a connection pool + * @return the underlying native JDBC Statement, if possible; + * else, the original Statement + * @throws SQLException if thrown by JDBC methods + */ + Statement getNativeStatement(Statement stmt) throws SQLException; + + /** + * Retrieve the underlying native JDBC PreparedStatement for the given statement. + * Supposed to return the given PreparedStatement if not capable of unwrapping. + * @param ps the PreparedStatement handle, potentially wrapped by a connection pool + * @return the underlying native JDBC PreparedStatement, if possible; + * else, the original PreparedStatement + * @throws SQLException if thrown by JDBC methods + */ + PreparedStatement getNativePreparedStatement(PreparedStatement ps) throws SQLException; + + /** + * Retrieve the underlying native JDBC CallableStatement for the given statement. + * Supposed to return the given CallableStatement if not capable of unwrapping. + * @param cs the CallableStatement handle, potentially wrapped by a connection pool + * @return the underlying native JDBC CallableStatement, if possible; + * else, the original CallableStatement + * @throws SQLException if thrown by JDBC methods + */ + CallableStatement getNativeCallableStatement(CallableStatement cs) throws SQLException; + + /** + * Retrieve the underlying native JDBC ResultSet for the given statement. + * Supposed to return the given ResultSet if not capable of unwrapping. + * @param rs the ResultSet handle, potentially wrapped by a connection pool + * @return the underlying native JDBC ResultSet, if possible; + * else, the original ResultSet + * @throws SQLException if thrown by JDBC methods + */ + ResultSet getNativeResultSet(ResultSet rs) throws SQLException; + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/nativejdbc/NativeJdbcExtractorAdapter.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/nativejdbc/NativeJdbcExtractorAdapter.java new file mode 100644 index 0000000000..659b171fbd --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/nativejdbc/NativeJdbcExtractorAdapter.java @@ -0,0 +1,166 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.support.nativejdbc; + +import java.sql.CallableStatement; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import org.springframework.jdbc.datasource.DataSourceUtils; + +/** + * Abstract adapter class for the {@link NativeJdbcExtractor} interface, + * for simplified implementation of basic extractors. + * Basically returns the passed-in JDBC objects on all methods. + * + *

getNativeConnection checks for a ConnectionProxy chain, + * for example from a TransactionAwareDataSourceProxy, before delegating to + * doGetNativeConnection for actual unwrapping. You can override + * either of the two for a specific connection pool, but the latter is + * recommended to participate in ConnectionProxy unwrapping. + * + *

getNativeConnection also applies a fallback if the first + * native extraction process failed, that is, returned the same Connection as + * passed in. It assumes that some additional proxying is going in this case: + * Hence, it retrieves the underlying native Connection from the DatabaseMetaData + * via conHandle.getMetaData().getConnection() and retries the native + * extraction process based on that Connection handle. This works, for example, + * for the Connection proxies exposed by Hibernate 3.1's Session.connection(). + * + *

The getNativeConnectionFromStatement method is implemented + * to simply delegate to getNativeConnection with the Statement's + * Connection. This is what most extractor implementations will stick to, + * unless there's a more efficient version for a specific pool. + * + * @author Juergen Hoeller + * @since 1.1 + * @see #getNativeConnection + * @see #getNativeConnectionFromStatement + * @see org.springframework.jdbc.datasource.ConnectionProxy + */ +public abstract class NativeJdbcExtractorAdapter implements NativeJdbcExtractor { + + /** + * Return false by default. + */ + public boolean isNativeConnectionNecessaryForNativeStatements() { + return false; + } + + /** + * Return false by default. + */ + public boolean isNativeConnectionNecessaryForNativePreparedStatements() { + return false; + } + + /** + * Return false by default. + */ + public boolean isNativeConnectionNecessaryForNativeCallableStatements() { + return false; + } + + /** + * Check for a ConnectionProxy chain, then delegate to doGetNativeConnection. + *

ConnectionProxy is used by Spring's TransactionAwareDataSourceProxy + * and LazyConnectionDataSourceProxy. The target connection behind it is + * typically one from a local connection pool, to be unwrapped by the + * doGetNativeConnection implementation of a concrete subclass. + * @see #doGetNativeConnection + * @see org.springframework.jdbc.datasource.ConnectionProxy + * @see org.springframework.jdbc.datasource.DataSourceUtils#getTargetConnection + * @see org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy + * @see org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy + */ + public Connection getNativeConnection(Connection con) throws SQLException { + if (con == null) { + return null; + } + Connection targetCon = DataSourceUtils.getTargetConnection(con); + Connection nativeCon = doGetNativeConnection(targetCon); + if (nativeCon == targetCon) { + // We haven't received a different Connection, so we'll assume that there's + // some additional proxying going on. Let's check whether we get something + // different back from the DatabaseMetaData.getConnection() call. + DatabaseMetaData metaData = targetCon.getMetaData(); + // The following check is only really there for mock Connections + // which might not carry a DatabaseMetaData instance. + if (metaData != null) { + Connection metaCon = metaData.getConnection(); + if (metaCon != null && metaCon != targetCon) { + // We've received a different Connection there: + // Let's retry the native extraction process with it. + nativeCon = doGetNativeConnection(metaCon); + } + } + } + return nativeCon; + } + + /** + * Not able to unwrap: return passed-in Connection. + */ + protected Connection doGetNativeConnection(Connection con) throws SQLException { + return con; + } + + /** + * Retrieve the Connection via the Statement's Connection. + * @see #getNativeConnection + * @see Statement#getConnection + */ + public Connection getNativeConnectionFromStatement(Statement stmt) throws SQLException { + if (stmt == null) { + return null; + } + return getNativeConnection(stmt.getConnection()); + } + + /** + * Not able to unwrap: return passed-in Statement. + */ + public Statement getNativeStatement(Statement stmt) throws SQLException { + return stmt; + } + + /** + * Not able to unwrap: return passed-in PreparedStatement. + */ + public PreparedStatement getNativePreparedStatement(PreparedStatement ps) throws SQLException { + return ps; + } + + /** + * Not able to unwrap: return passed-in CallableStatement. + */ + public CallableStatement getNativeCallableStatement(CallableStatement cs) throws SQLException { + return cs; + } + + /** + * Not able to unwrap: return passed-in ResultSet. + */ + public ResultSet getNativeResultSet(ResultSet rs) throws SQLException { + return rs; + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/nativejdbc/SimpleNativeJdbcExtractor.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/nativejdbc/SimpleNativeJdbcExtractor.java new file mode 100644 index 0000000000..cb59036b83 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/nativejdbc/SimpleNativeJdbcExtractor.java @@ -0,0 +1,141 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc.support.nativejdbc; + +/** + * Simple implementation of the {@link NativeJdbcExtractor} interface. + * Assumes a pool that wraps Connection handles but not DatabaseMetaData: + * In this case, the underlying native Connection can be retrieved by simply + * calling conHandle.getMetaData().getConnection(). + * All other JDBC objects will be returned as passed in. + * + *

This extractor should work with any pool that does not wrap DatabaseMetaData, + * and will also work with any plain JDBC driver. Note that a pool can still wrap + * Statements, PreparedStatements, etc: The only requirement of this extractor is + * that java.sql.DatabaseMetaData does not get wrapped, returning the + * native Connection of the JDBC driver on metaData.getConnection(). + * + *

Customize this extractor by setting the "nativeConnectionNecessaryForXxx" + * flags accordingly: If Statements, PreparedStatements, and/or CallableStatements + * are wrapped by your pool, set the corresponding "nativeConnectionNecessaryForXxx" + * flags to "true". If none of the statement types is wrapped - or you solely need + * Connection unwrapping in the first place -, the defaults are fine. + * + *

SimpleNativeJdbcExtractor is a common choice for use with OracleLobHandler, + * which just needs Connection unwrapping via the + * {@link #getNativeConnectionFromStatement} method. This usage will work + * with almost any connection pool. Known to work are, for example: + *

    + *
  • Caucho Resin 2.1.x, 3.0.x + *
  • Sun Java System Application Server 8 + *
  • Oracle OC4J 9.0.3, 9.0.4 + *
  • C3P0 0.8.x + *
  • Jakarta Commons DBCP 1.0, 1.1, 1.2 (used in Tomcat 4.1.x, 5.0.x) + *
  • JBoss 3.2.x + *
+ * + *

For full usage with JdbcTemplate, i.e. to also provide Statement unwrapping: + *

    + *
  • Use a default SimpleNativeJdbcExtractor for Resin and SJSAS (no JDBC + * Statement objects are wrapped, therefore no special unwrapping is necessary). + *
  • Use a SimpleNativeJdbcExtractor with all "nativeConnectionNecessaryForXxx" + * flags set to "true" for OC4J and C3P0 (all JDBC Statement objects are wrapped, + * but none of the wrappers allow for unwrapping). + *
  • Use a CommonsDbcpNativeJdbcExtractor for Jakarta Commons DBCP respectively + * a JBossNativeJdbcExtractor for JBoss (all JDBC Statement objects are wrapped, + * but all of them can be extracted by casting to implementation classes). + *
+ * + * @author Juergen Hoeller + * @since 05.12.2003 + * @see #setNativeConnectionNecessaryForNativeStatements + * @see #setNativeConnectionNecessaryForNativePreparedStatements + * @see #setNativeConnectionNecessaryForNativeCallableStatements + * @see Jdbc4NativeJdbcExtractor + * @see org.springframework.jdbc.core.JdbcTemplate#setNativeJdbcExtractor + * @see org.springframework.jdbc.support.lob.OracleLobHandler#setNativeJdbcExtractor + */ +public class SimpleNativeJdbcExtractor extends NativeJdbcExtractorAdapter { + + private boolean nativeConnectionNecessaryForNativeStatements = false; + + private boolean nativeConnectionNecessaryForNativePreparedStatements = false; + + private boolean nativeConnectionNecessaryForNativeCallableStatements = false; + + + /** + * Set whether it is necessary to work on the native Connection to + * receive native Statements. Default is "false". If true, the Connection + * will be unwrapped first to create a Statement. + *

This makes sense if you need to work with native Statements from + * a pool that does not allow to extract the native JDBC objects from its + * wrappers but returns the native Connection on DatabaseMetaData.getConnection. + *

The standard SimpleNativeJdbcExtractor is unable to unwrap statements, + * so set this to true if your connection pool wraps Statements. + * @see java.sql.Connection#createStatement + * @see java.sql.DatabaseMetaData#getConnection + */ + public void setNativeConnectionNecessaryForNativeStatements(boolean nativeConnectionNecessaryForNativeStatements) { + this.nativeConnectionNecessaryForNativeStatements = nativeConnectionNecessaryForNativeStatements; + } + + public boolean isNativeConnectionNecessaryForNativeStatements() { + return this.nativeConnectionNecessaryForNativeStatements; + } + + /** + * Set whether it is necessary to work on the native Connection to + * receive native PreparedStatements. Default is "false". If true, + * the Connection will be unwrapped first to create a PreparedStatement. + *

This makes sense if you need to work with native PreparedStatements from + * a pool that does not allow to extract the native JDBC objects from its + * wrappers but returns the native Connection on Statement.getConnection. + *

The standard SimpleNativeJdbcExtractor is unable to unwrap statements, + * so set this to true if your connection pool wraps PreparedStatements. + * @see java.sql.Connection#prepareStatement + * @see java.sql.DatabaseMetaData#getConnection + */ + public void setNativeConnectionNecessaryForNativePreparedStatements(boolean nativeConnectionNecessary) { + this.nativeConnectionNecessaryForNativePreparedStatements = nativeConnectionNecessary; + } + + public boolean isNativeConnectionNecessaryForNativePreparedStatements() { + return this.nativeConnectionNecessaryForNativePreparedStatements; + } + + /** + * Set whether it is necessary to work on the native Connection to + * receive native CallableStatements. Default is "false". If true, + * the Connection will be unwrapped first to create a CallableStatement. + *

This makes sense if you need to work with native CallableStatements from + * a pool that does not allow to extract the native JDBC objects from its + * wrappers but returns the native Connection on Statement.getConnection. + *

The standard SimpleNativeJdbcExtractor is unable to unwrap statements, + * so set this to true if your connection pool wraps CallableStatements. + * @see java.sql.Connection#prepareCall + * @see java.sql.DatabaseMetaData#getConnection + */ + public void setNativeConnectionNecessaryForNativeCallableStatements(boolean nativeConnectionNecessary) { + this.nativeConnectionNecessaryForNativeCallableStatements = nativeConnectionNecessary; + } + + public boolean isNativeConnectionNecessaryForNativeCallableStatements() { + return this.nativeConnectionNecessaryForNativeCallableStatements; + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/nativejdbc/WebLogicNativeJdbcExtractor.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/nativejdbc/WebLogicNativeJdbcExtractor.java new file mode 100644 index 0000000000..728e4bc461 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/nativejdbc/WebLogicNativeJdbcExtractor.java @@ -0,0 +1,101 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.support.nativejdbc; + +import java.lang.reflect.Method; +import java.sql.Connection; +import java.sql.SQLException; + +import org.springframework.util.ReflectionUtils; + +/** + * Implementation of the {@link NativeJdbcExtractor} interface for WebLogic, + * supporting WebLogic Server 8.1 and higher. + * + *

Returns the underlying native Connection to application code instead + * of WebLogic's wrapper implementation; unwraps the Connection for native + * statements. The returned JDBC classes can then safely be cast, e.g. to + * oracle.jdbc.OracleConnection. + * + *

This NativeJdbcExtractor can be set just to allow working + * with a WebLogic DataSource: If a given object is not a WebLogic + * Connection wrapper, it will be returned as-is. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @since 1.0.2 + * @see #getNativeConnection + * @see weblogic.jdbc.extensions.WLConnection#getVendorConnection + */ +public class WebLogicNativeJdbcExtractor extends NativeJdbcExtractorAdapter { + + private static final String JDBC_EXTENSION_NAME = "weblogic.jdbc.extensions.WLConnection"; + + + private final Class jdbcExtensionClass; + + private final Method getVendorConnectionMethod; + + + /** + * This constructor retrieves the WebLogic JDBC extension interface, + * so we can get the underlying vendor connection using reflection. + */ + public WebLogicNativeJdbcExtractor() { + try { + this.jdbcExtensionClass = getClass().getClassLoader().loadClass(JDBC_EXTENSION_NAME); + this.getVendorConnectionMethod = this.jdbcExtensionClass.getMethod("getVendorConnection", (Class[]) null); + } + catch (Exception ex) { + throw new IllegalStateException( + "Could not initialize WebLogicNativeJdbcExtractor because WebLogic API classes are not available: " + ex); + } + } + + + /** + * Return true, as WebLogic returns wrapped Statements. + */ + public boolean isNativeConnectionNecessaryForNativeStatements() { + return true; + } + + /** + * Return true, as WebLogic returns wrapped PreparedStatements. + */ + public boolean isNativeConnectionNecessaryForNativePreparedStatements() { + return true; + } + + /** + * Return true, as WebLogic returns wrapped CallableStatements. + */ + public boolean isNativeConnectionNecessaryForNativeCallableStatements() { + return true; + } + + /** + * Retrieve the Connection via WebLogic's getVendorConnection method. + */ + protected Connection doGetNativeConnection(Connection con) throws SQLException { + if (this.jdbcExtensionClass.isAssignableFrom(con.getClass())) { + return (Connection) ReflectionUtils.invokeJdbcMethod(this.getVendorConnectionMethod, con); + } + return con; + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/nativejdbc/WebSphereNativeJdbcExtractor.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/nativejdbc/WebSphereNativeJdbcExtractor.java new file mode 100644 index 0000000000..b631f303d9 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/nativejdbc/WebSphereNativeJdbcExtractor.java @@ -0,0 +1,105 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.support.nativejdbc; + +import java.lang.reflect.Method; +import java.sql.Connection; +import java.sql.SQLException; + +import org.springframework.util.ReflectionUtils; + +/** + * Implementation of the {@link NativeJdbcExtractor} interface for WebSphere, + * supporting WebSphere Application Server 5.1 and higher. + * + *

Returns the underlying native Connection to application code instead + * of WebSphere's wrapper implementation; unwraps the Connection for + * native statements. The returned JDBC classes can then safely be cast, + * e.g. to oracle.jdbc.OracleConnection. + * + *

This NativeJdbcExtractor can be set just to allow working + * with a WebSphere DataSource: If a given object is not a WebSphere + * Connection wrapper, it will be returned as-is. + * + * @author Juergen Hoeller + * @since 1.1 + * @see com.ibm.ws.rsadapter.jdbc.WSJdbcConnection + * @see com.ibm.ws.rsadapter.jdbc.WSJdbcUtil#getNativeConnection + */ +public class WebSphereNativeJdbcExtractor extends NativeJdbcExtractorAdapter { + + private static final String JDBC_ADAPTER_CONNECTION_NAME_5 = "com.ibm.ws.rsadapter.jdbc.WSJdbcConnection"; + + private static final String JDBC_ADAPTER_UTIL_NAME_5 = "com.ibm.ws.rsadapter.jdbc.WSJdbcUtil"; + + + private Class webSphere5ConnectionClass; + + private Method webSphere5NativeConnectionMethod; + + + /** + * This constructor retrieves WebSphere JDBC adapter classes, + * so we can get the underlying vendor connection using reflection. + */ + public WebSphereNativeJdbcExtractor() { + try { + this.webSphere5ConnectionClass = getClass().getClassLoader().loadClass(JDBC_ADAPTER_CONNECTION_NAME_5); + Class jdbcAdapterUtilClass = getClass().getClassLoader().loadClass(JDBC_ADAPTER_UTIL_NAME_5); + this.webSphere5NativeConnectionMethod = + jdbcAdapterUtilClass.getMethod("getNativeConnection", new Class[] {this.webSphere5ConnectionClass}); + } + catch (Exception ex) { + throw new IllegalStateException( + "Could not initialize WebSphereNativeJdbcExtractor because WebSphere API classes are not available: " + ex); + } + } + + + /** + * Return true, as WebSphere returns wrapped Statements. + */ + public boolean isNativeConnectionNecessaryForNativeStatements() { + return true; + } + + /** + * Return true, as WebSphere returns wrapped PreparedStatements. + */ + public boolean isNativeConnectionNecessaryForNativePreparedStatements() { + return true; + } + + /** + * Return true, as WebSphere returns wrapped CallableStatements. + */ + public boolean isNativeConnectionNecessaryForNativeCallableStatements() { + return true; + } + + /** + * Retrieve the Connection via WebSphere's getNativeConnection method. + */ + protected Connection doGetNativeConnection(Connection con) throws SQLException { + if (this.webSphere5ConnectionClass.isAssignableFrom(con.getClass())) { + return (Connection) ReflectionUtils.invokeJdbcMethod( + this.webSphere5NativeConnectionMethod, null, new Object[] {con}); + } + return con; + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/nativejdbc/XAPoolNativeJdbcExtractor.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/nativejdbc/XAPoolNativeJdbcExtractor.java new file mode 100644 index 0000000000..e1284165fe --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/nativejdbc/XAPoolNativeJdbcExtractor.java @@ -0,0 +1,75 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.jdbc.support.nativejdbc; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +import org.enhydra.jdbc.core.CoreConnection; +import org.enhydra.jdbc.core.CorePreparedStatement; + +/** + * Implementation of the {@link NativeJdbcExtractor} interface for + * ObjectWeb's XAPool, as shipped with JOTM and used in JOnAS. + * + *

Returns underlying native Connections and native PreparedStatements to + * application code instead of XAPool's wrapper implementations; unwraps the + * Connection for native Statements and native CallableStatements. + * The returned JDBC classes can then safely be cast, e.g. to + * oracle.jdbc.OracleConnection. + * + *

This NativeJdbcExtractor can be set just to allow working with + * an XAPool DataSource: If a given object is not an XAPool wrapper, it will + * be returned as-is. + * + * @author Juergen Hoeller + * @since 06.02.2004 + */ +public class XAPoolNativeJdbcExtractor extends NativeJdbcExtractorAdapter { + + /** + * Return true, as CoreStatement does not allow access to the + * underlying Connection. + */ + public boolean isNativeConnectionNecessaryForNativeStatements() { + return true; + } + + /** + * Return true, as CoreCallableStatement does not allow access to the + * underlying Connection. + */ + public boolean isNativeConnectionNecessaryForNativeCallableStatements() { + return true; + } + + protected Connection doGetNativeConnection(Connection con) throws SQLException { + if (con instanceof CoreConnection) { + return ((CoreConnection) con).con; + } + return con; + } + + public PreparedStatement getNativePreparedStatement(PreparedStatement ps) throws SQLException { + if (ps instanceof CorePreparedStatement) { + return ((CorePreparedStatement) ps).ps; + } + return ps; + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/nativejdbc/package.html b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/nativejdbc/package.html new file mode 100644 index 0000000000..409d0a1cf1 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/nativejdbc/package.html @@ -0,0 +1,10 @@ + + + +Provides a mechanism for extracting native implementations of JDBC +interfaces from wrapper objects that got returned from connection pools. + +

Can be used independently, for example in custom JDBC access code. + + + diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/package.html b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/package.html new file mode 100644 index 0000000000..ecdc7b151c --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/package.html @@ -0,0 +1,12 @@ + + + +Support classes for the JDBC framework, used by the classes in the +jdbc.core and jdbc.object packages. Provides a translator from +SQLExceptions Spring's generic DataAccessExceptions. + +

Can be used independently, for example in custom JDBC access code, +or in JDBC-based O/R mapping layers. + + + diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/rowset/ResultSetWrappingSqlRowSet.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/rowset/ResultSetWrappingSqlRowSet.java new file mode 100644 index 0000000000..c60c553a6d --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/rowset/ResultSetWrappingSqlRowSet.java @@ -0,0 +1,693 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.support.rowset; + +import java.math.BigDecimal; +import java.sql.Date; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Time; +import java.sql.Timestamp; +import java.util.Calendar; +import java.util.Map; + +import org.springframework.jdbc.InvalidResultSetAccessException; + +/** + * Default implementation of Spring's {@link SqlRowSet} interface. + * + *

This implementation wraps a javax.sql.ResultSet, + * catching any SQLExceptions and translating them to the + * appropriate Spring {@link InvalidResultSetAccessException}. + * + *

The passed-in ResultSets should already be disconnected if the SqlRowSet + * is supposed to be usable in a disconnected fashion. This means that + * you will usually pass in a javax.sql.rowset.CachedRowSet, + * which implements the ResultSet interface. + * + *

Note: This class implements the java.io.Serializable + * marker interface through the SqlRowSet interface, but is only actually + * serializable if the disconnected ResultSet/RowSet contained in it is + * serializable. Most CachedRowSet implementations are actually serializable. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @since 1.2 + * @see java.sql.ResultSet + * @see javax.sql.rowset.CachedRowSet + */ +public class ResultSetWrappingSqlRowSet implements SqlRowSet { + + /** use serialVersionUID from Spring 1.2 for interoperability */ + private static final long serialVersionUID = -4688694393146734764L; + + + private final ResultSet resultSet; + + private final SqlRowSetMetaData rowSetMetaData; + + + /** + * Create a new ResultSetWrappingSqlRowSet for the given ResultSet. + * @param resultSet a disconnected ResultSet to wrap + * (usually a javax.sql.rowset.CachedRowSet) + * @throws InvalidResultSetAccessException if extracting + * the ResultSetMetaData failed + * @see javax.sql.rowset.CachedRowSet + * @see java.sql.ResultSet#getMetaData + * @see ResultSetWrappingSqlRowSetMetaData + */ + public ResultSetWrappingSqlRowSet(ResultSet resultSet) throws InvalidResultSetAccessException { + this.resultSet = resultSet; + try { + this.rowSetMetaData = new ResultSetWrappingSqlRowSetMetaData(resultSet.getMetaData()); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + + /** + * Return the underlying ResultSet + * (usually a javax.sql.rowset.CachedRowSet). + * @see javax.sql.rowset.CachedRowSet + */ + public final ResultSet getResultSet() { + return this.resultSet; + } + + /** + * @see java.sql.ResultSetMetaData#getCatalogName(int) + */ + public final SqlRowSetMetaData getMetaData() { + return this.rowSetMetaData; + } + + /** + * @see java.sql.ResultSet#findColumn(String) + */ + public int findColumn(String columnName) throws InvalidResultSetAccessException { + try { + return this.resultSet.findColumn(columnName); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + + // RowSet methods for extracting data values + + /** + * @see java.sql.ResultSet#getBigDecimal(int) + */ + public BigDecimal getBigDecimal(int columnIndex) throws InvalidResultSetAccessException { + try { + return this.resultSet.getBigDecimal(columnIndex); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#getBigDecimal(String) + */ + public BigDecimal getBigDecimal(String columnName) throws InvalidResultSetAccessException { + try { + return this.resultSet.getBigDecimal(columnName); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#getBoolean(int) + */ + public boolean getBoolean(int columnIndex) throws InvalidResultSetAccessException { + try { + return this.resultSet.getBoolean(columnIndex); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#getBoolean(String) + */ + public boolean getBoolean(String columnName) throws InvalidResultSetAccessException { + try { + return this.resultSet.getBoolean(columnName); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#getByte(int) + */ + public byte getByte(int columnIndex) throws InvalidResultSetAccessException { + try { + return this.resultSet.getByte(columnIndex); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#getByte(String) + */ + public byte getByte(String columnName) throws InvalidResultSetAccessException { + try { + return this.resultSet.getByte(columnName); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#getDate(int, java.util.Calendar) + */ + public Date getDate(int columnIndex, Calendar cal) throws InvalidResultSetAccessException { + try { + return this.resultSet.getDate(columnIndex, cal); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#getDate(int) + */ + public Date getDate(int columnIndex) throws InvalidResultSetAccessException { + try { + return this.resultSet.getDate(columnIndex); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + /** + * @see java.sql.ResultSet#getDate(String, java.util.Calendar) + */ + public Date getDate(String columnName, Calendar cal) throws InvalidResultSetAccessException { + try { + return this.resultSet.getDate(columnName, cal); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#getDate(String) + */ + public Date getDate(String columnName) throws InvalidResultSetAccessException { + try { + return this.resultSet.getDate(columnName); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#getDouble(int) + */ + public double getDouble(int columnIndex) throws InvalidResultSetAccessException { + try { + return this.resultSet.getDouble(columnIndex); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#getDouble(String) + */ + public double getDouble(String columnName) throws InvalidResultSetAccessException { + try { + return this.resultSet.getDouble(columnName); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#getFloat(int) + */ + public float getFloat(int columnIndex) throws InvalidResultSetAccessException { + try { + return this.resultSet.getFloat(columnIndex); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#getFloat(String) + */ + public float getFloat(String columnName) throws InvalidResultSetAccessException { + try { + return this.resultSet.getFloat(columnName); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + /** + * @see java.sql.ResultSet#getInt(int) + */ + public int getInt(int columnIndex) throws InvalidResultSetAccessException { + try { + return this.resultSet.getInt(columnIndex); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#getInt(String) + */ + public int getInt(String columnName) throws InvalidResultSetAccessException { + try { + return this.resultSet.getInt(columnName); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#getLong(int) + */ + public long getLong(int columnIndex) throws InvalidResultSetAccessException { + try { + return this.resultSet.getLong(columnIndex); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#getLong(String) + */ + public long getLong(String columnName) throws InvalidResultSetAccessException { + try { + return this.resultSet.getLong(columnName); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#getObject(int, java.util.Map) + */ + public Object getObject(int i, Map map) throws InvalidResultSetAccessException { + try { + return this.resultSet.getObject(i, map); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#getObject(int) + */ + public Object getObject(int columnIndex) throws InvalidResultSetAccessException { + try { + return this.resultSet.getObject(columnIndex); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#getObject(String, java.util.Map) + */ + public Object getObject(String columnName, Map map) throws InvalidResultSetAccessException { + try { + return this.resultSet.getObject(columnName, map); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#getObject(String) + */ + public Object getObject(String columnName) throws InvalidResultSetAccessException { + try { + return this.resultSet.getObject(columnName); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#getShort(int) + */ + public short getShort(int columnIndex) throws InvalidResultSetAccessException { + try { + return this.resultSet.getShort(columnIndex); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#getShort(String) + */ + public short getShort(String columnName) throws InvalidResultSetAccessException { + try { + return this.resultSet.getShort(columnName); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#getString(int) + */ + public String getString(int columnIndex) throws InvalidResultSetAccessException { + try { + return this.resultSet.getString(columnIndex); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#getString(String) + */ + public String getString(String columnName) throws InvalidResultSetAccessException { + try { + return this.resultSet.getString(columnName); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#getTime(int, java.util.Calendar) + */ + public Time getTime(int columnIndex, Calendar cal) throws InvalidResultSetAccessException { + try { + return this.resultSet.getTime(columnIndex, cal); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#getTime(int) + */ + public Time getTime(int columnIndex) throws InvalidResultSetAccessException { + try { + return this.resultSet.getTime(columnIndex); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#getTime(String, java.util.Calendar) + */ + public Time getTime(String columnName, Calendar cal) throws InvalidResultSetAccessException { + try { + return this.resultSet.getTime(columnName, cal); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#getTime(String) + */ + public Time getTime(String columnName) throws InvalidResultSetAccessException { + try { + return this.resultSet.getTime(columnName); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#getTimestamp(int, java.util.Calendar) + */ + public Timestamp getTimestamp(int columnIndex, Calendar cal) throws InvalidResultSetAccessException { + try { + return this.resultSet.getTimestamp(columnIndex, cal); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#getTimestamp(int) + */ + public Timestamp getTimestamp(int columnIndex) throws InvalidResultSetAccessException { + try { + return this.resultSet.getTimestamp(columnIndex); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#getTimestamp(String, java.util.Calendar) + */ + public Timestamp getTimestamp(String columnName, Calendar cal) throws InvalidResultSetAccessException { + try { + return this.resultSet.getTimestamp(columnName, cal); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#getTimestamp(String) + */ + public Timestamp getTimestamp(String columnName) throws InvalidResultSetAccessException { + try { + return this.resultSet.getTimestamp(columnName); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + + // RowSet navigation methods + + /** + * @see java.sql.ResultSet#absolute(int) + */ + public boolean absolute(int row) throws InvalidResultSetAccessException { + try { + return this.resultSet.absolute(row); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#afterLast() + */ + public void afterLast() throws InvalidResultSetAccessException { + try { + this.resultSet.afterLast(); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#beforeFirst() + */ + public void beforeFirst() throws InvalidResultSetAccessException { + try { + this.resultSet.beforeFirst(); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#first() + */ + public boolean first() throws InvalidResultSetAccessException { + try { + return this.resultSet.first(); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#getRow() + */ + public int getRow() throws InvalidResultSetAccessException { + try { + return this.resultSet.getRow(); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#isAfterLast() + */ + public boolean isAfterLast() throws InvalidResultSetAccessException { + try { + return this.resultSet.isAfterLast(); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#isBeforeFirst() + */ + public boolean isBeforeFirst() throws InvalidResultSetAccessException { + try { + return this.resultSet.isBeforeFirst(); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#isFirst() + */ + public boolean isFirst() throws InvalidResultSetAccessException { + try { + return this.resultSet.isFirst(); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#isLast() + */ + public boolean isLast() throws InvalidResultSetAccessException { + try { + return this.resultSet.isLast(); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#last() + */ + public boolean last() throws InvalidResultSetAccessException { + try { + return this.resultSet.last(); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#next() + */ + public boolean next() throws InvalidResultSetAccessException { + try { + return this.resultSet.next(); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#previous() + */ + public boolean previous() throws InvalidResultSetAccessException { + try { + return this.resultSet.previous(); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#relative(int) + */ + public boolean relative(int rows) throws InvalidResultSetAccessException { + try { + return this.resultSet.relative(rows); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#wasNull() + */ + public boolean wasNull() throws InvalidResultSetAccessException { + try { + return this.resultSet.wasNull(); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/rowset/ResultSetWrappingSqlRowSetMetaData.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/rowset/ResultSetWrappingSqlRowSetMetaData.java new file mode 100644 index 0000000000..483ef36cc5 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/rowset/ResultSetWrappingSqlRowSetMetaData.java @@ -0,0 +1,203 @@ +/* + * Copyright 2002-2005 the original author or authors. + * + * Licensed 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.springframework.jdbc.support.rowset; + +import java.sql.ResultSetMetaData; +import java.sql.SQLException; + +import org.springframework.jdbc.InvalidResultSetAccessException; + +/** + * Default implementation of Spring's SqlRowSetMetaData interface. + * Used by ResultSetWrappingSqlRowSet. + * + *

This implementation wraps a javax.sql.ResultSetMetaData + * instance, catching any SQLExceptions and translating them to the + * appropriate Spring DataAccessException. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @since 1.2 + * @see ResultSetWrappingSqlRowSet#getMetaData + */ +public class ResultSetWrappingSqlRowSetMetaData implements SqlRowSetMetaData { + + private final ResultSetMetaData resultSetMetaData; + + private String[] columnNames; + + + /** + * Create a new ResultSetWrappingSqlRowSetMetaData object + * for the given ResultSetMetaData instance. + * @param resultSetMetaData a disconnected ResultSetMetaData instance + * to wrap (usually a javax.sql.RowSetMetaData instance) + * @see java.sql.ResultSet#getMetaData + * @see javax.sql.RowSetMetaData + * @see ResultSetWrappingSqlRowSet#getMetaData + */ + public ResultSetWrappingSqlRowSetMetaData(ResultSetMetaData resultSetMetaData) { + this.resultSetMetaData = resultSetMetaData; + } + + + public String getCatalogName(int column) throws InvalidResultSetAccessException { + try { + return this.resultSetMetaData.getCatalogName(column); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + public String getColumnClassName(int column) throws InvalidResultSetAccessException { + try { + return this.resultSetMetaData.getColumnClassName(column); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + public int getColumnCount() throws InvalidResultSetAccessException { + try { + return this.resultSetMetaData.getColumnCount(); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + public String[] getColumnNames() throws InvalidResultSetAccessException { + if (this.columnNames == null) { + this.columnNames = new String[getColumnCount()]; + for (int i = 0; i < getColumnCount(); i++) { + this.columnNames[i] = getColumnName(i + 1); + } + } + return this.columnNames; + } + + public int getColumnDisplaySize(int column) throws InvalidResultSetAccessException { + try { + return this.resultSetMetaData.getColumnDisplaySize(column); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + public String getColumnLabel(int column) throws InvalidResultSetAccessException { + try { + return this.resultSetMetaData.getColumnLabel(column); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + public String getColumnName(int column) throws InvalidResultSetAccessException { + try { + return this.resultSetMetaData.getColumnName(column); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + public int getColumnType(int column) throws InvalidResultSetAccessException { + try { + return this.resultSetMetaData.getColumnType(column); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + public String getColumnTypeName(int column) throws InvalidResultSetAccessException { + try { + return this.resultSetMetaData.getColumnTypeName(column); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + public int getPrecision(int column) throws InvalidResultSetAccessException { + try { + return this.resultSetMetaData.getPrecision(column); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + public int getScale(int column) throws InvalidResultSetAccessException { + try { + return this.resultSetMetaData.getScale(column); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + public String getSchemaName(int column) throws InvalidResultSetAccessException { + try { + return this.resultSetMetaData.getSchemaName(column); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + public String getTableName(int column) throws InvalidResultSetAccessException { + try { + return this.resultSetMetaData.getTableName(column); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + public boolean isCaseSensitive(int column) throws InvalidResultSetAccessException { + try { + return this.resultSetMetaData.isCaseSensitive(column); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + public boolean isCurrency(int column) throws InvalidResultSetAccessException { + try { + return this.resultSetMetaData.isCurrency(column); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + public boolean isSigned(int column) throws InvalidResultSetAccessException { + try { + return this.resultSetMetaData.isSigned(column); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/rowset/SqlRowSet.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/rowset/SqlRowSet.java new file mode 100644 index 0000000000..c6f8111ef9 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/rowset/SqlRowSet.java @@ -0,0 +1,489 @@ +/* + * Copyright 2002-2005 the original author or authors. + * + * Licensed 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.springframework.jdbc.support.rowset; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.sql.Date; +import java.sql.Time; +import java.sql.Timestamp; +import java.util.Calendar; +import java.util.Map; + +import org.springframework.jdbc.InvalidResultSetAccessException; + +/** + * Mirror interface for javax.sql.RowSet, representing + * disconnected java.sql.ResultSet data. + * + *

The main difference to the standard JDBC RowSet is that an SQLException + * is never thrown here. This allows a SqlRowSet to be used without having + * to deal with checked exceptions. A SqlRowSet will throw Spring's + * org.springframework.jdbc.InvalidResultSetAccessException + * instead (when appropriate). + * + *

Note: This interface extends the java.io.Serializable + * marker interface. Implementations, which typically hold disconnected data, + * are encouraged to be actually serializable (as far as possible). + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @since 1.2 + * @see javax.sql.RowSet + * @see java.sql.ResultSet + * @see org.springframework.jdbc.InvalidResultSetAccessException + * @see org.springframework.jdbc.core.JdbcTemplate#queryForRowSet + */ +public interface SqlRowSet extends Serializable { + + /** + * Retrieves the meta data (number, types and properties for the columns) + * of this row set. + * @return a corresponding SqlRowSetMetaData instance + * @see java.sql.ResultSet#getMetaData() + */ + SqlRowSetMetaData getMetaData(); + + /** + * Maps the given column name to its column index. + * @param columnName the name of the column + * @return the column index for the given column name + * @see java.sql.ResultSet#findColumn(String) + */ + int findColumn(String columnName) throws InvalidResultSetAccessException; + + + // RowSet methods for extracting data values + + /** + * Retrieves the value of the indicated column in the current row as + * an BigDecimal object. + * @param columnIndex the column index + * @return an BigDecimal object representing the column value + * @see java.sql.ResultSet#getBigDecimal(int) + */ + BigDecimal getBigDecimal(int columnIndex) throws InvalidResultSetAccessException; + + /** + * Retrieves the value of the indicated column in the current row as + * an BigDecimal object. + * @param columnName the column name + * @return an BigDecimal object representing the column value + * @see java.sql.ResultSet#getBigDecimal(java.lang.String) + */ + BigDecimal getBigDecimal(String columnName) throws InvalidResultSetAccessException; + + /** + * Retrieves the value of the indicated column in the current row as + * a boolean. + * @param columnIndex the column index + * @return a boolean representing the column value + * @see java.sql.ResultSet#getBoolean(int) + */ + boolean getBoolean(int columnIndex) throws InvalidResultSetAccessException; + + /** + * Retrieves the value of the indicated column in the current row as + * a boolean. + * @param columnName the column name + * @return a boolean representing the column value + * @see java.sql.ResultSet#getBoolean(java.lang.String) + */ + boolean getBoolean(String columnName) throws InvalidResultSetAccessException; + + /** + * Retrieves the value of the indicated column in the current row as + * a byte. + * @param columnIndex the column index + * @return a byte representing the column value + * @see java.sql.ResultSet#getByte(int) + */ + byte getByte(int columnIndex) throws InvalidResultSetAccessException; + + /** + * Retrieves the value of the indicated column in the current row as + * a byte. + * @param columnName the column name + * @return a byte representing the column value + * @see java.sql.ResultSet#getByte(java.lang.String) + */ + byte getByte(String columnName) throws InvalidResultSetAccessException; + + /** + * Retrieves the value of the indicated column in the current row as + * a Date object. + * @param columnIndex the column index + * @param cal the Calendar to use in constructing the Date + * @return a Date object representing the column value + * @see java.sql.ResultSet#getDate(int, java.util.Calendar) + */ + Date getDate(int columnIndex, Calendar cal) throws InvalidResultSetAccessException; + + /** + * Retrieves the value of the indicated column in the current row as + * a Date object. + * @param columnIndex the column index + * @return a Date object representing the column value + * @see java.sql.ResultSet#getDate(int) + */ + Date getDate(int columnIndex) throws InvalidResultSetAccessException; + + /** + * Retrieves the value of the indicated column in the current row as + * a Date object. + * @param columnName the column name + * @param cal the Calendar to use in constructing the Date + * @return a Date object representing the column value + * @see java.sql.ResultSet#getDate(java.lang.String, java.util.Calendar) + */ + Date getDate(String columnName, Calendar cal) throws InvalidResultSetAccessException; + + /** + * Retrieves the value of the indicated column in the current row as + * a Date object. + * @param columnName the column name + * @return a Date object representing the column value + * @see java.sql.ResultSet#getDate(java.lang.String) + */ + Date getDate(String columnName) throws InvalidResultSetAccessException; + + /** + * Retrieves the value of the indicated column in the current row as + * a Double object. + * @param columnIndex the column index + * @return a Double object representing the column value + * @see java.sql.ResultSet#getDouble(int) + */ + double getDouble(int columnIndex) throws InvalidResultSetAccessException; + + /** + * Retrieves the value of the indicated column in the current row as + * a Double object. + * @param columnName the column name + * @return a Double object representing the column value + * @see java.sql.ResultSet#getDouble(java.lang.String) + */ + double getDouble(String columnName) throws InvalidResultSetAccessException; + + /** + * Retrieves the value of the indicated column in the current row as + * a float. + * @param columnIndex the column index + * @return a float representing the column value + * @see java.sql.ResultSet#getFloat(int) + */ + float getFloat(int columnIndex) throws InvalidResultSetAccessException; + + /** + * Retrieves the value of the indicated column in the current row as + * a float. + * @param columnName the column name + * @return a float representing the column value + * @see java.sql.ResultSet#getFloat(java.lang.String) + */ + float getFloat(String columnName) throws InvalidResultSetAccessException; + + /** + * Retrieves the value of the indicated column in the current row as + * an int. + * @param columnIndex the column index + * @return an int representing the column value + * @see java.sql.ResultSet#getInt(int) + */ + int getInt(int columnIndex) throws InvalidResultSetAccessException; + + /** + * Retrieves the value of the indicated column in the current row as + * an int. + * @param columnName the column name + * @return an int representing the column value + * @see java.sql.ResultSet#getInt(java.lang.String) + */ + int getInt(String columnName) throws InvalidResultSetAccessException; + + /** + * Retrieves the value of the indicated column in the current row as + * a long. + * @param columnIndex the column index + * @return a long representing the column value + * @see java.sql.ResultSet#getLong(int) + */ + long getLong(int columnIndex) throws InvalidResultSetAccessException; + + /** + * Retrieves the value of the indicated column in the current row as + * a long. + * @param columnName the column name + * @return a long representing the column value + * @see java.sql.ResultSet#getLong(java.lang.String) + */ + long getLong(String columnName) throws InvalidResultSetAccessException; + + /** + * Retrieves the value of the indicated column in the current row as + * an Object. + * @param columnIndex the column index + * @param map a Map object containing the mapping from SQL types to Java types + * @return a Object representing the column value + * @see java.sql.ResultSet#getObject(int, java.util.Map) + */ + Object getObject(int columnIndex, Map map) throws InvalidResultSetAccessException; + + /** + * Retrieves the value of the indicated column in the current row as + * an Object. + * @param columnIndex the column index + * @return a Object representing the column value + * @see java.sql.ResultSet#getObject(int) + */ + Object getObject(int columnIndex) throws InvalidResultSetAccessException; + + /** + * Retrieves the value of the indicated column in the current row as + * an Object. + * @param columnName the column name + * @param map a Map object containing the mapping from SQL types to Java types + * @return a Object representing the column value + * @see java.sql.ResultSet#getObject(java.lang.String, java.util.Map) + */ + Object getObject(String columnName, Map map) throws InvalidResultSetAccessException; + + /** + * Retrieves the value of the indicated column in the current row as + * an Object. + * @param columnName the column name + * @return a Object representing the column value + * @see java.sql.ResultSet#getObject(java.lang.String) + */ + Object getObject(String columnName) throws InvalidResultSetAccessException; + + /** + * Retrieves the value of the indicated column in the current row as + * a short. + * @param columnIndex the column index + * @return a short representing the column value + * @see java.sql.ResultSet#getShort(int) + */ + short getShort(int columnIndex) throws InvalidResultSetAccessException; + + /** + * Retrieves the value of the indicated column in the current row as + * a short. + * @param columnName the column name + * @return a short representing the column value + * @see java.sql.ResultSet#getShort(java.lang.String) + */ + short getShort(String columnName) throws InvalidResultSetAccessException; + + /** + * Retrieves the value of the indicated column in the current row as + * a String. + * @param columnIndex the column index + * @return a String representing the column value + * @see java.sql.ResultSet#getString(int) + */ + String getString(int columnIndex) throws InvalidResultSetAccessException; + + /** + * Retrieves the value of the indicated column in the current row as + * a String. + * @param columnName the column name + * @return a String representing the column value + * @see java.sql.ResultSet#getString(java.lang.String) + */ + String getString(String columnName) throws InvalidResultSetAccessException; + + /** + * Retrieves the value of the indicated column in the current row as + * a Time object. + * @param columnIndex the column index + * @param cal the Calendar to use in constructing the Date + * @return a Time object representing the column value + * @see java.sql.ResultSet#getTime(int, java.util.Calendar) + */ + Time getTime(int columnIndex, Calendar cal) throws InvalidResultSetAccessException; + + /** + * Retrieves the value of the indicated column in the current row as + * a Time object. + * @param columnIndex the column index + * @return a Time object representing the column value + * @see java.sql.ResultSet#getTime(int) + */ + Time getTime(int columnIndex) throws InvalidResultSetAccessException; + + /** + * Retrieves the value of the indicated column in the current row as + * a Time object. + * @param columnName the column name + * @param cal the Calendar to use in constructing the Date + * @return a Time object representing the column value + * @see java.sql.ResultSet#getTime(java.lang.String, java.util.Calendar) + */ + Time getTime(String columnName, Calendar cal) throws InvalidResultSetAccessException; + + /** + * Retrieves the value of the indicated column in the current row as + * a Time object. + * @param columnName the column name + * @return a Time object representing the column value + * @see java.sql.ResultSet#getTime(java.lang.String) + */ + Time getTime(String columnName) throws InvalidResultSetAccessException; + + /** + * Retrieves the value of the indicated column in the current row as + * a Timestamp object. + * @param columnIndex the column index + * @param cal the Calendar to use in constructing the Date + * @return a Timestamp object representing the column value + * @see java.sql.ResultSet#getTimestamp(int, java.util.Calendar) + */ + Timestamp getTimestamp(int columnIndex, Calendar cal) throws InvalidResultSetAccessException; + + /** + * Retrieves the value of the indicated column in the current row as + * a Timestamp object. + * @param columnIndex the column index + * @return a Timestamp object representing the column value + * @see java.sql.ResultSet#getTimestamp(int) + */ + Timestamp getTimestamp(int columnIndex) throws InvalidResultSetAccessException; + + /** + * Retrieves the value of the indicated column in the current row as + * a Timestamp object. + * @param columnName the column name + * @param cal the Calendar to use in constructing the Date + * @return a Timestamp object representing the column value + * @see java.sql.ResultSet#getTimestamp(java.lang.String, java.util.Calendar) + */ + Timestamp getTimestamp(String columnName, Calendar cal) throws InvalidResultSetAccessException; + + /** + * Retrieves the value of the indicated column in the current row as + * a Timestamp object. + * @param columnName the column name + * @return a Timestamp object representing the column value + * @see java.sql.ResultSet#getTimestamp(java.lang.String) + */ + Timestamp getTimestamp(String columnName) throws InvalidResultSetAccessException; + + + // RowSet navigation methods + + /** + * Moves the cursor to the given row number in the RowSet, just after the last row. + * @param row the number of the row where the cursor should move + * @return true if the cursor is on the RowSet, false otherwise + * @see java.sql.ResultSet#absolute(int) + */ + boolean absolute(int row) throws InvalidResultSetAccessException; + + /** + * Moves the cursor to the end of this RowSet. + * @see java.sql.ResultSet#afterLast() + */ + void afterLast() throws InvalidResultSetAccessException; + + /** + * Moves the cursor to the front of this RowSet, just before the first row. + * @see java.sql.ResultSet#beforeFirst() + */ + void beforeFirst() throws InvalidResultSetAccessException; + + /** + * Moves the cursor to the first row of this RowSet. + * @return true if the cursor is on a valid row, false otherwise + * @see java.sql.ResultSet#first() + */ + boolean first() throws InvalidResultSetAccessException; + + /** + * Retrieves the current row number. + * @return the current row number + * @see java.sql.ResultSet#getRow() + */ + int getRow() throws InvalidResultSetAccessException; + + /** + * Retrieves whether the cursor is after the last row of this RowSet. + * @return true if the cursor is after the last row, false otherwise + * @see java.sql.ResultSet#isAfterLast() + */ + boolean isAfterLast() throws InvalidResultSetAccessException; + + /** + * Retrieves whether the cursor is after the first row of this RowSet. + * @return true if the cursor is after the first row, false otherwise + * @see java.sql.ResultSet#isBeforeFirst() + */ + boolean isBeforeFirst() throws InvalidResultSetAccessException; + + /** + * Retrieves whether the cursor is on the first row of this RowSet. + * @return true if the cursor is after the first row, false otherwise + * @see java.sql.ResultSet#isFirst() + */ + boolean isFirst() throws InvalidResultSetAccessException; + + /** + * Retrieves whether the cursor is on the last row of this RowSet. + * @return true if the cursor is after the last row, false otherwise + * @see java.sql.ResultSet#isLast() + */ + boolean isLast() throws InvalidResultSetAccessException; + + /** + * Moves the cursor to the last row of this RowSet. + * @return true if the cursor is on a valid row, false otherwise + * @see java.sql.ResultSet#last() + */ + boolean last() throws InvalidResultSetAccessException; + + /** + * Moves the cursor to the next row. + * @return true if the new row is valid, false if there are no more rows + * @see java.sql.ResultSet#next() + */ + boolean next() throws InvalidResultSetAccessException; + + /** + * Moves the cursor to the previous row. + * @return true if the new row is valid, false if it is off the RowSet + * @see java.sql.ResultSet#previous() + */ + boolean previous() throws InvalidResultSetAccessException; + + /** + * Moves the cursor a relative number f rows, either positive or negative. + * @return true if the cursor is on a row, false otherwise + * @see java.sql.ResultSet#relative(int) + */ + boolean relative(int rows) throws InvalidResultSetAccessException; + + /** + * Reports whether the last column read had a value of SQL NULL. + * Note that you must first call one of the getter methods and then call + * the wasNull method. + * @return true if the most recent coumn retrieved was SQL NULL, + * false otherwise + * @see java.sql.ResultSet#wasNull() + */ + boolean wasNull() throws InvalidResultSetAccessException; + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/rowset/SqlRowSetMetaData.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/rowset/SqlRowSetMetaData.java new file mode 100644 index 0000000000..17ca2b6cc7 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/rowset/SqlRowSetMetaData.java @@ -0,0 +1,165 @@ +/* + * Copyright 2002-2005 the original author or authors. + * + * Licensed 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.springframework.jdbc.support.rowset; + +import org.springframework.jdbc.InvalidResultSetAccessException; + +/** + * Meta data interface for Spring's SqlRowSet, + * analogous to javax.sql.ResultSetMetaData + * + *

The main difference to the standard JDBC RowSetMetaData is that an SQLException + * is never thrown here. This allows a SqlRowSetMetaData to be used without having + * to deal with checked exceptions. A SqlRowSetMetaData will throw Spring's + * org.springframework.jdbc.InvalidResultSetAccessException + * instead (when appropriate). + * + * @author Thomas Risberg + * @since 1.2 + * @see SqlRowSet#getMetaData + * @see java.sql.ResultSetMetaData + * @see org.springframework.jdbc.InvalidResultSetAccessException + */ +public interface SqlRowSetMetaData { + + /** + * Retrieves the catalog name of the table that served as the source for the specified column. + * @param columnIndex the index of the column + * @return the catalog name + * @see java.sql.ResultSetMetaData#getCatalogName(int) + */ + String getCatalogName(int columnIndex) throws InvalidResultSetAccessException; + + /** + * Retrieves the fully qualified class that the specified column will be mapped to. + * @param columnIndex the index of the column + * @return the class name as a String + * @see java.sql.ResultSetMetaData#getColumnClassName(int) + */ + String getColumnClassName(int columnIndex) throws InvalidResultSetAccessException; + + /** + * Retrives the number of columns in the RowSet. + * @return the number of columns + * @see java.sql.ResultSetMetaData#getColumnCount() + */ + int getColumnCount() throws InvalidResultSetAccessException; + + /** + * Return the column names of the table that the result set represents. + * @return the column names + */ + String[] getColumnNames() throws InvalidResultSetAccessException; + + /** + * Retrieves the maximum width of the designated column. + * @param columnIndex the index of the column + * @return the width of the column + * @see java.sql.ResultSetMetaData#getColumnDisplaySize(int) + */ + int getColumnDisplaySize(int columnIndex) throws InvalidResultSetAccessException; + + /** + * Retrieve the suggested column title for the column specified. + * @param columnIndex the index of the column + * @return the column title + * @see java.sql.ResultSetMetaData#getColumnLabel(int) + */ + String getColumnLabel(int columnIndex) throws InvalidResultSetAccessException; + + /** + * Retrieve the column name for the indicated column. + * @param columnIndex the index of the column + * @return the column name + * @see java.sql.ResultSetMetaData#getColumnName(int) + */ + String getColumnName(int columnIndex) throws InvalidResultSetAccessException; + + /** + * Retrieve the SQL type code for the indicated column. + * @param columnIndex the index of the column + * @return the SQL type code + * @see java.sql.ResultSetMetaData#getColumnType(int) + * @see java.sql.Types + */ + int getColumnType(int columnIndex) throws InvalidResultSetAccessException; + + /** + * Retrieves the DBMS-specific type name for the indicated column. + * @param columnIndex the index of the column + * @return the type name + * @see java.sql.ResultSetMetaData#getColumnTypeName(int) + */ + String getColumnTypeName(int columnIndex) throws InvalidResultSetAccessException; + + /** + * Retrieves the precision for the indicated column. + * @param columnIndex the index of the column + * @return the precision + * @see java.sql.ResultSetMetaData#getPrecision(int) + */ + int getPrecision(int columnIndex) throws InvalidResultSetAccessException; + + /** + * Retrieves the scale of the indicated column. + * @param columnIndex the index of the column + * @return the scale + * @see java.sql.ResultSetMetaData#getScale(int) + */ + int getScale(int columnIndex) throws InvalidResultSetAccessException; + + /** + * Retrieves the schema name of the table that served as the source for the specified column. + * @param columnIndex the index of the column + * @return the schema name + * @see java.sql.ResultSetMetaData#getSchemaName(int) + */ + String getSchemaName(int columnIndex) throws InvalidResultSetAccessException; + + /** + * Retrieves the name of the table that served as the source for the specified column. + * @param columnIndex the index of the column + * @return the name of the table + * @see java.sql.ResultSetMetaData#getTableName(int) + */ + String getTableName(int columnIndex) throws InvalidResultSetAccessException; + + /** + * Indicates whether the case of the designated column is significant. + * @param columnIndex the index of the column + * @return true if the case sensitive, false otherwise + * @see java.sql.ResultSetMetaData#isCaseSensitive(int) + */ + boolean isCaseSensitive(int columnIndex) throws InvalidResultSetAccessException; + + /** + * Indicates whether the designated column contains a currency value. + * @param columnIndex the index of the column + * @return true if the value is a currency value, false otherwise + * @see java.sql.ResultSetMetaData#isCurrency(int) + */ + boolean isCurrency(int columnIndex) throws InvalidResultSetAccessException; + + /** + * Indicates whether the designated column contains a signed number. + * @param columnIndex the index of the column + * @return true if the column contains a signed number, false otherwise + * @see java.sql.ResultSetMetaData#isSigned(int) + */ + boolean isSigned(int columnIndex) throws InvalidResultSetAccessException; + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/rowset/package.html b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/rowset/package.html new file mode 100644 index 0000000000..05a297c5ec --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/rowset/package.html @@ -0,0 +1,8 @@ + + + +Provides a convenient holder for disconnected result sets. +Supported by JdbcTemplate, but can be used independently too. + + + diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/sql-error-codes.xml b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/sql-error-codes.xml new file mode 100644 index 0000000000..1471888adc --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/sql-error-codes.xml @@ -0,0 +1,210 @@ + + + + + + + + + DB2* + + + -007,-029,-097,-104,-109,-115,-128,-199,-204,-206,-301,-408,-441,-491 + + + -407,-530,-531,-532,-543,-544,-545,-603,-667,-803 + + + -904,-971 + + + -1035,-30080,-30081 + + + -911,-913 + + + + + + Apache Derby + + + true + + + 42802,42821,42X01,42X02,42X03,42X04,42X05,42X06,42X07,42X08 + + + 22001,22005,23502,23503,23505,23513,X0Y32 + + + 04501,08004,42Y07 + + + 40XL1 + + + 40001 + + + + + + 42000,42001,42101,42102,42111,42112,42121,42122,42132 + + + 22003,22012,22025,23000,23001 + + + 90046,90100,90117,90121,90126 + + + + + + HSQL Database Engine + + + -22,-28 + + + -9 + + + -80 + + + + + + Informix Dynamic Server + + + -201,-217,-696 + + + -239,-268,-692,-11030 + + + + + + Microsoft SQL Server + + + 156,170,207,208 + + + 229 + + + 544,2601,2627,8114,8115 + + + 1222 + + + 1205 + + + + + + 1054,1064,1146 + + + 630,839,840,893,1062,1169,1215,1216,1217,1451,1452,1557 + + + 1 + + + 1205 + + + 1213 + + + + + + 900,903,904,917,936,942,17006 + + + 17003 + + + 1,1400,1722,2291,2292 + + + 17002,17447 + + + 54 + + + 8177 + + + 60 + + + + + + true + + + 03000,42000,42601,42602,42622,42804,42P01 + + + 23000,23502,23503,23505,23514 + + + 53000,53100,53200,53300 + + + 55P03 + + + 40001 + + + 40P01 + + + + + + + Sybase SQL Server + Adaptive Server Enterprise + sql server + + + + 101,102,103,104,105,106,107,108,109,110,111,112,113,116,120,121,123,207,208,213,257,512 + + + 233,423,511,515,530,547,2601,2615,2714 + + + 921,1105 + + + 1205 + + + + diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/xml/Jdbc4SqlXmlHandler.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/xml/Jdbc4SqlXmlHandler.java new file mode 100644 index 0000000000..a0a98bdb5c --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/xml/Jdbc4SqlXmlHandler.java @@ -0,0 +1,162 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.support.xml; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.SQLXML; + +import javax.xml.transform.Source; +import javax.xml.transform.dom.DOMResult; +import javax.xml.transform.dom.DOMSource; + +import org.w3c.dom.Document; + +import org.springframework.dao.DataAccessResourceFailureException; + +/** + * Default implementation of the {@link SqlXmlHandler} interface. + * Provides database-specific implementations for storing and + * retrieving XML documents to and from fields in a database, + * relying on the JDBC 4.0 java.sql.SQLXML facility. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @since 2.5.6 + * @see java.sql.SQLXML + * @see java.sql.ResultSet#getSQLXML + * @see java.sql.PreparedStatement#setSQLXML + */ +public class Jdbc4SqlXmlHandler implements SqlXmlHandler { + + //------------------------------------------------------------------------- + // Convenience methods for accessing XML content + //------------------------------------------------------------------------- + + public String getXmlAsString(ResultSet rs, String columnName) throws SQLException { + return rs.getSQLXML(columnName).getString(); + } + + public String getXmlAsString(ResultSet rs, int columnIndex) throws SQLException { + return rs.getSQLXML(columnIndex).getString(); + } + + public InputStream getXmlAsBinaryStream(ResultSet rs, String columnName) throws SQLException { + return rs.getSQLXML(columnName).getBinaryStream(); + } + + public InputStream getXmlAsBinaryStream(ResultSet rs, int columnIndex) throws SQLException { + return rs.getSQLXML(columnIndex).getBinaryStream(); + } + + public Reader getXmlAsCharacterStream(ResultSet rs, String columnName) throws SQLException { + return rs.getSQLXML(columnName).getCharacterStream(); + } + + public Reader getXmlAsCharacterStream(ResultSet rs, int columnIndex) throws SQLException { + return rs.getSQLXML(columnIndex).getCharacterStream(); + } + + public Source getXmlAsSource(ResultSet rs, String columnName, Class sourceClass) throws SQLException { + return rs.getSQLXML(columnName).getSource(sourceClass != null ? sourceClass : DOMSource.class); + } + + public Source getXmlAsSource(ResultSet rs, int columnIndex, Class sourceClass) throws SQLException { + return rs.getSQLXML(columnIndex).getSource(sourceClass != null ? sourceClass : DOMSource.class); + } + + + //------------------------------------------------------------------------- + // Convenience methods for building XML content + //------------------------------------------------------------------------- + + public SqlXmlValue newSqlXmlValue(final String value) { + return new AbstractJdbc4SqlXmlValue() { + protected void provideXml(SQLXML xmlObject) throws SQLException, IOException { + xmlObject.setString(value); + } + }; + } + + public SqlXmlValue newSqlXmlValue(final XmlBinaryStreamProvider provider) { + return new AbstractJdbc4SqlXmlValue() { + protected void provideXml(SQLXML xmlObject) throws SQLException, IOException { + provider.provideXml(xmlObject.setBinaryStream()); + } + }; + } + + public SqlXmlValue newSqlXmlValue(final XmlCharacterStreamProvider provider) { + return new AbstractJdbc4SqlXmlValue() { + protected void provideXml(SQLXML xmlObject) throws SQLException, IOException { + provider.provideXml(xmlObject.setCharacterStream()); + } + }; + } + + public SqlXmlValue newSqlXmlValue(final Class resultClass, final XmlResultProvider provider) { + return new AbstractJdbc4SqlXmlValue() { + protected void provideXml(SQLXML xmlObject) throws SQLException, IOException { + provider.provideXml(xmlObject.setResult(resultClass)); + } + }; + } + + public SqlXmlValue newSqlXmlValue(final Document document) { + return new AbstractJdbc4SqlXmlValue() { + protected void provideXml(SQLXML xmlObject) throws SQLException, IOException { + ((DOMResult) xmlObject.setResult(DOMResult.class)).setNode(document); + } + }; + } + + + /** + * Internal base class for {@link SqlXmlValue} implementations. + */ + private static abstract class AbstractJdbc4SqlXmlValue implements SqlXmlValue { + + private SQLXML xmlObject; + + public void setValue(PreparedStatement ps, int paramIndex) throws SQLException { + this.xmlObject = ps.getConnection().createSQLXML(); + try { + provideXml(this.xmlObject); + } + catch (IOException ex) { + throw new DataAccessResourceFailureException("Failure encountered while providing XML", ex); + } + ps.setSQLXML(paramIndex, this.xmlObject); + } + + public void cleanup() { + try { + this.xmlObject.free(); + } + catch (SQLException ex) { + throw new DataAccessResourceFailureException("Could not free SQLXML object", ex); + } + } + + protected abstract void provideXml(SQLXML xmlObject) throws SQLException, IOException; + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/xml/SqlXmlFeatureNotImplementedException.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/xml/SqlXmlFeatureNotImplementedException.java new file mode 100644 index 0000000000..9281373924 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/xml/SqlXmlFeatureNotImplementedException.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.support.xml; + +import org.springframework.dao.InvalidDataAccessApiUsageException; + +/** + * Exception thrown when the underlying implementation does not support the + * requested feature of the API. + * + * @author Thomas Risberg + * @since 2.5.5 + */ +public class SqlXmlFeatureNotImplementedException extends InvalidDataAccessApiUsageException { + + /** + * Constructor for SqlXmlFeatureNotImplementedException. + * @param msg the detail message + */ + public SqlXmlFeatureNotImplementedException(String msg) { + super(msg); + } + + /** + * Constructor for SqlXmlFeatureNotImplementedException. + * @param msg the detail message + * @param cause the root cause from the data access API in use + */ + public SqlXmlFeatureNotImplementedException(String msg, Throwable cause) { + super(msg, cause); + } +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/xml/SqlXmlHandler.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/xml/SqlXmlHandler.java new file mode 100644 index 0000000000..b34071201d --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/xml/SqlXmlHandler.java @@ -0,0 +1,221 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.support.xml; + +import java.io.InputStream; +import java.io.Reader; +import java.sql.ResultSet; +import java.sql.SQLException; + +import javax.xml.transform.Source; + +import org.w3c.dom.Document; + +/** + * Abstraction for handling XML fields in specific databases. Its main purpose + * is to isolate database-specific handling of XML stored in the database. + * + *

JDBC 4.0 introduces the new data type java.sql.SQLXML + * but most databases and their drivers currently rely on database-specific + * data types and features. + * + *

Provides accessor methods for XML fields and acts as factory for + * {@link SqlXmlValue} instances. + * + * @author Thomas Risberg + * @since 2.5.5 + * @see Jdbc4SqlXmlHandler + * @see java.sql.SQLXML + * @see java.sql.ResultSet#getSQLXML + * @see java.sql.PreparedStatement#setSQLXML + */ +public interface SqlXmlHandler { + + //------------------------------------------------------------------------- + // Convenience methods for accessing XML content + //------------------------------------------------------------------------- + + /** + * Retrieve the given column as String from the given ResultSet. + *

Might simply invoke ResultSet.getString or work with + * SQLXML or database-specific classes depending on the + * database and driver. + * @param rs the ResultSet to retrieve the content from + * @param columnName the column name to use + * @return the content as String, or null in case of SQL NULL + * @throws SQLException if thrown by JDBC methods + * @see java.sql.ResultSet#getString + * @see java.sql.ResultSet#getSQLXML + */ + String getXmlAsString(ResultSet rs, String columnName) throws SQLException; + + /** + * Retrieve the given column as String from the given ResultSet. + *

Might simply invoke ResultSet.getString or work with + * SQLXML or database-specific classes depending on the + * database and driver. + * @param rs the ResultSet to retrieve the content from + * @param columnIndex the column index to use + * @return the content as String, or null in case of SQL NULL + * @throws SQLException if thrown by JDBC methods + * @see java.sql.ResultSet#getString + * @see java.sql.ResultSet#getSQLXML + */ + String getXmlAsString(ResultSet rs, int columnIndex) throws SQLException; + + /** + * Retrieve the given column as binary stream from the given ResultSet. + *

Might simply invoke ResultSet.getAsciiStream or work with + * SQLXML or database-specific classes depending on the + * database and driver. + * @param rs the ResultSet to retrieve the content from + * @param columnName the column name to use + * @return the content as a binary stream, or null in case of SQL NULL + * @throws SQLException if thrown by JDBC methods + * @see java.sql.ResultSet#getSQLXML + * @see java.sql.SQLXML#getBinaryStream + */ + InputStream getXmlAsBinaryStream(ResultSet rs, String columnName) throws SQLException; + + /** + * Retrieve the given column as binary stream from the given ResultSet. + *

Might simply invoke ResultSet.getAsciiStream or work with + * SQLXML or database-specific classes depending on the + * database and driver. + * @param rs the ResultSet to retrieve the content from + * @param columnIndex the column index to use + * @return the content as binary stream, or null in case of SQL NULL + * @throws SQLException if thrown by JDBC methods + * @see java.sql.ResultSet#getSQLXML + * @see java.sql.SQLXML#getBinaryStream + */ + InputStream getXmlAsBinaryStream(ResultSet rs, int columnIndex) throws SQLException; + + /** + * Retrieve the given column as character stream from the given ResultSet. + *

Might simply invoke ResultSet.getCharacterStream or work with + * SQLXML or database-specific classes depending on the + * database and driver. + * @param rs the ResultSet to retrieve the content from + * @param columnName the column name to use + * @return the content as character stream + * @throws SQLException if thrown by JDBC methods + * @see java.sql.ResultSet#getSQLXML + * @see java.sql.SQLXML#getCharacterStream + */ + Reader getXmlAsCharacterStream(ResultSet rs, String columnName) throws SQLException; + + /** + * Retrieve the given column as character stream from the given ResultSet. + *

Might simply invoke ResultSet.getCharacterStream or work with + * SQLXML or database-specific classes depending on the + * database and driver. + * @param rs the ResultSet to retrieve the content from + * @param columnIndex the column index to use + * @return the content as character stream + * @throws SQLException if thrown by JDBC methods + * @see java.sql.ResultSet#getSQLXML + * @see java.sql.SQLXML#getCharacterStream + */ + Reader getXmlAsCharacterStream(ResultSet rs, int columnIndex) throws SQLException; + + /** + * Retrieve the given column as Source implemented using the specified source class + * from the given ResultSet. + *

Might work with SQLXML or database-specific classes depending + * on the database and driver. + * @param rs the ResultSet to retrieve the content from + * @param columnName the column name to use + * @param sourceClass the implementation class to be used + * @return the content as character stream + * @throws SQLException if thrown by JDBC methods + * @see java.sql.ResultSet#getSQLXML + * @see java.sql.SQLXML#getSource + */ + Source getXmlAsSource(ResultSet rs, String columnName, Class sourceClass) throws SQLException; + + /** + * Retrieve the given column as Source implemented using the specified source class + * from the given ResultSet. + *

Might work with SQLXML or database-specific classes depending + * on the database and driver. + * @param rs the ResultSet to retrieve the content from + * @param columnIndex the column index to use + * @param sourceClass the implementation class to be used + * @return the content as character stream + * @throws SQLException if thrown by JDBC methods + * @see java.sql.ResultSet#getSQLXML + * @see java.sql.SQLXML#getSource + */ + Source getXmlAsSource(ResultSet rs, int columnIndex, Class sourceClass) throws SQLException; + + + //------------------------------------------------------------------------- + // Convenience methods for building XML content + //------------------------------------------------------------------------- + + /** + * Create a SqlXmlValue instance for the given XML data, + * as supported by the underlying JDBC driver. + * @param value the XML String value providing XML data + * @return the implementation specific instance + * @see SqlXmlValue + * @see java.sql.SQLXML#setString(String) + */ + SqlXmlValue newSqlXmlValue(String value); + + /** + * Create a SqlXmlValue instance for the given XML data, + * as supported by the underlying JDBC driver. + * @param provider the XmlBinaryStreamProvider providing XML data + * @return the implementation specific instance + * @see SqlXmlValue + * @see java.sql.SQLXML#setBinaryStream() + */ + SqlXmlValue newSqlXmlValue(XmlBinaryStreamProvider provider); + + /** + * Create a SqlXmlValue instance for the given XML data, + * as supported by the underlying JDBC driver. + * @param provider the XmlCharacterStreamProvider providing XML data + * @return the implementation specific instance + * @see SqlXmlValue + * @see java.sql.SQLXML#setCharacterStream() + */ + SqlXmlValue newSqlXmlValue(XmlCharacterStreamProvider provider); + + /** + * Create a SqlXmlValue instance for the given XML data, + * as supported by the underlying JDBC driver. + * @param resultClass the Result implementation class to be used + * @param provider the XmlResultProvider that will provide the XML data + * @return the implementation specific instance + * @see SqlXmlValue + * @see java.sql.SQLXML#setResult(Class) + */ + SqlXmlValue newSqlXmlValue(Class resultClass, XmlResultProvider provider); + + /** + * Create a SqlXmlValue instance for the given XML data, + * as supported by the underlying JDBC driver. + * @param doc the XML Document to be used + * @return the implementation specific instance + * @see SqlXmlValue + */ + SqlXmlValue newSqlXmlValue(Document doc); + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/xml/SqlXmlObjectMappingHandler.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/xml/SqlXmlObjectMappingHandler.java new file mode 100644 index 0000000000..a2605c5834 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/xml/SqlXmlObjectMappingHandler.java @@ -0,0 +1,69 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.support.xml; + +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + * Abstraction for handling XML object mapping to fields in a database. + * + *

Provides accessor methods for XML fields unmarshalled to an Object, + * and acts as factory for {@link SqlXmlValue} instances for marshalling + * purposes. + * + * @author Thomas Risberg + * @since 2.5.5 + * @see java.sql.ResultSet#getSQLXML + * @see java.sql.SQLXML + */ +public interface SqlXmlObjectMappingHandler extends SqlXmlHandler { + + /** + * Retrieve the given column as an object marshalled from the XML data retrieved + * from the given ResultSet. + *

Works with an internal Object to XML Mapping implementation. + * @param rs the ResultSet to retrieve the content from + * @param columnName the column name to use + * @return the content as an Object, or null in case of SQL NULL + * @throws java.sql.SQLException if thrown by JDBC methods + * @see java.sql.ResultSet#getSQLXML + */ + Object getXmlAsObject(ResultSet rs, String columnName) throws SQLException; + + /** + * Retrieve the given column as an object marshalled from the XML data retrieved + * from the given ResultSet. + *

Works with an internal Object to XML Mapping implementation. + * @param rs the ResultSet to retrieve the content from + * @param columnIndex the column index to use + * @return the content as an Object, or null in case of SQL NULL + * @throws java.sql.SQLException if thrown by JDBC methods + * @see java.sql.ResultSet#getSQLXML + */ + Object getXmlAsObject(ResultSet rs, int columnIndex) throws SQLException; + + /** + * Get an instance of an SqlXmlValue implementation to be used together + * with the database specific implementation of this SqlXmlObjectMappingHandler. + * @param value the Object to be marshalled to XML + * @return the implementation specific instance + * @see SqlXmlValue + */ + SqlXmlValue newMarshallingSqlXmlValue(Object value); + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/xml/SqlXmlValue.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/xml/SqlXmlValue.java new file mode 100644 index 0000000000..8e1e883858 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/xml/SqlXmlValue.java @@ -0,0 +1,33 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.support.xml; + +import org.springframework.jdbc.support.SqlValue; + +/** + * Subinterface of {@link org.springframework.jdbc.support.SqlValue} + * that supports passing in XML data to specified column and adds a + * cleanup callback, to be invoked after the value has been set and + * the corresponding statement has been executed. + * + * @author Thomas Risberg + * @since 2.5.5 + * @see org.springframework.jdbc.support.SqlValue + */ +public interface SqlXmlValue extends SqlValue { + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/xml/XmlBinaryStreamProvider.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/xml/XmlBinaryStreamProvider.java new file mode 100644 index 0000000000..5ede88882b --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/xml/XmlBinaryStreamProvider.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.support.xml; + +import java.io.OutputStream; +import java.io.IOException; + +/** + * Interface defining handling involved with providing OutputStream + * data for XML input. + * + * @author Thomas Risberg + * @since 2.5.5 + * @see java.io.OutputStream + */ +public interface XmlBinaryStreamProvider { + + /** + * Implementations must implement this method to provide the XML content + * for the OutputStream. + * @param outputStream the OutputStream object being used to provide the XML input + * @throws IOException if an I/O error occurs while providing the XML + */ + void provideXml(OutputStream outputStream) throws IOException; + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/xml/XmlCharacterStreamProvider.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/xml/XmlCharacterStreamProvider.java new file mode 100644 index 0000000000..02ffbffa36 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/xml/XmlCharacterStreamProvider.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.support.xml; + +import java.io.Writer; +import java.io.IOException; + +/** + * Interface defining handling involved with providing Writer + * data for XML input. + * + * @author Thomas Risberg + * @since 2.5.5 + * @see java.io.Writer + */ +public interface XmlCharacterStreamProvider { + + /** + * Implementations must implement this method to provide the XML content + * for the Writer. + * @param writer the Writer object being used to provide the XML input + * @throws IOException if an I/O error occurs while providing the XML + */ + void provideXml(Writer writer) throws IOException; + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/xml/XmlResultProvider.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/xml/XmlResultProvider.java new file mode 100644 index 0000000000..5b8db54ffe --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/xml/XmlResultProvider.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed 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.springframework.jdbc.support.xml; + +import javax.xml.transform.Result; + +/** + * Interface defining handling involved with providing Result + * data for XML input. + * + * @author Thomas Risberg + * @since 2.5.5 + * @see javax.xml.transform.Result + */ +public interface XmlResultProvider { + + /** + * Implementations must implement this method to provide the XML content + * for the Result. Implementations will vary depending on + * the Result implementation used. + * @param result the Result object being used to provide the XML input + */ + void provideXml(Result result); + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/xml/package.html b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/xml/package.html new file mode 100644 index 0000000000..224b669658 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/support/xml/package.html @@ -0,0 +1,7 @@ + + + +Abstraction for handling fields of SQLXML data type. + + + diff --git a/org.springframework.jdbc/src/main/java/overview.html b/org.springframework.jdbc/src/main/java/overview.html new file mode 100644 index 0000000000..1eb7a2e8c1 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/overview.html @@ -0,0 +1,7 @@ + + +

+The Spring Data Binding framework, an internal library used by Spring Web Flow. +

+ + \ No newline at end of file diff --git a/org.springframework.jdbc/src/test/resources/log4j.xml b/org.springframework.jdbc/src/test/resources/log4j.xml new file mode 100644 index 0000000000..767b96d620 --- /dev/null +++ b/org.springframework.jdbc/src/test/resources/log4j.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/org.springframework.jdbc/template.mf b/org.springframework.jdbc/template.mf new file mode 100644 index 0000000000..fd7b0bd96e --- /dev/null +++ b/org.springframework.jdbc/template.mf @@ -0,0 +1,22 @@ +Bundle-SymbolicName: org.springframework.jdbc +Bundle-Name: Spring JDBC +Bundle-Vendor: SpringSource +Bundle-ManifestVersion: 2 +Import-Template: + com.mchange.v2.c3p0.*;version="[0.9.1.2, 1.0.0)";resolution:=optional, + com.sun.rowset.*;version="[1.0.1, 2.0.0)";resolution:=optional, + javax.sql.rowset.*;version="[1.0.1, 2.0.0)";resolution:=optional, + javax.transaction.*;version="[1.0.1, 2.0.0)";resolution:=optional, + org.apache.commons.logging.*;version="[1.1.1, 2.0.0)", + org.enhydra.jdbc.*;version="[1.5.0, 2.0.0)";resolution:=optional, + org.springframework.beans.*;version="[2.5.5.A, 2.5.5.A]", + org.springframework.core.*;version="[2.5.5.A, 2.5.5.A]", + org.springframework.dao.*;version="[2.5.5.A, 2.5.5.A]", + org.springframework.jndi.*;version="[2.5.5.A, 2.5.5.A]";resolution:=optional, + org.springframework.transaction.*;version="[2.5.5.A, 2.5.5.A]", + org.springframework.util;version="[2.5.5.A, 2.5.5.A]" +Unversioned-Imports: + javax.naming.*, + javax.sql, + javax.xml.transform.*, + org.w3c.dom.*