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:25 UTC

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

Repository: struts
Updated Branches:
  refs/heads/support-2-3 c6750c110 -> 5421930b4


http://git-wip-us.apache.org/repos/asf/struts/blob/5421930b/core/src/main/java/org/apache/struts2/util/tomcat/buf/MessageBytes.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/struts2/util/tomcat/buf/MessageBytes.java b/core/src/main/java/org/apache/struts2/util/tomcat/buf/MessageBytes.java
new file mode 100644
index 0000000..df07284
--- /dev/null
+++ b/core/src/main/java/org/apache/struts2/util/tomcat/buf/MessageBytes.java
@@ -0,0 +1,546 @@
+/*
+ *  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.charset.Charset;
+import java.util.Locale;
+
+/**
+ * This class is used to represent a subarray of bytes in an HTTP message.
+ * It represents all request/response elements. The byte/char conversions are
+ * delayed and cached. Everything is recyclable.
+ *
+ * The object can represent a byte[], a char[], or a (sub) String. All
+ * operations can be made in case sensitive mode or not.
+ *
+ * @author dac@eng.sun.com
+ * @author James Todd [gonzo@eng.sun.com]
+ * @author Costin Manolache
+ */
+public final class MessageBytes implements Cloneable, Serializable {
+    private static final long serialVersionUID = 1L;
+
+    // primary type ( whatever is set as original value )
+    private int type = T_NULL;
+
+    public static final int T_NULL = 0;
+    /** getType() is T_STR if the the object used to create the MessageBytes
+        was a String */
+    public static final int T_STR  = 1;
+    /** getType() is T_STR if the the object used to create the MessageBytes
+        was a byte[] */
+    public static final int T_BYTES = 2;
+    /** getType() is T_STR if the the object used to create the MessageBytes
+        was a char[] */
+    public static final int T_CHARS = 3;
+
+    private int hashCode=0;
+    // did we compute the hashcode ?
+    private boolean hasHashCode=false;
+
+    // Internal objects to represent array + offset, and specific methods
+    private final ByteChunk byteC=new ByteChunk();
+    private final CharChunk charC=new CharChunk();
+
+    // String
+    private String strValue;
+    // true if a String value was computed. Probably not needed,
+    // strValue!=null is the same
+    private boolean hasStrValue=false;
+
+    /**
+     * Creates a new, uninitialized MessageBytes object.
+     * Use static newInstance() in order to allow
+     *   future hooks.
+     */
+    private MessageBytes() {
+    }
+
+    /** Construct a new MessageBytes instance
+     */
+    public static MessageBytes newInstance() {
+        return factory.newInstance();
+    }
+
+    public boolean isNull() {
+        // should we check also hasStrValue ???
+        return byteC.isNull() && charC.isNull() && ! hasStrValue;
+        // bytes==null && strValue==null;
+    }
+
+    /**
+     * Resets the message bytes to an uninitialized (NULL) state.
+     */
+    public void recycle() {
+        type=T_NULL;
+        byteC.recycle();
+        charC.recycle();
+
+        strValue=null;
+
+        hasStrValue=false;
+        hasHashCode=false;
+        hasLongValue=false;
+    }
+
+
+    /**
+     * Sets the content to the specified subarray of bytes.
+     *
+     * @param b the 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) {
+        byteC.setBytes( b, off, len );
+        type=T_BYTES;
+        hasStrValue=false;
+        hasHashCode=false;
+        hasLongValue=false;
+    }
+
+    /**
+     * Sets the content to be a char[]
+     *
+     * @param c the bytes
+     * @param off the start offset of the bytes
+     * @param len the length of the bytes
+     */
+    public void setChars( char[] c, int off, int len ) {
+        charC.setChars( c, off, len );
+        type=T_CHARS;
+        hasStrValue=false;
+        hasHashCode=false;
+        hasLongValue=false;
+    }
+
+    /**
+     * Set the content to be a string
+     */
+    public void setString( String s ) {
+        strValue=s;
+        hasHashCode=false;
+        hasLongValue=false;
+        if (s == null) {
+            hasStrValue=false;
+            type=T_NULL;
+        } else {
+            hasStrValue=true;
+            type=T_STR;
+        }
+    }
+
+    // -------------------- Conversion and getters --------------------
+
+    /** Compute the string value
+     */
+    @Override
+    public String toString() {
+        if( hasStrValue ) {
+            return strValue;
+        }
+
+        switch (type) {
+        case T_CHARS:
+            strValue=charC.toString();
+            hasStrValue=true;
+            return strValue;
+        case T_BYTES:
+            strValue=byteC.toString();
+            hasStrValue=true;
+            return strValue;
+        }
+        return null;
+    }
+
+    //----------------------------------------
+    /** Return the type of the original content. Can be
+     *  T_STR, T_BYTES, T_CHARS or T_NULL
+     */
+    public int getType() {
+        return type;
+    }
+
+    /**
+     * Returns the byte chunk, representing the byte[] and offset/length.
+     * Valid only if T_BYTES or after a conversion was made.
+     */
+    public ByteChunk getByteChunk() {
+        return byteC;
+    }
+
+    /**
+     * Returns the char chunk, representing the char[] and offset/length.
+     * Valid only if T_CHARS or after a conversion was made.
+     */
+    public CharChunk getCharChunk() {
+        return charC;
+    }
+
+    /**
+     * Returns the string value.
+     * Valid only if T_STR or after a conversion was made.
+     */
+    public String getString() {
+        return strValue;
+    }
+
+    /**
+     * Get the Charset used for string<->byte conversions.
+     */
+    public Charset getCharset() {
+        return byteC.getCharset();
+    }
+
+    /**
+     * Set the Charset used for string<->byte conversions.
+     */
+    public void setCharset(Charset charset) {
+        byteC.setCharset(charset);
+    }
+
+    /** Do a char->byte conversion.
+     */
+    public void toBytes() {
+        if (!byteC.isNull()) {
+            type=T_BYTES;
+            return;
+        }
+        toString();
+        type=T_BYTES;
+        Charset charset = byteC.getCharset();
+        ByteBuffer result = charset.encode(strValue);
+        byteC.setBytes(result.array(), result.arrayOffset(), result.limit());
+    }
+
+    /** Convert to char[] and fill the CharChunk.
+     *  XXX Not optimized - it converts to String first.
+     */
+    public void toChars() {
+        if( ! charC.isNull() ) {
+            type=T_CHARS;
+            return;
+        }
+        // inefficient
+        toString();
+        type=T_CHARS;
+        char cc[]=strValue.toCharArray();
+        charC.setChars(cc, 0, cc.length);
+    }
+
+
+    /**
+     * Returns the length of the original buffer.
+     * Note that the length in bytes may be different from the length
+     * in chars.
+     */
+    public int getLength() {
+        if(type==T_BYTES) {
+            return byteC.getLength();
+        }
+        if(type==T_CHARS) {
+            return charC.getLength();
+        }
+        if(type==T_STR) {
+            return strValue.length();
+        }
+        toString();
+        if( strValue==null ) {
+            return 0;
+        }
+        return strValue.length();
+    }
+
+    // -------------------- 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) {
+        switch (type) {
+        case T_STR:
+            if (strValue == null) {
+                return s == null;
+            }
+            return strValue.equals( s );
+        case T_CHARS:
+            return charC.equals( s );
+        case T_BYTES:
+            return byteC.equals( s );
+        default:
+            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 equalsIgnoreCase(String s) {
+        switch (type) {
+        case T_STR:
+            if (strValue == null) {
+                return s == null;
+            }
+            return strValue.equalsIgnoreCase( s );
+        case T_CHARS:
+            return charC.equalsIgnoreCase( s );
+        case T_BYTES:
+            return byteC.equalsIgnoreCase( s );
+        default:
+            return false;
+        }
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj instanceof MessageBytes) {
+            return equals((MessageBytes) obj);
+        }
+        return false;
+    }
+
+    public boolean equals(MessageBytes mb) {
+        switch (type) {
+        case T_STR:
+            return mb.equals( strValue );
+        }
+
+        if( mb.type != T_CHARS &&
+            mb.type!= T_BYTES ) {
+            // it's a string or int/date string value
+            return equals( mb.toString() );
+        }
+
+        // mb is either CHARS or BYTES.
+        // this is either CHARS or BYTES
+        // Deal with the 4 cases ( in fact 3, one is symmetric)
+
+        if( mb.type == T_CHARS && type==T_CHARS ) {
+            return charC.equals( mb.charC );
+        }
+        if( mb.type==T_BYTES && type== T_BYTES ) {
+            return byteC.equals( mb.byteC );
+        }
+        if( mb.type== T_CHARS && type== T_BYTES ) {
+            return byteC.equals( mb.charC );
+        }
+        if( mb.type== T_BYTES && type== T_CHARS ) {
+            return mb.byteC.equals( charC );
+        }
+        // can't happen
+        return true;
+    }
+
+
+    /**
+     * Returns true if the message bytes starts with the specified string.
+     * @param s the string
+     * @param pos The start position
+     */
+    public boolean startsWithIgnoreCase(String s, int pos) {
+        switch (type) {
+        case T_STR:
+            if( strValue==null ) {
+                return false;
+            }
+            if( strValue.length() < pos + s.length() ) {
+                return false;
+            }
+
+            for( int i=0; i<s.length(); i++ ) {
+                if( Ascii.toLower( s.charAt( i ) ) !=
+                    Ascii.toLower( strValue.charAt( pos + i ))) {
+                    return false;
+                }
+            }
+            return true;
+        case T_CHARS:
+            return charC.startsWithIgnoreCase( s, pos );
+        case T_BYTES:
+            return byteC.startsWithIgnoreCase( s, pos );
+        default:
+            return false;
+        }
+    }
+
+
+    // -------------------- Hash code  --------------------
+    @Override
+    public  int hashCode() {
+        if( hasHashCode ) {
+            return hashCode;
+        }
+        int code = 0;
+
+        code=hash();
+        hashCode=code;
+        hasHashCode=true;
+        return code;
+    }
+
+    // normal hash.
+    private int hash() {
+        int code=0;
+        switch (type) {
+        case T_STR:
+            // We need to use the same hash function
+            for (int i = 0; i < strValue.length(); i++) {
+                code = code * 37 + strValue.charAt( i );
+            }
+            return code;
+        case T_CHARS:
+            return charC.hash();
+        case T_BYTES:
+            return byteC.hash();
+        default:
+            return 0;
+        }
+    }
+
+    // Inefficient initial implementation. Will be replaced on the next
+    // round of tune-up
+    public int indexOf(String s, int starting) {
+        toString();
+        return strValue.indexOf( s, starting );
+    }
+
+    // Inefficient initial implementation. Will be replaced on the next
+    // round of tune-up
+    public int indexOf(String s) {
+        return indexOf( s, 0 );
+    }
+
+    public int indexOfIgnoreCase(String s, int starting) {
+        toString();
+        String upper=strValue.toUpperCase(Locale.ENGLISH);
+        String sU=s.toUpperCase(Locale.ENGLISH);
+        return upper.indexOf( sU, starting );
+    }
+
+    /** Copy the src into this MessageBytes, allocating more space if
+     *  needed
+     */
+    public void duplicate( MessageBytes src ) throws IOException
+    {
+        switch( src.getType() ) {
+        case MessageBytes.T_BYTES:
+            type=T_BYTES;
+            ByteChunk bc=src.getByteChunk();
+            byteC.allocate( 2 * bc.getLength(), -1 );
+            byteC.append( bc );
+            break;
+        case MessageBytes.T_CHARS:
+            type=T_CHARS;
+            CharChunk cc=src.getCharChunk();
+            charC.allocate( 2 * cc.getLength(), -1 );
+            charC.append( cc );
+            break;
+        case MessageBytes.T_STR:
+            type=T_STR;
+            String sc=src.getString();
+            this.setString( sc );
+            break;
+        }
+    }
+
+    // -------------------- Deprecated code --------------------
+    // efficient long
+    // XXX used only for headers - shouldn't be stored here.
+    private long longValue;
+    private boolean hasLongValue=false;
+
+    /** Set the buffer to the representation of an long
+     */
+    public void setLong(long l) {
+        byteC.allocate(32, 64);
+        long current = l;
+        byte[] buf = byteC.getBuffer();
+        int start = 0;
+        int end = 0;
+        if (l == 0) {
+            buf[end++] = (byte) '0';
+        }
+        if (l < 0) {
+            current = -l;
+            buf[end++] = (byte) '-';
+        }
+        while (current > 0) {
+            int digit = (int) (current % 10);
+            current = current / 10;
+            buf[end++] = HexUtils.getHex(digit);
+        }
+        byteC.setOffset(0);
+        byteC.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--;
+        }
+        longValue=l;
+        hasStrValue=false;
+        hasHashCode=false;
+        hasLongValue=true;
+        type=T_BYTES;
+    }
+
+    // Used for headers conversion
+    /** Convert the buffer to an long, cache the value
+     */
+    public long getLong() {
+        if( hasLongValue ) {
+            return longValue;
+        }
+
+        switch (type) {
+        case T_BYTES:
+            longValue=byteC.getLong();
+            break;
+        default:
+            longValue= Long.parseLong(toString());
+        }
+
+        hasLongValue=true;
+        return longValue;
+
+     }
+
+    // -------------------- Future may be different --------------------
+
+    private static final MessageBytesFactory factory=new MessageBytesFactory();
+
+    private static class MessageBytesFactory {
+        protected MessageBytesFactory() {
+        }
+        public MessageBytes newInstance() {
+            return new MessageBytes();
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/struts/blob/5421930b/core/src/main/java/org/apache/struts2/util/tomcat/buf/StringCache.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/struts2/util/tomcat/buf/StringCache.java b/core/src/main/java/org/apache/struts2/util/tomcat/buf/StringCache.java
new file mode 100644
index 0000000..3a72d49
--- /dev/null
+++ b/core/src/main/java/org/apache/struts2/util/tomcat/buf/StringCache.java
@@ -0,0 +1,695 @@
+/*
+ *  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 com.opensymphony.xwork2.util.logging.Logger;
+import com.opensymphony.xwork2.util.logging.LoggerFactory;
+
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map.Entry;
+import java.util.TreeMap;
+
+/**
+ * This class implements a String cache for ByteChunk and CharChunk.
+ *
+ * @author Remy Maucherat
+ */
+public class StringCache {
+
+
+    private static final Logger log = LoggerFactory.getLogger(StringCache.class);
+
+
+    // ------------------------------------------------------- Static Variables
+
+
+    /**
+     * Enabled ?
+     */
+    protected static boolean byteEnabled = ("true".equals(System.getProperty(
+            "tomcat.util.buf.StringCache.byte.enabled", "false")));
+
+
+    protected static boolean charEnabled = ("true".equals(System.getProperty(
+            "tomcat.util.buf.StringCache.char.enabled", "false")));
+
+
+    protected static int trainThreshold = Integer.parseInt(System.getProperty(
+            "tomcat.util.buf.StringCache.trainThreshold", "20000"));
+
+
+    protected static int cacheSize = Integer.parseInt(System.getProperty(
+            "tomcat.util.buf.StringCache.cacheSize", "200"));
+
+
+    protected static final int maxStringSize =
+            Integer.parseInt(System.getProperty(
+                    "tomcat.util.buf.StringCache.maxStringSize", "128"));
+
+
+   /**
+     * Statistics hash map for byte chunk.
+     */
+    protected static final HashMap<ByteEntry,int[]> bcStats =
+            new HashMap<ByteEntry, int[]>(cacheSize);
+
+
+    /**
+     * toString count for byte chunk.
+     */
+    protected static int bcCount = 0;
+
+
+    /**
+     * Cache for byte chunk.
+     */
+    protected static ByteEntry[] bcCache = null;
+
+
+    /**
+     * Statistics hash map for char chunk.
+     */
+    protected static final HashMap<CharEntry,int[]> ccStats =
+            new HashMap<CharEntry, int[]>(cacheSize);
+
+
+    /**
+     * toString count for char chunk.
+     */
+    protected static int ccCount = 0;
+
+
+    /**
+     * Cache for char chunk.
+     */
+    protected static CharEntry[] ccCache = null;
+
+
+    /**
+     * Access count.
+     */
+    protected static int accessCount = 0;
+
+
+    /**
+     * Hit count.
+     */
+    protected static int hitCount = 0;
+
+
+    // ------------------------------------------------------------ Properties
+
+
+    /**
+     * @return Returns the cacheSize.
+     */
+    public int getCacheSize() {
+        return cacheSize;
+    }
+
+
+    /**
+     * @param cacheSize The cacheSize to set.
+     */
+    public void setCacheSize(int cacheSize) {
+        StringCache.cacheSize = cacheSize;
+    }
+
+
+    /**
+     * @return Returns the enabled.
+     */
+    public boolean getByteEnabled() {
+        return byteEnabled;
+    }
+
+
+    /**
+     * @param byteEnabled The enabled to set.
+     */
+    public void setByteEnabled(boolean byteEnabled) {
+        StringCache.byteEnabled = byteEnabled;
+    }
+
+
+    /**
+     * @return Returns the enabled.
+     */
+    public boolean getCharEnabled() {
+        return charEnabled;
+    }
+
+
+    /**
+     * @param charEnabled The enabled to set.
+     */
+    public void setCharEnabled(boolean charEnabled) {
+        StringCache.charEnabled = charEnabled;
+    }
+
+
+    /**
+     * @return Returns the trainThreshold.
+     */
+    public int getTrainThreshold() {
+        return trainThreshold;
+    }
+
+
+    /**
+     * @param trainThreshold The trainThreshold to set.
+     */
+    public void setTrainThreshold(int trainThreshold) {
+        StringCache.trainThreshold = trainThreshold;
+    }
+
+
+    /**
+     * @return Returns the accessCount.
+     */
+    public int getAccessCount() {
+        return accessCount;
+    }
+
+
+    /**
+     * @return Returns the hitCount.
+     */
+    public int getHitCount() {
+        return hitCount;
+    }
+
+
+    // -------------------------------------------------- Public Static Methods
+
+
+    public void reset() {
+        hitCount = 0;
+        accessCount = 0;
+        synchronized (bcStats) {
+            bcCache = null;
+            bcCount = 0;
+        }
+        synchronized (ccStats) {
+            ccCache = null;
+            ccCount = 0;
+        }
+    }
+
+
+    public static String toString(ByteChunk bc) {
+
+        // If the cache is null, then either caching is disabled, or we're
+        // still training
+        if (bcCache == null) {
+            String value = bc.toStringInternal();
+            if (byteEnabled && (value.length() < maxStringSize)) {
+                // If training, everything is synced
+                synchronized (bcStats) {
+                    // If the cache has been generated on a previous invocation
+                    // while waiting for the lock, just return the toString
+                    // value we just calculated
+                    if (bcCache != null) {
+                        return value;
+                    }
+                    // Two cases: either we just exceeded the train count, in
+                    // which case the cache must be created, or we just update
+                    // the count for the string
+                    if (bcCount > trainThreshold) {
+                        long t1 = System.currentTimeMillis();
+                        // Sort the entries according to occurrence
+                        TreeMap<Integer,ArrayList<ByteEntry>> tempMap =
+                                new TreeMap<Integer, ArrayList<ByteEntry>>();
+                        for (Entry<ByteEntry,int[]> item : bcStats.entrySet()) {
+                            ByteEntry entry = item.getKey();
+                            int[] countA = item.getValue();
+                            Integer count = Integer.valueOf(countA[0]);
+                            // Add to the list for that count
+                            ArrayList<ByteEntry> list = tempMap.get(count);
+                            if (list == null) {
+                                // Create list
+                                list = new ArrayList<ByteEntry>();
+                                tempMap.put(count, list);
+                            }
+                            list.add(entry);
+                        }
+                        // Allocate array of the right size
+                        int size = bcStats.size();
+                        if (size > cacheSize) {
+                            size = cacheSize;
+                        }
+                        ByteEntry[] tempbcCache = new ByteEntry[size];
+                        // Fill it up using an alphabetical order
+                        // and a dumb insert sort
+                        ByteChunk tempChunk = new ByteChunk();
+                        int n = 0;
+                        while (n < size) {
+                            Object key = tempMap.lastKey();
+                            ArrayList<ByteEntry> list = tempMap.get(key);
+                            for (int i = 0; i < list.size() && n < size; i++) {
+                                ByteEntry entry = list.get(i);
+                                tempChunk.setBytes(entry.name, 0,
+                                        entry.name.length);
+                                int insertPos = findClosest(tempChunk,
+                                        tempbcCache, n);
+                                if (insertPos == n) {
+                                    tempbcCache[n + 1] = entry;
+                                } else {
+                                    System.arraycopy(tempbcCache, insertPos + 1,
+                                            tempbcCache, insertPos + 2,
+                                            n - insertPos - 1);
+                                    tempbcCache[insertPos + 1] = entry;
+                                }
+                                n++;
+                            }
+                            tempMap.remove(key);
+                        }
+                        bcCount = 0;
+                        bcStats.clear();
+                        bcCache = tempbcCache;
+                        if (log.isDebugEnabled()) {
+                            long t2 = System.currentTimeMillis();
+                            log.debug("ByteCache generation time: " +
+                                    (t2 - t1) + "ms");
+                        }
+                    } else {
+                        bcCount++;
+                        // Allocate new ByteEntry for the lookup
+                        ByteEntry entry = new ByteEntry();
+                        entry.value = value;
+                        int[] count = bcStats.get(entry);
+                        if (count == null) {
+                            int end = bc.getEnd();
+                            int start = bc.getStart();
+                            // Create byte array and copy bytes
+                            entry.name = new byte[bc.getLength()];
+                            System.arraycopy(bc.getBuffer(), start, entry.name,
+                                    0, end - start);
+                            // Set encoding
+                            entry.charset = bc.getCharset();
+                            // Initialize occurrence count to one
+                            count = new int[1];
+                            count[0] = 1;
+                            // Set in the stats hash map
+                            bcStats.put(entry, count);
+                        } else {
+                            count[0] = count[0] + 1;
+                        }
+                    }
+                }
+            }
+            return value;
+        } else {
+            accessCount++;
+            // Find the corresponding String
+            String result = find(bc);
+            if (result == null) {
+                return bc.toStringInternal();
+            }
+            // Note: We don't care about safety for the stats
+            hitCount++;
+            return result;
+        }
+
+    }
+
+
+    public static String toString(CharChunk cc) {
+
+        // If the cache is null, then either caching is disabled, or we're
+        // still training
+        if (ccCache == null) {
+            String value = cc.toStringInternal();
+            if (charEnabled && (value.length() < maxStringSize)) {
+                // If training, everything is synced
+                synchronized (ccStats) {
+                    // If the cache has been generated on a previous invocation
+                    // while waiting for the lock, just return the toString
+                    // value we just calculated
+                    if (ccCache != null) {
+                        return value;
+                    }
+                    // Two cases: either we just exceeded the train count, in
+                    // which case the cache must be created, or we just update
+                    // the count for the string
+                    if (ccCount > trainThreshold) {
+                        long t1 = System.currentTimeMillis();
+                        // Sort the entries according to occurrence
+                        TreeMap<Integer,ArrayList<CharEntry>> tempMap =
+                                new TreeMap<Integer, ArrayList<CharEntry>>();
+                        for (Entry<CharEntry,int[]> item : ccStats.entrySet()) {
+                            CharEntry entry = item.getKey();
+                            int[] countA = item.getValue();
+                            Integer count = Integer.valueOf(countA[0]);
+                            // Add to the list for that count
+                            ArrayList<CharEntry> list = tempMap.get(count);
+                            if (list == null) {
+                                // Create list
+                                list = new ArrayList<CharEntry>();
+                                tempMap.put(count, list);
+                            }
+                            list.add(entry);
+                        }
+                        // Allocate array of the right size
+                        int size = ccStats.size();
+                        if (size > cacheSize) {
+                            size = cacheSize;
+                        }
+                        CharEntry[] tempccCache = new CharEntry[size];
+                        // Fill it up using an alphabetical order
+                        // and a dumb insert sort
+                        CharChunk tempChunk = new CharChunk();
+                        int n = 0;
+                        while (n < size) {
+                            Object key = tempMap.lastKey();
+                            ArrayList<CharEntry> list = tempMap.get(key);
+                            for (int i = 0; i < list.size() && n < size; i++) {
+                                CharEntry entry = list.get(i);
+                                tempChunk.setChars(entry.name, 0,
+                                        entry.name.length);
+                                int insertPos = findClosest(tempChunk,
+                                        tempccCache, n);
+                                if (insertPos == n) {
+                                    tempccCache[n + 1] = entry;
+                                } else {
+                                    System.arraycopy(tempccCache, insertPos + 1,
+                                            tempccCache, insertPos + 2,
+                                            n - insertPos - 1);
+                                    tempccCache[insertPos + 1] = entry;
+                                }
+                                n++;
+                            }
+                            tempMap.remove(key);
+                        }
+                        ccCount = 0;
+                        ccStats.clear();
+                        ccCache = tempccCache;
+                        if (log.isDebugEnabled()) {
+                            long t2 = System.currentTimeMillis();
+                            log.debug("CharCache generation time: " +
+                                    (t2 - t1) + "ms");
+                        }
+                    } else {
+                        ccCount++;
+                        // Allocate new CharEntry for the lookup
+                        CharEntry entry = new CharEntry();
+                        entry.value = value;
+                        int[] count = ccStats.get(entry);
+                        if (count == null) {
+                            int end = cc.getEnd();
+                            int start = cc.getStart();
+                            // Create char array and copy chars
+                            entry.name = new char[cc.getLength()];
+                            System.arraycopy(cc.getBuffer(), start, entry.name,
+                                    0, end - start);
+                            // Initialize occurrence count to one
+                            count = new int[1];
+                            count[0] = 1;
+                            // Set in the stats hash map
+                            ccStats.put(entry, count);
+                        } else {
+                            count[0] = count[0] + 1;
+                        }
+                    }
+                }
+            }
+            return value;
+        } else {
+            accessCount++;
+            // Find the corresponding String
+            String result = find(cc);
+            if (result == null) {
+                return cc.toStringInternal();
+            }
+            // Note: We don't care about safety for the stats
+            hitCount++;
+            return result;
+        }
+
+    }
+
+
+    // ----------------------------------------------------- Protected Methods
+
+
+    /**
+     * Compare given byte chunk with byte array.
+     * Return -1, 0 or +1 if inferior, equal, or superior to the String.
+     */
+    protected static final int compare(ByteChunk name, byte[] compareTo) {
+        int result = 0;
+
+        byte[] b = name.getBuffer();
+        int start = name.getStart();
+        int end = name.getEnd();
+        int len = compareTo.length;
+
+        if ((end - start) < len) {
+            len = end - start;
+        }
+        for (int i = 0; (i < len) && (result == 0); i++) {
+            if (b[i + start] > compareTo[i]) {
+                result = 1;
+            } else if (b[i + start] < compareTo[i]) {
+                result = -1;
+            }
+        }
+        if (result == 0) {
+            if (compareTo.length > (end - start)) {
+                result = -1;
+            } else if (compareTo.length < (end - start)) {
+                result = 1;
+            }
+        }
+        return result;
+    }
+
+
+    /**
+     * Find an entry given its name in the cache and return the associated
+     * String.
+     */
+    protected static final String find(ByteChunk name) {
+        int pos = findClosest(name, bcCache, bcCache.length);
+        if ((pos < 0) || (compare(name, bcCache[pos].name) != 0)
+                || !(name.getCharset().equals(bcCache[pos].charset))) {
+            return null;
+        } else {
+            return bcCache[pos].value;
+        }
+    }
+
+
+    /**
+     * Find an entry given its name in a sorted array of map elements.
+     * This will return the index for the closest inferior or equal item in the
+     * given array.
+     */
+    protected static final int findClosest(ByteChunk name, ByteEntry[] array,
+            int len) {
+
+        int a = 0;
+        int b = len - 1;
+
+        // Special cases: -1 and 0
+        if (b == -1) {
+            return -1;
+        }
+
+        if (compare(name, array[0].name) < 0) {
+            return -1;
+        }
+        if (b == 0) {
+            return 0;
+        }
+
+        int i = 0;
+        while (true) {
+            i = (b + a) >>> 1;
+            int result = compare(name, array[i].name);
+            if (result == 1) {
+                a = i;
+            } else if (result == 0) {
+                return i;
+            } else {
+                b = i;
+            }
+            if ((b - a) == 1) {
+                int result2 = compare(name, array[b].name);
+                if (result2 < 0) {
+                    return a;
+                } else {
+                    return b;
+                }
+            }
+        }
+
+    }
+
+
+    /**
+     * Compare given char chunk with char array.
+     * Return -1, 0 or +1 if inferior, equal, or superior to the String.
+     */
+    protected static final int compare(CharChunk name, char[] compareTo) {
+        int result = 0;
+
+        char[] c = name.getBuffer();
+        int start = name.getStart();
+        int end = name.getEnd();
+        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[i]) {
+                result = 1;
+            } else if (c[i + start] < compareTo[i]) {
+                result = -1;
+            }
+        }
+        if (result == 0) {
+            if (compareTo.length > (end - start)) {
+                result = -1;
+            } else if (compareTo.length < (end - start)) {
+                result = 1;
+            }
+        }
+        return result;
+    }
+
+
+    /**
+     * Find an entry given its name in the cache and return the associated
+     * String.
+     */
+    protected static final String find(CharChunk name) {
+        int pos = findClosest(name, ccCache, ccCache.length);
+        if ((pos < 0) || (compare(name, ccCache[pos].name) != 0)) {
+            return null;
+        } else {
+            return ccCache[pos].value;
+        }
+    }
+
+
+    /**
+     * Find an entry given its name in a sorted array of map elements.
+     * This will return the index for the closest inferior or equal item in the
+     * given array.
+     */
+    protected static final int findClosest(CharChunk name, CharEntry[] array,
+            int len) {
+
+        int a = 0;
+        int b = len - 1;
+
+        // Special cases: -1 and 0
+        if (b == -1) {
+            return -1;
+        }
+
+        if (compare(name, array[0].name) < 0 ) {
+            return -1;
+        }
+        if (b == 0) {
+            return 0;
+        }
+
+        int i = 0;
+        while (true) {
+            i = (b + a) >>> 1;
+            int result = compare(name, array[i].name);
+            if (result == 1) {
+                a = i;
+            } else if (result == 0) {
+                return i;
+            } else {
+                b = i;
+            }
+            if ((b - a) == 1) {
+                int result2 = compare(name, array[b].name);
+                if (result2 < 0) {
+                    return a;
+                } else {
+                    return b;
+                }
+            }
+        }
+
+    }
+
+
+    // -------------------------------------------------- ByteEntry Inner Class
+
+
+    private static class ByteEntry {
+
+        private byte[] name = null;
+        private Charset charset = null;
+        private String value = null;
+
+        @Override
+        public String toString() {
+            return value;
+        }
+        @Override
+        public int hashCode() {
+            return value.hashCode();
+        }
+        @Override
+        public boolean equals(Object obj) {
+            if (obj instanceof ByteEntry) {
+                return value.equals(((ByteEntry) obj).value);
+            }
+            return false;
+        }
+
+    }
+
+
+    // -------------------------------------------------- CharEntry Inner Class
+
+
+    private static class CharEntry {
+
+        private char[] name = null;
+        private String value = null;
+
+        @Override
+        public String toString() {
+            return value;
+        }
+        @Override
+        public int hashCode() {
+            return value.hashCode();
+        }
+        @Override
+        public boolean equals(Object obj) {
+            if (obj instanceof CharEntry) {
+                return value.equals(((CharEntry) obj).value);
+            }
+            return false;
+        }
+
+    }
+
+
+}

http://git-wip-us.apache.org/repos/asf/struts/blob/5421930b/core/src/main/java/org/apache/struts2/util/tomcat/buf/UDecoder.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/struts2/util/tomcat/buf/UDecoder.java b/core/src/main/java/org/apache/struts2/util/tomcat/buf/UDecoder.java
new file mode 100644
index 0000000..b52cda7
--- /dev/null
+++ b/core/src/main/java/org/apache/struts2/util/tomcat/buf/UDecoder.java
@@ -0,0 +1,421 @@
+/*
+ *  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 com.opensymphony.xwork2.util.logging.LoggerFactory;
+
+import com.opensymphony.xwork2.util.logging.Logger;
+import java.io.CharConversionException;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+
+/**
+ *  All URL decoding happens here. This way we can reuse, review, optimize
+ *  without adding complexity to the buffers.
+ *
+ *  The conversion will modify the original buffer.
+ *
+ *  @author Costin Manolache
+ */
+public final class UDecoder {
+
+    private static final Logger log = LoggerFactory.getLogger(UDecoder.class);
+
+    public static final boolean ALLOW_ENCODED_SLASH =
+        Boolean.parseBoolean(System.getProperty("org.apache.tomcat.util.buf.UDecoder.ALLOW_ENCODED_SLASH", "false"));
+
+    private static class DecodeException extends CharConversionException {
+        private static final long serialVersionUID = 1L;
+        public DecodeException(String s) {
+            super(s);
+        }
+
+        @Override
+        public synchronized Throwable fillInStackTrace() {
+            // This class does not provide a stack trace
+            return this;
+        }
+    }
+
+    /** Unexpected end of data. */
+    private static final IOException EXCEPTION_EOF = new DecodeException("EOF");
+
+    /** %xx with not-hex digit */
+    private static final IOException EXCEPTION_NOT_HEX_DIGIT = new DecodeException(
+            "isHexDigit");
+
+    /** %-encoded slash is forbidden in resource path */
+    private static final IOException EXCEPTION_SLASH = new DecodeException(
+            "noSlash");
+
+    public UDecoder()
+    {
+    }
+
+    /** URLDecode, will modify the source.
+     */
+    public void convert( ByteChunk mb, boolean query )
+        throws IOException
+    {
+        int start=mb.getOffset();
+        byte buff[]=mb.getBytes();
+        int end=mb.getEnd();
+
+        int idx= ByteChunk.findByte( buff, start, end, (byte) '%' );
+        int idx2=-1;
+        if( query ) {
+            idx2= ByteChunk.findByte( buff, start, (idx >= 0 ? idx : end), (byte) '+' );
+        }
+        if( idx<0 && idx2<0 ) {
+            return;
+        }
+
+        // idx will be the smallest positive index ( first % or + )
+        if( (idx2 >= 0 && idx2 < idx) || idx < 0 ) {
+            idx=idx2;
+        }
+
+        final boolean noSlash = !(ALLOW_ENCODED_SLASH || 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 EXCEPTION_EOF;
+                }
+                byte b1= buff[j+1];
+                byte b2=buff[j+2];
+                if( !isHexDigit( b1 ) || ! isHexDigit(b2 )) {
+                    throw EXCEPTION_NOT_HEX_DIGIT;
+                }
+
+                j+=2;
+                int res=x2c( b1, b2 );
+                if (noSlash && (res == '/')) {
+                    throw EXCEPTION_SLASH;
+                }
+                buff[idx]=(byte)res;
+            }
+        }
+
+        mb.setEnd( idx );
+
+        return;
+    }
+
+    // -------------------- Additional methods --------------------
+    // XXX What do we do about charset ????
+
+    /** In-buffer processing - the buffer will be modified
+     */
+    public void convert( CharChunk mb, boolean query )
+        throws IOException
+    {
+        //        log( "Converting a char chunk ");
+        int start=mb.getOffset();
+        char buff[]=mb.getBuffer();
+        int cend=mb.getEnd();
+
+        int idx= CharChunk.indexOf( buff, start, cend, '%' );
+        int idx2=-1;
+        if( query ) {
+            idx2= CharChunk.indexOf( buff, start, (idx >= 0 ? idx : cend), '+' );
+        }
+        if( idx<0 && idx2<0 ) {
+            return;
+        }
+
+        // idx will be the smallest positive index ( first % or + )
+        if( (idx2 >= 0 && idx2 < idx) || idx < 0 ) {
+            idx=idx2;
+        }
+
+        final boolean noSlash = !(ALLOW_ENCODED_SLASH || query);
+
+        for( int j=idx; j<cend; j++, idx++ ) {
+            if( buff[ j ] == '+' && query ) {
+                buff[idx]=( ' ' );
+            } else if( buff[ j ] != '%' ) {
+                buff[idx]=buff[j];
+            } else {
+                // read next 2 digits
+                if( j+2 >= cend ) {
+                    // invalid
+                    throw EXCEPTION_EOF;
+                }
+                char b1= buff[j+1];
+                char b2=buff[j+2];
+                if( !isHexDigit( b1 ) || ! isHexDigit(b2 )) {
+                    throw EXCEPTION_NOT_HEX_DIGIT;
+                }
+
+                j+=2;
+                int res=x2c( b1, b2 );
+                if (noSlash && (res == '/')) {
+                    throw EXCEPTION_SLASH;
+                }
+                buff[idx]=(char)res;
+            }
+        }
+        mb.setEnd( idx );
+    }
+
+    /** URLDecode, will modify the source
+     */
+    public void convert(MessageBytes mb, boolean query)
+        throws IOException
+    {
+
+        switch (mb.getType()) {
+        case MessageBytes.T_STR:
+            String strValue=mb.toString();
+            if( strValue==null ) {
+                return;
+            }
+            try {
+                mb.setString( convert( strValue, query ));
+            } catch (RuntimeException ex) {
+                throw new DecodeException(ex.getMessage());
+            }
+            break;
+        case MessageBytes.T_CHARS:
+            CharChunk charC=mb.getCharChunk();
+            convert( charC, query );
+            break;
+        case MessageBytes.T_BYTES:
+            ByteChunk bytesC=mb.getByteChunk();
+            convert( bytesC, query );
+            break;
+        }
+    }
+
+    // XXX Old code, needs to be replaced !!!!
+    //
+    public final String convert(String str, boolean query)
+    {
+        if (str == null) {
+            return  null;
+        }
+
+        if( (!query || str.indexOf( '+' ) < 0) && str.indexOf( '%' ) < 0 ) {
+            return str;
+        }
+
+        final boolean noSlash = !(ALLOW_ENCODED_SLASH || query);
+
+        StringBuilder dec = new StringBuilder();    // decoded string output
+        int strPos = 0;
+        int strLen = str.length();
+
+        dec.ensureCapacity(str.length());
+        while (strPos < strLen) {
+            int laPos;        // lookahead position
+
+            // look ahead to next URLencoded metacharacter, if any
+            for (laPos = strPos; laPos < strLen; laPos++) {
+                char laChar = str.charAt(laPos);
+                if ((laChar == '+' && query) || (laChar == '%')) {
+                    break;
+                }
+            }
+
+            // if there were non-metacharacters, copy them all as a block
+            if (laPos > strPos) {
+                dec.append(str.substring(strPos,laPos));
+                strPos = laPos;
+            }
+
+            // shortcut out of here if we're at the end of the string
+            if (strPos >= strLen) {
+                break;
+            }
+
+            // process next metacharacter
+            char metaChar = str.charAt(strPos);
+            if (metaChar == '+') {
+                dec.append(' ');
+                strPos++;
+                continue;
+            } else if (metaChar == '%') {
+                // We throw the original exception - the super will deal with
+                // it
+                //                try {
+                char res = (char) Integer.parseInt(
+                        str.substring(strPos + 1, strPos + 3), 16);
+                if (noSlash && (res == '/')) {
+                    throw new IllegalArgumentException("noSlash");
+                }
+                dec.append(res);
+                strPos += 3;
+            }
+        }
+
+        return dec.toString();
+    }
+
+
+    /**
+     * Decode and return the specified URL-encoded String.
+     * When the byte array is converted to a string, the system default
+     * character encoding is used...  This may be different than some other
+     * servers. It is assumed the string is not a query string.
+     *
+     * @param str The url-encoded string
+     *
+     * @exception IllegalArgumentException if a '%' character is not followed
+     * by a valid 2-digit hexadecimal number
+     */
+    public static String URLDecode(String str) {
+        return URLDecode(str, null);
+    }
+
+
+    /**
+     * Decode and return the specified URL-encoded String. It is assumed the
+     * string is not a query string.
+     *
+     * @param str The url-encoded string
+     * @param enc The encoding to use; if null, the default encoding is used. If
+     * an unsupported encoding is specified null will be returned
+     * @exception IllegalArgumentException if a '%' character is not followed
+     * by a valid 2-digit hexadecimal number
+     */
+    public static String URLDecode(String str, String enc) {
+        return URLDecode(str, enc, false);
+    }
+
+
+    /**
+     * Decode and return the specified URL-encoded String.
+     *
+     * @param str The url-encoded string
+     * @param enc The encoding to use; if null, the default encoding is used. If
+     * an unsupported encoding is specified null will be returned
+     * @param isQuery Is this a query string being processed
+     * @exception IllegalArgumentException if a '%' character is not followed
+     * by a valid 2-digit hexadecimal number
+     */
+    public static String URLDecode(String str, String enc, boolean isQuery) {
+        if (str == null)
+            return (null);
+
+        // use the specified encoding to extract bytes out of the
+        // given string so that the encoding is not lost. If an
+        // encoding is not specified, use ISO-8859-1
+        byte[] bytes = null;
+        try {
+            if (enc == null) {
+                bytes = str.getBytes("ISO-8859-1");
+            } else {
+                bytes = str.getBytes(B2CConverter.getCharset(enc));
+            }
+        } catch (UnsupportedEncodingException uee) {
+            if (log.isDebugEnabled()) {
+                log.debug("Unable to URL decode the specified input since the encoding "+ enc + " is not supported.", uee);
+            }
+        }
+
+        return URLDecode(bytes, enc, isQuery);
+
+    }
+
+
+    /**
+     * Decode and return the specified URL-encoded byte array.
+     *
+     * @param bytes The url-encoded byte array
+     * @param enc The encoding to use; if null, the default encoding is used. If
+     * an unsupported encoding is specified null will be returned
+     * @param isQuery Is this a query string being processed
+     * @exception IllegalArgumentException if a '%' character is not followed
+     * by a valid 2-digit hexadecimal number
+     */
+    public static String URLDecode(byte[] bytes, String enc, boolean isQuery) {
+
+        if (bytes == null)
+            return null;
+
+        int len = bytes.length;
+        int ix = 0;
+        int ox = 0;
+        while (ix < len) {
+            byte b = bytes[ix++];     // Get byte to test
+            if (b == '+' && isQuery) {
+                b = (byte)' ';
+            } else if (b == '%') {
+                if (ix + 2 > len) {
+                    throw new IllegalArgumentException(
+                            "The % character must be followed by two hexademical digits");
+                }
+                b = (byte) ((convertHexDigit(bytes[ix++]) << 4)
+                            + convertHexDigit(bytes[ix++]));
+            }
+            bytes[ox++] = b;
+        }
+        if (enc != null) {
+            try {
+                return new String(bytes, 0, ox, B2CConverter.getCharset(enc));
+            } catch (UnsupportedEncodingException uee) {
+                if (log.isDebugEnabled()) {
+                    log.debug("Unable to URL decode the specified input since the encoding " + enc + " is not supported.", uee);
+                }
+                return null;
+            }
+        }
+        return new String(bytes, 0, ox);
+
+    }
+
+
+    private static byte convertHexDigit( byte b ) {
+        if ((b >= '0') && (b <= '9')) return (byte)(b - '0');
+        if ((b >= 'a') && (b <= 'f')) return (byte)(b - 'a' + 10);
+        if ((b >= 'A') && (b <= 'F')) return (byte)(b - 'A' + 10);
+        throw new IllegalArgumentException(((char) b) + " is not a hexadecimal digit");
+    }
+
+
+    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;
+    }
+
+
+    private static int x2c( char b1, char b2 ) {
+        int digit= (b1>='A') ? ( (b1 & 0xDF)-'A') + 10 :
+            (b1 -'0');
+        digit*=16;
+        digit +=(b2>='A') ? ( (b2 & 0xDF)-'A') + 10 :
+            (b2 -'0');
+        return digit;
+    }
+}

http://git-wip-us.apache.org/repos/asf/struts/blob/5421930b/core/src/main/java/org/apache/struts2/util/tomcat/buf/Utf8Decoder.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/struts2/util/tomcat/buf/Utf8Decoder.java b/core/src/main/java/org/apache/struts2/util/tomcat/buf/Utf8Decoder.java
new file mode 100644
index 0000000..b08b236
--- /dev/null
+++ b/core/src/main/java/org/apache/struts2/util/tomcat/buf/Utf8Decoder.java
@@ -0,0 +1,293 @@
+/*
+ * 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.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.CharsetDecoder;
+import java.nio.charset.CoderResult;
+
+/**
+ * Decodes bytes to UTF-8. Extracted from Apache Harmony and modified to reject
+ * code points from U+D800 to U+DFFF as per RFC3629. The standard Java decoder
+ * does not reject these. It has also been modified to reject code points
+ * greater than U+10FFFF which the standard Java decoder rejects but the harmony
+ * one does not.
+ */
+public class Utf8Decoder extends CharsetDecoder {
+
+    // The next table contains information about UTF-8 charset and
+    // correspondence of 1st byte to the length of sequence
+    // For information please visit http://www.ietf.org/rfc/rfc3629.txt
+    //
+    // Please note, o means 0, actually.
+    // -------------------------------------------------------------------
+    // 0 1 2 3 Value
+    // -------------------------------------------------------------------
+    // oxxxxxxx                            00000000 00000000 0xxxxxxx
+    // 11oyyyyy 1oxxxxxx                   00000000 00000yyy yyxxxxxx
+    // 111ozzzz 1oyyyyyy 1oxxxxxx          00000000 zzzzyyyy yyxxxxxx
+    // 1111ouuu 1ouuzzzz 1oyyyyyy 1oxxxxxx 000uuuuu zzzzyyyy yyxxxxxx
+    private static final int remainingBytes[] = {
+            // 1owwwwww
+            -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+            -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+            -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+            -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+            // 11oyyyyy
+            -1, -1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+            // 111ozzzz
+            2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+            // 1111ouuu
+            3, 3, 3, 3, 3, -1, -1, -1,
+            // > 11110111
+            -1, -1, -1, -1, -1, -1, -1, -1};
+    private static final int remainingNumbers[] = {0, // 0 1 2 3
+            4224, // (01o00000b << 6)+(1o000000b)
+            401536, // (011o0000b << 12)+(1o000000b << 6)+(1o000000b)
+            29892736 // (0111o000b << 18)+(1o000000b << 12)+(1o000000b <<
+            // 6)+(1o000000b)
+    };
+    private static final int lowerEncodingLimit[] = {-1, 0x80, 0x800, 0x10000};
+
+
+    public Utf8Decoder() {
+        super(B2CConverter.UTF_8, 1.0f, 1.0f);
+    }
+
+
+    @Override
+    protected CoderResult decodeLoop(ByteBuffer in, CharBuffer out) {
+        if (in.hasArray() && out.hasArray()) {
+            return decodeHasArray(in, out);
+        }
+        return decodeNotHasArray(in, out);
+    }
+
+
+    private CoderResult decodeNotHasArray(ByteBuffer in, CharBuffer out) {
+        int outRemaining = out.remaining();
+        int pos = in.position();
+        int limit = in.limit();
+        try {
+            while (pos < limit) {
+                if (outRemaining == 0) {
+                    return CoderResult.OVERFLOW;
+                }
+                int jchar = in.get();
+                if (jchar < 0) {
+                    jchar = jchar & 0x7F;
+                    int tail = remainingBytes[jchar];
+                    if (tail == -1) {
+                        return CoderResult.malformedForLength(1);
+                    }
+                    if (limit - pos < 1 + tail) {
+                        // No early test for invalid sequences here as peeking
+                        // at the next byte is harder
+                        return CoderResult.UNDERFLOW;
+                    }
+                    int nextByte;
+                    for (int i = 0; i < tail; i++) {
+                        nextByte = in.get() & 0xFF;
+                        if ((nextByte & 0xC0) != 0x80) {
+                            return CoderResult.malformedForLength(1 + i);
+                        }
+                        jchar = (jchar << 6) + nextByte;
+                    }
+                    jchar -= remainingNumbers[tail];
+                    if (jchar < lowerEncodingLimit[tail]) {
+                        // Should have been encoded in a fewer octets
+                        return CoderResult.malformedForLength(1);
+                    }
+                    pos += tail;
+                }
+                // Apache Tomcat added test
+                if (jchar >= 0xD800 && jchar <= 0xDFFF) {
+                    return CoderResult.unmappableForLength(3);
+                }
+                // Apache Tomcat added test
+                if (jchar > 0x10FFFF) {
+                    return CoderResult.unmappableForLength(4);
+                }
+                if (jchar <= 0xffff) {
+                    out.put((char) jchar);
+                    outRemaining--;
+                } else {
+                    if (outRemaining < 2) {
+                        return CoderResult.OVERFLOW;
+                    }
+                    out.put((char) ((jchar >> 0xA) + 0xD7C0));
+                    out.put((char) ((jchar & 0x3FF) + 0xDC00));
+                    outRemaining -= 2;
+                }
+                pos++;
+            }
+            return CoderResult.UNDERFLOW;
+        } finally {
+            in.position(pos);
+        }
+    }
+
+
+    private CoderResult decodeHasArray(ByteBuffer in, CharBuffer out) {
+        int outRemaining = out.remaining();
+        int pos = in.position();
+        int limit = in.limit();
+        final byte[] bArr = in.array();
+        final char[] cArr = out.array();
+        final int inIndexLimit = limit + in.arrayOffset();
+        int inIndex = pos + in.arrayOffset();
+        int outIndex = out.position() + out.arrayOffset();
+        // if someone would change the limit in process,
+        // he would face consequences
+        for (; inIndex < inIndexLimit && outRemaining > 0; inIndex++) {
+            int jchar = bArr[inIndex];
+            if (jchar < 0) {
+                jchar = jchar & 0x7F;
+                // If first byte is invalid, tail will be set to -1
+                int tail = remainingBytes[jchar];
+                if (tail == -1) {
+                    in.position(inIndex - in.arrayOffset());
+                    out.position(outIndex - out.arrayOffset());
+                    return CoderResult.malformedForLength(1);
+                }
+                // Additional checks to detect invalid sequences ASAP
+                // Checks derived from Unicode 6.2, Chapter 3, Table 3-7
+                // Check 2nd byte
+                int tailAvailable = inIndexLimit - inIndex - 1;
+                if (tailAvailable > 0) {
+                    // First byte C2..DF, second byte 80..BF
+                    if (jchar > 0x41 && jchar < 0x60 &&
+                            (bArr[inIndex + 1] & 0xC0) != 0x80) {
+                        in.position(inIndex - in.arrayOffset());
+                        out.position(outIndex - out.arrayOffset());
+                        return CoderResult.malformedForLength(1);
+                    }
+                    // First byte E0, second byte A0..BF
+                    if (jchar == 0x60 && (bArr[inIndex + 1] & 0xE0) != 0xA0) {
+                        in.position(inIndex - in.arrayOffset());
+                        out.position(outIndex - out.arrayOffset());
+                        return CoderResult.malformedForLength(1);
+                    }
+                    // First byte E1..EC, second byte 80..BF
+                    if (jchar > 0x60 && jchar < 0x6D &&
+                            (bArr[inIndex + 1] & 0xC0) != 0x80) {
+                        in.position(inIndex - in.arrayOffset());
+                        out.position(outIndex - out.arrayOffset());
+                        return CoderResult.malformedForLength(1);
+                    }
+                    // First byte ED, second byte 80..9F
+                    if (jchar == 0x6D && (bArr[inIndex + 1] & 0xE0) != 0x80) {
+                        in.position(inIndex - in.arrayOffset());
+                        out.position(outIndex - out.arrayOffset());
+                        return CoderResult.malformedForLength(1);
+                    }
+                    // First byte EE..EF, second byte 80..BF
+                    if (jchar > 0x6D && jchar < 0x70 &&
+                            (bArr[inIndex + 1] & 0xC0) != 0x80) {
+                        in.position(inIndex - in.arrayOffset());
+                        out.position(outIndex - out.arrayOffset());
+                        return CoderResult.malformedForLength(1);
+                    }
+                    // First byte F0, second byte 90..BF
+                    if (jchar == 0x70 &&
+                            ((bArr[inIndex + 1] & 0xFF) < 0x90 ||
+                                    (bArr[inIndex + 1] & 0xFF) > 0xBF)) {
+                        in.position(inIndex - in.arrayOffset());
+                        out.position(outIndex - out.arrayOffset());
+                        return CoderResult.malformedForLength(1);
+                    }
+                    // First byte F1..F3, second byte 80..BF
+                    if (jchar > 0x70 && jchar < 0x74 &&
+                            (bArr[inIndex + 1] & 0xC0) != 0x80) {
+                        in.position(inIndex - in.arrayOffset());
+                        out.position(outIndex - out.arrayOffset());
+                        return CoderResult.malformedForLength(1);
+                    }
+                    // First byte F4, second byte 80..8F
+                    if (jchar == 0x74 &&
+                            (bArr[inIndex + 1] & 0xF0) != 0x80) {
+                        in.position(inIndex - in.arrayOffset());
+                        out.position(outIndex - out.arrayOffset());
+                        return CoderResult.malformedForLength(1);
+                    }
+                }
+                // Check third byte if present and expected
+                if (tailAvailable > 1 && tail > 1) {
+                    if ((bArr[inIndex + 2] & 0xC0) != 0x80) {
+                        in.position(inIndex - in.arrayOffset());
+                        out.position(outIndex - out.arrayOffset());
+                        return CoderResult.malformedForLength(2);
+                    }
+                }
+                // Check fourth byte if present and expected
+                if (tailAvailable > 2 && tail > 2) {
+                    if ((bArr[inIndex + 3] & 0xC0) != 0x80) {
+                        in.position(inIndex - in.arrayOffset());
+                        out.position(outIndex - out.arrayOffset());
+                        return CoderResult.malformedForLength(3);
+                    }
+                }
+                if (tailAvailable < tail) {
+                    break;
+                }
+                for (int i = 0; i < tail; i++) {
+                    int nextByte = bArr[inIndex + i + 1] & 0xFF;
+                    if ((nextByte & 0xC0) != 0x80) {
+                        in.position(inIndex - in.arrayOffset());
+                        out.position(outIndex - out.arrayOffset());
+                        return CoderResult.malformedForLength(1 + i);
+                    }
+                    jchar = (jchar << 6) + nextByte;
+                }
+                jchar -= remainingNumbers[tail];
+                if (jchar < lowerEncodingLimit[tail]) {
+                    // Should have been encoded in fewer octets
+                    in.position(inIndex - in.arrayOffset());
+                    out.position(outIndex - out.arrayOffset());
+                    return CoderResult.malformedForLength(1);
+                }
+                inIndex += tail;
+            }
+            // Apache Tomcat added test
+            if (jchar >= 0xD800 && jchar <= 0xDFFF) {
+                return CoderResult.unmappableForLength(3);
+            }
+            // Apache Tomcat added test
+            if (jchar > 0x10FFFF) {
+                return CoderResult.unmappableForLength(4);
+            }
+            if (jchar <= 0xffff) {
+                cArr[outIndex++] = (char) jchar;
+                outRemaining--;
+            } else {
+                if (outRemaining < 2) {
+                    return CoderResult.OVERFLOW;
+                }
+                cArr[outIndex++] = (char) ((jchar >> 0xA) + 0xD7C0);
+                cArr[outIndex++] = (char) ((jchar & 0x3FF) + 0xDC00);
+                outRemaining -= 2;
+            }
+        }
+        in.position(inIndex - in.arrayOffset());
+        out.position(outIndex - out.arrayOffset());
+        return (outRemaining == 0 && inIndex < inIndexLimit) ?
+                CoderResult.OVERFLOW :
+                CoderResult.UNDERFLOW;
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/struts/blob/5421930b/core/src/main/java/org/apache/struts2/views/util/DefaultUrlHelper.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/struts2/views/util/DefaultUrlHelper.java b/core/src/main/java/org/apache/struts2/views/util/DefaultUrlHelper.java
index 4d20ee2..66a9b7c 100644
--- a/core/src/main/java/org/apache/struts2/views/util/DefaultUrlHelper.java
+++ b/core/src/main/java/org/apache/struts2/views/util/DefaultUrlHelper.java
@@ -27,11 +27,11 @@ import com.opensymphony.xwork2.util.logging.LoggerFactory;
 import org.apache.commons.lang3.StringEscapeUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.struts2.StrutsConstants;
+import org.apache.struts2.util.URLDecoderUtil;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import java.io.UnsupportedEncodingException;
-import java.net.URLDecoder;
 import java.net.URLEncoder;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -276,15 +276,15 @@ public class DefaultUrlHelper implements UrlHelper {
 	}
 
 	/**
-	 * Decodes the URL using {@link java.net.URLDecoder#decode(String, String)} with the encoding specified in the configuration.
+	 * Decodes the URL using {@link URLDecoderUtil#decode(String, String)} with the encoding specified in the configuration.
 	 *
 	 * @param input the input to decode
 	 * @return the encoded string
 	 */
 	public String decode( String input ) {
 		try {
-			return URLDecoder.decode(input, encoding);
-		} catch (UnsupportedEncodingException e) {
+			return URLDecoderUtil.decode(input, encoding);
+		} catch (Exception e) {
 			if (LOG.isWarnEnabled()) {
 				LOG.warn("Could not decode URL parameter '#0', returning value un-decoded", input);
 			}

http://git-wip-us.apache.org/repos/asf/struts/blob/5421930b/core/src/test/java/org/apache/struts2/util/URLDecoderUtilTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/struts2/util/URLDecoderUtilTest.java b/core/src/test/java/org/apache/struts2/util/URLDecoderUtilTest.java
new file mode 100644
index 0000000..f21c08f
--- /dev/null
+++ b/core/src/test/java/org/apache/struts2/util/URLDecoderUtilTest.java
@@ -0,0 +1,71 @@
+package org.apache.struts2.util;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+public class URLDecoderUtilTest {
+
+    @Test
+    public void testURLDecodeStringInvalid() {
+        // %n rather than %nn should throw an IAE according to the Javadoc
+        Exception exception = null;
+        try {
+            URLDecoderUtil.decode("%5xxxxx", "ISO-8859-1");
+        } catch (Exception e) {
+            exception = e;
+        }
+        assertTrue(exception instanceof IllegalArgumentException);
+
+        // Edge case trying to trigger ArrayIndexOutOfBoundsException
+        exception = null;
+        try {
+            URLDecoderUtil.decode("%5", "ISO-8859-1");
+        } catch (Exception e) {
+            exception = e;
+        }
+        assertTrue(exception instanceof IllegalArgumentException);
+    }
+
+    @Test
+    public void testURLDecodeStringValidIso88591Start() {
+
+        String result = URLDecoderUtil.decode("%41xxxx", "ISO-8859-1");
+        assertEquals("Axxxx", result);
+    }
+
+    @Test
+    public void testURLDecodeStringValidIso88591Middle() {
+
+        String result = URLDecoderUtil.decode("xx%41xx", "ISO-8859-1");
+        assertEquals("xxAxx", result);
+    }
+
+    @Test
+    public void testURLDecodeStringValidIso88591End() {
+
+        String result = URLDecoderUtil.decode("xxxx%41", "ISO-8859-1");
+        assertEquals("xxxxA", result);
+    }
+
+    @Test
+    public void testURLDecodeStringValidUtf8Start() {
+        String result = URLDecoderUtil.decode("%c3%aaxxxx", "UTF-8");
+        assertEquals("\u00eaxxxx", result);
+    }
+
+    @Test
+    public void testURLDecodeStringValidUtf8Middle() {
+
+        String result = URLDecoderUtil.decode("xx%c3%aaxx", "UTF-8");
+        assertEquals("xx\u00eaxx", result);
+    }
+
+    @Test
+    public void testURLDecodeStringValidUtf8End() {
+
+        String result = URLDecoderUtil.decode("xxxx%c3%aa", "UTF-8");
+        assertEquals("xxxx\u00ea", result);
+    }
+
+}
\ No newline at end of file


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

Posted by rg...@apache.org.
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;
+    }
+}