/* ******************************************************************************
* Copyright (c) 2006-2012 XMind Ltd. and others.
*
* This file is a part of XMind 3. XMind releases 3 and
* above are dual-licensed under the Eclipse Public License (EPL),
* which is available at http://www.eclipse.org/legal/epl-v10.html
* and the GNU Lesser General Public License (LGPL),
* which is available at http://www.gnu.org/licenses/lgpl.html
* See http://www.xmind.net/license.html for details.
*
* Contributors:
* XMind Ltd. - initial API and implementation
*******************************************************************************/
package org.xmind.core.command.transfer;
import java.io.Closeable;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.UnmappableCharacterException;
import java.util.Arrays;
/**
* A helper class that reads data from an input stream in chunks.
*
* @author Frank Shaka
* @see ChunkWriter
*/
public class ChunkReader implements Closeable {
private class ChunkInputStream extends FilterInputStream {
private boolean closed = false;
public ChunkInputStream(InputStream in) {
super(in);
chunkEnded = false;
}
@Override
public int read() throws IOException {
if (closed)
throw new IOException("Stream already closed."); //$NON-NLS-1$
if (chunkEnded && binaryBuffered <= 0)
return -1;
if (binaryBuffered <= 0) {
refillBinary();
}
if (chunkEnded && binaryBuffered <= 0)
return -1;
byte b = binaryBuffer[0];
useBinary(1);
return b;
}
@Override
public int read(byte[] b) throws IOException {
if (closed)
throw new IOException("Stream already closed."); //$NON-NLS-1$
return read(b, 0, b.length);
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
if (closed)
throw new IOException("Stream already closed."); //$NON-NLS-1$
if (off < 0 || off + len > b.length)
throw new IndexOutOfBoundsException(
"Offset or length out of bounds."); //$NON-NLS-1$
int read = 0;
while (len > 0) {
if (chunkEnded && binaryBuffered <= 0)
break;
if (binaryBuffered <= 0) {
refillBinary();
}
if (chunkEnded && binaryBuffered <= 0)
break;
int size = Math.min(len, binaryBuffered);
System.arraycopy(binaryBuffer, 0, b, off, size);
read += size;
off += size;
len -= size;
useBinary(size);
}
if (read == 0)
read = -1;
return read;
}
@Override
public void close() throws IOException {
closed = true;
binaryBuffered = 0;
}
}
private static final byte DEFAULT_CHUNK_SEPARATOR = (byte) '\n';
private static final String DEFAULT_TEXT_ENCODING = "UTF-8"; //$NON-NLS-1$
private static final int DEFAULT_BUFFER_SIZE = 4096;
private static final int DEFAULT_TEXT_BUFFER_SIZE = 1024;
private static final int DEFAULT_BINARY_BUFFER_SIZE = DEFAULT_BUFFER_SIZE / 4 * 3;
private static final String EMPTY_TEXT = ""; //$NON-NLS-1$
/**
* This array is a lookup table that translates unicode characters drawn
* from the "Base64 Alphabet" (as specified in Table 1 of RFC 2045) into
* their 6-bit positive integer equivalents. Characters that are not in the
* Base64 alphabet but fall within the bounds of the array are translated to
* -1.
*/
private static final byte base64ToInt[] = { -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, 62, -1, -1, -1, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1,
-1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12,
13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1,
-1, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51 };
private InputStream in;
private String textEncoding;
private byte separator;
private boolean eof = false;
private byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
private int buffered = 0;
private byte[] textBuffer = new byte[DEFAULT_TEXT_BUFFER_SIZE];
private ChunkInputStream binaryStream = null;
private byte[] binaryBuffer = new byte[DEFAULT_BINARY_BUFFER_SIZE];
private int binaryBuffered = 0;
private boolean chunkEnded = false;
public ChunkReader(InputStream in) {
this(in, DEFAULT_CHUNK_SEPARATOR, DEFAULT_TEXT_ENCODING);
}
public ChunkReader(InputStream in, byte chunkSeparator, String textEncoding) {
this.in = in;
this.separator = chunkSeparator;
this.textEncoding = textEncoding;
}
private boolean isStreamEnded() {
return eof && buffered <= 0;
}
private boolean isBinaryChunkEnded() {
return chunkEnded && binaryBuffered <= 0;
}
/**
* Read a line of text and return it.
*
* @return a line of text, or <code>null</code> if the main stream has
* reached the end
* @throws IOException
*/
public String readText() throws IOException {
closeBinaryStream(true);
// The number of bytes to return as text.
int size = 0;
// The index of the separator char in the byte buffer.
int sep;
// A new number of bytes that we need to enlarge the text buffer.
int newSize;
// The number of bytes in buffer.
int numBuffered;
// The total number of bytes that we consumed from the stream.
// This number may be one more than 'size' as we consume the separator
// char as well.
int consumed = 0;
// Run until the end of stream is reached.
while (!isStreamEnded()) {
// Find the next separator char in the byte buffer,
// or the end of the byte buffer.
sep = findSep(0);
// Calculate new text buffer size.
newSize = size + sep;
// Enlarge the text buffer to receive new bytes.
if (newSize > textBuffer.length) {
textBuffer = Arrays.copyOf(textBuffer,
Math.max(textBuffer.length << 1, newSize));
}
// Copy required bytes from byte buffer to text buffer.
if (sep > 0) {
System.arraycopy(buffer, 0, textBuffer, size, sep);
}
// Now we have 'size' bytes in the text buffer.
size = newSize;
// Consume bytes from the byte buffer.
numBuffered = buffered;
if (sep == numBuffered) {
use(numBuffered);
consumed += numBuffered;
} else {
use(sep + 1);
consumed += sep + 1;
break;
}
// Reload byte buffer from the stream.
reload();
}
// No bytes consumed from stream, this must be caused by the end of stream,
// otherwise there must have been at least one separator char.
if (consumed == 0)
return null;
// Return the cached empty string to prevent creating a new String instance.
if (size == 0)
return EMPTY_TEXT;
// Construct a new String instance from the text buffer.
return new String(textBuffer, 0, size, textEncoding);
}
/**
* Create a stream for reading the next chunk of data.
*
* @return an input stream for reading the next chunk of data, or
* <code>null</code> if the main stream has reached the end
* @throws IOException
*/
public InputStream openNextChunkAsStream() throws IOException {
closeBinaryStream(true);
if (isStreamEnded())
return null;
return binaryStream = new ChunkInputStream(in);
}
private void closeBinaryStream(boolean drain) throws IOException {
if (binaryStream != null && !binaryStream.closed
&& !isBinaryChunkEnded()) {
if (drain) {
byte[] buf = new byte[4096];
while (binaryStream.read(buf) > 0) {
}
}
binaryStream.close();
binaryStream = null;
}
}
public void close() throws IOException {
closeBinaryStream(false);
in.close();
}
private void reload() throws IOException {
if (eof)
return;
if (!eof && buffered < buffer.length) {
int len = buffer.length - buffered;
int read = in.read(buffer, buffered, len);
if (read < 0) {
eof = true;
read = 0;
}
buffered += read;
}
}
private static int base64toInt(byte c) throws IOException {
int b = base64ToInt[c];
if (b < 0)
throw new UnmappableCharacterException(1);
return b;
}
private int findSep(int off) {
for (int i = off; i < buffered; i++) {
if (buffer[i] == separator)
return i;
}
return buffered;
}
private void use(int used) {
if (used >= buffered) {
buffered = 0;
} else {
int newCount = buffered - used;
System.arraycopy(buffer, used, buffer, 0, newCount);
buffered = newCount;
}
}
private void refillBinary() throws IOException {
reload();
if (eof) {
chunkEnded = true;
}
int used = 0;
int max = buffered - 3;
int bmax = binaryBuffer.length - 2;
while (used < max && binaryBuffered < bmax) {
byte b0 = buffer[used++];
if (b0 == separator) {
chunkEnded = true;
break;
}
byte b1 = buffer[used++];
if (b1 == separator) {
chunkEnded = true;
break;
}
byte b2 = buffer[used++];
if (b2 == separator) {
chunkEnded = true;
break;
}
byte b3 = buffer[used++];
if (b3 == separator) {
chunkEnded = true;
break;
}
int c0 = base64toInt(b0);
int c1 = base64toInt(b1);
binaryBuffer[binaryBuffered++] = (byte) ((c0 << 2) | (c1 >> 4));
if (b2 != '=') {
int c2 = base64toInt(b2);
binaryBuffer[binaryBuffered++] = (byte) ((c1 << 4) | (c2 >> 2));
if (b3 != '=') {
int c3 = base64toInt(b3);
binaryBuffer[binaryBuffered++] = (byte) ((c2 << 6) | c3);
}
}
}
use(used);
}
private void useBinary(int used) {
if (used >= binaryBuffered) {
binaryBuffered = 0;
} else {
int newCount = binaryBuffered - used;
System.arraycopy(binaryBuffer, used, binaryBuffer, 0, newCount);
binaryBuffered = newCount;
}
}
}