/*
 * 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.

/** * <p>ReaderInputStream accepts a UTF-16 char stream (Reader) as input and * converts it to a UTF-8 byte stream (InputStream) as output.</p> * * <p>This is the inverse of java.io.InputStreamReader which converts a * binary stream to a character stream.</p> */
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.
/** * 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.
/** * 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] & 0xFF); } } 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(); } if (endOfInput && !bbuf.hasRemaining() && totalRead == 0) { return -1; } return totalRead; } @Override public void close() throws IOException { endOfInput = true; reader.close(); } }