This is the first draft of the refactored coyote connector.
authorcostin <costin@13f79535-47bb-0310-9956-ffa450edef68>
Thu, 26 Nov 2009 06:35:43 +0000 (06:35 +0000)
committercostin <costin@13f79535-47bb-0310-9956-ffa450edef68>
Thu, 26 Nov 2009 06:35:43 +0000 (06:35 +0000)
Lots of big changes:
- MessageBytes is gone - BBuffer, CBuffer are used as buffers, BBucket/CBucket are used for wrapping bytes
- C2B/B2C are gone - replaced by nio charsets in IOReader/IOWriter. CBuffer is still needed (instead of a StringBuilder ) to have
access to the raw chars and avoid copy
- speaking of copy, IOChannel uses a list of buffers instead of growing a buffer. For proxy we should be able to move
buffers from in to out.  This works a bit better with the char conversions, etc
- Nio code heavily refactored - I didn't add APR but shouldn't be hard if anyonew wants.
- Ssl is barely working - needs improvements

git-svn-id: https://svn.apache.org/repos/asf/tomcat/trunk@884410 13f79535-47bb-0310-9956-ffa450edef68

27 files changed:
modules/tomcat-lite/java/org/apache/tomcat/lite/io/BBucket.java [new file with mode: 0644]
modules/tomcat-lite/java/org/apache/tomcat/lite/io/BBuffer.java [new file with mode: 0644]
modules/tomcat-lite/java/org/apache/tomcat/lite/io/BufferedIOReader.java [new file with mode: 0644]
modules/tomcat-lite/java/org/apache/tomcat/lite/io/CBucket.java [new file with mode: 0644]
modules/tomcat-lite/java/org/apache/tomcat/lite/io/CBuffer.java [new file with mode: 0644]
modules/tomcat-lite/java/org/apache/tomcat/lite/io/DumpChannel.java [new file with mode: 0644]
modules/tomcat-lite/java/org/apache/tomcat/lite/io/FastHttpDateFormat.java [new file with mode: 0644]
modules/tomcat-lite/java/org/apache/tomcat/lite/io/FileConnector.java [new file with mode: 0644]
modules/tomcat-lite/java/org/apache/tomcat/lite/io/FileConnectorJavaIo.java [new file with mode: 0644]
modules/tomcat-lite/java/org/apache/tomcat/lite/io/Hex.java [new file with mode: 0644]
modules/tomcat-lite/java/org/apache/tomcat/lite/io/IOBuffer.java [new file with mode: 0644]
modules/tomcat-lite/java/org/apache/tomcat/lite/io/IOChannel.java [new file with mode: 0644]
modules/tomcat-lite/java/org/apache/tomcat/lite/io/IOConnector.java [new file with mode: 0644]
modules/tomcat-lite/java/org/apache/tomcat/lite/io/IOInputStream.java [new file with mode: 0644]
modules/tomcat-lite/java/org/apache/tomcat/lite/io/IOOutputStream.java [new file with mode: 0644]
modules/tomcat-lite/java/org/apache/tomcat/lite/io/IOReader.java [new file with mode: 0644]
modules/tomcat-lite/java/org/apache/tomcat/lite/io/IOWriter.java [new file with mode: 0644]
modules/tomcat-lite/java/org/apache/tomcat/lite/io/MemoryIOConnector.java [new file with mode: 0644]
modules/tomcat-lite/java/org/apache/tomcat/lite/io/NioChannel.java [new file with mode: 0644]
modules/tomcat-lite/java/org/apache/tomcat/lite/io/NioThread.java [new file with mode: 0644]
modules/tomcat-lite/java/org/apache/tomcat/lite/io/SocketConnector.java [new file with mode: 0644]
modules/tomcat-lite/java/org/apache/tomcat/lite/io/SocketIOChannel.java [new file with mode: 0644]
modules/tomcat-lite/java/org/apache/tomcat/lite/io/SslChannel.java [new file with mode: 0644]
modules/tomcat-lite/java/org/apache/tomcat/lite/io/SslConnector.java [new file with mode: 0644]
modules/tomcat-lite/java/org/apache/tomcat/lite/io/UrlEncoding.java [new file with mode: 0644]
modules/tomcat-lite/java/org/apache/tomcat/lite/io/WrappedException.java [new file with mode: 0644]
modules/tomcat-lite/java/org/apache/tomcat/lite/io/package.html [new file with mode: 0644]

diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/io/BBucket.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/BBucket.java
new file mode 100644 (file)
index 0000000..a12eb4a
--- /dev/null
@@ -0,0 +1,40 @@
+/*
+ */
+package org.apache.tomcat.lite.io;
+
+import java.nio.ByteBuffer;
+
+
+
+/** 
+ * Holds raw data. Similar interface with a ByteBuffer in 'channel write'
+ * or 'read mode'. Data is between position and limit - allways.
+ * 
+ * TODO: FileBucket, DirectBufferBucket, CharBucket, ...
+ * 
+ * @author Costin Manolache
+ */
+public interface BBucket {
+    
+    public void release();
+
+    public byte[] array();
+    public int position();
+    public int remaining();
+    public int limit();
+    
+    public boolean hasRemaining();
+    
+    public void position(int newStart);
+
+    /** 
+     * Return a byte buffer, with data between position and limit. 
+     * Changes in the ByteBuffer position will not be reflected 
+     * in the IOBucket.
+     * 
+     * @return
+     */
+    public ByteBuffer getByteBuffer();
+
+
+}
\ No newline at end of file
diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/io/BBuffer.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/BBuffer.java
new file mode 100644 (file)
index 0000000..a83c5fb
--- /dev/null
@@ -0,0 +1,1193 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package org.apache.tomcat.lite.io;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Serializable;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+
+/*
+ * In a server it is very important to be able to operate on
+ * the original byte[] without converting everything to chars.
+ * Some protocols are ASCII only, and some allow different
+ * non-UNICODE encodings. The encoding is not known beforehand,
+ * and can even change during the execution of the protocol.
+ * ( for example a multipart message may have parts with different
+ *  encoding )
+ *
+ * For HTTP it is not very clear how the encoding of RequestURI
+ * and mime values can be determined, but it is a great advantage
+ * to be able to parse the request without converting to string.
+ */
+
+// Renamed from ByteChunk to make it easier to write code using both
+
+/**
+ * This class is used to represent a chunk of bytes, and utilities to manipulate
+ * byte[].
+ * 
+ * The buffer can be modified and used for both input and output.
+ * 
+ * There are 2 modes: The chunk can be associated with a sink - ByteInputChannel
+ * or ByteOutputChannel, which will be used when the buffer is empty ( on input
+ * ) or filled ( on output ). For output, it can also grow. This operating mode
+ * is selected by calling setLimit() or allocate(initial, limit) with limit !=
+ * -1.
+ * 
+ * Various search and append method are defined - similar with String and
+ * StringBuffer, but operating on bytes.
+ * 
+ * This is important because it allows processing the http headers directly on
+ * the received bytes, without converting to chars and Strings until the strings
+ * are needed. In addition, the charset is determined later, from headers or
+ * user code.
+ * 
+ * 
+ * @author dac@sun.com
+ * @author James Todd [gonzo@sun.com]
+ * @author Costin Manolache
+ * @author Remy Maucherat
+ */
+public class BBuffer implements Cloneable, Serializable, 
+    BBucket {
+    
+    /**
+     * Default encoding used to convert to strings. It should be UTF8, but:
+     * - the servlet API requires 8859_1 as default
+     * -  
+     */
+    public static final String DEFAULT_CHARACTER_ENCODING = "ISO-8859-1";
+
+    // byte[]
+    private byte[] buff;
+
+    private int start = 0;
+
+    private int end;
+
+    private ByteBuffer byteBuffer;
+
+    public static final String CRLF = "\r\n";
+
+    /* Various constant "strings" */
+    public static final byte[] CRLF_BYTES = convertToBytes(BBuffer.CRLF);
+
+    /**
+     * HT.
+     */
+    public static final byte HT = (byte) '\t';
+
+    /**
+     * SP.
+     */
+    public static final byte SP = (byte) ' ';
+
+    /**
+     * LF.
+     */
+    public static final byte LF = (byte) '\n';
+
+    /**
+     * CR.
+     */
+    public static final byte CR = (byte) '\r';
+    
+    //private int useCount;
+    
+    
+    private static final boolean[] isDigit = new boolean[256];
+
+    static Charset UTF8;
+
+    public static final byte A = (byte) 'A';
+
+    public static final byte Z = (byte) 'Z';
+
+    public static final byte a = (byte) 'a';
+
+    public static final byte LC_OFFSET = A - a;
+    private static final byte[] toLower = new byte[256];
+    private static final boolean[] isUpper = new boolean[256];
+
+    static {
+        for (int i = 0; i < 256; i++) {
+            toLower[i] = (byte)i;
+        }
+
+        for (int lc = 'a'; lc <= 'z'; lc++) {
+            int uc = lc + 'A' - 'a';
+            toLower[uc] = (byte)lc;
+            isUpper[uc] = true;
+        }
+    }
+
+    static {
+        for (int d = '0'; d <= '9'; d++) {
+            isDigit[d] = true;
+        }        
+        UTF8 = Charset.forName("UTF-8");
+    }
+
+    public static BBuffer allocate() {
+        return new BBuffer();
+    }
+
+    public static BBuffer allocate(int initial) {
+        return new BBuffer().makeSpace(initial);
+    }
+
+    
+    public static BBuffer allocate(String msg) {
+        BBuffer bc = allocate();
+        byte[] data = msg.getBytes(UTF8);
+        bc.append(data, 0, data.length);
+        return bc;
+    }
+
+    public static BBuffer wrapper(String msg) {
+        BBuffer bc = new IOBucketWrap();
+        byte[] data = msg.getBytes(UTF8);
+        bc.setBytes(data, 0, data.length);
+        return bc;
+    }
+
+    public static BBuffer wrapper() {
+        return new IOBucketWrap();
+    }
+
+    public static BBuffer wrapper(BBuffer bb) {
+        BBuffer res = new IOBucketWrap();
+        res.setBytes(bb.array(), bb.position(), bb.remaining());
+        return res;
+    }
+
+    public static BBuffer wrapper(byte b[], int off, int len) {
+        BBuffer res = new IOBucketWrap();
+        res.setBytes(b, off, len);
+        return res;
+    }
+    
+    public static BBuffer wrapper(BBucket bb, int start, int len) {
+        BBuffer res = new IOBucketWrap();
+        res.setBytes(bb.array(), bb.position() + start, len);
+        return res;
+    }
+
+    /**
+     * Creates a new, uninitialized ByteChunk object.
+     */
+    private BBuffer() {
+    }
+
+    public void append(BBuffer src) {
+        append(src.array(), src.getStart(), src.getLength());
+    }
+
+    /**
+     * Add data to the buffer
+     */
+    public void append(byte src[], int off, int len) {
+        // will grow, up to limit
+        makeSpace(len);
+
+        // assert: makeSpace made enough space
+        System.arraycopy(src, off, buff, end, len);
+        end += len;
+        return;
+    }
+
+    // -------------------- Adding data to the buffer --------------------
+    /**
+     * Append a char, by casting it to byte. This IS NOT intended for unicode.
+     * 
+     * @param c
+     */
+    public void append(char c) {
+        put((byte) c);
+    }
+
+    // -------------------- Removing data from the buffer --------------------
+
+    /**
+     * Returns the message bytes.
+     */
+    @Override
+    public byte[] array() {
+        return buff;
+    }
+    
+    public int capacity() {
+        return buff.length;
+    }
+
+    public boolean equals(BBuffer bb) {
+        return equals(bb.array(), bb.getStart(), bb.getLength());
+    }
+
+    public boolean equals(byte b2[], int off2, int len2) {
+        byte b1[] = buff;
+        if (b1 == null && b2 == null)
+            return true;
+
+        int len = end - start;
+        if (len2 != len || b1 == null || b2 == null)
+            return false;
+
+        int off1 = start;
+
+        while (len-- > 0) {
+            if (b1[off1++] != b2[off2++]) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+
+    public boolean equals(char c2[], int off2, int len2) {
+        // XXX works only for enc compatible with ASCII/UTF !!!
+        byte b1[] = buff;
+        if (c2 == null && b1 == null)
+            return true;
+
+        if (b1 == null || c2 == null || end - start != len2) {
+            return false;
+        }
+        int off1 = start;
+        int len = end - start;
+
+        while (len-- > 0) {
+            if ((char) b1[off1++] != c2[off2++]) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    // -------------------- Conversion and getters --------------------
+
+    /**
+     * Compares the message bytes to the specified String object.
+     * 
+     * @param s
+     *            the String to compare
+     * @return true if the comparison succeeded, false otherwise
+     */
+    public boolean equals(String s) {
+        // XXX ENCODING - this only works if encoding is UTF8-compat
+        // ( ok for tomcat, where we compare ascii - header names, etc )!!!
+
+        byte[] b = buff;
+        int blen = end - start;
+        if (b == null || blen != s.length()) {
+            return false;
+        }
+        int boff = start;
+        for (int i = 0; i < blen; i++) {
+            if (b[boff++] != s.charAt(i)) {
+                return false;
+            }
+        }
+        return true;
+    }
+    
+    /**
+     * Compares the message bytes to the specified String object.
+     * 
+     * @param s
+     *            the String to compare
+     * @return true if the comparison succeeded, false otherwise
+     */
+    public boolean equalsIgnoreCase(String s) {
+        byte[] b = buff;
+        int blen = end - start;
+        if (b == null || blen != s.length()) {
+            return false;
+        }
+        int boff = start;
+        for (int i = 0; i < blen; i++) {
+            if (toLower(b[boff++]) != toLower(s.charAt(i))) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    public byte get(int off) {
+        if (start + off >= end) {
+            throw new ArrayIndexOutOfBoundsException();
+        }
+        return buff[start + off];
+    }
+
+    /** 
+     * Return a byte buffer. Changes in the ByteBuffer position will
+     * not be reflected in the IOBucket
+     * @return
+     */
+    public ByteBuffer getByteBuffer() {
+        if (byteBuffer == null || byteBuffer.array() != buff) {
+            byteBuffer = ByteBuffer.wrap(buff, start, end - start);
+        } else {
+            byteBuffer.position(start);
+            byteBuffer.limit(end);
+        }
+        return byteBuffer;
+    }
+
+    // --------------------
+    public BBuffer getClone() {
+        try {
+            return (BBuffer) this.clone();
+        } catch (Exception ex) {
+            return null;
+        }
+    }
+    
+    public int getEnd() {
+        return end;
+    }
+
+    public int getInt() {
+        return parseInt(buff, start, end - start);
+    }
+    /**
+     * Returns the length of the bytes. XXX need to clean this up
+     */
+    public int getLength() {
+        return end - start;
+    }
+    
+    public long getLong() {
+        return parseLong(buff, start, end - start);
+    }
+
+    public int getOffset() {
+        return start;
+    }
+
+    // -------------------- equals --------------------
+
+    /**
+     * Returns the start offset of the bytes. For output this is the end of the
+     * buffer.
+     */
+    public int getStart() {
+        return start;
+    }
+
+    public ByteBuffer getWriteByteBuffer(int space) {
+        if (space == 0) {
+            space = 16;
+        }
+        makeSpace(space);
+        if (byteBuffer == null || byteBuffer.array() != buff) {
+            byteBuffer = ByteBuffer.wrap(buff, end, buff.length);
+        } else {
+            byteBuffer.position(end);
+            byteBuffer.limit(buff.length);
+        }
+        return byteBuffer;
+    }
+
+    // -------------------- Hash code --------------------
+    public int hashCode() {
+        return hashBytes(buff, start, end - start);
+    }
+
+    public boolean hasLFLF() {
+        return hasLFLF(this);
+    }
+
+    public boolean hasRemaining() {
+        return start < end;
+    }
+
+    /**
+     * Returns true if the message bytes starts with the specified string.
+     * 
+     * @param s
+     *            the string
+     */
+//    public boolean startsWith(String s) {
+//        // Works only if enc==UTF
+//        byte[] b = buff;
+//        int blen = s.length();
+//        if (b == null || blen > end - start) {
+//            return false;
+//        }
+//        int boff = start;
+//        for (int i = 0; i < blen; i++) {
+//            if (b[boff++] != s.charAt(i)) {
+//                return false;
+//            }
+//        }
+//        return true;
+//    }
+
+    /* Returns true if the message bytes start with the specified byte array */
+//    public boolean startsWith(byte[] b2) {
+//        byte[] b1 = buff;
+//        if (b1 == null && b2 == null) {
+//            return true;
+//        }
+//
+//        int len = end - start;
+//        if (b1 == null || b2 == null || b2.length > len) {
+//            return false;
+//        }
+//        for (int i = start, j = 0; i < end && j < b2.length;) {
+//            if (b1[i++] != b2[j++])
+//                return false;
+//        }
+//        return true;
+//    }
+
+    /**
+     * Returns true if the message bytes starts with the specified string.
+     * 
+     * @param c
+     *            the character
+     * @param starting
+     *            The start position
+     */
+    public int indexOf(char c, int starting) {
+        int ret = indexOf(buff, start + starting, end, c);
+        return (ret >= start) ? ret - start : -1;
+    }
+
+    /**
+     * Returns true if the message bytes starts with the specified string.
+     * 
+     * @param s
+     *            the string
+     * @param pos
+     *            The position
+     */
+//    public boolean startsWithIgnoreCase(String s, int pos) {
+//        byte[] b = buff;
+//        int len = s.length();
+//        if (b == null || len + pos > end - start) {
+//            return false;
+//        }
+//        int off = start + pos;
+//        for (int i = 0; i < len; i++) {
+//            if (Ascii.toLower(b[off++]) != Ascii.toLower(s.charAt(i))) {
+//                return false;
+//            }
+//        }
+//        return true;
+//    }
+
+    public int indexOf(String src, int srcOff, int srcLen, int myOff) {
+        char first = src.charAt(srcOff);
+
+        // Look for first char
+        int srcEnd = srcOff + srcLen;
+
+        for (int i = myOff + start; i <= (end - srcLen); i++) {
+            if (buff[i] != first)
+                continue;
+            // found first char, now look for a match
+            int myPos = i + 1;
+            for (int srcPos = srcOff + 1; srcPos < srcEnd;) {
+                if (buff[myPos++] != src.charAt(srcPos++))
+                    break;
+                if (srcPos == srcEnd)
+                    return i - start; // found it
+            }
+        }
+        return -1;
+    }
+
+    // hash ignoring case
+//    public int hashIgnoreCase() {
+//        return hashBytesIC(buff, start, end - start);
+//    }
+
+    public boolean isNull() {
+        return start == end;
+    }
+
+//    private static int hashBytesIC(byte bytes[], int start, int bytesLen) {
+//        int max = start + bytesLen;
+//        byte bb[] = bytes;
+//        int code = 0;
+//        for (int i = start; i < max; i++) {
+//            code = code * 37 + Ascii.toLower(bb[i]);
+//        }
+//        return code;
+//    }
+
+    @Override
+    public int limit() {
+        return end;
+    }
+
+    public void limit(int newEnd) {
+        end = newEnd;
+    }
+
+    /**
+     * Make space for len chars. 
+     * If len is small, allocate a reserve space too.
+     */
+    public BBuffer makeSpace(int count) {
+        byte[] tmp = null;
+
+        int newSize;
+        int desiredSize = end + count;
+
+        if (buff == null) {
+            if (desiredSize < 16)
+                desiredSize = 16; // take a minimum
+            buff = new byte[desiredSize];
+            start = 0;
+            end = 0;
+            return this;
+        }
+
+        // limit < buf.length ( the buffer is already big )
+        // or we already have space XXX
+        if (desiredSize <= buff.length) {
+            return this;
+        }
+        // grow in larger chunks
+        if (desiredSize < 2 * buff.length) {
+            newSize = buff.length * 2;
+            tmp = new byte[newSize];
+        } else {
+            newSize = buff.length * 2 + count;
+            tmp = new byte[newSize];
+        }
+
+        System.arraycopy(buff, start, tmp, 0, end - start);
+        buff = tmp;
+        tmp = null;
+        end = end - start;
+        start = 0;
+        return this;
+    }
+
+//    /**
+//     * Find a character, no side effects.
+//     * 
+//     * @return index of char if found, -1 if not
+//     */
+//    public static int findChars(byte buf[], int start, int end, byte c[]) {
+//        int clen = c.length;
+//        int offset = start;
+//        while (offset < end) {
+//            for (int i = 0; i < clen; i++)
+//                if (buf[offset] == c[i]) {
+//                    return offset;
+//                }
+//            offset++;
+//        }
+//        return -1;
+//    }
+
+//    /**
+//     * Find the first character != c
+//     * 
+//     * @return index of char if found, -1 if not
+//     */
+//    public static int findNotChars(byte buf[], int start, int end, byte c[]) {
+//        int clen = c.length;
+//        int offset = start;
+//        boolean found;
+//
+//        while (offset < end) {
+//            found = true;
+//            for (int i = 0; i < clen; i++) {
+//                if (buf[offset] == c[i]) {
+//                    found = false;
+//                    break;
+//                }
+//            }
+//            if (found) { // buf[offset] != c[0..len]
+//                return offset;
+//            }
+//            offset++;
+//        }
+//        return -1;
+//    }
+
+    @Override
+    public int position() {
+        return start;
+    }
+
+    
+
+    @Override
+    public void position(int newStart) {
+        start = newStart;
+    }
+
+    public void put(byte b) {
+        makeSpace(1);
+        buff[end++] = b;
+    }
+
+    public int read(BBuffer res) {
+        res.setBytes(buff, start, remaining());
+        end = start;
+        return res.remaining();
+    }
+
+    /** 
+     * Read a chunk from is.
+     * 
+     * You don't need to use buffered input stream, we do the 
+     * buffering.
+     */
+    public int read(InputStream is) throws IOException {
+        makeSpace(1024);
+        int res = is.read(buff, end, buff.length - end);
+        if (res > 0) {
+            end += res;
+        }
+        return res;
+    }
+
+    public int readAll(InputStream is) throws IOException {
+        int size = 0;
+        while (true) {
+            int res = read(is);
+            if (res < 0) {
+                return size;
+            }
+            size += res;
+        }
+    }
+
+    public int readByte() {
+        if (start == end) {
+            return -1;
+        }
+        return buff[start++];
+    }
+
+
+    /**
+     *  Read a line - excluding the line terminator, which is consummed as 
+     *  well but not included in the response.
+     *  
+     *  Line can end with CR, LF or CR/LF
+     * 
+     * @param res
+     * @return number of bytes read, or -1 if line ending not found in buffer.
+     */
+    public int readLine(BBuffer res) {
+        int cstart = start;
+        while(start < end) {
+            byte chr = buff[start++];
+            if (chr == CR || chr == LF) {
+                res.setBytes(buff, cstart, start - cstart -1);
+                if (chr == CR) {
+                    if (start < end) {
+                        byte chr2 = buff[start];
+                        if (chr2 == LF) {
+                            start++;
+                        }
+                    }
+                }
+                return res.remaining();
+            }
+        }
+        start = cstart;
+        return -1;
+    }
+    /** 
+     * Consume up to but not including delim.
+     * 
+     */
+    public final int readToDelimOrSpace(byte delim, 
+            BBuffer res) {
+        int resStart = start;
+        while (true) {
+            if (start >= end) {
+                break;
+            }
+            byte chr = buff[start];
+            if (chr == delim || chr == SP || chr == HT) {
+                break;
+            }
+            start++;
+        }
+        res.setBytes(buff, resStart, start - resStart);
+        return res.remaining();
+    }
+
+
+    /** 
+     * Consume all up to the first space or \t, which will be the
+     * first character in the buffer.
+     * 
+     * Consumed data is wrapped in res.
+     */
+    public int readToSpace(BBuffer res) {
+        int resStart = start;
+        while (true) {
+          if (start >= end) {
+              break;
+          }
+          if (buff[start] == SP 
+                  || buff[start] == HT) {
+              break;
+          }
+          start++;
+        }
+        res.setBytes(buff, resStart, start - resStart);
+        return res.remaining();
+    }
+    /**
+     * Resets the message buff to an uninitialized state.
+     */
+    public void recycle() {
+        start = 0;
+        end = 0;
+    }
+    @Override
+    public void release() { 
+//        synchronized (this) {
+//            useCount--;
+//            if (useCount == -1) {
+//                // all slices have been released -
+//                // TODO: callback, return to pool
+//            }
+//        }
+    }
+    public int remaining() {
+        return end - start;
+    }
+
+    public void reset() {
+        buff = null;
+    }
+    
+    // -------------------- Setup --------------------
+    /**
+     * Sets the message bytes to the specified subarray of bytes.
+     * 
+     * @param b
+     *            the ascii bytes
+     * @param off
+     *            the start offset of the bytes
+     * @param len
+     *            the length of the bytes
+     */
+    public void setBytes(byte[] b, int off, int len) {
+        throw new RuntimeException("Can't setBytes on allocated buffer");
+    }
+    
+    public void wrap(BBucket b) {
+        setBytes(b.array(), b.position(), b.remaining());
+    }
+
+    public void wrap(ByteBuffer b) {
+        setBytes(b.array(), b.position(), b.remaining());
+    }
+    
+    protected void setBytesInternal(byte[] b, int off, int len) {
+        buff = b;
+        start = off;
+        end = start + len;
+    }
+    
+//    public final void lowerCase() {
+//        while (start < end) {
+//            byte chr = buff[start];
+//            if ((chr >= A) && (chr <= Z)) {
+//                buff[start] = (byte) (chr - LC_OFFSET);
+//            }
+//            start++;
+//        }
+//    }
+
+    public void setEnd(int i) {
+        end = i;
+    }
+    
+    /**
+     * The old code from MessageBytes, used for setContentLength
+     * and setStatus.
+     * TODO: just use StringBuilder, the method is faster.
+     */
+    public void setLong(long l) {
+        if (array() == null) {
+            makeSpace(20);
+        }
+        long current = l;
+        byte[] buf = array();
+        int start = 0;
+        int end = 0;
+        if (l == 0) {
+            buf[end++] = (byte) '0';
+        } else if (l < 0) {
+            current = -l;
+            buf[end++] = (byte) '-';
+        }
+        while (current > 0) {
+            int digit = (int) (current % 10);
+            current = current / 10;
+            buf[end++] = Hex.HEX[digit];
+        }
+        setOffset(0);
+        setEnd(end);
+        // Inverting buffer
+        end--;
+        if (l < 0) {
+            start++;
+        }
+        while (end > start) {
+            byte temp = buf[start];
+            buf[start] = buf[end];
+            buf[end] = temp;
+            start++;
+            end--;
+        }
+    }
+    
+    public void setOffset(int off) {
+        if (end < off)
+            end = off;
+        start = off;
+    }
+    
+    
+    public int skipEmptyLines() {
+        int resStart = start;
+        while (buff[start] == CR || buff[start] == LF) {
+            start++;
+            if (start == end) {
+                break;
+            }
+        }
+        return start - resStart;
+    }
+    
+    public int skipSpace() {
+        int cstart = start;
+        while (true) {
+          if (start >= end) {
+            return start - cstart;
+          }
+          if ((buff[start] == SP) || (buff[start] == HT)) {
+            start++;
+          } else {
+            return start - cstart;
+          }
+        }
+    }
+
+    public int substract() {
+
+        if ((end - start) == 0) {
+            return -1;
+        }
+
+        return (buff[start++] & 0xFF);
+
+    }
+
+    public int substract(BBuffer src) {
+
+        if ((end - start) == 0) {
+            return -1;
+        }
+
+        int len = getLength();
+        src.append(buff, start, len);
+        start = end;
+        return len;
+
+    }
+    
+    public int substract(byte src[], int off, int len)  {
+
+        if ((end - start) == 0) {
+            return -1;
+        }
+
+        int n = len;
+        if (len > getLength()) {
+            n = getLength();
+        }
+        System.arraycopy(buff, start, src, off, n);
+        start += n;
+        return n;
+
+    }
+    
+    public String toString() {
+        return toString(DEFAULT_CHARACTER_ENCODING);
+    }
+
+    public String toString(String enc) {
+        if (null == buff) {
+            return null;
+        } else if (end - start == 0) {
+            return "";
+        }
+        
+        String strValue = null;
+        try {
+            if (enc == null) {
+                enc = DEFAULT_CHARACTER_ENCODING;
+            }
+            
+            strValue = new String(buff, start, end - start, enc);
+            /*
+             * Does not improve the speed too much on most systems, it's safer
+             * to use the "clasical" new String().
+             * 
+             * Most overhead is in creating char[] and copying, the internal
+             * implementation of new String() is very close to what we do. The
+             * decoder is nice for large buffers and if we don't go to String (
+             * so we can take advantage of reduced GC)
+             * 
+             * // Method is commented out, in: return B2CConverter.decodeString(
+             * enc );
+             */
+        } catch (java.io.UnsupportedEncodingException e) {
+            // Use the platform encoding in that case; the usage of a bad
+            // encoding will have been logged elsewhere already
+            strValue = new String(buff, start, end - start);
+        }
+        return strValue;
+    }
+
+    public void wrapTo(BBuffer res) {
+        res.setBytes(buff, start, remaining());
+    }
+
+    /**
+     * Convert specified String to a byte array. This ONLY WORKS for ascii, UTF
+     * chars will be truncated.
+     * 
+     * @param value
+     *            to convert to byte array
+     * @return the byte array value
+     */
+    public static final byte[] convertToBytes(String value) {
+        byte[] result = new byte[value.length()];
+        for (int i = 0; i < value.length(); i++) {
+            result[i] = (byte) value.charAt(i);
+        }
+        return result;
+    }
+    
+    /**
+     * Find a character, no side effects.
+     * 
+     * @return index of char if found, -1 if not
+     */
+    public static int findChar(byte buf[], int start, int end, char c) {
+        byte b = (byte) c;
+        int offset = start;
+        while (offset < end) {
+            if (buf[offset] == b) {
+                return offset;
+            }
+            offset++;
+        }
+        return -1;
+    }
+    private static int hashBytes(byte buff[], int start, int bytesLen) {
+        int max = start + bytesLen;
+        byte bb[] = buff;
+        int code = 0;
+        for (int i = start; i < max; i++) {
+            code = code * 31 + bb[i];
+            // TODO: if > 0x7F, convert to chars / switch to UTF8
+        }
+        return code;
+    }
+
+    public static boolean hasLFLF(BBucket bucket) {
+        int pos = bucket.position();
+        int lastValid = bucket.limit();
+        byte[] buf = bucket.array();
+        
+        for (int i = pos; i < lastValid; i++) {
+            byte chr = buf[i];
+            if (chr == LF) {
+                if (i + 1 < lastValid && buf[i + 1] == CR) {
+                    // \n\r\n
+                    i++;
+                }
+                if (i + 1 < lastValid && buf[i + 1] == LF) {
+                    return true; // \n\n
+                }
+            } else if (chr == CR) {
+                if (i + 1 < lastValid && buf[i + 1] == CR) {
+                    return true; // \r\r
+                }
+                if (i + 1 < lastValid && buf[i + 1] == LF) {
+                        // \r\n
+                    i++; // skip LF
+                    if (i + 1 < lastValid && buf[i + 1] == CR &&
+                            i + 2 < lastValid && buf[i + 2] == LF) {
+                        i++;
+                        return true;
+                    }
+                }
+                
+            }
+        }
+        return false;
+    }
+
+    public static int indexOf(byte bytes[], int off, int end, char qq) {
+        // Works only for UTF
+        while (off < end) {
+            byte b = bytes[off];
+            if (b == qq)
+                return off;
+            off++;
+        }
+        return -1;
+    }
+
+    /**
+     * Returns true if the specified ASCII character is a digit.
+     */
+
+    public static boolean isDigit(int c) {
+        return isDigit[c & 0xff];
+    }
+
+    /**
+     * Parses an unsigned integer from the specified subarray of bytes.
+     * @param b the bytes to parse
+     * @param off the start offset of the bytes
+     * @param len the length of the bytes
+     * @exception NumberFormatException if the integer format was invalid
+     */
+    public static int parseInt(byte[] b, int off, int len)
+        throws NumberFormatException
+    {
+        int c;
+
+        if (b == null || len <= 0 || !isDigit(c = b[off++])) {
+            throw new NumberFormatException();
+        }
+
+        int n = c - '0';
+
+        while (--len > 0) {
+            if (!isDigit(c = b[off++])) {
+                throw new NumberFormatException();
+            }
+            n = n * 10 + c - '0';
+        }
+
+        return n;
+    }
+
+    /**
+     * Parses an unsigned long from the specified subarray of bytes.
+     * @param b the bytes to parse
+     * @param off the start offset of the bytes
+     * @param len the length of the bytes
+     * @exception NumberFormatException if the long format was invalid
+     */
+    public static long parseLong(byte[] b, int off, int len)
+        throws NumberFormatException
+    {
+        int c;
+
+        if (b == null || len <= 0 || !isDigit(c = b[off++])) {
+            throw new NumberFormatException();
+        }
+
+        long n = c - '0';
+        long m;
+        
+        while (--len > 0) {
+            if (!isDigit(c = b[off++])) {
+                throw new NumberFormatException();
+            }
+            m = n * 10 + c - '0';
+
+            if (m < n) {
+                // Overflow
+                throw new NumberFormatException();
+            } else {
+                n = m;
+            }
+        }
+
+        return n;
+    }
+
+
+
+    /**
+     * Returns the lower case equivalent of the specified ASCII character.
+     */
+    public static int toLower(int c) {
+        if (c > 0x7f) return c;
+        return toLower[c & 0xff] & 0xff;
+    }
+
+    /**
+     * Returns true if the specified ASCII character is upper case.
+     */
+
+    public static boolean isUpper(int c) {
+        return c < 0x7f && isUpper[c];
+    }
+
+    /**
+     * A slice of a bucket, holding reference to a parent bucket.
+     * 
+     * This is used when a filter splits a bucket - the original
+     * will be replaced with 1 or more slices. When all slices are
+     * released, the parent will also be released.
+     * 
+     * It is not possible to add data.
+     * 
+     * @author Costin Manolache
+     */
+    static class IOBucketWrap extends BBuffer {
+        //IOBucket parent;
+        
+        
+        public BBuffer makeSpace(int count) {
+            throw new RuntimeException("Attempting to change buffer " +
+                       "on a wrapped BBuffer");
+        }
+
+        public void release() { 
+//            if (parent != null) {
+//                parent.release();
+//            }
+        }
+
+        public void setBytes(byte[] b, int off, int len) {
+            super.setBytesInternal(b, off, len);
+        }
+    }
+    
+    
+}
diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/io/BufferedIOReader.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/BufferedIOReader.java
new file mode 100644 (file)
index 0000000..6f7dd0d
--- /dev/null
@@ -0,0 +1,381 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.lite.io;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.Reader;
+import java.nio.CharBuffer;
+
+
+/**
+ * Cut&pasted from Harmony buffered reader ( apache license ).
+ * Changes:
+ * - additional method to recycle to avoid re-allocating on 
+ * each request.
+ */
+public class BufferedIOReader extends BufferedReader {
+
+    // Not recycled - the buffer is tied to the message/IOReader
+    IOReader in;
+
+    private String enc;
+    boolean closed;
+    private char[] buf;
+    private int marklimit = -1;
+
+    private int count;
+
+    private int markpos = -1;
+
+    private int pos;
+
+    public BufferedIOReader(IOReader realReader) {
+        // we're not using super - we override all methods, but need the 
+        // signature 
+        super(DUMMY_READER, 1); 
+        this.in = realReader;
+        buf = new char[8192];
+    }
+
+    public void recycle() {
+        enc = null;
+        closed = false;
+        
+        if (in != null) {
+            in.recycle();
+        }
+        marklimit = -1;
+        count = 0;
+        markpos = -1;
+        pos = 0;
+    }
+
+    private void checkClosed() throws IOException {
+        if (closed) throw new IOException("closed");
+    }
+    
+    public int read(CharBuffer target) throws IOException {
+        checkClosed();
+        int len = target.remaining();
+        int n = read(target.array(), target.position(), target.remaining());
+        if (n > 0)
+            target.position(target.position() + n);
+        return n;
+    }
+
+
+    public int read(char[] cbuf) throws IOException {
+        return read(cbuf, 0, cbuf.length);
+    }
+
+
+    /**
+     * Closes this reader. This implementation closes the buffered source reader
+     * and releases the buffer. Nothing is done if this reader has already been
+     * closed.
+     * 
+     * @throws IOException
+     *             if an error occurs while closing this reader.
+     */
+    @Override
+    public void close() throws IOException {
+        synchronized (lock) {
+            if (!isClosed()) {
+                in.close();
+                closed = true;
+                // buf remains
+            }
+        }
+    }
+
+    private int fillbuf() throws IOException {
+        if (markpos == -1 || (pos - markpos >= marklimit)) {
+            /* Mark position not set or exceeded readlimit */
+            int result = in.read(buf, 0, buf.length);
+            if (result > 0) {
+                markpos = -1;
+                pos = 0;
+                count = result == -1 ? 0 : result;
+            }
+            return result;
+        }
+        if (markpos == 0 && marklimit > buf.length) {
+            /* Increase buffer size to accommodate the readlimit */
+            int newLength = buf.length * 2;
+            if (newLength > marklimit) {
+                newLength = marklimit;
+            }
+            char[] newbuf = new char[newLength];
+            System.arraycopy(buf, 0, newbuf, 0, buf.length);
+            buf = newbuf;
+        } else if (markpos > 0) {
+            System.arraycopy(buf, markpos, buf, 0, buf.length - markpos);
+        }
+
+        /* Set the new position and mark position */
+        pos -= markpos;
+        count = markpos = 0;
+        int charsread = in.read(buf, pos, buf.length - pos);
+        count = charsread == -1 ? pos : pos + charsread;
+        return charsread;
+    }
+
+    private boolean isClosed() {
+        return closed;
+    }
+
+    @Override
+    public void mark(int readlimit) throws IOException {
+        if (readlimit < 0) {
+            throw new IllegalArgumentException();
+        }
+        synchronized (lock) {
+            checkClosed();
+            marklimit = readlimit;
+            markpos = pos;
+        }
+    }
+
+    @Override
+    public boolean markSupported() {
+        return true;
+    }
+
+    @Override
+    public int read() throws IOException {
+        synchronized (lock) {
+            checkClosed();
+            /* Are there buffered characters available? */
+            if (pos < count || fillbuf() != -1) {
+                return buf[pos++];
+            }
+            markpos = -1;
+            return -1;
+        }
+    }
+
+    @Override
+    public int read(char[] buffer, int offset, int length) throws IOException {
+        synchronized (lock) {
+            checkClosed();
+            if (offset < 0 || offset > buffer.length - length || length < 0) {
+                throw new IndexOutOfBoundsException();
+            }
+            if (length == 0) {
+                return 0;
+            }
+            int required;
+            if (pos < count) {
+                /* There are bytes available in the buffer. */
+                int copylength = count - pos >= length ? length : count - pos;
+                System.arraycopy(buf, pos, buffer, offset, copylength);
+                pos += copylength;
+                if (copylength == length || !in.ready()) {
+                    return copylength;
+                }
+                offset += copylength;
+                required = length - copylength;
+            } else {
+                required = length;
+            }
+
+            while (true) {
+                int read;
+                /*
+                 * If we're not marked and the required size is greater than the
+                 * buffer, simply read the bytes directly bypassing the buffer.
+                 */
+                if (markpos == -1 && required >= buf.length) {
+                    read = in.read(buffer, offset, required);
+                    if (read == -1) {
+                        return required == length ? -1 : length - required;
+                    }
+                } else {
+                    if (fillbuf() == -1) {
+                        return required == length ? -1 : length - required;
+                    }
+                    read = count - pos >= required ? required : count - pos;
+                    System.arraycopy(buf, pos, buffer, offset, read);
+                    pos += read;
+                }
+                required -= read;
+                if (required == 0) {
+                    return length;
+                }
+                if (!in.ready()) {
+                    return length - required;
+                }
+                offset += read;
+            }
+        }
+    }
+
+    /**
+     * Returns the next line of text available from this reader. A line is
+     * represented by zero or more characters followed by {@code '\n'},
+     * {@code '\r'}, {@code "\r\n"} or the end of the reader. The string does
+     * not include the newline sequence.
+     * 
+     * @return the contents of the line or {@code null} if no characters were
+     *         read before the end of the reader has been reached.
+     * @throws IOException
+     *             if this reader is closed or some other I/O error occurs.
+     */
+    public String readLine() throws IOException {
+        synchronized (lock) {
+            checkClosed();
+            /* Are there buffered characters available? */
+            if ((pos >= count) && (fillbuf() == -1)) {
+                return null;
+            }
+            for (int charPos = pos; charPos < count; charPos++) {
+                char ch = buf[charPos];
+                if (ch > '\r') {
+                    continue;
+                }
+                if (ch == '\n') {
+                    String res = new String(buf, pos, charPos - pos);
+                    pos = charPos + 1;
+                    return res;
+                } else if (ch == '\r') {
+                    String res = new String(buf, pos, charPos - pos);
+                    pos = charPos + 1;
+                    if (((pos < count) || (fillbuf() != -1))
+                            && (buf[pos] == '\n')) {
+                        pos++;
+                    }
+                    return res;
+                }
+            }
+
+            char eol = '\0';
+            StringBuilder result = new StringBuilder(80);
+            /* Typical Line Length */
+
+            result.append(buf, pos, count - pos);
+            pos = count;
+            while (true) {
+                /* Are there buffered characters available? */
+                if (pos >= count) {
+                    if (eol == '\n') {
+                        return result.toString();
+                    }
+                    // attempt to fill buffer
+                    if (fillbuf() == -1) {
+                        // characters or null.
+                        return result.length() > 0 || eol != '\0' ? result
+                                .toString() : null;
+                    }
+                }
+                for (int charPos = pos; charPos < count; charPos++) {
+                    if (eol == '\0') {
+                        if ((buf[charPos] == '\n' || buf[charPos] == '\r')) {
+                            eol = buf[charPos];
+                        }
+                    } else if (eol == '\r' && (buf[charPos] == '\n')) {
+                        if (charPos > pos) {
+                            result.append(buf, pos, charPos - pos - 1);
+                        }
+                        pos = charPos + 1;
+                        return result.toString();
+                    } else {
+                        if (charPos > pos) {
+                            result.append(buf, pos, charPos - pos - 1);
+                        }
+                        pos = charPos;
+                        return result.toString();
+                    }
+                }
+                if (eol == '\0') {
+                    result.append(buf, pos, count - pos);
+                } else {
+                    result.append(buf, pos, count - pos - 1);
+                }
+                pos = count;
+            }
+        }
+
+    }
+
+    
+    @Override
+    public boolean ready() throws IOException {
+        synchronized (lock) {
+            checkClosed();
+            return ((count - pos) > 0) || in.ready();
+        }
+    }
+
+    @Override
+    public void reset() throws IOException {
+        synchronized (lock) {
+            checkClosed();
+            if (markpos == -1) {
+                throw new IOException("No mark");
+            }
+            pos = markpos;
+        }
+    }
+
+    @Override
+    public long skip(long amount) throws IOException {
+        if (amount < 0) {
+            throw new IllegalArgumentException();
+        }
+        synchronized (lock) {
+            checkClosed();
+            if (amount < 1) {
+                return 0;
+            }
+            if (count - pos >= amount) {
+                pos += amount;
+                return amount;
+            }
+
+            long read = count - pos;
+            pos = count;
+            while (read < amount) {
+                if (fillbuf() == -1) {
+                    return read;
+                }
+                if (count - pos >= amount - read) {
+                    pos += amount - read;
+                    return amount;
+                }
+                // Couldn't get all the characters, skip what we read
+                read += (count - pos);
+                pos = count;
+            }
+            return amount;
+        }
+    }
+    
+    private static Reader DUMMY_READER = new Reader() {
+        @Override
+        public void close() throws IOException {
+        }
+
+        @Override
+        public int read(char[] cbuf, int off, int len) throws IOException {
+            return 0;
+        }
+    };
+
+    
+
+}
diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/io/CBucket.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/CBucket.java
new file mode 100644 (file)
index 0000000..e28d16a
--- /dev/null
@@ -0,0 +1,510 @@
+/*
+ */
+package org.apache.tomcat.lite.io;
+
+import java.io.Serializable;
+import java.nio.CharBuffer;
+
+/**
+ * Wraps a char[]. 
+ * 
+ * Doesn't provide any mutation methods. Classes in this package
+ * have access to the buffer, for conversions. 
+ * 
+ * 
+ * @author Costin Manolache
+ */
+public class CBucket implements CharSequence, Comparable, Serializable {
+    protected char value[];
+
+    protected int start;
+
+    protected int end;
+
+    // Reused.
+    protected CharBuffer cb;
+    
+    // cache
+    protected String strValue;
+    protected int hash;
+
+    public CBucket() {
+    }
+
+    /** 
+     * Used by IOWriter for conversion. Will not modify the content.
+     */
+    CharBuffer getNioBuffer() {
+        if (cb == null || cb.array() != value) {
+            cb = CharBuffer.wrap(value, start, end - start);
+        } else {
+            cb.position(start);
+            cb.limit(end);
+        }
+        return cb;
+    }
+    
+    public void recycle() {
+        start = 0;
+        end = 0;
+        value = null;
+        strValue = null;
+        hash = 0;
+    }
+    
+    public String toString() {
+        if (null == value) {
+            return null;
+        } else if (end - start == 0) {
+            return "";
+        }
+        if (strValue == null) {
+            strValue = new String(value, start, end - start);
+        }
+        return strValue;
+    }
+
+    /**
+     * Same as String 
+     */
+    public int hashCode() {
+        int h = hash;
+        if (h == 0) {
+            int off = start;
+            char val[] = value;
+
+            for (int i = start; i < end; i++) {
+                h = 31*h + val[off++];
+            }
+            hash = h;
+        }
+        return h;
+    }
+    
+    public long getLong() {
+        return parseLong(value, start, end - start);
+    }
+
+    public int getInt() {
+        return parseInt(value, start, end - start);
+    }
+
+    public static int parseInt(char[] b, int off, int len)
+        throws NumberFormatException
+    {
+        int c;
+
+        if (b == null || len <= 0 || !BBuffer.isDigit(c = b[off++])) {
+            throw new NumberFormatException();
+        }
+
+        int n = c - '0';
+
+        while (--len > 0) {
+            if (!BBuffer.isDigit(c = b[off++])) {
+                throw new NumberFormatException();
+            }
+            n = n * 10 + c - '0';
+        }
+
+        return n;
+    }
+
+
+    public static long parseLong(char[] b, int off, int len)
+        throws NumberFormatException
+    {
+        int c;
+
+        if (b == null || len <= 0 || !BBuffer.isDigit(c = b[off++])) {
+            throw new NumberFormatException();
+        }
+
+        long n = c - '0';
+        long m;
+
+        while (--len > 0) {
+            if (!BBuffer.isDigit(c = b[off++])) {
+                throw new NumberFormatException();
+            }
+            m = n * 10 + c - '0';
+
+            if (m < n) {
+                // Overflow
+                throw new NumberFormatException();
+            } else {
+                n = m;
+            }
+        }
+
+        return n;
+    }
+
+
+    
+
+    /**
+     * Compares the message bytes to the specified String object.
+     * 
+     * @param s
+     *            the String to compare
+     * @return true if the comparison succeeded, false otherwise
+     */
+    public boolean equals(String s) {
+        char[] c = value;
+        int len = end - start;
+        if (c == null || len != s.length()) {
+            return false;
+        }
+        int off = start;
+        for (int i = 0; i < len; i++) {
+            if (c[off++] != s.charAt(i)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Compares the message bytes to the specified String object.
+     * 
+     * @param s
+     *            the String to compare
+     * @return true if the comparison succeeded, false otherwise
+     */
+    public boolean equalsIgnoreCase(String s) {
+        char[] c = value;
+        int len = end - start;
+        if (c == null || len != s.length()) {
+            return false;
+        }
+        int off = start;
+        for (int i = 0; i < len; i++) {
+            if (BBuffer.toLower(c[off++]) != BBuffer.toLower(s.charAt(i))) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    public boolean equals(Object obj) {
+        if (obj instanceof CBuffer) {
+            CBuffer cc = (CBuffer) obj;
+            return equals(cc.value, cc.start, cc.length());
+        } else if (obj instanceof String) {
+            return equals((String)obj);
+        }
+        return false;
+    }
+
+    public boolean equals(char b2[], int off2, int len2) {
+        char b1[] = value;
+        if (b1 == null && b2 == null)
+            return true;
+
+        if (b1 == null || b2 == null || end - start != len2) {
+            return false;
+        }
+        int off1 = start;
+        int len = end - start;
+        while (len-- > 0) {
+            if (b1[off1++] != b2[off2++]) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    public boolean equals(byte b2[], int off2, int len2) {
+        char b1[] = value;
+        if (b2 == null && b1 == null)
+            return true;
+
+        if (b1 == null || b2 == null || end - start != len2) {
+            return false;
+        }
+        int off1 = start;
+        int len = end - start;
+
+        while (len-- > 0) {
+            if (b1[off1++] != (char) b2[off2++]) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    
+    /**
+     * Returns true if the message bytes starts with the specified string.
+     * 
+     * @param s
+     *            the string
+     */
+    public boolean startsWith(String s) {
+        char[] c = value;
+        int len = s.length();
+        if (c == null || len > end - start) {
+            return false;
+        }
+        int off = start;
+        for (int i = 0; i < len; i++) {
+            if (c[off++] != s.charAt(i)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Returns true if the message bytes starts with the specified string.
+     * 
+     * @param s
+     *            the string
+     */
+    public boolean startsWithIgnoreCase(String s, int pos) {
+        char[] c = value;
+        int len = s.length();
+        if (c == null || len + pos > end - start) {
+            return false;
+        }
+        int off = start + pos;
+        for (int i = 0; i < len; i++) {
+            if (BBuffer.toLower(c[off++]) != BBuffer.toLower(s.charAt(i))) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    public int indexOf(char c) {
+        return indexOf(c, start);
+    }
+    
+    public int lastIndexOf(char c) {
+        return lastIndexOf(c, 0, end - start);
+    }
+
+    /** 
+     */
+    public int lastIndexOf(char c, int off, int len) {
+        char[] buf = value;
+        int slash = -1;
+        for (int i = start + len - 1; i >= start + off; i--) {
+            if (buf[i] == c) {
+                slash = i - start;
+                break;
+            }
+        }
+        return slash;
+    }
+    
+    /**
+     * Returns true if the message bytes starts with the specified string.
+     * 
+     * @param c
+     *            the character
+     */
+    public int indexOf(char c, int starting) {
+        int ret = indexOf(value, start + starting, end, c);
+        return (ret >= start) ? ret - start : -1;
+    }
+
+    public static int indexOf(char chars[], int off, int cend, char qq) {
+        while (off < cend) {
+            char b = chars[off];
+            if (b == qq)
+                return off;
+            off++;
+        }
+        return -1;
+    }
+
+    public int indexOf(String src) {
+        return indexOf(src, 0, src.length(), 0);
+    }
+    
+    public int indexOf(String src, int srcOff, int srcLen, int myOff) {
+        char first = src.charAt(srcOff);
+
+        // Look for first char
+        int srcEnd = srcOff + srcLen;
+
+        for (int i = myOff + start; i <= (end - srcLen); i++) {
+            if (value[i] != first)
+                continue;
+            // found first char, now look for a match
+            int myPos = i + 1;
+            for (int srcPos = srcOff + 1; srcPos < srcEnd;) {
+                if (value[myPos++] != src.charAt(srcPos++))
+                    break;
+                if (srcPos == srcEnd)
+                    return i - start; // found it
+            }
+        }
+        return -1;
+    }
+    
+    public char lastChar() {
+        return value[end - 1];
+    }
+
+    public char charAt(int index) {
+        return value[index + start];
+    }
+    
+    public void wrap(char[] buff, int start, int end) {
+        if (value != null) {
+            throw new RuntimeException("Can wrap only once");
+        }
+        this.value = buff;
+        this.start = start;
+        this.end = end;
+    }
+    
+    public CharSequence subSequence(int sstart, int send) {
+        CBucket seq = new CBucket();
+        seq.wrap(this.value, start + sstart, start + send);
+        return seq;
+    }
+    
+    public int length() {
+        return end - start;
+    }
+
+    @Override
+    public int compareTo(Object o) {
+        // Code based on Harmony
+        if (o instanceof CBuffer) {
+            CBuffer dest = (CBuffer) o;
+            int o1 = start, o2 = dest.start, result;
+            int len = end - start;
+            int destLen = dest.end - dest.start;
+            int fin = (len < destLen ?
+                    end : start + destLen);
+            char[] target = dest.value;
+            while (o1 < fin) {
+                if ((result = value[o1++] - target[o2++]) != 0) {
+                    return result;
+                }
+            }
+            return len - destLen;
+            
+        } else if (o instanceof CharSequence) {
+            CharSequence dest = (CharSequence) o;
+            int o1 = start, o2 = 0, result;
+            int len = end - start;
+            int destLen = dest.length();
+            int fin = (len < destLen ? 
+                    end : start + destLen);
+            while (o1 < fin) {
+                if ((result = value[o1++] - dest.charAt(o2++)) != 0) {
+                    return result;
+                }
+            }
+            return len - destLen;
+            
+        } else {
+            throw new RuntimeException("CompareTo not supported " + o);
+        }
+    }
+
+    /**
+     * Compare given char chunk with String ignoring case.
+     * Return -1, 0 or +1 if inferior, equal, or superior to the String.
+     */
+    public final int compareIgnoreCase(String compareTo) {
+        int result = 0;
+        char[] c = value;
+        int len = compareTo.length();
+        if ((end - start) < len) {
+            len = end - start;
+        }
+        for (int i = 0; (i < len) && (result == 0); i++) {
+            if (BBuffer.toLower(c[i + start]) > BBuffer.toLower(compareTo.charAt(i))) {
+                result = 1;
+            } else if (BBuffer.toLower(c[i + start]) < BBuffer.toLower(compareTo.charAt(i))) {
+                result = -1;
+            }
+        }
+        if (result == 0) {
+            if (compareTo.length() > (end - start)) {
+                result = -1;
+            } else if (compareTo.length() < (end - start)) {
+                result = 1;
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Compare given char chunk with String.
+     * Return -1, 0 or +1 if inferior, equal, or superior to the String.
+     */
+    public final int compare(String compareTo) {
+        int result = 0;
+        char[] c = value;
+        int len = compareTo.length();
+        if ((end - start) < len) {
+            len = end - start;
+        }
+        for (int i = 0; (i < len) && (result == 0); i++) {
+            if (c[i + start] > compareTo.charAt(i)) {
+                result = 1;
+            } else if (c[i + start] < compareTo.charAt(i)) {
+                result = -1;
+            }
+        }
+        if (result == 0) {
+            if (compareTo.length() > (end - start)) {
+                result = -1;
+            } else if (compareTo.length() < (end - start)) {
+                result = 1;
+            }
+        }
+        return result;
+    }
+    
+    public int getExtension(CBuffer ext, char slashC, char dotC) {
+        int slash = lastIndexOf(slashC);
+        if (slash < 0) {
+            slash = 0;
+        }
+        int dot = lastIndexOf(dotC, slash, length());
+        if (dot < 0) {
+            return -1;
+        }
+        ext.wrap(this, dot + 1, length());
+        return dot;
+    }
+
+    /**
+     * Find the position of the nth slash, in the given char chunk.
+     */
+    public final int nthSlash(int n) {
+        char[] c = value;
+        int pos = start;
+        int count = 0;
+    
+        while (pos < end) {
+            if ((c[pos++] == '/') && ((++count) == n)) {
+                pos--;
+                break;
+            }
+        }
+    
+        return pos - start;
+    }
+    
+
+    public boolean hasUpper() {
+        for (int i = start; i < end; i++) {
+            char c = value[i];
+            if (c < 0x7F && BBuffer.isUpper(c)) {
+                return true;
+            }
+        }
+        return false;
+    }
+    
+}
diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/io/CBuffer.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/CBuffer.java
new file mode 100644 (file)
index 0000000..bfc36f9
--- /dev/null
@@ -0,0 +1,371 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package org.apache.tomcat.lite.io;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.nio.CharBuffer;
+
+
+/**
+ * Similar with StringBuilder or StringBuffer, but with access to the 
+ * raw buffer - this avoids copying the data.
+ * 
+ * Utilities to manipluate char chunks. While String is the easiest way to
+ * manipulate chars ( search, substrings, etc), it is known to not be the most
+ * efficient solution - Strings are designed as imutable and secure objects.
+ * 
+ * @author dac@sun.com
+ * @author James Todd [gonzo@sun.com]
+ * @author Costin Manolache
+ * @author Remy Maucherat
+ */
+public class CBuffer extends CBucket implements Cloneable,   
+        Appendable {
+
+    
+    /**
+     * Creates a new, uninitialized CharChunk object.
+     */
+    public static CBuffer newInstance() {
+        return new CBuffer();
+    }
+
+    private CBuffer() {
+    }
+
+    /**
+     * Resets the message bytes to an uninitialized state.
+     */
+    public void recycle() {
+        dirty();
+        start = 0;
+        end = 0;
+    }
+    
+    /**
+     * Same as String 
+     */
+    public int hashCode() {
+        int h = 0;
+        int off = start;
+        char val[] = value;
+        
+        for (int i = start; i < end; i++) {
+            h = 31*h + val[off++];
+        }
+        return h;
+    }
+
+    public String toString() {
+        if (null == value) {
+            return null;
+        } else if (end - start == 0) {
+            return "";
+        }
+        return new String(value, start, end - start);
+    }
+
+    public void wrap(char[] buff, int start, int end) {
+        dirty();
+        this.value = buff;
+        this.start = start;
+        this.end = end;
+    }
+
+    public void wrap(CBucket buff, int off, int srcEnd) {
+        dirty();
+        this.value = buff.value;
+        this.start = buff.start + off;
+        this.end = this.start + srcEnd - off;
+    }
+    
+    
+    // ----------- Used for IOWriter / conversion ---------
+    
+    public char[] array() {
+        return value;
+    }
+    
+    public int position() {
+        return start;
+    }
+    
+    CharBuffer getAppendCharBuffer() {
+        makeSpace(16);
+        if (cb == null || cb.array() != value) {
+            cb = CharBuffer.wrap(value, end, value.length - end);
+        } else {
+            cb.position(end);
+            cb.limit(value.length);
+        }
+        return cb;        
+    }
+
+    void returnNioBuffer(CharBuffer c) {
+        dirty();
+        start = c.position();
+    }
+    
+    void returnAppendCharBuffer(CharBuffer c) {
+        dirty();
+        end = c.position();
+    }
+
+    // -------- Delete / replace ---------------
+    
+    /** 
+     * 'Delete' all chars after offset.
+     * 
+     * @param offset
+     */
+    public void delete(int offset) {
+       dirty();
+       end = start + offset;
+    }
+
+    // -------------------- Adding data --------------------
+
+    /**
+     * Append methods take start and end - similar with this one.
+     * The source is not modified.
+     */
+    @Override
+    public CBuffer append(CharSequence csq, int astart, int aend)
+            throws IOException {
+        makeSpace(aend - astart);
+        
+        for (int i = astart; i < aend; i++) {
+            value[end++] = csq.charAt(i);
+        }
+        return this;
+    }
+
+    public CBuffer append(char b) {
+        makeSpace(1);
+        value[end++] = b;
+        return this;
+    }
+
+    /**
+     * Add data to the buffer
+     */
+    public CBuffer append(char src[], int srcStart, int srcEnd)  {
+        int len = srcEnd - srcStart;
+        // will grow, up to limit
+        makeSpace(len);
+
+        // assert: makeSpace made enough space
+        System.arraycopy(src, srcStart, value, end, len);
+        end += len;
+        return this;
+    }
+
+    /**
+     * Add data to the buffer
+     */
+    public CBuffer append(StringBuffer sb) {
+        int len = sb.length();
+        makeSpace(len);
+        sb.getChars(0, len, value, end);
+        end += len;
+        return this;
+    }
+
+    /**
+     * Append a string to the buffer
+     */
+    public CBuffer append(String s) {
+        if (s == null) {
+            return this;
+        }
+        append(s, 0, s.length());
+        return this;
+    }
+
+
+    
+    /**
+     * Append a string to the buffer
+     */
+    public CBuffer append(String s, int off, int srcEnd) {
+        if (s == null)
+            return this;
+
+        // will grow, up to limit
+        makeSpace(srcEnd - off);
+
+        // assert: makeSpace made enough space
+        s.getChars(off, srcEnd, value, end);
+        end += srcEnd - off;
+        return this;
+    }
+
+    // TODO: long, int conversions -> get from harmony Long
+    public CBuffer appendInt(int i) {
+        // TODO: copy from harmony StringBuffer
+        append(Integer.toString(i));
+        return this;
+    }
+    
+    
+    public Appendable append(CharSequence cs) {
+        if (cs instanceof CBuffer) {
+            CBuffer src = (CBuffer) cs;
+            append(src.value, src.start, src.end);
+        } else if (cs instanceof String) {
+            append((String) cs);
+        } else {
+            for (int i = 0; i < cs.length(); i++) {
+                append(cs.charAt(i));
+            }
+        }
+        return  this;
+    }
+
+    public CBuffer append(CBuffer src) {
+        append(src.value, src.start, src.end);
+        return  this;
+    }
+
+    
+    public CBuffer append(BBucket bb) {
+        byte[] bbuf = bb.array();
+        int start = bb.position();
+        appendAscii(bbuf, start, bb.remaining());
+        return this;
+    }
+
+    public CBuffer appendAscii(byte[] bbuf, int start, int len) {
+        makeSpace(len);
+        char[] cbuf = value;
+        for (int i = 0; i < len; i++) {
+            cbuf[end + i] = (char) (bbuf[i + start] & 0xff);
+        }
+        end += len;
+        return this;
+    }
+    
+    
+    /**
+     *  Append and advance CharBuffer.
+     * 
+     * @param c
+     */
+    public CBuffer put(CharBuffer c) {
+        append(c.array(), c.position(), c.limit());
+        c.position(c.limit());
+        return this;
+    }
+
+    // ------------- 'set' methods ---------------
+    // equivalent with clean + append
+    
+    public CBuffer set(CBuffer csq, int off, int len) {
+        recycle();
+        append(csq.value, csq.start + off, csq.start + off + len);
+        return this;
+    }
+
+    public CBuffer setChars(char[] c, int off, int len) {
+        recycle();
+        append(c, off, off + len);
+        return this;
+    }
+    
+    public CBuffer set(BBucket bb) {
+        recycle();
+        byte[] bbuf = bb.array();
+        int start = bb.position();
+        appendAscii(bbuf, start, bb.remaining());
+        return this;
+    }
+
+    public CBuffer set(CharSequence csq) {
+        recycle();
+        append(csq);
+        return this;
+    }
+
+    public CBuffer set(CBuffer csq) {
+        recycle();
+        append(csq);
+        return this;
+    }
+
+    public CBuffer set(String csq) {
+        recycle();
+        append(csq);
+        return this;
+    }
+    
+    private void dirty() {
+        hash = 0;
+        strValue = null;
+    }
+    
+    /**
+     * Make space for len chars. If len is small, allocate a reserve space too.
+     * Never grow bigger than limit.
+     */
+    private void makeSpace(int count) {
+        dirty();
+        char[] tmp = null;
+
+        int newSize;
+        int desiredSize = end + count;
+
+        if (value == null) {
+            if (desiredSize < 256)
+                desiredSize = 256; // take a minimum
+            value = new char[desiredSize];
+        }
+
+        // limit < buf.length ( the buffer is already big )
+        // or we already have space XXX
+        if (desiredSize <= value.length) {
+            return;
+        }
+        // grow in larger chunks
+        if (desiredSize < 2 * value.length) {
+            newSize = value.length * 2;
+            tmp = new char[newSize];
+        } else {
+            newSize = value.length * 2 + count;
+            tmp = new char[newSize];
+        }
+
+        System.arraycopy(value, 0, tmp, 0, end);
+        value = tmp;
+        tmp = null;
+    }
+
+    public void toLower() {
+        for (int i = start; i < end; i++) {
+            char c = value[i];
+            if (c < 0x7F) {
+                if (BBuffer.isUpper(c)) {
+                    value[i] = (char) BBuffer.toLower(c);
+                }
+                
+            }
+        }
+    }
+
+    
+}
diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/io/DumpChannel.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/DumpChannel.java
new file mode 100644 (file)
index 0000000..bb8ce5f
--- /dev/null
@@ -0,0 +1,93 @@
+/*
+ */
+package org.apache.tomcat.lite.io;
+
+import java.io.IOException;
+
+// TODO: dump to a file, hex, etc.
+/**
+ * For debug - will print all bytes that go trough the channel
+ */
+public class DumpChannel extends IOChannel {
+    
+    IOBuffer in = new IOBuffer(this);
+    IOBuffer out = new IOBuffer(this);
+    
+    public DumpChannel(String id) {
+        this.id = id;
+    }
+    
+    public String toString() {
+        return "Dump-" + id + "-" + net.toString();
+    }
+    
+    @Override
+    public void handleReceived(IOChannel ch) throws IOException {
+        processInput(ch.getIn());
+    }
+
+    private void processInput(IOBuffer netIn) throws IOException {
+        boolean any = false;
+        while (true) {
+            BBucket first = netIn.popFirst();
+            if (first == null) {
+                if (netIn.isClosedAndEmpty()) {
+                    out("IN", first, true);
+                    in.close();
+                    any = true;
+                }
+                if (any) {
+                    sendHandleReceivedCallback();
+                }
+                return;
+            }
+            any = true;
+            out("IN", first, false);
+            in.queue(first);
+        }
+    }
+
+    public void startSending() throws IOException {
+        while (true) {
+            BBucket first = out.popFirst();
+            if (first == null) {
+                if (out.isClosedAndEmpty()) {
+                    out("OUT", first, true);
+                    net.getOut().close();
+                }
+                
+                net.startSending();
+                return;
+            }
+            // Dump
+            out("OUT", first, net.getOut().isAppendClosed());
+            net.getOut().queue(first);
+        }
+    }
+    
+    private void out(String dir, BBucket first, boolean closed) {
+        // Dump
+        if (first != null) {
+            String hd = Hex.getHexDump(first.array(), first.position(), 
+                    first.remaining(), true);
+            System.err.println("\n" + dir + ": " + id + " " +
+                    (closed ? "CLS" : "") +
+                    + first.remaining() + "\n" + 
+                    hd);
+        } else {
+            System.err.println("\n" + dir + ": " + id + " " +
+                    (closed ? "CLS" : "") +
+                     "END\n"); 
+        }
+    }
+    
+    @Override
+    public IOBuffer getIn() {
+        return in;
+    }
+
+    @Override
+    public IOBuffer getOut() {
+        return out;
+    }
+}
diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/io/FastHttpDateFormat.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/FastHttpDateFormat.java
new file mode 100644 (file)
index 0000000..5681767
--- /dev/null
@@ -0,0 +1,231 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package org.apache.tomcat.lite.io;
+
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.TimeZone;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Utility class to generate HTTP dates.
+ * 
+ * @author Remy Maucherat
+ */
+public final class FastHttpDateFormat {
+
+
+    // -------------------------------------------------------------- Variables
+
+
+    protected static final int CACHE_SIZE = 
+        Integer.parseInt(System.getProperty("org.apache.tomcat.util.http.FastHttpDateFormat.CACHE_SIZE", "1000"));
+
+    
+    /**
+     * HTTP date format.
+     */
+    protected static final SimpleDateFormat format = 
+        new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US);
+
+
+    /**
+     * The set of SimpleDateFormat formats to use in getDateHeader().
+     */
+    protected static final SimpleDateFormat formats[] = {
+        new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US),
+        new SimpleDateFormat("EEEEEE, dd-MMM-yy HH:mm:ss zzz", Locale.US),
+        new SimpleDateFormat("EEE MMMM d HH:mm:ss yyyy", Locale.US)
+    };
+
+
+    protected final static TimeZone gmtZone = TimeZone.getTimeZone("GMT");
+
+
+    /**
+     * GMT timezone - all HTTP dates are on GMT
+     */
+    static {
+
+        format.setTimeZone(gmtZone);
+
+        formats[0].setTimeZone(gmtZone);
+        formats[1].setTimeZone(gmtZone);
+        formats[2].setTimeZone(gmtZone);
+
+    }
+
+
+    /**
+     * Instant on which the currentDate object was generated.
+     */
+    protected static long currentDateGenerated = 0L;
+
+
+    /**
+     * Current formatted date.
+     */
+    protected static String currentDate = null;
+
+
+    /**
+     * Formatter cache.
+     */
+    protected static final ConcurrentHashMap<Long, String> formatCache = 
+        new ConcurrentHashMap<Long, String>(CACHE_SIZE);
+
+
+    /**
+     * Parser cache.
+     */
+    protected static final ConcurrentHashMap<String, Long> parseCache = 
+        new ConcurrentHashMap<String, Long>(CACHE_SIZE);
+
+
+    // --------------------------------------------------------- Public Methods
+
+
+    /**
+     * Get the current date in HTTP format.
+     */
+    public static final String getCurrentDate() {
+
+        long now = System.currentTimeMillis();
+        if ((now - currentDateGenerated) > 1000) {
+            synchronized (format) {
+                if ((now - currentDateGenerated) > 1000) {
+                    currentDateGenerated = now;
+                    currentDate = format.format(new Date(now));
+                }
+            }
+        }
+        return currentDate;
+
+    }
+
+
+    /**
+     * Get the HTTP format of the specified date.
+     */
+    public static final String formatDate
+        (long value, DateFormat threadLocalformat) {
+
+        Long longValue = new Long(value);
+        String cachedDate = formatCache.get(longValue);
+        if (cachedDate != null)
+            return cachedDate;
+
+        String newDate = null;
+        Date dateValue = new Date(value);
+        if (threadLocalformat != null) {
+            newDate = threadLocalformat.format(dateValue);
+            updateFormatCache(longValue, newDate);
+        } else {
+            synchronized (formatCache) {
+                synchronized (format) {
+                    newDate = format.format(dateValue);
+                }
+                updateFormatCache(longValue, newDate);
+            }
+        }
+        return newDate;
+
+    }
+
+
+    /**
+     * Try to parse the given date as a HTTP date.
+     */
+    public static final long parseDate(String value, 
+                                       DateFormat[] threadLocalformats) {
+
+        Long cachedDate = parseCache.get(value);
+        if (cachedDate != null)
+            return cachedDate.longValue();
+
+        Long date = null;
+        if (threadLocalformats != null) {
+            date = internalParseDate(value, threadLocalformats);
+            updateParseCache(value, date);
+        } else {
+            synchronized (parseCache) {
+                date = internalParseDate(value, formats);
+                updateParseCache(value, date);
+            }
+        }
+        if (date == null) {
+            return (-1L);
+        } else {
+            return date.longValue();
+        }
+
+    }
+
+
+    /**
+     * Parse date with given formatters.
+     */
+    private static final Long internalParseDate
+        (String value, DateFormat[] formats) {
+        Date date = null;
+        for (int i = 0; (date == null) && (i < formats.length); i++) {
+            try {
+                date = formats[i].parse(value);
+            } catch (ParseException e) {
+                ;
+            }
+        }
+        if (date == null) {
+            return null;
+        }
+        return new Long(date.getTime());
+    }
+
+
+    /**
+     * Update cache.
+     */
+    private static void updateFormatCache(Long key, String value) {
+        if (value == null) {
+            return;
+        }
+        if (formatCache.size() > CACHE_SIZE) {
+            formatCache.clear();
+        }
+        formatCache.put(key, value);
+    }
+
+
+    /**
+     * Update cache.
+     */
+    private static void updateParseCache(String key, Long value) {
+        if (value == null) {
+            return;
+        }
+        if (parseCache.size() > CACHE_SIZE) {
+            parseCache.clear();
+        }
+        parseCache.put(key, value);
+    }
+
+
+}
diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/io/FileConnector.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/FileConnector.java
new file mode 100644 (file)
index 0000000..5fb4525
--- /dev/null
@@ -0,0 +1,27 @@
+/*
+ */
+package org.apache.tomcat.lite.io;
+
+
+/**
+ * Initial abstraction for non-blocking File access and to 
+ * support other abstraction. 
+ * 
+ * Tomcat uses JNDI - but that's blocking, does lots of data copy,
+ * is complex.
+ * 
+ * Work in progress..
+ */
+public abstract class FileConnector extends IOConnector {
+
+    public static class FileInfo {
+        String type;
+        int mode;
+        long size;
+        
+    }
+    
+    public abstract boolean isDirectory(String path);
+
+    public abstract boolean isFile(String path);
+}
diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/io/FileConnectorJavaIo.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/FileConnectorJavaIo.java
new file mode 100644 (file)
index 0000000..2b6de3b
--- /dev/null
@@ -0,0 +1,49 @@
+/*
+ */
+package org.apache.tomcat.lite.io;
+
+import java.io.File;
+import java.io.IOException;
+
+
+/**
+ * Catalina uses JNDI to abstract filesystem - this is both heavy and
+ * a bit complex. 
+ * 
+ * This is also a bit complex - but hopefully we can implement it as 
+ * non-blocking and without much copy.
+ * 
+ */
+public class FileConnectorJavaIo extends FileConnector {
+    File base;
+    
+    public FileConnectorJavaIo(File file) {
+        this.base = file;
+    }
+
+    @Override
+    public boolean isDirectory(String path) {
+        File file = new File(base, path);
+        return file.isDirectory();
+    }
+
+    @Override
+    public boolean isFile(String path) {
+        File file = new File(base, path);
+        return file.exists() && !file.isDirectory();
+    }
+
+    @Override
+    public void acceptor(ConnectedCallback sc, 
+            CharSequence port, 
+            Object extra) throws IOException {
+        // TODO: unix domain socket impl.
+        // Maybe: detect new files in the filesystem ? 
+    }
+
+    @Override
+    public void connect(String host, int port, ConnectedCallback sc)
+            throws IOException {
+    }
+
+}
\ No newline at end of file
diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/io/Hex.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/Hex.java
new file mode 100644 (file)
index 0000000..832b813
--- /dev/null
@@ -0,0 +1,249 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package org.apache.tomcat.lite.io;
+
+import java.io.ByteArrayOutputStream;
+
+/**
+ * Tables useful when converting byte arrays to and from strings of hexadecimal
+ * digits.
+ * Code from Ajp11, from Apache's JServ.
+ *
+ * @author Craig R. McClanahan
+ */
+
+public final class Hex {
+
+
+    // -------------------------------------------------------------- Constants
+
+    /**
+     *  Table for HEX to DEC byte translation.
+     */
+    public static final int[] DEC = {
+        -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, -1, -1, -1, -1, -1,
+        00, 01, 02, 03, 04, 05, 06, 07,  8,  9, -1, -1, -1, -1, -1, -1,
+        -1, 10, 11, 12, 13, 14, 15, -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, 10, 11, 12, 13, 14, 15, -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, -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, -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, -1, -1, -1, -1, -1, -1, -1, -1,
+        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+    };
+
+
+    /**
+     * Table for DEC to HEX byte translation.
+     */
+    public static final byte[] HEX = 
+    { (byte) '0', (byte) '1', (byte) '2', (byte) '3', (byte) '4', (byte) '5', 
+      (byte) '6', (byte) '7', (byte) '8', (byte) '9', (byte) 'a', (byte) 'b', 
+      (byte) 'c', (byte) 'd', (byte) 'e', (byte) 'f' };
+
+
+    // --------------------------------------------------------- Static Methods
+
+
+    /**
+     * Convert a String of hexadecimal digits into the corresponding
+     * byte array by encoding each two hexadecimal digits as a byte.
+     *
+     * @param digits Hexadecimal digits representation
+     *
+     * @exception IllegalArgumentException if an invalid hexadecimal digit
+     *  is found, or the input string contains an odd number of hexadecimal
+     *  digits
+     */
+    public static byte[] convert(String digits) {
+
+       ByteArrayOutputStream baos = new ByteArrayOutputStream();
+       for (int i = 0; i < digits.length(); i += 2) {
+           char c1 = digits.charAt(i);
+           if ((i+1) >= digits.length())
+               throw new IllegalArgumentException
+                       ("hexUtil.odd");
+           char c2 = digits.charAt(i + 1);
+           byte b = 0;
+           if ((c1 >= '0') && (c1 <= '9'))
+               b += ((c1 - '0') * 16);
+           else if ((c1 >= 'a') && (c1 <= 'f'))
+               b += ((c1 - 'a' + 10) * 16);
+           else if ((c1 >= 'A') && (c1 <= 'F'))
+               b += ((c1 - 'A' + 10) * 16);
+           else
+               throw new IllegalArgumentException
+                   ("hexUtil.bad");
+           if ((c2 >= '0') && (c2 <= '9'))
+               b += (c2 - '0');
+           else if ((c2 >= 'a') && (c2 <= 'f'))
+               b += (c2 - 'a' + 10);
+           else if ((c2 >= 'A') && (c2 <= 'F'))
+               b += (c2 - 'A' + 10);
+           else
+               throw new IllegalArgumentException
+                   ("hexUtil.bad");
+           baos.write(b);
+       }
+       return (baos.toByteArray());
+
+    }
+
+
+    /**
+     * Convert a byte array into a printable format containing a
+     * String of hexadecimal digit characters (two per byte).
+     *
+     * @param bytes Byte array representation
+     */
+    public static String convert(byte bytes[]) {
+
+       StringBuffer sb = new StringBuffer(bytes.length * 2);
+       for (int i = 0; i < bytes.length; i++) {
+           sb.append(convertDigit((bytes[i] >> 4)));
+           sb.append(convertDigit((bytes[i] & 0x0f)));
+       }
+       return (sb.toString());
+
+    }
+
+    
+    /**
+     * Convert 4 hex digits to an int, and return the number of converted
+     * bytes.
+     *
+     * @param hex Byte array containing exactly four hexadecimal digits
+     *
+     * @exception IllegalArgumentException if an invalid hexadecimal digit
+     *  is included
+     */
+    public static int convert2Int( byte[] hex ) {
+       // Code from Ajp11, from Apache's JServ
+    
+       // assert b.length==4
+       // assert valid data
+       int len;
+       if(hex.length < 4 ) return 0;
+       if( DEC[hex[0]]<0 )
+           throw new IllegalArgumentException("hexUtil.bad");
+       len = DEC[hex[0]];
+       len = len << 4;
+       if( DEC[hex[1]]<0 )
+           throw new IllegalArgumentException("hexUtil.bad");
+       len += DEC[hex[1]];
+       len = len << 4;
+       if( DEC[hex[2]]<0 )
+           throw new IllegalArgumentException("hexUtil.bad");
+       len += DEC[hex[2]];
+       len = len << 4;
+       if( DEC[hex[3]]<0 )
+           throw new IllegalArgumentException("hexUtil.bad");
+       len += DEC[hex[3]];
+       return len;
+    }
+
+
+
+    /**
+     * Provide a mechanism for ensuring this class is loaded. 
+     */
+    public static void load() {
+        // Nothing to do
+    }
+
+    /**
+     * [Private] Convert the specified value (0 .. 15) to the corresponding
+     * hexadecimal digit.
+     *
+     * @param value Value to be converted
+     */
+    private static char convertDigit(int value) {
+
+       value &= 0x0f;
+       if (value >= 10)
+           return ((char) (value - 10 + 'a'));
+       else
+           return ((char) (value + '0'));
+
+    }
+
+    /**
+     * <code>getHexValue</code> displays a formatted hex
+     * representation of the passed byte array.  It also
+     * allows for only a specified offset and length of 
+     * a particular array to be returned.
+     *
+     * @param bytes <code>byte[]</code> array to process.
+     * @param pos offset to begin processing.
+     * @param len number of bytes to process.
+     * @return <code>String</code> formatted hex representation of processed 
+     *         array.
+     */
+    public static String getHexDump(byte[] bytes, int pos, int len,
+                                     boolean displayOffset) {
+        StringBuffer out = new StringBuffer( len * 2 );
+
+        for (int j = 0; j < len; j += 16) {
+            hexLine(out, bytes, pos + j, pos + len, displayOffset);
+        }
+     
+        return out.toString();
+    }
+    
+    private static void hexLine(StringBuffer out, 
+                                byte[] bytes, int start, int end,
+                                boolean displayOffset) {
+
+        if ( displayOffset ) {
+            out.append(convertDigit((int) (start >> 12)));
+            out.append(convertDigit((int) (start >> 8)));
+            out.append(convertDigit((int) (start >> 4)));
+            out.append(convertDigit(start & 0x0F));
+            out.append(": ");
+        }
+        for (int i = start; i < start + 16; i++) {
+
+            if (i < end) {
+                out.append(convertDigit((int) (bytes[i] >> 4)));
+                out.append(convertDigit(bytes[i] & 0x0F));
+                out.append(" ");
+            } else { 
+                out.append("   ");
+            }
+        }
+        
+        out.append(" | ");
+        
+        for (int i = start; i < start + 16 && i < end; i++) {
+            if( ! Character.isISOControl( (char)bytes[i] )) {
+                out.append( new Character((char)bytes[i]) );
+            } else {
+                out.append( "." );
+            }
+        }
+        
+        out.append("\n");
+    }
+}
diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/io/IOBuffer.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/IOBuffer.java
new file mode 100644 (file)
index 0000000..135059c
--- /dev/null
@@ -0,0 +1,666 @@
+/*
+ */
+package org.apache.tomcat.lite.io;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.util.LinkedList;
+import java.util.logging.Logger;
+
+import org.apache.tomcat.lite.http.FutureCallbacks;
+
+// TODO: append() will trigger callbacks - do it explicitely !!!
+// TODO: queue() shouldn't modify the buffer 
+
+
+/** 
+ * A list of data buckets.
+ * 
+ * @author Costin Manolache
+ */
+public class IOBuffer {
+    static Logger log = Logger.getLogger("IOBrigade");
+    
+    static int ALLOC_SIZE = 8192;
+    long defaultTimeout = Long.MAX_VALUE;
+    
+    private LinkedList<BBucket> buffers = new LinkedList<BBucket>();
+    
+    // close() has been called for out,
+    // or EOF/FIN received for in. It may still have data.
+    boolean closeQueued; 
+    
+    // Will be signalled (open) when there is data in the buffer.
+    // also used to sync on.
+    FutureCallbacks<IOBuffer> hasDataLock = new FutureCallbacks<IOBuffer>() {
+        protected boolean isSignaled() {
+            return hasData();
+        }
+    };
+
+    // may be null
+    protected IOChannel ch;
+    
+    // Support for appending - needs improvements.
+    // appendable buffer is part of the buffer list if it has 
+    // data, and kept here if empty.
+    BBuffer appendable;
+    boolean appending = false;
+    ByteBuffer writeBuffer;
+    
+
+    public IOBuffer() {
+    }
+    
+    public IOBuffer(IOChannel ch) {
+        this.ch = ch;
+    }
+    
+    public IOChannel getChannel() {
+        return ch;
+    }
+    
+    // ===== Buffer access =====
+    
+    BBucket first;
+    
+    /**
+     * Return first non-empty buffer.
+     * 
+     * The append buffer is part of the buffer list, and is left alone and
+     * empty.
+     * 
+     * @return
+     */
+    public BBucket peekFirst() {
+        synchronized (buffers) {
+            BBucket o = (buffers.size() == 0) ? null : buffers.getFirst();
+            
+            while (true) {
+                boolean empty = o == null || isEmpty(o);
+                if (o == null) {
+                    //hasDataLock.reset();
+                    return null; // no data in buffers
+                }
+                // o != null
+                if (empty) {
+                    buffers.removeFirst();
+                    o = (buffers.size() == 0) ? null : buffers.getFirst();                    
+                } else {
+                    first = o;
+                    return o;
+                }
+            }
+        }
+    }
+    
+    public BBucket peekBucket(int idx) {
+        synchronized (buffers) {
+            return buffers.get(idx);
+        }
+    }
+    
+
+    public void advance(int len) {
+        first.position(first.position() + len);
+    }
+
+    public void queue(String s) throws IOException {
+        // TODO: decode with prober charset
+        byte[] bytes = s.getBytes("UTF8");
+        queueInternal(BBuffer.wrapper(bytes, 0, bytes.length));
+    }
+    
+    public void queue(BBuffer bc) throws IOException {
+        queueInternal(bc);
+    }
+    
+    public void queue(Object bb) throws IOException {
+        queueInternal(bb);
+    }
+    
+    private void queueInternal(Object bb) throws IOException {
+        if (closeQueued) {
+            throw new IOException("Closed");
+        }
+        synchronized (buffers) {
+            if (appending) {
+                throw new RuntimeException("Unexpected queue while " +
+                                "appending");
+            }
+            BBucket add = wrap(bb);
+            buffers.add(add);
+            //log.info("QUEUED: " + add.remaining() + " " + this);
+            notifyDataAvailable(add);
+        }
+        
+    }
+    
+    public int getBufferCount() {
+        peekFirst();
+        synchronized (buffers) {
+            return buffers.size();
+        }
+    }
+    
+    public void clear() {
+        synchronized (buffers) {
+            buffers.clear();
+        }
+    }
+    
+    public void recycle() {
+        closeQueued = false;
+        clear();
+        // Normally unlocked
+        hasDataLock.recycle();
+
+        appending = false;
+        appendable = null;
+    }
+    
+    // ===================
+    /**
+     * Closed for append. It may still have data.
+     * @return
+     */
+    public boolean isClosedAndEmpty() {
+        return closeQueued && 0 == getBufferCount();
+    }
+
+    
+    /** 
+     * Mark as closed - but will not send data.
+     */
+    public void close() throws IOException {
+        if (closeQueued) {
+            return;
+        }
+        closeQueued = true;
+        notifyDataAvailable(null);
+    }
+    
+    
+    private boolean isEmpty(BBucket o) {
+        if (o instanceof BBucket && 
+                ((BBucket) o).remaining() == 0) {
+            return true;
+        }        
+        return false;
+    }
+    
+    private BBucket wrap(Object src) {
+        if (src instanceof byte[]) {
+            return BBuffer.wrapper((byte[]) src, 0, ((byte[]) src).length);
+        }
+        if (src instanceof ByteBuffer) {
+            //return src;
+            ByteBuffer bb = (ByteBuffer) src;
+            return BBuffer.wrapper(bb.array(), bb.position(), 
+                        bb.remaining());            
+        }
+        if (src instanceof byte[]) {
+            byte[] bb = (byte[]) src;
+            return BBuffer.wrapper(bb, 0, bb.length); 
+        }
+        return (BBucket) src;
+    }
+    
+    protected void notifyDataAvailable(Object bb) throws IOException {
+        synchronized (hasDataLock) {
+            hasDataLock.signal(this); // or bb ?
+        }
+    }
+    
+    public boolean hasData() {
+        return closeQueued || peekFirst() != null; 
+    }
+    
+    public void waitData(long timeMs) throws IOException {
+        if (timeMs == 0) {
+            timeMs = defaultTimeout;
+        }
+        long exp = (timeMs == Long.MAX_VALUE) ?
+                Long.MAX_VALUE : System.currentTimeMillis() + timeMs;
+        synchronized (hasDataLock) {
+            if (hasData()) {
+                return;
+            }
+            hasDataLock.reset();
+        }
+        if (timeMs == 0) {
+            timeMs = Long.MAX_VALUE;
+        }
+        long wait = (timeMs == Long.MAX_VALUE) ? Long.MAX_VALUE : 
+            exp - System.currentTimeMillis();
+
+        hasDataLock.waitSignal(wait);
+        if (exp < System.currentTimeMillis()) {
+            throw new IOException("Timeout");
+        }
+    }
+    
+
+    
+    public boolean isAppendClosed() {
+        return closeQueued;
+    }
+
+    // =================== Helper methods ==================
+
+    /**
+     * Non-blocking.
+     */
+    public int read() throws IOException {
+        BBucket bucket = peekFirst();
+        if (bucket == null) {
+            return -1;
+        }
+        int res = bucket.array()[bucket.position()];
+        bucket.position(bucket.position() + 1);
+        return res;
+    }
+    
+    public int peek() throws IOException {
+        BBucket bucket = peekFirst();
+        if (bucket == null) {
+            return -1;
+        }
+        int res = bucket.array()[bucket.position()];
+        return res;
+    }
+    
+    public int find(char c) {
+        int pos = 0;
+        for (int i = 0; i < buffers.size(); i++) {
+            BBucket bucket = buffers.get(i);
+            if (bucket == null || bucket.remaining() == 0) {
+                continue;
+            }
+            int found= BBuffer.findChar(bucket.array(), bucket.position(),
+                    bucket.limit(), c);
+            if (found >= 0) {
+                return pos + found; 
+            }
+            pos += bucket.remaining();
+        }
+        return -1;
+    }
+    
+    public int readLine(BBuffer bc) throws IOException {
+        return readToDelim(bc, '\n');
+    }
+    
+    /**
+     * Copy up to and including "delim". 
+     * 
+     * @return number of bytes read, or -1 for end of stream.
+     */
+    int readToDelim(BBuffer bc, int delim) throws IOException {
+        int len = 0;
+        for (int idx = 0; idx < buffers.size(); idx++) {
+            BBucket bucket = buffers.get(idx);
+            if (bucket == null || bucket.remaining() == 0) {
+                continue;
+            }
+            byte[] data = bucket.array();
+            int end = bucket.limit();
+            int start = bucket.position();
+            for (int i = start; i < end; i++) {
+                byte chr = data[i];
+                bc.put(chr);
+                if (chr == delim) {
+                    bucket.position(i + 1);
+                    len += (i - start + 1); 
+                    return len;
+                }
+            }
+            bucket.position(end); // empty - should be removed
+        }
+        if (len == 0 && isClosedAndEmpty()) {
+            return -1;
+        }
+        return len;
+    }
+    
+    
+    public int write(ByteBuffer bb) throws IOException {
+        int len = bb.remaining();
+        append(bb);
+        bb.position(bb.position() + len);
+        return len;
+    }
+    
+    public int read(byte[] buf, int off, int len) throws IOException {
+        BBucket bucket = peekFirst();
+        if (isClosedAndEmpty()) {
+            return -1;
+        }
+        if (bucket == null) {
+            return 0;
+        }
+        int toCopy = Math.min(len, bucket.remaining());
+        System.arraycopy(bucket.array(), bucket.position(), buf, 
+                off, toCopy);
+        bucket.position(bucket.position() + toCopy);
+        return toCopy;
+        
+    }
+
+    /**
+     * Non-blocking read.
+     */
+    public int read(ByteBuffer bb) {
+        if (isClosedAndEmpty()) {
+            return -1;
+        }
+        int len = 0;
+        while (true) {
+            int space = bb.remaining(); // to append
+            if (space == 0) {
+                return len;
+            }
+            BBucket first = peekFirst();
+            if (first == null) {
+                return len;
+            }
+            BBucket iob = ((BBucket) first);
+            if (space > iob.remaining()) {
+                space = iob.remaining();
+            }
+            bb.put(iob.array(), iob.position(), space);
+
+            iob.position(iob.position() + space);
+            iob.release();
+            len += space;
+        }        
+    }
+    
+
+    public BBuffer readAll(BBuffer chunk) throws IOException {
+        if (chunk == null) {
+            chunk = allocate();
+        }
+        while (true) {
+            if (isClosedAndEmpty()) {
+                return chunk;
+            }
+            BBucket first = peekFirst();
+            if (first == null) {
+                return chunk;
+            }
+            BBucket iob = ((BBucket) first);
+            chunk.append(iob.array(), iob.position(), iob.remaining());
+            iob.position(iob.position() + iob.remaining());
+            iob.release();
+    
+        }
+    }
+
+    private BBuffer allocate() {
+        int size = 0;
+        for (int i = 0; i < getBufferCount(); i++) { 
+            BBucket first = peekBucket(i);
+            if (first != null) {
+                size += first.remaining();
+            }
+        }
+        return BBuffer.allocate(size);
+    }
+    
+    public BBuffer copyAll(BBuffer chunk) throws IOException {
+        if (chunk == null) {
+            chunk = allocate();
+        }
+        for (int i = 0; i < getBufferCount(); i++) { 
+            BBucket iob = peekBucket(i);
+            chunk.append(iob.array(), iob.position(), iob.remaining());
+        }
+        return chunk;
+    }
+
+    public IOBuffer append(InputStream is) throws IOException {
+        while (true) {
+            ByteBuffer bb = getWriteBuffer();
+            int rd = is.read(bb.array(), bb.position(), bb.remaining());
+            if (rd <= 0) {
+                return this;
+            }
+            bb.position(bb.position() + rd);
+            releaseWriteBuffer(rd);
+        }
+    }
+
+    public IOBuffer append(BBuffer bc) throws IOException {
+        return append(bc.array(), bc.getStart(), bc.getLength());
+    }
+
+    public IOBuffer append(byte[] data) throws IOException {
+        return append(data, 0, data.length);
+    }
+    
+    public IOBuffer append(byte[] data, int start, int len) throws IOException {
+        if (closeQueued) {
+            throw new IOException("Closed");
+        }
+        ByteBuffer bb = getWriteBuffer();
+        
+        int i = start;
+        int end = start + len;
+        while (i < end) {
+            int rem = Math.min(end - i, bb.remaining()); 
+            // to write
+            bb.put(data, i, rem);
+            i += rem; 
+            if (bb.remaining() < 8) {
+                releaseWriteBuffer(1);
+                bb = getWriteBuffer();
+            }
+        }
+
+        releaseWriteBuffer(1);
+        return this;        
+    }
+    
+    public IOBuffer append(int data) throws IOException {
+        if (closeQueued) {
+            throw new IOException("Closed");
+        }
+        ByteBuffer bb = getWriteBuffer();
+        bb.put((byte) data);
+        releaseWriteBuffer(1);
+        return this;        
+    }
+    
+    public IOBuffer append(ByteBuffer cs) throws IOException {
+        return append(cs.array(), cs.position() + cs.arrayOffset(), 
+                cs.remaining());
+    }
+    
+    public IOBuffer append(BBucket cs) throws IOException {
+        append(cs.array(), cs.position(), cs.remaining());
+        return this;
+    }
+    
+    public IOBuffer append(IOBuffer cs) throws IOException {
+        for (int i = 0; i < cs.getBufferCount(); i++) {
+            Object o = cs.peekBucket(i);
+            if (o instanceof BBucket) {
+                append((BBucket)o);
+            } else if (o instanceof ByteBuffer) {
+                append((ByteBuffer) o);
+            } else if (o instanceof CharSequence) {
+                append((CharSequence) o);                
+            } else {
+                throw new IOException("Unknown type " + o);
+            }
+        }
+
+        return this;        
+    }
+    
+    public IOBuffer append(CharSequence cs) throws IOException {
+        byte[] data = cs.toString().getBytes();
+        append(data, 0, data.length);
+        return this;
+    }
+    
+    public IOBuffer append(char c) throws IOException {
+        ByteBuffer bb = getWriteBuffer();
+        bb.put((byte) c);
+        releaseWriteBuffer(1);
+        return this;        
+    }
+    
+    /**
+     * All operations that iterate over buffers must be 
+     * sync
+     * @return
+     */
+    public synchronized int available() {
+        int a = 0;
+        int cnt = buffers.size();
+        for (int i = 0; i < cnt; i++) {
+            a += buffers.get(i).remaining();
+        }
+        return a;
+    }
+    
+    public String toString() {
+        return "IOB:{c:" + getBufferCount() +
+          ", b:" + available() +
+          (isAppendClosed() ? ", C}" : " }");
+    }
+
+    public BBucket popLen(int lenToConsume) {
+        BBucket o = peekFirst(); // skip empty
+        if (o == null) {
+            return null;
+        }
+        BBucket sb = BBuffer.wrapper(o.array(),
+                o.position(), lenToConsume);
+        o.position(o.position() + lenToConsume);
+        return sb;
+    }
+    
+    public BBucket popFirst() {
+        BBucket o = peekFirst(); // skip empty
+        if (o == null) {
+            return null;
+        }
+        if (o == appendable) {
+            synchronized (buffers) {
+                    // TODO: concurrency ???
+                    BBucket sb = 
+                        BBuffer.wrapper(appendable.array(),
+                                appendable.position(), 
+                                appendable.limit() - appendable.position());
+                    appendable.position(appendable.limit());
+                    return sb;
+            }
+        } else {
+            buffers.removeFirst();
+        }
+        return o;
+    }
+    
+
+    public ByteBuffer getWriteBuffer() throws IOException {
+        synchronized (buffers) {
+            if (closeQueued) {
+                throw new IOException("Closed");
+            }
+            synchronized (buffers) {
+                BBucket last = (buffers.size() == 0) ? 
+                        null : buffers.getLast();
+                if (last == null || last != appendable ||
+                        last.array().length - last.limit() < 16) {
+                    last = BBuffer.allocate(ALLOC_SIZE);
+                }
+                appending = true;
+                appendable = (BBuffer) last;            
+            }
+            
+            if (writeBuffer == null || writeBuffer.array() != appendable.array()) {
+                writeBuffer = ByteBuffer.wrap(appendable.array());
+            }
+            writeBuffer.position(appendable.limit());
+            writeBuffer.limit(appendable.array().length);
+            return writeBuffer;
+        }
+    }        
+    
+    public void releaseWriteBuffer(int read) throws IOException {
+        if (!appending) {
+            throw new IOException("Not appending");
+        }
+        synchronized (buffers) {
+            if (writeBuffer != null) {
+                if (read > 0) {
+                    appendable.limit(writeBuffer.position());
+                    // We have some more data.
+                    if (buffers.size() == 0 || 
+                            buffers.getLast() != appendable) {
+                        buffers.add(appendable);
+                    }
+                    notifyDataAvailable(appendable);                
+                }
+            }
+        }
+        appending = false;
+    }
+
+    
+
+    // ------ More utilities - for parsing request ( later )-------
+//  public final int skipBlank(ByteBuffer bb, int start) {
+//  // Skipping blank lines
+//  byte chr = 0;
+//  do {
+//    if (!bb.hasRemaining()) {
+//      return -1;
+//    }
+//    chr = bb.get();
+//  } while ((chr == HttpParser.CR) || (chr == HttpParser.LF));
+//  return bb.position();
+//}
+
+//public final int readToDelimAndLowerCase(ByteBuffer bb,
+//                                         byte delim, 
+//                                         boolean lower) {
+//  boolean space = false;
+//  byte chr = 0;
+//  while (!space) {
+//    if (!bb.hasRemaining()) {
+//      return -1;
+//    }
+//    chr = bb.get();
+//    if (chr == delim) {
+//      space = true;
+//    }
+//    if (lower && (chr >= HttpParser.A) && (chr <= HttpParser.Z)) {
+//      bb.put(bb.position() - 1, 
+//          (byte) (chr - HttpParser.LC_OFFSET));
+//    }
+//  }
+//  return bb.position();
+//}
+
+//public boolean skipSpace(ByteBuffer bb) {
+//  boolean space = true;
+//  while (space) {
+//    if (!bb.hasRemaining()) {
+//      return false;
+//    }
+//    byte chr = bb.get();
+//    if ((chr == HttpParser.SP) || (chr == HttpParser.HT)) {
+//      //
+//    } else {
+//      space = false;
+//      bb.position(bb.position() -1); // move back
+//    }
+//  }
+//  return true;
+//}
+}
diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/io/IOChannel.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/IOChannel.java
new file mode 100644 (file)
index 0000000..23b642b
--- /dev/null
@@ -0,0 +1,290 @@
+/*
+ */
+package org.apache.tomcat.lite.io;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.ByteChannel;
+
+
+
+/**
+ * Buffered ByteChannel, backed by a buffer brigade to allow
+ * some zero-copy operations.
+ * 
+ * - you can use it as a normal non-blocking ByteChannel.
+ * - you can call getRead
+ * 
+ * Very different from MINA IoFilters, also much lower level.
+ * 
+ * 
+ * @author Costin Manolache
+ */
+public abstract class IOChannel implements ByteChannel, IOConnector.DataReceivedCallback, 
+        IOConnector.DataFlushedCallback { //, IOConnector.ClosedCallback {
+    
+    protected IOChannel net;
+    protected IOChannel app;
+    
+    protected String id;
+    protected String target;    
+
+    protected IOConnector connector;
+
+    protected IOConnector.ConnectedCallback connectedCallback;
+    protected IOConnector.DataReceivedCallback dataReceivedCallback;
+    protected IOConnector.DataFlushedCallback dataFlushedCallback;
+
+    // Last activity timestamp.
+    // TODO: update, etc
+    public long ts;
+    
+    public void setConnectedCallback(IOConnector.ConnectedCallback connectedCallback) {
+        this.connectedCallback = connectedCallback;
+    }
+
+    public void setDataReceivedCallback(IOConnector.DataReceivedCallback dataReceivedCallback) {
+        this.dataReceivedCallback = dataReceivedCallback;
+    }
+
+    /**
+     * Callback called when the bottom ( OS ) channel has finished flushing.
+     * 
+     * @param dataFlushedCallback
+     */
+    public void setDataFlushedCallback(IOConnector.DataFlushedCallback dataFlushedCallback) {
+        this.dataFlushedCallback = dataFlushedCallback;
+    }
+
+    protected IOChannel() {
+    }
+    
+    // Input
+    public abstract IOBuffer getIn();
+
+    // Output 
+    public abstract IOBuffer getOut();
+    
+    
+    /** 
+     * From downstream ( NET ). Pass it to the next channel.
+     */
+    public void handleReceived(IOChannel net) throws IOException {
+        sendHandleReceivedCallback();
+    }
+    
+    /** 
+     * Called from lower layer (NET) when the last flush is 
+     * done and all buffers have been sent to OS ( or 
+     * intended recipient ).
+     * 
+     * Will call the callback or next filter, may do additional
+     * processing.
+     * 
+     * @throws IOException
+     */
+    public void handleFlushed(IOChannel net) throws IOException {
+        sendHandleFlushedCallback();
+    }
+    
+    public void sendHandleFlushedCallback() throws IOException {
+        try {
+            if (dataFlushedCallback != null) {
+                dataFlushedCallback.handleFlushed(this);
+            }
+            if (app != null) {
+                app.handleFlushed(this);
+            }
+        } catch (Throwable t) {
+            close();
+            if (t instanceof IOException) {
+                throw (IOException) t;
+            } else {
+                throw new WrappedException("Error in handleFlushed", t);
+            }
+        } 
+    }
+    
+    
+    /**
+     * Notify next channel that data has been received.  
+     */
+    public void sendHandleReceivedCallback() throws IOException {
+        try {
+            if (dataReceivedCallback != null) {
+                dataReceivedCallback.handleReceived(this);
+            }
+            if (app != null) {
+                app.handleReceived(this);
+            }
+        } catch (Throwable t) {
+            t.printStackTrace();
+            try {
+                close();
+            } catch(Throwable t2) {
+                t2.printStackTrace();
+            }
+            if (t instanceof IOException) {
+                throw (IOException) t;
+            } else {
+                throw new WrappedException(t);
+            }
+        } 
+    }
+  
+    public void close() throws IOException {
+        shutdownOutput();
+        // Should it read the buffers ? 
+        
+        if (getIn().isAppendClosed()) {
+            return;
+        } else {
+            getIn().close();
+            sendHandleReceivedCallback();
+        }
+        getIn().hasDataLock.signal(getIn());
+    }
+
+    public boolean isOpen() {
+        return !getIn().isAppendClosed() && !getOut().isAppendClosed();
+    }
+    
+    public void shutdownOutput() throws IOException {
+        if (getOut().isAppendClosed()) {
+            return;
+        } else {
+            getOut().close();
+            startSending();
+        }
+    }
+
+    public void setSink(IOChannel previous) {
+        this.net = previous;
+    }
+
+    public IOChannel getSink() {
+        return net;
+    }
+
+    // Chaining/filtering
+    
+    /** 
+     * Called to add an filter _after_ the current channel.
+     */
+    public IOChannel addFilterAfter(IOChannel next) {
+        this.app = next;
+        app.setSink(this);
+
+        // TODO: do we want to migrate them automatically ?
+        app.setDataReceivedCallback(dataReceivedCallback);
+        app.setDataFlushedCallback(dataFlushedCallback);
+        // app.setClosedCallback(closedCallback);
+
+        dataReceivedCallback = null;
+        dataFlushedCallback = null;
+        return this;
+    }
+
+    public IOChannel getFirst() {
+        IOChannel first = this;
+        while (true) {
+            if (!(first instanceof IOChannel)) {
+                return first;
+            }
+            IOChannel before = ((IOChannel) first).getSink();
+            if (before == null) {
+                return first;
+            } else {
+                first = before;
+            }
+        }
+    }
+    
+    // Socket support
+    
+    public int getPort(boolean remote) {
+        if (net != null) {
+            return net.getPort(remote);
+        }
+        return 80;
+    }
+    
+    public void readInterest(boolean b) throws IOException {
+        if (net != null) {
+            net.readInterest(b);
+        }
+    }
+    
+    // Helpers
+
+    public int read(ByteBuffer bb) throws IOException {
+        return getIn().read(bb);
+    }
+    
+    public int readNonBlocking(ByteBuffer bb) throws IOException {
+        return getIn().read(bb);
+    }
+
+    public int readBlocking(ByteBuffer bb, long timeMs) throws IOException {
+        getIn().waitData(timeMs);
+        return getIn().read(bb);
+    }
+    
+    /** 
+     * Capture all output in a buffer. 
+     */
+    public BBuffer readAll(BBuffer chunk, long to) 
+            throws IOException {
+        if (chunk == null) {
+            chunk = BBuffer.allocate();
+        }
+        while (true) { 
+            getIn().waitData(to);
+            BBucket next = getIn().peekFirst();
+            if (getIn().isClosedAndEmpty() && next == null) {
+                return chunk;
+            }
+            if (next == null) {
+                continue; // false positive
+            }
+            chunk.append(next.array(), next.position(), next.remaining());
+            getIn().advance(next.remaining());
+        }
+    }
+    
+    public int write(ByteBuffer bb) throws IOException {
+        return getOut().write(bb);
+    }
+    
+    public void write(byte[] data) throws IOException {
+        getOut().append(data, 0, data.length);
+    }
+
+    public void write(String string) throws IOException {
+        write(string.getBytes());
+    }
+    
+    /** 
+     * Send data in out to the intended recipient.
+     * This is not blocking.
+     */
+    public abstract void startSending() throws IOException;
+
+    
+    public void setId(String id) {
+        this.id = id;
+    }
+    
+    public String getId() {
+        return id;
+    }
+    
+    public CharSequence getTarget() {
+        if (net != null) {
+            return net.getTarget();
+        }
+        return target;
+    }
+    
+    
+}
diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/io/IOConnector.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/IOConnector.java
new file mode 100644 (file)
index 0000000..1bb33b3
--- /dev/null
@@ -0,0 +1,42 @@
+/*
+ */
+package org.apache.tomcat.lite.io;
+
+import java.io.IOException;
+
+
+/**
+ * Factory for IOChannels, with support for caching.
+ * 
+ * 
+ * @author Costin Manolache
+ */
+public abstract class IOConnector {
+
+    public static interface DataReceivedCallback {
+        /** 
+         * Called when data or EOF has been received.
+         */
+        public void handleReceived(IOChannel ch) throws IOException;
+    }
+
+    public static interface ConnectedCallback {
+        public void handleConnected(IOChannel ch) throws IOException;
+    }
+
+    public static interface DataFlushedCallback {
+        public void handleFlushed(IOChannel ch) throws IOException;
+    }
+    
+    public abstract void acceptor(IOConnector.ConnectedCallback sc, 
+                         CharSequence port, Object extra)
+        throws IOException; 
+
+    // TODO: failures ? 
+    public abstract void connect(String host, int port, 
+                                      IOConnector.ConnectedCallback sc) throws IOException;
+    
+    public void stop() {
+        
+    }
+}
diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/io/IOInputStream.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/IOInputStream.java
new file mode 100644 (file)
index 0000000..3c167f1
--- /dev/null
@@ -0,0 +1,71 @@
+/*
+ */
+package org.apache.tomcat.lite.io;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+
+/**
+ * Similar with ServletInputStream - adds readLine(byte[]..), using
+ * a IOBuffer.
+ * 
+ * 
+ * 
+ * @author Costin Manolache
+ */
+public class IOInputStream extends InputStream {
+
+    IOBuffer bb;
+    long timeout;
+    
+    public IOInputStream(IOChannel httpCh, long to) {
+        bb = httpCh.getIn();
+        this.timeout = to;
+    }
+    
+    @Override
+    public int read() throws IOException {
+        // getReadableBucket/peekFirst returns a buffer with at least
+        // 1 byte in it.
+        if (bb.isClosedAndEmpty()) {
+            return -1;
+        }
+        bb.waitData(timeout);
+        if (bb.isClosedAndEmpty()) {
+            return -1;
+        }
+        
+        return bb.read();
+    }
+    
+    public int read(byte[] buf, int off, int len) throws IOException {
+        if (bb.isClosedAndEmpty()) {
+            return -1;
+        }
+        bb.waitData(timeout);
+        if (bb.isClosedAndEmpty()) {
+            return -1;
+        }
+        return bb.read(buf, off, len);
+    }
+
+    /**
+     *  Servlet-style read line: terminator is \n or \r\n, left in buffer.
+     */
+    public int readLine(byte[] b, int off, int len) throws IOException {
+        if (len <= 0) {
+            return 0;
+        }
+        int count = 0, c;
+
+        while ((c = read()) != -1) {
+            b[off++] = (byte)c;
+            count++;
+            if (c == '\n' || count == len) {
+                break;
+            }
+        }
+        return count > 0 ? count : -1;
+    }
+}
diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/io/IOOutputStream.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/IOOutputStream.java
new file mode 100644 (file)
index 0000000..3331603
--- /dev/null
@@ -0,0 +1,207 @@
+/*
+ */
+package org.apache.tomcat.lite.io;
+
+import java.io.CharConversionException;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.text.MessageFormat;
+
+import org.apache.tomcat.lite.http.HttpMessage;
+import org.apache.tomcat.lite.http.HttpWriter;
+
+/**
+ * Same methods with ServletOutputStream.
+ * 
+ * There is no restriction in using the Writer and InputStream at the 
+ * same time - the servlet layer will impose it for compat. You can also use 
+ * IOBuffer directly. 
+ * 
+ * If you mix stream and writer:
+ *  - call BufferWriter.push() to make sure all chars are sent down
+ *  - the BufferOutputStream doesn't cache any data, all goes to the 
+ *   IOBuffer.
+ *  - flush() on BufferOutputStream and BufferWriter will send the data
+ *  to the network and block until it gets to the socket ( so it can
+ *  throw exception ). 
+ *  - You can also use non-blocking flush methods in IOBuffer, and a
+ *  callback  if you want to know when the write was completed.
+ *    
+ * @author Costin Manolache
+ */
+public class IOOutputStream extends OutputStream {
+
+    IOBuffer bb;
+    HttpMessage message;
+    int bufferSize = HttpWriter.DEFAULT_BUFFER_SIZE;
+    
+    int wSinceFlush = 0;
+    
+    public IOOutputStream(IOBuffer out, HttpMessage httpMessage) {
+        this.bb = out;
+        message = httpMessage;
+    }
+
+    public void recycle() {
+        wSinceFlush = 0;
+        bufferSize = HttpWriter.DEFAULT_BUFFER_SIZE;
+    }
+    
+    public void reset() {
+        wSinceFlush = 0;
+        bb.clear();
+    }
+
+    public int getWrittenSinceFlush() {
+        return wSinceFlush;
+    }
+
+
+    public int getBufferSize() {
+        return bufferSize;
+    }
+
+    public void setBufferSize(int size) {
+        if (size > bufferSize) {
+            bufferSize = size;
+        }
+    }
+
+    private void updateSize(int cnt) throws IOException {
+        wSinceFlush += cnt;
+        if (wSinceFlush > bufferSize) {
+            flush();
+        }
+    }
+    
+    @Override
+    public void write(int b) throws IOException {
+        bb.append((char) b);
+        updateSize(1);
+    }
+    
+    @Override
+    public void write(byte data[]) throws IOException {
+      write(data, 0, data.length);
+    }    
+
+    @Override
+    public void write(byte data[], int start, int len) throws IOException {
+        bb.append(data, start, len);
+        updateSize(len);
+    }    
+    
+    public void flush() throws IOException {
+        if (message.getHttpChannel() != null) {
+            message.getHttpChannel().startSending();
+            
+            message.getHttpChannel().waitFlush(Long.MAX_VALUE);
+        }
+        wSinceFlush = 0;
+    }
+
+    public void close() throws IOException {
+        flush();
+        bb.close();
+    }
+    
+
+    public void write(ByteBuffer source) throws IOException {
+        write(source.array(), source.position(), source.remaining());
+        source.position(source.limit());
+    }
+    
+    public void print(String s) throws IOException {
+        if (s==null) s="null";
+        int len = s.length();
+        for (int i = 0; i < len; i++) {
+            char c = s.charAt (i);
+
+            //
+            // XXX NOTE:  This is clearly incorrect for many strings,
+            // but is the only consistent approach within the current
+            // servlet framework.  It must suffice until servlet output
+            // streams properly encode their output.
+            //
+            if ((c & 0xff00) != 0) {    // high order byte must be zero
+                String errMsg = "Not ISO-8859-1";
+                Object[] errArgs = new Object[1];
+                errArgs[0] = new Character(c);
+                errMsg = MessageFormat.format(errMsg, errArgs);
+                throw new CharConversionException(errMsg);
+            }
+            write (c);
+        }
+    }
+
+
+    public void print(boolean b) throws IOException {
+        String msg;
+        if (b) {
+            msg = "true";
+        } else {
+            msg = "false";
+        }
+        print(msg);
+    }
+
+    public void print(char c) throws IOException {
+        print(String.valueOf(c));
+    }
+
+    public void print(int i) throws IOException {
+        print(String.valueOf(i));
+    }
+
+    public void print(long l) throws IOException {
+        print(String.valueOf(l));
+    }
+
+    public void print(float f) throws IOException {
+        print(String.valueOf(f));
+    }
+
+    public void print(double d) throws IOException {
+        print(String.valueOf(d));
+    }
+
+    public void println() throws IOException {
+        print("\r\n");
+    }
+
+    public void println(String s) throws IOException {
+        print(s);
+        println();
+    }
+
+    public void println(boolean b) throws IOException {
+        print(b);
+        println();
+    }
+
+    public void println(char c) throws IOException {
+        print(c);
+        println();
+    }
+
+    public void println(int i) throws IOException {
+        print(i);
+        println();
+    }
+
+    public void println(long l) throws IOException {
+        print(l);
+        println();
+    }
+
+    public void println(float f) throws IOException {
+        print(f);
+        println();
+    }
+
+    public void println(double d) throws IOException {
+        print(d);
+        println();
+    }
+}
diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/io/IOReader.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/IOReader.java
new file mode 100644 (file)
index 0000000..2095dce
--- /dev/null
@@ -0,0 +1,231 @@
+/*
+ */
+package org.apache.tomcat.lite.io;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetDecoder;
+import java.nio.charset.CoderResult;
+import java.nio.charset.CodingErrorAction;
+import java.nio.charset.MalformedInputException;
+import java.nio.charset.UnmappableCharacterException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+
+/**
+ * Conversion from Bytes to Chars and support for decoding.
+ * 
+ * Replaces tomcat B2CConverter with NIO equivalent. B2CConverter was a hack 
+ * (re)using an dummy InputStream backed by a ByteChunk. 
+ * 
+ * @author Costin Manolache
+ */
+public class IOReader extends Reader {
+    
+    IOBuffer iob;
+    Map<String, CharsetDecoder> decoders = new HashMap<String, CharsetDecoder>();
+    CharsetDecoder decoder;
+    
+    private static boolean REUSE = true;
+    String enc;
+    private boolean closed;
+    public static final String DEFAULT_ENCODING = "ISO-8859-1";
+    
+    public IOReader(IOBuffer iob) {
+        this.iob = iob;
+    }
+    
+    public void setEncoding(String charset) {
+        enc = charset;
+        if (enc == null) {
+            enc = DEFAULT_ENCODING;
+        }
+        decoder = REUSE ? decoders.get(enc) : null;
+        if (decoder == null) {
+            decoder = Charset.forName(enc).newDecoder()
+                .onMalformedInput(CodingErrorAction.REPLACE)
+                .onUnmappableCharacter(CodingErrorAction.REPLACE);
+            if (REUSE) {
+                decoders.put(enc, decoder);
+            }
+        }
+    }
+    
+    public String getEncoding() {
+        return enc;
+    }
+    
+    public void recycle() {
+        if (decoder != null) {
+            decoder.reset();
+        }
+        closed = false;
+        enc = null;
+    }
+    
+    private void checkClosed() throws IOException {
+        if (closed) throw new IOException("closed");
+    }
+    
+    public boolean ready() {
+        return iob.peekFirst() != null;
+    }
+
+    public int read(java.nio.CharBuffer target) throws IOException {
+        int len = target.remaining();
+        char[] cbuf = new char[len];
+        int n = read(cbuf, 0, len);
+        if (n > 0)
+            target.put(cbuf, 0, n);
+        return n;
+    }
+
+    public int read() throws IOException {
+        char cb[] = new char[1];
+        if (read(cb, 0, 1) == -1)
+            return -1;
+        else
+            return cb[0];
+    }
+    
+    @Override
+    public void close() throws IOException {
+        closed = true;
+        iob.close();
+    }
+    
+    /** 
+     * Used if a bucket ends on a char boundary
+     */
+    BBuffer underFlowBuffer = BBuffer.allocate(10);
+    public static AtomicInteger underFlows = new AtomicInteger();
+
+    /**
+     * Decode all bytes - for example a URL or header.
+     */
+    public void decodeAll(BBucket bb, CBuffer c) {
+        
+        while (bb.hasRemaining()) {
+            CharBuffer charBuffer = c.getAppendCharBuffer();
+            CoderResult res = decode1(bb, charBuffer, true);
+            c.returnAppendCharBuffer(charBuffer);
+            if (res != CoderResult.OVERFLOW) {
+                if (res == CoderResult.UNDERFLOW || bb.hasRemaining()) {
+                    System.err.println("Ignored trailing bytes " + bb.remaining());
+                } 
+                return;
+            }
+        }
+        
+    }
+    
+    /** 
+     * Do one decode pass.  
+     */
+    public CoderResult decode1(BBucket bb, CharBuffer c, boolean eof) {
+        ByteBuffer b = bb.getByteBuffer();
+
+        if (underFlowBuffer.hasRemaining()) {
+            // Need to get rid of the underFlow first
+            for (int i = 0; i < 10; i++) {
+                underFlowBuffer.put(b.get());
+                bb.position(b.position());
+                ByteBuffer ub = underFlowBuffer.getByteBuffer();
+                CoderResult res = decoder.decode(ub, c, eof);
+                if (! ub.hasRemaining()) {
+                    // underflow resolved
+                    break;
+                }
+                if (res == CoderResult.OVERFLOW) {
+                    return res;
+                }
+            }
+            if (underFlowBuffer.hasRemaining()) {
+                throw new RuntimeException("Can't resolve underflow after " +
+                               "10 bytes");
+            }
+        }
+        
+        CoderResult res = decoder.decode(b, c, eof);
+        bb.position(b.position());
+        
+        if (res == CoderResult.UNDERFLOW && bb.hasRemaining()) {
+            // b ends on a boundary
+            underFlowBuffer.append(bb.array(), bb.position(), bb.remaining());
+            bb.position(bb.limit());
+        } 
+        return res;
+    }
+    
+    @Override
+    public int read(char[] cbuf, int offset, int length) throws IOException {
+        checkClosed();
+        if (length == 0) {
+            return 0;
+        }
+        // we can either allocate a new CharBuffer or use a 
+        // static one and copy. Seems simpler this way - needs some
+        // load test, but InputStreamReader seems to do the same.
+        CharBuffer out = CharBuffer.wrap(cbuf, offset, length);
+        
+        CoderResult result = CoderResult.UNDERFLOW;
+
+        BBucket bucket = iob.peekFirst();
+        
+        // Consume as much as possible without blocking
+        while (result == CoderResult.UNDERFLOW) {
+            // fill the buffer if needed
+            if (bucket == null || ! bucket.hasRemaining()) {
+                if (out.position() > offset) {
+                    // we could return the result without blocking read
+                    break;
+                }
+                bucket = null;
+                while (bucket == null) {
+                    iob.waitData(0);
+                    bucket = iob.peekFirst();
+                    if (bucket == null && iob.isClosedAndEmpty()) {
+                        // EOF, we couldn't decode anything
+                        break;
+                    }
+                }
+                
+                if (bucket == null) {
+                    // eof
+                    break;
+                }
+            }
+
+            result = decode1(bucket, out, false);
+        }
+
+        if (result == CoderResult.UNDERFLOW && iob.isClosedAndEmpty()) {
+            // Flush out any remaining data
+            ByteBuffer bytes = bucket == null ? 
+                    underFlowBuffer.getByteBuffer() : bucket.getByteBuffer();
+            result = decoder.decode(bytes, out, true);
+            if (bucket == null) {
+                underFlowBuffer.position(bytes.position());
+            } else {
+                bucket.position(bytes.position());
+            }
+
+            decoder.flush(out);
+            decoder.reset();
+        }
+
+        if (result.isMalformed()) {
+            throw new MalformedInputException(result.length());
+        } else if (result.isUnmappable()) {
+            throw new UnmappableCharacterException(result.length());
+        }
+
+        int rd = out.position() - offset;
+        return rd == 0 ? -1 : rd;
+    }
+}
diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/io/IOWriter.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/IOWriter.java
new file mode 100644 (file)
index 0000000..ad8f952
--- /dev/null
@@ -0,0 +1,180 @@
+/*
+ */
+package org.apache.tomcat.lite.io;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetEncoder;
+import java.nio.charset.CoderResult;
+import java.nio.charset.CodingErrorAction;
+import java.util.BitSet;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Converts chars to bytes, and associated encoding.
+ * 
+ * Replaces C2B from old tomcat. 
+ * 
+ * @author Costin Manolache
+ */
+public class IOWriter extends Writer {
+    
+    IOBuffer iob;
+    Map<String, CharsetEncoder> encoders = new HashMap<String, CharsetEncoder>();
+    CharsetEncoder encoder;
+    
+    private static boolean REUSE = true;
+    String enc;
+    private boolean closed;
+    IOChannel ioCh;
+    
+    public IOWriter(IOChannel iob) {
+        this.ioCh = iob;
+        if (iob != null) {
+            this.iob = iob.getOut();
+        }
+    }
+    
+    public void setEncoding(String charset) {
+        if (charset == null) {
+            charset = "UTF-8"; 
+        }
+        enc = charset;
+        encoder = getEncoder(charset);
+        if (encoder == null) {
+            encoder = Charset.forName(charset).newEncoder()
+                .onMalformedInput(CodingErrorAction.REPLACE)
+                .onUnmappableCharacter(CodingErrorAction.REPLACE);
+            if (REUSE) {
+                encoders.put(charset, encoder);
+            }
+        }
+    }
+    
+    CharsetEncoder getEncoder(String charset) {
+        if (charset == null) {
+            charset = "UTF-8"; 
+        }
+        encoder = REUSE ? encoders.get(charset) : null;
+        if (encoder == null) {
+            encoder = Charset.forName(charset).newEncoder()
+                .onMalformedInput(CodingErrorAction.REPLACE)
+                .onUnmappableCharacter(CodingErrorAction.REPLACE);
+            if (REUSE) {
+                encoders.put(charset, encoder);
+            }
+        }
+        return encoder;
+    }
+    
+    public String getEncoding() {
+        return enc;
+    }
+    
+    public void recycle() {
+        if (encoder != null) {
+            encoder.reset();
+        }
+        closed = false;
+        enc = null;
+    }
+    
+    
+    private void checkClosed() throws IOException {
+        if (closed) throw new IOException("closed");
+    }
+    
+    @Override
+    public void close() throws IOException {
+        closed = true;
+        // flush the buffer ?
+        ByteBuffer out = iob.getWriteBuffer();
+        encoder.flush(out);
+        iob.releaseWriteBuffer(1);
+        
+        iob.close();
+    }
+    
+    /** 
+     * Used if a bucket ends on a char boundary
+     */
+    CBuffer underFlowBuffer = CBuffer.newInstance();
+
+    public void encode1(CBuffer cc, 
+            BBuffer bb, CharsetEncoder encoder, boolean eof) {
+        CharBuffer c = cc.getNioBuffer();
+        ByteBuffer b = bb.getWriteByteBuffer(c.remaining() * 2);
+        encode1(c, b, encoder, eof);
+        cc.returnNioBuffer(c);
+        bb.limit(b.position());
+    }
+
+    /** 
+     * 
+     * @param cc
+     * @return
+     */
+    public void encode1(CharBuffer c, 
+            ByteBuffer b, CharsetEncoder encoder, boolean eof) {
+        
+        // TODO: buffer growth in caller
+        
+        CoderResult res = encoder.encode(c, b, eof);
+        if (res == CoderResult.OVERFLOW) {
+            // bb is full - next call will get a larger buffer ( it 
+            // grows ) or maybe will be flushed.
+        }
+        if (res == CoderResult.UNDERFLOW && c.remaining() > 0 && !eof) {
+            // TODO: if eof -> exception ?
+            // cc has remaining chars - for example a surrogate start.
+            underFlowBuffer.put(c);
+        } 
+        
+    }
+
+    public void encodeAll(CBuffer cc, 
+            BBuffer bb, CharsetEncoder encoder, boolean eof) {
+        while (cc.length() > 0) {
+            encode1(cc, bb, encoder, eof);
+        }
+    }    
+
+    public void encodeAll(CBuffer cc, 
+            BBuffer bb, String cs) {
+        encodeAll(cc, bb, getEncoder(cs), true);
+    }    
+    
+    @Override
+    public void flush() throws IOException {
+        if (ioCh != null) {
+            ioCh.startSending();
+        }
+    }
+
+    /**
+     * Just send the chars to the byte[], without flushing down.
+     * 
+     * @throws IOException
+     */
+    public void push() throws IOException {
+        // we don't cache here.
+    }
+    
+    @Override
+    public void write(char[] cbuf, int off, int len) throws IOException {
+        checkClosed();
+        CharBuffer cb = CharBuffer.wrap(cbuf, off, len);
+        
+        while (cb.remaining() > 0) {
+            ByteBuffer wb = iob.getWriteBuffer();
+            encode1(cb, wb, encoder, false);
+            iob.releaseWriteBuffer(1);
+        }
+    }
+    
+    
+}
diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/io/MemoryIOConnector.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/MemoryIOConnector.java
new file mode 100644 (file)
index 0000000..0b9f56b
--- /dev/null
@@ -0,0 +1,83 @@
+/*
+ */
+package org.apache.tomcat.lite.io;
+
+import java.io.IOException;
+
+public class MemoryIOConnector extends IOConnector {
+
+    public static class MemoryIOChannel extends IOChannel {
+        IOBuffer netIn = new IOBuffer(this) {
+            protected void notifyDataAvailable(Object bb) throws IOException {
+                sendHandleReceivedCallback();
+                super.notifyDataAvailable(bb);
+            }
+        };
+        IOBuffer netOut = new IOBuffer(this);
+        
+        /**
+         * All flushed output will be saved to 'out'.
+         */
+        public BBuffer out = BBuffer.allocate(4096);
+        
+        public MemoryIOChannel() {
+        }
+        
+        public void startSending() throws IOException {
+            //
+            IOBuffer bb = netOut;
+            while (true) {
+                if (bb.isClosedAndEmpty()) {
+                    break;
+                }
+                BBucket first = bb.peekFirst();
+                if (first == null) {
+                    break;
+                }
+                BBucket iob = ((BBucket) first);
+                out.append(iob.array(), iob.position(), iob.remaining());
+                bb.advance(iob.remaining());
+                iob.release();
+            }
+
+            handleFlushed(this);
+        }
+        
+        @Override
+        public IOBuffer getIn() {
+            return netIn;
+        }
+        @Override
+        public IOBuffer getOut() {
+            return netOut;
+        }
+    } 
+    // TODO: in-process communication without sockets for testing
+    ConnectedCallback acceptor;
+    MemoryIOConnector server;
+    
+    public MemoryIOConnector withServer(MemoryIOConnector server) {
+        this.server = server;
+        return server;
+    }
+    
+    @Override
+    public void acceptor(ConnectedCallback sc, CharSequence port, Object extra) 
+            throws IOException {
+        this.acceptor = sc;
+    }
+
+    @Override
+    public void connect(String host, int port, ConnectedCallback sc)
+            throws IOException {
+        IOChannel ch = new MemoryIOChannel();
+        IOChannel sch = new MemoryIOChannel();
+        // TODO: mix
+        if (server != null && server.acceptor != null) {
+            server.acceptor.handleConnected(sch);
+        }
+        sc.handleConnected(ch);
+    }
+    
+}
\ No newline at end of file
diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/io/NioChannel.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/NioChannel.java
new file mode 100644 (file)
index 0000000..c136b8b
--- /dev/null
@@ -0,0 +1,200 @@
+package org.apache.tomcat.lite.io;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+import java.nio.channels.ByteChannel;
+import java.nio.channels.Channel;
+import java.nio.channels.SelectionKey;
+
+
+/** 
+ * Wrapper around the real channel, with selector-specific info.
+ * 
+ * It is stored as an attachment in the selector.
+ */
+public class NioChannel implements ByteChannel {
+    
+    public static interface NioChannelCallback {
+        public void handleConnected(NioChannel ch) throws IOException;
+        public void handleClosed(NioChannel ch) throws IOException;
+        public void handleReadable(NioChannel ch) throws IOException;
+        public void handleWriteable(NioChannel ch) throws IOException;
+        
+    }
+    
+    NioChannel(NioThread sel) {
+        this.sel = sel;
+    }
+
+    // APR long is wrapped in a ByteChannel as well - with few other longs.
+    Channel channel;
+    
+    // sync access.
+    Object selKey;
+
+    NioThread sel;
+    
+    /** 
+     * If != 0 - the callback will be notified closely after this time.
+     * Used for timeouts. 
+     */
+    long nextTimeEvent = 0;
+    
+    // Callbacks
+    Runnable timeEvent;
+    
+    NioChannelCallback callback;
+
+    
+    Throwable lastException;
+
+    // True if the callback wants to be notified of read/write
+    boolean writeInterest;
+    boolean readInterest;
+
+    // shutdownOutput has been called ?
+    private boolean outClosed = false;
+    
+    // read() returned -1 OR input buffer closed ( no longer interested )
+    boolean inClosed = false;
+    
+    // Saved to allow debug messages for bad interest/looping
+    int lastReadResult;
+    int zeroReads = 0;
+    int lastWriteResult;
+    
+    protected NioChannel() {
+        
+    }
+    
+    public NioThread getSelectorThread() {
+        return sel;
+    }
+    
+    public String toString() {
+        StringBuffer sb = new StringBuffer();
+        sb.append("SelData/")
+        .append(writeInterest ? "W/" : "")
+        .append(readInterest ? "R/" : "")
+        .append(outClosed ? "Out-CLOSE/" : "")
+        .append(inClosed ? "In-CLOSE/" : "")
+        .append(selKey == null ? -1 : ((SelectionKey) (selKey)).interestOps())
+        .append("/")
+        .append(channel.toString());
+        
+        return sb.toString();
+    }
+    
+    public Channel getChannel() {
+        return channel;
+    }
+    
+    public boolean isOpen() {
+        // in and out open
+        return channel.isOpen() && !outClosed && !inClosed;
+    }
+    
+    public int read(ByteBuffer bb) throws IOException {
+        return sel.readNonBlocking(this, bb);
+    }
+
+    public int write(ByteBuffer bb) throws IOException {
+        return sel.writeNonBlocking(this, bb);
+    }
+    
+    public void readInterest(boolean b) throws IOException {
+        sel.readInterest(this, b);
+    }
+
+    public void writeInterest() throws IOException {
+        sel.writeInterest(this);
+    }
+    public InetAddress getAddress(boolean remote) {
+        return sel.getAddress(this, remote);
+    }
+
+    public int getPort(boolean remote) {
+        return sel.getPort(this, remote);
+    }
+    
+    /**
+     * Run in selector thread.
+     */
+    public void runInSelectorThread(Runnable t) throws IOException {
+        sel.runInSelectorThread(t);
+    }
+
+    /**
+     * Request a timer event. The thread will generate the events at 
+     * a configurable interval - for example no more often than 0.5 sec.
+     */
+    public void setTimer(long timeMs, Runnable cb) {
+        this.nextTimeEvent = timeMs;
+        this.timeEvent = cb;
+    }
+    
+    /**
+     *  shutdown out + in
+     *  If there is still data in the input buffer - RST will be sent
+     *  instead of FIN.
+     *  
+     * 
+     * The proper way to close a connection is to shutdownOutput() first, 
+     * wait until read() return -1, then call close().
+     * 
+     * If read() returns -1, you need to finish sending, call shutdownOutput()
+     * than close. 
+     * If read() returns -1 and there is an error - call close() 
+     * directly. 
+     * 
+     */
+    @Override
+    public void close() throws IOException {
+        shutdownOutput();
+        sel.close(this, null);
+    }
+
+    /**
+     *  Send TCP close(FIN). HTTP uses this to transmit end of body. The other end
+     *  detects this with a '-1' in read().
+     *  
+     *  All other forms of close() are reported as exceptions in read().
+     * 
+     * @throws IOException
+     */
+    public void shutdownOutput() throws IOException {
+        synchronized (channel) {
+            if (!outClosed) {
+                outClosed = true;
+                try {
+                    sel.shutdownOutput(this);
+                } catch (IOException ex) {
+                    // ignore
+                }
+            } 
+            if (inClosed) {
+                sel.close(this, null);
+            }
+        }
+    }
+    
+    void inputClosed() throws IOException {
+        synchronized (channel) {
+            if (inClosed) {
+                // already closed
+                return;
+            }
+            inClosed = true; // detected end
+            if (outClosed) {
+                sel.close(this, null);
+            } else {
+                // Don't close the channel - write may still work ?
+                readInterest(false);
+            }
+        }
+    }
+    
+    boolean closeCalled = false;
+}
\ No newline at end of file
diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/io/NioThread.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/NioThread.java
new file mode 100644 (file)
index 0000000..95e34e7
--- /dev/null
@@ -0,0 +1,1147 @@
+/*  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.apache.tomcat.lite.io;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.SocketAddress;
+import java.net.SocketException;
+import java.nio.ByteBuffer;
+import java.nio.channels.ByteChannel;
+import java.nio.channels.Channel;
+import java.nio.channels.ClosedChannelException;
+import java.nio.channels.SelectableChannel;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.Selector;
+import java.nio.channels.ServerSocketChannel;
+import java.nio.channels.SocketChannel;
+import java.nio.channels.spi.SelectorProvider;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import org.apache.tomcat.lite.io.NioChannel.NioChannelCallback;
+
+/**
+ * Abstract NIO/APR to avoid some of the complexity and allow more code
+ * sharing and experiments. 
+ *
+ * SelectorThread provides non-blocking methods for read/write and generates 
+ * callbacks using SelectorCallback. It has no buffers of its own. 
+ *
+ * Additional requirements: 
+ *  - support timers ( setTimer() in SelectorChannel )
+ * 
+ * @author Costin Manolache
+ */
+public class NioThread implements Runnable {
+
+  // ----------- IO handling -----------
+  protected long inactivityTimeout = 5000;
+  protected Thread selectorThread;
+
+
+  static Logger log = Logger.getLogger("NIO");
+
+  Selector selector;
+
+  // will be processed in the selector thread
+  List<NioChannel> readInterest = new ArrayList<NioChannel>();
+  List<NioChannel> writeInterest = new ArrayList<NioChannel>();
+  List<NioChannel> connectAcceptInterest = new ArrayList<NioChannel>();
+  List<NioChannel> updateCallback = new ArrayList<NioChannel>();
+  List<NioChannel> closeInterest = new LinkedList<NioChannel>();
+  List<Runnable> runnableInterest = new ArrayList<Runnable>();
+
+  // Statistics
+  AtomicInteger opened = new AtomicInteger();
+  AtomicInteger closed = new AtomicInteger();
+  AtomicInteger loops = new AtomicInteger();
+  
+  AtomicInteger callbackCount = new AtomicInteger();
+  AtomicLong callbackTotalTime = new AtomicLong();
+  long maxCallbackTime = 0;
+
+  // actives are also stored in the Selector. This is only updated in the main 
+  // thread
+  public ArrayList<NioChannel> active = new ArrayList<NioChannel>();
+
+  public static boolean debug = false;
+  boolean debugWakeup = false;
+  boolean running = true;
+
+  long lastWakeup = System.currentTimeMillis(); // last time we woke
+  long nextWakeup; // next scheduled wakeup
+
+  // Normally select will wait for the next time event - if it's 
+  // too far in future, maxSleep will override it.
+  private long maxSleep = 600000;
+  long sleepTime = maxSleep;
+
+  // Never sleep less than minSleep. This defines the resulution for 
+  // time events.
+  private long minSleep = 100;
+
+  boolean daemon = true;
+  
+  // TODO: trace log - record all events with timestamps, replay
+
+  public NioThread(String name, boolean daemon) {
+      try {
+          selectorThread = (name == null) ? new Thread(this) :
+              new Thread(this, name);
+          
+          selector = Selector.open();
+          // TODO: start it on-demand, close it when not in use
+          selectorThread.setDaemon(daemon);
+          this.daemon = daemon;
+          
+          selectorThread.start();
+          
+      } catch(IOException e) {
+          throw new RuntimeException(e);
+      }        
+  }
+  
+  /**
+   * Opened sockets, waiting for something ( close at least ) 
+   */
+  public int getOpen() {
+      return opened.get();
+  }
+
+  /**
+   * Closed - we're done with them. 
+   */
+  public int getClosed() {
+      return closed.get();
+  }
+
+  public int getActive() {
+      return active.size();
+  }
+  
+  public int getCallbacks() {
+      return callbackCount.get();
+  }
+  
+  public long getMaxCallbackTime() {
+      return maxCallbackTime;
+  }
+
+  public long getAvgCallbackTime() {
+      int cnt = callbackCount.get();
+      if (cnt == 0) {
+          return 0;
+      }
+      return callbackTotalTime.get() / cnt;
+  }
+  
+  /** 
+   * How many times we looped
+   */
+  public int getLoops() {
+      return loops.get();
+  }
+
+  public long getLastWakeup() {
+      return lastWakeup;
+  }
+
+  public long getTimeSinceLastWakeup() {
+      return System.currentTimeMillis() - lastWakeup;
+  }
+
+  /** 
+   * Close all resources, stop accepting, stop the thread.
+   * The actual stop will happen in background.
+   */
+  public void stop() {
+      running = false;
+      if (debug) {
+          log.info("Selector thread stop " + this);
+      }
+      selector.wakeup();
+  }
+
+  public void run() {
+      int sloops = 0;
+      if (debug) {
+          log.info("Start NIO thread, daemon=" + daemon);
+      }
+      while (running) {
+          // if we want timeouts - set here.
+          try {
+              loops.incrementAndGet();
+              
+              // Check if new requests were added
+              processPending();
+
+              // Timers
+              long now = System.currentTimeMillis();
+              if (nextWakeup < now) {
+                  // We don't want to iterate on every I/O
+                  updateSleepTimeAndProcessTimeouts(now);
+              }
+              
+              int selected = selector.select(sleepTime);
+              
+              lastWakeup = System.currentTimeMillis();
+              long slept = lastWakeup - now;
+
+              if (debugWakeup && selected == 0) {
+                  if (sleepTime < maxSleep - 1000) { // short wakeup
+                      log.info("Wakeup " + selected + " " + slept
+                              + " " + sleepTime);
+                  }
+              }
+              if (slept < 10 && selected == 0) {
+                  if (sloops > 50) {
+                      sloops = 0;
+                      log.severe("Looping !");
+                      resetSelector();
+                  }
+                  sloops++;
+              }
+              
+              // handle events for existing req first.
+              if (selected != 0) {
+                  sloops = 0;
+                  Set<SelectionKey> sel = selector.selectedKeys();
+                  Iterator<SelectionKey> i = sel.iterator();
+
+                  while (i.hasNext()) {
+                      SelectionKey sk = i.next();
+                      i.remove();
+                      
+                      boolean valid = sk.isValid();
+                      int readyOps = (valid) ? sk.readyOps() : 0;
+                      
+                      NioChannel ch = (NioChannel) sk.attachment();
+                      if (debugWakeup) {
+                          log.info("Wakeup selCnt=" + selected + " slept=" + (lastWakeup - now) + 
+                                  " ready: " + readyOps + " v=" + 
+                                  sk.isValid() + " ch=" + ch);
+                      }
+                      if (ch == null) {
+                          log.severe("Missing channel");
+                          sk.cancel();
+                          continue;
+                      }
+                      if (ch.selKey != sk) {
+                          // if (ch.selKey != null) { // null if closed
+                          log.severe("Invalid state, selKey doesn't match ");
+                          ch.selKey = sk;
+                      }
+                      if (ch.channel != sk.channel()) {
+                          ch.channel = sk.channel();
+                          log.severe("Invalid state, channel doesn't match ");
+                      }
+
+                      if (!sk.isValid()) {
+                          if (debug) {
+                              log.info("!isValid, closed socket " + ch);
+                          }
+                          ch.close();
+                          continue;
+                      }
+
+                      try {
+                          int ready = sk.readyOps();
+                          // callbacks
+                          if (sk.isValid() && sk.isAcceptable()) {
+                              handleAccept(ch, sk);
+                          }
+
+                          if (sk.isValid() && sk.isConnectable()) {
+                              sk.interestOps(sk.interestOps() & ~SelectionKey.OP_CONNECT);
+                              SocketChannel sc = (SocketChannel) sk.channel();
+                              handleConnect(ch, sc);
+                          }
+
+                          if (sk.isValid() && sk.isWritable()) {
+                              // Needs to be explicitely re-enabled by callback
+                              // if more data.
+                              sk.interestOps(sk.interestOps() & ~SelectionKey.OP_WRITE);
+                              ch.writeInterest = false;
+                              handleDataWriteable(ch);
+                          }
+
+                          if (sk.isValid() && sk.isReadable()) {
+                              // Leave readable interest !
+                              handleReadable(ch);
+                          }
+                          
+                          long callbackTime = 
+                              System.currentTimeMillis() - lastWakeup;
+                          
+                          if (callbackTime > 250) { 
+                              log.warning("Callback too long ! ops=" + ready + 
+                                      " time=" + callbackTime + " ch=" + ch);
+                          }
+                          if (callbackTime > maxCallbackTime) {
+                              maxCallbackTime = callbackTime;
+                          }
+                          callbackCount.incrementAndGet();
+                          this.callbackTotalTime.addAndGet(callbackTime);
+                          
+                      } catch (Throwable t) {
+                          log.log(Level.SEVERE, "SelectorThread: Channel error, closing", t);
+                          ch.lastException = t;
+                          ch.close();
+                      }
+
+                  }
+                  // All at once
+                  sel.clear();
+              }
+
+          } catch (Throwable e) {
+              log.log(Level.SEVERE, "SelectorThread: Error in select", e);
+          }
+      } // while(running)
+      log.info("SelectorThread done");
+  }
+
+  private void log(String msg, int selected, long slept, SelectionKey sk, int readyOps) {
+      log.info(msg + " " + selected 
+              + " " + slept    
+              + " ready: " + readyOps + " " 
+              + sk.readyOps() + " " + sk);
+  }
+
+  private void resetSelector() throws IOException, ClosedChannelException {
+      // Let's close all sockets - one is bad, but we can't do much.
+      Set<SelectionKey> keys = selector.keys();
+      //Set<SelectionKey> keys = selector.keys();
+      ArrayList<NioChannel> oldCh = new ArrayList<NioChannel>();
+      ArrayList<Integer> interests = new ArrayList<Integer>();
+      for (SelectionKey k : keys) {
+          NioChannel cd = (NioChannel) k.attachment();
+          interests.add(k.interestOps());
+          oldCh.add(cd);
+          k.cancel();
+      }
+      
+      selector.close();
+      selector = Selector.open();
+      for (int i = 0; i < oldCh.size(); i++) {
+          NioChannel selectorData = oldCh.get(i);
+          int interest = interests.get(i);
+          if (selectorData.channel instanceof ServerSocketChannel) {
+              ServerSocketChannel socketChannel = 
+                  (ServerSocketChannel) selectorData.channel;
+              selectorData.selKey = socketChannel.register(selector, SelectionKey.OP_ACCEPT);
+          } else {
+              SocketChannel socketChannel =
+                  (SocketChannel) selectorData.channel;
+              if (interest != 0) {
+                  selectorData.selKey = socketChannel.register(selector, 
+                      interest);
+              }
+              
+          }
+      }
+  }
+
+  private void handleReadable(NioChannel ch) throws IOException {
+      ch.lastReadResult = 0;
+      if (ch.callback != null) {
+          ch.callback.handleReadable(ch);
+      }
+      if (ch.lastReadResult != 0 && ch.readInterest && !ch.inClosed) {
+          log.warning("LOOP: read interest" +
+                      " after incomplete read");
+          ch.close();
+      }                                        
+  }
+  
+  private void handleDataWriteable(NioChannel ch) throws IOException {
+      ch.lastWriteResult = 0;
+      if (ch.callback != null) {
+          ch.callback.handleWriteable(ch);
+      }
+      if (ch.lastWriteResult > 0 && ch.writeInterest) {
+          log.warning("SelectorThread: write interest" +
+                      " after incomplete write, LOOP");
+      }
+  }
+
+  private void handleConnect(NioChannel ch, SocketChannel sc)
+          throws IOException, SocketException {
+      try {
+          if (!sc.finishConnect()) {
+              log.warning("handleConnected - finishConnect returns false");
+          }
+          ch.sel = this;
+          //sc.socket().setSoLinger(true, 0);
+          if (debug) {
+              log.info("connected() " + ch + " isConnected()=" + sc.isConnected() + " " + 
+                      sc.isConnectionPending());
+          }
+
+          readInterest(ch, true);
+
+          if (ch.callback != null) {
+              ch.callback.handleConnected(ch);
+          }
+      } catch (Throwable t) {
+          log.warning("Error in connect, closing ");
+          close(ch, t);
+          try {
+              if (ch.callback != null) {
+                  ch.callback.handleConnected(ch);
+              }
+          } catch(Throwable t1) {
+              log.warning("Double error in connect, callback broken too");
+              t1.printStackTrace();
+          }
+          
+      }
+  }
+
+  private void handleAccept(NioChannel ch, SelectionKey sk)
+          throws IOException, ClosedChannelException {
+      SelectableChannel selc = sk.channel();
+      ServerSocketChannel ssc=(ServerSocketChannel)selc;
+      SocketChannel sockC = ssc.accept();
+      sockC.configureBlocking(false);
+      
+      NioChannel acceptedChannel = new NioChannel(this);
+      acceptedChannel.selKey = sockC.register(selector, 
+              SelectionKey.OP_READ, 
+              acceptedChannel);
+      acceptedChannel.channel = sockC;
+      
+      synchronized (active) {
+          active.add(acceptedChannel);
+      }
+
+      // Find the callback for the new socket
+      if (ch.callback != null) {
+          // TODO: use future !
+          try {
+              ch.callback.handleConnected(acceptedChannel);
+          } catch (Throwable t) {
+              log.log(Level.SEVERE, "SelectorThread: Channel error, closing ", t);
+              acceptedChannel.lastException = t;
+              acceptedChannel.close();              
+          }
+     }
+      
+      //sk.interestOps(sk.interestOps() | SelectionKey.OP_ACCEPT);
+      if (debug) {
+          log.info("handleAccept " + ch);
+      }
+  }
+
+
+  public void shutdownOutput(NioChannel ch) throws IOException {
+      Channel channel = ch.channel;
+      if (channel instanceof SocketChannel) {
+          SocketChannel sc = (SocketChannel) channel;
+          if (sc.isOpen() && sc.isConnected()) {
+              sc.socket().shutdownOutput(); // TCP end to the other side
+          }
+      }
+  }
+  
+  /** 
+   * Called from the IO thread 
+   */
+  private void closeIOThread(NioChannel ch, boolean remove) {
+      SelectionKey sk = (SelectionKey) ch.selKey;
+      Channel channel = ch.channel;
+      try {
+          synchronized(closeInterest) {
+              if (ch.closeCalled) {
+                  if (debug) {
+                      log.severe("Close called 2x ");
+                  }
+                  return;
+              }
+              ch.closeCalled = true;
+              int o = opened.decrementAndGet();
+              if (debug) {
+                  log.info("-------------> close: " + ch + " t=" + ch.lastException);
+              }
+              if (sk != null) {
+                  if (sk.isValid()) {
+                      sk.interestOps(0);
+                  }
+                  sk.cancel();
+                  ch.selKey = null;
+              }
+              
+              if (channel instanceof SocketChannel) {
+                  SocketChannel sc = (SocketChannel) channel;
+
+                  if (sc.isConnected()) {
+                      //System.err.println("Close socket, opened=" + o);
+                      try {
+                          sc.socket().shutdownInput();
+                      } catch(IOException io1) {
+                      }
+                      try {
+                          sc.socket().shutdownOutput(); // TCP end to the other side
+                      } catch(IOException io1) {
+                      }
+                      sc.socket().close();
+                  }
+              }
+              channel.close();
+
+              closed.incrementAndGet();
+              
+              if (ch.callback != null) {
+                  ch.callback.handleClosed(ch);
+              }
+              // remove from active - false only if already removed
+              if (remove) {
+                  synchronized (active) {
+                      boolean removed = active.remove(ch);
+                  }
+              }
+      }
+      } catch (IOException ex2) {
+          log.severe("SelectorThread: Error closing socket " + ex2);
+          ex2.printStackTrace();
+      }
+  }
+
+  // --------------- Socket op abstractions ------------
+
+  public int readNonBlocking(NioChannel selectorData, ByteBuffer bb) 
+  throws IOException {
+      try {
+          int off = bb.position();
+
+          int done = 0;
+          
+          done = ((SocketChannel) selectorData.channel).read(bb);
+          
+          if (debug) {
+              log.info("-------------readNB rd=" + done + " bb.limit=" + 
+                      bb.limit() + " pos=" + bb.position() + " " + selectorData);
+          }
+          if (done > 0) {
+              if (debug) {
+                  if (!bb.isDirect()) {
+                      String s = new String(bb.array(), off,
+                          bb.position() - off);
+                      log.info("Data:\n" + s);
+                  } else {
+                      log.info("Data: " + bb.toString());
+                  }
+              }
+              selectorData.zeroReads = 0;
+          } else if (done < 0) {
+              if (debug) {
+                  log.info("SelectorThread: EOF while reading");
+              }
+          } else {
+              // need more...
+              if (selectorData.lastReadResult == 0) {
+                  selectorData.zeroReads++;
+                  if (selectorData.zeroReads > 6) {
+                      log.severe("LOOP 0 reading ");
+                      selectorData.lastException = new IOException("Polling read");
+                      selectorData.close();
+                      return -1;
+                  }
+              }
+          }
+          selectorData.lastReadResult = done;
+          return done;
+      } catch(IOException ex) {
+          if (debug) {
+              log.info("readNB error rd=" + -1 + " bblen=" + 
+                      (bb.limit() - bb.position()) + " " + selectorData + " " + ex);
+          }
+          // common case: other side closed the connection. No need for trace
+          if (ex.toString().indexOf("Connection reset by peer") < 0) {
+              ex.printStackTrace();
+          }
+          selectorData.lastException = ex;
+          selectorData.close();
+          return -1;
+      }
+  }
+
+  /**
+   *  May be called from any thread
+   */
+  public int writeNonBlocking(NioChannel selectorData, ByteBuffer bb) 
+          throws IOException {
+      try {
+          if (debug) {
+              log.info("writeNB pos=" + bb.position() + " len=" + 
+                      (bb.limit() - bb.position()) + " " + selectorData);
+             if (!bb.isDirect()) {
+                  String s = new String(bb.array(), bb.position(),
+              
+                      bb.limit() - bb.position());
+                  log.info("Data:\n" + s);
+              }
+          }
+          if (selectorData.writeInterest) {
+              // writeInterest will be false after a callback, if it is 
+              // set it means we want to wait for the callback.
+              if (debug) {
+                  log.info("Prevent writeNB when writeInterest is set");
+              }
+              return 0;
+          }
+
+          int done = 0;
+          done = ((SocketChannel) selectorData.channel).write(bb);
+          selectorData.lastWriteResult = done;
+          return done;
+      } catch(IOException ex) {
+          if (debug) {
+              log.info("writeNB error pos=" + bb.position() + " len=" + 
+                      (bb.limit() - bb.position()) + " " + selectorData + " " + 
+                      ex);
+          }
+          //ex.printStackTrace();
+          selectorData.lastException = ex;
+          selectorData.close();
+          throw ex;
+          // return -1;
+      }
+  }
+
+  public int getPort(NioChannel sd, boolean remote) {
+      SocketChannel socketChannel = (SocketChannel) sd.channel;        
+      
+      if (remote) {
+          return socketChannel.socket().getPort();
+      } else {
+          return socketChannel.socket().getLocalPort();
+      }
+  }
+  
+  public InetAddress getAddress(NioChannel sd, boolean remote) {
+      SocketChannel socketChannel = (SocketChannel) sd.channel;        
+      
+      if (remote) {
+          return socketChannel.socket().getInetAddress();
+      } else {
+          return socketChannel.socket().getLocalAddress();
+      }
+  }
+
+  /** 
+   */
+  public void connect(String host, int port, NioChannelCallback cstate) 
+          throws IOException {
+      connect(new InetSocketAddress(host, port), cstate);
+  }
+  
+
+  public void connect(SocketAddress sa, NioChannelCallback cstate)
+          throws IOException {
+      connect(sa, cstate, null);
+  }
+  
+  public void connect(SocketAddress sa, NioChannelCallback cstate, 
+                      NioChannel filter) 
+          throws IOException {
+
+      SocketChannel socketChannel = SocketChannel.open();
+      socketChannel.configureBlocking(false);
+      NioChannel selectorData = new NioChannel(this);
+      selectorData.sel = this;
+      selectorData.callback = cstate;
+      selectorData.channel = socketChannel;
+      selectorData.channel = socketChannel; // no key
+      
+      socketChannel.connect(sa);
+      opened.incrementAndGet();
+      
+      synchronized (connectAcceptInterest) {
+          connectAcceptInterest.add(selectorData);
+      }
+      selector.wakeup();
+  }
+
+  // TODO
+  public void configureSocket(ByteChannel ch,
+                              boolean noDelay) throws IOException {
+      SocketChannel sockC = (SocketChannel) ch;
+      sockC.socket().setTcpNoDelay(noDelay);
+  }
+
+  // TODO
+  public void setSocketOptions(NioChannel selectorData,
+                               int linger, 
+                               boolean tcpNoDelay,
+                               int socketTimeout)
+  throws IOException {
+
+      SocketChannel socketChannel = 
+          (SocketChannel) selectorData.channel;
+      Socket socket = socketChannel.socket();
+
+      if(linger >= 0 ) 
+          socket.setSoLinger( true, linger);
+      if( tcpNoDelay )
+          socket.setTcpNoDelay(tcpNoDelay);
+      if( socketTimeout > 0 )
+          socket.setSoTimeout( socketTimeout );
+  }
+
+  /** 
+   * Can be called from multiple threads or multiple times.
+   */
+  public int close(NioChannel selectorData, Throwable exception) throws IOException {
+      synchronized (closeInterest) {
+          if (isSelectorThread()) {
+              closeIOThread(selectorData, true);
+              return 0;
+          }
+          if (exception != null) {
+              selectorData.lastException = exception;
+          }
+          if (!selectorData.inClosed) {
+              closeInterest.add(selectorData);
+          }
+          selectorData.readInterest = false;
+      }
+      selector.wakeup();
+      return 0;
+  }
+
+
+
+  public void acceptor(NioChannelCallback cstate, 
+                       int port, 
+                       InetAddress inet, 
+                       int backlog,
+                       int serverTimeout)
+  throws IOException 
+  {
+      ServerSocketChannel ssc=ServerSocketChannel.open();
+      ServerSocket serverSocket = ssc.socket();
+      
+      SocketAddress sa = null;
+      
+      if (inet == null) {
+          sa = new InetSocketAddress( port );
+      } else {
+          sa = new InetSocketAddress(inet, port);
+      }
+      if (backlog > 0) {
+          serverSocket.bind( sa , backlog);
+      } else {
+          serverSocket.bind(sa);
+      }
+      if( serverTimeout >= 0 ) {
+          serverSocket.setSoTimeout( serverTimeout );
+      }
+      
+
+      ssc.configureBlocking(false);
+
+      NioChannel selectorData = new NioChannel(this);
+      selectorData.channel = ssc; // no key yet
+      selectorData.callback = cstate; 
+      // key will be set in pending
+
+      // TODO: add SSL here
+      
+      synchronized (connectAcceptInterest) {
+          connectAcceptInterest.add(selectorData);
+      }
+      selector.wakeup();
+  }
+  
+  public void runInSelectorThread(Runnable cb) throws IOException {
+      if (isSelectorThread()) {
+          cb.run();
+      } else {
+          synchronized (runnableInterest) {
+              runnableInterest.add(cb);
+          }
+          selector.wakeup();
+      }
+  }
+
+  /**
+   * Example config: 
+   * 
+   * www stream tcp wait USER  PATH_TO_tomcatInetd.sh
+   * 
+   * For a different port, you need to add it to /etc/services.
+   * 
+   * 'wait' is critical - the common use of inetd is 'nowait' for 
+   * tcp services, which doesn't make sense for java ( too slow startup
+   * time ). It may make sense in future with something like android VM.
+   * 
+   * In 'wait' mode, inetd will pass the acceptor socket to java - so
+   * you can listen on port 80 and run as regular user with no special
+   * code and magic.
+   * If tomcat dies, inetd will get back the acceptor and on next connection
+   * restart tomcat. 
+   * 
+   * This also works with xinetd. It might work with Apple launchd.
+   * 
+   * TODO: detect inactivity for N minutes, exist - to free resources. 
+   */
+  public void inetdAcceptor(NioChannelCallback cstate) throws IOException {
+      SelectorProvider sp=SelectorProvider.provider();
+
+      Channel ch=sp.inheritedChannel();
+      if(ch!=null ) {
+          log.info("Inherited: " + ch.getClass().getName());
+          // blocking mode
+          ServerSocketChannel ssc=(ServerSocketChannel)ch;
+          ssc.configureBlocking(false);
+
+          NioChannel selectorData = new NioChannel(this);
+          selectorData.channel = ssc;
+          selectorData.callback = cstate;
+          
+          synchronized (connectAcceptInterest) {
+              connectAcceptInterest.add(selectorData);
+          }
+          selector.wakeup();
+      } else {
+          log.severe("No inet socket ");
+          throw new IOException("Invalid inheritedChannel");
+      }
+  }
+
+  // -------------- Housekeeping -------------
+  /**
+   *  Same as APR connector - iterate over tasks, get 
+   *  smallest timeout
+   * @throws IOException 
+   */
+  void updateSleepTimeAndProcessTimeouts(long now) 
+          throws IOException {
+      long min = Long.MAX_VALUE;
+      // TODO: test with large sets, maybe sort
+      synchronized (active) {
+          Iterator<NioChannel> activeIt = active.iterator();
+
+          while(activeIt.hasNext()) {
+              NioChannel selectorData = activeIt.next();
+              if (! selectorData.channel.isOpen()) {
+                  if (debug) {
+                      log.info("Found closed socket, removing " + 
+                              selectorData.channel);
+                  }
+//                  activeIt.remove();
+//                  selectorData.close();
+              }
+
+              long t = selectorData.nextTimeEvent;
+              if (t == 0) {
+                  continue;
+              }
+              if (t < now) {
+                  // Timeout
+                  if (debug) {
+                      log.info("Time event " + selectorData);
+                  }
+                  if (selectorData.timeEvent != null) {
+                      selectorData.timeEvent.run();
+                  }
+                  // TODO: make sure this is updated if it was selected
+                  continue;
+              }
+              if (t < min) {
+                  min = t;
+              }
+          }
+      }
+      long nextSleep = min - now;
+      if (nextSleep > maxSleep) {
+          sleepTime = maxSleep;
+      } else if (nextSleep < minSleep) {
+          sleepTime = minSleep;
+      } else {
+          sleepTime = nextSleep;
+      }
+      nextWakeup = now + sleepTime;
+  }
+
+  /** 
+   * Request a callback whenever data can be written. 
+   * When the callback is invoked, the write interest is removed ( to avoid 
+   * looping ). If the write() operation doesn't complete, you must call
+   * writeInterest - AND stop writing, some implementations will throw
+   * exception. write() will actually attempt to detect this and avoid the 
+   * error.
+   * 
+   * @param sc
+   */
+  public void writeInterest(NioChannel selectorData) {
+      // TODO: suspended ? 
+
+      SelectionKey sk = (SelectionKey) selectorData.selKey;
+      if (!sk.isValid()) {
+          return;
+      }
+      selectorData.writeInterest = true;
+      int interest = sk.interestOps();
+      if ((interest & SelectionKey.OP_WRITE) != 0) {
+          return;
+      }
+      if (Thread.currentThread() == selectorThread) {
+          interest = 
+              interest | SelectionKey.OP_WRITE;
+          sk.interestOps(interest);                
+          if (debug) {
+              log.info("Write interest " + selectorData + " i=" + interest);
+          }
+          return;
+      }
+      if (debug) {
+          log.info("Pending write interest " + selectorData);
+      }
+      synchronized (writeInterest) {
+          writeInterest.add(selectorData);
+      }
+      selector.wakeup();
+  }
+  
+  
+  public void readInterest(NioChannel selectorData, boolean b) throws IOException {
+      if (Thread.currentThread() == selectorThread) {
+          selectorData.readInterest = b; 
+          selThreadReadInterest(selectorData);
+          return;
+      }
+      SelectionKey sk = (SelectionKey) selectorData.selKey;
+      if (sk == null) {
+          close(selectorData, null);
+          return;
+      }
+      int interest = sk.interestOps();
+      selectorData.readInterest = b;
+      if (b && (interest & SelectionKey.OP_READ) != 0) {
+          return;
+      }
+      if (!b && (interest & SelectionKey.OP_READ) == 0) {
+          return;
+      }
+      // Schedule the interest update.
+      synchronized (readInterest) {
+          readInterest.add(selectorData);
+      }
+      if (debug) {
+          log.info("Registering pending read interest");
+      }
+      selector.wakeup();
+  }
+
+
+  private void selThreadReadInterest(NioChannel selectorData) throws IOException {
+      SelectionKey sk = (SelectionKey) selectorData.selKey;
+      if (sk == null) {
+          if (selectorData.readInterest) {
+              if (debug) {
+                  log.info("Register again for read interest");
+              }
+              SocketChannel socketChannel = 
+                  (SocketChannel) selectorData.channel;
+              if (socketChannel.isOpen()) {
+                  selectorData.sel = this;
+                  selectorData.selKey = 
+                      socketChannel.register(selector, 
+                              SelectionKey.OP_READ, selectorData);
+                  selectorData.channel = socketChannel;
+              }
+          }
+          return;
+      }
+      if (!sk.isValid()) {
+          return;
+      }
+      int interest = sk.interestOps();
+      if (sk != null && sk.isValid()) {
+          if (selectorData.readInterest) {
+//              if ((interest | SelectionKey.OP_READ) != 0) {
+//                  return;
+//              }
+              interest = 
+                  interest | SelectionKey.OP_READ;
+          } else {
+//              if ((interest | SelectionKey.OP_READ) == 0) {
+//                  return;
+//              }
+              interest = 
+                  interest & ~SelectionKey.OP_READ;                
+          }
+          if (interest == 0) {
+              if (!selectorData.inClosed) {
+                  new Throwable().printStackTrace();
+                  log.warning("No interest(rd removed) " + selectorData);
+              }
+              // TODO: should we remove it ? It needs to be re-activated
+              // later.
+              sk.cancel(); //??
+              selectorData.selKey = null;
+          } else {
+              sk.interestOps(interest);
+          }
+          if (debug) {
+              log.info(((selectorData.readInterest) 
+                      ? "RESUME read " : "SUSPEND read ") 
+                      + selectorData);
+          }
+      }
+  }
+  
+
+  private void processPendingConnectAccept() throws IOException {
+      synchronized (connectAcceptInterest) {
+          Iterator<NioChannel> ci = connectAcceptInterest.iterator();
+
+          while (ci.hasNext()) {
+              NioChannel selectorData = ci.next();
+              
+              // Find host, port - initiate connection
+              try {
+                  // Accept interest ?
+                  if (selectorData.channel instanceof ServerSocketChannel) {
+                      ServerSocketChannel socketChannel = 
+                          (ServerSocketChannel) selectorData.channel;
+                      selectorData.sel = this;
+                      selectorData.selKey = 
+                        socketChannel.register(selector, 
+                            SelectionKey.OP_ACCEPT, selectorData);
+                      
+                      selectorData.channel = socketChannel;
+                      synchronized (active) {
+                          active.add(selectorData);
+                      }
+                      if (debug) {
+                          log.info("Pending acceptor added: " + selectorData);
+                      }
+                  } else {
+                      SocketChannel socketChannel =
+                          (SocketChannel) selectorData.channel;
+                      selectorData.sel = this;
+                      selectorData.selKey = 
+                        socketChannel.register(selector, 
+                            SelectionKey.OP_CONNECT, selectorData);
+                      synchronized (active) {
+                          active.add(selectorData);
+                      }
+                      if (debug) {
+                          log.info("Pending connect added: " + selectorData);
+                      }
+                  }
+              } catch (Throwable e) {
+                  log.log(Level.SEVERE, "error registering connect/accept", 
+                          e);
+              }
+          }
+          connectAcceptInterest.clear();
+      }
+  }
+  
+  private void processPending() throws IOException {
+      if (closeInterest.size() > 0) {
+          synchronized (closeInterest) {
+              List<NioChannel> closeList = new ArrayList(closeInterest);
+              closeInterest.clear();
+              
+              Iterator<NioChannel> ci = closeList.iterator();
+              
+              while (ci.hasNext()) {
+                  try {
+                      NioChannel selectorData = ci.next();
+                      closeIOThread(selectorData, true);
+                  } catch (Throwable t) {
+                      t.printStackTrace();
+                  }
+              }
+          }
+      }
+      processPendingConnectAccept();
+      processPendingReadWrite();
+      
+      if (runnableInterest.size() > 0) {
+          synchronized (runnableInterest) {
+              Iterator<Runnable> ci = runnableInterest.iterator();
+              while (ci.hasNext()) {
+                  Runnable cstate = ci.next();
+                  try {
+                      cstate.run();
+                  } catch (Throwable t) {
+                      t.printStackTrace();
+                  }
+                  if (debug) {
+                      log.info("Run in selthread: " + cstate);
+                  }
+              }
+              runnableInterest.clear();
+          }
+      }
+      //processPendingUpdateCallback();
+  }
+
+  private void processPendingReadWrite() throws IOException {
+      // Update interest 
+      if (readInterest.size() > 0) {
+          synchronized (readInterest) {
+              Iterator<NioChannel> ci = readInterest.iterator();
+              while (ci.hasNext()) {
+                  NioChannel cstate = ci.next();
+                  selThreadReadInterest(cstate);
+                  if (debug) {
+                      log.info("Read interest added: " + cstate);
+                  }
+              }
+              readInterest.clear();
+          }
+      }
+      if (writeInterest.size() > 0) {
+          synchronized (writeInterest) {
+              Iterator<NioChannel> ci = writeInterest.iterator();
+              while (ci.hasNext()) {
+                  NioChannel cstate = ci.next();
+                  // Fake callback - will update as side effect
+                  handleDataWriteable(cstate);
+                  if (debug) {
+                      log.info("Write interest, calling dataWritable: " + cstate);
+                  }
+              }
+              writeInterest.clear();
+          }
+      }
+  }
+  
+  
+  
+  protected boolean isSelectorThread() {
+      return Thread.currentThread() == selectorThread;
+  }
+  
+}
\ No newline at end of file
diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/io/SocketConnector.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/SocketConnector.java
new file mode 100644 (file)
index 0000000..8cd33ee
--- /dev/null
@@ -0,0 +1,127 @@
+/*
+ */
+package org.apache.tomcat.lite.io;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.logging.Logger;
+
+import org.apache.tomcat.lite.io.NioChannel.NioChannelCallback;
+
+/**
+ * Class for handling sockets. It manages a pool of SelectorThreads, fully
+ * non-blocking. There is no caching or buffer management. SelectorChannel
+ * represents on connection. 
+ * 
+ * In the old types, the connector was socket-centric, and quite ugly. After
+ * many refactoring the buffers ( buckets and brigade ) and callbacks are
+ * used everywhere, and the sockets play a supporting role.
+ * 
+ * TODO: discover if APR is available and use it, or fall back to NIO. 
+ * 
+ * @author Costin Manolache
+ */
+public class SocketConnector extends IOConnector {
+    static Logger log = Logger.getLogger(SocketConnector.class.getName());
+    static boolean debug = false;
+    
+    // TODO: pool, balanced usage
+    // TODO: bind into OM or callback when created 
+
+    private NioThread selector;
+    
+    // For resolving DNS ( i.e. connect )
+    Executor threadPool = Executors.newCachedThreadPool();
+    
+    public SocketConnector() {
+    }
+
+    /**
+     * This may be blocking - involves host resolution, connect.
+     * If the IP address is provided - it shouldn't block.
+     */
+    @Override
+    public void connect(final String host, final int port, 
+                             final IOConnector.ConnectedCallback sc) throws IOException {
+        final SocketIOChannel ioch = new SocketIOChannel(this, null, host + ":" + port);
+        ioch.setConnectedCallback(sc);
+        threadPool.execute(new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    getSelector().connect(new InetSocketAddress(host, port), ioch, null);
+                } catch (Throwable e) {
+                    e.printStackTrace();
+                    try {
+                        sc.handleConnected(ioch);
+                        ioch.close();
+                    } catch (Throwable e1) {
+                        e1.printStackTrace();
+                    }
+                }
+            }
+        });
+    }
+    
+    /**
+     * Create a new server socket, register the callback. 
+     * If port == 0 it'll use the inherited channel, i.e. inetd mode.
+     * TODO: if port == -1, detect a free port. May block.
+     */
+    public void acceptor(final IOConnector.ConnectedCallback sc, 
+                         final CharSequence address, Object extra)
+        throws IOException 
+    {
+        final int port = Integer.parseInt(address.toString());
+        NioChannelCallback acceptCb = new NioChannelCallback() {
+            @Override
+            public void handleClosed(NioChannel ch) throws IOException {
+            }
+
+            @Override
+            public void handleConnected(NioChannel ch) throws IOException {
+                SocketIOChannel ioch = new SocketIOChannel(SocketConnector.this, 
+                        ch, ":" + port);
+                sc.handleConnected(ioch);
+            }
+
+            @Override
+            public void handleReadable(NioChannel ch) throws IOException {
+            }
+
+            @Override
+            public void handleWriteable(NioChannel ch) throws IOException {
+            }
+        };
+        
+        if (port == -1) {
+            // TODO: find an unused port
+        } else if (port == 0) {
+            getSelector().inetdAcceptor(acceptCb);          
+        }  else {
+            getSelector().acceptor(acceptCb, port, null, 200, 20000);
+        }
+    }
+
+    static int id = 0;
+
+    synchronized NioThread getSelector() {
+        if (selector == null) {
+            String name = "SelectorThread-" + id++;
+            selector = new NioThread(name, true);
+        }
+
+        return selector;
+    }
+    
+    public void stop() {
+        getSelector().stop();
+    }
+
+
+    
+    // TODO: suspendAccept(boolean)
+    
+}
diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/io/SocketIOChannel.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/SocketIOChannel.java
new file mode 100644 (file)
index 0000000..785f4e0
--- /dev/null
@@ -0,0 +1,266 @@
+/*
+ */
+package org.apache.tomcat.lite.io;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+
+import org.apache.tomcat.lite.io.NioChannel.NioChannelCallback;
+
+/**
+ * Buffered socket channel
+ */
+public class SocketIOChannel extends IOChannel implements NioChannelCallback {
+    IOBuffer out;
+    IOBuffer in;
+    
+    NioChannel ch;
+        
+    SocketIOChannel(IOConnector connector, NioChannel data, 
+            String target)
+            throws IOException {
+        this.connector = connector;
+        in = new IOBuffer(this);
+        out = new IOBuffer(this);
+        this.ch = data;
+        setOutBuffer(out);
+        setChannel(data);
+        this.target = target;
+    }
+
+    void setChannel(NioChannel data) {
+        this.ch = data;
+        if (ch != null) {
+            ch.callback = this;
+        }
+    }
+    
+    
+    @Override
+    public IOBuffer getIn() {
+        return in;
+    }
+
+    @Override
+    public IOBuffer getOut() {
+        return out;
+    }
+    
+    /** 
+     * Both in and out open
+     */
+    public boolean isOpen() {
+        if (ch == null) {
+            return false;
+        }
+        return ch.isOpen() && ch.channel != null && 
+            ch.channel.isOpen() && !getIn().isAppendClosed() &&
+            !getOut().isAppendClosed();
+    }
+
+    NioChannel getSelectorChannel() {
+        return ch;
+    }
+
+    public String toString() {
+        return ch.toString();
+    }
+    
+    public void setOutBuffer(IOBuffer out) {
+        this.out = out;
+    }
+    
+    ByteBuffer flushBuffer;
+
+    /**
+     * Send as much as possible. 
+     * 
+     * Adjust write interest so we can send more when possible.
+     */
+    private void flush(NioChannel ch) throws IOException {
+        synchronized (this) {
+            if (ch == null) {
+                if (out.isClosedAndEmpty()) {
+                    return;
+                }
+                throw new IOException("flush() with closed socket");
+            }
+            while (true) {
+                if (out.isClosedAndEmpty()) {
+                    ch.shutdownOutput();
+                    break;
+                }
+                BBucket bb = out.peekFirst(); 
+                if (bb == null) {
+                    break;
+                }
+                flushBuffer = getReadableBuffer(flushBuffer, bb);
+                int before = flushBuffer.position();
+                
+                int done = 0;
+                while (flushBuffer.remaining() > 0) {
+                    try {
+                        done = ch.write(flushBuffer);
+                    } catch (IOException ex) {
+                        // can't write - was closed !
+                        done = -1;
+                    }
+
+                    if (done < 0) {
+                        ch.close();
+                        out.close();
+                        handleFlushed(this);
+                        //throw new IOException("Closed while writting ");
+                        return;
+                    }
+                    if (done == 0) {
+                        bb.position(flushBuffer.position());
+                        ch.writeInterest(); // it is cleared on next dataWriteable
+                        return;
+                    }
+                }
+                releaseReadableBuffer(flushBuffer, bb);
+            }
+            handleFlushed(this);
+
+        }
+    }
+    
+    /**
+     * Data available for read, called from IO thread.
+     * You MUST read all data ( i.e. until read() returns 0).
+     *  
+     * OP_READ remain active - call readInterest(false) to disable - 
+     * for example to suspend reading if buffer is full.
+     */
+    public void handleReceived(IOChannel net) throws IOException {
+        // All data will go to currentReceiveBuffer, until it's full.
+        // Then a new buffer will be allocated/pooled.
+        
+        // When we fill the buffers or finish this round of reading - 
+        // we place the Buckets in the queue, as 'readable' buffers.
+        boolean newData = false;
+        try {
+            synchronized(in) {
+                // data between 0 and position
+                while (true) {
+                    if (in.isAppendClosed()) { // someone closed me ?
+                        ch.inputClosed(); // remove read interest.
+                        // if outClosed - close completely
+                        super.sendHandleReceivedCallback();
+                        return;
+                    }
+                    
+                    ByteBuffer bb = in.getWriteBuffer();
+                    int read = ch.read(bb);
+                    in.releaseWriteBuffer(read);
+    
+                    if (in == null) {
+                        // Detached.
+                        if (newData) {
+                            sendHandleReceivedCallback();
+                        }
+                        return;
+                    }
+                    
+                    if (read < 0) {
+                        ch.inputClosed();
+                        // mark the in buffer as closed
+                        in.close();
+                        sendHandleReceivedCallback();
+                        return;
+                    }
+                    if (read == 0) {
+                        if (newData) {
+                            super.sendHandleReceivedCallback();
+                        }
+                        return;
+                    }
+                    newData = true;
+                }
+            }
+        } catch (Throwable t) {
+            close();
+            if (t instanceof IOException) {
+                throw (IOException) t;
+            } else {
+                throw new IOException(t.toString());
+            }
+        }
+    }
+
+    public static final ByteBuffer getReadableBuffer(ByteBuffer orig, BBucket bucket) {
+        if (orig == null || orig.array() != bucket.array()) {
+            orig = ByteBuffer.wrap(bucket.array());
+        }
+        orig.position(bucket.position());
+        orig.limit(bucket.limit());
+        return orig;
+    }
+    
+    public static final void releaseReadableBuffer(ByteBuffer bb, BBucket bucket) {
+        bucket.position(bb.position());
+    }
+    
+    
+    
+    public void readInterest(boolean b) throws IOException {
+        ch.readInterest(b);
+    }
+    
+    public InetAddress getAddress(boolean remote) {
+        return ch.getAddress(remote);
+    }
+
+    public int getPort(boolean remote) {
+        return ch.getPort(remote);
+    }
+
+    public String getRemoteAddress() {
+        return getAddress(true).toString();
+    }
+
+    public int getRemotePort() {
+        return getPort(true);
+    }
+
+
+    public void startSending() throws IOException {
+        flush(ch);
+    }
+
+    public void shutdownOutput() throws IOException {
+        getOut().close();
+        if (ch != null) {
+            startSending();
+        }
+    }
+    
+    @Override
+    public void handleClosed(NioChannel ch) throws IOException {
+        closed(); // our callback.
+    }
+    
+    public void closed() throws IOException {
+        getIn().close();
+        sendHandleReceivedCallback();
+        //super.closed();
+    }
+    
+    @Override
+    public void handleConnected(NioChannel ch) throws IOException {
+        setChannel(ch);
+        connectedCallback.handleConnected(this);
+    }
+
+    @Override
+    public void handleReadable(NioChannel ch) throws IOException {
+        handleReceived(this);
+    }
+
+    @Override
+    public void handleWriteable(NioChannel ch) throws IOException {
+        flush(ch);
+    }
+}
diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/io/SslChannel.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/SslChannel.java
new file mode 100644 (file)
index 0000000..7c5b708
--- /dev/null
@@ -0,0 +1,421 @@
+/*
+ */
+package org.apache.tomcat.lite.io;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.security.GeneralSecurityException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLEngineResult;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.SSLEngineResult.HandshakeStatus;
+import javax.net.ssl.SSLEngineResult.Status;
+
+
+public class SslChannel extends IOChannel implements Runnable {
+
+    static Logger log = Logger.getLogger("SSL");
+
+    SSLEngine sslEngine;
+    // Last result
+    SSLEngineResult unwrapR;
+    
+    boolean handshakeDone = false;
+    boolean handshakeInProgress = false;
+    boolean flushing = false;
+    
+    IOBuffer in = new IOBuffer(this);
+    IOBuffer out = new IOBuffer(this);
+    
+    public SslChannel() {
+        
+    }
+    
+    ByteBuffer myAppOutData;
+    ByteBuffer myNetOutData; 
+
+    /*
+     * Special: SSL works in packet mode, and we may receive an incomplete
+     * packet. This should be in compacted write mode (i.e. data from 0 to pos,
+     * limit at end )  
+     */
+    ByteBuffer myNetInData;
+    
+    ByteBuffer myAppInData;
+    boolean client = true;
+
+    private SSLContext sslCtx;
+
+    private boolean closeHandshake = false;
+    
+    private void initSsl() throws GeneralSecurityException {
+        if (sslEngine != null) {
+            return;
+        }
+        
+        if (client) {
+            sslEngine = sslCtx.createSSLEngine();
+            sslEngine.setUseClientMode(client);
+        } else {
+            sslEngine = sslCtx.createSSLEngine();
+            sslEngine.setUseClientMode(false);
+            String[] cs = sslEngine.getEnabledCipherSuites();
+            cs =sslEngine.getSupportedCipherSuites();
+            
+        }
+        SSLSession session = sslEngine.getSession();
+    
+        myAppOutData = ByteBuffer.allocate(session.getApplicationBufferSize());
+        myNetOutData = ByteBuffer.allocate(session.getPacketBufferSize());
+        myAppInData = ByteBuffer.allocate(session.getApplicationBufferSize());
+        myNetInData = ByteBuffer.allocate(session.getPacketBufferSize());
+        myNetInData.flip();
+        myNetOutData.flip();
+        myAppInData.flip();
+        myAppOutData.flip();
+        
+        // TODO: enable anon suites
+        //sslEngine.setEnabledCipherSuites(sslEngine.getSupportedCipherSuites());
+    }
+    
+    public SslChannel withServer() {
+        client = false;
+        return this;
+    }
+    
+    
+    @Override
+    public void setSink(IOChannel net) {
+        try {
+            initSsl();
+            super.setSink(net);
+        } catch (GeneralSecurityException e) {
+            log.log(Level.SEVERE, "Error initializing ", e);
+        }
+    }
+
+    @Override
+    public IOBuffer getIn() {
+        return in;
+    }
+
+    @Override
+    public IOBuffer getOut() {
+        return out;
+    }
+    
+    /**
+     * Typically called when a dataReceived callback is passed up.
+     * It's up to the higher layer to decide if it can handle more data 
+     * and disable read interest and manage its buffers.
+     * 
+     * We have to use one buffer.
+     * @throws IOException 
+     */
+    public int processInput(IOBuffer netIn, IOBuffer appIn) throws IOException {
+        if (log.isLoggable(Level.FINEST)) {
+            log.finest("JSSE: processInput " + handshakeInProgress + " " + netIn.getBufferCount());
+        }
+        if (!handshakeDone && !handshakeInProgress) {
+            handshakeInProgress = true;
+            handleHandshking();
+            return 0; 
+        }
+        if (handshakeInProgress) {
+            return 0; // leave it there
+        }
+        return processRealInput(netIn, appIn);
+    }
+    
+    private synchronized int processRealInput(IOBuffer netIn, IOBuffer appIn) throws IOException {
+        int rd = 0;
+        boolean needsMore = true;
+
+        while (needsMore) {
+            if (netIn.isClosedAndEmpty()) {
+                appIn.close();
+                sendHandleReceivedCallback();
+                return -1;
+            }
+            myNetInData.compact();
+            int rdNow = netIn.read(myNetInData);
+            myNetInData.flip();
+            if (rdNow == 0) {
+                return rd;
+            }
+            if (rdNow == -1) {
+                appIn.close();
+                sendHandleReceivedCallback();
+                return rd;
+            }
+
+            while (myNetInData.remaining() > 0) {
+                myAppInData.compact();
+                unwrapR = sslEngine.unwrap(myNetInData, myAppInData);
+                myAppInData.flip();
+                if (myAppInData.remaining() > 0) {
+                    in.write(myAppInData); // all will be written
+                }
+                if (unwrapR.getStatus() == Status.CLOSED) {
+                    in.close();
+                    if (unwrapR.getHandshakeStatus() == HandshakeStatus.NEED_WRAP) {
+                        // TODO: send/receive one more packet ( handshake mode ? )
+                        handshakeInProgress = true;
+                        closeHandshake  = true;
+                        handleHandshking();
+                        
+                        startSending();
+                    }
+                    break;
+                }
+                
+                if (unwrapR.getHandshakeStatus() == HandshakeStatus.NEED_TASK) {
+                    tasks();                    
+                }
+                if (unwrapR.getStatus() == Status.BUFFER_OVERFLOW ||
+                        unwrapR.getStatus() == Status.BUFFER_UNDERFLOW) {
+                    log.severe("Unhandled overflow");
+                    break;
+                }
+            }
+            sendHandleReceivedCallback();
+            
+            
+        }
+        return rd;
+    }
+
+    protected SSLEngineResult.HandshakeStatus tasks() {
+        Runnable r = null;
+        while ( (r = sslEngine.getDelegatedTask()) != null) {
+            r.run();
+        }
+        return sslEngine.getHandshakeStatus();
+    }
+
+    static ByteBuffer EMPTY = ByteBuffer.allocate(0);
+    
+    public void startSending() throws IOException {
+        
+        flushing = true;
+
+        if (handshakeInProgress) {
+            return; // don't bother me.
+        }
+        
+        if (!handshakeDone) {
+            handshakeInProgress = true;
+            handleHandshking();
+            return; // can't write yet.
+        }
+        startRealSending();
+    }
+    
+    public void close() throws IOException {
+        sslEngine.closeOutbound(); // mark as closed
+        myNetOutData.compact();
+        SSLEngineResult wrap = sslEngine.wrap(EMPTY, 
+                myNetOutData);
+        myNetOutData.flip();
+        net.getOut().write(myNetOutData);
+        // TODO: timer to close socket if we don't get
+        // clean close handshake
+    }
+    
+    private synchronized void startRealSending() throws IOException {
+        IOBuffer netOut = net.getOut();
+        while (true) {
+        
+            myAppOutData.compact();
+            int rd = out.read(myAppOutData);
+            myAppOutData.flip();
+            if (rd == 0) {
+                break;
+            }
+            if (rd < 0) {
+                close();
+                break;
+            }
+    
+            myNetOutData.compact();
+            SSLEngineResult wrap = sslEngine.wrap(myAppOutData, 
+                    myNetOutData);
+            myNetOutData.flip();
+            net.getOut().write(myNetOutData);
+                        
+            if (wrap != null) {
+                switch (wrap.getStatus()) {
+                case BUFFER_UNDERFLOW: {
+                    break;
+                }
+                case OK: {
+                    break;
+                }
+                case BUFFER_OVERFLOW: {
+                    throw new IOException("Overflow");
+                }
+                }
+            }
+        }
+        
+        net.startSending();
+    }
+
+
+    
+    // SSL handshake require slow tasks - that will need to be executed in a 
+    // thread anyways. Better to keep it simple ( the code is very complex ) - 
+    // and do the initial handshake in a thread, not in the IO thread.
+    // We'll need to unregister and register again from the selector.
+    private void handleHandshking() { 
+        if (log.isLoggable(Level.FINEST)) {
+            log.finest("Starting handshake");
+        }
+        handshakeInProgress = true;
+
+        new Thread(this).start();
+    }
+    
+    private void endHandshake() throws IOException {
+        if (log.isLoggable(Level.FINEST)) {
+            log.finest("Handshake done");
+        }
+        handshakeDone = true;
+        handshakeInProgress = false;
+        if (flushing) {
+            flushing = false;
+            startSending();
+        }
+    }
+
+    /**
+     * Actual handshake magic, in background thread.
+     */
+    public void run() {
+        try {
+            boolean initial = true;
+            SSLEngineResult wrap = null;
+
+            HandshakeStatus hstatus = sslEngine.getHandshakeStatus();
+            if (!closeHandshake && 
+                    (hstatus == HandshakeStatus.NOT_HANDSHAKING || initial)) {
+                sslEngine.beginHandshake();
+                hstatus = sslEngine.getHandshakeStatus();
+            }
+            
+            long t0 = System.currentTimeMillis();
+            
+            while (hstatus != HandshakeStatus.FINISHED) {
+                if (wrap != null && wrap.getStatus() == Status.CLOSED) {
+                    break;
+                }
+                if (log.isLoggable(Level.FINEST)) {
+                    log.finest("-->doHandshake() loop: status = " + hstatus + " " +
+                            sslEngine.getHandshakeStatus());
+                }
+                
+                if (hstatus == HandshakeStatus.NEED_WRAP) {
+                    // || initial - for client
+                    initial = false;
+                    myNetOutData.compact();
+                    
+                    wrap = sslEngine.wrap(myAppOutData, myNetOutData);
+                    myNetOutData.flip();
+                    
+                    hstatus = wrap.getHandshakeStatus();
+
+                    net.getOut().write(myNetOutData);
+                    net.startSending();
+
+                    
+                } else if (hstatus == HandshakeStatus.NEED_UNWRAP) {
+
+                    while (hstatus == HandshakeStatus.NEED_UNWRAP) {
+                        // If we have few remaining bytes - process them
+                        if (myNetInData.remaining() > 0) {
+                            myAppInData.clear();
+                            wrap = sslEngine.unwrap(myNetInData, myAppInData);
+                            hstatus = wrap.getHandshakeStatus();
+
+                            myAppInData.flip();
+                            if (myAppInData.remaining() > 0) {
+                                throw new IOException("Unexpected data");
+                            }
+                            if (wrap.getStatus() == Status.CLOSED) {
+                                break;
+                            }
+                        }
+                        // Still need unwrap 
+                        if (wrap == null 
+                                || wrap.getStatus() == Status.BUFFER_UNDERFLOW
+                                || (hstatus == HandshakeStatus.NEED_UNWRAP && myNetInData.remaining() == 0)) {
+                            myNetInData.compact();
+                            int rd = net.getIn().read(myNetInData);
+                            myNetInData.flip();
+                            if (rd == 0) {
+                                net.getIn().waitData(10000);
+                                continue;
+                            }
+                            if (rd < 0) {
+                                // in closed
+                                break;
+                            }
+                        }
+                        if (log.isLoggable(Level.FINEST)) {
+                            log.finest("Unwrap chunk done " + hstatus + " " + wrap 
+                                + " " + sslEngine.getHandshakeStatus());
+                        }
+
+                    }
+                    
+                    // rd may have some input bytes.
+                } else if (hstatus == HandshakeStatus.NEED_TASK) {
+                    long t0task = System.currentTimeMillis();
+                    Runnable r;
+                    while ((r = sslEngine.getDelegatedTask()) != null) {
+                        r.run();
+                    }
+                    long t1task = System.currentTimeMillis();
+                    hstatus = sslEngine.getHandshakeStatus();
+                    if (log.isLoggable(Level.FINEST)) {
+                        log.finest("Tasks done in " + (t1task - t0task) + " new status " +
+                                hstatus);
+                    }
+                    
+                }
+                if (hstatus == HandshakeStatus.NOT_HANDSHAKING) {
+                    //log.warning("NOT HANDSHAKING " + this);
+                    break;
+                }
+            }
+            endHandshake();
+            processRealInput(net.getIn(), in);
+        } catch (Throwable t) {
+            log.log(Level.SEVERE, "Error handshaking", t);
+            try {
+                close();
+                net.close();
+            } catch (IOException ex) {
+                log.log(Level.SEVERE, "Error closing", ex);                
+            }
+        }
+    }
+
+
+    @Override
+    public void handleReceived(IOChannel ch) throws IOException {
+        processInput(net.getIn(), in);
+        sendHandleReceivedCallback();
+    }
+
+    public SslChannel setSslContext(SSLContext sslCtx) {
+        this.sslCtx = sslCtx;
+        return this;
+    }
+}
diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/io/SslConnector.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/SslConnector.java
new file mode 100644 (file)
index 0000000..39a3139
--- /dev/null
@@ -0,0 +1,375 @@
+/*
+ */
+package org.apache.tomcat.lite.io;
+
+import java.io.ByteArrayInputStream;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.Socket;
+import java.security.GeneralSecurityException;
+import java.security.Key;
+import java.security.KeyFactory;
+import java.security.KeyManagementException;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.Principal;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.security.spec.RSAKeyGenParameterSpec;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.concurrent.Executor;
+
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+import javax.net.ssl.X509ExtendedKeyManager;
+import javax.net.ssl.X509TrustManager;
+
+
+public class SslConnector extends IOConnector {
+
+    public static class BasicTrustManager implements X509TrustManager {
+    
+        private X509Certificate[] chain;
+        
+        public void checkClientTrusted(X509Certificate[] chain, String authType)
+                throws CertificateException {
+            this.chain = chain;
+        }
+    
+        public void checkServerTrusted(X509Certificate[] chain, String authType)
+                throws CertificateException {
+            this.chain = chain;
+        }
+    
+        public X509Certificate[] getAcceptedIssuers() {
+            return new X509Certificate[0];
+        }
+    }
+
+    public static TrustManager[] trustAllCerts = new TrustManager[] { 
+        new BasicTrustManager() }; 
+
+    static final boolean debug = false;
+
+    static {
+        if (debug) {
+            System.setProperty("javax.net.debug", "ssl");
+        }
+    }
+    IOConnector net;
+    private KeyManager[] keyManager; 
+    SSLContext sslCtx;
+    boolean server;
+    private TrustManager[] trustManagers;
+    
+    Executor handshakeExecutor;
+    
+    public SslConnector() {
+    }
+    
+    public void start() {
+        
+    }
+    
+    public IOConnector getNet() {
+        if (net == null) {
+            try {
+                sslCtx = SSLContext.getInstance("TLS");
+                if (trustManagers == null) {
+                    trustManagers =
+                        new TrustManager[] {new BasicTrustManager()}; 
+
+                }
+                sslCtx.init(keyManager, trustManagers, null);
+            } catch (NoSuchAlgorithmException e) {
+                throw new RuntimeException(e);
+            } catch (KeyManagementException e) {
+                // TODO Auto-generated catch block
+                e.printStackTrace();
+            }
+            
+            net = new SocketConnector();
+        }
+        return net;
+    }
+    
+    static int id = 0;
+    
+    @Override
+    public void acceptor(final ConnectedCallback sc, CharSequence port, Object extra) 
+            throws IOException {
+        getNet().acceptor(new ConnectedCallback() {
+            @Override
+            public void handleConnected(IOChannel ch) throws IOException {
+                IOChannel first = ch;
+                if (debug) {
+                    DumpChannel dch = new DumpChannel("S-ENC-" + id );
+                    ch.addFilterAfter(dch);
+                    first = dch;
+                }
+                
+                IOChannel sslch = new SslChannel()
+                    .setSslContext(sslCtx)
+                    .withServer();
+                sslch.setSink(first);
+                first.addFilterAfter(sslch);
+
+                if (debug) {
+                    DumpChannel dch2 = new DumpChannel("S-CLR-" + id);
+                    sslch.addFilterAfter(dch2);
+                    sslch = dch2;
+                    id++;
+                }
+                
+                sc.handleConnected(sslch);
+            }
+        }, port, extra);
+    }
+    
+    @Override
+    public void connect(String host, int port, final ConnectedCallback sc)
+            throws IOException {
+        getNet().connect(host, port, new ConnectedCallback() {
+
+            @Override
+            public void handleConnected(IOChannel ch) throws IOException {
+                IOChannel first = ch;
+                if (debug) {
+                    DumpChannel dch = new DumpChannel("ENC-" + id);
+                    ch.addFilterAfter(dch);
+                    first = dch;
+                }
+                
+                IOChannel sslch = new SslChannel()
+                    .setSslContext(sslCtx);
+                sslch.setSink(first);
+                first.addFilterAfter(sslch);
+
+                if (debug) {
+                    DumpChannel dch2 = new DumpChannel("CLR-" + id);
+                    sslch.addFilterAfter(dch2);
+                    sslch = dch2;
+                    id++;
+                }
+                
+                sc.handleConnected(sslch);
+            }
+            
+        });
+    }
+
+    public SslConnector withKeyManager(KeyManager[] kms) {
+        this.keyManager = kms;
+        return this;
+    }
+    
+    public SslConnector setKeysFile(String file, String pass) throws IOException {
+        return setKeys(new FileInputStream(file), pass);
+    }
+
+    public SslConnector setKeysResource(String res, String pass) throws IOException {
+        return setKeys(this.getClass().getClassLoader().getResourceAsStream(res), 
+                pass);
+    }
+    
+    public SslConnector setKeys(InputStream file, String pass) {
+        char[] passphrase = pass.toCharArray();
+        KeyStore ks;
+        try {
+            ks = KeyStore.getInstance("JKS");
+            ks.load(file, passphrase);
+            KeyManagerFactory kmf = 
+                KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+            kmf.init(ks, passphrase);
+            
+            TrustManagerFactory tmf = 
+                TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+            tmf.init(ks);
+            
+            keyManager = kmf.getKeyManagers();
+            trustManagers = tmf.getTrustManagers();
+        } catch (KeyStoreException e) {
+            // TODO Auto-generated catch block
+        }catch (NoSuchAlgorithmException e) {
+            // TODO Auto-generated catch block
+            e.printStackTrace();
+        } catch (CertificateException e) {
+            // TODO Auto-generated catch block
+            e.printStackTrace();
+        } catch (FileNotFoundException e) {
+            // TODO Auto-generated catch block
+            e.printStackTrace();
+        } catch (IOException e) {
+            // TODO Auto-generated catch block
+            e.printStackTrace();
+        } catch (UnrecoverableKeyException e) {
+            // TODO Auto-generated catch block
+            e.printStackTrace();
+        }
+                
+        return this;
+    }
+    
+    public SslConnector setKeys(X509Certificate cert, PrivateKey privKey) {
+        keyManager = new KeyManager[] {
+                new TestKeyManager(cert, privKey)
+        };
+        return this;
+    }
+    
+    /**
+     * Initialize using a PEM certificate and key bytes.
+     * ( TODO: base64 dep to set the key as PEM )
+     * 
+     * 
+     * Key was generated with 
+     *   keytool -genkey -alias server -keyalg RSA -storepass changeit
+     *   keytool -selfcert -storepass changeit -alias server
+     *    
+     * Then the bytes printed with printPrivateKey()
+     * 
+     * I found no way to generate the self-signed keys from jsse 
+     * except CLI. 
+     * 
+     */
+    public SslConnector setKeys(String certPem, byte[] keyBytes) throws NoSuchAlgorithmException, InvalidKeySpecException, GeneralSecurityException {
+        // convert key 
+        KeyFactory kf = KeyFactory.getInstance("RSA");
+        PKCS8EncodedKeySpec keysp = new PKCS8EncodedKeySpec(keyBytes);
+        PrivateKey priv = kf.generatePrivate (keysp);
+
+        // Convert cert pem to certificate
+        InputStream is = new ByteArrayInputStream(certPem.getBytes());
+        CertificateFactory cf = CertificateFactory.getInstance("X.509");
+        final X509Certificate cert =  (X509Certificate) cf.generateCertificate(is);
+        
+        setKeys(cert, priv);
+        
+        return this;
+    }
+
+    public class TestKeyManager extends X509ExtendedKeyManager {
+        X509Certificate cert;
+        PrivateKey privKey;
+        
+        public TestKeyManager(X509Certificate cert2, PrivateKey privKey2) {
+            cert = cert2;
+            privKey = privKey2;
+        }
+
+        public String chooseEngineClientAlias(String[] keyType, 
+                java.security.Principal[] issuers, javax.net.ssl.SSLEngine engine) {
+            return "client";
+        }
+        
+        public String chooseEngineServerAlias(String keyType, 
+                java.security.Principal[] issuers, javax.net.ssl.SSLEngine engine) {
+            return "server";
+        }
+        
+        public String chooseClientAlias(String[] keyType,
+                                        Principal[] issuers, Socket socket) {
+            return "client";
+        }
+
+        public String chooseServerAlias(String keyType,
+                                        Principal[] issuers, Socket socket) {
+            return "server";
+        }
+
+        public X509Certificate[] getCertificateChain(String alias) {
+            return new X509Certificate[] {cert};
+        }
+
+        public String[] getClientAliases(String keyType, Principal[] issuers) {
+            return null;
+        }
+
+        public PrivateKey getPrivateKey(String alias) {
+            
+            return privKey; 
+        }
+
+        public String[] getServerAliases(String keyType, Principal[] issuers) {
+            return null;
+        }
+    }
+
+    public static void fixUrlConnection() {
+        try {
+            SSLContext sc = SSLContext.getInstance("SSL");
+            sc.init(null, SslConnector.trustAllCerts, null);
+            javax.net.ssl.HttpsURLConnection.setDefaultSSLSocketFactory(
+                    sc.getSocketFactory());
+        } catch (Exception e) {
+            e.printStackTrace();
+        } 
+    }
+
+    // Utilities
+    public static byte[] getPrivateKeyFromStore(String file, String pass) 
+            throws Exception {
+        KeyStore store = KeyStore.getInstance("JKS");
+        store.load(new FileInputStream(file), pass.toCharArray());
+        Key key = store.getKey("tomcat", "changeit".toCharArray());
+        PrivateKey pk = (PrivateKey) key;
+        byte[] encoded = pk.getEncoded();
+        return encoded;
+    }
+
+    public static byte[] getCertificateFromStore(String file, String pass) 
+            throws Exception {
+        KeyStore store = KeyStore.getInstance("JKS");
+        store.load(new FileInputStream(file), pass.toCharArray());
+        Certificate certificate = store.getCertificate("tomcat");
+
+        return certificate.getEncoded();
+    }
+    
+    public static KeyPair generateRsaOrDsa(boolean rsa) throws Exception {
+        if (rsa) {
+            KeyPairGenerator keyPairGen =
+                KeyPairGenerator.getInstance("RSA");
+            keyPairGen.initialize(1024);
+
+            RSAKeyGenParameterSpec keySpec = new RSAKeyGenParameterSpec(1024,
+                    RSAKeyGenParameterSpec.F0);
+            keyPairGen.initialize(keySpec);
+
+            KeyPair rsaKeyPair = keyPairGen.generateKeyPair();
+
+            return rsaKeyPair;
+        } else {
+            KeyPairGenerator keyPairGen =
+                KeyPairGenerator.getInstance("DSA");
+            keyPairGen.initialize(1024);
+
+            KeyPair pair = keyPairGen.generateKeyPair();
+
+            return pair;
+        }
+    }
+    
+    /**
+     * I know 2 ways to generate certs:
+     *  - keytool
+     *  - openssl req -x509 -nodes -days 365 \
+     *    -newkey rsa:1024 -keyout mycert.pem -out mycert.pem
+     *  openssl s_server -accept 9443 -cert mycert.pem -debug -msg -state -www
+     */
+}
\ No newline at end of file
diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/io/UrlEncoding.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/UrlEncoding.java
new file mode 100644 (file)
index 0000000..3266491
--- /dev/null
@@ -0,0 +1,215 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package org.apache.tomcat.lite.io;
+
+import java.io.CharConversionException;
+import java.io.IOException;
+import java.nio.charset.CharsetEncoder;
+import java.util.BitSet;
+
+
+/**
+ * Support for %xx URL encoding.
+ * 
+ * @author Costin Manolache
+ */
+public final class UrlEncoding {
+
+    protected static final boolean ALLOW_ENCODED_SLASH = 
+        Boolean.valueOf(
+                System.getProperty(
+                        "org.apache.tomcat.util.buf.UDecoder.ALLOW_ENCODED_SLASH",
+                "false")).booleanValue();
+
+    public UrlEncoding() {
+    }
+
+    // Utilities for URL encoding.
+    static BitSet SAFE_CHARS_URL = new BitSet(128);
+    static BitSet SAFE_CHARS = new BitSet(128);
+    BBuffer tmpBuffer = BBuffer.allocate(1024);
+    CBuffer tmpCharBuffer = CBuffer.newInstance();
+    
+    public void urlEncode(CBuffer url, CBuffer encoded, IOWriter enc) {
+        tmpBuffer.recycle();
+        urlEncode(url, tmpBuffer, encoded, enc.getEncoder("UTF-8"), 
+                SAFE_CHARS_URL, true, enc);
+    }
+
+    public void urlEncode(String url, CBuffer encoded, IOWriter enc) {
+        tmpCharBuffer.recycle();
+        tmpCharBuffer.append(url);
+        urlEncode(tmpCharBuffer, encoded, enc);
+    }
+    
+    /** Only works for UTF-8 or charsets preserving ascii.
+     * 
+     * @param url
+     * @param tmpBuffer
+     * @param encoded
+     * @param utf8Enc
+     * @param safeChars
+     */
+    public void urlEncode(CBuffer url,
+            BBuffer tmpBuffer,
+            CBuffer encoded,
+            CharsetEncoder utf8Enc,
+            BitSet safeChars, boolean last, IOWriter enc) {
+        // tomcat charset-encoded each character first. I don't think
+        // this is needed.
+        
+        // TODO: space to +
+        enc.encodeAll(url, tmpBuffer, utf8Enc, last);
+        byte[] array = tmpBuffer.array();
+        for (int i = tmpBuffer.position(); i < tmpBuffer.limit(); i++) {
+            int c = array[i];
+            if (safeChars.get(c)) {
+                encoded.append((char) c);
+            } else {
+                encoded.append('%');
+                char ch = Character.forDigit((c >> 4) & 0xF, 16);
+                encoded.append(ch);
+                ch = Character.forDigit(c & 0xF, 16);
+                encoded.append(ch);
+            }
+        }
+    }
+    
+    static {
+        initSafeChars(SAFE_CHARS);
+        initSafeChars(SAFE_CHARS_URL);
+        SAFE_CHARS_URL.set('/');
+    }
+    
+    private static void initSafeChars(BitSet safeChars) {
+        int i;
+        for (i = 'a'; i <= 'z'; i++) {
+            safeChars.set(i);
+        }
+        for (i = 'A'; i <= 'Z'; i++) {
+            safeChars.set(i);
+        }
+        for (i = '0'; i <= '9'; i++) {
+            safeChars.set(i);
+        }
+        // safe
+        safeChars.set('-');
+        safeChars.set('_');
+        safeChars.set('.');
+
+        // Dangerous: someone may treat this as " "
+        // RFC1738 does allow it, it's not reserved
+        // safeChars.set('+');
+        // extra
+        safeChars.set('*');
+        // tomcat has them - not sure if this is correct
+        safeChars.set('$'); // ?
+        safeChars.set('!'); // ?
+        safeChars.set('\''); // ?
+        safeChars.set('('); // ? 
+        safeChars.set(')'); // ? 
+        safeChars.set(','); // ? 
+    }
+    
+    public void urlDecode(BBuffer bb, CBuffer dest, boolean q, 
+            IOReader charDec) throws IOException {
+        // Replace %xx
+        tmpBuffer.append(bb);
+        urlDecode(tmpBuffer, q);
+        charDec.decodeAll(bb, dest);
+    }
+
+    
+    public void urlDecode(BBuffer bb, CBuffer dest,  
+            IOReader charDec) throws IOException {
+        // Replace %xx
+        tmpBuffer.append(bb);
+        urlDecode(tmpBuffer, true);
+        charDec.decodeAll(bb, dest);
+    }
+
+    
+    /**
+     * URLDecode, will modify the source. This is only at byte level - 
+     * it needs conversion to chars using the right charset. 
+     * 
+     * @param query Converts '+' to ' ' and allow '/'
+     */
+    public void urlDecode(BBuffer mb, boolean query) throws IOException {
+        int start = mb.getOffset();
+        byte buff[] = mb.array();
+        int end = mb.getEnd();
+
+        int idx = BBuffer.indexOf(buff, start, end, '%');
+        int idx2 = -1;
+        if (query)
+            idx2 = BBuffer.indexOf(buff, start, end, '+');
+        if (idx < 0 && idx2 < 0) {
+            return;
+        }
+
+        // idx will be the smallest positive inxes ( first % or + )
+        if (idx2 >= 0 && idx2 < idx)
+            idx = idx2;
+        if (idx < 0)
+            idx = idx2;
+
+        //boolean noSlash = !query;
+
+        for (int j = idx; j < end; j++, idx++) {
+            if (buff[j] == '+' && query) {
+                buff[idx] = (byte) ' ';
+            } else if (buff[j] != '%') {
+                buff[idx] = buff[j];
+            } else {
+                // read next 2 digits
+                if (j + 2 >= end) {
+                    throw new CharConversionException("EOF");
+                }
+                byte b1 = buff[j + 1];
+                byte b2 = buff[j + 2];
+                if (!isHexDigit(b1) || !isHexDigit(b2))
+                    throw new CharConversionException("isHexDigit");
+
+                j += 2;
+                int res = x2c(b1, b2);
+//                if (noSlash && (res == '/')) {
+//                    throw new CharConversionException("noSlash " + mb);
+//                }
+                buff[idx] = (byte) res;
+            }
+        }
+
+        mb.setEnd(idx);
+
+        return;
+    }
+
+
+    private static boolean isHexDigit(int c) {
+        return ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'));
+    }
+
+    private static int x2c(byte b1, byte b2) {
+        int digit = (b1 >= 'A') ? ((b1 & 0xDF) - 'A') + 10 : (b1 - '0');
+        digit *= 16;
+        digit += (b2 >= 'A') ? ((b2 & 0xDF) - 'A') + 10 : (b2 - '0');
+        return digit;
+    }
+
+}
diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/io/WrappedException.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/WrappedException.java
new file mode 100644 (file)
index 0000000..3d8a281
--- /dev/null
@@ -0,0 +1,40 @@
+/*
+ */
+package org.apache.tomcat.lite.io;
+
+import java.io.IOException;
+
+/**
+ * For specific exceptions - also has cause ( good if compiling against
+ * JDK1.5 ) 
+ * 
+ * @author Costin Manolache
+ */
+public class WrappedException extends IOException {
+
+    public WrappedException() {
+        super();
+    }
+    
+    public WrappedException(String message) {
+        super(message);
+    }
+
+    public WrappedException(String message, Throwable cause) {
+        super(message);
+        initCause(cause);
+    }
+    
+    public WrappedException(Throwable cause) {
+        super("");
+        initCause(cause);
+    }
+    
+    
+    public static class ClientAbortException extends WrappedException {
+        public ClientAbortException(Throwable throwable) {
+            super(null, throwable);
+        }
+    }
+    
+}
diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/io/package.html b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/package.html
new file mode 100644 (file)
index 0000000..07a0f30
--- /dev/null
@@ -0,0 +1,7 @@
+IO layer based on tomcat coyote connector and utils.
+
+There are many big changes:
+<ul>
+  <li>
+  <li>
+</ul>
\ No newline at end of file