diff options
author | Roman Kennke <roman@kennke.org> | 2007-03-27 09:51:26 +0000 |
---|---|---|
committer | Roman Kennke <roman@kennke.org> | 2007-03-27 09:51:26 +0000 |
commit | 10bde828619168f3dbb2d70426eb0d110039b6dc (patch) | |
tree | a3cd08e7254f6a01e0ff0807fce93552099f4512 /java | |
parent | 38b3923ebff100a2654bcc0df731d3d2fc52e452 (diff) | |
download | classpath-10bde828619168f3dbb2d70426eb0d110039b6dc.tar.gz |
2007-03-27 Roman Kennke <roman@kennke.org>
* java/io/InputStreamReader.java
(BUFFER_SIZE): New constant.
(bytesCache): Removed.
(cacheLock): Removed.
(hasSavedSurrogate): Removed.
(lastArray): New field. Used for caching CharBuffers.
(lastBuffer): New field. Used for caching CharBuffers.
(maxBytesPerChar): Removed.
(oneChar): New field. Caches a char array for read().
(savedSurrogate): New field.
(InputStreamReader): (all constructors) Cleaned up.
Use initDecoderAndBuffer() method. Check for null parameters.
Use new EncodingHelper.getDefaultCharset() for fetching the
default charset.
(decode): New helper method. Decodes using the NIO decoder or
using a raw Latin1 decoding.
(getCharBuffer): New helper method. Implements caching of
CharBuffers for output arrays.
(initDecoderAndBuffer): New helper method. Initializes the decoder
and input buffer.
(read): Use cached array.
(read(char[],int,int)): Reworked using a cleaner NIO based
implementation. This decodes the incoming data in bigger chunks
rather then calling the decoder for each character.
(ready): Also check the input buffer.
(refillInputBuffer): New helper methods. Refills the input buffer
when it runs out of data.
* java/io/OutputStreamWriter.java
(lastArray): Implements caching of the output array buffer.
(lastBuffer): Implements caching of the output array buffer.
(oneChar): New field. Caches a char array for write().
(outputBuffer): Make this a ByteBuffer.
(OutputStreamWriter): (all constructors) Cleaned up.
Use initEncoderAndBuffer() method. Check for null parameters.
Use new EncodingHelper.getDefaultCharset() for fetching the
default charset.
(encode): New helper method. Encodes the input buffer to the output
buffer using either the NIO encoder or a raw Latin1 encoding.
(encodeChars): New helper method. The encoding loop.
(flush): Directly use the array of the output buffer.
(getCharBuffer): New helper method. Implements caching of the
output buffer.
(initEncoderAndBuffer): New helper method for initialization.
(write(char[],int,int)): Reworked to make better use of the NIO
encoders.
(write): Use cached array.
(write(String,int,int)): Don't copy the string but rather wrap it
and handle it the same as the wrapped char array.
(writeConvert): Removed.
* gnu/java/nio/charset/EncodingHelper.java
(getDefaultCharset): New method. Returns the default charset for
the case when the file.encoding charset is not valid. This
always returns an UTF8 codec.
Diffstat (limited to 'java')
-rw-r--r-- | java/io/InputStreamReader.java | 434 | ||||
-rw-r--r-- | java/io/OutputStreamWriter.java | 347 |
2 files changed, 434 insertions, 347 deletions
diff --git a/java/io/InputStreamReader.java b/java/io/InputStreamReader.java index 6c5297f6b..bc9932c75 100644 --- a/java/io/InputStreamReader.java +++ b/java/io/InputStreamReader.java @@ -98,6 +98,11 @@ import java.nio.charset.CodingErrorAction; public class InputStreamReader extends Reader { /** + * The default buffer size. + */ + private final static int BUFFER_SIZE = 1024; + + /** * The input stream. */ private InputStream in; @@ -113,11 +118,6 @@ public class InputStreamReader extends Reader private boolean isDone = false; /** - * Need this. - */ - private float maxBytesPerChar; - - /** * Buffer holding surplus loaded bytes (if any) */ private ByteBuffer byteBuffer; @@ -128,21 +128,22 @@ public class InputStreamReader extends Reader private String encoding; /** - * We might decode to a 2-char UTF-16 surrogate, which won't fit in the - * output buffer. In this case we need to save the surrogate char. + * One char as array to be used in {@link #read()}. */ - private char savedSurrogate; - private boolean hasSavedSurrogate = false; + private char[] oneChar = new char[1]; /** - * A byte array to be reused in read(byte[], int, int). + * The last char array that has been passed to read(char[],int,int). This + * is used to cache the associated CharBuffer because read(char[],int,int) + * is usually called with the same array repeatedly and we don't want to + * allocate a new CharBuffer object on each call. */ - private byte[] bytesCache; + private char[] lastArray; /** - * Locks the bytesCache above in read(byte[], int, int). + * The cached CharBuffer associated with the above array. */ - private Object cacheLock = new Object(); + private CharBuffer lastBuffer; /** * This method initializes a new instance of <code>InputStreamReader</code> @@ -154,38 +155,31 @@ public class InputStreamReader extends Reader { if (in == null) throw new NullPointerException(); + this.in = in; - try - { - encoding = SystemProperties.getProperty("file.encoding"); - // Don't use NIO if avoidable - if(EncodingHelper.isISOLatin1(encoding)) - { - encoding = "ISO8859_1"; - maxBytesPerChar = 1f; - decoder = null; - return; - } - Charset cs = EncodingHelper.getCharset(encoding); - decoder = cs.newDecoder(); - encoding = EncodingHelper.getOldCanonical(cs.name()); - try { - maxBytesPerChar = cs.newEncoder().maxBytesPerChar(); - } catch(UnsupportedOperationException _){ - maxBytesPerChar = 1f; - } - decoder.onMalformedInput(CodingErrorAction.REPLACE); - decoder.onUnmappableCharacter(CodingErrorAction.REPLACE); - decoder.reset(); - } catch(RuntimeException e) { - encoding = "ISO8859_1"; - maxBytesPerChar = 1f; - decoder = null; - } catch(UnsupportedEncodingException e) { - encoding = "ISO8859_1"; - maxBytesPerChar = 1f; - decoder = null; - } + + String encodingName = SystemProperties.getProperty("file.encoding"); + try + { + Charset cs = EncodingHelper.getCharset(encodingName); + decoder = cs.newDecoder(); + // The encoding should be the old name, if such exists. + encoding = EncodingHelper.getOldCanonical(cs.name()); + } + catch(RuntimeException e) + { + // For bootstrapping problems only. + decoder = null; + encoding = "ISO8859_1"; + } + catch (UnsupportedEncodingException ex) + { + Charset cs = EncodingHelper.getDefaultCharset(); + decoder = cs.newDecoder(); + // The encoding should be the old name, if such exists. + encoding = EncodingHelper.getOldCanonical(cs.name()); + } + initDecoderAndBuffer(); } /** @@ -203,39 +197,26 @@ public class InputStreamReader extends Reader public InputStreamReader(InputStream in, String encoding_name) throws UnsupportedEncodingException { - if (in == null - || encoding_name == null) + if (in == null || encoding_name == null) throw new NullPointerException(); - + this.in = in; - // Don't use NIO if avoidable - if(EncodingHelper.isISOLatin1(encoding_name)) + + try { - encoding = "ISO8859_1"; - maxBytesPerChar = 1f; - decoder = null; - return; + Charset cs = EncodingHelper.getCharset(encoding_name); + decoder = cs.newDecoder(); + // The encoding should be the old name, if such exists. + encoding = EncodingHelper.getOldCanonical(cs.name()); + } + catch(RuntimeException e) + { + // For bootstrapping problems only. + decoder = null; + encoding = "ISO8859_1"; } - try { - Charset cs = EncodingHelper.getCharset(encoding_name); - try { - maxBytesPerChar = cs.newEncoder().maxBytesPerChar(); - } catch(UnsupportedOperationException _){ - maxBytesPerChar = 1f; - } - - decoder = cs.newDecoder(); - decoder.onMalformedInput(CodingErrorAction.REPLACE); - decoder.onUnmappableCharacter(CodingErrorAction.REPLACE); - decoder.reset(); - - // The encoding should be the old name, if such exists. - encoding = EncodingHelper.getOldCanonical(cs.name()); - } catch(RuntimeException e) { - encoding = "ISO8859_1"; - maxBytesPerChar = 1f; - decoder = null; - } + + initDecoderAndBuffer(); } /** @@ -245,22 +226,15 @@ public class InputStreamReader extends Reader * * @since 1.4 */ - public InputStreamReader(InputStream in, Charset charset) { - if (in == null) + public InputStreamReader(InputStream in, Charset charset) + { + if (in == null || charset == null) throw new NullPointerException(); + this.in = in; decoder = charset.newDecoder(); - - try { - maxBytesPerChar = charset.newEncoder().maxBytesPerChar(); - } catch(UnsupportedOperationException _){ - maxBytesPerChar = 1f; - } - - decoder.onMalformedInput(CodingErrorAction.REPLACE); - decoder.onUnmappableCharacter(CodingErrorAction.REPLACE); - decoder.reset(); encoding = EncodingHelper.getOldCanonical(charset.name()); + initDecoderAndBuffer(); } /** @@ -269,31 +243,34 @@ public class InputStreamReader extends Reader * * @since 1.4 */ - public InputStreamReader(InputStream in, CharsetDecoder decoder) { - if (in == null) + public InputStreamReader(InputStream in, CharsetDecoder decoder) + { + if (in == null || decoder == null) throw new NullPointerException(); + this.in = in; this.decoder = decoder; + encoding = EncodingHelper.getOldCanonical(decoder.charset().name()); + initDecoderAndBuffer(); + } - Charset charset = decoder.charset(); - try { - if (charset == null) - maxBytesPerChar = 1f; - else - maxBytesPerChar = charset.newEncoder().maxBytesPerChar(); - } catch(UnsupportedOperationException _){ - maxBytesPerChar = 1f; - } - - decoder.onMalformedInput(CodingErrorAction.REPLACE); - decoder.onUnmappableCharacter(CodingErrorAction.REPLACE); - decoder.reset(); - if (charset == null) - encoding = "US-ASCII"; - else - encoding = EncodingHelper.getOldCanonical(decoder.charset().name()); + /** + * Initializes the decoder and the input buffer. + */ + private void initDecoderAndBuffer() + { + if (decoder != null) + { + decoder.onMalformedInput(CodingErrorAction.REPLACE); + decoder.onUnmappableCharacter(CodingErrorAction.REPLACE); + decoder.reset(); + } + + byteBuffer = ByteBuffer.allocate(BUFFER_SIZE); + // No bytes available initially. + byteBuffer.position(byteBuffer.limit()); } - + /** * This method closes this stream, as well as the underlying * <code>InputStream</code>. @@ -342,8 +319,7 @@ public class InputStreamReader extends Reader { if (in == null) throw new IOException("Reader has been closed"); - - return in.available() != 0; + return byteBuffer.hasRemaining() || in.available() != 0; } /** @@ -365,109 +341,28 @@ public class InputStreamReader extends Reader throw new IOException("Reader has been closed"); if (isDone) return -1; - if(decoder != null) - { - int totalBytes = (int)((double) length * maxBytesPerChar); - if (byteBuffer != null) - totalBytes = Math.max(totalBytes, byteBuffer.remaining()); - byte[] bytes; - // Fetch cached bytes array if available and big enough. - synchronized(cacheLock) - { - bytes = bytesCache; - if (bytes == null || bytes.length < totalBytes) - bytes = new byte[totalBytes]; - else - bytesCache = null; - } - int remaining = 0; - if(byteBuffer != null) - { - remaining = byteBuffer.remaining(); - byteBuffer.get(bytes, 0, remaining); - } - int read; - if(totalBytes - remaining > 0) - { - read = in.read(bytes, remaining, totalBytes - remaining); - if(read == -1){ - read = remaining; - isDone = true; - } else - read += remaining; - } else - read = remaining; - byteBuffer = ByteBuffer.wrap(bytes, 0, read); - CharBuffer cb = CharBuffer.wrap(buf, offset, length); - int startPos = cb.position(); - - if(hasSavedSurrogate){ - hasSavedSurrogate = false; - cb.put(savedSurrogate); - read++; - } - - CoderResult cr = decoder.decode(byteBuffer, cb, isDone); - decoder.reset(); - // 1 char remains which is the first half of a surrogate pair. - if(cr.isOverflow() && cb.hasRemaining()){ - CharBuffer overflowbuf = CharBuffer.allocate(2); - cr = decoder.decode(byteBuffer, overflowbuf, isDone); - overflowbuf.flip(); - if(overflowbuf.hasRemaining()) - { - cb.put(overflowbuf.get()); - savedSurrogate = overflowbuf.get(); - hasSavedSurrogate = true; - isDone = false; - } - } - - if(byteBuffer.hasRemaining()) { - byteBuffer.compact(); - byteBuffer.flip(); - isDone = false; - } else - byteBuffer = null; - - read = cb.position() - startPos; - - // Put cached bytes array back if we are finished and the cache - // is null or smaller than the used bytes array. - synchronized (cacheLock) - { - if (byteBuffer == null - && (bytesCache == null || bytesCache.length < bytes.length)) - bytesCache = bytes; - } - return (read <= 0) ? -1 : read; - } - else + CharBuffer outBuffer = getCharBuffer(buf, offset, length); + int startPos = outBuffer.position(); + int remaining = outBuffer.remaining(); + int start = remaining; + CoderResult cr = null; + // Try to decode as long as the output buffer can hold more data. + // Decode at least one character (block when necessary). + boolean moreAvailable = true; + while (remaining > 0 && moreAvailable) { - byte[] bytes; - // Fetch cached bytes array if available and big enough. - synchronized (cacheLock) + if (byteBuffer.remaining() == 0 + || (cr != null && (cr.isUnderflow()))) { - bytes = bytesCache; - if (bytes == null || length < bytes.length) - bytes = new byte[length]; - else - bytesCache = null; + // Block when we have not yet decoded at least one character. + boolean block = remaining == start; + moreAvailable = refillInputBuffer(block); } - - int read = in.read(bytes); - for(int i=0;i<read;i++) - buf[offset+i] = (char)(bytes[i]&0xFF); - - // Put back byte array into cache if appropriate. - synchronized (cacheLock) - { - if (bytesCache == null || bytesCache.length < bytes.length) - bytesCache = bytes; - } - return read; - } + cr = decode(outBuffer); + remaining = outBuffer.remaining(); + } + return outBuffer.position() - startPos; } /** @@ -483,9 +378,8 @@ public class InputStreamReader extends Reader */ public int read() throws IOException { - char[] buf = new char[1]; - int count = read(buf, 0, 1); - return count > 0 ? buf[0] : -1; + int count = read(oneChar, 0, 1); + return count > 0 ? oneChar[0] : -1; } /** @@ -503,7 +397,127 @@ public class InputStreamReader extends Reader { if (in == null) throw new IOException("Reader has been closed"); - + return super.skip(count); } + + /** + * Returns a CharBuffer that wraps the specified char array. This tries + * to return a cached instance because usually the read() method is called + * repeatedly with the same char array instance, or the no-arg read + * method is called repeatedly which uses the oneChar field of this class + * over and over again. + * + * @param buf the array to wrap + * @param offset the offset + * @param length the length + * + * @return a prepared CharBuffer to write to + */ + private final CharBuffer getCharBuffer(char[] buf, int offset, int length) + { + CharBuffer outBuffer; + if (lastArray == buf) + { + outBuffer = lastBuffer; + outBuffer.position(offset); + outBuffer.limit(offset + length); + } + else + { + lastArray = buf; + lastBuffer = CharBuffer.wrap(buf, offset, length); + outBuffer = lastBuffer; + } + return outBuffer; + } + + /** + * Refills the input buffer by reading a chunk of bytes from the underlying + * input stream + * + * @param block true when this method is allowed to block when necessary, + * false otherwise + * + * @return true when data has been read, false when no data has been + * available without blocking + * + * @throws IOException from the underlying stream + */ + private final boolean refillInputBuffer(boolean block) + throws IOException + { + boolean refilled = false; + + // Refill input buffer. + byteBuffer.compact(); + if (byteBuffer.hasArray()) + { + byte[] buffer = byteBuffer.array(); + int offs = byteBuffer.arrayOffset(); + int pos = byteBuffer.position(); + int rem = byteBuffer.remaining(); + int avail = in.available(); + int readBytes = 0; + int len; + // Try to not block. + if (block) + len = avail != 0 ? Math.min(avail, rem) : rem; + else + len = Math.min(avail, rem); + readBytes = in.read(buffer, offs + pos, len); + + if (readBytes > 0) + { + byteBuffer.position(pos + readBytes); + byteBuffer.limit(pos + readBytes); + refilled = true; + } + isDone = readBytes == -1; + } + else + { + assert false; + // Shouldn't happen, but anyway... + byte[] buffer = new byte[byteBuffer.limit() + - byteBuffer.position()]; + int readBytes = in.read(buffer); + isDone = readBytes == -1; + byteBuffer.put(buffer); + } + byteBuffer.flip(); + return refilled; + } + + /** + * Decodes the current byteBuffer into the specified outBuffer. This takes + * care of the corner case when we have no decoder (i.e. bootstrap problems) + * and performs a primitive Latin1 decoding in this case. + * + * @param outBuffer the buffer to decode to + * + * @return the coder result + */ + private CoderResult decode(CharBuffer outBuffer) + { + CoderResult cr; + if (decoder != null) + { + cr = decoder.decode(byteBuffer, outBuffer, false); + } + else + { + // Perform primitive Latin1 decoding. + while (outBuffer.hasRemaining() && byteBuffer.hasRemaining()) + { + outBuffer.put((char) (0xff & byteBuffer.get())); + } + // One of the buffers must be drained. + if (! outBuffer.hasRemaining()) + cr = CoderResult.OVERFLOW; + else + cr = CoderResult.UNDERFLOW; + } + return cr; + } } diff --git a/java/io/OutputStreamWriter.java b/java/io/OutputStreamWriter.java index 26363401f..26b3c96d0 100644 --- a/java/io/OutputStreamWriter.java +++ b/java/io/OutputStreamWriter.java @@ -42,11 +42,10 @@ import gnu.java.nio.charset.EncodingHelper; 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; import java.nio.charset.CodingErrorAction; -import java.nio.charset.MalformedInputException; /** * This class writes characters to an output stream that is byte oriented @@ -84,6 +83,11 @@ import java.nio.charset.MalformedInputException; public class OutputStreamWriter extends Writer { /** + * The default buffer size. + */ + private final static int BUFFER_SIZE = 1024; + + /** * The output stream. */ private OutputStream out; @@ -99,10 +103,28 @@ public class OutputStreamWriter extends Writer private String encodingName; /** - * Buffer output before character conversion as it has costly overhead. + * This buffer receives the encoded data and is flushed to the underlying + * stream when it gets too full. */ - private CharBuffer outputBuffer; - private final static int BUFFER_SIZE = 1024; + private ByteBuffer outputBuffer; + + /** + * A one-char array to be reused in read(). + */ + private char[] oneChar = new char[1]; + + /** + * The last char array that has been passed to write(char[],int,int). This + * is used to cache the associated CharBuffer because write(char[],int,int) + * is usually called with the same array repeatedly and we don't want to + * allocate a new CharBuffer object on each call. + */ + private Object lastArray; + + /** + * The cached char buffer. + */ + private CharBuffer lastBuffer; /** * This method initializes a new instance of <code>OutputStreamWriter</code> @@ -120,60 +142,49 @@ public class OutputStreamWriter extends Writer public OutputStreamWriter (OutputStream out, String encoding_scheme) throws UnsupportedEncodingException { + if (out == null || encoding_scheme == null) + throw new NullPointerException(); + this.out = out; try { - // Don't use NIO if avoidable - if(EncodingHelper.isISOLatin1(encoding_scheme)) - { - encodingName = "ISO8859_1"; - encoder = null; - return; - } - - /* - * Workraround for encodings with a byte-order-mark. - * We only want to write it once per stream. - */ - try - { - if(encoding_scheme.equalsIgnoreCase("UnicodeBig") || - encoding_scheme.equalsIgnoreCase("UTF-16") || - encoding_scheme.equalsIgnoreCase("UTF16")) - { - encoding_scheme = "UTF-16BE"; - out.write((byte)0xFE); - out.write((byte)0xFF); - } - else if(encoding_scheme.equalsIgnoreCase("UnicodeLittle")){ - encoding_scheme = "UTF-16LE"; - out.write((byte)0xFF); - out.write((byte)0xFE); - } - } - catch(IOException ioe) - { - } - - outputBuffer = CharBuffer.allocate(BUFFER_SIZE); - - Charset cs = EncodingHelper.getCharset(encoding_scheme); - if(cs == null) - throw new UnsupportedEncodingException("Encoding "+encoding_scheme+ - " unknown"); - encoder = cs.newEncoder(); - encodingName = EncodingHelper.getOldCanonical(cs.name()); - - encoder.onMalformedInput(CodingErrorAction.REPLACE); - encoder.onUnmappableCharacter(CodingErrorAction.REPLACE); + /* + * Workraround for encodings with a byte-order-mark. + * We only want to write it once per stream. + */ + try + { + if(encoding_scheme.equalsIgnoreCase("UnicodeBig") + ||encoding_scheme.equalsIgnoreCase("UTF-16") || + encoding_scheme.equalsIgnoreCase("UTF16")) + { + encoding_scheme = "UTF-16BE"; + out.write((byte)0xFE); + out.write((byte)0xFF); + } + else if(encoding_scheme.equalsIgnoreCase("UnicodeLittle")){ + encoding_scheme = "UTF-16LE"; + out.write((byte)0xFF); + out.write((byte)0xFE); + } + } + catch(IOException ioe) + { + } + + Charset cs = EncodingHelper.getCharset(encoding_scheme); + encoder = cs.newEncoder(); + encodingName = EncodingHelper.getOldCanonical(cs.name()); + } catch(RuntimeException e) { - // Default to ISO Latin-1, will happen if this is called, for instance, - // before the NIO provider is loadable. - encoder = null; - encodingName = "ISO8859_1"; + // Default to ISO Latin-1, will happen if this is called, for instance, + // before the NIO provider is loadable. + encoder = null; + encodingName = "ISO8859_1"; } + initEncoderAndBuffer(); } /** @@ -185,26 +196,21 @@ public class OutputStreamWriter extends Writer public OutputStreamWriter (OutputStream out) { this.out = out; - outputBuffer = null; try { - String encoding = System.getProperty("file.encoding"); - Charset cs = Charset.forName(encoding); - encoder = cs.newEncoder(); - encodingName = EncodingHelper.getOldCanonical(cs.name()); + String encoding = System.getProperty("file.encoding"); + Charset cs = Charset.forName(encoding); + encoder = cs.newEncoder(); + encodingName = EncodingHelper.getOldCanonical(cs.name()); } catch(RuntimeException e) { - encoder = null; - encodingName = "ISO8859_1"; + // For bootstrap problems. + encoder = null; + encodingName = "ISO8859_1"; } - if(encoder != null) - { - encoder.onMalformedInput(CodingErrorAction.REPLACE); - encoder.onUnmappableCharacter(CodingErrorAction.REPLACE); - outputBuffer = CharBuffer.allocate(BUFFER_SIZE); - } + initEncoderAndBuffer(); } /** @@ -220,10 +226,8 @@ public class OutputStreamWriter extends Writer { this.out = out; encoder = cs.newEncoder(); - encoder.onMalformedInput(CodingErrorAction.REPLACE); - encoder.onUnmappableCharacter(CodingErrorAction.REPLACE); - outputBuffer = CharBuffer.allocate(BUFFER_SIZE); encodingName = EncodingHelper.getOldCanonical(cs.name()); + initEncoderAndBuffer(); } /** @@ -240,12 +244,25 @@ public class OutputStreamWriter extends Writer { this.out = out; encoder = enc; - outputBuffer = CharBuffer.allocate(BUFFER_SIZE); Charset cs = enc.charset(); if (cs == null) encodingName = "US-ASCII"; else encodingName = EncodingHelper.getOldCanonical(cs.name()); + initEncoderAndBuffer(); + } + + /** + * Initializes the encoder and the output buffer. + */ + private void initEncoderAndBuffer() + { + if (encoder != null) + { + encoder.onMalformedInput(CodingErrorAction.REPLACE); + encoder.onUnmappableCharacter(CodingErrorAction.REPLACE); + } + outputBuffer = ByteBuffer.allocate(BUFFER_SIZE); } /** @@ -282,18 +299,26 @@ public class OutputStreamWriter extends Writer */ public void flush () throws IOException { - if(out != null){ - if(outputBuffer != null){ - char[] buf = new char[outputBuffer.position()]; - if(buf.length > 0){ - outputBuffer.flip(); - outputBuffer.get(buf); - writeConvert(buf, 0, buf.length); - outputBuffer.clear(); - } - } - out.flush (); + int len = outputBuffer.position(); + if (len > 0) + { + outputBuffer.flip(); + if (outputBuffer.hasArray()) + { + byte[] bytes = outputBuffer.array(); + int p = outputBuffer.arrayOffset(); + out.write(bytes, p, len); + } + else + { + // Shouldn't happen for normal (non-direct) ByteBuffers. + byte[] bytes = new byte[len]; + outputBuffer.get(bytes); + out.write(bytes, 0, len); + } + outputBuffer.clear(); } + out.flush (); } /** @@ -314,59 +339,9 @@ public class OutputStreamWriter extends Writer if(buf == null) throw new IOException("Buffer is null."); - if(outputBuffer != null) - { - if(count >= outputBuffer.remaining()) - { - int r = outputBuffer.remaining(); - outputBuffer.put(buf, offset, r); - writeConvert(outputBuffer.array(), 0, BUFFER_SIZE); - outputBuffer.clear(); - offset += r; - count -= r; - // if the remaining bytes is larger than the whole buffer, - // just don't buffer. - if(count >= outputBuffer.remaining()){ - writeConvert(buf, offset, count); - return; - } - } - outputBuffer.put(buf, offset, count); - } else writeConvert(buf, offset, count); - } - - /** - * Converts and writes characters. - */ - private void writeConvert (char[] buf, int offset, int count) - throws IOException - { - if(encoder == null) - { - byte[] b = new byte[count]; - for(int i=0;i<count;i++) - b[i] = (byte)((buf[offset+i] <= 0xFF)?buf[offset+i]:'?'); - out.write(b); - } else { - try { - ByteBuffer output = encoder.encode(CharBuffer.wrap(buf,offset,count)); - encoder.reset(); - if(output.hasArray()) - out.write(output.array()); - else - { - byte[] outbytes = new byte[output.remaining()]; - output.get(outbytes); - out.write(outbytes); - } - } catch(IllegalStateException e) { - throw new IOException("Internal error."); - } catch(MalformedInputException e) { - throw new IOException("Invalid character sequence."); - } catch(CharacterCodingException e) { - throw new IOException("Unmappable character."); - } - } + CharBuffer charBuffer = getCharBuffer(buf, offset, count); + encodeChars(charBuffer); + flush(); } /** @@ -383,10 +358,16 @@ public class OutputStreamWriter extends Writer */ public void write (String str, int offset, int count) throws IOException { - if(str == null) - throw new IOException("String is null."); + if (out == null) + throw new IOException("Stream is closed."); + if (str == null) + throw new IOException("Buffer is null."); - write(str.toCharArray(), offset, count); + // Don't call str.toCharArray() here to avoid allocation. + // TODO: CharBuffer.wrap(String) should not allocate a char array either. + CharBuffer charBuffer = getCharBuffer(str, offset, count); + encodeChars(charBuffer); + flush(); } /** @@ -398,7 +379,99 @@ public class OutputStreamWriter extends Writer */ public void write (int ch) throws IOException { - write(new char[]{ (char)ch }, 0, 1); + oneChar[0] = (char) ch; + write(oneChar, 0, 1); + } + + /** + * Encodes the specified buffer of characters. The encoded data is stored + * in an intermediate buffer and only flushed when this buffer gets full. + * + * @param chars the characters to encode + * + * @throws IOException if something goes wrong on the underlying stream + */ + private void encodeChars(CharBuffer chars) + throws IOException + { + assert out != null; + assert encoder != null; + int remaining = chars.remaining(); + while (remaining > 0) + { + CoderResult cr = encode(chars); + remaining = chars.remaining(); + // Flush when the output buffer has no more space or when the + // space is not enough to hold more encoded data (that when the + // input buffer does not change). + if (cr.isOverflow()) + flush(); + } + } + + /** + * Encodes the specified CharBuffer into the output buffer. This takes + * care for the seldom case when we have no decoder, i.e. bootstrapping + * problems. + * + * @param chars the char buffer to encode + */ + private CoderResult encode(CharBuffer chars) + { + CoderResult cr; + if (encoder != null) + { + cr = encoder.encode(chars, outputBuffer, false); + } + else + { + // For bootstrapping weirdness. + // Perform primitive Latin1 decoding. + while (chars.hasRemaining() && outputBuffer.hasRemaining()) + { + outputBuffer.put((byte) (chars.get())); + } + // One of the buffers must be drained. + if (! outputBuffer.hasRemaining()) + cr = CoderResult.OVERFLOW; + else + cr = CoderResult.UNDERFLOW; + } + return cr; + } + + /** + * Returns a CharBuffer that wraps the specified char array. This tries + * to return a cached instance because usually the read() method is called + * repeatedly with the same char array instance, or the no-arg read + * method is called repeatedly which uses the oneChar field of this class + * over and over again. + * + * @param buf the array to wrap + * @param offset the offset + * @param length the length + * + * @return a prepared CharBuffer to write to + */ + private final CharBuffer getCharBuffer(Object buf, int offset, int length) + { + CharBuffer outBuffer; + if (lastArray == buf) + { + outBuffer = lastBuffer; + outBuffer.position(offset); + outBuffer.limit(offset + length); + } + else + { + lastArray = buf; + if (buf instanceof String) + lastBuffer = CharBuffer.wrap((String) buf, offset, length); + else + lastBuffer = CharBuffer.wrap((char[]) buf, offset, length); + outBuffer = lastBuffer; + } + return outBuffer; } } // class OutputStreamWriter |