001/*
002 * Copyright 2002-2017 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 *      https://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
017package org.springframework.jdbc.support.lob;
018
019import java.io.ByteArrayInputStream;
020import java.io.InputStream;
021import java.io.InputStreamReader;
022import java.io.Reader;
023import java.io.StringReader;
024import java.nio.charset.StandardCharsets;
025import java.sql.Blob;
026import java.sql.Clob;
027import java.sql.PreparedStatement;
028import java.sql.ResultSet;
029import java.sql.SQLException;
030
031import org.apache.commons.logging.Log;
032import org.apache.commons.logging.LogFactory;
033
034import org.springframework.lang.Nullable;
035
036/**
037 * Default implementation of the {@link LobHandler} interface.
038 * Invokes the direct accessor methods that {@code java.sql.ResultSet}
039 * and {@code java.sql.PreparedStatement} offer.
040 *
041 * <p>By default, incoming streams are going to be passed to the appropriate
042 * {@code setBinary/Ascii/CharacterStream} method on the JDBC driver's
043 * {@link PreparedStatement}. If the specified content length is negative,
044 * this handler will use the JDBC 4.0 variants of the set-stream methods
045 * without a length parameter; otherwise, it will pass the specified length
046 * on to the driver.
047 *
048 * <p>This LobHandler should work for any JDBC driver that is JDBC compliant
049 * in terms of the spec's suggestions regarding simple BLOB and CLOB handling.
050 * This does not apply to Oracle 9i's drivers at all; as of Oracle 10g,
051 * it does work but may still come with LOB size limitations. Consider using
052 * recent Oracle drivers even when working against an older database server.
053 * See the {@link LobHandler} javadoc for the full set of recommendations.
054 *
055 * <p>Some JDBC drivers require values with a BLOB/CLOB target column to be
056 * explicitly set through the JDBC {@code setBlob} / {@code setClob} API:
057 * for example, PostgreSQL's driver. Switch the {@link #setWrapAsLob "wrapAsLob"}
058 * property to "true" when operating against such a driver.
059 *
060 * <p>On JDBC 4.0, this LobHandler also supports streaming the BLOB/CLOB content
061 * via the {@code setBlob} / {@code setClob} variants that take a stream
062 * argument directly. Consider switching the {@link #setStreamAsLob "streamAsLob"}
063 * property to "true" when operating against a fully compliant JDBC 4.0 driver.
064 *
065 * <p>Finally, this LobHandler also supports the creation of temporary BLOB/CLOB
066 * objects. Consider switching the {@link #setCreateTemporaryLob "createTemporaryLob"}
067 * property to "true" when "streamAsLob" happens to run into LOB size limitations.
068 *
069 * <p>See the {@link LobHandler} interface javadoc for a summary of recommendations.
070 *
071 * @author Juergen Hoeller
072 * @since 04.12.2003
073 * @see java.sql.ResultSet#getBytes
074 * @see java.sql.ResultSet#getBinaryStream
075 * @see java.sql.ResultSet#getString
076 * @see java.sql.ResultSet#getAsciiStream
077 * @see java.sql.ResultSet#getCharacterStream
078 * @see java.sql.PreparedStatement#setBytes
079 * @see java.sql.PreparedStatement#setBinaryStream
080 * @see java.sql.PreparedStatement#setString
081 * @see java.sql.PreparedStatement#setAsciiStream
082 * @see java.sql.PreparedStatement#setCharacterStream
083 */
084public class DefaultLobHandler extends AbstractLobHandler {
085
086        protected final Log logger = LogFactory.getLog(getClass());
087
088        private boolean wrapAsLob = false;
089
090        private boolean streamAsLob = false;
091
092        private boolean createTemporaryLob = false;
093
094
095        /**
096         * Specify whether to submit a byte array / String to the JDBC driver
097         * wrapped in a JDBC Blob / Clob object, using the JDBC {@code setBlob} /
098         * {@code setClob} method with a Blob / Clob argument.
099         * <p>Default is "false", using the common JDBC 2.0 {@code setBinaryStream}
100         * / {@code setCharacterStream} method for setting the content. Switch this
101         * to "true" for explicit Blob / Clob wrapping against JDBC drivers that
102         * are known to require such wrapping (e.g. PostgreSQL's for access to OID
103         * columns, whereas BYTEA columns need to be accessed the standard way).
104         * <p>This setting affects byte array / String arguments as well as stream
105         * arguments, unless {@link #setStreamAsLob "streamAsLob"} overrides this
106         * handling to use JDBC 4.0's new explicit streaming support (if available).
107         * @see java.sql.PreparedStatement#setBlob(int, java.sql.Blob)
108         * @see java.sql.PreparedStatement#setClob(int, java.sql.Clob)
109         */
110        public void setWrapAsLob(boolean wrapAsLob) {
111                this.wrapAsLob = wrapAsLob;
112        }
113
114        /**
115         * Specify whether to submit a binary stream / character stream to the JDBC
116         * driver as explicit LOB content, using the JDBC 4.0 {@code setBlob} /
117         * {@code setClob} method with a stream argument.
118         * <p>Default is "false", using the common JDBC 2.0 {@code setBinaryStream}
119         * / {@code setCharacterStream} method for setting the content.
120         * Switch this to "true" for explicit JDBC 4.0 streaming, provided that your
121         * JDBC driver actually supports those JDBC 4.0 operations (e.g. Derby's).
122         * <p>This setting affects stream arguments as well as byte array / String
123         * arguments, requiring JDBC 4.0 support. For supporting LOB content against
124         * JDBC 3.0, check out the {@link #setWrapAsLob "wrapAsLob"} setting.
125         * @see java.sql.PreparedStatement#setBlob(int, java.io.InputStream, long)
126         * @see java.sql.PreparedStatement#setClob(int, java.io.Reader, long)
127         */
128        public void setStreamAsLob(boolean streamAsLob) {
129                this.streamAsLob = streamAsLob;
130        }
131
132        /**
133         * Specify whether to copy a byte array / String into a temporary JDBC
134         * Blob / Clob object created through the JDBC 4.0 {@code createBlob} /
135         * {@code createClob} methods.
136         * <p>Default is "false", using the common JDBC 2.0 {@code setBinaryStream}
137         * / {@code setCharacterStream} method for setting the content. Switch this
138         * to "true" for explicit Blob / Clob creation using JDBC 4.0.
139         * <p>This setting affects stream arguments as well as byte array / String
140         * arguments, requiring JDBC 4.0 support. For supporting LOB content against
141         * JDBC 3.0, check out the {@link #setWrapAsLob "wrapAsLob"} setting.
142         * @see java.sql.Connection#createBlob()
143         * @see java.sql.Connection#createClob()
144         */
145        public void setCreateTemporaryLob(boolean createTemporaryLob) {
146                this.createTemporaryLob = createTemporaryLob;
147        }
148
149
150        @Override
151        @Nullable
152        public byte[] getBlobAsBytes(ResultSet rs, int columnIndex) throws SQLException {
153                logger.debug("Returning BLOB as bytes");
154                if (this.wrapAsLob) {
155                        Blob blob = rs.getBlob(columnIndex);
156                        return blob.getBytes(1, (int) blob.length());
157                }
158                else {
159                        return rs.getBytes(columnIndex);
160                }
161        }
162
163        @Override
164        @Nullable
165        public InputStream getBlobAsBinaryStream(ResultSet rs, int columnIndex) throws SQLException {
166                logger.debug("Returning BLOB as binary stream");
167                if (this.wrapAsLob) {
168                        Blob blob = rs.getBlob(columnIndex);
169                        return blob.getBinaryStream();
170                }
171                else {
172                        return rs.getBinaryStream(columnIndex);
173                }
174        }
175
176        @Override
177        @Nullable
178        public String getClobAsString(ResultSet rs, int columnIndex) throws SQLException {
179                logger.debug("Returning CLOB as string");
180                if (this.wrapAsLob) {
181                        Clob clob = rs.getClob(columnIndex);
182                        return clob.getSubString(1, (int) clob.length());
183                }
184                else {
185                        return rs.getString(columnIndex);
186                }
187        }
188
189        @Override
190        public InputStream getClobAsAsciiStream(ResultSet rs, int columnIndex) throws SQLException {
191                logger.debug("Returning CLOB as ASCII stream");
192                if (this.wrapAsLob) {
193                        Clob clob = rs.getClob(columnIndex);
194                        return clob.getAsciiStream();
195                }
196                else {
197                        return rs.getAsciiStream(columnIndex);
198                }
199        }
200
201        @Override
202        public Reader getClobAsCharacterStream(ResultSet rs, int columnIndex) throws SQLException {
203                logger.debug("Returning CLOB as character stream");
204                if (this.wrapAsLob) {
205                        Clob clob = rs.getClob(columnIndex);
206                        return clob.getCharacterStream();
207                }
208                else {
209                        return rs.getCharacterStream(columnIndex);
210                }
211        }
212
213        @Override
214        public LobCreator getLobCreator() {
215                return (this.createTemporaryLob ? new TemporaryLobCreator() : new DefaultLobCreator());
216        }
217
218
219        /**
220         * Default LobCreator implementation as an inner class.
221         * Can be subclassed in DefaultLobHandler extensions.
222         */
223        protected class DefaultLobCreator implements LobCreator {
224
225                @Override
226                public void setBlobAsBytes(PreparedStatement ps, int paramIndex, @Nullable byte[] content)
227                                throws SQLException {
228
229                        if (streamAsLob) {
230                                if (content != null) {
231                                        ps.setBlob(paramIndex, new ByteArrayInputStream(content), content.length);
232                                }
233                                else {
234                                        ps.setBlob(paramIndex, (Blob) null);
235                                }
236                        }
237                        else if (wrapAsLob) {
238                                if (content != null) {
239                                        ps.setBlob(paramIndex, new PassThroughBlob(content));
240                                }
241                                else {
242                                        ps.setBlob(paramIndex, (Blob) null);
243                                }
244                        }
245                        else {
246                                ps.setBytes(paramIndex, content);
247                        }
248                        if (logger.isDebugEnabled()) {
249                                logger.debug(content != null ? "Set bytes for BLOB with length " + content.length :
250                                                "Set BLOB to null");
251                        }
252                }
253
254                @Override
255                public void setBlobAsBinaryStream(
256                                PreparedStatement ps, int paramIndex, @Nullable InputStream binaryStream, int contentLength)
257                                throws SQLException {
258
259                        if (streamAsLob) {
260                                if (binaryStream != null) {
261                                        if (contentLength >= 0) {
262                                                ps.setBlob(paramIndex, binaryStream, contentLength);
263                                        }
264                                        else {
265                                                ps.setBlob(paramIndex, binaryStream);
266                                        }
267                                }
268                                else {
269                                        ps.setBlob(paramIndex, (Blob) null);
270                                }
271                        }
272                        else if (wrapAsLob) {
273                                if (binaryStream != null) {
274                                        ps.setBlob(paramIndex, new PassThroughBlob(binaryStream, contentLength));
275                                }
276                                else {
277                                        ps.setBlob(paramIndex, (Blob) null);
278                                }
279                        }
280                        else if (contentLength >= 0) {
281                                ps.setBinaryStream(paramIndex, binaryStream, contentLength);
282                        }
283                        else {
284                                ps.setBinaryStream(paramIndex, binaryStream);
285                        }
286                        if (logger.isDebugEnabled()) {
287                                logger.debug(binaryStream != null ? "Set binary stream for BLOB with length " + contentLength :
288                                                "Set BLOB to null");
289                        }
290                }
291
292                @Override
293                public void setClobAsString(PreparedStatement ps, int paramIndex, @Nullable String content)
294                                throws SQLException {
295
296                        if (streamAsLob) {
297                                if (content != null) {
298                                        ps.setClob(paramIndex, new StringReader(content), content.length());
299                                }
300                                else {
301                                        ps.setClob(paramIndex, (Clob) null);
302                                }
303                        }
304                        else if (wrapAsLob) {
305                                if (content != null) {
306                                        ps.setClob(paramIndex, new PassThroughClob(content));
307                                }
308                                else {
309                                        ps.setClob(paramIndex, (Clob) null);
310                                }
311                        }
312                        else {
313                                ps.setString(paramIndex, content);
314                        }
315                        if (logger.isDebugEnabled()) {
316                                logger.debug(content != null ? "Set string for CLOB with length " + content.length() :
317                                                "Set CLOB to null");
318                        }
319                }
320
321                @Override
322                public void setClobAsAsciiStream(
323                                PreparedStatement ps, int paramIndex, @Nullable InputStream asciiStream, int contentLength)
324                                throws SQLException {
325
326                        if (streamAsLob) {
327                                if (asciiStream != null) {
328                                        Reader reader = new InputStreamReader(asciiStream, StandardCharsets.US_ASCII);
329                                        if (contentLength >= 0) {
330                                                ps.setClob(paramIndex, reader, contentLength);
331                                        }
332                                        else {
333                                                ps.setClob(paramIndex, reader);
334                                        }
335                                }
336                                else {
337                                        ps.setClob(paramIndex, (Clob) null);
338                                }
339                        }
340                        else if (wrapAsLob) {
341                                if (asciiStream != null) {
342                                        ps.setClob(paramIndex, new PassThroughClob(asciiStream, contentLength));
343                                }
344                                else {
345                                        ps.setClob(paramIndex, (Clob) null);
346                                }
347                        }
348                        else if (contentLength >= 0) {
349                                ps.setAsciiStream(paramIndex, asciiStream, contentLength);
350                        }
351                        else {
352                                ps.setAsciiStream(paramIndex, asciiStream);
353                        }
354                        if (logger.isDebugEnabled()) {
355                                logger.debug(asciiStream != null ? "Set ASCII stream for CLOB with length " + contentLength :
356                                                "Set CLOB to null");
357                        }
358                }
359
360                @Override
361                public void setClobAsCharacterStream(
362                                PreparedStatement ps, int paramIndex, @Nullable Reader characterStream, int contentLength)
363                                throws SQLException {
364
365                        if (streamAsLob) {
366                                if (characterStream != null) {
367                                        if (contentLength >= 0) {
368                                                ps.setClob(paramIndex, characterStream, contentLength);
369                                        }
370                                        else {
371                                                ps.setClob(paramIndex, characterStream);
372                                        }
373                                }
374                                else {
375                                        ps.setClob(paramIndex, (Clob) null);
376                                }
377                        }
378                        else if (wrapAsLob) {
379                                if (characterStream != null) {
380                                        ps.setClob(paramIndex, new PassThroughClob(characterStream, contentLength));
381                                }
382                                else {
383                                        ps.setClob(paramIndex, (Clob) null);
384                                }
385                        }
386                        else if (contentLength >= 0) {
387                                ps.setCharacterStream(paramIndex, characterStream, contentLength);
388                        }
389                        else {
390                                ps.setCharacterStream(paramIndex, characterStream);
391                        }
392                        if (logger.isDebugEnabled()) {
393                                logger.debug(characterStream != null ? "Set character stream for CLOB with length " + contentLength :
394                                                "Set CLOB to null");
395                        }
396                }
397
398                @Override
399                public void close() {
400                        // nothing to do when not creating temporary LOBs
401                }
402        }
403
404}