/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.tomcat.util.http;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Enumeration;
import org.apache.tomcat.util.buf.ByteChunk;
import org.apache.tomcat.util.buf.CharChunk;
import org.apache.tomcat.util.buf.MessageBytes;
import org.apache.tomcat.util.buf.UDecoder;
/**
*
* @author Costin Manolache
* @author Remy Maucherat
*/
public final class Parameters {
protected static org.jboss.logging.Logger log = org.jboss.logging.Logger
.getLogger(Parameters.class);
protected static final int NEED_NEXT = -2;
protected static final int LAST = -1;
public static final int INITIAL_SIZE = 8;
protected static final String[] ARRAY_TYPE = new String[0];
protected static final int MAX_COUNT =
Integer.valueOf(System.getProperty("org.apache.tomcat.util.http.Parameters.MAX_COUNT", "512")).intValue();
protected class Field {
MessageBytes name = MessageBytes.newInstance();
MessageBytes value = MessageBytes.newInstance();
// Extra info for speed
// multiple fields with same name - a linked list will
// speed up multiple name enumerations and search.
int nextPos;
// hashkey
int hash;
Field nextSameHash;
Field() {
nextPos = NEED_NEXT;
}
void recycle() {
name.recycle();
value.recycle();
nextPos = NEED_NEXT;
}
}
/**
* Enumerate the distinct header names. Each nextElement() is O(n) ( a
* comparation is done with all previous elements ).
*
* This is less frequesnt than add() - we want to keep add O(1).
*/
protected class NamesEnumeration implements Enumeration {
int pos;
String next;
// toString and unique options are not implemented -
// we allways to toString and unique.
/**
* Create a new multi-map enumeration.
*
* @param headers
* the collection to enumerate
* @param toString
* convert each name to string
* @param unique
* return only unique names
*/
public NamesEnumeration() {
pos = 0;
findNext();
}
private void findNext() {
next = null;
for (; pos < count; pos++) {
next = getName(pos).toString();
for (int j = 0; j < pos; j++) {
if (getName(j).equals(next)) {
// duplicate.
next = null;
break;
}
}
if (next != null) {
// it's not a duplicate
break;
}
}
// next time findNext is called it will try the
// next element
pos++;
}
public boolean hasMoreElements() {
return next != null;
}
public Object nextElement() {
String current = next;
findNext();
return current;
}
}
protected Field[] fields;
// fields in use
protected int count;
protected boolean didQueryParameters = false;
protected boolean didMerge = false;
protected MessageBytes queryMB;
protected UDecoder urlDec;
protected MessageBytes decodedQuery = MessageBytes.newInstance();
protected String encoding = null;
protected String queryStringEncoding = null;
/**
*
*/
public Parameters() {
fields = new Field[INITIAL_SIZE];
}
public void setQuery(MessageBytes queryMB) {
this.queryMB = queryMB;
}
public void setHeaders(MimeHeaders headers) {
// Not used anymore at the moment
}
public void setEncoding(String s) {
encoding = s;
}
public void setURLDecoder(UDecoder u) {
urlDec = u;
}
public void setQueryStringEncoding(String s) {
queryStringEncoding = s;
}
public void recycle() {
for (int i = 0; i < count; i++) {
fields[i].recycle();
}
count = 0;
didQueryParameters = false;
didMerge = false;
encoding = null;
decodedQuery.recycle();
}
/**
* Returns the current number of header fields.
*/
protected int size() {
return count;
}
/**
* Returns the Nth header name This may be used to iterate through all
* header fields.
*
* An exception is thrown if the index is not valid ( <0 or >size )
*/
protected MessageBytes getName(int n) {
// n >= 0 && n < count ? headers[n].getName() : null
return fields[n].name;
}
/**
* Returns the Nth header value This may be used to iterate through all
* header fields.
*/
protected MessageBytes getValue(int n) {
return fields[n].value;
}
/**
* Create a new, unitialized entry.
*/
protected int addField() {
int len = fields.length;
int pos = count;
if (count >= len) {
if (count >= MAX_COUNT) {
throw new IllegalStateException("Parameter count exceeded allowed maximum: " + MAX_COUNT);
}
// expand header list array
Field tmp[] = new Field[pos * 2];
System.arraycopy(fields, 0, tmp, 0, len);
fields = tmp;
}
if (fields[pos] == null) {
fields[pos] = new Field();
}
count++;
return pos;
}
protected int findFirst(String name) {
for (int i = 0; i < count; i++) {
if (fields[i].name.equals(name)) {
return i;
}
}
return -1;
}
protected int findNext(int startPos) {
int next = fields[startPos].nextPos;
if (next != NEED_NEXT) {
return next;
}
// next==NEED_NEXT, we never searched for this header
MessageBytes name = fields[startPos].name;
for (int i = (startPos + 1); i < count; i++) {
if (fields[i].name.equals(name)) {
// cache the search result
fields[startPos].nextPos = i;
return i;
}
}
fields[startPos].nextPos = LAST;
return -1;
}
// -------------------- Data access --------------------
// Access to the current name/values, no side effect ( processing ).
// You must explicitely call handleQueryParameters and the post methods.
// This is the original data representation ( hash of String->String[])
public void addParameterValues(String name, String[] values) {
if (name == null || values == null) {
return;
}
for (int i = 0; i < values.length; i++) {
String value = values[i];
int pos = addField();
getName(pos).setString(name);
getValue(pos).setString(value);
}
}
public String[] getParameterValues(String name) {
handleQueryParameters();
int pos = findFirst(name);
if (pos >= 0) {
ArrayList<String> result = new ArrayList<String>();
while (pos >= 0) {
result.add(getValue(pos).toString());
pos = findNext(pos);
}
return result.toArray(ARRAY_TYPE);
} else {
return null;
}
}
public Enumeration getParameterNames() {
handleQueryParameters();
return new NamesEnumeration();
}
// Shortcut.
public String getParameter(String name) {
handleQueryParameters();
int pos = findFirst(name);
if (pos >= 0) {
return getValue(pos).toString();
} else {
return null;
}
}
// -------------------- Processing --------------------
/** Process the query string into parameters
*/
public void handleQueryParameters() {
if (didQueryParameters)
return;
didQueryParameters = true;
if (queryMB == null || queryMB.isNull())
return;
if (debug > 0)
log("Decoding query " + decodedQuery + " " + queryStringEncoding);
try {
decodedQuery.duplicate(queryMB);
} catch (IOException e) {
// Can't happen, as decodedQuery can't overflow
e.printStackTrace();
}
processParameters(decodedQuery, queryStringEncoding);
}
protected void addParam(String name, String value) {
if (name == null) {
return;
}
int pos = addField();
getName(pos).setString(name);
getValue(pos).setString(value);
}
// -------------------- Parameter parsing --------------------
// we are called from a single thread - we can do it the hard way
// if needed
protected ByteChunk tmpName = new ByteChunk();
protected ByteChunk tmpValue = new ByteChunk();
protected CharChunk tmpNameC = new CharChunk(32);
protected CharChunk tmpValueC = new CharChunk(128);
public void processParameters(MessageBytes data) {
processParameters(data, encoding);
}
public void processParameters(MessageBytes data, String encoding) {
if (data == null || data.isNull() || data.getLength() <= 0)
return;
if (data.getType() != MessageBytes.T_BYTES) {
data.toBytes();
}
ByteChunk bc = data.getByteChunk();
processParameters(bc.getBytes(), bc.getOffset(), bc.getLength(),
encoding);
}
public void processParameters(byte bytes[], int start, int len) {
processParameters(bytes, start, len, encoding);
}
public void processParameters(byte bytes[], int start, int len, String enc) {
int end = start + len;
int pos = start;
if (debug > 0)
log("Bytes: " + new String(bytes, start, len));
do {
boolean noEq = false;
int valStart = -1;
int valEnd = -1;
int nameStart = pos;
int nameEnd = ByteChunk.indexOf(bytes, nameStart, end, '=');
// Workaround for a&b&c encoding
int nameEnd2 = ByteChunk.indexOf(bytes, nameStart, end, '&');
if ((nameEnd2 != -1) && (nameEnd == -1 || nameEnd > nameEnd2)) {
nameEnd = nameEnd2;
noEq = true;
valStart = nameEnd;
valEnd = nameEnd;
if (debug > 0)
log("no equal " + nameStart + " " + nameEnd + " "
+ new String(bytes, nameStart, nameEnd - nameStart));
}
if (nameEnd == -1)
nameEnd = end;
if (!noEq) {
valStart = (nameEnd < end) ? nameEnd + 1 : end;
valEnd = ByteChunk.indexOf(bytes, valStart, end, '&');
if (valEnd == -1)
valEnd = (valStart < end) ? end : valStart;
}
pos = valEnd + 1;
if (nameEnd <= nameStart) {
log.warn("Parameters: Invalid chunk ignored.");
continue;
// invalid chunk - it's better to ignore
}
tmpName.setBytes(bytes, nameStart, nameEnd - nameStart);
tmpValue.setBytes(bytes, valStart, valEnd - valStart);
try {
addParam(urlDecode(tmpName, enc), urlDecode(tmpValue, enc));
} catch (IOException e) {
// Exception during character decoding: skip parameter
log.warn("Parameters: Character decoding failed. "
+ "Parameter skipped.", e);
}
tmpName.recycle();
tmpValue.recycle();
} while (pos < end);
}
protected String urlDecode(ByteChunk bc, String enc) throws IOException {
if (urlDec == null) {
urlDec = new UDecoder();
}
urlDec.convert(bc);
String result = null;
if (enc != null) {
bc.setEncoding(enc);
result = bc.toString();
} else {
CharChunk cc = tmpNameC;
int length = bc.getLength();
cc.allocate(length, -1);
// Default encoding: fast conversion
byte[] bbuf = bc.getBuffer();
char[] cbuf = cc.getBuffer();
int start = bc.getStart();
for (int i = 0; i < length; i++) {
cbuf[i] = (char) (bbuf[i + start] & 0xff);
}
cc.setChars(cbuf, 0, length);
result = cc.toString();
cc.recycle();
}
return result;
}
/** Debug purpose
*/
public String paramsAsString() {
StringBuffer sb = new StringBuffer();
Enumeration en = getParameterNames();
while (en.hasMoreElements()) {
String k = (String) en.nextElement();
sb.append(k).append("=");
String v[] = (String[]) getParameterValues(k);
for (int i = 0; i < v.length; i++)
sb.append(v[i]).append(",");
sb.append("\n");
}
return sb.toString();
}
private static int debug = 0;
private void log(String s) {
if (log.isDebugEnabled())
log.debug("Parameters: " + s);
}
}