001: /*
002: * Copyright 2002-2007 the original author or authors.
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License");
005: * you may not use this file except in compliance with the License.
006: * You may obtain a copy of the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS,
012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013: * See the License for the specific language governing permissions and
014: * limitations under the License.
015: */
016:
017: package org.springframework.jdbc.support.lob;
018:
019: import java.io.InputStream;
020: import java.io.OutputStream;
021: import java.io.Reader;
022: import java.io.Writer;
023: import java.lang.reflect.InvocationTargetException;
024: import java.lang.reflect.Method;
025: import java.sql.Blob;
026: import java.sql.Clob;
027: import java.sql.Connection;
028: import java.sql.PreparedStatement;
029: import java.sql.ResultSet;
030: import java.sql.SQLException;
031: import java.util.HashMap;
032: import java.util.Iterator;
033: import java.util.LinkedList;
034: import java.util.List;
035: import java.util.Map;
036:
037: import org.apache.commons.logging.Log;
038: import org.apache.commons.logging.LogFactory;
039:
040: import org.springframework.dao.DataAccessResourceFailureException;
041: import org.springframework.dao.InvalidDataAccessApiUsageException;
042: import org.springframework.jdbc.support.nativejdbc.NativeJdbcExtractor;
043: import org.springframework.util.FileCopyUtils;
044:
045: /**
046: * {@link LobHandler} implementation for Oracle databases. Uses proprietary API
047: * to create <code>oracle.sql.BLOB</code> and <code>oracle.sql.CLOB</code>
048: * instances, as necessary when working with Oracle's JDBC driver.
049: * Note that this LobHandler requires Oracle JDBC driver 9i or higher!
050: *
051: * <p>While most databases are able to work with {@link DefaultLobHandler},
052: * Oracle just accepts Blob/Clob instances created via its own proprietary
053: * BLOB/CLOB API, and additionally doesn't accept large streams for
054: * PreparedStatement's corresponding setter methods. Therefore, you need
055: * to use a strategy like this LobHandler implementation.
056: *
057: * <p>Needs to work on a native JDBC Connection, to be able to cast it to
058: * <code>oracle.jdbc.OracleConnection</code>. If you pass in Connections from a
059: * connection pool (the usual case in a J2EE environment), you need to set an
060: * appropriate {@link org.springframework.jdbc.support.nativejdbc.NativeJdbcExtractor}
061: * to allow for automatical retrieval of the underlying native JDBC Connection.
062: * LobHandler and NativeJdbcExtractor are separate concerns, therefore they
063: * are represented by separate strategy interfaces.
064: *
065: * <p>Coded via reflection to avoid dependencies on Oracle classes.
066: * Even reads in Oracle constants via reflection because of different Oracle
067: * drivers (classes12, ojdbc14) having different constant values! As this
068: * LobHandler initializes Oracle classes on instantiation, do not define this
069: * as eager-initializing singleton if you do not want to depend on the Oracle
070: * JAR being in the class path: use "lazy-init=true" to avoid this issue.
071: *
072: * @author Juergen Hoeller
073: * @since 04.12.2003
074: * @see #setNativeJdbcExtractor
075: * @see oracle.sql.BLOB
076: * @see oracle.sql.CLOB
077: */
078: public class OracleLobHandler extends AbstractLobHandler {
079:
080: private static final String BLOB_CLASS_NAME = "oracle.sql.BLOB";
081:
082: private static final String CLOB_CLASS_NAME = "oracle.sql.CLOB";
083:
084: private static final String DURATION_SESSION_FIELD_NAME = "DURATION_SESSION";
085:
086: private static final String MODE_READWRITE_FIELD_NAME = "MODE_READWRITE";
087:
088: protected final Log logger = LogFactory.getLog(getClass());
089:
090: private NativeJdbcExtractor nativeJdbcExtractor;
091:
092: private Boolean cache = Boolean.TRUE;
093:
094: private Class blobClass;
095:
096: private Class clobClass;
097:
098: private final Map durationSessionConstants = new HashMap(2);
099:
100: private final Map modeReadWriteConstants = new HashMap(2);
101:
102: /**
103: * Set an appropriate NativeJdbcExtractor to be able to retrieve the underlying
104: * native <code>oracle.jdbc.OracleConnection</code>. This is necessary for
105: * DataSource-based connection pools, as those need to return wrapped JDBC
106: * Connection handles that cannot be cast to a native Connection implementation.
107: * <p>Effectively, this LobHandler just invokes a single NativeJdbcExtractor
108: * method, namely <code>getNativeConnectionFromStatement</code> with a
109: * PreparedStatement argument (falling back to a
110: * <code>PreparedStatement.getConnection()</code> call if no extractor is set).
111: * <p>A common choice is SimpleNativeJdbcExtractor, whose Connection unwrapping
112: * (which is what OracleLobHandler needs) will work with many connection pools.
113: * See SimpleNativeJdbcExtractor's javadoc for details.
114: * @see org.springframework.jdbc.support.nativejdbc.NativeJdbcExtractor#getNativeConnectionFromStatement
115: * @see org.springframework.jdbc.support.nativejdbc.SimpleNativeJdbcExtractor
116: * @see oracle.jdbc.OracleConnection
117: */
118: public void setNativeJdbcExtractor(
119: NativeJdbcExtractor nativeJdbcExtractor) {
120: this .nativeJdbcExtractor = nativeJdbcExtractor;
121: }
122:
123: /**
124: * Set whether to cache the temporary LOB in the buffer cache.
125: * This value will be passed into BLOB/CLOB.createTemporary. Default is "true".
126: * @see oracle.sql.BLOB#createTemporary
127: * @see oracle.sql.CLOB#createTemporary
128: */
129: public void setCache(boolean cache) {
130: this .cache = new Boolean(cache);
131: }
132:
133: /**
134: * Retrieve the <code>oracle.sql.BLOB</code> and <code>oracle.sql.CLOB</code>
135: * classes via reflection, and initialize the values for the
136: * DURATION_SESSION and MODE_READWRITE constants defined there.
137: * @param con the Oracle Connection, for using the exact same class loader
138: * that the Oracle driver was loaded with
139: * @see oracle.sql.BLOB#DURATION_SESSION
140: * @see oracle.sql.BLOB#MODE_READWRITE
141: * @see oracle.sql.CLOB#DURATION_SESSION
142: * @see oracle.sql.CLOB#MODE_READWRITE
143: */
144: protected synchronized void initOracleDriverClasses(Connection con) {
145: if (this .blobClass == null) {
146: try {
147: // Initialize oracle.sql.BLOB class
148: this .blobClass = con.getClass().getClassLoader()
149: .loadClass(BLOB_CLASS_NAME);
150: this .durationSessionConstants.put(this .blobClass,
151: new Integer(this .blobClass.getField(
152: DURATION_SESSION_FIELD_NAME).getInt(
153: null)));
154: this .modeReadWriteConstants
155: .put(this .blobClass, new Integer(this .blobClass
156: .getField(MODE_READWRITE_FIELD_NAME)
157: .getInt(null)));
158:
159: // Initialize oracle.sql.CLOB class
160: this .clobClass = con.getClass().getClassLoader()
161: .loadClass(CLOB_CLASS_NAME);
162: this .durationSessionConstants.put(this .clobClass,
163: new Integer(this .clobClass.getField(
164: DURATION_SESSION_FIELD_NAME).getInt(
165: null)));
166: this .modeReadWriteConstants
167: .put(this .clobClass, new Integer(this .clobClass
168: .getField(MODE_READWRITE_FIELD_NAME)
169: .getInt(null)));
170: } catch (Exception ex) {
171: throw new InvalidDataAccessApiUsageException(
172: "Couldn't initialize OracleLobHandler because Oracle driver classes are not available. "
173: + "Note that OracleLobHandler requires Oracle JDBC driver 9i or higher!",
174: ex);
175: }
176: }
177: }
178:
179: public byte[] getBlobAsBytes(ResultSet rs, int columnIndex)
180: throws SQLException {
181: logger.debug("Returning Oracle BLOB as bytes");
182: Blob blob = rs.getBlob(columnIndex);
183: return (blob != null ? blob.getBytes(1, (int) blob.length())
184: : null);
185: }
186:
187: public InputStream getBlobAsBinaryStream(ResultSet rs,
188: int columnIndex) throws SQLException {
189: logger.debug("Returning Oracle BLOB as binary stream");
190: Blob blob = rs.getBlob(columnIndex);
191: return (blob != null ? blob.getBinaryStream() : null);
192: }
193:
194: public String getClobAsString(ResultSet rs, int columnIndex)
195: throws SQLException {
196: logger.debug("Returning Oracle CLOB as string");
197: Clob clob = rs.getClob(columnIndex);
198: return (clob != null ? clob
199: .getSubString(1, (int) clob.length()) : null);
200: }
201:
202: public InputStream getClobAsAsciiStream(ResultSet rs,
203: int columnIndex) throws SQLException {
204: logger.debug("Returning Oracle CLOB as ASCII stream");
205: Clob clob = rs.getClob(columnIndex);
206: return (clob != null ? clob.getAsciiStream() : null);
207: }
208:
209: public Reader getClobAsCharacterStream(ResultSet rs, int columnIndex)
210: throws SQLException {
211: logger.debug("Returning Oracle CLOB as character stream");
212: Clob clob = rs.getClob(columnIndex);
213: return (clob != null ? clob.getCharacterStream() : null);
214: }
215:
216: public LobCreator getLobCreator() {
217: return new OracleLobCreator();
218: }
219:
220: /**
221: * LobCreator implementation for Oracle databases.
222: * Creates Oracle-style temporary BLOBs and CLOBs that it frees on close.
223: * @see #close
224: */
225: protected class OracleLobCreator implements LobCreator {
226:
227: private final List createdLobs = new LinkedList();
228:
229: public void setBlobAsBytes(PreparedStatement ps,
230: int paramIndex, final byte[] content)
231: throws SQLException {
232:
233: if (content != null) {
234: Blob blob = (Blob) createLob(ps, false,
235: new LobCallback() {
236: public void populateLob(Object lob)
237: throws Exception {
238: Method methodToInvoke = lob
239: .getClass()
240: .getMethod(
241: "getBinaryOutputStream",
242: new Class[0]);
243: OutputStream out = (OutputStream) methodToInvoke
244: .invoke(lob, (Object[]) null);
245: FileCopyUtils.copy(content, out);
246: }
247: });
248: ps.setBlob(paramIndex, blob);
249: if (logger.isDebugEnabled()) {
250: logger
251: .debug("Set bytes for Oracle BLOB with length "
252: + blob.length());
253: }
254: } else {
255: ps.setBlob(paramIndex, (Blob) null);
256: logger.debug("Set Oracle BLOB to null");
257: }
258: }
259:
260: public void setBlobAsBinaryStream(PreparedStatement ps,
261: int paramIndex, final InputStream binaryStream,
262: int contentLength) throws SQLException {
263:
264: if (binaryStream != null) {
265: Blob blob = (Blob) createLob(ps, false,
266: new LobCallback() {
267: public void populateLob(Object lob)
268: throws Exception {
269: Method methodToInvoke = lob
270: .getClass()
271: .getMethod(
272: "getBinaryOutputStream",
273: (Class[]) null);
274: OutputStream out = (OutputStream) methodToInvoke
275: .invoke(lob, (Object[]) null);
276: FileCopyUtils.copy(binaryStream, out);
277: }
278: });
279: ps.setBlob(paramIndex, blob);
280: if (logger.isDebugEnabled()) {
281: logger
282: .debug("Set binary stream for Oracle BLOB with length "
283: + blob.length());
284: }
285: } else {
286: ps.setBlob(paramIndex, (Blob) null);
287: logger.debug("Set Oracle BLOB to null");
288: }
289: }
290:
291: public void setClobAsString(PreparedStatement ps,
292: int paramIndex, final String content)
293: throws SQLException {
294:
295: if (content != null) {
296: Clob clob = (Clob) createLob(ps, true,
297: new LobCallback() {
298: public void populateLob(Object lob)
299: throws Exception {
300: Method methodToInvoke = lob
301: .getClass()
302: .getMethod(
303: "getCharacterOutputStream",
304: (Class[]) null);
305: Writer writer = (Writer) methodToInvoke
306: .invoke(lob, (Object[]) null);
307: FileCopyUtils.copy(content, writer);
308: }
309: });
310: ps.setClob(paramIndex, clob);
311: if (logger.isDebugEnabled()) {
312: logger
313: .debug("Set string for Oracle CLOB with length "
314: + clob.length());
315: }
316: } else {
317: ps.setClob(paramIndex, (Clob) null);
318: logger.debug("Set Oracle CLOB to null");
319: }
320: }
321:
322: public void setClobAsAsciiStream(PreparedStatement ps,
323: int paramIndex, final InputStream asciiStream,
324: int contentLength) throws SQLException {
325:
326: if (asciiStream != null) {
327: Clob clob = (Clob) createLob(ps, true,
328: new LobCallback() {
329: public void populateLob(Object lob)
330: throws Exception {
331: Method methodToInvoke = lob.getClass()
332: .getMethod(
333: "getAsciiOutputStream",
334: (Class[]) null);
335: OutputStream out = (OutputStream) methodToInvoke
336: .invoke(lob, (Object[]) null);
337: FileCopyUtils.copy(asciiStream, out);
338: }
339: });
340: ps.setClob(paramIndex, clob);
341: if (logger.isDebugEnabled()) {
342: logger
343: .debug("Set ASCII stream for Oracle CLOB with length "
344: + clob.length());
345: }
346: } else {
347: ps.setClob(paramIndex, (Clob) null);
348: logger.debug("Set Oracle CLOB to null");
349: }
350: }
351:
352: public void setClobAsCharacterStream(PreparedStatement ps,
353: int paramIndex, final Reader characterStream,
354: int contentLength) throws SQLException {
355:
356: if (characterStream != null) {
357: Clob clob = (Clob) createLob(ps, true,
358: new LobCallback() {
359: public void populateLob(Object lob)
360: throws Exception {
361: Method methodToInvoke = lob
362: .getClass()
363: .getMethod(
364: "getCharacterOutputStream",
365: (Class[]) null);
366: Writer writer = (Writer) methodToInvoke
367: .invoke(lob, (Object[]) null);
368: FileCopyUtils.copy(characterStream,
369: writer);
370: }
371: });
372: ps.setClob(paramIndex, clob);
373: if (logger.isDebugEnabled()) {
374: logger
375: .debug("Set character stream for Oracle CLOB with length "
376: + clob.length());
377: }
378: } else {
379: ps.setClob(paramIndex, (Clob) null);
380: logger.debug("Set Oracle CLOB to null");
381: }
382: }
383:
384: /**
385: * Create a LOB instance for the given PreparedStatement,
386: * populating it via the given callback.
387: */
388: protected Object createLob(PreparedStatement ps, boolean clob,
389: LobCallback callback) throws SQLException {
390:
391: Connection con = null;
392: try {
393: con = getOracleConnection(ps);
394: initOracleDriverClasses(con);
395: Object lob = prepareLob(con, clob ? clobClass
396: : blobClass);
397: callback.populateLob(lob);
398: lob.getClass().getMethod("close", (Class[]) null)
399: .invoke(lob, (Object[]) null);
400: this .createdLobs.add(lob);
401: if (logger.isDebugEnabled()) {
402: logger.debug("Created new Oracle "
403: + (clob ? "CLOB" : "BLOB"));
404: }
405: return lob;
406: } catch (SQLException ex) {
407: throw ex;
408: } catch (InvocationTargetException ex) {
409: if (ex.getTargetException() instanceof SQLException) {
410: throw (SQLException) ex.getTargetException();
411: } else if (con != null
412: && ex.getTargetException() instanceof ClassCastException) {
413: throw new InvalidDataAccessApiUsageException(
414: "OracleLobCreator needs to work on [oracle.jdbc.OracleConnection], not on ["
415: + con.getClass().getName()
416: + "]: specify a corresponding NativeJdbcExtractor",
417: ex.getTargetException());
418: } else {
419: throw new DataAccessResourceFailureException(
420: "Could not create Oracle LOB", ex
421: .getTargetException());
422: }
423: } catch (Exception ex) {
424: throw new DataAccessResourceFailureException(
425: "Could not create Oracle LOB", ex);
426: }
427: }
428:
429: /**
430: * Retrieve the underlying OracleConnection, using a NativeJdbcExtractor if set.
431: */
432: protected Connection getOracleConnection(PreparedStatement ps)
433: throws SQLException, ClassNotFoundException {
434:
435: return (nativeJdbcExtractor != null) ? nativeJdbcExtractor
436: .getNativeConnectionFromStatement(ps) : ps
437: .getConnection();
438: }
439:
440: /**
441: * Create and open an oracle.sql.BLOB/CLOB instance via reflection.
442: */
443: protected Object prepareLob(Connection con, Class lobClass)
444: throws Exception {
445: /*
446: BLOB blob = BLOB.createTemporary(con, false, BLOB.DURATION_SESSION);
447: blob.open(BLOB.MODE_READWRITE);
448: return blob;
449: */
450: Method createTemporary = lobClass.getMethod(
451: "createTemporary", new Class[] { Connection.class,
452: boolean.class, int.class });
453: Object lob = createTemporary.invoke(null,
454: new Object[] { con, cache,
455: durationSessionConstants.get(lobClass) });
456: Method open = lobClass.getMethod("open",
457: new Class[] { int.class });
458: open.invoke(lob, new Object[] { modeReadWriteConstants
459: .get(lobClass) });
460: return lob;
461: }
462:
463: /**
464: * Free all temporary BLOBs and CLOBs created by this creator.
465: */
466: public void close() {
467: try {
468: for (Iterator it = this .createdLobs.iterator(); it
469: .hasNext();) {
470: /*
471: BLOB blob = (BLOB) it.next();
472: blob.freeTemporary();
473: */
474: Object lob = it.next();
475: Method freeTemporary = lob.getClass().getMethod(
476: "freeTemporary", new Class[0]);
477: freeTemporary.invoke(lob, new Object[0]);
478: it.remove();
479: }
480: } catch (InvocationTargetException ex) {
481: logger.error("Could not free Oracle LOB", ex
482: .getTargetException());
483: } catch (Exception ex) {
484: throw new DataAccessResourceFailureException(
485: "Could not free Oracle LOB", ex);
486: }
487: }
488: }
489:
490: /**
491: * Internal callback interface for use with createLob.
492: */
493: protected static interface LobCallback {
494:
495: /**
496: * Populate the given BLOB or CLOB instance with content.
497: * @throws Exception any exception including InvocationTargetException
498: */
499: void populateLob(Object lob) throws Exception;
500: }
501:
502: }
|