1. /*
  2. * Copyright 2002-2004 the original author or authors.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. package org.springframework.jdbc.support.lob;
  17. import java.io.ByteArrayInputStream;
  18. import java.io.IOException;
  19. import java.io.InputStream;
  20. import java.io.OutputStream;
  21. import java.io.Reader;
  22. import java.io.StringReader;
  23. import java.io.Writer;
  24. import java.lang.reflect.InvocationTargetException;
  25. import java.lang.reflect.Method;
  26. import java.sql.Blob;
  27. import java.sql.Clob;
  28. import java.sql.Connection;
  29. import java.sql.PreparedStatement;
  30. import java.sql.ResultSet;
  31. import java.sql.SQLException;
  32. import java.util.HashMap;
  33. import java.util.Iterator;
  34. import java.util.LinkedList;
  35. import java.util.List;
  36. import java.util.Map;
  37. import org.apache.commons.logging.Log;
  38. import org.apache.commons.logging.LogFactory;
  39. import org.springframework.dao.DataAccessResourceFailureException;
  40. import org.springframework.dao.InvalidDataAccessApiUsageException;
  41. import org.springframework.jdbc.support.nativejdbc.NativeJdbcExtractor;
  42. import org.springframework.util.FileCopyUtils;
  43. /**
  44. * LobHandler implementation for Oracle databases. Uses proprietary API to
  45. * create oracle.sql.BLOB and oracle.sql.CLOB instances, as necessary when
  46. * working with Oracle's JDBC driver. Developed and tested on Oracle 9i.
  47. *
  48. * <p>While most databases are able to work with DefaultLobHandler, Oracle just
  49. * accepts Blob/Clob instances created via its own proprietary BLOB/CLOB API,
  50. * and additionally doesn't accept large streams for PreparedStatement's
  51. * corresponding setter methods. Therefore, you need to use a strategy like
  52. * this LobHandler implementation.
  53. *
  54. * <p>Needs to work on a native JDBC Connection, to be able to cast it to
  55. * oracle.jdbc.OracleConnection. If you pass in Connections from a connection
  56. * pool (the usual case in a J2EE environment), you need to set an appropriate
  57. * NativeJdbcExtractor to allow for automatical retrieval of the underlying
  58. * native JDBC Connection. LobHandler and NativeJdbcExtractor are separate
  59. * concerns, therefore they are represented by separate strategy interfaces.
  60. *
  61. * <p>Coded via reflection to avoid dependencies on Oracle classes.
  62. * Even reads in Oracle constants via reflection because of different Oracle
  63. * drivers (classes12, ojdbc14) having different constant values!
  64. * As it initializes the Oracle classes on instantiation, do not define this
  65. * as eager-initializing singleton if you do not want to depend on the Oracle
  66. * JAR being in the class path: use "lazy-init=true" to avoid this issue.
  67. *
  68. * @author Juergen Hoeller
  69. * @since 04.12.2003
  70. * @see #setNativeJdbcExtractor
  71. * @see oracle.sql.BLOB
  72. * @see oracle.sql.CLOB
  73. */
  74. public class OracleLobHandler implements LobHandler {
  75. private static final String CONNECTION_CLASS_NAME = "oracle.jdbc.OracleConnection";
  76. private static final String BLOB_CLASS_NAME = "oracle.sql.BLOB";
  77. private static final String CLOB_CLASS_NAME = "oracle.sql.CLOB";
  78. private static final String DURATION_SESSION_FIELD_NAME = "DURATION_SESSION";
  79. private static final String MODE_READWRITE_FIELD_NAME = "MODE_READWRITE";
  80. protected final Log logger = LogFactory.getLog(getClass());
  81. private final Class connectionClass;
  82. private final Class blobClass;
  83. private final Class clobClass;
  84. private final Map durationSessionConstants = new HashMap(2);
  85. private final Map modeReadWriteConstants = new HashMap(2);
  86. private NativeJdbcExtractor nativeJdbcExtractor;
  87. private Boolean cache = Boolean.TRUE;
  88. /**
  89. * This constructor retrieves the oracle.sql.BLOB and oracle.sql.CLOB
  90. * classes via reflection, and initializes the values for the
  91. * DURATION_SESSION and MODE_READWRITE constants defined there.
  92. * @see oracle.sql.BLOB#DURATION_SESSION
  93. * @see oracle.sql.BLOB#MODE_READWRITE
  94. * @see oracle.sql.CLOB#DURATION_SESSION
  95. * @see oracle.sql.CLOB#MODE_READWRITE
  96. */
  97. public OracleLobHandler() throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
  98. this.connectionClass = getClass().getClassLoader().loadClass(CONNECTION_CLASS_NAME);
  99. // initialize oracle.sql.BLOB class
  100. this.blobClass = getClass().getClassLoader().loadClass(BLOB_CLASS_NAME);
  101. this.durationSessionConstants.put(this.blobClass,
  102. new Integer(this.blobClass.getField(DURATION_SESSION_FIELD_NAME).getInt(null)));
  103. this.modeReadWriteConstants.put(this.blobClass,
  104. new Integer(this.blobClass.getField(MODE_READWRITE_FIELD_NAME).getInt(null)));
  105. // initialize oracle.sql.CLOB class
  106. this.clobClass = getClass().getClassLoader().loadClass(CLOB_CLASS_NAME);
  107. this.durationSessionConstants.put(this.clobClass,
  108. new Integer(this.clobClass.getField(DURATION_SESSION_FIELD_NAME).getInt(null)));
  109. this.modeReadWriteConstants.put(this.clobClass,
  110. new Integer(this.clobClass.getField(MODE_READWRITE_FIELD_NAME).getInt(null)));
  111. }
  112. /**
  113. * Set an appropriate NativeJdbcExtractor to be able to retrieve the underlying
  114. * native oracle.jdbc.OracleConnection. This is necessary for DataSource-based
  115. * connection pools, as those need to return wrapped JDBC Connection handles.
  116. * <p>Effectively, this LobHandler just invokes a single NativeJdbcExtractor
  117. * method, namely <code>getNativeConnectionFromStatement</code> with a
  118. * PreparedStatement argument (falling back to a
  119. * <code>PreparedStatement.getConnection()</code> call if no extractor is set).
  120. * <p>A common choice is SimpleNativeJdbcExtractor, whose Connection unwrapping
  121. * (which is what OracleLobHandler needs) will work with almost any connection
  122. * pool. See SimpleNativeJdbcExtractor's javadoc for details.
  123. * @see org.springframework.jdbc.support.nativejdbc.NativeJdbcExtractor#getNativeConnectionFromStatement
  124. * @see org.springframework.jdbc.support.nativejdbc.SimpleNativeJdbcExtractor
  125. * @see oracle.jdbc.OracleConnection
  126. */
  127. public void setNativeJdbcExtractor(NativeJdbcExtractor nativeJdbcExtractor) {
  128. this.nativeJdbcExtractor = nativeJdbcExtractor;
  129. }
  130. /**
  131. * Set whether to cache the temporary LOB in the buffer cache.
  132. * This value will be passed into BLOB/CLOB.createTemporary. Default is true.
  133. * @see oracle.sql.BLOB#createTemporary
  134. * @see oracle.sql.CLOB#createTemporary
  135. */
  136. public void setCache(boolean cache) {
  137. this.cache = new Boolean(cache);
  138. }
  139. public byte[] getBlobAsBytes(ResultSet rs, int columnIndex) throws SQLException {
  140. logger.debug("Returning BLOB as bytes");
  141. Blob blob = rs.getBlob(columnIndex);
  142. return (blob != null ? blob.getBytes(1, (int) blob.length()) : new byte[0]);
  143. }
  144. public InputStream getBlobAsBinaryStream(ResultSet rs, int columnIndex) throws SQLException {
  145. logger.debug("Returning BLOB as binary stream");
  146. Blob blob = rs.getBlob(columnIndex);
  147. return (blob != null ? blob.getBinaryStream() : new ByteArrayInputStream(new byte[0]));
  148. }
  149. public String getClobAsString(ResultSet rs, int columnIndex) throws SQLException {
  150. logger.debug("Returning CLOB as string");
  151. Clob clob = rs.getClob(columnIndex);
  152. return (clob != null ? clob.getSubString(1, (int) clob.length()) : "");
  153. }
  154. public InputStream getClobAsAsciiStream(ResultSet rs, int columnIndex) throws SQLException {
  155. logger.debug("Returning CLOB as ASCII stream");
  156. Clob clob = rs.getClob(columnIndex);
  157. return (clob != null ? clob.getAsciiStream() : new ByteArrayInputStream(new byte[0]));
  158. }
  159. public Reader getClobAsCharacterStream(ResultSet rs, int columnIndex) throws SQLException {
  160. logger.debug("Returning CLOB as character stream");
  161. Clob clob = rs.getClob(columnIndex);
  162. return (clob != null ? clob.getCharacterStream() : new StringReader(""));
  163. }
  164. public LobCreator getLobCreator() {
  165. return new OracleLobCreator();
  166. }
  167. /**
  168. * LobCreator implementation for Oracle databases.
  169. * Creates Oracle-style temporary BLOBs and CLOBs that it frees on close.
  170. * @see #close
  171. */
  172. protected class OracleLobCreator implements LobCreator {
  173. private final List createdLobs = new LinkedList();
  174. public void setBlobAsBytes(PreparedStatement ps, int parameterIndex, final byte[] content)
  175. throws SQLException {
  176. if (content != null) {
  177. Blob blob = (Blob) createLob(ps, blobClass, new LobCallback() {
  178. public void populateLob(Object lob) throws Exception {
  179. Method methodToInvoke = lob.getClass().getMethod("getBinaryOutputStream", new Class[0]);
  180. OutputStream out = (OutputStream) methodToInvoke.invoke(lob, null);
  181. try {
  182. out.write(content);
  183. out.flush();
  184. }
  185. finally {
  186. try {
  187. out.close();
  188. }
  189. catch (IOException ex) {
  190. logger.warn("Could not close BLOB OutputStream", ex);
  191. }
  192. }
  193. }
  194. });
  195. ps.setBlob(parameterIndex, blob);
  196. logger.debug("Set bytes for BLOB with length " + blob.length());
  197. }
  198. else {
  199. ps.setBlob(parameterIndex, null);
  200. logger.debug("Set BLOB to null");
  201. }
  202. }
  203. public void setBlobAsBinaryStream(PreparedStatement ps, int parameterIndex,
  204. final InputStream binaryStream, int contentLength)
  205. throws SQLException {
  206. if (binaryStream != null) {
  207. Blob blob = (Blob) createLob(ps, blobClass, new LobCallback() {
  208. public void populateLob(Object lob) throws Exception {
  209. Method methodToInvoke = lob.getClass().getMethod("getBinaryOutputStream", null);
  210. FileCopyUtils.copy(binaryStream, ((OutputStream) methodToInvoke.invoke(lob, null)));
  211. }
  212. });
  213. ps.setBlob(parameterIndex, blob);
  214. logger.debug("Set binary stream for BLOB with length " + blob.length());
  215. }
  216. else {
  217. ps.setBlob(parameterIndex, null);
  218. logger.debug("Set BLOB to null");
  219. }
  220. }
  221. public void setClobAsString(PreparedStatement ps, int parameterIndex, final String content)
  222. throws SQLException {
  223. if (content != null) {
  224. Clob clob = (Clob) createLob(ps, clobClass, new LobCallback() {
  225. public void populateLob(Object lob) throws Exception {
  226. Method methodToInvoke = lob.getClass().getMethod("getCharacterOutputStream", null);
  227. Writer writer = ((Writer) methodToInvoke.invoke(lob, null));
  228. try {
  229. writer.write(content);
  230. writer.flush();
  231. }
  232. finally {
  233. try {
  234. writer.close();
  235. }
  236. catch (IOException ex) {
  237. logger.warn("Could not close CLOB Writer", ex);
  238. }
  239. }
  240. }
  241. });
  242. ps.setClob(parameterIndex, clob);
  243. logger.debug("Set string for CLOB with length " + clob.length());
  244. }
  245. else {
  246. ps.setClob(parameterIndex, null);
  247. logger.debug("Set CLOB to null");
  248. }
  249. }
  250. public void setClobAsAsciiStream(PreparedStatement ps, int parameterIndex,
  251. final InputStream asciiStream, int contentLength)
  252. throws SQLException {
  253. if (asciiStream != null) {
  254. Clob clob = (Clob) createLob(ps, clobClass, new LobCallback() {
  255. public void populateLob(Object lob) throws Exception {
  256. Method methodToInvoke = lob.getClass().getMethod("getAsciiOutputStream", null);
  257. FileCopyUtils.copy(asciiStream, ((OutputStream) methodToInvoke.invoke(lob, null)));
  258. }
  259. });
  260. ps.setClob(parameterIndex, clob);
  261. logger.debug("Set ASCII stream for CLOB with length " + clob.length());
  262. }
  263. else {
  264. ps.setClob(parameterIndex, null);
  265. logger.debug("Set CLOB to null");
  266. }
  267. }
  268. public void setClobAsCharacterStream(PreparedStatement ps, int parameterIndex,
  269. final Reader characterStream, int contentLength)
  270. throws SQLException {
  271. if (characterStream != null) {
  272. Clob clob = (Clob) createLob(ps, clobClass, new LobCallback() {
  273. public void populateLob(Object lob) throws Exception {
  274. Method methodToInvoke = lob.getClass().getMethod("getCharacterOutputStream", null);
  275. FileCopyUtils.copy(characterStream, ((Writer) methodToInvoke.invoke(lob, null)));
  276. }
  277. });
  278. ps.setClob(parameterIndex, clob);
  279. logger.debug("Set character stream for CLOB with length " + clob.length());
  280. }
  281. else {
  282. ps.setClob(parameterIndex, null);
  283. logger.debug("Set CLOB to null");
  284. }
  285. }
  286. /**
  287. * Create a LOB instance for the given PreparedStatement,
  288. * populating it via the given callback.
  289. */
  290. protected Object createLob(PreparedStatement ps, Class lobClass, LobCallback callback) throws SQLException {
  291. try {
  292. Object lob = prepareLob(getOracleConnection(ps), lobClass);
  293. callback.populateLob(lob);
  294. lob.getClass().getMethod("close", null).invoke(lob, null);
  295. this.createdLobs.add(lob);
  296. logger.debug("Created new Oracle LOB");
  297. return lob;
  298. }
  299. catch (SQLException ex) {
  300. throw ex;
  301. }
  302. catch (InvocationTargetException ex) {
  303. if (ex.getTargetException() instanceof SQLException) {
  304. throw (SQLException) ex.getTargetException();
  305. }
  306. else {
  307. throw new DataAccessResourceFailureException("Could not create Oracle LOB", ex.getTargetException());
  308. }
  309. }
  310. catch (Exception ex) {
  311. throw new DataAccessResourceFailureException("Could not create Oracle LOB", ex);
  312. }
  313. }
  314. /**
  315. * Retrieve the underlying OracleConnection, using a NativeJdbcExtractor if set.
  316. */
  317. protected Connection getOracleConnection(PreparedStatement ps) throws SQLException, ClassNotFoundException {
  318. Connection conToUse = (nativeJdbcExtractor != null) ?
  319. nativeJdbcExtractor.getNativeConnectionFromStatement(ps) : ps.getConnection();
  320. if (!connectionClass.isAssignableFrom(conToUse.getClass())) {
  321. throw new InvalidDataAccessApiUsageException("OracleLobCreator needs to work on " +
  322. "[oracle.jdbc.OracleConnection], not on [" + conToUse.getClass() +
  323. "] - specify a corresponding NativeJdbcExtractor");
  324. }
  325. return conToUse;
  326. }
  327. /**
  328. * Create and open an oracle.sql.BLOB/CLOB instance via reflection.
  329. */
  330. protected Object prepareLob(Connection con, Class lobClass) throws Exception {
  331. /*
  332. BLOB blob = BLOB.createTemporary(con, false, BLOB.DURATION_SESSION);
  333. blob.open(BLOB.MODE_READWRITE);
  334. return blob;
  335. */
  336. Method createTemporary = lobClass.getMethod("createTemporary",
  337. new Class[] {Connection.class, boolean.class, int.class});
  338. Object lob = createTemporary.invoke(null, new Object[] {con, cache,
  339. durationSessionConstants.get(lobClass)});
  340. Method open = lobClass.getMethod("open", new Class[] {int.class});
  341. open.invoke(lob, new Object[] {modeReadWriteConstants.get(lobClass)});
  342. return lob;
  343. }
  344. /**
  345. * Free all temporary BLOBs and CLOBs created by this creator.
  346. */
  347. public void close() {
  348. try {
  349. for (Iterator it = this.createdLobs.iterator(); it.hasNext();) {
  350. /*
  351. BLOB blob = (BLOB) it.next();
  352. blob.freeTemporary();
  353. */
  354. Object lob = it.next();
  355. Method freeTemporary = lob.getClass().getMethod("freeTemporary", new Class[0]);
  356. freeTemporary.invoke(lob, new Object[0]);
  357. it.remove();
  358. }
  359. }
  360. catch (InvocationTargetException ex) {
  361. logger.error("Could not free Oracle LOB", ex.getTargetException());
  362. }
  363. catch (Exception ex) {
  364. throw new DataAccessResourceFailureException("Could not free Oracle LOB", ex);
  365. }
  366. }
  367. }
  368. /**
  369. * Internal callback interface for use with createLob.
  370. * @see OracleLobCreator#createLob
  371. */
  372. protected static interface LobCallback {
  373. /**
  374. * Populate the given BLOB or CLOB instance with content.
  375. * @throws Exception any exception including InvocationTargetException
  376. */
  377. void populateLob(Object lob) throws Exception;
  378. }
  379. }