summaryrefslogtreecommitdiff
path: root/java/io/OutputStreamWriter.java
diff options
context:
space:
mode:
Diffstat (limited to 'java/io/OutputStreamWriter.java')
-rw-r--r--java/io/OutputStreamWriter.java347
1 files changed, 210 insertions, 137 deletions
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