1/*
2 * Copyright (c) 2002-2012, the original author or authors.
3 *
4 * This software is distributable under the BSD license. See the terms of the
5 * BSD license in the documentation provided with this software.
6 *
7 * http://www.opensource.org/licenses/bsd-license.php
8 */
9package jdk.internal.jline.internal;
10
11import java.io.IOException;
12import java.io.InputStream;
13import java.io.OutputStreamWriter;
14import java.io.Reader;
15import java.io.UnsupportedEncodingException;
16import java.nio.ByteBuffer;
17import java.nio.CharBuffer;
18import java.nio.charset.Charset;
19import java.nio.charset.CharsetDecoder;
20import java.nio.charset.CoderResult;
21import java.nio.charset.CodingErrorAction;
22import java.nio.charset.MalformedInputException;
23import java.nio.charset.UnmappableCharacterException;
24
25
26/**
27 *
28 * NOTE for JLine: the default InputStreamReader that comes from the JRE
29 * usually read more bytes than needed from the input stream, which
30 * is not usable in a character per character model used in the console.
31 * We thus use the harmony code which only reads the minimal number of bytes,
32 * with a modification to ensure we can read larger characters (UTF-16 has
33 * up to 4 bytes, and UTF-32, rare as it is, may have up to 8).
34 */
35/**
36 * A class for turning a byte stream into a character stream. Data read from the
37 * source input stream is converted into characters by either a default or a
38 * provided character converter. The default encoding is taken from the
39 * "file.encoding" system property. {@code InputStreamReader} contains a buffer
40 * of bytes read from the source stream and converts these into characters as
41 * needed. The buffer size is 8K.
42 *
43 * @see OutputStreamWriter
44 */
45public class InputStreamReader extends Reader {
46    private InputStream in;
47
48    private static final int BUFFER_SIZE = 8192;
49
50    private boolean endOfInput = false;
51
52    String encoding;
53
54    CharsetDecoder decoder;
55
56    ByteBuffer bytes = ByteBuffer.allocate(BUFFER_SIZE);
57
58    /**
59     * Constructs a new {@code InputStreamReader} on the {@link InputStream}
60     * {@code in}. This constructor sets the character converter to the encoding
61     * specified in the "file.encoding" property and falls back to ISO 8859_1
62     * (ISO-Latin-1) if the property doesn't exist.
63     *
64     * @param in
65     *            the input stream from which to read characters.
66     */
67    public InputStreamReader(InputStream in) {
68        super(in);
69        this.in = in;
70        // FIXME: This should probably use Configuration.getFileEncoding()
71        encoding = System.getProperty("file.encoding", "ISO8859_1"); //$NON-NLS-1$//$NON-NLS-2$
72        decoder = Charset.forName(encoding).newDecoder().onMalformedInput(
73                CodingErrorAction.REPLACE).onUnmappableCharacter(
74                CodingErrorAction.REPLACE);
75        bytes.limit(0);
76    }
77
78    /**
79     * Constructs a new InputStreamReader on the InputStream {@code in}. The
80     * character converter that is used to decode bytes into characters is
81     * identified by name by {@code enc}. If the encoding cannot be found, an
82     * UnsupportedEncodingException error is thrown.
83     *
84     * @param in
85     *            the InputStream from which to read characters.
86     * @param enc
87     *            identifies the character converter to use.
88     * @throws NullPointerException
89     *             if {@code enc} is {@code null}.
90     * @throws UnsupportedEncodingException
91     *             if the encoding specified by {@code enc} cannot be found.
92     */
93    public InputStreamReader(InputStream in, final String enc)
94            throws UnsupportedEncodingException {
95        super(in);
96        if (enc == null) {
97            throw new NullPointerException();
98        }
99        this.in = in;
100        try {
101            decoder = Charset.forName(enc).newDecoder().onMalformedInput(
102                    CodingErrorAction.REPLACE).onUnmappableCharacter(
103                    CodingErrorAction.REPLACE);
104        } catch (IllegalArgumentException e) {
105            throw (UnsupportedEncodingException)
106                    new UnsupportedEncodingException(enc).initCause(e);
107        }
108        bytes.limit(0);
109    }
110
111    /**
112     * Constructs a new InputStreamReader on the InputStream {@code in} and
113     * CharsetDecoder {@code dec}.
114     *
115     * @param in
116     *            the source InputStream from which to read characters.
117     * @param dec
118     *            the CharsetDecoder used by the character conversion.
119     */
120    public InputStreamReader(InputStream in, CharsetDecoder dec) {
121        super(in);
122        dec.averageCharsPerByte();
123        this.in = in;
124        decoder = dec;
125        bytes.limit(0);
126    }
127
128    /**
129     * Constructs a new InputStreamReader on the InputStream {@code in} and
130     * Charset {@code charset}.
131     *
132     * @param in
133     *            the source InputStream from which to read characters.
134     * @param charset
135     *            the Charset that defines the character converter
136     */
137    public InputStreamReader(InputStream in, Charset charset) {
138        super(in);
139        this.in = in;
140        decoder = charset.newDecoder().onMalformedInput(
141                CodingErrorAction.REPLACE).onUnmappableCharacter(
142                CodingErrorAction.REPLACE);
143        bytes.limit(0);
144    }
145
146    /**
147     * Closes this reader. This implementation closes the source InputStream and
148     * releases all local storage.
149     *
150     * @throws IOException
151     *             if an error occurs attempting to close this reader.
152     */
153    @Override
154    public void close() throws IOException {
155        synchronized (lock) {
156            decoder = null;
157            if (in != null) {
158                in.close();
159                in = null;
160            }
161        }
162    }
163
164    /**
165     * Returns the name of the encoding used to convert bytes into characters.
166     * The value {@code null} is returned if this reader has been closed.
167     *
168     * @return the name of the character converter or {@code null} if this
169     *         reader is closed.
170     */
171    public String getEncoding() {
172        if (!isOpen()) {
173            return null;
174        }
175        return encoding;
176    }
177
178    /**
179     * Reads a single character from this reader and returns it as an integer
180     * with the two higher-order bytes set to 0. Returns -1 if the end of the
181     * reader has been reached. The byte value is either obtained from
182     * converting bytes in this reader's buffer or by first filling the buffer
183     * from the source InputStream and then reading from the buffer.
184     *
185     * @return the character read or -1 if the end of the reader has been
186     *         reached.
187     * @throws IOException
188     *             if this reader is closed or some other I/O error occurs.
189     */
190    @Override
191    public int read() throws IOException {
192        synchronized (lock) {
193            if (!isOpen()) {
194                throw new IOException("InputStreamReader is closed.");
195            }
196
197            char buf[] = new char[4];
198            return read(buf, 0, 4) != -1 ? Character.codePointAt(buf, 0) : -1;
199        }
200    }
201
202    /**
203     * Reads at most {@code length} characters from this reader and stores them
204     * at position {@code offset} in the character array {@code buf}. Returns
205     * the number of characters actually read or -1 if the end of the reader has
206     * been reached. The bytes are either obtained from converting bytes in this
207     * reader's buffer or by first filling the buffer from the source
208     * InputStream and then reading from the buffer.
209     *
210     * @param buf
211     *            the array to store the characters read.
212     * @param offset
213     *            the initial position in {@code buf} to store the characters
214     *            read from this reader.
215     * @param length
216     *            the maximum number of characters to read.
217     * @return the number of characters read or -1 if the end of the reader has
218     *         been reached.
219     * @throws IndexOutOfBoundsException
220     *             if {@code offset < 0} or {@code length < 0}, or if
221     *             {@code offset + length} is greater than the length of
222     *             {@code buf}.
223     * @throws IOException
224     *             if this reader is closed or some other I/O error occurs.
225     */
226    @Override
227    public int read(char[] buf, int offset, int length) throws IOException {
228        synchronized (lock) {
229            if (!isOpen()) {
230                throw new IOException("InputStreamReader is closed.");
231            }
232            if (offset < 0 || offset > buf.length - length || length < 0) {
233                throw new IndexOutOfBoundsException();
234            }
235            if (length == 0) {
236                return 0;
237            }
238
239            CharBuffer out = CharBuffer.wrap(buf, offset, length);
240            CoderResult result = CoderResult.UNDERFLOW;
241
242            // bytes.remaining() indicates number of bytes in buffer
243            // when 1-st time entered, it'll be equal to zero
244            boolean needInput = !bytes.hasRemaining();
245
246            while (out.hasRemaining()) {
247                // fill the buffer if needed
248                if (needInput) {
249                    try {
250                        if ((in.available() == 0)
251                            && (out.position() > offset)) {
252                            // we could return the result without blocking read
253                            break;
254                        }
255                    } catch (IOException e) {
256                        // available didn't work so just try the read
257                    }
258
259                    int to_read = bytes.capacity() - bytes.limit();
260                    int off = bytes.arrayOffset() + bytes.limit();
261                    int was_red = in.read(bytes.array(), off, to_read);
262
263                    if (was_red == -1) {
264                        endOfInput = true;
265                        break;
266                    } else if (was_red == 0) {
267                        break;
268                    }
269                    bytes.limit(bytes.limit() + was_red);
270                    needInput = false;
271                }
272
273                // decode bytes
274                result = decoder.decode(bytes, out, false);
275
276                if (result.isUnderflow()) {
277                    // compact the buffer if no space left
278                    if (bytes.limit() == bytes.capacity()) {
279                        bytes.compact();
280                        bytes.limit(bytes.position());
281                        bytes.position(0);
282                    }
283                    needInput = true;
284                } else {
285                    break;
286                }
287            }
288
289            if (result == CoderResult.UNDERFLOW && endOfInput) {
290                result = decoder.decode(bytes, out, true);
291                decoder.flush(out);
292                decoder.reset();
293            }
294            if (result.isMalformed()) {
295                throw new MalformedInputException(result.length());
296            } else if (result.isUnmappable()) {
297                throw new UnmappableCharacterException(result.length());
298            }
299
300            return out.position() - offset == 0 ? -1 : out.position() - offset;
301        }
302    }
303
304    /*
305     * Answer a boolean indicating whether or not this InputStreamReader is
306     * open.
307     */
308    private boolean isOpen() {
309        return in != null;
310    }
311
312    /**
313     * Indicates whether this reader is ready to be read without blocking. If
314     * the result is {@code true}, the next {@code read()} will not block. If
315     * the result is {@code false} then this reader may or may not block when
316     * {@code read()} is called. This implementation returns {@code true} if
317     * there are bytes available in the buffer or the source stream has bytes
318     * available.
319     *
320     * @return {@code true} if the receiver will not block when {@code read()}
321     *         is called, {@code false} if unknown or blocking will occur.
322     * @throws IOException
323     *             if this reader is closed or some other I/O error occurs.
324     */
325    @Override
326    public boolean ready() throws IOException {
327        synchronized (lock) {
328            if (in == null) {
329                throw new IOException("InputStreamReader is closed.");
330            }
331            try {
332                return bytes.hasRemaining() || in.available() > 0;
333            } catch (IOException e) {
334                return false;
335            }
336        }
337    }
338}
339