531 lines
16 KiB
Java
531 lines
16 KiB
Java
/*
|
|
* 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.PrintWriter;
|
|
import java.io.StringWriter;
|
|
import java.util.Enumeration;
|
|
|
|
import org.apache.tomcat.util.buf.MessageBytes;
|
|
import org.apache.tomcat.util.res.StringManager;
|
|
|
|
/**
|
|
* This class is used to contain standard internet message headers,
|
|
* used for SMTP (RFC822) and HTTP (RFC2068) messages as well as for
|
|
* MIME (RFC 2045) applications such as transferring typed data and
|
|
* grouping related items in multipart message bodies.
|
|
*
|
|
* <P> Message headers, as specified in RFC822, include a field name
|
|
* and a field body. Order has no semantic significance, and several
|
|
* fields with the same name may exist. However, most fields do not
|
|
* (and should not) exist more than once in a header.
|
|
*
|
|
* <P> Many kinds of field body must conform to a specified syntax,
|
|
* including the standard parenthesized comment syntax. This class
|
|
* supports only two simple syntaxes, for dates and integers.
|
|
*
|
|
* <P> When processing headers, care must be taken to handle the case of
|
|
* multiple same-name fields correctly. The values of such fields are
|
|
* only available as strings. They may be accessed by index (treating
|
|
* the header as an array of fields), or by name (returning an array
|
|
* of string values).
|
|
*/
|
|
|
|
/* Headers are first parsed and stored in the order they are
|
|
received. This is based on the fact that most servlets will not
|
|
directly access all headers, and most headers are single-valued.
|
|
( the alternative - a hash or similar data structure - will add
|
|
an overhead that is not needed in most cases )
|
|
|
|
Apache seems to be using a similar method for storing and manipulating
|
|
headers.
|
|
|
|
Future enhancements:
|
|
- hash the headers the first time a header is requested ( i.e. if the
|
|
servlet needs direct access to headers).
|
|
- scan "common" values ( length, cookies, etc ) during the parse
|
|
( addHeader hook )
|
|
|
|
*/
|
|
|
|
|
|
/**
|
|
* Memory-efficient repository for Mime Headers. When the object is recycled, it
|
|
* will keep the allocated headers[] and all the MimeHeaderField - no GC is generated.
|
|
*
|
|
* For input headers it is possible to use the MessageByte for Fields - so no GC
|
|
* will be generated.
|
|
*
|
|
* The only garbage is generated when using the String for header names/values -
|
|
* this can't be avoided when the servlet calls header methods, but is easy
|
|
* to avoid inside tomcat. The goal is to use _only_ MessageByte-based Fields,
|
|
* and reduce to 0 the memory overhead of tomcat.
|
|
*
|
|
* TODO:
|
|
* XXX one-buffer parsing - for http ( other protocols don't need that )
|
|
* XXX remove unused methods
|
|
* XXX External enumerations, with 0 GC.
|
|
* XXX use HeaderName ID
|
|
*
|
|
*
|
|
* @author dac@eng.sun.com
|
|
* @author James Todd [gonzo@eng.sun.com]
|
|
* @author Costin Manolache
|
|
* @author kevin seguin
|
|
*/
|
|
public class MimeHeaders {
|
|
/** Initial size - should be == average number of headers per request
|
|
* XXX make it configurable ( fine-tuning of web-apps )
|
|
*/
|
|
public static final int DEFAULT_HEADER_SIZE=8;
|
|
|
|
private static final StringManager sm =
|
|
StringManager.getManager("org.apache.tomcat.util.http");
|
|
|
|
/**
|
|
* The header fields.
|
|
*/
|
|
private MimeHeaderField[] headers = new
|
|
MimeHeaderField[DEFAULT_HEADER_SIZE];
|
|
|
|
/**
|
|
* The current number of header fields.
|
|
*/
|
|
private int count;
|
|
|
|
/**
|
|
* The limit on the number of header fields.
|
|
*/
|
|
private int limit = -1;
|
|
|
|
/**
|
|
* Creates a new MimeHeaders object using a default buffer size.
|
|
*/
|
|
public MimeHeaders() {
|
|
// NO-OP
|
|
}
|
|
|
|
/**
|
|
* Set limit on the number of header fields.
|
|
* @param limit The new limit
|
|
*/
|
|
public void setLimit(int limit) {
|
|
this.limit = limit;
|
|
if (limit > 0 && headers.length > limit && count < limit) {
|
|
// shrink header list array
|
|
MimeHeaderField tmp[] = new MimeHeaderField[limit];
|
|
System.arraycopy(headers, 0, tmp, 0, count);
|
|
headers = tmp;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clears all header fields.
|
|
*/
|
|
// [seguin] added for consistency -- most other objects have recycle().
|
|
public void recycle() {
|
|
clear();
|
|
}
|
|
|
|
/**
|
|
* Clears all header fields.
|
|
*/
|
|
public void clear() {
|
|
for (int i = 0; i < count; i++) {
|
|
headers[i].recycle();
|
|
}
|
|
count = 0;
|
|
}
|
|
|
|
/**
|
|
* EXPENSIVE!!! only for debugging.
|
|
*/
|
|
@Override
|
|
public String toString() {
|
|
StringWriter sw = new StringWriter();
|
|
PrintWriter pw = new PrintWriter(sw);
|
|
pw.println("=== MimeHeaders ===");
|
|
Enumeration<String> e = names();
|
|
while (e.hasMoreElements()) {
|
|
String n = e.nextElement();
|
|
Enumeration<String> ev = values(n);
|
|
while (ev.hasMoreElements()) {
|
|
pw.print(n);
|
|
pw.print(" = ");
|
|
pw.println(ev.nextElement());
|
|
}
|
|
}
|
|
return sw.toString();
|
|
}
|
|
|
|
|
|
public void duplicate(MimeHeaders source) throws IOException {
|
|
for (int i = 0; i < source.size(); i++) {
|
|
MimeHeaderField mhf = createHeader();
|
|
mhf.getName().duplicate(source.getName(i));
|
|
mhf.getValue().duplicate(source.getValue(i));
|
|
}
|
|
}
|
|
|
|
|
|
// -------------------- Idx access to headers ----------
|
|
|
|
/**
|
|
* @return the current number of header fields.
|
|
*/
|
|
public int size() {
|
|
return count;
|
|
}
|
|
|
|
/**
|
|
* @param n The header index
|
|
* @return the Nth header name, or null if there is no such header.
|
|
* This may be used to iterate through all header fields.
|
|
*/
|
|
public MessageBytes getName(int n) {
|
|
return n >= 0 && n < count ? headers[n].getName() : null;
|
|
}
|
|
|
|
/**
|
|
* @param n The header index
|
|
* @return the Nth header value, or null if there is no such header.
|
|
* This may be used to iterate through all header fields.
|
|
*/
|
|
public MessageBytes getValue(int n) {
|
|
return n >= 0 && n < count ? headers[n].getValue() : null;
|
|
}
|
|
|
|
/**
|
|
* Find the index of a header with the given name.
|
|
* @param name The header name
|
|
* @param starting Index on which to start looking
|
|
* @return the header index
|
|
*/
|
|
public int findHeader( String name, int starting ) {
|
|
// We can use a hash - but it's not clear how much
|
|
// benefit you can get - there is an overhead
|
|
// and the number of headers is small (4-5 ?)
|
|
// Another problem is that we'll pay the overhead
|
|
// of constructing the hashtable
|
|
|
|
// A custom search tree may be better
|
|
for (int i = starting; i < count; i++) {
|
|
if (headers[i].getName().equalsIgnoreCase(name)) {
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
// -------------------- --------------------
|
|
|
|
/**
|
|
* Returns an enumeration of strings representing the header field names.
|
|
* Field names may appear multiple times in this enumeration, indicating
|
|
* that multiple fields with that name exist in this header.
|
|
* @return the enumeration
|
|
*/
|
|
public Enumeration<String> names() {
|
|
return new NamesEnumerator(this);
|
|
}
|
|
|
|
public Enumeration<String> values(String name) {
|
|
return new ValuesEnumerator(this, name);
|
|
}
|
|
|
|
// -------------------- Adding headers --------------------
|
|
|
|
|
|
/**
|
|
* Adds a partially constructed field to the header. This
|
|
* field has not had its name or value initialized.
|
|
*/
|
|
private MimeHeaderField createHeader() {
|
|
if (limit > -1 && count >= limit) {
|
|
throw new IllegalStateException(sm.getString(
|
|
"headers.maxCountFail", Integer.valueOf(limit)));
|
|
}
|
|
MimeHeaderField mh;
|
|
int len = headers.length;
|
|
if (count >= len) {
|
|
// expand header list array
|
|
int newLength = count * 2;
|
|
if (limit > 0 && newLength > limit) {
|
|
newLength = limit;
|
|
}
|
|
MimeHeaderField tmp[] = new MimeHeaderField[newLength];
|
|
System.arraycopy(headers, 0, tmp, 0, len);
|
|
headers = tmp;
|
|
}
|
|
if ((mh = headers[count]) == null) {
|
|
headers[count] = mh = new MimeHeaderField();
|
|
}
|
|
count++;
|
|
return mh;
|
|
}
|
|
|
|
/**
|
|
* Create a new named header , return the MessageBytes
|
|
* container for the new value
|
|
* @param name The header name
|
|
* @return the message bytes container for the value
|
|
*/
|
|
public MessageBytes addValue( String name ) {
|
|
MimeHeaderField mh = createHeader();
|
|
mh.getName().setString(name);
|
|
return mh.getValue();
|
|
}
|
|
|
|
/**
|
|
* Create a new named header using un-translated byte[].
|
|
* The conversion to chars can be delayed until
|
|
* encoding is known.
|
|
* @param b The header name bytes
|
|
* @param startN Offset
|
|
* @param len Length
|
|
* @return the message bytes container for the value
|
|
*/
|
|
public MessageBytes addValue(byte b[], int startN, int len) {
|
|
MimeHeaderField mhf=createHeader();
|
|
mhf.getName().setBytes(b, startN, len);
|
|
return mhf.getValue();
|
|
}
|
|
|
|
/**
|
|
* Allow "set" operations, which removes all current values
|
|
* for this header.
|
|
* @param name The header name
|
|
* @return the message bytes container for the value
|
|
*/
|
|
public MessageBytes setValue( String name ) {
|
|
for ( int i = 0; i < count; i++ ) {
|
|
if(headers[i].getName().equalsIgnoreCase(name)) {
|
|
for ( int j=i+1; j < count; j++ ) {
|
|
if(headers[j].getName().equalsIgnoreCase(name)) {
|
|
removeHeader(j--);
|
|
}
|
|
}
|
|
return headers[i].getValue();
|
|
}
|
|
}
|
|
MimeHeaderField mh = createHeader();
|
|
mh.getName().setString(name);
|
|
return mh.getValue();
|
|
}
|
|
|
|
//-------------------- Getting headers --------------------
|
|
/**
|
|
* Finds and returns a header field with the given name. If no such
|
|
* field exists, null is returned. If more than one such field is
|
|
* in the header, an arbitrary one is returned.
|
|
* @param name The header name
|
|
* @return the value
|
|
*/
|
|
public MessageBytes getValue(String name) {
|
|
for (int i = 0; i < count; i++) {
|
|
if (headers[i].getName().equalsIgnoreCase(name)) {
|
|
return headers[i].getValue();
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Finds and returns a unique header field with the given name. If no such
|
|
* field exists, null is returned. If the specified header field is not
|
|
* unique then an {@link IllegalArgumentException} is thrown.
|
|
* @param name The header name
|
|
* @return the value if unique
|
|
* @throws IllegalArgumentException if the header has multiple values
|
|
*/
|
|
public MessageBytes getUniqueValue(String name) {
|
|
MessageBytes result = null;
|
|
for (int i = 0; i < count; i++) {
|
|
if (headers[i].getName().equalsIgnoreCase(name)) {
|
|
if (result == null) {
|
|
result = headers[i].getValue();
|
|
} else {
|
|
throw new IllegalArgumentException();
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// bad shortcut - it'll convert to string ( too early probably,
|
|
// encoding is guessed very late )
|
|
public String getHeader(String name) {
|
|
MessageBytes mh = getValue(name);
|
|
return mh != null ? mh.toString() : null;
|
|
}
|
|
|
|
// -------------------- Removing --------------------
|
|
/**
|
|
* Removes a header field with the specified name. Does nothing
|
|
* if such a field could not be found.
|
|
* @param name the name of the header field to be removed
|
|
*/
|
|
public void removeHeader(String name) {
|
|
// XXX
|
|
// warning: rather sticky code; heavily tuned
|
|
|
|
for (int i = 0; i < count; i++) {
|
|
if (headers[i].getName().equalsIgnoreCase(name)) {
|
|
removeHeader(i--);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* reset and swap with last header
|
|
* @param idx the index of the header to remove.
|
|
*/
|
|
public void removeHeader(int idx) {
|
|
MimeHeaderField mh = headers[idx];
|
|
|
|
mh.recycle();
|
|
headers[idx] = headers[count - 1];
|
|
headers[count - 1] = mh;
|
|
count--;
|
|
}
|
|
|
|
}
|
|
|
|
/** Enumerate the distinct header names.
|
|
Each nextElement() is O(n) ( a comparison is
|
|
done with all previous elements ).
|
|
|
|
This is less frequent than add() -
|
|
we want to keep add O(1).
|
|
*/
|
|
class NamesEnumerator implements Enumeration<String> {
|
|
private int pos;
|
|
private final int size;
|
|
private String next;
|
|
private final MimeHeaders headers;
|
|
|
|
public NamesEnumerator(MimeHeaders headers) {
|
|
this.headers=headers;
|
|
pos=0;
|
|
size = headers.size();
|
|
findNext();
|
|
}
|
|
|
|
private void findNext() {
|
|
next=null;
|
|
for(; pos< size; pos++ ) {
|
|
next=headers.getName( pos ).toString();
|
|
for( int j=0; j<pos ; j++ ) {
|
|
if( headers.getName( j ).equalsIgnoreCase( 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++;
|
|
}
|
|
|
|
@Override
|
|
public boolean hasMoreElements() {
|
|
return next!=null;
|
|
}
|
|
|
|
@Override
|
|
public String nextElement() {
|
|
String current=next;
|
|
findNext();
|
|
return current;
|
|
}
|
|
}
|
|
|
|
/** Enumerate the values for a (possibly ) multiple
|
|
value element.
|
|
*/
|
|
class ValuesEnumerator implements Enumeration<String> {
|
|
private int pos;
|
|
private final int size;
|
|
private MessageBytes next;
|
|
private final MimeHeaders headers;
|
|
private final String name;
|
|
|
|
ValuesEnumerator(MimeHeaders headers, String name) {
|
|
this.name=name;
|
|
this.headers=headers;
|
|
pos=0;
|
|
size = headers.size();
|
|
findNext();
|
|
}
|
|
|
|
private void findNext() {
|
|
next=null;
|
|
for(; pos< size; pos++ ) {
|
|
MessageBytes n1=headers.getName( pos );
|
|
if( n1.equalsIgnoreCase( name )) {
|
|
next=headers.getValue( pos );
|
|
break;
|
|
}
|
|
}
|
|
pos++;
|
|
}
|
|
|
|
@Override
|
|
public boolean hasMoreElements() {
|
|
return next!=null;
|
|
}
|
|
|
|
@Override
|
|
public String nextElement() {
|
|
MessageBytes current=next;
|
|
findNext();
|
|
return current.toString();
|
|
}
|
|
}
|
|
|
|
class MimeHeaderField {
|
|
|
|
private final MessageBytes nameB = MessageBytes.newInstance();
|
|
private final MessageBytes valueB = MessageBytes.newInstance();
|
|
|
|
/**
|
|
* Creates a new, uninitialized header field.
|
|
*/
|
|
public MimeHeaderField() {
|
|
// NO-OP
|
|
}
|
|
|
|
public void recycle() {
|
|
nameB.recycle();
|
|
valueB.recycle();
|
|
}
|
|
|
|
public MessageBytes getName() {
|
|
return nameB;
|
|
}
|
|
|
|
public MessageBytes getValue() {
|
|
return valueB;
|
|
}
|
|
}
|