--- /dev/null
+/*
+ */
+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
--- /dev/null
+/*
+ * 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);
+ }
+ }
+
+
+}
--- /dev/null
+/*
+ * 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;
+ }
+ };
+
+
+
+}
--- /dev/null
+/*
+ */
+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;
+ }
+
+}
--- /dev/null
+/*
+ * 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);
+ }
+
+ }
+ }
+ }
+
+
+}
--- /dev/null
+/*
+ */
+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;
+ }
+}
--- /dev/null
+/*
+ * 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);
+ }
+
+
+}
--- /dev/null
+/*
+ */
+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);
+}
--- /dev/null
+/*
+ */
+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
--- /dev/null
+/*
+ * 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");
+ }
+}
--- /dev/null
+/*
+ */
+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;
+//}
+}
--- /dev/null
+/*
+ */
+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;
+ }
+
+
+}
--- /dev/null
+/*
+ */
+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() {
+
+ }
+}
--- /dev/null
+/*
+ */
+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;
+ }
+}
--- /dev/null
+/*
+ */
+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();
+ }
+}
--- /dev/null
+/*
+ */
+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;
+ }
+}
--- /dev/null
+/*
+ */
+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);
+ }
+ }
+
+
+}
--- /dev/null
+/*
+ */
+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
--- /dev/null
+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
--- /dev/null
+/* 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
--- /dev/null
+/*
+ */
+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)
+
+}
--- /dev/null
+/*
+ */
+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);
+ }
+}
--- /dev/null
+/*
+ */
+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;
+ }
+}
--- /dev/null
+/*
+ */
+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
--- /dev/null
+/*
+ * 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;
+ }
+
+}
--- /dev/null
+/*
+ */
+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);
+ }
+ }
+
+}
--- /dev/null
+IO layer based on tomcat coyote connector and utils.
+
+There are many big changes:
+<ul>
+ <li>
+ <li>
+</ul>
\ No newline at end of file