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:19:29 UTC
[1/2] struts git commit: WW-4507 - clone Tomcat UDecoder and use it
for in query string handling (cherry picked from commit 5421930)
Repository: struts
Updated Branches:
refs/heads/master 460faa33d -> 72471d707
http://git-wip-us.apache.org/repos/asf/struts/blob/72471d70/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/72471d70/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/72471d70/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/72471d70/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/72471d70/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 4e62c21..9e8418b 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,6 +27,7 @@ import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.StrutsConstants;
+import org.apache.struts2.util.URLDecoderUtil;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@@ -267,15 +268,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) {
LOG.warn("Could not decode URL parameter '{}', returning value un-decoded", input);
return input;
}
http://git-wip-us.apache.org/repos/asf/struts/blob/72471d70/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 (cherry picked from commit 5421930)
Posted by rg...@apache.org.
WW-4507 - clone Tomcat UDecoder and use it for in query string handling
(cherry picked from commit 5421930)
Project: http://git-wip-us.apache.org/repos/asf/struts/repo
Commit: http://git-wip-us.apache.org/repos/asf/struts/commit/72471d70
Tree: http://git-wip-us.apache.org/repos/asf/struts/tree/72471d70
Diff: http://git-wip-us.apache.org/repos/asf/struts/diff/72471d70
Branch: refs/heads/master
Commit: 72471d7075681bea52046645ad7aa34e9c53751e
Parents: 460faa3
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 15:04:38 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 | 7 +-
.../apache/struts2/util/URLDecoderUtilTest.java | 71 ++
14 files changed, 4262 insertions(+), 9 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/struts/blob/72471d70/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 b474913..c2a9bfc 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
@@ -27,9 +27,9 @@ import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
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;
@@ -132,10 +132,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/72471d70/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 d7ae2c0..9db58e3 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 org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
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/72471d70/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/72471d70/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/72471d70/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/72471d70/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/72471d70/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/72471d70/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;
+ }
+}