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
018package org.apache.commons.io.input;
019
020import java.io.IOException;
021import java.io.InputStream;
022import java.nio.ByteBuffer;
023import java.nio.CharBuffer;
024import java.nio.charset.CharacterCodingException;
025import java.nio.charset.Charset;
026import java.nio.charset.CharsetEncoder;
027import java.nio.charset.CoderResult;
028import java.nio.charset.CodingErrorAction;
029
030/**
031 * {@link InputStream} implementation that can read from String, StringBuffer,
032 * StringBuilder or CharBuffer.
033 * <p>
034 * <strong>Note:</strong> Supports {@link #mark(int)} and {@link #reset()}.
035 *
036 * @since 2.2
037 */
038public class CharSequenceInputStream extends InputStream {
039
040    private final CharsetEncoder encoder;
041    private final CharBuffer cbuf;
042    private final ByteBuffer bbuf;
043
044    private int mark;
045    
046    /**
047     * Constructor.
048     * 
049     * @param s the input character sequence
050     * @param charset the character set name to use
051     * @param bufferSize the buffer size to use.
052     */
053    public CharSequenceInputStream(final CharSequence s, final Charset charset, int bufferSize) {
054        super();
055        this.encoder = charset.newEncoder()
056            .onMalformedInput(CodingErrorAction.REPLACE)
057            .onUnmappableCharacter(CodingErrorAction.REPLACE);
058        this.bbuf = ByteBuffer.allocate(bufferSize);
059        this.bbuf.flip();
060        this.cbuf = CharBuffer.wrap(s);
061        this.mark = -1;
062    }
063
064    /**
065     * Constructor, calls {@link #CharSequenceInputStream(CharSequence, Charset, int)}.
066     * 
067     * @param s the input character sequence
068     * @param charset the character set name to use
069     * @param bufferSize the buffer size to use.
070     */
071    public CharSequenceInputStream(final CharSequence s, final String charset, int bufferSize) {
072        this(s, Charset.forName(charset), bufferSize);
073    }
074
075    /**
076     * Constructor, calls {@link #CharSequenceInputStream(CharSequence, Charset, int)}
077     * with a buffer size of 2048.
078     * 
079     * @param s the input character sequence
080     * @param charset the character set name to use
081     */
082    public CharSequenceInputStream(final CharSequence s, final Charset charset) {
083        this(s, charset, 2048);
084    }
085
086    /**
087     * Constructor, calls {@link #CharSequenceInputStream(CharSequence, String, int)}
088     * with a buffer size of 2048.
089     * 
090     * @param s the input character sequence
091     * @param charset the character set name to use
092     */
093    public CharSequenceInputStream(final CharSequence s, final String charset) {
094        this(s, charset, 2048);
095    }
096
097    /**
098     * Fills the byte output buffer from the input char buffer.
099     * 
100     * @throws CharacterCodingException
101     *             an error encoding data
102     */
103    private void fillBuffer() throws CharacterCodingException {
104        this.bbuf.compact();
105        CoderResult result = this.encoder.encode(this.cbuf, this.bbuf, true);
106        if (result.isError()) {
107            result.throwException();
108        }
109        this.bbuf.flip();
110    }
111    
112    @Override
113    public int read(byte[] b, int off, int len) throws IOException {
114        if (b == null) {
115            throw new NullPointerException("Byte array is null");
116        }
117        if (len < 0 || (off + len) > b.length) {
118            throw new IndexOutOfBoundsException("Array Size=" + b.length +
119                    ", offset=" + off + ", length=" + len);
120        }
121        if (len == 0) {
122            return 0; // must return 0 for zero length read
123        }
124        if (!this.bbuf.hasRemaining() && !this.cbuf.hasRemaining()) {
125            return -1;
126        }
127        int bytesRead = 0;
128        while (len > 0) {
129            if (this.bbuf.hasRemaining()) {
130                int chunk = Math.min(this.bbuf.remaining(), len);
131                this.bbuf.get(b, off, chunk);
132                off += chunk;
133                len -= chunk;
134                bytesRead += chunk;
135            } else {
136                fillBuffer();
137                if (!this.bbuf.hasRemaining() && !this.cbuf.hasRemaining()) {
138                    break;
139                }
140            }
141        }
142        return bytesRead == 0 && !this.cbuf.hasRemaining() ? -1 : bytesRead;
143    }
144
145    @Override
146    public int read() throws IOException {
147        for (;;) {
148            if (this.bbuf.hasRemaining()) {
149                return this.bbuf.get() & 0xFF;
150            } else {
151                fillBuffer();
152                if (!this.bbuf.hasRemaining() && !this.cbuf.hasRemaining()) {
153                    return -1;
154                }
155            }
156        }
157    }
158
159    @Override
160    public int read(byte[] b) throws IOException {
161        return read(b, 0, b.length);
162    }
163
164    @Override
165    public long skip(long n) throws IOException {
166        int skipped = 0;
167        while (n > 0 && this.cbuf.hasRemaining()) {
168            this.cbuf.get();
169            n--;
170            skipped++;
171        }
172        return skipped;
173    }
174
175    @Override
176    public int available() throws IOException {
177        return this.cbuf.remaining();
178    }
179
180    @Override
181    public void close() throws IOException {
182    }
183
184    /**
185     * {@inheritDoc}
186     * @param readlimit max read limit (ignored)
187     */
188    @Override
189    public synchronized void mark(@SuppressWarnings("unused") int readlimit) {
190        this.mark = this.cbuf.position();
191    }
192
193    @Override
194    public synchronized void reset() throws IOException {
195        if (this.mark != -1) {
196            this.cbuf.position(this.mark);
197            this.mark = -1;
198        }
199    }
200
201    @Override
202    public boolean markSupported() {
203        return true;
204    }
205    
206}