From d586513d66f75de2bf43a83c6c4caf2ae7cd8060 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 31 Jan 2024 17:12:05 +0100 Subject: [PATCH] Introduce SqlBinaryValue/SqlCharacterValue as alternative to SqlLobValue Includes direct byte array support via setBytes in StatementCreatorUtils. Closes gh-32161 --- .../jdbc/core/StatementCreatorUtils.java | 7 +- .../jdbc/core/support/SqlBinaryValue.java | 150 ++++++++++++++++++ .../jdbc/core/support/SqlCharacterValue.java | 142 +++++++++++++++++ .../jdbc/core/support/SqlLobValue.java | 9 +- .../core/support/SqlBinaryValueTests.java | 113 +++++++++++++ .../core/support/SqlCharacterValueTests.java | 134 ++++++++++++++++ 6 files changed, 551 insertions(+), 4 deletions(-) create mode 100644 spring-jdbc/src/main/java/org/springframework/jdbc/core/support/SqlBinaryValue.java create mode 100644 spring-jdbc/src/main/java/org/springframework/jdbc/core/support/SqlCharacterValue.java create mode 100644 spring-jdbc/src/test/java/org/springframework/jdbc/core/support/SqlBinaryValueTests.java create mode 100644 spring-jdbc/src/test/java/org/springframework/jdbc/core/support/SqlCharacterValueTests.java diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/StatementCreatorUtils.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/StatementCreatorUtils.java index d3b0a98dc99..5bd9f4571cb 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/StatementCreatorUtils.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/StatementCreatorUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -436,7 +436,10 @@ public abstract class StatementCreatorUtils { } else if (sqlType == SqlTypeValue.TYPE_UNKNOWN || (sqlType == Types.OTHER && "Oracle".equals(ps.getConnection().getMetaData().getDatabaseProductName()))) { - if (isStringValue(inValue.getClass())) { + if (inValue instanceof byte[] bytes) { + ps.setBytes(paramIndex, bytes); + } + else if (isStringValue(inValue.getClass())) { ps.setString(paramIndex, inValue.toString()); } else if (isDateValue(inValue.getClass())) { diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/SqlBinaryValue.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/SqlBinaryValue.java new file mode 100644 index 00000000000..4ac66702deb --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/SqlBinaryValue.java @@ -0,0 +1,150 @@ +/* + * Copyright 2002-2024 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 + * + * https://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.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Types; + +import org.springframework.core.io.InputStreamSource; +import org.springframework.core.io.Resource; +import org.springframework.jdbc.core.SqlTypeValue; +import org.springframework.lang.Nullable; + +/** + * Object to represent a binary parameter value for a SQL statement, e.g. + * a binary stream for a BLOB or a LONGVARBINARY or PostgreSQL BYTEA column. + * + *

Designed for use with {@link org.springframework.jdbc.core.JdbcTemplate} + * as well as {@link org.springframework.jdbc.core.simple.JdbcClient}, to be + * passed in as a parameter value wrapping the target content value. Can be + * combined with {@link org.springframework.jdbc.core.SqlParameterValue} for + * specifying a SQL type, e.g. + * {@code new SqlParameterValue(Types.BLOB, new SqlBinaryValue(myContent))}. + * With most database drivers, the type hint is not actually necessary. + * + * @author Juergen Hoeller + * @since 6.1.4 + * @see SqlCharacterValue + * @see org.springframework.jdbc.core.SqlParameterValue + */ +public class SqlBinaryValue implements SqlTypeValue { + + private final Object content; + + private final long length; + + + /** + * Create a new {@code SqlBinaryValue} for the given content. + * @param bytes the content as a byte array + */ + public SqlBinaryValue(byte[] bytes) { + this.content = bytes; + this.length = bytes.length; + } + + /** + * Create a new {@code SqlBinaryValue} for the given content. + * @param stream the content stream + * @param length the length of the content + */ + public SqlBinaryValue(InputStream stream, long length) { + this.content = stream; + this.length = length; + } + + /** + * Create a new {@code SqlBinaryValue} for the given content. + *

Consider specifying a {@link Resource} with content length support + * when available: {@link SqlBinaryValue#SqlBinaryValue(Resource)}. + * @param resource the resource to obtain a content stream from + * @param length the length of the content + */ + public SqlBinaryValue(InputStreamSource resource, long length) { + this.content = resource; + this.length = length; + } + + /** + * Create a new {@code SqlBinaryValue} for the given content. + *

The length will get derived from {@link Resource#contentLength()}. + * @param resource the resource to obtain a content stream from + */ + public SqlBinaryValue(Resource resource) { + this.content = resource; + this.length = -1; + } + + + @Override + public void setTypeValue(PreparedStatement ps, int paramIndex, int sqlType, @Nullable String typeName) + throws SQLException { + + if (this.content instanceof byte[] bytes) { + setByteArray(ps, paramIndex, sqlType, bytes); + } + else if (this.content instanceof InputStream inputStream) { + setInputStream(ps, paramIndex, sqlType, inputStream, this.length); + } + else if (this.content instanceof Resource resource) { + try { + setInputStream(ps, paramIndex, sqlType, resource.getInputStream(), resource.contentLength()); + } + catch (IOException ex) { + throw new IllegalArgumentException("Cannot open binary stream for JDBC value: " + resource, ex); + } + } + else if (this.content instanceof InputStreamSource resource) { + try { + setInputStream(ps, paramIndex, sqlType, resource.getInputStream(), this.length); + } + catch (IOException ex) { + throw new IllegalArgumentException("Cannot open binary stream for JDBC value: " + resource, ex); + } + } + else { + throw new IllegalArgumentException("Illegal content type: " + this.content.getClass().getName()); + } + } + + private void setByteArray(PreparedStatement ps, int paramIndex, int sqlType, byte[] bytes) + throws SQLException { + + if (sqlType == Types.BLOB) { + ps.setBlob(paramIndex, new ByteArrayInputStream(bytes), bytes.length); + } + else { + ps.setBytes(paramIndex, bytes); + } + } + + private void setInputStream(PreparedStatement ps, int paramIndex, int sqlType, InputStream is, long length) + throws SQLException { + + if (sqlType == Types.BLOB) { + ps.setBlob(paramIndex, is, length); + } + else { + ps.setBinaryStream(paramIndex, is, length); + } + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/SqlCharacterValue.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/SqlCharacterValue.java new file mode 100644 index 00000000000..655e60b3008 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/SqlCharacterValue.java @@ -0,0 +1,142 @@ +/* + * Copyright 2002-2024 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 + * + * https://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.CharArrayReader; +import java.io.InputStream; +import java.io.Reader; +import java.io.StringReader; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Types; + +import org.springframework.jdbc.core.SqlTypeValue; +import org.springframework.lang.Nullable; + +/** + * Object to represent a character-based parameter value for a SQL statement, + * e.g. a character stream for a CLOB/NCLOB or a LONGVARCHAR column. + * + *

Designed for use with {@link org.springframework.jdbc.core.JdbcTemplate} + * as well as {@link org.springframework.jdbc.core.simple.JdbcClient}, to be + * passed in as a parameter value wrapping the target content value. Can be + * combined with {@link org.springframework.jdbc.core.SqlParameterValue} for + * specifying a SQL type, e.g. + * {@code new SqlParameterValue(Types.CLOB, new SqlCharacterValue(myContent))}. + * With most database drivers, the type hint is not actually necessary. + * + * @author Juergen Hoeller + * @since 6.1.4 + * @see SqlBinaryValue + * @see org.springframework.jdbc.core.SqlParameterValue + */ +public class SqlCharacterValue implements SqlTypeValue { + + private final Object content; + + private final long length; + + + /** + * Create a new CLOB value with the given content string. + * @param string the content as a String or other CharSequence + */ + public SqlCharacterValue(CharSequence string) { + this.content = string; + this.length = string.length(); + } + + /** + * Create a new {@code SqlCharacterValue} for the given content. + * @param characters the content as a character array + */ + public SqlCharacterValue(char[] characters) { + this.content = characters; + this.length = characters.length; + } + + /** + * Create a new {@code SqlCharacterValue} for the given content. + * @param reader the content reader + * @param length the length of the content + */ + public SqlCharacterValue(Reader reader, long length) { + this.content = reader; + this.length = length; + } + + /** + * Create a new {@code SqlCharacterValue} for the given content. + * @param asciiStream the content as ASCII stream + * @param length the length of the content + */ + public SqlCharacterValue(InputStream asciiStream, long length) { + this.content = asciiStream; + this.length = length; + } + + + @Override + public void setTypeValue(PreparedStatement ps, int paramIndex, int sqlType, @Nullable String typeName) + throws SQLException { + + if (this.content instanceof CharSequence) { + setString(ps, paramIndex, sqlType, this.content.toString()); + } + else if (this.content instanceof char[] chars) { + setReader(ps, paramIndex, sqlType, new CharArrayReader(chars), this.length); + } + else if (this.content instanceof Reader reader) { + setReader(ps, paramIndex, sqlType, reader, this.length); + } + else if (this.content instanceof InputStream asciiStream) { + ps.setAsciiStream(paramIndex, asciiStream, this.length); + } + else { + throw new IllegalArgumentException("Illegal content type: " + this.content.getClass().getName()); + } + } + + private void setString(PreparedStatement ps, int paramIndex, int sqlType, String string) + throws SQLException { + + if (sqlType == Types.CLOB) { + ps.setClob(paramIndex, new StringReader(string), string.length()); + } + else if (sqlType == Types.NCLOB) { + ps.setNClob(paramIndex, new StringReader(string), string.length()); + } + else { + ps.setString(paramIndex, string); + } + } + + private void setReader(PreparedStatement ps, int paramIndex, int sqlType, Reader reader, long length) + throws SQLException { + + if (sqlType == Types.CLOB) { + ps.setClob(paramIndex, reader, length); + } + else if (sqlType == Types.NCLOB) { + ps.setNClob(paramIndex, reader, length); + } + else { + ps.setCharacterStream(paramIndex, reader, length); + } + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/SqlLobValue.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/SqlLobValue.java index 621921436a0..192dd580073 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/SqlLobValue.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/SqlLobValue.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -35,6 +35,11 @@ import org.springframework.lang.Nullable; * The type is based on which constructor is used. Instances of this class are * stateful and immutable: use them and discard them. * + *

NOTE: As of 6.1.4, this class is effectively superseded by + * {@link SqlBinaryValue} and {@link SqlCharacterValue} which are capable of + * modern BLOB/CLOB handling while also handling LONGVARBINARY/LONGVARCHAR. + * The only reason to keep using this class is a custom {@link LobHandler}. + * *

This class holds a reference to a {@link LobCreator} that must be closed after * the update has completed. This is done via a call to the {@link #cleanup()} method. * All handling of the {@code LobCreator} is done by the framework classes that use it - @@ -209,7 +214,7 @@ public class SqlLobValue implements DisposableSqlTypeValue { } /** - * Close the LobCreator, if any. + * Close the LobCreator. */ @Override public void cleanup() { diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/support/SqlBinaryValueTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/support/SqlBinaryValueTests.java new file mode 100644 index 00000000000..0bce17dd9ca --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/support/SqlBinaryValueTests.java @@ -0,0 +1,113 @@ +/* + * Copyright 2002-2024 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 + * + * https://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.ByteArrayInputStream; +import java.io.InputStream; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Types; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.io.ByteArrayResource; +import org.springframework.jdbc.support.JdbcUtils; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * @author Juergen Hoeller + * @since 6.1.4 + */ +class SqlBinaryValueTests { + + @Test + void withByteArray() throws SQLException { + byte[] content = new byte[] {0, 1, 2}; + SqlBinaryValue value = new SqlBinaryValue(content); + PreparedStatement ps = mock(); + value.setTypeValue(ps, 1, JdbcUtils.TYPE_UNKNOWN, null); + verify(ps).setBytes(1, content); + } + + @Test + void withByteArrayForBlob() throws SQLException { + byte[] content = new byte[] {0, 1, 2}; + SqlBinaryValue value = new SqlBinaryValue(content); + PreparedStatement ps = mock(); + value.setTypeValue(ps, 1, Types.BLOB, null); + verify(ps).setBlob(eq(1), any(ByteArrayInputStream.class), eq(3L)); + } + + @Test + void withInputStream() throws SQLException { + InputStream content = new ByteArrayInputStream(new byte[] {0, 1, 2}); + SqlBinaryValue value = new SqlBinaryValue(content, 3); + PreparedStatement ps = mock(); + value.setTypeValue(ps, 1, JdbcUtils.TYPE_UNKNOWN, null); + verify(ps).setBinaryStream(1, content, 3L); + } + + @Test + void withInputStreamForBlob() throws SQLException { + InputStream content = new ByteArrayInputStream(new byte[] {0, 1, 2}); + SqlBinaryValue value = new SqlBinaryValue(content, 3); + PreparedStatement ps = mock(); + value.setTypeValue(ps, 1, Types.BLOB, null); + verify(ps).setBlob(1, content, 3L); + } + + @Test + void withInputStreamSource() throws SQLException { + InputStream content = new ByteArrayInputStream(new byte[] {0, 1, 2}); + SqlBinaryValue value = new SqlBinaryValue(() -> content, 3); + PreparedStatement ps = mock(); + value.setTypeValue(ps, 1, JdbcUtils.TYPE_UNKNOWN, null); + verify(ps).setBinaryStream(1, content, 3L); + } + + @Test + void withInputStreamSourceForBlob() throws SQLException { + InputStream content = new ByteArrayInputStream(new byte[] {0, 1, 2}); + SqlBinaryValue value = new SqlBinaryValue(() -> content, 3); + PreparedStatement ps = mock(); + value.setTypeValue(ps, 1, Types.BLOB, null); + verify(ps).setBlob(1, content, 3L); + } + + @Test + void withResource() throws SQLException { + byte[] content = new byte[] {0, 1, 2}; + SqlBinaryValue value = new SqlBinaryValue(new ByteArrayResource(content)); + PreparedStatement ps = mock(); + value.setTypeValue(ps, 1, JdbcUtils.TYPE_UNKNOWN, null); + verify(ps).setBinaryStream(eq(1), any(ByteArrayInputStream.class), eq(3L)); + } + + @Test + void withResourceForBlob() throws SQLException { + InputStream content = new ByteArrayInputStream(new byte[] {0, 1, 2}); + SqlBinaryValue value = new SqlBinaryValue(() -> content, 3); + PreparedStatement ps = mock(); + value.setTypeValue(ps, 1, Types.BLOB, null); + verify(ps).setBlob(eq(1), any(ByteArrayInputStream.class), eq(3L)); + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/support/SqlCharacterValueTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/support/SqlCharacterValueTests.java new file mode 100644 index 00000000000..ead5902f3b0 --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/support/SqlCharacterValueTests.java @@ -0,0 +1,134 @@ +/* + * Copyright 2002-2024 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 + * + * https://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.ByteArrayInputStream; +import java.io.CharArrayReader; +import java.io.InputStream; +import java.io.Reader; +import java.io.StringReader; +import java.nio.charset.StandardCharsets; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Types; + +import org.junit.jupiter.api.Test; + +import org.springframework.jdbc.support.JdbcUtils; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * @author Juergen Hoeller + * @since 6.1.4 + */ +class SqlCharacterValueTests { + + @Test + void withString() throws SQLException { + String content = "abc"; + SqlCharacterValue value = new SqlCharacterValue(content); + PreparedStatement ps = mock(); + value.setTypeValue(ps, 1, JdbcUtils.TYPE_UNKNOWN, null); + verify(ps).setString(1, content); + } + + @Test + void withStringForClob() throws SQLException { + String content = "abc"; + SqlCharacterValue value = new SqlCharacterValue(content); + PreparedStatement ps = mock(); + value.setTypeValue(ps, 1, Types.CLOB, null); + verify(ps).setClob(eq(1), any(StringReader.class), eq(3L)); + } + + @Test + void withStringForNClob() throws SQLException { + String content = "abc"; + SqlCharacterValue value = new SqlCharacterValue(content); + PreparedStatement ps = mock(); + value.setTypeValue(ps, 1, Types.NCLOB, null); + verify(ps).setNClob(eq(1), any(StringReader.class), eq(3L)); + } + + @Test + void withCharArray() throws SQLException { + char[] content = "abc".toCharArray(); + SqlCharacterValue value = new SqlCharacterValue(content); + PreparedStatement ps = mock(); + value.setTypeValue(ps, 1, JdbcUtils.TYPE_UNKNOWN, null); + verify(ps).setCharacterStream(eq(1), any(CharArrayReader.class), eq(3L)); + } + + @Test + void withCharArrayForClob() throws SQLException { + char[] content = "abc".toCharArray(); + SqlCharacterValue value = new SqlCharacterValue(content); + PreparedStatement ps = mock(); + value.setTypeValue(ps, 1, Types.CLOB, null); + verify(ps).setClob(eq(1), any(CharArrayReader.class), eq(3L)); + } + + @Test + void withCharArrayForNClob() throws SQLException { + char[] content = "abc".toCharArray(); + SqlCharacterValue value = new SqlCharacterValue(content); + PreparedStatement ps = mock(); + value.setTypeValue(ps, 1, Types.NCLOB, null); + verify(ps).setNClob(eq(1), any(CharArrayReader.class), eq(3L)); + } + + @Test + void withReader() throws SQLException { + Reader content = new StringReader("abc"); + SqlCharacterValue value = new SqlCharacterValue(content, 3); + PreparedStatement ps = mock(); + value.setTypeValue(ps, 1, JdbcUtils.TYPE_UNKNOWN, null); + verify(ps).setCharacterStream(1, content, 3L); + } + + @Test + void withReaderForClob() throws SQLException { + Reader content = new StringReader("abc"); + SqlCharacterValue value = new SqlCharacterValue(content, 3); + PreparedStatement ps = mock(); + value.setTypeValue(ps, 1, Types.CLOB, null); + verify(ps).setClob(1, content, 3L); + } + + @Test + void withReaderForNClob() throws SQLException { + Reader content = new StringReader("abc"); + SqlCharacterValue value = new SqlCharacterValue(content, 3); + PreparedStatement ps = mock(); + value.setTypeValue(ps, 1, Types.NCLOB, null); + verify(ps).setNClob(1, content, 3L); + } + + @Test + void withAsciiStream() throws SQLException { + InputStream content = new ByteArrayInputStream("abc".getBytes(StandardCharsets.US_ASCII)); + SqlCharacterValue value = new SqlCharacterValue(content, 3); + PreparedStatement ps = mock(); + value.setTypeValue(ps, 1, JdbcUtils.TYPE_UNKNOWN, null); + verify(ps).setAsciiStream(1, content, 3L); + } + +}