You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@struts.apache.org by rg...@apache.org on 2016/01/14 15:16:26 UTC

[2/2] struts git commit: WW-4507 - clone Tomcat UDecoder and use it for in query string handling

WW-4507 - clone Tomcat UDecoder and use it for in query string handling


Project: http://git-wip-us.apache.org/repos/asf/struts/repo
Commit: http://git-wip-us.apache.org/repos/asf/struts/commit/5421930b
Tree: http://git-wip-us.apache.org/repos/asf/struts/tree/5421930b
Diff: http://git-wip-us.apache.org/repos/asf/struts/diff/5421930b

Branch: refs/heads/support-2-3
Commit: 5421930b49822606792f36653b17d3d95ef106f9
Parents: c6750c1
Author: Rene Gielen <rg...@apache.org>
Authored: Thu Jan 14 14:52:03 2016 +0100
Committer: Rene Gielen <rg...@apache.org>
Committed: Thu Jan 14 14:52:03 2016 +0100

----------------------------------------------------------------------
 .../dispatcher/mapper/Restful2ActionMapper.java |   6 +-
 .../dispatcher/mapper/RestfulActionMapper.java  |   6 +-
 .../org/apache/struts2/util/URLDecoderUtil.java |  22 +
 .../apache/struts2/util/tomcat/buf/Ascii.java   | 255 +++++
 .../struts2/util/tomcat/buf/B2CConverter.java   | 201 ++++
 .../struts2/util/tomcat/buf/ByteChunk.java      | 935 +++++++++++++++++++
 .../struts2/util/tomcat/buf/CharChunk.java      | 700 ++++++++++++++
 .../struts2/util/tomcat/buf/HexUtils.java       | 113 +++
 .../struts2/util/tomcat/buf/MessageBytes.java   | 546 +++++++++++
 .../struts2/util/tomcat/buf/StringCache.java    | 695 ++++++++++++++
 .../struts2/util/tomcat/buf/UDecoder.java       | 421 +++++++++
 .../struts2/util/tomcat/buf/Utf8Decoder.java    | 293 ++++++
 .../struts2/views/util/DefaultUrlHelper.java    |   8 +-
 .../apache/struts2/util/URLDecoderUtilTest.java |  71 ++
 14 files changed, 4262 insertions(+), 10 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/struts/blob/5421930b/core/src/main/java/org/apache/struts2/dispatcher/mapper/Restful2ActionMapper.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/struts2/dispatcher/mapper/Restful2ActionMapper.java b/core/src/main/java/org/apache/struts2/dispatcher/mapper/Restful2ActionMapper.java
index 3f08e84..0d93711 100644
--- a/core/src/main/java/org/apache/struts2/dispatcher/mapper/Restful2ActionMapper.java
+++ b/core/src/main/java/org/apache/struts2/dispatcher/mapper/Restful2ActionMapper.java
@@ -26,9 +26,9 @@ import com.opensymphony.xwork2.inject.Inject;
 import com.opensymphony.xwork2.util.logging.Logger;
 import com.opensymphony.xwork2.util.logging.LoggerFactory;
 import org.apache.struts2.StrutsConstants;
+import org.apache.struts2.util.URLDecoderUtil;
 
 import javax.servlet.http.HttpServletRequest;
-import java.net.URLDecoder;
 import java.util.HashMap;
 import java.util.StringTokenizer;
 
@@ -133,10 +133,10 @@ public class Restful2ActionMapper extends DefaultActionMapper {
 
                     while (st.hasMoreTokens()) {
                         if (isNameTok) {
-                            paramName = URLDecoder.decode(st.nextToken(), "UTF-8");
+                            paramName = URLDecoderUtil.decode(st.nextToken(), "UTF-8");
                             isNameTok = false;
                         } else {
-                            paramValue = URLDecoder.decode(st.nextToken(), "UTF-8");
+                            paramValue = URLDecoderUtil.decode(st.nextToken(), "UTF-8");
 
                             if ((paramName != null) && (paramName.length() > 0)) {
                                 parameters.put(paramName, paramValue);

http://git-wip-us.apache.org/repos/asf/struts/blob/5421930b/core/src/main/java/org/apache/struts2/dispatcher/mapper/RestfulActionMapper.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/struts2/dispatcher/mapper/RestfulActionMapper.java b/core/src/main/java/org/apache/struts2/dispatcher/mapper/RestfulActionMapper.java
index b2378f4..4b98409 100644
--- a/core/src/main/java/org/apache/struts2/dispatcher/mapper/RestfulActionMapper.java
+++ b/core/src/main/java/org/apache/struts2/dispatcher/mapper/RestfulActionMapper.java
@@ -25,9 +25,9 @@ import com.opensymphony.xwork2.config.ConfigurationManager;
 import com.opensymphony.xwork2.util.logging.Logger;
 import com.opensymphony.xwork2.util.logging.LoggerFactory;
 import org.apache.struts2.RequestUtils;
+import org.apache.struts2.util.URLDecoderUtil;
 
 import javax.servlet.http.HttpServletRequest;
-import java.net.URLDecoder;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.StringTokenizer;
@@ -67,10 +67,10 @@ public class RestfulActionMapper implements ActionMapper {
 
             while (st.hasMoreTokens()) {
                 if (isNameTok) {
-                    paramName = URLDecoder.decode(st.nextToken(), "UTF-8");
+                    paramName = URLDecoderUtil.decode(st.nextToken(), "UTF-8");
                     isNameTok = false;
                 } else {
-                    paramValue = URLDecoder.decode(st.nextToken(), "UTF-8");
+                    paramValue = URLDecoderUtil.decode(st.nextToken(), "UTF-8");
 
                     if ((paramName != null) && (paramName.length() > 0)) {
                         parameters.put(paramName, paramValue);

http://git-wip-us.apache.org/repos/asf/struts/blob/5421930b/core/src/main/java/org/apache/struts2/util/URLDecoderUtil.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/struts2/util/URLDecoderUtil.java b/core/src/main/java/org/apache/struts2/util/URLDecoderUtil.java
new file mode 100644
index 0000000..10f2a78
--- /dev/null
+++ b/core/src/main/java/org/apache/struts2/util/URLDecoderUtil.java
@@ -0,0 +1,22 @@
+package org.apache.struts2.util;
+
+import org.apache.struts2.util.tomcat.buf.UDecoder;
+
+/**
+ * URLDecoderUtil serves as a facade for a correct URL decoding implementation.
+ * As of Struts 2.3.25 it uses Tomcat URLDecoder functionality rather than the one found in java.io.
+ */
+public class URLDecoderUtil {
+
+    /**
+     * Decodes a <code>x-www-form-urlencoded</code> string.
+     * @param sequence the String to decode
+     * @param charset The name of a supported character encoding.
+     * @return the newly decoded <code>String</code>
+     * @exception IllegalArgumentException If the encoding is not valid
+     */
+    public static String decode(String sequence, String charset) {
+        return UDecoder.URLDecode(sequence, charset);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/struts/blob/5421930b/core/src/main/java/org/apache/struts2/util/tomcat/buf/Ascii.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/struts2/util/tomcat/buf/Ascii.java b/core/src/main/java/org/apache/struts2/util/tomcat/buf/Ascii.java
new file mode 100644
index 0000000..1b0ccb6
--- /dev/null
+++ b/core/src/main/java/org/apache/struts2/util/tomcat/buf/Ascii.java
@@ -0,0 +1,255 @@
+/*
+ *  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.struts2.util.tomcat.buf;
+
+/**
+ * This class implements some basic ASCII character handling functions.
+ *
+ * @author dac@eng.sun.com
+ * @author James Todd [gonzo@eng.sun.com]
+ */
+public final class Ascii {
+    /*
+     * Character translation tables.
+     */
+
+    private static final byte[] toUpper = new byte[256];
+    private static final byte[] toLower = new byte[256];
+
+    /*
+     * Character type tables.
+     */
+
+    private static final boolean[] isAlpha = new boolean[256];
+    private static final boolean[] isUpper = new boolean[256];
+    private static final boolean[] isLower = new boolean[256];
+    private static final boolean[] isWhite = new boolean[256];
+    private static final boolean[] isDigit = new boolean[256];
+
+    private static final long OVERFLOW_LIMIT = Long.MAX_VALUE / 10;
+
+    /*
+     * Initialize character translation and type tables.
+     */
+    static {
+        for (int i = 0; i < 256; i++) {
+            toUpper[i] = (byte)i;
+            toLower[i] = (byte)i;
+        }
+
+        for (int lc = 'a'; lc <= 'z'; lc++) {
+            int uc = lc + 'A' - 'a';
+
+            toUpper[lc] = (byte)uc;
+            toLower[uc] = (byte)lc;
+            isAlpha[lc] = true;
+            isAlpha[uc] = true;
+            isLower[lc] = true;
+            isUpper[uc] = true;
+        }
+
+        isWhite[ ' '] = true;
+        isWhite['\t'] = true;
+        isWhite['\r'] = true;
+        isWhite['\n'] = true;
+        isWhite['\f'] = true;
+        isWhite['\b'] = true;
+
+        for (int d = '0'; d <= '9'; d++) {
+            isDigit[d] = true;
+        }
+    }
+
+    /**
+     * Returns the upper case equivalent of the specified ASCII character.
+     * @deprecated Unused. Will be removed in Tomcat 8.0.x onwards.
+     */
+    @Deprecated
+    public static int toUpper(int c) {
+        return toUpper[c & 0xff] & 0xff;
+    }
+
+    /**
+     * Returns the lower case equivalent of the specified ASCII character.
+     */
+
+    public static int toLower(int c) {
+        return toLower[c & 0xff] & 0xff;
+    }
+
+    /**
+     * Returns true if the specified ASCII character is upper or lower case.
+     * @deprecated Unused. Will be removed in Tomcat 8.0.x onwards.
+     */
+    @Deprecated
+    public static boolean isAlpha(int c) {
+        return isAlpha[c & 0xff];
+    }
+
+    /**
+     * Returns true if the specified ASCII character is upper case.
+     * @deprecated Unused. Will be removed in Tomcat 8.0.x onwards.
+     */
+    @Deprecated
+    public static boolean isUpper(int c) {
+        return isUpper[c & 0xff];
+    }
+
+    /**
+     * Returns true if the specified ASCII character is lower case.
+     * @deprecated Unused. Will be removed in Tomcat 8.0.x onwards.
+     */
+    @Deprecated
+    public static boolean isLower(int c) {
+        return isLower[c & 0xff];
+    }
+
+    /**
+     * Returns true if the specified ASCII character is white space.
+     * @deprecated Unused. Will be removed in Tomcat 8.0.x onwards.
+     */
+    @Deprecated
+    public static boolean isWhite(int c) {
+        return isWhite[c & 0xff];
+    }
+
+    /**
+     * 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
+     * @deprecated Unused. Will be removed in Tomcat 8.0.x onwards.
+     */
+    @Deprecated
+    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;
+    }
+
+    /**
+     * @deprecated Unused. Will be removed in Tomcat 8.0.x onwards.
+     */
+    @Deprecated
+    public static int parseInt(char[] 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';
+        while (--len > 0) {
+            if (isDigit(c = b[off++]) &&
+                    (n < OVERFLOW_LIMIT || (n == OVERFLOW_LIMIT && (c - '0') < 8))) {
+                n = n * 10 + c - '0';
+            } else {
+                throw new NumberFormatException();
+            }
+        }
+
+        return n;
+    }
+
+    /**
+     * @deprecated Unused. Will be removed in Tomcat 8.0.x onwards.
+     */
+    @Deprecated
+    public static long parseLong(char[] 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;
+    }
+
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/struts/blob/5421930b/core/src/main/java/org/apache/struts2/util/tomcat/buf/B2CConverter.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/struts2/util/tomcat/buf/B2CConverter.java b/core/src/main/java/org/apache/struts2/util/tomcat/buf/B2CConverter.java
new file mode 100644
index 0000000..a3fc6d1
--- /dev/null
+++ b/core/src/main/java/org/apache/struts2/util/tomcat/buf/B2CConverter.java
@@ -0,0 +1,201 @@
+/*
+ *  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.struts2.util.tomcat.buf;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+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.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * NIO based character decoder.
+ */
+public class B2CConverter {
+
+    private static final Map<String, Charset> encodingToCharsetCache =
+            new HashMap<String, Charset>();
+
+    public static final Charset ISO_8859_1;
+    public static final Charset UTF_8;
+
+    // Protected so unit tests can use it
+    protected static final int LEFTOVER_SIZE = 9;
+
+    static {
+        for (Charset charset: Charset.availableCharsets().values()) {
+            encodingToCharsetCache.put(
+                    charset.name().toLowerCase(Locale.ENGLISH), charset);
+            for (String alias : charset.aliases()) {
+                encodingToCharsetCache.put(
+                        alias.toLowerCase(Locale.ENGLISH), charset);
+            }
+        }
+        Charset iso88591 = null;
+        Charset utf8 = null;
+        try {
+            iso88591 = getCharset("ISO-8859-1");
+            utf8 = getCharset("UTF-8");
+        } catch (UnsupportedEncodingException e) {
+            // Impossible. All JVMs must support these.
+            e.printStackTrace();
+        }
+        ISO_8859_1 = iso88591;
+        UTF_8 = utf8;
+    }
+
+    public static Charset getCharset(String enc)
+            throws UnsupportedEncodingException {
+
+        // Encoding names should all be ASCII
+        String lowerCaseEnc = enc.toLowerCase(Locale.ENGLISH);
+
+        return getCharsetLower(lowerCaseEnc);
+    }
+
+    /**
+     * Only to be used when it is known that the encoding name is in lower case.
+     */
+    public static Charset getCharsetLower(String lowerCaseEnc)
+            throws UnsupportedEncodingException {
+
+        Charset charset = encodingToCharsetCache.get(lowerCaseEnc);
+
+        if (charset == null) {
+            // Pre-population of the cache means this must be invalid
+            throw new UnsupportedEncodingException("The character encoding " + lowerCaseEnc + " is not supported");
+        }
+        return charset;
+    }
+
+    private final CharsetDecoder decoder;
+    private ByteBuffer bb = null;
+    private CharBuffer cb = null;
+
+    /**
+     * Leftover buffer used for incomplete characters.
+     */
+    private final ByteBuffer leftovers;
+
+    public B2CConverter(String encoding) throws IOException {
+        this(encoding, false);
+    }
+
+    public B2CConverter(String encoding, boolean replaceOnError)
+            throws IOException {
+        byte[] left = new byte[LEFTOVER_SIZE];
+        leftovers = ByteBuffer.wrap(left);
+        CodingErrorAction action;
+        if (replaceOnError) {
+            action = CodingErrorAction.REPLACE;
+        } else {
+            action = CodingErrorAction.REPORT;
+        }
+        Charset charset = getCharset(encoding);
+        // Special case. Use the Apache Harmony based UTF-8 decoder because it
+        // - a) rejects invalid sequences that the JVM decoder does not
+        // - b) fails faster for some invalid sequences
+        if (charset.equals(UTF_8)) {
+            decoder = new Utf8Decoder();
+        } else {
+            decoder = charset.newDecoder();
+        }
+        decoder.onMalformedInput(action);
+        decoder.onUnmappableCharacter(action);
+    }
+
+    /**
+     * Reset the decoder state.
+     */
+    public void recycle() {
+        decoder.reset();
+        leftovers.position(0);
+    }
+
+    /**
+     * Convert the given bytes to characters.
+     *
+     * @param bc byte input
+     * @param cc char output
+     * @param endOfInput    Is this all of the available data
+     */
+    public void convert(ByteChunk bc, CharChunk cc, boolean endOfInput)
+            throws IOException {
+        if ((bb == null) || (bb.array() != bc.getBuffer())) {
+            // Create a new byte buffer if anything changed
+            bb = ByteBuffer.wrap(bc.getBuffer(), bc.getStart(), bc.getLength());
+        } else {
+            // Initialize the byte buffer
+            bb.limit(bc.getEnd());
+            bb.position(bc.getStart());
+        }
+        if ((cb == null) || (cb.array() != cc.getBuffer())) {
+            // Create a new char buffer if anything changed
+            cb = CharBuffer.wrap(cc.getBuffer(), cc.getEnd(),
+                    cc.getBuffer().length - cc.getEnd());
+        } else {
+            // Initialize the char buffer
+            cb.limit(cc.getBuffer().length);
+            cb.position(cc.getEnd());
+        }
+        CoderResult result = null;
+        // Parse leftover if any are present
+        if (leftovers.position() > 0) {
+            int pos = cb.position();
+            // Loop until one char is decoded or there is a decoder error
+            do {
+                leftovers.put(bc.substractB());
+                leftovers.flip();
+                result = decoder.decode(leftovers, cb, endOfInput);
+                leftovers.position(leftovers.limit());
+                leftovers.limit(leftovers.array().length);
+            } while (result.isUnderflow() && (cb.position() == pos));
+            if (result.isError() || result.isMalformed()) {
+                result.throwException();
+            }
+            bb.position(bc.getStart());
+            leftovers.position(0);
+        }
+        // Do the decoding and get the results into the byte chunk and the char
+        // chunk
+        result = decoder.decode(bb, cb, endOfInput);
+        if (result.isError() || result.isMalformed()) {
+            result.throwException();
+        } else if (result.isOverflow()) {
+            // Propagate current positions to the byte chunk and char chunk, if
+            // this continues the char buffer will get resized
+            bc.setOffset(bb.position());
+            cc.setEnd(cb.position());
+        } else if (result.isUnderflow()) {
+            // Propagate current positions to the byte chunk and char chunk
+            bc.setOffset(bb.position());
+            cc.setEnd(cb.position());
+            // Put leftovers in the leftovers byte buffer
+            if (bc.getLength() > 0) {
+                leftovers.limit(leftovers.array().length);
+                leftovers.position(bc.getLength());
+                bc.substract(leftovers.array(), 0, bc.getLength());
+            }
+        }
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/struts/blob/5421930b/core/src/main/java/org/apache/struts2/util/tomcat/buf/ByteChunk.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/struts2/util/tomcat/buf/ByteChunk.java b/core/src/main/java/org/apache/struts2/util/tomcat/buf/ByteChunk.java
new file mode 100644
index 0000000..aff247f
--- /dev/null
+++ b/core/src/main/java/org/apache/struts2/util/tomcat/buf/ByteChunk.java
@@ -0,0 +1,935 @@
+/*
+ *  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.struts2.util.tomcat.buf;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+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.
+ */
+
+// TODO: This class could either extend ByteBuffer, or better a ByteBuffer
+// inside this way it could provide the search/etc on ByteBuffer, as a helper.
+
+/**
+ * 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 final class ByteChunk implements Cloneable, Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /** Input interface, used when the buffer is empty
+     *
+     * Same as java.nio.channel.ReadableByteChannel
+     */
+    public static interface ByteInputChannel {
+        /**
+         * Read new bytes ( usually the internal conversion buffer ).
+         * The implementation is allowed to ignore the parameters,
+         * and mutate the chunk if it wishes to implement its own buffering.
+         */
+        public int realReadBytes(byte cbuf[], int off, int len)
+                throws IOException;
+    }
+
+    /** Same as java.nio.channel.WrittableByteChannel.
+     */
+    public static interface ByteOutputChannel {
+        /**
+         * Send the bytes ( usually the internal conversion buffer ).
+         * Expect 8k output if the buffer is full.
+         */
+        public void realWriteBytes(byte cbuf[], int off, int len)
+                throws IOException;
+    }
+
+    // --------------------
+
+    /** Default encoding used to convert to strings. It should be UTF8,
+     as most standards seem to converge, but the servlet API requires
+     8859_1, and this object is used mostly for servlets.
+     */
+    public static final Charset DEFAULT_CHARSET = B2CConverter.ISO_8859_1;
+
+    // byte[]
+    private byte[] buff;
+
+    private int start=0;
+    private int end;
+
+    private Charset charset;
+
+    private boolean isSet=false; // XXX
+
+    // How much can it grow, when data is added
+    private int limit=-1;
+
+    private ByteInputChannel in = null;
+    private ByteOutputChannel out = null;
+
+    private boolean optimizedWrite=true;
+
+    /**
+     * Creates a new, uninitialized ByteChunk object.
+     */
+    public ByteChunk() {
+        // NO-OP
+    }
+
+    public ByteChunk( int initial ) {
+        allocate( initial, -1 );
+    }
+
+    /**
+     * @deprecated Unused. Will be removed in Tomcat 8.0.x onwards.
+     */
+    @Deprecated
+    public ByteChunk getClone() {
+        try {
+            return (ByteChunk)this.clone();
+        } catch( Exception ex) {
+            return null;
+        }
+    }
+
+    public boolean isNull() {
+        return ! isSet; // buff==null;
+    }
+
+    /**
+     * Resets the message buff to an uninitialized state.
+     */
+    public void recycle() {
+        //        buff = null;
+        charset=null;
+        start=0;
+        end=0;
+        isSet=false;
+    }
+
+    public void reset() {
+        buff=null;
+    }
+
+    // -------------------- Setup --------------------
+
+    public void allocate( int initial, int limit  ) {
+        if( buff==null || buff.length < initial ) {
+            buff=new byte[initial];
+        }
+        this.limit=limit;
+        start=0;
+        end=0;
+        isSet=true;
+    }
+
+    /**
+     * 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) {
+        buff = b;
+        start = off;
+        end = start+ len;
+        isSet=true;
+    }
+
+    /**
+     * @deprecated Unused. Will be removed in Tomcat 8.0.x onwards.
+     */
+    @Deprecated
+    public void setOptimizedWrite(boolean optimizedWrite) {
+        this.optimizedWrite = optimizedWrite;
+    }
+
+    public void setCharset(Charset charset) {
+        this.charset = charset;
+    }
+
+    public Charset getCharset() {
+        if (charset == null) {
+            charset = DEFAULT_CHARSET;
+        }
+        return charset;
+    }
+
+    /**
+     * Returns the message bytes.
+     */
+    public byte[] getBytes() {
+        return getBuffer();
+    }
+
+    /**
+     * Returns the message bytes.
+     */
+    public byte[] getBuffer() {
+        return buff;
+    }
+
+    /**
+     * Returns the start offset of the bytes.
+     * For output this is the end of the buffer.
+     */
+    public int getStart() {
+        return start;
+    }
+
+    public int getOffset() {
+        return start;
+    }
+
+    public void setOffset(int off) {
+        if (end < off ) {
+            end=off;
+        }
+        start=off;
+    }
+
+    /**
+     * Returns the length of the bytes.
+     * XXX need to clean this up
+     */
+    public int getLength() {
+        return end-start;
+    }
+
+    /** Maximum amount of data in this buffer.
+     *
+     *  If -1 or not set, the buffer will grow indefinitely.
+     *  Can be smaller than the current buffer size ( which will not shrink ).
+     *  When the limit is reached, the buffer will be flushed ( if out is set )
+     *  or throw exception.
+     */
+    public void setLimit(int limit) {
+        this.limit=limit;
+    }
+
+    public int getLimit() {
+        return limit;
+    }
+
+    /**
+     * When the buffer is empty, read the data from the input channel.
+     */
+    public void setByteInputChannel(ByteInputChannel in) {
+        this.in = in;
+    }
+
+    /** When the buffer is full, write the data to the output channel.
+     *         Also used when large amount of data is appended.
+     *
+     *  If not set, the buffer will grow to the limit.
+     */
+    public void setByteOutputChannel(ByteOutputChannel out) {
+        this.out=out;
+    }
+
+    public int getEnd() {
+        return end;
+    }
+
+    public void setEnd( int i ) {
+        end=i;
+    }
+
+    // -------------------- Adding data to the buffer --------------------
+    /** Append a char, by casting it to byte. This IS NOT intended for unicode.
+     *
+     * @param c
+     * @throws IOException
+     * @deprecated Unused. Will be removed in Tomcat 8.0.x onwards.
+     */
+    @Deprecated
+    public void append( char c )
+            throws IOException
+    {
+        append( (byte)c);
+    }
+
+    public void append( byte b )
+            throws IOException
+    {
+        makeSpace( 1 );
+
+        // couldn't make space
+        if( limit >0 && end >= limit ) {
+            flushBuffer();
+        }
+        buff[end++]=b;
+    }
+
+    public void append( ByteChunk src )
+            throws IOException
+    {
+        append( src.getBytes(), src.getStart(), src.getLength());
+    }
+
+    /** Add data to the buffer
+     */
+    public void append( byte src[], int off, int len )
+            throws IOException
+    {
+        // will grow, up to limit
+        makeSpace( len );
+
+        // if we don't have limit: makeSpace can grow as it wants
+        if( limit < 0 ) {
+            // assert: makeSpace made enough space
+            System.arraycopy( src, off, buff, end, len );
+            end+=len;
+            return;
+        }
+
+        // Optimize on a common case.
+        // If the buffer is empty and the source is going to fill up all the
+        // space in buffer, may as well write it directly to the output,
+        // and avoid an extra copy
+        if ( optimizedWrite && len == limit && end == start && out != null ) {
+            out.realWriteBytes( src, off, len );
+            return;
+        }
+        // if we have limit and we're below
+        if( len <= limit - end ) {
+            // makeSpace will grow the buffer to the limit,
+            // so we have space
+            System.arraycopy( src, off, buff, end, len );
+            end+=len;
+            return;
+        }
+
+        // need more space than we can afford, need to flush
+        // buffer
+
+        // the buffer is already at ( or bigger than ) limit
+
+        // We chunk the data into slices fitting in the buffer limit, although
+        // if the data is written directly if it doesn't fit
+
+        int avail=limit-end;
+        System.arraycopy(src, off, buff, end, avail);
+        end += avail;
+
+        flushBuffer();
+
+        int remain = len - avail;
+
+        while (remain > (limit - end)) {
+            out.realWriteBytes( src, (off + len) - remain, limit - end );
+            remain = remain - (limit - end);
+        }
+
+        System.arraycopy(src, (off + len) - remain, buff, end, remain);
+        end += remain;
+
+    }
+
+
+    // -------------------- Removing data from the buffer --------------------
+
+    public int substract()
+            throws IOException {
+
+        if ((end - start) == 0) {
+            if (in == null) {
+                return -1;
+            }
+            int n = in.realReadBytes( buff, 0, buff.length );
+            if (n < 0) {
+                return -1;
+            }
+        }
+
+        return (buff[start++] & 0xFF);
+
+    }
+
+
+    /**
+     * @deprecated Unused. Will be removed in Tomcat 8.0.x onwards.
+     */
+    @Deprecated
+    public int substract(ByteChunk src)
+            throws IOException {
+
+        if ((end - start) == 0) {
+            if (in == null) {
+                return -1;
+            }
+            int n = in.realReadBytes( buff, 0, buff.length );
+            if (n < 0) {
+                return -1;
+            }
+        }
+
+        int len = getLength();
+        src.append(buff, start, len);
+        start = end;
+        return len;
+
+    }
+
+
+    public byte substractB()
+            throws IOException {
+
+        if ((end - start) == 0) {
+            if (in == null)
+                return -1;
+            int n = in.realReadBytes( buff, 0, buff.length );
+            if (n < 0)
+                return -1;
+        }
+
+        return (buff[start++]);
+
+    }
+
+
+    public int substract( byte src[], int off, int len )
+            throws IOException {
+
+        if ((end - start) == 0) {
+            if (in == null) {
+                return -1;
+            }
+            int n = in.realReadBytes( buff, 0, buff.length );
+            if (n < 0) {
+                return -1;
+            }
+        }
+
+        int n = len;
+        if (len > getLength()) {
+            n = getLength();
+        }
+        System.arraycopy(buff, start, src, off, n);
+        start += n;
+        return n;
+
+    }
+
+
+    /**
+     * Send the buffer to the sink. Called by append() when the limit is
+     * reached. You can also call it explicitly to force the data to be written.
+     *
+     * @throws IOException
+     */
+    public void flushBuffer()
+            throws IOException
+    {
+        //assert out!=null
+        if( out==null ) {
+            throw new IOException( "Buffer overflow, no sink " + limit + " " +
+                    buff.length  );
+        }
+        out.realWriteBytes( buff, start, end-start );
+        end=start;
+    }
+
+    /**
+     * Make space for len chars. If len is small, allocate a reserve space too.
+     * Never grow bigger than limit.
+     */
+    public void makeSpace(int count) {
+        byte[] tmp = null;
+
+        int newSize;
+        int desiredSize=end + count;
+
+        // Can't grow above the limit
+        if( limit > 0 &&
+                desiredSize > limit) {
+            desiredSize=limit;
+        }
+
+        if( buff==null ) {
+            if( desiredSize < 256 )
+            {
+                desiredSize=256; // take a minimum
+            }
+            buff=new byte[desiredSize];
+        }
+
+        // limit < buf.length ( the buffer is already big )
+        // or we already have space XXX
+        if( desiredSize <= buff.length ) {
+            return;
+        }
+        // grow in larger chunks
+        if( desiredSize < 2 * buff.length ) {
+            newSize= buff.length * 2;
+            if( limit >0 &&
+                    newSize > limit ) {
+                newSize=limit;
+            }
+            tmp=new byte[newSize];
+        } else {
+            newSize= buff.length * 2 + count ;
+            if( limit > 0 &&
+                    newSize > limit ) {
+                newSize=limit;
+            }
+            tmp=new byte[newSize];
+        }
+
+        System.arraycopy(buff, start, tmp, 0, end-start);
+        buff = tmp;
+        tmp = null;
+        end=end-start;
+        start=0;
+    }
+
+    // -------------------- Conversion and getters --------------------
+
+    @Override
+    public String toString() {
+        if (null == buff) {
+            return null;
+        } else if (end-start == 0) {
+            return "";
+        }
+        return StringCache.toString(this);
+    }
+
+    public String toStringInternal() {
+        if (charset == null) {
+            charset = DEFAULT_CHARSET;
+        }
+        // new String(byte[], int, int, Charset) takes a defensive copy of the
+        // entire byte array. This is expensive if only a small subset of the
+        // bytes will be used. The code below is from Apache Harmony.
+        CharBuffer cb;
+        cb = charset.decode(ByteBuffer.wrap(buff, start, end-start));
+        return new String(cb.array(), cb.arrayOffset(), cb.length());
+    }
+
+    /**
+     * @deprecated Unused. Will be removed in Tomcat 8.0.x onwards.
+     */
+    @Deprecated
+    public int getInt()
+    {
+        return Ascii.parseInt(buff, start,end-start);
+    }
+
+    public long getLong() {
+        return Ascii.parseLong(buff, start,end-start);
+    }
+
+
+    // -------------------- equals --------------------
+
+    /**
+     * 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 (Ascii.toLower(b[boff++]) != Ascii.toLower(s.charAt(i))) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    public boolean equals( ByteChunk bb ) {
+        return equals( bb.getBytes(), 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( CharChunk cc ) {
+        return equals( cc.getChars(), cc.getStart(), cc.getLength());
+    }
+
+    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;
+    }
+
+    /**
+     * Returns true if the message bytes starts with the specified string.
+     * @param s the string
+     * @deprecated Unused. Will be removed in Tomcat 8.0.x onwards.
+     */
+    @Deprecated
+    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.
+     * @deprecated Unused. Will be removed in Tomcat 8.0.x onwards.
+     */
+    @Deprecated
+    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 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;
+
+        mainLoop:
+        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++ )) {
+                    continue mainLoop;
+                }
+            }
+            return i-start; // found it
+        }
+        return -1;
+    }
+
+    // -------------------- Hash code  --------------------
+
+    // normal hash.
+    public int hash() {
+        return hashBytes( buff, start, end-start);
+    }
+
+    /**
+     * @deprecated Unused. Will be removed in Tomcat 8.0.x onwards.
+     */
+    @Deprecated
+    public int hashIgnoreCase() {
+        return hashBytesIC( buff, start, end-start );
+    }
+
+    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 * 37 + bb[i];
+        }
+        return code;
+    }
+
+    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;
+    }
+
+    /**
+     * Returns the first instance of the given character in this ByteChunk
+     * starting at the specified byte. If the character is not found, -1 is
+     * returned.
+     * <br/>
+     * NOTE: This only works for characters in the range 0-127.
+     *
+     * @param c         The character
+     * @param starting  The start position
+     * @return          The position of the first instance of the character or
+     *                      -1 if the character is not found.
+     */
+    public int indexOf(char c, int starting) {
+        int ret = indexOf(buff, start + starting, end, c);
+        return (ret >= start) ? ret - start : -1;
+    }
+
+    /**
+     * Returns the first instance of the given character in the given byte array
+     * between the specified start and end.
+     * <br/>
+     * NOTE: This only works for characters in the range 0-127.
+     *
+     * @param bytes The byte array to search
+     * @param start The point to start searching from in the byte array
+     * @param end   The point to stop searching in the byte array
+     * @param c     The character to search for
+     * @return      The position of the first instance of the character or -1
+     *                  if the character is not found.
+     */
+    public static int indexOf(byte bytes[], int start, int end, char c) {
+        int offset = start;
+
+        while (offset < end) {
+            byte b=bytes[offset];
+            if (b == c) {
+                return offset;
+            }
+            offset++;
+        }
+        return -1;
+    }
+
+    /**
+     * Returns the first instance of the given byte in the byte array between
+     * the specified start and end.
+     *
+     * @param bytes The byte array to search
+     * @param start The point to start searching from in the byte array
+     * @param end   The point to stop searching in the byte array
+     * @param b     The byte to search for
+     * @return      The position of the first instance of the byte or -1 if the
+     *                  byte is not found.
+     */
+    public static int findByte(byte bytes[], int start, int end, byte b) {
+        int offset = start;
+        while (offset < end) {
+            if (bytes[offset] == b) {
+                return offset;
+            }
+            offset++;
+        }
+        return -1;
+    }
+
+    /**
+     * Returns the first instance of any of the given bytes in the byte array
+     * between the specified start and end.
+     *
+     * @param bytes The byte array to search
+     * @param start The point to start searching from in the byte array
+     * @param end   The point to stop searching in the byte array
+     * @param b     The array of bytes to search for
+     * @return      The position of the first instance of the byte or -1 if the
+     *                  byte is not found.
+     */
+    public static int findBytes(byte bytes[], int start, int end, byte b[]) {
+        int blen = b.length;
+        int offset = start;
+        while (offset < end) {
+            for (int i = 0;  i < blen; i++) {
+                if (bytes[offset] == b[i]) {
+                    return offset;
+                }
+            }
+            offset++;
+        }
+        return -1;
+    }
+
+    /**
+     * Returns the first instance of any byte that is not one of the given bytes
+     * in the byte array between the specified start and end.
+     *
+     * @param bytes The byte array to search
+     * @param start The point to start searching from in the byte array
+     * @param end   The point to stop searching in the byte array
+     * @param b     The list of bytes to search for
+     * @return      The position of the first instance a byte that is not
+     *                  in the list of bytes to search for or -1 if no such byte
+     *                  is found.
+     * @deprecated Unused. Will be removed in Tomcat 8.0.x onwards.
+     */
+    @Deprecated
+    public static int findNotBytes(byte bytes[], int start, int end, byte b[]) {
+        int blen = b.length;
+        int offset = start;
+        boolean found;
+
+        while (offset < end) {
+            found = true;
+            for (int i = 0; i < blen; i++) {
+                if (bytes[offset] == b[i]) {
+                    found=false;
+                    break;
+                }
+            }
+            if (found) {
+                return offset;
+            }
+            offset++;
+        }
+        return -1;
+    }
+
+
+    /**
+     * 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;
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/struts/blob/5421930b/core/src/main/java/org/apache/struts2/util/tomcat/buf/CharChunk.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/struts2/util/tomcat/buf/CharChunk.java b/core/src/main/java/org/apache/struts2/util/tomcat/buf/CharChunk.java
new file mode 100644
index 0000000..527707a
--- /dev/null
+++ b/core/src/main/java/org/apache/struts2/util/tomcat/buf/CharChunk.java
@@ -0,0 +1,700 @@
+/*
+ *  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.struts2.util.tomcat.buf;
+
+import java.io.IOException;
+import java.io.Serializable;
+
+/**
+ * Utilities to manipulate 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 immutable and secure objects.
+ *
+ * @author dac@sun.com
+ * @author James Todd [gonzo@sun.com]
+ * @author Costin Manolache
+ * @author Remy Maucherat
+ */
+public final class CharChunk implements Cloneable, Serializable, CharSequence {
+
+    private static final long serialVersionUID = 1L;
+
+    // Input interface, used when the buffer is emptied.
+    public static interface CharInputChannel {
+        /**
+         * Read new bytes ( usually the internal conversion buffer ).
+         * The implementation is allowed to ignore the parameters,
+         * and mutate the chunk if it wishes to implement its own buffering.
+         */
+        public int realReadChars(char cbuf[], int off, int len)
+            throws IOException;
+    }
+    /**
+     *  When we need more space we'll either
+     *  grow the buffer ( up to the limit ) or send it to a channel.
+     */
+    public static interface CharOutputChannel {
+        /** Send the bytes ( usually the internal conversion buffer ).
+         *  Expect 8k output if the buffer is full.
+         */
+        public void realWriteChars(char cbuf[], int off, int len)
+            throws IOException;
+    }
+
+    // --------------------
+
+    private int hashCode = 0;
+    // did we compute the hashcode ?
+    private boolean hasHashCode = false;
+
+    // char[]
+    private char buff[];
+
+    private int start;
+    private int end;
+
+    private boolean isSet=false;  // XXX
+
+    // -1: grow indefinitely
+    // maximum amount to be cached
+    private int limit=-1;
+
+    private CharInputChannel in = null;
+    private CharOutputChannel out = null;
+
+    private boolean optimizedWrite=true;
+
+    /**
+     * Creates a new, uninitialized CharChunk object.
+     */
+    public CharChunk() {
+    }
+
+    public CharChunk(int size) {
+        allocate( size, -1 );
+    }
+
+    // --------------------
+
+    public boolean isNull() {
+        if( end > 0 ) {
+            return false;
+        }
+        return !isSet; //XXX
+    }
+
+    /**
+     * Resets the message bytes to an uninitialized state.
+     */
+    public void recycle() {
+        //        buff=null;
+        isSet=false; // XXX
+        hasHashCode = false;
+        start=0;
+        end=0;
+    }
+
+    // -------------------- Setup --------------------
+
+    public void allocate( int initial, int limit  ) {
+        if( buff==null || buff.length < initial ) {
+            buff=new char[initial];
+        }
+        this.limit=limit;
+        start=0;
+        end=0;
+        isSet=true;
+        hasHashCode = false;
+    }
+
+
+    public void setOptimizedWrite(boolean optimizedWrite) {
+        this.optimizedWrite = optimizedWrite;
+    }
+
+    public void setChars( char[] c, int off, int len ) {
+        buff=c;
+        start=off;
+        end=start + len;
+        isSet=true;
+        hasHashCode = false;
+    }
+
+    /** Maximum amount of data in this buffer.
+     *
+     *  If -1 or not set, the buffer will grow indefinitely.
+     *  Can be smaller than the current buffer size ( which will not shrink ).
+     *  When the limit is reached, the buffer will be flushed ( if out is set )
+     *  or throw exception.
+     */
+    public void setLimit(int limit) {
+        this.limit=limit;
+    }
+
+    public int getLimit() {
+        return limit;
+    }
+
+    /**
+     * When the buffer is empty, read the data from the input channel.
+     */
+    public void setCharInputChannel(CharInputChannel in) {
+        this.in = in;
+    }
+
+    /** When the buffer is full, write the data to the output channel.
+     *         Also used when large amount of data is appended.
+     *
+     *  If not set, the buffer will grow to the limit.
+     */
+    public void setCharOutputChannel(CharOutputChannel out) {
+        this.out=out;
+    }
+
+    // compat
+    public char[] getChars()
+    {
+        return getBuffer();
+    }
+
+    public char[] getBuffer()
+    {
+        return buff;
+    }
+
+    /**
+     * Returns the start offset of the bytes.
+     * For output this is the end of the buffer.
+     */
+    public int getStart() {
+        return start;
+    }
+
+    public int getOffset() {
+        return start;
+    }
+
+    /**
+     * Returns the start offset of the bytes.
+     */
+    public void setOffset(int off) {
+        start=off;
+    }
+
+    /**
+     * Returns the length of the bytes.
+     */
+    public int getLength() {
+        return end-start;
+    }
+
+
+    public int getEnd() {
+        return end;
+    }
+
+    public void setEnd( int i ) {
+        end=i;
+    }
+
+    // -------------------- Adding data --------------------
+
+    public void append( char b )
+        throws IOException
+    {
+        makeSpace( 1 );
+
+        // couldn't make space
+        if( limit >0 && end >= limit ) {
+            flushBuffer();
+        }
+        buff[end++]=b;
+    }
+
+    public void append( CharChunk src )
+        throws IOException
+    {
+        append( src.getBuffer(), src.getOffset(), src.getLength());
+    }
+
+    /** Add data to the buffer
+     */
+    public void append( char src[], int off, int len )
+        throws IOException
+    {
+        // will grow, up to limit
+        makeSpace( len );
+
+        // if we don't have limit: makeSpace can grow as it wants
+        if( limit < 0 ) {
+            // assert: makeSpace made enough space
+            System.arraycopy( src, off, buff, end, len );
+            end+=len;
+            return;
+        }
+
+        // Optimize on a common case.
+        // If the source is going to fill up all the space in buffer, may
+        // as well write it directly to the output, and avoid an extra copy
+        if ( optimizedWrite && len == limit && end == start && out != null ) {
+            out.realWriteChars( src, off, len );
+            return;
+        }
+
+        // if we have limit and we're below
+        if( len <= limit - end ) {
+            // makeSpace will grow the buffer to the limit,
+            // so we have space
+            System.arraycopy( src, off, buff, end, len );
+
+            end+=len;
+            return;
+        }
+
+        // need more space than we can afford, need to flush
+        // buffer
+
+        // the buffer is already at ( or bigger than ) limit
+
+        // Optimization:
+        // If len-avail < length ( i.e. after we fill the buffer with
+        // what we can, the remaining will fit in the buffer ) we'll just
+        // copy the first part, flush, then copy the second part - 1 write
+        // and still have some space for more. We'll still have 2 writes, but
+        // we write more on the first.
+
+        if( len + end < 2 * limit ) {
+            /* If the request length exceeds the size of the output buffer,
+               flush the output buffer and then write the data directly.
+               We can't avoid 2 writes, but we can write more on the second
+            */
+            int avail=limit-end;
+            System.arraycopy(src, off, buff, end, avail);
+            end += avail;
+
+            flushBuffer();
+
+            System.arraycopy(src, off+avail, buff, end, len - avail);
+            end+= len - avail;
+
+        } else {        // len > buf.length + avail
+            // long write - flush the buffer and write the rest
+            // directly from source
+            flushBuffer();
+
+            out.realWriteChars( src, off, len );
+        }
+    }
+
+
+    /** Append a string to the buffer
+     */
+    public void append(String s) throws IOException {
+        append(s, 0, s.length());
+    }
+
+    /** Append a string to the buffer
+     */
+    public void append(String s, int off, int len) throws IOException {
+        if (s==null) {
+            return;
+        }
+
+        // will grow, up to limit
+        makeSpace( len );
+
+        // if we don't have limit: makeSpace can grow as it wants
+        if( limit < 0 ) {
+            // assert: makeSpace made enough space
+            s.getChars(off, off+len, buff, end );
+            end+=len;
+            return;
+        }
+
+        int sOff = off;
+        int sEnd = off + len;
+        while (sOff < sEnd) {
+            int d = min(limit - end, sEnd - sOff);
+            s.getChars( sOff, sOff+d, buff, end);
+            sOff += d;
+            end += d;
+            if (end >= limit) {
+                flushBuffer();
+            }
+        }
+    }
+
+    // -------------------- Removing data from the buffer --------------------
+
+    public int substract()
+        throws IOException {
+
+        if ((end - start) == 0) {
+            if (in == null) {
+                return -1;
+            }
+            int n = in.realReadChars(buff, end, buff.length - end);
+            if (n < 0) {
+                return -1;
+            }
+        }
+
+        return (buff[start++]);
+
+    }
+
+    public int substract( char src[], int off, int len )
+        throws IOException {
+
+        if ((end - start) == 0) {
+            if (in == null) {
+                return -1;
+            }
+            int n = in.realReadChars( buff, end, buff.length - end);
+            if (n < 0) {
+                return -1;
+            }
+        }
+
+        int n = len;
+        if (len > getLength()) {
+            n = getLength();
+        }
+        System.arraycopy(buff, start, src, off, n);
+        start += n;
+        return n;
+
+    }
+
+
+    public void flushBuffer()
+        throws IOException
+    {
+        //assert out!=null
+        if( out==null ) {
+            throw new IOException( "Buffer overflow, no sink " + limit + " " +
+                                   buff.length  );
+        }
+        out.realWriteChars( buff, start, end - start );
+        end=start;
+    }
+
+    /** Make space for len chars. If len is small, allocate
+     *        a reserve space too. Never grow bigger than limit.
+     */
+    public void makeSpace(int count)
+    {
+        char[] tmp = null;
+
+        int newSize;
+        int desiredSize=end + count;
+
+        // Can't grow above the limit
+        if( limit > 0 &&
+            desiredSize > limit) {
+            desiredSize=limit;
+        }
+
+        if( buff==null ) {
+            if( desiredSize < 256 )
+             {
+                desiredSize=256; // take a minimum
+            }
+            buff=new char[desiredSize];
+        }
+
+        // limit < buf.length ( the buffer is already big )
+        // or we already have space XXX
+        if( desiredSize <= buff.length) {
+            return;
+        }
+        // grow in larger chunks
+        if( desiredSize < 2 * buff.length ) {
+            newSize= buff.length * 2;
+            if( limit >0 &&
+                newSize > limit ) {
+                newSize=limit;
+            }
+            tmp=new char[newSize];
+        } else {
+            newSize= buff.length * 2 + count ;
+            if( limit > 0 &&
+                newSize > limit ) {
+                newSize=limit;
+            }
+            tmp=new char[newSize];
+        }
+
+        System.arraycopy(buff, 0, tmp, 0, end);
+        buff = tmp;
+        tmp = null;
+    }
+
+    // -------------------- Conversion and getters --------------------
+
+    @Override
+    public String toString() {
+        if (null == buff) {
+            return null;
+        } else if (end-start == 0) {
+            return "";
+        }
+        return StringCache.toString(this);
+    }
+
+    public String toStringInternal() {
+        return new String(buff, start, end-start);
+    }
+
+    // -------------------- equals --------------------
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj instanceof CharChunk) {
+            return equals((CharChunk) obj);
+        }
+        return false;
+    }
+
+    /**
+     * 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 = buff;
+        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 = buff;
+        int len = end-start;
+        if (c == null || len != s.length()) {
+            return false;
+        }
+        int off = start;
+        for (int i = 0; i < len; i++) {
+            if (Ascii.toLower( c[off++] ) != Ascii.toLower( s.charAt(i))) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    public boolean equals(CharChunk cc) {
+        return equals( cc.getChars(), cc.getOffset(), cc.getLength());
+    }
+
+    public boolean equals(char b2[], int off2, int len2) {
+        char b1[]=buff;
+        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;
+    }
+
+    /**
+     * Returns true if the message bytes starts with the specified string.
+     * @param s the string
+     */
+    public boolean startsWith(String s) {
+        char[] c = buff;
+        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 = buff;
+        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 (Ascii.toLower( c[off++] ) != Ascii.toLower( s.charAt(i))) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+
+    /**
+     * Returns true if the message bytes end with the specified string.
+     * @param s the string
+     */
+    public boolean endsWith(String s) {
+        char[] c = buff;
+        int len = s.length();
+        if (c == null || len > end-start) {
+            return false;
+        }
+        int off = end - len;
+        for (int i = 0; i < len; i++) {
+            if (c[off++] != s.charAt(i)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    // -------------------- Hash code  --------------------
+
+    @Override
+    public int hashCode() {
+        if (hasHashCode) {
+            return hashCode;
+        }
+        int code = 0;
+
+        code = hash();
+        hashCode = code;
+        hasHashCode = true;
+        return code;
+    }
+
+    // normal hash.
+    public int hash() {
+        int code=0;
+        for (int i = start; i < start + end-start; i++) {
+            code = code * 37 + buff[i];
+        }
+        return code;
+    }
+
+    public int indexOf(char c) {
+        return indexOf( c, start);
+    }
+
+    /**
+     * 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( buff, 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, 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;
+    }
+
+    // -------------------- utils
+    private int min(int a, int b) {
+        if (a < b) {
+            return a;
+        }
+        return b;
+    }
+
+    // Char sequence impl
+
+    public char charAt(int index) {
+        return buff[index + start];
+    }
+
+    public CharSequence subSequence(int start, int end) {
+        try {
+            CharChunk result = (CharChunk) this.clone();
+            result.setOffset(this.start + start);
+            result.setEnd(this.start + end);
+            return result;
+        } catch (CloneNotSupportedException e) {
+            // Cannot happen
+            return null;
+        }
+    }
+
+    public int length() {
+        return end - start;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/struts/blob/5421930b/core/src/main/java/org/apache/struts2/util/tomcat/buf/HexUtils.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/struts2/util/tomcat/buf/HexUtils.java b/core/src/main/java/org/apache/struts2/util/tomcat/buf/HexUtils.java
new file mode 100644
index 0000000..0b9a116
--- /dev/null
+++ b/core/src/main/java/org/apache/struts2/util/tomcat/buf/HexUtils.java
@@ -0,0 +1,113 @@
+/*
+ *  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.struts2.util.tomcat.buf;
+
+/**
+ * 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 HexUtils {
+
+    // -------------------------------------------------------------- Constants
+
+    /**
+     *  Table for HEX to DEC byte translation.
+     */
+    private static final int[] DEC = {
+        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,
+    };
+
+
+    /**
+     * Table for DEC to HEX byte translation.
+     */
+    private 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' };
+
+
+    /**
+     * Table for byte to hex string translation.
+     */
+    private static final char[] hex = "0123456789abcdef".toCharArray();
+
+
+    // --------------------------------------------------------- Static Methods
+
+    public static int getDec(int index) {
+        // Fast for correct values, slower for incorrect ones
+        try {
+            return DEC[index - '0'];
+        } catch (ArrayIndexOutOfBoundsException ex) {
+            return -1;
+        }
+    }
+
+
+    public static byte getHex(int index) {
+        return HEX[index];
+    }
+
+
+    public static String toHexString(byte[] bytes) {
+        if (null == bytes) {
+            return null;
+        }
+
+        StringBuilder sb = new StringBuilder(bytes.length << 1);
+
+        for(int i = 0; i < bytes.length; ++i) {
+            sb.append(hex[(bytes[i] & 0xf0) >> 4])
+                .append(hex[(bytes[i] & 0x0f)])
+                ;
+        }
+
+        return sb.toString();
+    }
+
+
+    public static byte[] fromHexString(String input) {
+        if (input == null) {
+            return null;
+        }
+
+        if ((input.length() & 1) == 1) {
+            // Odd number of characters
+            throw new IllegalArgumentException("The input must consist of an even number of hex digits");
+        }
+
+        char[] inputChars = input.toCharArray();
+        byte[] result = new byte[input.length() >> 1];
+        for (int i = 0; i < result.length; i++) {
+            int upperNibble = getDec(inputChars[2*i]);
+            int lowerNibble =  getDec(inputChars[2*i + 1]);
+            if (upperNibble < 0 || lowerNibble < 0) {
+                // Non hex character
+                throw new IllegalArgumentException("The input must consist only of hex digits");
+            }
+            result[i] = (byte) ((upperNibble << 4) + lowerNibble);
+        }
+        return result;
+    }
+}