org.eclipse.jetty.http.HttpFields 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.AbstractSet;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.TimeUnit;
import java.util.function.BiFunction;
import java.util.function.BiPredicate;
import java.util.function.Supplier;
import java.util.function.ToIntFunction;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
/**
* An ordered collection of {@link HttpField}s that represent the HTTP headers
* or HTTP trailers of an HTTP request or an HTTP response.
*
* {@link HttpFields} is immutable and typically used in server-side HTTP requests
* and client-side HTTP responses, while {@link HttpFields.Mutable} is mutable and
* typically used in server-side HTTP responses and client-side HTTP requests.
*
* Access is always more efficient using {@link HttpHeader} keys rather than {@link String} field names.
*
* The primary implementations of {@code HttpFields} have been optimized assuming few
* lookup operations, thus typically if many {@link HttpField}s need to looked up, it may be
* better to use an {@link Iterator} to find multiple fields in a single iteration.
*/
public interface HttpFields extends Iterable, Supplier
{
/**
* A constant {@link HttpField} for the HTTP header:
* {@code Expires: Thu, 01 Jan 1970 00:00:00 GMT}
*/
HttpField EXPIRES_01JAN1970 = new PreEncodedHttpField(HttpHeader.EXPIRES, DateGenerator.__01Jan1970);
/**
* A constant {@link HttpField} for the HTTP header:
* {@code Connection: close}
*/
HttpField CONNECTION_CLOSE = new PreEncodedHttpField(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE.asString());
/**
* A constant {@link HttpField} for the HTTP header:
* {@code Connection: keep-alive}
*/
HttpField CONNECTION_KEEPALIVE = new PreEncodedHttpField(HttpHeader.CONNECTION, HttpHeaderValue.KEEP_ALIVE.asString());
/**
* A constant {@link HttpField} for the HTTP header:
* {@code Content-Length: 0}
*/
HttpField CONTENT_LENGTH_0 = new PreEncodedHttpField(HttpHeader.CONTENT_LENGTH, 0L);
/**
* A constant for an immutable and empty {@link HttpFields}.
*/
HttpFields EMPTY = new EmptyHttpFields();
/**
* Returns an empty {@link Mutable} instance.
*
* @return an empty {@link Mutable} instance
*/
static Mutable build()
{
return new org.eclipse.jetty.http.MutableHttpFields();
}
/**
* Returns an empty {@link Mutable} instance with the given initial {@code capacity}.
* The given {@code capacity} indicates the initial capacity of the storage for the
* {@link HttpField}s.
* When the capacity is exceeded, the storage is resized to accommodate the additional
* {@link HttpField}s.
*
* @param capacity the initial capacity of the storage for the {@link HttpField}s
* @return an empty instance with the given initial {@code capacity}
*/
static Mutable build(int capacity)
{
return new org.eclipse.jetty.http.MutableHttpFields(capacity);
}
/**
* Returns a new {@link Mutable} instance containing a copy of all the
* {@link HttpField}s of the given {@link HttpFields} parameter.
*
* @param fields the {@link HttpFields} to copy
* @return a new {@link Mutable} instance containing a copy of the given {@link HttpFields}
*/
static Mutable build(HttpFields fields)
{
return new org.eclipse.jetty.http.MutableHttpFields(fields);
}
/**
* Returns a new {@link Mutable} instance containing a copy of all the
* {@link HttpField}s of the given {@link HttpFields}, replacing with the
* given {@link HttpField} all the fields {@link HttpField}s with the same
* name.
* All existing {@link HttpField}s with the same name as the
* replacing {@link HttpField} will be replaced by the given {@link HttpField}.
* If there are no fields with that name, the given {@link HttpField}
* is added to the returned {@link Mutable} instance.
*
* @param fields the {@link HttpFields} to copy
* @param replaceField the {@link HttpField} that replaces others with the same name
* @return a new {@link Mutable} instance containing the given {@link HttpFields}
* and the given replacement {@link HttpField}
*/
static Mutable build(HttpFields fields, HttpField replaceField)
{
return new org.eclipse.jetty.http.MutableHttpFields(fields, replaceField);
}
/**
* Returns a new {@link Mutable} instance containing a copy of all the
* {@link HttpField}s of the given {@link HttpFields}, removing the
* {@link HttpField}s with the given names.
*
* @param fields the {@link HttpFields} to copy
* @param removeFields the names of the fields to remove
* @return a new {@link Mutable} instance containing the given {@link HttpFields}
* without the fields with the given names
*/
static Mutable build(HttpFields fields, EnumSet removeFields)
{
return new org.eclipse.jetty.http.MutableHttpFields(fields, removeFields);
}
/**
* Returns an immutable {@link HttpFields} instance containing the given
* {@link HttpField}s.
*
* @param fields the {@link HttpField}s to be contained in the returned instance
* @return a new immutable {@link HttpFields} instance with the given {@link HttpField}s
*/
static HttpFields from(HttpField... fields)
{
return new org.eclipse.jetty.http.ImmutableHttpFields(fields);
}
/**
* Supplies this instance, typically used to supply HTTP trailers.
*
* @return this instance
*/
@Override
default HttpFields get()
{
return this;
}
@Override
default Iterator iterator()
{
return listIterator();
}
/**
* @return an iterator over the {@link HttpField}s in this {@code HttpFields}.
* @see #listIterator(int)
*/
default ListIterator listIterator()
{
return listIterator(0);
}
/**
* @return an iterator over the {@link HttpField}s in this {@code HttpFields} starting at the given index.
* @see #listIterator()
*/
ListIterator listIterator(int index);
/**
* Returns an immutable copy of this {@link HttpFields} instance.
*
* @return a new immutable copy of this {@link HttpFields} instance
*/
default HttpFields asImmutable()
{
return HttpFields.build(this).asImmutable();
}
/**
* Returns the HTTP/1.1 string representation of the {@link HttpField}s of this instance.
* The format of each field is: {@code : \r\n}, and there will be
* an additional {@code \r\n} after the last field, for example:
* {@code
* Host: localhost\r\n
* Connection: close\r\n
* \r\n
* }
*
* @return an HTTP/1.1 string representation of the {@link HttpField}s of this instance
*/
default String asString()
{
StringBuilder buffer = new StringBuilder();
for (HttpField field : this)
{
if (field != null)
{
String tmp = field.getName();
if (tmp != null)
buffer.append(tmp);
buffer.append(": ");
tmp = field.getValue();
if (tmp != null)
buffer.append(tmp);
buffer.append("\r\n");
}
}
buffer.append("\r\n");
return buffer.toString();
}
/**
* Returns whether this instance contains the given {@link HttpField}.
* The comparison of {@link HttpField} names is case-insensitive via
* {@link HttpField#isSameName(HttpField)}.
* The comparison of the field value is case-insensitive via
* {@link HttpField#contains(String)}.
*
* @param field the {@link HttpField} to search for
* @return whether this instance contains the given {@link HttpField}
*/
default boolean contains(HttpField field)
{
for (HttpField f : this)
{
if (f.isSameName(field) && (f.equals(field) || f.contains(field.getValue())))
return true;
}
return false;
}
/**
* Returns whether this instance contains the given {@link HttpHeader}
* with the given value.
* The comparison of the field value is case-insensitive via
* {@link HttpField#contains(String)}.
*
* @param header the name of the field to search for
* @param value the value to search for
* @return whether this instance contains the given
* {@link HttpHeader} with the given value
*/
default boolean contains(HttpHeader header, String value)
{
for (HttpField f : this)
{
if (f.getHeader() == header && f.contains(value))
return true;
}
return false;
}
/**
* Look for a value as the last value in a possible multivalued field.
* Parameters and specifically quality parameters are not considered.
* @param header The {@link HttpHeader} type to search for.
* @param value The value 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.
* @see HttpField#containsLast(String)
*/
default boolean containsLast(HttpHeader header, String value)
{
for (ListIterator i = listIterator(size()); i.hasPrevious();)
{
HttpField f = i.previous();
if (f.getHeader() == header)
return f.containsLast(value);
}
return false;
}
/**
* Returns whether this instance contains the given field name
* with the given value.
* The comparison of field name is case-insensitive via
* {@link HttpField#is(String)}.
* The comparison of the field value is case-insensitive via
* {@link HttpField#contains(String)}.
*
* @param name the case-insensitive field name to search for
* @param value the field value to search for
* @return whether this instance contains the given
* field name with the given value
*/
default boolean contains(String name, String value)
{
for (HttpField f : this)
{
if (f.is(name) && f.contains(value))
return true;
}
return false;
}
/**
* Returns whether this instance contains the given field name.
*
* @param header the field name to search for
* @return whether this instance contains the given field name
*/
default boolean contains(HttpHeader header)
{
for (HttpField f : this)
{
if (f.getHeader() == header)
return true;
}
return false;
}
/**
* Returns whether this instance contains at least one of the given
* field names.
*
* @param headers the field names to search among
* @return whether this instance contains at least one of the given field names
*/
default boolean contains(EnumSet headers)
{
for (HttpField f : this)
{
if (headers.contains(f.getHeader()))
return true;
}
return false;
}
/**
* Returns whether this instance contains the given field name.
* The comparison of field name is case-insensitive via
* {@link HttpField#is(String)}. If possible, it is more efficient to use
* {@link #contains(HttpHeader)}.
*
* @param name the case-insensitive field name to search for
* @return whether this instance contains the given field name
* @see #contains(HttpHeader)
*/
default boolean contains(String name)
{
for (HttpField f : this)
{
if (f.is(name))
return true;
}
return false;
}
/**
*
Returns the encoded value of the first field with the given field name,
* or {@code null} if no such header is present.
* In case of multi-valued fields, the returned value is the encoded
* value, including commas and quotes, as returned by {@link HttpField#getValue()}.
*
* @param header the field name to search for
* @return the raw value of the first field with the given field name,
* or {@code null} if no such header is present
* @see HttpField#getValue()
*/
default String get(HttpHeader header)
{
for (HttpField f : this)
{
if (f.getHeader() == header)
return f.getValue();
}
return null;
}
/**
* Returns the encoded value of the last field with the given field name,
* or {@code null} if no such header is present.
* In case of multi-valued fields, the returned value is the encoded
* value, including commas and quotes, as returned by {@link HttpField#getValue()}.
*
* @param header the field name to search for
* @return the raw value of the last field with the given field name,
* or {@code null} if no such header is present
* @see HttpField#getValue()
*/
default String getLast(HttpHeader header)
{
for (ListIterator i = listIterator(size()); i.hasPrevious();)
{
HttpField f = i.previous();
if (f.getHeader() == header)
return f.getValue();
}
return null;
}
/**
* Returns the encoded value of the first field with the given field name,
* or {@code null} if no such field is present.
* The comparison of field name is case-insensitive via
* {@link HttpField#is(String)}. If possible, it is more efficient to use {@link #get(HttpHeader)}.
* In case of multi-valued fields, the returned value is the encoded
* value, including commas and quotes, as returned by {@link HttpField#getValue()}.
*
* @param name the case-insensitive field name to search for
* @return the raw value of the first field with the given field name,
* or {@code null} if no such field is present
* @see HttpField#getValue()
* @see #get(HttpHeader)
*/
default String get(String name)
{
for (HttpField f : this)
{
if (f.is(name))
return f.getValue();
}
return null;
}
/**
* Returns all the values of all the fields with the given field name.
* In case of multi-valued fields, the multi-value of the field is split using
* {@link QuotedCSV}, taking into account the given {@code keepQuotes} parameter.
*
* @param header the field name to search for
* @param keepQuotes whether the fields values should retain the quotes
* @return a list of all the values of all the fields with the given name,
* or an empty list if no such field name is present
*/
default List getCSV(HttpHeader header, boolean keepQuotes)
{
QuotedCSV values = null;
for (HttpField f : this)
{
if (f.getHeader() == header)
{
if (values == null)
values = new QuotedCSV(keepQuotes);
values.addValue(f.getValue());
}
}
return values == null ? Collections.emptyList() : values.getValues();
}
/**
* Returns all the values of all the fields with the given field name.
* The comparison of field name is case-insensitive via
* {@link HttpField#is(String)}.
* In case of multi-valued fields, the multi-value of the field is split using
* {@link QuotedCSV}, taking into account the given {@code keepQuotes} parameter.
*
* @param name the case-insensitive field name to search for
* @param keepQuotes whether the fields values should retain the quotes
* @return a list of all the values of all the fields with the given name,
* or an empty list if no such field name is present
*/
default List getCSV(String name, boolean keepQuotes)
{
QuotedCSV values = null;
for (HttpField f : this)
{
if (f.is(name))
{
if (values == null)
values = new QuotedCSV(keepQuotes);
values.addValue(f.getValue());
}
}
return values == null ? Collections.emptyList() : values.getValues();
}
/**
* Returns the value of a date field as the number of milliseconds
* since the Unix Epoch, or -1 if no such field is present.
* The comparison of field name is case-insensitive via
* {@link HttpField#is(String)}.
*
* @param name the case-insensitive field name to search for
* @return the value of the field as the number of milliseconds since Unix Epoch,
* or -1 if no such field is present
*/
default long getDateField(String name)
{
return parseDateField(getField(name));
}
/**
* Returns the value of a date field as the number of milliseconds
* since the Unix Epoch, or -1 if no such field is present.
*
* @param header the field name to search for
* @return the value of the field as the number of milliseconds since Unix Epoch,
* or -1 if no such field is present
*/
default long getDateField(HttpHeader header)
{
return parseDateField(getField(header));
}
private static long parseDateField(HttpField field)
{
if (field == null)
return -1;
String val = HttpField.getValueParameters(field.getValue(), null);
if (val == null)
return -1;
return TimeUnit.SECONDS.toMillis(HttpDateTime.parse(val).toEpochSecond());
}
/**
* Returns the {@link HttpField} at the given {@code index},
* or {@code null} if there is no field at the given index.
*
* @param index the index of the {@link HttpField}
* @return the {@link HttpField} at the given {@code index},
* or {@code null} if there is no field at the given index
*/
default HttpField getField(int index)
{
int i = 0;
for (HttpField f : this)
{
if (i++ == index)
return f;
}
return null;
}
/**
* Returns the first {@link HttpField} with the given field name,
* or {@code null} if no such field is present.
*
* @param header the field name to search for
* @return the first {@link HttpField} with the given field name,
* or {@code null} if no such field is present
*/
default HttpField getField(HttpHeader header)
{
for (HttpField f : this)
{
if (f.getHeader() == header)
return f;
}
return null;
}
/**
* Returns the first {@link HttpField} with the given field name,
* or {@code null} if no such field is present.
* The comparison of field name is case-insensitive via
* {@link HttpField#is(String)}.
*
* @param name the case-insensitive field name to search for
* @return the first {@link HttpField} with the given field name,
* or {@code null} if no such field is present
*/
default HttpField getField(String name)
{
for (HttpField f : this)
{
if (f.is(name))
return f;
}
return null;
}
/**
* @return an enumeration of field names
* @deprecated use {@link #getFieldNamesCollection()}
*/
@Deprecated
default Enumeration getFieldNames()
{
return Collections.enumeration(getFieldNamesCollection());
}
/**
* Returns a {@link Set} of the field names.
* Case-sensitivity of the field names is preserved.
*
* @return an immutable {@link Set} of the field names. Changes made to the
* {@code HttpFields} after this call are not reflected in the set.
*/
default Set getFieldNamesCollection()
{
Set seenByHeader = EnumSet.noneOf(HttpHeader.class);
Set buildByName = null;
List list = new ArrayList<>(size());
for (HttpField f : this)
{
HttpHeader header = f.getHeader();
if (header == null)
{
if (buildByName == null)
buildByName = new TreeSet<>(String::compareToIgnoreCase);
if (buildByName.add(f.getName()))
list.add(f.getName());
}
else if (seenByHeader.add(header))
{
list.add(f.getName());
}
}
Set seenByName = buildByName;
// use the list to retain a rough ordering
return new AbstractSet<>()
{
@Override
public Iterator iterator()
{
return list.iterator();
}
@Override
public int size()
{
return list.size();
}
@Override
public boolean contains(Object o)
{
if (o instanceof String s)
return seenByName != null && seenByName.contains(s) || seenByHeader.contains(HttpHeader.CACHE.get(s));
return false;
}
};
}
/**
* Returns all the {@link HttpField}s with the given field name.
*
* @param header the field name to search for
* @return a list of the {@link HttpField}s with the given field name,
* or an empty list if no such field name is present
*/
default List getFields(HttpHeader header)
{
return getFields(header, (f, h) -> f.getHeader() == h);
}
/**
* Returns all the {@link HttpField}s with the given field name.
* The comparison of field name is case-insensitive via
* {@link HttpField#is(String)}.
*
* @param name the case-insensitive field name to search for
* @return a list of the {@link HttpField}s with the given field name,
* or an empty list if no such field name is present
*/
default List getFields(String name)
{
return getFields(name, (f, n) -> f.is(name));
}
private List getFields(T header, BiPredicate predicate)
{
return stream()
.filter(f -> predicate.test(f, header))
.collect(Collectors.toList());
}
/**
* Returns the value of a field as a {@code long} value,
* or -1 if no such field is present.
* The comparison of field name is case-insensitive via
* {@link HttpField#is(String)}.
*
* @param name the case-insensitive field name
* @return the value of the field as a {@code long},
* or -1 if no such field is present
* @throws NumberFormatException if the value of the field
* cannot be converted to a {@code long}
*/
default long getLongField(String name) throws NumberFormatException
{
HttpField field = getField(name);
return field == null ? -1L : field.getLongValue();
}
/**
* Returns the value of a field as a {@code long} value,
* or -1 if no such field is present.
*
* @param header the field name
* @return the value of the field as a {@code long},
* or -1 if no such field is present
* @throws NumberFormatException if the value of the field
* cannot be converted to a {@code long}
*/
default long getLongField(HttpHeader header) throws NumberFormatException
{
HttpField field = getField(header);
return field == null ? -1L : field.getLongValue();
}
/**
* Returns all the values of all the fields with the given name,
* split and sorted in quality order using {@link QuotedQualityCSV}.
*
* @param header the field name to search for
* @return a list of all the values of all the fields with the given name,
* split and sorted in quality order,
* or an empty list if no such field name is present
*/
default List getQualityCSV(HttpHeader header)
{
return getQualityCSV(header, null);
}
/**
* Returns all the values of all the fields with the given name,
* split and sorted first in quality order and then optionally further
* sorted with the given function, using {@link QuotedQualityCSV}.
*
* @param header the field name to search for
* @param secondaryOrdering the secondary sort function, or {@code null}
* for no secondary sort
* @return a list of all the values of all the fields with the given name,
* split and sorted in quality order and optionally further sorted with
* the given function,
* or an empty list if no such field name is present
*/
default List getQualityCSV(HttpHeader header, ToIntFunction secondaryOrdering)
{
QuotedQualityCSV values = null;
for (HttpField f : this)
{
if (f.getHeader() == header)
{
if (values == null)
values = new QuotedQualityCSV(secondaryOrdering);
values.addValue(f.getValue());
}
}
return values == null ? Collections.emptyList() : values.getValues();
}
/**
* Returns all the values of all the fields with the given name,
* split and sorted in quality order using {@link QuotedQualityCSV}.
* The comparison of field name is case-insensitive via
* {@link HttpField#is(String)}.
*
* @param name the case-insensitive field name to search for
* @return a list of all the values of all the fields with the given name,
* split and sorted in quality order,
* or an empty list if no such field name is present
*/
default List getQualityCSV(String name)
{
QuotedQualityCSV values = null;
for (HttpField f : this)
{
if (f.is(name))
{
if (values == null)
values = new QuotedQualityCSV();
values.addValue(f.getValue());
}
}
return values == null ? Collections.emptyList() : values.getValues();
}
/**
* Returns an {@link Enumeration} of the encoded values of all the fields
* with the given name.
* The comparison of field name is case-insensitive via
* {@link HttpField#is(String)}.
* In case of multi-valued fields, the returned value is the encoded
* value, including commas and quotes, as returned by {@link HttpField#getValue()}.
*
* @param name the case-insensitive field name to search for
* @return an {@link Enumeration} of the encoded values of all
* the fields with the given name
*/
default Enumeration getValues(String name)
{
Iterator i = iterator();
return new Enumeration<>()
{
HttpField _field;
@Override
public boolean hasMoreElements()
{
if (_field != null)
return true;
while (i.hasNext())
{
HttpField f = i.next();
if (f.is(name) && f.getValue() != null)
{
_field = f;
return true;
}
}
return false;
}
@Override
public String nextElement()
{
if (hasMoreElements())
{
String value = _field.getValue();
_field = null;
return value;
}
throw new NoSuchElementException();
}
};
}
/**
* Returns a list of the encoded values of all the fields
* with the given name.
* In case of multi-valued fields, the returned value is the encoded
* value, including commas and quotes, as returned by {@link HttpField#getValue()}.
*
* @param header the field name to search for
* @return a list of the encoded values of all the fields
* with the given name,
* or an empty list if no such field name is present
*/
default List getValuesList(HttpHeader header)
{
final List list = new ArrayList<>();
for (HttpField f : this)
{
if (f.getHeader() == header)
list.add(f.getValue());
}
return list;
}
/**
* Returns a list of the encoded values of all the fields
* with the given name.
* The comparison of field name is case-insensitive via
* {@link HttpField#is(String)}.
* In case of multi-valued fields, the returned value is the encoded
* value, including commas and quotes, as returned by {@link HttpField#getValue()}.
*
* @param name the field name to search for
* @return a list of the encoded values of all the fields
* with the given name,
* or an empty list if no such field name is present
*/
default List getValuesList(String name)
{
final List list = new ArrayList<>();
for (HttpField f : this)
{
if (f.is(name))
list.add(f.getValue());
}
return list;
}
/**
* Returns whether this instance is equal to the given instance.
* Returns {@code true} if and only if the two instances have the
* same number of fields, in the same order, and each field is equal,
* but makes no difference between {@link Mutable} and non-{@link Mutable}
* instances.
*
* @param that the {@link HttpFields} instance to compare to
* @return whether the two instances are equal
*/
default boolean isEqualTo(HttpFields that)
{
if (size() != that.size())
return false;
Iterator i = that.iterator();
for (HttpField f : this)
{
if (!i.hasNext())
return false;
if (!f.equals(i.next()))
return false;
}
return !i.hasNext();
}
/**
* @return the number of {@link HttpField}s in this instance
*/
default int size()
{
int size = 0;
for (HttpField ignored : this)
{
size++;
}
return size;
}
/**
* @return a sequential stream of the {@link HttpField}s in this instance
*/
default Stream stream()
{
return StreamSupport.stream(spliterator(), false);
}
/**
* A mutable version of {@link HttpFields}.
* Name and value pairs representing HTTP headers or HTTP
* trailers can be added to or removed from this instance.
*/
interface Mutable extends HttpFields
{
/**
* Adds a new {@link HttpField} with the given name and string value.
* The new {@link HttpField} is added even if a field with the
* same name is already present.
*
* @param name the non-{@code null} name of the field
* @param value the non-{@code null} value of the field
* @return this instance
*/
default Mutable add(String name, String value)
{
Objects.requireNonNull(name);
Objects.requireNonNull(value);
return add(new HttpField(name, value));
}
/**
* Adds a new {@link HttpField} with the given name and {@code long} value.
* The new {@link HttpField} is added even if a field with the
* same name is already present.
*
* @param name the non-{@code null} name of the field
* @param value the value of the field
* @return this instance
*/
default Mutable add(String name, long value)
{
Objects.requireNonNull(name);
return add(new HttpField.LongValueHttpField(name, value));
}
/**
* Adds a new {@link HttpField} with the given name and value.
* The new {@link HttpField} is added even if a field with the
* same name is already present.
*
* @param header the non-{@code null} name of the field
* @param value the non-{@code null} value of the field
* @return this instance
*/
default Mutable add(HttpHeader header, HttpHeaderValue value)
{
Objects.requireNonNull(header);
Objects.requireNonNull(value);
return add(header, value.toString());
}
/**
* Adds a new {@link HttpField} with the given name and string value.
* The new {@link HttpField} is added even if a field with the
* same name is already present.
*
* @param header the non-{@code null} name of the field
* @param value the non-{@code null} value of the field
* @return this instance
*/
default Mutable add(HttpHeader header, String value)
{
Objects.requireNonNull(header);
Objects.requireNonNull(value);
return add(new HttpField(header, value));
}
/**
* Adds a new {@link HttpField} with the given name and {@code long} value.
* The new {@link HttpField} is added even if a field with the
* same name is already present.
*
* @param header the non-{@code null} name of the field
* @param value the value of the field
* @return this instance
*/
default Mutable add(HttpHeader header, long value)
{
Objects.requireNonNull(header);
return add(new HttpField.LongValueHttpField(header, value));
}
/**
* Adds the given {@link HttpField} to this instance.
*
* @param field the {@link HttpField} to add
* @return this instance
*/
default Mutable add(HttpField field)
{
Objects.requireNonNull(field);
ListIterator i = listIterator(size());
i.add(field);
return this;
}
/**
* Adds all the {@link HttpField}s of the given {@link HttpFields}
* to this instance.
*
* @param fields the fields to add
* @return this instance
*/
default Mutable add(HttpFields fields)
{
for (HttpField field : fields)
{
add(field);
}
return this;
}
/**
* Adds a field associated with a list of values.
*
* @param name the name of the field
* @param list the List value of the field.
* @return this builder
*/
default Mutable add(String name, List list)
{
Objects.requireNonNull(name);
Objects.requireNonNull(list);
if (list.isEmpty())
return this;
if (list.size() == 1)
{
String v = list.get(0);
return add(name, v == null ? "" : v);
}
HttpField field = new HttpField.MultiHttpField(name, list);
return add(field);
}
/**
* Adds the given value(s) to the {@link HttpField} with the given name,
* encoding them as comma-separated if necessary,
* unless they are already present in existing fields with the same name.
*
* @param header the field name of the field to add the value(s) to
* @param values the value(s) to add
* @return this instance
*/
default Mutable addCSV(HttpHeader header, String... values)
{
Objects.requireNonNull(header);
QuotedCSV existing = null;
for (HttpField f : this)
{
if (f.getHeader() == header)
{
if (existing == null)
existing = new QuotedCSV(false);
existing.addValue(f.getValue());
}
}
String value = formatCsvExcludingExisting(existing, values);
if (value != null)
add(header, value);
return this;
}
/**
* Adds the given value(s) to the {@link HttpField} with the given name,
* encoding them as comma-separated if necessary,
* unless they are already present in existing fields with the same name.
*
* @param name the field name of the field to add the value(s) to
* @param values the value(s) to add
* @return this instance
*/
default Mutable addCSV(String name, String... values)
{
Objects.requireNonNull(name);
QuotedCSV existing = null;
for (HttpField f : this)
{
if (f.is(name))
{
if (existing == null)
existing = new QuotedCSV(false);
existing.addValue(f.getValue());
}
}
String value = formatCsvExcludingExisting(existing, values);
if (value != null)
add(name, value);
return this;
}
/**
* Adds a new date {@link HttpField} with the given name and {@code date} value.
* The {@code date} parameter is the number of milliseconds from the Unix Epoch,
* and it is formatted into a string via {@link DateGenerator#formatDate(long)}.
*
* @param name the non-{@code null} name of the field
* @param date the field date value
* @return this instance
*/
default Mutable addDateField(String name, long date)
{
Objects.requireNonNull(name);
add(name, DateGenerator.formatDate(date));
return this;
}
/**
* Removes all the fields from this instance.
*
* @return this instance
*/
default Mutable clear()
{
Iterator i = iterator();
while (i.hasNext())
{
i.next();
i.remove();
}
return this;
}
/**
* Ensures that the given {@link HttpField} is present when the field
* may not exist or may exist and be multi-valued.
* Multiple existing {@link HttpField}s are merged into a single field.
*
* @param field the field to ensure to be present
*/
default void ensureField(HttpField field)
{
Objects.requireNonNull(field);
HttpHeader header = field.getHeader();
// Is the field value multi valued?
if (field.getValue().indexOf(',') < 0)
{
if (header != null)
computeField(header, (h, l) -> computeEnsure(field, l));
else
computeField(field.getName(), (h, l) -> computeEnsure(field, l));
}
else
{
if (header != null)
computeField(header, (h, l) -> computeEnsure(field, field.getValues(), l));
else
computeField(field.getName(), (h, l) -> computeEnsure(field, field.getValues(), l));
}
}
/**
* Puts the given {@link HttpField} into this instance.
* If a fields with the same name is present, the given field
* replaces it, and other existing fields with the same name
* are removed, so that only the given field will be present.
* If a field with the same name is not present, the given
* field will be added.
*
* @param field the field to put
* @return this instance
*/
default Mutable put(HttpField field)
{
Objects.requireNonNull(field);
boolean put = false;
ListIterator i = listIterator();
while (i.hasNext())
{
HttpField f = i.next();
if (f.isSameName(field))
{
if (put)
{
i.remove();
}
else
{
i.set(field);
put = true;
}
}
}
if (!put)
add(field);
return this;
}
/**
* This method behaves like {@link #remove(String)} when
* the given {@code value} is {@code null}, otherwise behaves
* like {@link #put(HttpField)}.
*
* @param name the name of the field
* @param value the value of the field; if {@code null} the field is removed
* @return this instance
*/
default Mutable put(String name, String value)
{
Objects.requireNonNull(name);
if (value == null)
return remove(name);
return put(new HttpField(name, value));
}
/**
* This method behaves like {@link #remove(HttpHeader)} when
* the given {@code value} is {@code null}, otherwise behaves
* like {@link #put(HttpField)}.
*
* @param header the name of the field
* @param value the value of the field; if {@code null} the field is removed
* @return this instance
*/
default Mutable put(HttpHeader header, HttpHeaderValue value)
{
Objects.requireNonNull(header);
if (value == null)
return remove(header);
return put(new HttpField(header, value.toString()));
}
/**
* This method behaves like {@link #remove(HttpHeader)} when
* the given {@code value} is {@code null}, otherwise behaves
* like {@link #put(HttpField)}.
*
* @param header the name of the field
* @param value the value of the field; if {@code null} the field is removed
* @return this instance
*/
default Mutable put(HttpHeader header, String value)
{
Objects.requireNonNull(header);
if (value == null)
return remove(header);
return put(new HttpField(header, value));
}
/**
* Puts a field associated with a list of values.
*
* @param name the name of the field
* @param list the List value of the field. If null the field is cleared.
* @return this builder
*/
default Mutable put(String name, List list)
{
Objects.requireNonNull(name);
if (list == null || list.isEmpty())
return remove(name);
if (list.size() == 1)
{
String value = list.get(0);
return put(name, value == null ? "" : value);
}
HttpField field = new HttpField.MultiHttpField(name, list);
return put(field);
}
/**
* Puts a new date {@link HttpField} with the given name and {@code date} value,
* with the semantic of {@link #put(HttpField)}.
* The {@code date} parameter is the number of milliseconds from the Unix Epoch,
* and it is formatted into a string via {@link DateGenerator#formatDate(long)}.
*
* @param name the non-{@code null} name of the field
* @param date the field date value
* @return this instance
*/
default Mutable putDate(HttpHeader name, long date)
{
Objects.requireNonNull(name);
return put(name, DateGenerator.formatDate(date));
}
/**
* Puts a new date {@link HttpField} with the given name and {@code date} value,
* with the semantic of {@link #put(HttpField)}.
* The {@code date} parameter is the number of milliseconds from the Unix Epoch,
* and it is formatted into a string via {@link DateGenerator#formatDate(long)}.
*
* @param name the non-{@code null} name of the field
* @param date the field date value
* @return this instance
*/
default Mutable putDate(String name, long date)
{
Objects.requireNonNull(name);
return put(name, DateGenerator.formatDate(date));
}
/**
* Puts a new {@link HttpField} with the given name and {@code long} value,
* with the semantic of {@link #put(HttpField)}.
*
* @param header the non-{@code null} name of the field
* @param value the value of the field
* @return this instance
*/
default Mutable put(HttpHeader header, long value)
{
Objects.requireNonNull(header);
if (value == 0 && header == HttpHeader.CONTENT_LENGTH)
return put(HttpFields.CONTENT_LENGTH_0);
return put(new HttpField.LongValueHttpField(header, value));
}
/**
* Puts a new {@link HttpField} with the given name and {@code long} value,
* with the semantic of {@link #put(HttpField)}.
*
* @param name the non-{@code null} name of the field
* @param value the value of the field
* @return this instance
*/
default Mutable put(String name, long value)
{
Objects.requireNonNull(name);
if (value == 0 && HttpHeader.CONTENT_LENGTH.is(name))
return put(HttpFields.CONTENT_LENGTH_0);
return put(new HttpField.LongValueHttpField(name, value));
}
/**
* Computes a single field for the given {@link HttpHeader} and for existing fields with the same header.
*
* The compute function receives the field name and a list of fields with the same name
* so that their values can be used to compute the value of the field that is returned
* by the compute function parameter.
* If the compute function returns {@code null}, the fields with the given name are removed.
* This method comes handy when you want to add an HTTP header if it does not exist,
* or add a value if the HTTP header already exists, similarly to
* {@link Map#compute(Object, BiFunction)}.
*
* This method can be used to {@link #put(HttpField) put} a new field (or blindly replace its value):
* {@code
* httpFields.computeField("X-New-Header",
* (name, fields) -> new HttpField(name, "NewValue"));
* }
*
* This method can be used to coalesce many fields into one:
* {@code
* // Input:
* GET / HTTP/1.1
* Host: localhost
* Cookie: foo=1
* Cookie: bar=2,baz=3
* User-Agent: Jetty
*
* // Computation:
* httpFields.computeField("Cookie", (name, fields) ->
* {
* // No cookies, nothing to do.
* if (fields == null)
* return null;
*
* // Coalesces all cookies.
* String coalesced = fields.stream()
* .flatMap(field -> Stream.of(field.getValues()))
* .collect(Collectors.joining(", "));
*
* // Returns a single Cookie header with all cookies.
* return new HttpField(name, coalesced);
* }
*
* // Output:
* GET / HTTP/1.1
* Host: localhost
* Cookie: foo=1, bar=2, baz=3
* User-Agent: Jetty
* }
*
* This method can be used to replace a field:
* {@code
* httpFields.computeField("X-Length", (name, fields) ->
* {
* if (fields == null)
* return null;
*
* // Get any value among the X-Length headers.
* String length = fields.stream()
* .map(HttpField::getValue)
* .findAny()
* .orElse("0");
*
* // Replace X-Length headers with X-Capacity header.
* return new HttpField("X-Capacity", length);
* });
* }
*
* This method can be used to remove a field:
* {@code
* httpFields.computeField("Connection", (name, fields) -> null);
* }
*
* @param header the field name
* @param computeFn the compute function
* @return this instance
*/
default Mutable computeField(HttpHeader header, BiFunction, HttpField> computeFn)
{
Objects.requireNonNull(header);
HttpField result = computeFn.apply(header, stream().filter(f -> f.getHeader() == header).toList());
return result != null ? put(result) : remove(header);
}
/**
* Computes a single field for the given HTTP field name and for existing fields with the same name.
*
* @param name the field name
* @param computeFn the compute function
* @return this instance
* @see #computeField(HttpHeader, BiFunction)
*/
default Mutable computeField(String name, BiFunction, HttpField> computeFn)
{
Objects.requireNonNull(name);
HttpField result = computeFn.apply(name, stream().filter(f -> f.is(name)).toList());
return result != null ? put(result) : remove(name);
}
/**
* Removes all the fields with the given name.
*
* @param header the name of the fields to remove
* @return this instance
*/
default Mutable remove(HttpHeader header)
{
Objects.requireNonNull(header);
Iterator i = iterator();
while (i.hasNext())
{
HttpField f = i.next();
if (f.getHeader() == header)
i.remove();
}
return this;
}
/**
* Removes all the fields with the given names.
*
* @param headers the names of the fields to remove
* @return this instance
*/
default Mutable remove(EnumSet headers)
{
Iterator i = iterator();
while (i.hasNext())
{
HttpField f = i.next();
HttpHeader h = f.getHeader();
if (h != null && headers.contains(h))
i.remove();
}
return this;
}
/**
* Removes all the fields with the given name.
*
* @param name the name of the fields to remove
* @return this instance
*/
default Mutable remove(String name)
{
Objects.requireNonNull(name);
for (ListIterator i = listIterator(); i.hasNext(); )
{
HttpField f = i.next();
if (f.is(name))
i.remove();
}
return this;
}
private static String formatCsvExcludingExisting(QuotedCSV existing, String... values)
{
// remove any existing values from the new values
boolean add = true;
if (existing != null && !existing.isEmpty())
{
add = false;
for (int i = values.length; i-- > 0; )
{
String unquoted = QuotedCSV.unquote(values[i]);
if (existing.getValues().contains(unquoted))
values[i] = null;
else
add = true;
}
}
if (add)
{
StringBuilder value = new StringBuilder();
for (String v : values)
{
if (v == null)
continue;
if (!value.isEmpty())
value.append(", ");
value.append(v);
}
if (!value.isEmpty())
return value.toString();
}
return null;
}
private static HttpField computeEnsure(HttpField ensure, List fields)
{
// If no existing fields return the ensure field
if (fields == null || fields.isEmpty())
return ensure;
String ensureValue = ensure.getValue();
// Handle a single existing field
if (fields.size() == 1)
{
// If the existing field contains the ensure value, return it, else append values.
HttpField f = fields.get(0);
return f.contains(ensureValue)
? f
: new HttpField(ensure.getHeader(), ensure.getName(), f.getValue() + ", " + ensureValue);
}
// Handle multiple existing fields
StringBuilder v = new StringBuilder();
for (HttpField f : fields)
{
// Always append multiple fields into a single field value
if (!v.isEmpty())
v.append(", ");
v.append(f.getValue());
// check if the ensure value is already contained
if (ensureValue != null && f.contains(ensureValue))
ensureValue = null;
}
// If the ensure value was not contained append it
if (ensureValue != null)
v.append(", ").append(ensureValue);
return new HttpField(ensure.getHeader(), ensure.getName(), v.toString());
}
private static HttpField computeEnsure(HttpField ensure, String[] values, List fields)
{
// If no existing fields return the ensure field
if (fields == null || fields.isEmpty())
return ensure;
// Handle a single existing field
if (fields.size() == 1)
{
HttpField f = fields.get(0);
// check which ensured values are already contained
int ensured = values.length;
for (int i = 0; i < values.length; i++)
{
if (f.contains(values[i]))
{
ensured--;
values[i] = null;
}
}
// if all ensured values contained return the existing field
if (ensured == 0)
return f;
// else if no ensured values contained append the entire ensured valued
if (ensured == values.length)
return new HttpField(ensure.getHeader(), ensure.getName(),
f.getValue() + ", " + ensure.getValue());
// else append just the ensured values that are not contained
StringBuilder v = new StringBuilder(f.getValue());
for (String value : values)
{
if (value != null)
v.append(", ").append(value);
}
return new HttpField(ensure.getHeader(), ensure.getName(), v.toString());
}
// Handle a multiple existing field
StringBuilder v = new StringBuilder();
int ensured = values.length;
for (HttpField f : fields)
{
// Always append multiple fields into a single field value
if (!v.isEmpty())
v.append(", ");
v.append(f.getValue());
// null out ensured values that are included
for (int i = 0; i < values.length; i++)
{
if (values[i] != null && f.contains(values[i]))
{
ensured--;
values[i] = null;
}
}
}
// if no ensured values exist append them all
if (ensured == values.length)
v.append(", ").append(ensure.getValue());
// else if some ensured values are missing, append them
else if (ensured > 0)
{
for (String value : values)
{
if (value != null)
v.append(", ").append(value);
}
}
// return a merged header with missing ensured values added
return new HttpField(ensure.getHeader(), ensure.getName(), v.toString());
}
/**
* A wrapper of {@link HttpFields} instances.
*/
class Wrapper implements Mutable
{
private final Mutable _fields;
public Wrapper(Mutable fields)
{
_fields = fields;
}
/**
* Called when a field is added (including as part of a put).
*
* @param field The field being added.
* @return The field to add, or null if the add is to be ignored.
*/
public HttpField onAddField(HttpField field)
{
return field;
}
/**
* Called when a field is removed (including as part of a put).
*
* @param field The field being removed.
* @return True if the field should be removed, false otherwise.
*/
public boolean onRemoveField(HttpField field)
{
return true;
}
public HttpField onReplaceField(HttpField oldField, HttpField newField)
{
return newField;
}
@Override
public int size()
{
// This impl needed only as an optimization
return _fields.size();
}
@Override
public Stream stream()
{
// This impl needed only as an optimization
return _fields.stream();
}
@Override
public Mutable add(HttpField field)
{
if (field != null)
{
field = onAddField(field);
if (field != null)
_fields.add(field);
}
return this;
}
@Override
public Mutable put(HttpField field)
{
Objects.requireNonNull(field);
// rewrite put to ensure that removes are called before replace
int put = -1;
ListIterator i = _fields.listIterator();
while (i.hasNext())
{
HttpField f = i.next();
if (f.isSameName(field))
{
if (put < 0)
put = i.previousIndex();
else if (onRemoveField(f))
i.remove();
}
}
if (put < 0)
{
field = onAddField(field);
if (field != null)
_fields.add(field);
}
else
{
i = _fields.listIterator(put);
HttpField old = i.next();
field = onReplaceField(old, field);
if (field != null)
i.set(field);
}
return this;
}
@Override
public Mutable clear()
{
_fields.clear();
return this;
}
@Override
public ListIterator listIterator(int index)
{
ListIterator i = _fields.listIterator(index);
return new ListIterator<>()
{
HttpField last;
@Override
public boolean hasNext()
{
return i.hasNext();
}
@Override
public HttpField next()
{
return last = i.next();
}
@Override
public boolean hasPrevious()
{
return i.hasPrevious();
}
@Override
public HttpField previous()
{
return last = i.previous();
}
@Override
public int nextIndex()
{
return i.nextIndex();
}
@Override
public int previousIndex()
{
return i.previousIndex();
}
@Override
public void remove()
{
if (last != null && onRemoveField(last))
{
last = null;
i.remove();
}
}
@Override
public void set(HttpField field)
{
if (field == null)
{
if (last != null && onRemoveField(last))
{
last = null;
i.remove();
}
}
else
{
if (last != null)
{
field = onReplaceField(last, field);
if (field != null)
{
last = null;
i.set(field);
}
}
}
}
@Override
public void add(HttpField field)
{
if (field != null)
{
field = onAddField(field);
if (field != null)
{
last = null;
i.add(field);
}
}
}
};
}
}
}
@Deprecated(forRemoval = true)
class MutableHttpFields extends org.eclipse.jetty.http.MutableHttpFields
{
}
@Deprecated(forRemoval = true)
class ImmutableHttpFields extends org.eclipse.jetty.http.ImmutableHttpFields
{
protected ImmutableHttpFields(HttpField[] fields, int size)
{
super(fields, size);
}
}
}