package org.jibble.pircbot;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CodingErrorAction;
public class FallbackInputStreamReader extends Reader {
/**
* Turn this on for low level debugging output
*/
private static final boolean debug = false;
private final PircBotLogger logger;
private final InputStream in;
private final Charset primaryCharset;
private final Charset fallbackCharset;
private boolean gotEof; // if true, fillBuffer() will not try to read more
private ByteBuffer buf = ByteBuffer.allocate(1024); // if null after call to fillBuffer(), no more data available
private int bufFillLevel; // how much of buf is filled (position in buf) - always kept up to date, even inside method calls
private byte[] tmp = new byte[1024];
private CharBuffer decBuf; // if null after call to fillDecodeBuffer(), no more data available
private Charset lastCharset;
public FallbackInputStreamReader(PircBotLogger logger, InputStream in, String primaryCharsetName, String fallbackCharsetName) throws UnsupportedEncodingException {
this.logger = logger;
this.in = in;
this.primaryCharset = Charset.forName(primaryCharsetName);
this.fallbackCharset = Charset.forName(fallbackCharsetName);
buf.limit(0);
}
@Override
public void close() throws IOException {
in.close();
}
@Override
public int read(char[] cbuf, int off, int len) throws IOException {
fillDecodeBuffer();
if(decBuf == null) {
return -1;
}
int rem = decBuf.remaining();
if(rem < len) {
len = rem;
}
decBuf.get(cbuf, off, len);
if(debug) logger.log(lastCharset + " READ return '" + new String(cbuf, off, len) + "'");
return len;
}
private void fillDecodeBuffer() throws IOException {
if (decBuf != null && decBuf.hasRemaining()) {
return;
}
fillBuffer();
if (buf == null) {
decBuf = null;
return;
}
CharsetDecoder d = primaryCharset.newDecoder();
d.onMalformedInput(CodingErrorAction.REPORT);
d.onUnmappableCharacter(CodingErrorAction.REPORT);
int origPos = buf.position();
try {
decBuf = d.decode(buf);
lastCharset = primaryCharset;
} catch(IOException e) {
d = fallbackCharset.newDecoder();
d.onMalformedInput(CodingErrorAction.REPLACE);
d.onUnmappableCharacter(CodingErrorAction.REPLACE);
try {
// rewind
buf.position(origPos);
decBuf = d.decode(buf);
lastCharset = fallbackCharset;
} catch (CharacterCodingException e1) {
throw new RuntimeException(e1);
}
}
}
private void fillBuffer() throws IOException {
if(debug) logger.log(buf == null ? "FISR bp null bl null fl n/a" : ("bp " + buf.position() + " bl " + buf.limit() + " fl " + bufFillLevel));
// if we got EOF, just give everything we got left, if any
if(gotEof) {
if(buf != null && !buf.hasRemaining()) {
buf = null;
}
if(debug) logger.log("FISR -> EOF");
return;
}
// sanity check
if(buf != null && buf.hasRemaining()) {
// this should never happen
logger.log("### BUG in FISR: pos<lim: " + buf.position() + " < " + buf.limit());
// discard remaining
buf.position(buf.limit());
}
// compact remaining data
buf.limit(bufFillLevel);
buf.compact();
bufFillLevel = buf.position();
// see if we got a line feed already last round
if(checkLF(0)) {
return;
}
try {
while(true) {
// read some more data
int r = in.read(tmp);
// if we got EOF, just toss the remaining data back and remember EOF happened
if(r == -1) {
gotEof = true;
tmp = null;
buf.flip();
if(debug) logger.log("FISR -> 2 bp " + buf.position() + " bl " + buf.limit() + " fl " + bufFillLevel);
if(buf.remaining() == 0) {
buf = null;
}
return;
}
// make sure we have space to receive the data
if(buf.remaining() < r) {
if(debug) logger.log("FISR bp Doubling buffer");
buf.flip();
ByteBuffer bb = ByteBuffer.allocate(buf.capacity() * 2);
bb.put(buf);
buf = bb;
}
// copy the newly received data to the buffer
int start = buf.position();
buf.put(tmp, 0, r);
bufFillLevel = buf.position();
// see if we got a line feed
if(checkLF(start)) {
return;
}
}
} catch (IOException e) {
// this might happen non-fatally, for example SocketTimeoutException, so just update state so we can continue later.
// set limit to 0 since nothing of what may be in the buffer will get consumed this round. This way next fillBuffer() call will continue where we left
buf.limit(0);
if(debug) logger.log("FISR -> " + e + " bp " + buf.position() + " bl " + buf.limit() + " fl " + bufFillLevel);
throw e;
}
}
private boolean checkLF(int start) {
for(int i=start; i<buf.position(); ++i) {
if(buf.get(i) == '\n') {
// do a flip, but only give away up until and including LF
buf.position(0);
buf.limit(i+1);
if(debug) logger.log("FISR -> 1 / " + start + " bp " + buf.position() + " bl " + buf.limit() + " fl " + bufFillLevel);
return true;
}
}
return false;
}
}