1/*
2 *  Licensed to the Apache Software Foundation (ASF) under one or more
3 *  contributor license agreements.  See the NOTICE file distributed with
4 *  this work for additional information regarding copyright ownership.
5 *  The ASF licenses this file to You under the Apache License, Version 2.0
6 *  (the "License"); you may not use this file except in compliance with
7 *  the License.  You may obtain a copy of the License at
8 *
9 *     http://www.apache.org/licenses/LICENSE-2.0
10 *
11 *  Unless required by applicable law or agreed to in writing, software
12 *  distributed under the License is distributed on an "AS IS" BASIS,
13 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 *  See the License for the specific language governing permissions and
15 *  limitations under the License.
16 */
17
18package java.io;
19
20import java.nio.ByteBuffer;
21import java.nio.CharBuffer;
22import java.nio.charset.Charset;
23import java.nio.charset.CharsetEncoder;
24import java.nio.charset.CoderResult;
25import java.nio.charset.CodingErrorAction;
26import java.util.Arrays;
27
28/**
29 * A class for turning a character stream into a byte stream. Data written to
30 * the target input stream is converted into bytes by either a default or a
31 * provided character converter. The default encoding is taken from the
32 * "file.encoding" system property. {@code OutputStreamWriter} contains a buffer
33 * of bytes to be written to target stream and converts these into characters as
34 * needed. The buffer size is 8K.
35 *
36 * @see InputStreamReader
37 */
38public class OutputStreamWriter extends Writer {
39
40    private final OutputStream out;
41
42    private CharsetEncoder encoder;
43
44    private ByteBuffer bytes = ByteBuffer.allocate(8192);
45
46    /**
47     * Constructs a new OutputStreamWriter using {@code out} as the target
48     * stream to write converted characters to. The default character encoding
49     * is used.
50     *
51     * @param out
52     *            the non-null target stream to write converted bytes to.
53     */
54    public OutputStreamWriter(OutputStream out) {
55        this(out, Charset.defaultCharset());
56    }
57
58    /**
59     * Constructs a new OutputStreamWriter using {@code out} as the target
60     * stream to write converted characters to and {@code charsetName} as the character
61     * encoding. If the encoding cannot be found, an
62     * UnsupportedEncodingException error is thrown.
63     *
64     * @param out
65     *            the target stream to write converted bytes to.
66     * @param charsetName
67     *            the string describing the desired character encoding.
68     * @throws NullPointerException
69     *             if {@code charsetName} is {@code null}.
70     * @throws UnsupportedEncodingException
71     *             if the encoding specified by {@code charsetName} cannot be found.
72     */
73    public OutputStreamWriter(OutputStream out, final String charsetName)
74            throws UnsupportedEncodingException {
75        super(out);
76        if (charsetName == null) {
77            throw new NullPointerException("charsetName == null");
78        }
79        this.out = out;
80        try {
81            encoder = Charset.forName(charsetName).newEncoder();
82        } catch (Exception e) {
83            throw new UnsupportedEncodingException(charsetName);
84        }
85        encoder.onMalformedInput(CodingErrorAction.REPLACE);
86        encoder.onUnmappableCharacter(CodingErrorAction.REPLACE);
87    }
88
89    /**
90     * Constructs a new OutputStreamWriter using {@code out} as the target
91     * stream to write converted characters to and {@code cs} as the character
92     * encoding.
93     *
94     * @param out
95     *            the target stream to write converted bytes to.
96     * @param cs
97     *            the {@code Charset} that specifies the character encoding.
98     */
99    public OutputStreamWriter(OutputStream out, Charset cs) {
100        super(out);
101        this.out = out;
102        encoder = cs.newEncoder();
103        encoder.onMalformedInput(CodingErrorAction.REPLACE);
104        encoder.onUnmappableCharacter(CodingErrorAction.REPLACE);
105    }
106
107    /**
108     * Constructs a new OutputStreamWriter using {@code out} as the target
109     * stream to write converted characters to and {@code charsetEncoder} as the character
110     * encoder.
111     *
112     * @param out
113     *            the target stream to write converted bytes to.
114     * @param charsetEncoder
115     *            the character encoder used for character conversion.
116     */
117    public OutputStreamWriter(OutputStream out, CharsetEncoder charsetEncoder) {
118        super(out);
119        charsetEncoder.charset();
120        this.out = out;
121        encoder = charsetEncoder;
122    }
123
124    /**
125     * Closes this writer. This implementation flushes the buffer as well as the
126     * target stream. The target stream is then closed and the resources for the
127     * buffer and converter are released.
128     *
129     * <p>Only the first invocation of this method has any effect. Subsequent calls
130     * do nothing.
131     *
132     * @throws IOException
133     *             if an error occurs while closing this writer.
134     */
135    @Override
136    public void close() throws IOException {
137        synchronized (lock) {
138            if (encoder != null) {
139                drainEncoder();
140                flushBytes(false);
141                out.close();
142                encoder = null;
143                bytes = null;
144            }
145        }
146    }
147
148    /**
149     * Flushes this writer. This implementation ensures that all buffered bytes
150     * are written to the target stream. After writing the bytes, the target
151     * stream is flushed as well.
152     *
153     * @throws IOException
154     *             if an error occurs while flushing this writer.
155     */
156    @Override
157    public void flush() throws IOException {
158        flushBytes(true);
159    }
160
161    private void flushBytes(boolean flushUnderlyingStream) throws IOException {
162        synchronized (lock) {
163            checkStatus();
164            int position = bytes.position();
165            if (position > 0) {
166                bytes.flip();
167                out.write(bytes.array(), bytes.arrayOffset(), position);
168                bytes.clear();
169            }
170            if (flushUnderlyingStream) {
171                out.flush();
172            }
173        }
174    }
175
176    private void convert(CharBuffer chars) throws IOException {
177        while (true) {
178            CoderResult result = encoder.encode(chars, bytes, false);
179            if (result.isOverflow()) {
180                // Make room and try again.
181                flushBytes(false);
182                continue;
183            } else if (result.isError()) {
184                result.throwException();
185            }
186            break;
187        }
188    }
189
190    private void drainEncoder() throws IOException {
191        // Strictly speaking, I think it's part of the CharsetEncoder contract that you call
192        // encode with endOfInput true before flushing. Our ICU-based implementations don't
193        // actually need this, and you'd hope that any reasonable implementation wouldn't either.
194        // CharsetEncoder.encode doesn't actually pass the boolean through to encodeLoop anyway!
195        CharBuffer chars = CharBuffer.allocate(0);
196        while (true) {
197            CoderResult result = encoder.encode(chars, bytes, true);
198            if (result.isError()) {
199                result.throwException();
200            } else if (result.isOverflow()) {
201                flushBytes(false);
202                continue;
203            }
204            break;
205        }
206
207        // Some encoders (such as ISO-2022-JP) have stuff to write out after all the
208        // characters (such as shifting back into a default state). In our implementation,
209        // this is actually the first time ICU is told that we've run out of input.
210        CoderResult result = encoder.flush(bytes);
211        while (!result.isUnderflow()) {
212            if (result.isOverflow()) {
213                flushBytes(false);
214                result = encoder.flush(bytes);
215            } else {
216                result.throwException();
217            }
218        }
219    }
220
221    private void checkStatus() throws IOException {
222        if (encoder == null) {
223            throw new IOException("OutputStreamWriter is closed");
224        }
225    }
226
227    /**
228     * Returns the historical name of the encoding used by this writer to convert characters to
229     * bytes, or null if this writer has been closed. Most callers should probably keep
230     * track of the String or Charset they passed in; this method may not return the same
231     * name.
232     */
233    public String getEncoding() {
234        if (encoder == null) {
235            return null;
236        }
237        return HistoricalCharsetNames.get(encoder.charset());
238    }
239
240    /**
241     * Writes {@code count} characters starting at {@code offset} in {@code buf}
242     * to this writer. The characters are immediately converted to bytes by the
243     * character converter and stored in a local buffer. If the buffer gets full
244     * as a result of the conversion, this writer is flushed.
245     *
246     * @param buffer
247     *            the array containing characters to write.
248     * @param offset
249     *            the index of the first character in {@code buf} to write.
250     * @param count
251     *            the maximum number of characters to write.
252     * @throws IndexOutOfBoundsException
253     *             if {@code offset < 0} or {@code count < 0}, or if
254     *             {@code offset + count} is greater than the size of
255     *             {@code buf}.
256     * @throws IOException
257     *             if this writer has already been closed or another I/O error
258     *             occurs.
259     */
260    @Override
261    public void write(char[] buffer, int offset, int count) throws IOException {
262        synchronized (lock) {
263            checkStatus();
264            Arrays.checkOffsetAndCount(buffer.length, offset, count);
265            CharBuffer chars = CharBuffer.wrap(buffer, offset, count);
266            convert(chars);
267        }
268    }
269
270    /**
271     * Writes the character {@code oneChar} to this writer. The lowest two bytes
272     * of the integer {@code oneChar} are immediately converted to bytes by the
273     * character converter and stored in a local buffer. If the buffer gets full
274     * by converting this character, this writer is flushed.
275     *
276     * @param oneChar
277     *            the character to write.
278     * @throws IOException
279     *             if this writer is closed or another I/O error occurs.
280     */
281    @Override
282    public void write(int oneChar) throws IOException {
283        synchronized (lock) {
284            checkStatus();
285            CharBuffer chars = CharBuffer.wrap(new char[] { (char) oneChar });
286            convert(chars);
287        }
288    }
289
290    /**
291     * Writes {@code count} characters starting at {@code offset} in {@code str}
292     * to this writer. The characters are immediately converted to bytes by the
293     * character converter and stored in a local buffer. If the buffer gets full
294     * as a result of the conversion, this writer is flushed.
295     *
296     * @param str
297     *            the string containing characters to write.
298     * @param offset
299     *            the start position in {@code str} for retrieving characters.
300     * @param count
301     *            the maximum number of characters to write.
302     * @throws IOException
303     *             if this writer has already been closed or another I/O error
304     *             occurs.
305     * @throws IndexOutOfBoundsException
306     *             if {@code offset < 0} or {@code count < 0}, or if
307     *             {@code offset + count} is bigger than the length of
308     *             {@code str}.
309     */
310    @Override
311    public void write(String str, int offset, int count) throws IOException {
312        synchronized (lock) {
313            if (count < 0) {
314                throw new StringIndexOutOfBoundsException(str, offset, count);
315            }
316            if (str == null) {
317                throw new NullPointerException("str == null");
318            }
319            if ((offset | count) < 0 || offset > str.length() - count) {
320                throw new StringIndexOutOfBoundsException(str, offset, count);
321            }
322            checkStatus();
323            CharBuffer chars = CharBuffer.wrap(str, offset, count + offset);
324            convert(chars);
325        }
326    }
327
328    @Override boolean checkError() {
329        return out.checkError();
330    }
331}
332