Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
feat: add support for PreparedStatement.setCharacterStream(int, Reader)
In pgjdbc 9.4 (1211) and earlier, the PreparedStatement method setCharacterStream(int, Reader) throws a not implemented exception. The setCharacterStream(int, Reader, int) method does not throw an exception, but it materializes the Reader as a String and then uses the same code path as setString(int, String). Materializing the readers can cause memory usage issues (OutOfMemoryError) when working with large text values. This patch implements the setCharacterStream(int, Reader) method using in memory buffers for small streams and temp files for large streams. To do so, it uses code added in pull request #220 that was used to implement setBinaryStream(int, InputStream). This patch adds a utility class to convert the Reader's UTF-16 character stream into a UTF-8 binary stream. The setCharacterStream(int, Reader) method implemented in this patch supports both simple and extended query modes. However, in simple mode, it materializes the reader as a String just like setCharacterStream(int, Reader, int). fixes #671
- Loading branch information
Showing
with
669 additions
and 22 deletions.
- +12 −0 pgjdbc/src/main/java/org/postgresql/core/ParameterList.java
- +5 −0 pgjdbc/src/main/java/org/postgresql/core/v3/CompositeParameterList.java
- +5 −0 pgjdbc/src/main/java/org/postgresql/core/v3/SimpleParameterList.java
- +34 −22 pgjdbc/src/main/java/org/postgresql/jdbc/PgPreparedStatement.java
- +159 −0 pgjdbc/src/main/java/org/postgresql/util/ReaderInputStream.java
- +246 −0 pgjdbc/src/test/java/org/postgresql/test/jdbc4/CharacterStreamTest.java
- +1 −0 pgjdbc/src/test/java/org/postgresql/test/jdbc4/Jdbc4TestSuite.java
- +207 −0 pgjdbc/src/test/java/org/postgresql/util/ReaderInputStreamTest.java
@@ -0,0 +1,159 @@ | ||
/* | ||
* Copyright (c) 2016, PostgreSQL Global Development Group | ||
* See the LICENSE file in the project root for more information. | ||
*/ | ||
|
||
package org.postgresql.util; | ||
|
||
import java.io.IOException; | ||
import java.io.InputStream; | ||
import java.io.Reader; | ||
import java.nio.ByteBuffer; | ||
import java.nio.CharBuffer; | ||
import java.nio.charset.CharacterCodingException; | ||
import java.nio.charset.Charset; | ||
import java.nio.charset.CharsetEncoder; | ||
import java.nio.charset.CoderResult; | ||
|
||
/** | ||
* ReaderInputStream accepts a UTF-16 char stream (Reader) as input and | ||
* converts it to a UTF-8 byte stream (InputStream) as output. | ||
* | ||
* This is the inverse of java.io.InputStreamReader which converts a | ||
* binary stream to a character stream. | ||
*/ | ||
public class ReaderInputStream extends InputStream { | ||
private static final int DEFAULT_CHAR_BUFFER_SIZE = 8 * 1024; | ||
|
||
private static final Charset UTF_8 = Charset.forName("UTF-8"); | ||
|
||
private final Reader reader; | ||
private final CharsetEncoder encoder; | ||
private final ByteBuffer bbuf; | ||
private final CharBuffer cbuf; | ||
|
||
/** | ||
* true when all of the characters have been read from the reader into inbuf | ||
*/ | ||
private boolean endOfInput; | ||
private final byte[] oneByte = new byte[1]; | ||
|
||
public ReaderInputStream(Reader reader) { | ||
this(reader, DEFAULT_CHAR_BUFFER_SIZE); | ||
} | ||
|
||
/** | ||
* Allow ReaderInputStreamTest to use small buffers to force UTF-16 | ||
* surrogate pairs to cross buffer boundaries in interesting ways. | ||
* Because this constructor is package-private, the unit test must be in | ||
* the same package. | ||
*/ | ||
ReaderInputStream(Reader reader, int charBufferSize) { | ||
if (reader == null) { | ||
throw new IllegalArgumentException("reader cannot be null"); | ||
} | ||
|
||
// The standard UTF-8 encoder will only encode a UTF-16 surrogate pair | ||
// when both surrogates are available in the CharBuffer. | ||
if (charBufferSize < 2) { | ||
throw new IllegalArgumentException("charBufferSize must be at least 2 chars"); | ||
} | ||
|
||
this.reader = reader; | ||
this.encoder = UTF_8.newEncoder(); | ||
// encoder.maxBytesPerChar() always returns 3.0 for UTF-8 | ||
this.bbuf = ByteBuffer.allocate(3 * charBufferSize); | ||
this.bbuf.flip(); // prepare for subsequent write | ||
this.cbuf = CharBuffer.allocate(charBufferSize); | ||
this.cbuf.flip(); // prepare for subsequent write | ||
} | ||
|
||
private void advance() throws IOException { | ||
assert !endOfInput; | ||
assert !bbuf.hasRemaining() | ||
: "advance() should be called when output byte buffer is empty. bbuf: " + bbuf + ", as string: " + bbuf.asCharBuffer().toString(); | ||
assert cbuf.remaining() < 2; | ||
|
||
// given that bbuf.capacity = 3 x cbuf.capacity, the only time that we should have a | ||
// remaining char is if the last char read was the 1st half of a surrogate pair | ||
if (cbuf.remaining() == 0) { | ||
cbuf.clear(); | ||
} else { | ||
cbuf.compact(); | ||
} | ||
|
||
int n = reader.read(cbuf); // read #1 | ||
cbuf.flip(); | ||
|
||
CoderResult result; | ||
|
||
endOfInput = n == -1; | ||
|
||
bbuf.clear(); | ||
result = encoder.encode(cbuf, bbuf, endOfInput); | ||
checkEncodeResult(result); | ||
|
||
if (endOfInput) { | ||
result = encoder.flush(bbuf); | ||
checkEncodeResult(result); | ||
} | ||
|
||
bbuf.flip(); | ||
} | ||
|
||
private void checkEncodeResult(CoderResult result) throws CharacterCodingException { | ||
if (result.isError()) { | ||
result.throwException(); | ||
} | ||
} | ||
|
||
@Override | ||
public int read() throws IOException { | ||
int res = 0; | ||
while (res != -1) { | ||
res = read(oneByte); | ||
if (res != 0) { | ||
return oneByte[0]; | ||
} | ||
} | ||
return -1; | ||
} | ||
|
||
// The implementation of InputStream.read(byte[], int, int) silently ignores | ||
// an IOException thrown by overrides of the read() method. | ||
@Override | ||
public int read(byte b[], int off, int len) throws IOException { | ||
if (b == null) { | ||
throw new NullPointerException(); | ||
} else if (off < 0 || len < 0 || len > b.length - off) { | ||
throw new IndexOutOfBoundsException(); | ||
} else if (len == 0) { | ||
return 0; | ||
} | ||
if (endOfInput && !bbuf.hasRemaining()) { | ||
return -1; | ||
} | ||
|
||
int totalRead = 0; | ||
while (len > 0 && !endOfInput) { | ||
if (bbuf.hasRemaining()) { | ||
int remaining = Math.min(len, bbuf.remaining()); | ||
bbuf.get(b, off, remaining); | ||
totalRead += remaining; | ||
off += remaining; | ||
len -= remaining; | ||
if (len == 0) { | ||
return totalRead; | ||
} | ||
} | ||
advance(); | ||
} | ||
return totalRead; | ||
} | ||
|
||
@Override | ||
public void close() throws IOException { | ||
endOfInput = true; | ||
reader.close(); | ||
} | ||
} |
Oops, something went wrong.