    001    /*
    002     * Licensed to the Apache Software Foundation (ASF) under one or more
    003     * contributor license agreements.  See the NOTICE file distributed with
    004     * this work for additional information regarding copyright ownership.
    005     * The ASF licenses this file to You under the Apache License, Version 2.0
    006     * (the "License"); you may not use this file except in compliance with
    007     * the License.  You may obtain a copy of the License at
    008     *
    009     *      http://www.apache.org/licenses/LICENSE-2.0
    010     *
    011     * Unless required by applicable law or agreed to in writing, software
    012     * distributed under the License is distributed on an "AS IS" BASIS,
    013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014     * See the License for the specific language governing permissions and
    015     * limitations under the License.
    016     */
    017    package org.apache.commons.io.output;
    018
    019    import java.io.IOException;
    020    import java.io.OutputStream;
    021    import java.io.Writer;
    022    import java.nio.ByteBuffer;
    023    import java.nio.CharBuffer;
    024    import java.nio.charset.Charset;
    025    import java.nio.charset.CharsetDecoder;
    026    import java.nio.charset.CoderResult;
    027    import java.nio.charset.CodingErrorAction;
    028
    029    /**
    030     * {@link OutputStream} implementation that transforms a byte stream to a
    031     * character stream using a specified charset encoding and writes the resulting
    032     * stream to a {@link Writer}. The stream is transformed using a
    033     * {@link CharsetDecoder} object, guaranteeing that all charset
    034     * encodings supported by the JRE are handled correctly.
    035     * <p>
    036     * The output of the {@link CharsetDecoder} is buffered using a fixed size buffer.
    037     * This implies that the data is written to the underlying {@link Writer} in chunks
    038     * that are no larger than the size of this buffer. By default, the buffer is
    039     * flushed only when it overflows or when {@link #flush()} or {@link #close()}
    040     * is called. In general there is therefore no need to wrap the underlying {@link Writer}
    041     * in a {@link java.io.BufferedWriter}. {@link WriterOutputStream} can also
    042     * be instructed to flush the buffer after each write operation. In this case, all
    043     * available data is written immediately to the underlying {@link Writer}, implying that
    044     * the current position of the {@link Writer} is correlated to the current position
    045     * of the {@link WriterOutputStream}.
    046     * <p>
    047     * {@link WriterOutputStream} implements the inverse transformation of {@link java.io.OutputStreamWriter};
    048     * in the following example, writing to <tt>out2</tt> would have the same result as writing to
    049     * <tt>out</tt> directly (provided that the byte sequence is legal with respect to the
    050     * charset encoding):
    051     * <pre>
    052     * OutputStream out = ...
    053     * Charset cs = ...
    054     * OutputStreamWriter writer = new OutputStreamWriter(out, cs);
    055     * WriterOutputStream out2 = new WriterOutputStream(writer, cs);</pre>
    056     * {@link WriterOutputStream} implements the same transformation as {@link java.io.InputStreamReader},
    057     * except that the control flow is reversed: both classes transform a byte stream
    058     * into a character stream, but {@link java.io.InputStreamReader} pulls data from the underlying stream,
    059     * while {@link WriterOutputStream} pushes it to the underlying stream.
    060     * <p>
    061     * Note that while there are use cases where there is no alternative to using
    062     * this class, very often the need to use this class is an indication of a flaw
    063     * in the design of the code. This class is typically used in situations where an existing
    064     * API only accepts an {@link OutputStream} object, but where the stream is known to represent
    065     * character data that must be decoded for further use.
    066     * <p>
    067     * Instances of {@link WriterOutputStream} are not thread safe.
    068     *
    069     * @see org.apache.commons.io.input.ReaderInputStream
    070     *
    071     * @author <a href="mailto:veithen@apache.org">Andreas Veithen</a>
    072     * @since Commons IO 2.0
    073     */
    074    public class WriterOutputStream extends OutputStream {
    075        private static final int DEFAULT_BUFFER_SIZE = 1024;
    076
    077        private final Writer writer;
    078        private final CharsetDecoder decoder;
    079        private final boolean writeImmediately;
    080
    081        /**
    082         * ByteBuffer used as input for the decoder. This buffer can be small
    083         * as it is used only to transfer the received data to the
    084         * decoder.
    085         */
    086        private final ByteBuffer decoderIn = ByteBuffer.allocate(128);
    087
    088        /**
    089         * CharBuffer used as output for the decoder. It should be
    090         * somewhat larger as we write from this buffer to the
    091         * underlying Writer.
    092         */
    093        private final CharBuffer decoderOut;
    094
    095        /**
    096         * Constructs a new {@link WriterOutputStream}.
    097         *
    098         * @param writer the target {@link Writer}
    099         * @param charset the charset encoding
    100         * @param bufferSize the size of the output buffer in number of characters
    101         * @param writeImmediately If <tt>true</tt> the output buffer will be flushed after each
    102         *                         write operation, i.e. all available data will be written to the
    103         *                         underlying {@link Writer} immediately. If <tt>false</tt>, the
    104         *                         output buffer will only be flushed when it overflows or when
    105         *                         {@link #flush()} or {@link #close()} is called.
    106         */
    107        public WriterOutputStream(Writer writer, Charset charset, int bufferSize, boolean writeImmediately) {
    108            this.writer = writer;
    109            decoder = charset.newDecoder();
    110            decoder.onMalformedInput(CodingErrorAction.REPLACE);
    111            decoder.onUnmappableCharacter(CodingErrorAction.REPLACE);
    112            decoder.replaceWith("?");
    113            this.writeImmediately = writeImmediately;
    114            decoderOut = CharBuffer.allocate(bufferSize);
    115        }
    116
    117        /**
    118         * Constructs a new {@link WriterOutputStream} with a default output buffer size of
    119         * 1024 characters. The output buffer will only be flushed when it overflows or when
    120         * {@link #flush()} or {@link #close()} is called.
    121         *
    122         * @param writer the target {@link Writer}
    123         * @param charset the charset encoding
    124         */
    125        public WriterOutputStream(Writer writer, Charset charset) {
    126            this(writer, charset, DEFAULT_BUFFER_SIZE, false);
    127        }
    128
    129        /**
    130         * Constructs a new {@link WriterOutputStream}.
    131         *
    132         * @param writer the target {@link Writer}
    133         * @param charsetName the name of the charset encoding
    134         * @param bufferSize the size of the output buffer in number of characters
    135         * @param writeImmediately If <tt>true</tt> the output buffer will be flushed after each
    136         *                         write operation, i.e. all available data will be written to the
    137         *                         underlying {@link Writer} immediately. If <tt>false</tt>, the
    138         *                         output buffer will only be flushed when it overflows or when
    139         *                         {@link #flush()} or {@link #close()} is called.
    140         */
    141        public WriterOutputStream(Writer writer, String charsetName, int bufferSize, boolean writeImmediately) {
    142            this(writer, Charset.forName(charsetName), bufferSize, writeImmediately);
    143        }
    144
    145        /**
    146         * Constructs a new {@link WriterOutputStream} with a default output buffer size of
    147         * 1024 characters. The output buffer will only be flushed when it overflows or when
    148         * {@link #flush()} or {@link #close()} is called.
    149         *
    150         * @param writer the target {@link Writer}
    151         * @param charsetName the name of the charset encoding
    152         */
    153        public WriterOutputStream(Writer writer, String charsetName) {
    154            this(writer, charsetName, DEFAULT_BUFFER_SIZE, false);
    155        }
    156
    157        /**
    158         * Constructs a new {@link WriterOutputStream} that uses the default character encoding
    159         * and with a default output buffer size of 1024 characters. The output buffer will only
    160         * be flushed when it overflows or when {@link #flush()} or {@link #close()} is called.
    161         *
    162         * @param writer the target {@link Writer}
    163         */
    164        public WriterOutputStream(Writer writer) {
    165            this(writer, Charset.defaultCharset(), DEFAULT_BUFFER_SIZE, false);
    166        }
    167
    168        /**
    169         * Write bytes from the specified byte array to the stream.
    170         *
    171         * @param b the byte array containing the bytes to write
    172         * @param off the start offset in the byte array
    173         * @param len the number of bytes to write
    174         * @throws IOException if an I/O error occurs
    175         */
    176        @Override
    177        public void write(byte[] b, int off, int len) throws IOException {
    178            while (len > 0) {
    179                int c = Math.min(len, decoderIn.remaining());
    180                decoderIn.put(b, off, c);
    181                processInput(false);
    182                len -= c;
    183                off += c;
    184            }
    185            if (writeImmediately) {
    186                flushOutput();
    187            }
    188        }
    189
    190        /**
    191         * Write bytes from the specified byte array to the stream.
    192         *
    193         * @param b the byte array containing the bytes to write
    194         * @throws IOException if an I/O error occurs
    195         */
    196        @Override
    197        public void write(byte[] b) throws IOException {
    198            write(b, 0, b.length);
    199        }
    200
    201        /**
    202         * Write a single byte to the stream.
    203         *
    204         * @param b the byte to write
    205         * @throws IOException if an I/O error occurs
    206         */
    207        @Override
    208        public void write(int b) throws IOException {
    209            write(new byte[] { (byte)b }, 0, 1);
    210        }
    211
    212        /**
    213         * Flush the stream. Any remaining content accumulated in the output buffer
    214         * will be written to the underlying {@link Writer}. After that
    215         * {@link Writer#flush()} will be called.
    216         * @throws IOException if an I/O error occurs
    217         */
    218        @Override
    219        public void flush() throws IOException {
    220            flushOutput();
    221            writer.flush();
    222        }
    223
    224        /**
    225         * Close the stream. Any remaining content accumulated in the output buffer
    226         * will be written to the underlying {@link Writer}. After that
    227         * {@link Writer#close()} will be called.
    228         * @throws IOException if an I/O error occurs
    229         */
    230        @Override
    231        public void close() throws IOException {
    232            processInput(true);
    233            flushOutput();
    234            writer.close();
    235        }
    236
    237        /**
    238         * Decode the contents of the input ByteBuffer into a CharBuffer.
    239         *
    240         * @param endOfInput indicates end of input
    241         * @throws IOException if an I/O error occurs
    242         */
    243        private void processInput(boolean endOfInput) throws IOException {
    244            // Prepare decoderIn for reading
    245            decoderIn.flip();
    246            CoderResult coderResult;
    247            while (true) {
    248                coderResult = decoder.decode(decoderIn, decoderOut, endOfInput);
    249                if (coderResult.isOverflow()) {
    250                    flushOutput();
    251                } else if (coderResult.isUnderflow()) {
    252                    break;
    253                } else {
    254                    // The decoder is configured to replace malformed input and unmappable characters,
    255                    // so we should not get here.
    256                    throw new IOException("Unexpected coder result");
    257                }
    258            }
    259            // Discard the bytes that have been read
    260            decoderIn.compact();
    261        }
    262
    263        /**
    264         * Flush the output.
    265         *
    266         * @throws IOException if an I/O error occurs
    267         */
    268        private void flushOutput() throws IOException {
    269            if (decoderOut.position() > 0) {
    270                writer.write(decoderOut.array(), 0, decoderOut.position());
    271                decoderOut.rewind();
    272            }
    273        }
    274    }