blob: e1b30f56f5b462ae5a9302ebb26132ab916017f1 [file] [log] [blame]
/*
* 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.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import org.apache.tomcat.util.buf.B2CConverter;
import org.apache.tomcat.util.buf.ByteChunk;
import org.apache.tomcat.util.buf.MessageBytes;
import org.apache.tomcat.util.buf.UDecoder;
import org.apache.tomcat.util.res.StringManager;
/**
*
* @author Costin Manolache
*/
public final class Parameters {
private static final org.apache.commons.logging.Log log =
org.apache.commons.logging.LogFactory.getLog(Parameters.class);
protected static final StringManager sm =
StringManager.getManager("org.apache.tomcat.util.http");
// HashMap<String,ArrayList<String>>
private final HashMap paramHashValues = new HashMap();
private boolean didQueryParameters=false;
MessageBytes queryMB;
UDecoder urlDec;
MessageBytes decodedQuery=MessageBytes.newInstance();
String encoding=null;
String queryStringEncoding=null;
private int limit = -1;
private int parameterCount = 0;
/**
* Is set to <code>true</code> if there were failures during parameter
* parsing.
*/
private boolean parseFailed = false;
public Parameters() {
// NO-OP
}
public void setQuery( MessageBytes queryMB ) {
this.queryMB=queryMB;
}
public void setLimit(int limit) {
this.limit = limit;
}
public String getEncoding() {
return encoding;
}
public void setEncoding( String s ) {
encoding=s;
if(log.isDebugEnabled()) {
log.debug( "Set encoding to " + s );
}
}
public void setQueryStringEncoding( String s ) {
queryStringEncoding=s;
if(log.isDebugEnabled()) {
log.debug( "Set query string encoding to " + s );
}
}
public boolean isParseFailed() {
return parseFailed;
}
public void setParseFailed(boolean parseFailed) {
this.parseFailed = parseFailed;
}
public void recycle() {
parameterCount = 0;
paramHashValues.clear();
didQueryParameters=false;
encoding=null;
decodedQuery.recycle();
parseFailed = false;
}
// -------------------- Data access --------------------
// Access to the current name/values, no side effect ( processing ).
// You must explicitly call handleQueryParameters and the post methods.
public void addParameterValues(String key, String[] newValues) {
if (key == null) {
return;
}
ArrayList values = (ArrayList) paramHashValues.get(key);
if (values == null) {
values = new ArrayList(newValues.length);
paramHashValues.put(key, values);
} else {
values.ensureCapacity(values.size() + newValues.length);
}
for (int i = 0; i < newValues.length; i++) {
values.add(newValues[i]);
}
}
public String[] getParameterValues(String name) {
handleQueryParameters();
// no "facade"
ArrayList values = (ArrayList) paramHashValues.get(name);
if (values == null) {
return null;
}
return (String[]) values.toArray(new String[values.size()]);
}
public Enumeration getParameterNames() {
handleQueryParameters();
return Collections.enumeration(paramHashValues.keySet());
}
// Shortcut.
public String getParameter(String name ) {
handleQueryParameters();
ArrayList values = (ArrayList) paramHashValues.get(name);
if (values != null) {
if(values.size() == 0) {
return "";
}
return (String) values.get(0);
} 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(log.isDebugEnabled()) {
log.debug("Decoding query " + decodedQuery + " " +
queryStringEncoding);
}
try {
decodedQuery.duplicate( queryMB );
} catch (IOException e) {
// Can't happen, as decodedQuery can't overflow
e.printStackTrace();
}
processParameters( decodedQuery, queryStringEncoding );
}
private void addParam( String key, String value ) {
if( key==null ) return;
ArrayList values = (ArrayList) paramHashValues.get(key);
if (values == null) {
values = new ArrayList(1);
paramHashValues.put(key, values);
}
values.add(value);
}
public void setURLDecoder( UDecoder u ) {
urlDec=u;
}
// -------------------- Parameter parsing --------------------
// we are called from a single thread - we can do it the hard way
// if needed
ByteChunk tmpName=new ByteChunk();
ByteChunk tmpValue=new ByteChunk();
private ByteChunk origName=new ByteChunk();
private ByteChunk origValue=new ByteChunk();
private static final String DEFAULT_ENCODING = "ISO-8859-1";
private static final Charset DEFAULT_CHARSET =
Charset.forName(DEFAULT_ENCODING);
public void processParameters( byte bytes[], int start, int len ) {
processParameters(bytes, start, len, getCharset(encoding));
}
private void processParameters(byte bytes[], int start, int len,
Charset charset) {
if(log.isDebugEnabled()) {
try {
log.debug(sm.getString("parameters.bytes",
new String(bytes, start, len, DEFAULT_CHARSET.name())));
} catch (UnsupportedEncodingException uee) {
// Not possible. All JVMs must support ISO-8859-1
}
}
int decodeFailCount = 0;
int pos = start;
int end = start + len;
while(pos < end) {
parameterCount ++;
if (limit > -1 && parameterCount > limit) {
parseFailed = true;
log.info(sm.getString("parameters.maxCountFail",
Integer.toString(limit)));
break;
}
int nameStart = pos;
int nameEnd = -1;
int valueStart = -1;
int valueEnd = -1;
boolean parsingName = true;
boolean decodeName = false;
boolean decodeValue = false;
boolean parameterComplete = false;
do {
switch(bytes[pos]) {
case '=':
if (parsingName) {
// Name finished. Value starts from next character
nameEnd = pos;
parsingName = false;
valueStart = ++pos;
} else {
// Equals character in value
pos++;
}
break;
case '&':
if (parsingName) {
// Name finished. No value.
nameEnd = pos;
} else {
// Value finished
valueEnd = pos;
}
parameterComplete = true;
pos++;
break;
case '%':
case '+':
// Decoding required
if (parsingName) {
decodeName = true;
} else {
decodeValue = true;
}
pos ++;
break;
default:
pos ++;
break;
}
} while (!parameterComplete && pos < end);
if (pos == end) {
if (nameEnd == -1) {
nameEnd = pos;
} else if (valueStart > -1 && valueEnd == -1){
valueEnd = pos;
}
}
if (log.isDebugEnabled() && valueStart == -1) {
try {
log.debug(sm.getString("parameters.noequal",
Integer.toString(nameStart),
Integer.toString(nameEnd),
new String(bytes, nameStart, nameEnd-nameStart,
DEFAULT_CHARSET.name())));
} catch (UnsupportedEncodingException uee) {
// Not possible. All JVMs must support ISO-8859-1
}
}
if (nameEnd <= nameStart ) {
if (valueStart == -1) {
// &&
if (log.isDebugEnabled()) {
log.debug(sm.getString("parameters.emptyChunk"));
}
// Do not flag as error
continue;
}
// &=foo&
if (log.isInfoEnabled()) {
if (valueEnd >= nameStart && log.isDebugEnabled()) {
String extract = null;
try {
extract = new String(bytes, nameStart,
valueEnd - nameStart,
DEFAULT_CHARSET.name());
} catch (UnsupportedEncodingException uee) {
// Not possible. All JVMs must support ISO-8859-1
}
log.info(sm.getString("parameters.invalidChunk",
Integer.toString(nameStart),
Integer.toString(valueEnd),
extract));
} else {
log.info(sm.getString("parameters.invalidChunk",
Integer.toString(nameStart),
Integer.toString(nameEnd),
null));
}
}
parseFailed = true;
continue;
// invalid chunk - it's better to ignore
}
tmpName.setBytes(bytes, nameStart, nameEnd - nameStart);
if (valueStart >= 0) {
tmpValue.setBytes(bytes, valueStart, valueEnd - valueStart);
} else {
tmpValue.setBytes(bytes, 0, 0);
}
// Take copies as if anything goes wrong originals will be
// corrupted. This means original values can be logged.
// For performance - only done for debug
if (log.isDebugEnabled()) {
try {
origName.append(bytes, nameStart, nameEnd - nameStart);
if (valueStart >= 0) {
origValue.append(bytes, valueStart, valueEnd - valueStart);
} else {
origValue.append(bytes, 0, 0);
}
} catch (IOException ioe) {
// Should never happen...
log.error(sm.getString("parameters.copyFail"), ioe);
}
}
try {
String name;
String value;
if (decodeName) {
urlDecode(tmpName);
}
tmpName.setCharset(charset);
name = tmpName.toString();
if (valueStart >= 0) {
if (decodeValue) {
urlDecode(tmpValue);
}
tmpValue.setCharset(charset);
value = tmpValue.toString();
} else {
value = "";
}
addParam(name, value);
} catch (IOException e) {
parseFailed = true;
decodeFailCount++;
if (decodeFailCount == 1 || log.isDebugEnabled()) {
if (log.isDebugEnabled()) {
log.debug(sm.getString("parameters.decodeFail.debug",
origName.toString(), origValue.toString()), e);
} else if (log.isInfoEnabled()) {
log.info(sm.getString("parameters.decodeFail.info",
tmpName.toString(), tmpValue.toString()), e);
}
}
}
tmpName.recycle();
tmpValue.recycle();
// Only recycle copies if we used them
if (log.isDebugEnabled()) {
origName.recycle();
origValue.recycle();
}
}
if (decodeFailCount > 1 && !log.isDebugEnabled()) {
log.info(sm.getString("parameters.multipleDecodingFail",
Integer.toString(decodeFailCount)));
}
}
private void urlDecode(ByteChunk bc)
throws IOException {
if( urlDec==null ) {
urlDec=new UDecoder();
}
urlDec.convert(bc);
}
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(), getCharset(encoding));
}
private Charset getCharset(String encoding) {
if (encoding == null) {
return DEFAULT_CHARSET;
}
try {
return B2CConverter.getCharset(encoding);
} catch (UnsupportedEncodingException e) {
return DEFAULT_CHARSET;
}
}
/**
* Debug purpose
*/
public String paramsAsString() {
StringBuffer sb = new StringBuffer();
Iterator it = paramHashValues.entrySet().iterator();
while (it.hasNext()) {
Map.Entry e = (Map.Entry) it.next();
sb.append(e.getKey()).append('=');
ArrayList values = (ArrayList) e.getValue();
for(int i = 0; i < values.size(); i++) {
sb.append(values.get(i)).append(',');
}
sb.append('\n');
}
return sb.toString();
}
}