content: 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;
      }
  }
  