MimeHeaders.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.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.apache.tomcat.util.buf.MessageBytes;
import org.apache.tomcat.util.buf.StringUtils;
import org.apache.tomcat.util.res.StringManager;
/**
* 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.
* <p>
* For input headers it is possible to use the MessageByte for Fields - so no GC will be generated.
* <p>
* 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.
* <p>
* 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).
* <p>
* 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)
* <p>
* Apache seems to be using a similar method for storing and manipulating
* headers.
*
* @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
*/
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.
*/
public void recycle() {
for (int i = 0; i < count; i++) {
headers[i].recycle();
}
count = 0;
}
@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 Map<String,String> toMap() {
if (count == 0) {
return Collections.emptyMap();
}
Map<String,String> result = new HashMap<>();
for (int i = 0; i < count; i++) {
String name = headers[i].getName().toStringType();
String value = headers[i].getValue().toStringType();
result.merge(name, value, StringUtils::join);
}
return result;
}
public void filter(Set<String> allowedHeaders) {
int j = -1;
for (int i = 0; i < count; i++) {
String name = headers[i].getName().toStringType();
if (allowedHeaders.contains(name)) {
++j;
if (j != i) {
headers[j] = headers[i];
}
}
}
count = ++j;
}
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;
}
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) {
for (int i = 0; i < count; i++) {
if (headers[i].getName().equalsIgnoreCase(name)) {
removeHeader(i--);
}
}
}
/**
* Reset, move to the end and then reduce count by 1.
*
* @param idx the index of the header to remove.
*/
public void removeHeader(int idx) {
// Implementation note. This method must not change the order of the
// remaining headers because, if there are multiple header values for
// the same name, the order of those headers is significant. It is
// simpler to retain order for all values than try to determine if there
// are multiple header values for the same name.
// Clear the header to remove
MimeHeaderField mh = headers[idx];
mh.recycle();
// Move the remaining headers
System.arraycopy(headers, idx + 1, headers, idx, count - idx - 1);
// Place the removed header at the end
headers[count - 1] = mh;
// Reduce the count
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;
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).toStringType();
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.toStringType();
}
}
class MimeHeaderField {
private final MessageBytes nameB = MessageBytes.newInstance();
private final MessageBytes valueB = MessageBytes.newInstance();
/**
* Creates a new, uninitialized header field.
*/
MimeHeaderField() {
// NO-OP
}
public void recycle() {
nameB.recycle();
valueB.recycle();
}
public MessageBytes getName() {
return nameB;
}
public MessageBytes getValue() {
return valueB;
}
@Override
public String toString() {
return nameB + ": " + valueB;
}
}