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.ArrayList;
import java.util.Arrays;
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.function.BiFunction;
import java.util.function.BiPredicate;
import java.util.function.ToIntFunction;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* Interface that represents on ordered collection of {@link HttpField}s.
* Both {@link Mutable} and {@link Immutable} implementations are available
* via the static methods such as {@link #build()} and {@link #from(HttpField...)}.
*/
public interface HttpFields extends Iterable
{
/**
* A constant for an immutable and empty {@link HttpFields}.
*/
HttpFields EMPTY = new EmptyHttpFields();
static Mutable build()
{
return new Mutable();
}
static Mutable build(int capacity)
{
return new Mutable(capacity);
}
static Mutable build(HttpFields fields)
{
return new Mutable(fields);
}
static Mutable build(HttpFields fields, HttpField replaceField)
{
return new Mutable(fields, replaceField);
}
static Mutable build(HttpFields fields, EnumSet removeFields)
{
return new Mutable(fields, removeFields);
}
static Immutable from(HttpField... fields)
{
return new Immutable(fields);
}
Immutable asImmutable();
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();
}
default boolean contains(HttpField field)
{
for (HttpField f : this)
{
if (f.isSameName(field) && (f.equals(field) || f.contains(field.getValue())))
return true;
}
return false;
}
default boolean contains(HttpHeader header, String value)
{
for (HttpField f : this)
{
if (f.getHeader() == header && f.contains(value))
return true;
}
return false;
}
default boolean contains(String name, String value)
{
for (HttpField f : this)
{
if (f.is(name) && f.contains(value))
return true;
}
return false;
}
default boolean contains(HttpHeader header)
{
for (HttpField f : this)
{
if (f.getHeader() == header)
return true;
}
return false;
}
default boolean contains(EnumSet headers)
{
for (HttpField f : this)
{
if (headers.contains(f.getHeader()))
return true;
}
return false;
}
default boolean contains(String name)
{
for (HttpField f : this)
{
if (f.is(name))
return true;
}
return false;
}
default String get(HttpHeader header)
{
for (HttpField f : this)
{
if (f.getHeader() == header)
return f.getValue();
}
return null;
}
default String get(String header)
{
for (HttpField f : this)
{
if (f.is(header))
return f.getValue();
}
return null;
}
/**
* Get multiple field values of the same name, split
* as a {@link QuotedCSV}
*
* @param header The header
* @param keepQuotes True if the fields are kept quoted
* @return List the values with OWS stripped
*/
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();
}
/**
* Get multiple field values of the same name
* as a {@link QuotedCSV}
*
* @param name the case-insensitive field name
* @param keepQuotes True if the fields are kept quoted
* @return List the values with OWS stripped
*/
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();
}
/**
* Get a header as a date value. Returns the value of a date field, or -1 if not found. The case
* of the field name is ignored.
*
* @param name the case-insensitive field name
* @return the value of the field as a number of milliseconds since unix epoch
*/
default long getDateField(String name)
{
HttpField field = getField(name);
if (field == null)
return -1;
String val = HttpField.getValueParameters(field.getValue(), null);
if (val == null)
return -1;
final long date = DateParser.parseDate(val);
if (date == -1)
throw new IllegalArgumentException("Cannot convert date: " + val);
return date;
}
/**
* Get a Field by index.
*
* @param index the field index
* @return A Field value or null if the Field value has not been set
*/
HttpField getField(int index);
default HttpField getField(HttpHeader header)
{
for (HttpField f : this)
{
if (f.getHeader() == header)
return f;
}
return null;
}
default HttpField getField(String name)
{
for (HttpField f : this)
{
if (f.is(name))
return f;
}
return null;
}
/**
* Get enumeration of header _names. Returns an enumeration of strings representing the header
* _names for this request.
*
* @return an enumeration of field names
*/
default Enumeration getFieldNames()
{
return Collections.enumeration(getFieldNamesCollection());
}
/**
* Get Set of header names.
*
* @return the unique set of field names.
*/
default Set getFieldNamesCollection()
{
return stream().map(HttpField::getName).collect(Collectors.toSet());
}
/**
* Get multiple fields of the same header
*
* @param header the header
* @return List the fields
*/
default List getFields(HttpHeader header)
{
return getFields(header, (f, h) -> f.getHeader() == h);
}
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());
}
/**
* Get a header as an long value. Returns the value of an integer field or -1 if not found. The
* case of the field name is ignored.
*
* @param name the case-insensitive field name
* @return the value of the field as a long
* @throws NumberFormatException If bad long found
*/
default long getLongField(String name) throws NumberFormatException
{
HttpField field = getField(name);
return field == null ? -1L : field.getLongValue();
}
/**
* Get a header as an long value. Returns the value of an integer field or -1 if not found. The
* case of the field name is ignored.
*
* @param header the header type
* @return the value of the field as a long
* @throws NumberFormatException If bad long found
*/
default long getLongField(HttpHeader header) throws NumberFormatException
{
HttpField field = getField(header);
return field == null ? -1L : field.getLongValue();
}
/**
* Get multiple field values of the same name, split and
* sorted as a {@link QuotedQualityCSV}
*
* @param header The header
* @return List the values in quality order with the q param and OWS stripped
*/
default List getQualityCSV(HttpHeader header)
{
return getQualityCSV(header, null);
}
/**
* Get multiple field values of the same name, split and
* sorted as a {@link QuotedQualityCSV}
*
* @param header The header
* @param secondaryOrdering Function to apply an ordering other than specified by quality
* @return List the values in quality order with the q param and OWS stripped
*/
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();
}
/**
* Get multiple field values of the same name, split and
* sorted as a {@link QuotedQualityCSV}
*
* @param name the case-insensitive field name
* @return List the values in quality order with the q param and OWS stripped
*/
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();
}
/**
* Get multi headers
*
* @param name the case-insensitive field name
* @return Enumeration of the values
*/
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();
}
};
}
/**
* Get multiple field values of the same name
*
* @param header the header
* @return List the values
*/
default List getValuesList(HttpHeader header)
{
final List list = new ArrayList<>();
for (HttpField f : this)
{
if (f.getHeader() == header)
list.add(f.getValue());
}
return list;
}
/**
* Get multiple header of the same name
*
* @param name the case-insensitive field name
* @return List the header values
*/
default List getValuesList(String name)
{
final List list = new ArrayList<>();
for (HttpField f : this)
{
if (f.is(name))
list.add(f.getValue());
}
return list;
}
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();
}
int size();
Stream stream();
/**
* HTTP Fields. A collection of HTTP header and or Trailer fields.
*
* This class is not synchronized as it is expected that modifications will only be performed by a
* single thread.
*
*
The cookie handling provided by this class is guided by the Servlet specification and RFC6265.
*/
class Mutable implements Iterable, HttpFields
{
private HttpField[] _fields;
private int _size;
/**
* Initialize an empty HttpFields.
*/
protected Mutable()
{
this(16); // Based on small sample of Chrome requests.
}
/**
* Initialize an empty HttpFields.
*
* @param capacity the capacity of the http fields
*/
Mutable(int capacity)
{
_fields = new HttpField[capacity];
}
/**
* Initialize HttpFields from another.
*
* @param fields the fields to copy data from
*/
Mutable(HttpFields fields)
{
add(fields);
}
/**
* Initialize HttpFields from another and replace a field
*
* @param fields the fields to copy data from
* @param replaceField the replacement field
*/
Mutable(HttpFields fields, HttpField replaceField)
{
_fields = new HttpField[fields.size() + 4];
_size = 0;
boolean put = false;
for (HttpField f : fields)
{
if (replaceField.isSameName(f))
{
if (!put)
_fields[_size++] = replaceField;
put = true;
}
else
{
_fields[_size++] = f;
}
}
if (!put)
_fields[_size++] = replaceField;
}
/**
* Initialize HttpFields from another and remove fields
*
* @param fields the fields to copy data from
* @param removeFields the the fields to remove
*/
Mutable(HttpFields fields, EnumSet removeFields)
{
_fields = new HttpField[fields.size() + 4];
_size = 0;
for (HttpField f : fields)
{
if (f.getHeader() == null || !removeFields.contains(f.getHeader()))
_fields[_size++] = f;
}
}
/**
* Add to or set a field. If the field is allowed to have multiple values, add will add multiple
* headers of the same name.
*
* @param name the name of the field
* @param value the value of the field.
* @return this builder
*/
public Mutable add(String name, String value)
{
if (value != null)
return add(new HttpField(name, value));
return this;
}
public Mutable add(HttpHeader header, HttpHeaderValue value)
{
return add(header, value.toString());
}
/**
* Add to or set a field. If the field is allowed to have multiple values, add will add multiple
* headers of the same name.
*
* @param header the header
* @param value the value of the field.
* @return this builder
*/
public Mutable add(HttpHeader header, String value)
{
if (value == null)
throw new IllegalArgumentException("null value");
HttpField field = new HttpField(header, value);
return add(field);
}
public Mutable add(HttpField field)
{
if (field != null)
{
if (_size == _fields.length)
_fields = Arrays.copyOf(_fields, _size * 2);
_fields[_size++] = field;
}
return this;
}
public Mutable add(HttpFields fields)
{
if (_fields == null)
_fields = new HttpField[fields.size() + 4];
else if (_size + fields.size() >= _fields.length)
_fields = Arrays.copyOf(_fields, _size + fields.size() + 4);
if (fields.size() == 0)
return this;
if (fields instanceof Immutable)
{
Immutable b = (Immutable)fields;
System.arraycopy(b._fields, 0, _fields, _size, b._fields.length);
_size += b._fields.length;
}
else if (fields instanceof Mutable)
{
Mutable b = (Mutable)fields;
System.arraycopy(b._fields, 0, _fields, _size, b._size);
_size += b._size;
}
else
{
for (HttpField f : fields)
_fields[_size++] = f;
}
return this;
}
/**
* Add comma separated values, but only if not already
* present.
*
* @param header The header to add the value(s) to
* @param values The value(s) to add
* @return this builder
*/
public Mutable addCSV(HttpHeader header, String... values)
{
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;
}
/**
* Add comma separated values, but only if not already
* present.
*
* @param name The header to add the value(s) to
* @param values The value(s) to add
* @return this builder
*/
public Mutable addCSV(String name, String... values)
{
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;
}
/**
* Sets the value of a date field.
*
* @param name the field name
* @param date the field date value
* @return this builder
*/
public Mutable addDateField(String name, long date)
{
add(name, DateGenerator.formatDate(date));
return this;
}
@Override
public Immutable asImmutable()
{
return new Immutable(Arrays.copyOf(_fields, _size));
}
public Mutable clear()
{
_size = 0;
return this;
}
/** Ensure that specific HttpField exists when the field may not exist or may
* exist and be multi valued. Multiple existing fields are merged into a
* single field.
* @param field The header to ensure is contained. The field is used
* directly if possible so {@link PreEncodedHttpField}s can be
* passed. If the value needs to be merged with existing values,
* then a new field is created.
*/
public void ensureField(HttpField field)
{
// Is the field value multi valued?
if (field.getValue().indexOf(',') < 0)
{
// Call Single valued computeEnsure with either String header name or enum HttpHeader
if (field.getHeader() != null)
computeField(field.getHeader(), (h, l) -> computeEnsure(field, l));
else
computeField(field.getName(), (h, l) -> computeEnsure(field, l));
}
else
{
// call multi valued computeEnsure with either String header name or enum HttpHeader
if (field.getHeader() != null)
computeField(field.getHeader(), (h, l) -> computeEnsure(field, field.getValues(), l));
else
computeField(field.getName(), (h, l) -> computeEnsure(field, field.getValues(), l));
}
}
/**
* Compute ensure field with a single value
* @param ensure The field to ensure exists
* @param fields The list of existing fields with the same header
*/
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.length() > 0)
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());
}
/**
* Compute ensure field with a multiple values
* @param ensure The field to ensure exists
* @param values The QuotedCSV parsed field values.
* @param fields The list of existing fields with the same header
*/
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.length() > 0)
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());
}
@Override
public boolean equals(Object o)
{
if (this == o)
return true;
if (!(o instanceof Mutable))
return false;
return isEqualTo((HttpFields)o);
}
/**
* Get a Field by index.
*
* @param index the field index
* @return A Field value or null if the Field value has not been set
*/
@Override
public HttpField getField(int index)
{
if (index >= _size || index < 0)
throw new NoSuchElementException();
return _fields[index];
}
@Override
public int hashCode()
{
int hash = 0;
for (int i = _fields.length; i-- > 0; )
hash ^= _fields[i].hashCode();
return hash;
}
@Override
public Iterator iterator()
{
return new Iterator<>()
{
int _index = 0;
@Override
public boolean hasNext()
{
return _index < _size;
}
@Override
public HttpField next()
{
return _fields[_index++];
}
@Override
public void remove()
{
if (_size == 0)
throw new IllegalStateException();
Mutable.this.remove(--_index);
}
};
}
public ListIterator listIterator()
{
return new ListItr();
}
public Mutable put(HttpField field)
{
boolean put = false;
for (int i = 0; i < _size; i++)
{
HttpField f = _fields[i];
if (f.isSameName(field))
{
if (put)
System.arraycopy(_fields, i + 1, _fields, i, _size-- - i-- - 1);
else
{
_fields[i] = field;
put = true;
}
}
}
if (!put)
add(field);
return this;
}
/**
* Set a field.
*
* @param name the name of the field
* @param value the value of the field. If null the field is cleared.
* @return this builder
*/
public Mutable put(String name, String value)
{
return (value == null)
? remove(name)
: put(new HttpField(name, value));
}
public Mutable put(HttpHeader header, HttpHeaderValue value)
{
return put(header, value.toString());
}
/**
* Set a field.
*
* @param header the header name of the field
* @param value the value of the field. If null the field is cleared.
* @return this builder
*/
public Mutable put(HttpHeader header, String value)
{
return (value == null)
? remove(header)
: put(new HttpField(header, value));
}
/**
* Set a field.
*
* @param name the name of the field
* @param list the List value of the field. If null the field is cleared.
* @return this builder
*/
public Mutable put(String name, List list)
{
Objects.requireNonNull(name, "name must not be null");
Objects.requireNonNull(list, "list must not be null");
remove(name);
for (String v : list)
{
if (v != null)
add(name, v);
}
return this;
}
/**
* Sets the value of a date field.
*
* @param name the field name
* @param date the field date value
* @return this builder
*/
public Mutable putDateField(HttpHeader name, long date)
{
return put(name, DateGenerator.formatDate(date));
}
/**
* Sets the value of a date field.
*
* @param name the field name
* @param date the field date value
* @return this builder
*/
public Mutable putDateField(String name, long date)
{
return put(name, DateGenerator.formatDate(date));
}
/**
* Sets the value of an long field.
*
* @param name the field name
* @param value the field long value
* @return this builder
*/
public Mutable putLongField(HttpHeader name, long value)
{
return put(name, Long.toString(value));
}
/**
* Sets the value of an long field.
*
* @param name the field name
* @param value the field long value
* @return this builder
*/
public Mutable putLongField(String name, long value)
{
return put(name, Long.toString(value));
}
/**
* Computes a single field for the given 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.
* 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):
*
* httpFields.computeField("X-New-Header",
* (name, fields) -> new HttpField(name, "NewValue"));
*
*
* This method can be used to coalesce many fields into one:
*
* // 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:
*
* 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:
*
* httpFields.computeField("Connection", (name, fields) -> null);
*
*
* @param header the HTTP header
* @param computeFn the compute function
*/
public void computeField(HttpHeader header, BiFunction, HttpField> computeFn)
{
computeField(header, computeFn, (f, h) -> f.getHeader() == h);
}
/**
* Computes a single field for the given HTTP header name and for existing fields with the same name.
*
* @param name the HTTP header name
* @param computeFn the compute function
* @see #computeField(HttpHeader, BiFunction)
*/
public void computeField(String name, BiFunction, HttpField> computeFn)
{
computeField(name, computeFn, HttpField::is);
}
private void computeField(T header, BiFunction, HttpField> computeFn, BiPredicate matcher)
{
// Look for first occurrence
int first = -1;
for (int i = 0; i < _size; i++)
{
HttpField f = _fields[i];
if (matcher.test(f, header))
{
first = i;
break;
}
}
// If the header is not found, add a new one;
if (first < 0)
{
HttpField newField = computeFn.apply(header, null);
if (newField != null)
add(newField);
return;
}
// Are there any more occurrences?
List found = null;
for (int i = first + 1; i < _size; i++)
{
HttpField f = _fields[i];
if (matcher.test(f, header))
{
if (found == null)
{
found = new ArrayList<>();
found.add(_fields[first]);
}
// Remember and remove additional fields
found.add(f);
remove(i--);
}
}
// If no additional fields were found, handle singleton case
if (found == null)
found = Collections.singletonList(_fields[first]);
else
found = Collections.unmodifiableList(found);
HttpField newField = computeFn.apply(header, found);
if (newField == null)
remove(first);
else
_fields[first] = newField;
}
/**
* Remove a field.
*
* @param name the field to remove
* @return this builder
*/
public Mutable remove(HttpHeader name)
{
for (int i = 0; i < _size; i++)
{
HttpField f = _fields[i];
if (f.getHeader() == name)
remove(i--);
}
return this;
}
public Mutable remove(EnumSet fields)
{
for (int i = 0; i < _size; i++)
{
HttpField f = _fields[i];
if (fields.contains(f.getHeader()))
remove(i--);
}
return this;
}
/**
* Remove a field.
*
* @param name the field to remove
* @return this builder
*/
public Mutable remove(String name)
{
for (int i = 0; i < _size; i++)
{
HttpField f = _fields[i];
if (f.is(name))
remove(i--);
}
return this;
}
private void remove(int i)
{
_size--;
System.arraycopy(_fields, i + 1, _fields, i, _size - i);
_fields[_size] = null;
}
public int size()
{
return _size;
}
@Override
public Stream stream()
{
return Arrays.stream(_fields, 0, _size);
}
@Override
public String toString()
{
return asString();
}
private 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.length() > 0)
value.append(", ");
value.append(v);
}
if (value.length() > 0)
return value.toString();
}
return null;
}
private class ListItr implements ListIterator
{
int _cursor; // index of next element to return
int _current = -1;
@Override
public void add(HttpField field)
{
if (field == null)
return;
_fields = Arrays.copyOf(_fields, _fields.length + 1);
System.arraycopy(_fields, _cursor, _fields, _cursor + 1, _size++);
_fields[_cursor++] = field;
_current = -1;
}
@Override
public boolean hasNext()
{
return _cursor != _size;
}
@Override
public boolean hasPrevious()
{
return _cursor > 0;
}
@Override
public HttpField next()
{
if (_cursor == _size)
throw new NoSuchElementException();
_current = _cursor++;
return _fields[_current];
}
@Override
public int nextIndex()
{
return _cursor + 1;
}
@Override
public HttpField previous()
{
if (_cursor == 0)
throw new NoSuchElementException();
_current = --_cursor;
return _fields[_current];
}
@Override
public int previousIndex()
{
return _cursor - 1;
}
@Override
public void remove()
{
if (_current < 0)
throw new IllegalStateException();
Mutable.this.remove(_current);
_cursor = _current;
_current = -1;
}
@Override
public void set(HttpField field)
{
if (_current < 0)
throw new IllegalStateException();
if (field == null)
remove();
else
_fields[_current] = field;
}
}
}
/**
* HTTP Fields. A collection of HTTP header and or Trailer fields.
*
* This class is not synchronized as it is expected that modifications will only be performed by a
* single thread.
*
*
The cookie handling provided by this class is guided by the Servlet specification and RFC6265.
*/
class Immutable implements HttpFields
{
final HttpField[] _fields;
/**
* Initialize HttpFields from copy.
*
* @param fields the fields to copy data from
*/
public Immutable(HttpField[] fields)
{
_fields = fields;
}
@Override
public Immutable asImmutable()
{
return this;
}
@Override
public boolean equals(Object o)
{
if (this == o)
return true;
if (!(o instanceof Immutable))
return false;
return isEqualTo((HttpFields)o);
}
@Override
public String get(String header)
{
// default impl overridden for efficiency
for (HttpField f : _fields)
if (f.is(header))
return f.getValue();
return null;
}
@Override
public String get(HttpHeader header)
{
// default impl overridden for efficiency
for (HttpField f : _fields)
if (f.getHeader() == header)
return f.getValue();
return null;
}
@Override
public HttpField getField(HttpHeader header)
{
// default impl overridden for efficiency
for (HttpField f : _fields)
if (f.getHeader() == header)
return f;
return null;
}
@Override
public HttpField getField(String name)
{
// default impl overridden for efficiency
for (HttpField f : _fields)
if (f.is(name))
return f;
return null;
}
@Override
public HttpField getField(int index)
{
if (index >= _fields.length)
throw new NoSuchElementException();
return _fields[index];
}
@Override
public int hashCode()
{
int hash = 0;
for (int i = _fields.length; i-- > 0; )
hash ^= _fields[i].hashCode();
return hash;
}
@Override
public Iterator iterator()
{
return new Iterator<>()
{
int _index = 0;
@Override
public boolean hasNext()
{
return _index < _fields.length;
}
@Override
public HttpField next()
{
return _fields[_index++];
}
};
}
@Override
public int size()
{
return _fields.length;
}
@Override
public Stream stream()
{
return Arrays.stream(_fields);
}
@Override
public String toString()
{
return asString();
}
}
}