org.eclipse.jetty.http.HttpField Maven / Gradle / Ivy
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.http;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import org.eclipse.jetty.util.QuotedStringTokenizer;
import org.eclipse.jetty.util.StringUtil;
/**
* An immutable class representing an HTTP header or trailer.
* {@link HttpField} has a case-insensitive name and case sensitive value,
* and may be multi-valued with each value separated by a comma.
*
* @see HttpFields
*/
public class HttpField
{
/**
* A constant {@link QuotedStringTokenizer} configured for quoting/tokenizing {@code parameters} lists as defined by
* RFC9110
*/
public static final QuotedStringTokenizer PARAMETER_TOKENIZER = QuotedStringTokenizer.builder().delimiters(";").ignoreOptionalWhiteSpace().allowEmbeddedQuotes().returnQuotes().build();
/**
* A constant {@link QuotedStringTokenizer} configured for quoting/tokenizing a single {@code parameter} as defined by
* RFC9110
*/
public static final QuotedStringTokenizer NAME_VALUE_TOKENIZER = QuotedStringTokenizer.builder().delimiters("=").build();
private static final String ZERO_QUALITY = "q=0";
private final HttpHeader _header;
private final String _name;
private final String _value;
private int _hash = 0;
/**
* Creates a new {@link HttpField} with the given {@link HttpHeader},
* name string and value string.
* A {@code null} field value may be passed as parameter, and will
* be converted to the empty string.
* This allows the direct constructions of fields that have no value,
* and/or {@link HttpField} subclasses that override {@link #getValue()}.
*
* @param header the {@link HttpHeader} referencing a well-known HTTP header name;
* may be {@code null} in case of an unknown or custom HTTP header name
* @param name the field name; if {@code null}, then {@link HttpHeader#asString()} is used
* @param value the field value; if {@code null}, the empty string will be used
*/
public HttpField(HttpHeader header, String name, String value)
{
_header = header;
if (_header != null && name == null)
_name = _header.asString();
else
_name = Objects.requireNonNull(name, "name");
_value = value != null ? value : "";
}
/**
* Creates a new {@link HttpField} with the given {@link HttpHeader}
* and value string.
*
* @param header the non-{@code null} {@link HttpHeader} referencing a well-known HTTP header name
* @param value the field value; if {@code null}, the empty string will be used
*/
public HttpField(HttpHeader header, String value)
{
this(header, header.asString(), value);
}
/**
* Creates a new {@link HttpField} with the given {@link HttpHeader}
* and value.
*
* @param header the non-{@code null} {@link HttpHeader} referencing a well-known HTTP header name
* @param value the field value; if {@code null}, the empty string will be used
*/
public HttpField(HttpHeader header, HttpHeaderValue value)
{
this(header, header.asString(), value != null ? value.asString() : null);
}
/**
* Creates a new {@link HttpField} with the given name string
* and value string.
*
* @param name the non-{@code null} field name
* @param value the field value; if {@code null}, the empty string will be used
*/
public HttpField(String name, String value)
{
this(HttpHeader.CACHE.get(name), name, value);
}
/**
* Returns the field value and its parameters.
* A field value may have parameters, typically separated by {@code ;}, for example
* {@code
* Content-Type: text/plain; charset=UTF-8
* Accept: text/html, text/plain; q=0.5
* }
*
* @param valueParams the field value, possibly with parameters
* @param parameters An output map to populate with the parameters,
* or {@code null} to strip the parameters
* @return the field value without parameters
* @see #stripParameters(String)
*/
public static String getValueParameters(String valueParams, Map parameters)
{
if (valueParams == null)
return null;
Iterator tokens = PARAMETER_TOKENIZER.tokenize(valueParams);
if (!tokens.hasNext())
return null;
String value = tokens.next();
if (parameters != null)
{
while (tokens.hasNext())
{
String token = tokens.next();
Iterator nameValue = NAME_VALUE_TOKENIZER.tokenize(token);
if (nameValue.hasNext())
{
String paramName = nameValue.next();
String paramVal = null;
if (nameValue.hasNext())
paramVal = nameValue.next();
parameters.put(paramName, paramVal);
}
}
}
return value;
}
/**
* Returns the field value, stripped of its parameters.
*
* @param value the field value, possibly with parameters
* @return the field value without parameters
* @see #getValueParameters(String, Map)
*/
public static String stripParameters(String value)
{
return getValueParameters(value, null);
}
/**
* @deprecated use {@link #getValueParameters(String, Map)} instead
*/
@Deprecated
public static String valueParameters(String value, Map parameters)
{
return getValueParameters(value, parameters);
}
/**
* Returns whether this field value, possibly multi-valued,
* contains the specified search string, case-insensitively.
* Only values, and not parameters, are compared with the
* search string.
*
* @param search the string to search for
* @return whether this field value, possibly multi-valued,
* contains the specified search string
*/
public boolean contains(String search)
{
return contains(getValue(), search);
}
/**
* Returns whether the given value, possibly multi-valued,
* contains the specified search string, case-insensitively.
* Only values, and not parameters, are compared with the
* search string.
*
* @param value the value string to search into
* @param search the string to search for
* @return whether the given value, possibly multi-valued,
* contains the specified search string
*/
public static boolean contains(String value, String search)
{
if (search == null)
return value == null;
if (search.isEmpty())
return false;
if (value == null)
return false;
if (search.equalsIgnoreCase(value))
return true;
int state = 0;
int match = 0;
int param = 0;
for (int i = 0; i < value.length(); i++)
{
char c = StringUtil.asciiToLowerCase(value.charAt(i));
switch (state)
{
case 0 -> // initial white space
{
switch (c)
{
case '"': // open quote
match = 0;
state = 2;
break;
case ',': // ignore leading empty field
break;
case ';': // ignore leading empty field parameter
param = -1;
match = -1;
state = 5;
break;
case ' ': // more white space
case '\t':
break;
default: // character
match = c == StringUtil.asciiToLowerCase(search.charAt(0)) ? 1 : -1;
state = 1;
break;
}
}
case 1 -> // In token
{
switch (c)
{
case ',' -> // next field
{
// Have we matched the token?
if (match == search.length())
return true;
state = 0;
}
case ';' ->
{
param = match >= 0 ? 0 : -1;
state = 5; // parameter
}
default ->
{
if (match > 0)
{
if (match < search.length())
match = c == StringUtil.asciiToLowerCase(search.charAt(match)) ? (match + 1) : -1;
else if (c != ' ' && c != '\t')
match = -1;
}
}
}
}
case 2 -> // In Quoted token
{
switch (c)
{
case '\\' -> state = 3; // quoted character
case '"' -> state = 4; // end quote
default ->
{
if (match >= 0)
{
if (match < search.length())
match = c == StringUtil.asciiToLowerCase(search.charAt(match)) ? (match + 1) : -1;
else
match = -1;
}
}
}
}
case 3 -> // In Quoted character in quoted token
{
if (match >= 0)
{
if (match < search.length())
match = c == StringUtil.asciiToLowerCase(search.charAt(match)) ? (match + 1) : -1;
else
match = -1;
}
state = 2;
}
case 4 -> // WS after end quote
{
switch (c)
{
case ' ': // white space
case '\t': // white space
break;
case ';':
state = 5; // parameter
break;
case ',': // end token
// Have we matched the token?
if (match == search.length())
return true;
state = 0;
break;
default:
// This is an illegal token, just ignore
match = -1;
}
}
case 5 -> // parameter
{
switch (c)
{
case ',': // end token
// Have we matched the token and not q=0?
if (param != ZERO_QUALITY.length() && match == search.length())
return true;
param = 0;
state = 0;
break;
case ' ': // white space
case '\t': // white space
break;
default:
if (param >= 0)
{
if (param < ZERO_QUALITY.length())
param = c == ZERO_QUALITY.charAt(param) ? (param + 1) : -1;
else if (c != '0' && c != '.')
param = -1;
}
}
}
default -> throw new IllegalStateException();
}
}
return param != ZERO_QUALITY.length() && match == search.length();
}
/**
* Look for a value as the last value in a possible multivalued field
* Parameters and specifically quality parameters are not considered.
* @param search Values to search for (case-insensitive)
* @return True iff the value is contained in the field value entirely or
* as the last element of a quoted comma separated list.
*/
public boolean containsLast(String search)
{
return containsLast(getValue(), search);
}
/**
* Look for the last value in a possible multivalued field
* Parameters and specifically quality parameters are not considered.
* @param value The field value to search in.
* @param search Values to search for (case-insensitive)
* @return True iff the value is contained in the field value entirely or
* as the last element of a quoted comma separated list.
*/
public static boolean containsLast(String value, String search)
{
if (search == null)
return value == null;
if (search.isEmpty())
return false;
if (value == null)
return false;
if (search.equalsIgnoreCase(value))
return true;
if (value.endsWith(search))
{
int i = value.length() - search.length() - 1;
while (i >= 0)
{
char c = value.charAt(i--);
if (c == ',')
return true;
if (c != ' ')
return false;
}
return true;
}
QuotedCSV csv = new QuotedCSV(false, value);
List values = csv.getValues();
return !values.isEmpty() && search.equalsIgnoreCase(values.get(values.size() - 1));
}
@Override
public int hashCode()
{
int vhc = Objects.hashCode(getValue());
if (_header == null)
return vhc ^ nameHashCode();
return vhc ^ _header.hashCode();
}
@Override
public boolean equals(Object o)
{
if (o == this)
return true;
if (!(o instanceof HttpField field))
return false;
if (_header != field.getHeader())
return false;
if (!_name.equalsIgnoreCase(field.getName()))
return false;
return Objects.equals(getValue(), field.getValue());
}
/**
* Get the {@link HttpHeader} of this field, or {@code null}.
* @return the {@link HttpHeader} of this field, or {@code null}
*/
public HttpHeader getHeader()
{
return _header;
}
/**
* @return the value of this field as an {@code int}
* @throws NumberFormatException if the value cannot be parsed as an {@code int}
*/
public int getIntValue()
{
return Integer.parseInt(getValue());
}
/**
* @return the value of this field as an {@code long}
* @throws NumberFormatException if the value cannot be parsed as an {@code long}
*/
public long getLongValue()
{
return Long.parseLong(getValue());
}
/**
* Get the field name in lower-case.
* @return the field name in lower-case
*/
public String getLowerCaseName()
{
return _header != null ? _header.lowerCaseName() : StringUtil.asciiToLowerCase(_name);
}
/**
* Get the field name.
* @return the field name
*/
public String getName()
{
return _name;
}
/**
* Get the field value.
* @return the field value
*/
public String getValue()
{
return _value;
}
/**
* @return the field values as a {@code String[]}
* @see #getValueList()
*/
public String[] getValues()
{
List values = getValueList();
if (values == null)
return null;
return values.toArray(String[]::new);
}
/**
* Returns a list of the field values.
* If the field value is multi-valued, the encoded field value is split
* into multiple values using {@link QuotedCSV} and the different values
* returned in a list.
* If the field value is single-valued, the value is wrapped into a list.
*
* @return a list of the field values
*/
public List getValueList()
{
String value = getValue();
if (value == null)
return null;
QuotedCSV list = new QuotedCSV(false, value);
return list.getValues();
}
/**
* Returns whether this field has the same name as the given field.
* The comparison of field name is case-insensitive via
* {@link #is(String)}.
*
* @param field the field to compare the name to
* @return whether this field has the same name as the given field
*/
public boolean isSameName(HttpField field)
{
if (field == null)
return false;
if (field == this)
return true;
if (_header != null && _header == field.getHeader())
return true;
return is(field.getName());
}
/**
* Returns whether this field name is the same as the given string.
* The comparison of field name is case-insensitive.
*
* @param name the field name to compare to
* @return whether this field name is the same as the given string
*/
public boolean is(String name)
{
return _name.equalsIgnoreCase(name);
}
/**
* Return a {@link HttpField} without a given value (case-insensitive)
* @param value The value to remove
* @return A new {@link HttpField} if the value was removed, but others remain; this {@link HttpField} if it
* did not contain the value; or {@code null} if the value was the only value.
*/
public HttpField withoutValue(String value)
{
if (_value.length() < value.length())
return this;
if (_value.equalsIgnoreCase(value))
return null;
if (_value.length() == value.length())
return this;
QuotedCSV csv = new QuotedCSV(false, _value);
boolean removed = false;
for (Iterator i = csv.iterator(); i.hasNext();)
{
String element = i.next();
if (element.equalsIgnoreCase(value))
{
removed = true;
i.remove();
}
}
if (!removed)
return this;
return new HttpField(_header, _name, csv.asString());
}
private int nameHashCode()
{
int h = this._hash;
int len = _name.length();
if (h == 0 && len > 0)
{
for (int i = 0; i < len; i++)
{
// simple case insensitive hash
char c = _name.charAt(i);
// assuming us-ascii (per last paragraph on http://tools.ietf.org/html/rfc7230#section-3.2.4)
if ((c >= 'a' && c <= 'z'))
c -= 0x20;
h = 31 * h + c;
}
this._hash = h;
}
return h;
}
@Override
public String toString()
{
String v = getValue();
return getName() + ": " + (v == null ? "" : v);
}
/**
* A specialized {@link HttpField} whose value is an {@code int}.
*/
public static class IntValueHttpField extends HttpField
{
private final int _int;
public IntValueHttpField(HttpHeader header, String name, String value, int intValue)
{
super(header, name, value);
_int = intValue;
}
public IntValueHttpField(HttpHeader header, String name, String value)
{
this(header, name, value, Integer.parseInt(value));
}
public IntValueHttpField(HttpHeader header, String name, int intValue)
{
this(header, name, Integer.toString(intValue), intValue);
}
public IntValueHttpField(HttpHeader header, int value)
{
this(header, header.asString(), value);
}
public IntValueHttpField(String header, int value)
{
this(HttpHeader.CACHE.get(header), header, value);
}
@Override
public int getIntValue()
{
return _int;
}
@Override
public long getLongValue()
{
return _int;
}
}
/**
* A specialized {@link HttpField} whose value is a {@code long}.
*/
public static class LongValueHttpField extends HttpField
{
private final long _long;
public LongValueHttpField(HttpHeader header, String name, String value, long longValue)
{
super(header, name, value);
_long = longValue;
}
public LongValueHttpField(HttpHeader header, String name, String value)
{
this(header, name, value, Long.parseLong(value));
}
public LongValueHttpField(HttpHeader header, String name, long value)
{
this(header, name, Long.toString(value), value);
}
public LongValueHttpField(HttpHeader header, long value)
{
this(header, header.asString(), value);
}
public LongValueHttpField(String header, long value)
{
this(HttpHeader.CACHE.get(header), header, value);
}
@Override
public int getIntValue()
{
return Math.toIntExact(_long);
}
@Override
public long getLongValue()
{
return _long;
}
}
static class MultiHttpField extends HttpField
{
private final List _list;
public MultiHttpField(String name, List list)
{
super(name, buildValue(list));
_list = list;
}
private static String buildValue(List list)
{
StringBuilder builder = null;
for (String v : list)
{
if (StringUtil.isBlank(v))
throw new IllegalArgumentException("blank element");
if (builder == null)
builder = new StringBuilder(list.size() * (v == null ? 5 : v.length()) * 2);
else
builder.append(", ");
builder.append(v);
}
return builder == null ? null : builder.toString();
}
@Override
public List getValueList()
{
return _list;
}
@Override
public boolean contains(String search)
{
for (String v : _list)
if (StringUtil.asciiEqualsIgnoreCase(v, search))
return true;
return false;
}
}
}