summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRoman Kennke <roman@kennke.org>2007-03-27 09:51:26 +0000
committerRoman Kennke <roman@kennke.org>2007-03-27 09:51:26 +0000
commit10bde828619168f3dbb2d70426eb0d110039b6dc (patch)
treea3cd08e7254f6a01e0ff0807fce93552099f4512
parent38b3923ebff100a2654bcc0df731d3d2fc52e452 (diff)
downloadclasspath-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.
-rw-r--r--ChangeLog56
-rw-r--r--gnu/java/nio/charset/EncodingHelper.java11
-rw-r--r--java/io/InputStreamReader.java434
-rw-r--r--java/io/OutputStreamWriter.java347
4 files changed, 501 insertions, 347 deletions
diff --git a/ChangeLog b/ChangeLog
index 2f5e5ac64..b73a95a28 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,59 @@
+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.
+
2007-03-27 Roman Kennke <kennke@aicas.com>
* java/awt/Frame.java
diff --git a/gnu/java/nio/charset/EncodingHelper.java b/gnu/java/nio/charset/EncodingHelper.java
index 033440d5d..be7b4afe0 100644
--- a/gnu/java/nio/charset/EncodingHelper.java
+++ b/gnu/java/nio/charset/EncodingHelper.java
@@ -148,6 +148,17 @@ public class EncodingHelper
throw new UnsupportedEncodingException("Charset "+name+" not found.");
}
}
+
+ /**
+ * Returns the default charset without throwing any exceptions. The default
+ * charset is UTF8.
+ *
+ * @return the default charset
+ */
+ public static Charset getDefaultCharset()
+ {
+ return new UTF_8();
+ }
}
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