org.eclipse.jetty.util.Fields Maven / Gradle / Ivy
Show all versions of jetty-util Show documentation
//
// ========================================================================
// 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.util;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* A container for name/value pairs, known as fields.
* A {@link Field} is immutable and is composed of a name string that can be case-sensitive
* or case-insensitive (by specifying the option at the constructor) and
* of a case-sensitive set of value strings.
* The implementation of this class is not thread safe.
*/
public class Fields implements Iterable
{
public static final Fields EMPTY = new Fields(Collections.emptyMap());
private final Map fields;
/**
* Creates an empty, modifiable, case insensitive {@code Fields} instance.
*/
public Fields()
{
this(false);
}
/**
* Creates an empty, modifiable, case insensitive {@code Fields} instance.
**
*
* @param caseSensitive whether this {@code Fields} instance must be case sensitive
*/
public Fields(boolean caseSensitive)
{
this(caseSensitive ? new LinkedHashMap<>() : new TreeMap<>(String::compareToIgnoreCase));
}
public Fields(MultiMap params)
{
this(multiMapToMapOfFields(params));
}
public Fields(Map fields)
{
this.fields = fields;
}
public Fields(Fields fields)
{
if (fields.fields instanceof TreeMap)
{
this.fields = new TreeMap<>(String::compareToIgnoreCase);
this.fields.putAll(fields.fields);
}
else if (fields.fields instanceof LinkedHashMap)
{
this.fields = new LinkedHashMap<>(fields.fields);
}
else if (Collections.unmodifiableMap(fields.fields) == fields.fields)
{
this.fields = fields.fields;
}
else
{
throw new IllegalStateException("unknown case sensitivity");
}
}
public Fields asImmutable()
{
Map unmodifiable = Collections.unmodifiableMap(fields);
return unmodifiable == fields ? this : new Fields(unmodifiable);
}
@Override
public boolean equals(Object obj)
{
if (this == obj)
return true;
if (obj == null)
return false;
if (obj instanceof Fields that)
{
if (getSize() != that.getSize())
return false;
if (!fields.getClass().equals(that.fields.getClass()))
return false;
for (Map.Entry entry : fields.entrySet())
{
String name = entry.getKey();
Field value = entry.getValue();
if (!value.equals(that.get(name)))
return false;
}
return true;
}
return false;
}
@Override
public int hashCode()
{
return fields.hashCode();
}
/**
* @return a set of field names
*/
public Set getNames()
{
return fields.keySet();
}
public Stream stream()
{
return fields.values().stream();
}
/**
* @param name the field name
* @return the {@link Field} with the given name, or null if no such field exists
*/
public Field get(String name)
{
return fields.get(name);
}
/**
* @param name the field name
* @return the first value of the field with the given name, or null if no such field exists
*/
public String getValue(String name)
{
Field field = get(name);
if (field == null)
return null;
return field.getValue();
}
/**
* @param name the field name
* @return the values of the field with the given name, or null if no such field exists
*/
public List getValues(String name)
{
Field field = get(name);
if (field == null)
return null;
return field.getValues();
}
/**
* @param name the field name
* @return the values of the field with the given name, or empty list if no such field exists
*/
public List getValuesOrEmpty(String name)
{
Field field = get(name);
if (field == null)
return Collections.emptyList();
return field.getValues();
}
/**
* Inserts or replaces the given name/value pair as a single-valued {@link Field}.
*
* @param name the field name
* @param value the field value
*/
public void put(String name, String value)
{
// Preserve the case for the field name
Field field = new Field(name, value);
fields.put(name, field);
}
/**
* Inserts or replaces the given {@link Field}, mapped to the {@link Field#getName() field's name}
*
* @param field the field to put
*/
public void put(Field field)
{
if (field != null)
{
String s = field.getName();
fields.put(s, field);
}
}
/**
* Adds the given value to a field with the given name,
* creating a {@link Field} is none exists for the given name.
*
* @param name the field name
* @param value the field value to add
*/
public void add(String name, String value)
{
fields.compute(name, (k, f) ->
{
if (f == null)
// Preserve the case for the field name
return new Field(name, value);
else
return new Field(f.getName(), f.getValues(), value);
});
}
/**
* Adds the given value to a field with the given name,
* creating a {@link Field} is none exists for the given name.
*
* @param name the field name
* @param values the field values to add
*/
public void add(String name, String... values)
{
if (values == null || values.length == 0)
return;
if (values.length == 1)
add(name, values[0]);
else
{
fields.compute(name, (k, f) ->
{
if (f == null)
return new Field(name, List.of(values));
else
return new Field(f.getName(), f.getValues(), List.of(values));
});
}
}
/**
* Adds the given field, storing it if none exists for the given name,
* or adding all the values to the existing field with the given name.
*
* @param field the field to add
*/
public void add(Field field)
{
String key = field.getName();
fields.compute(key, (k, f) ->
{
if (f == null)
return field;
else
return new Field(f.getName(), f.getValues(), field.getValues());
});
}
public void addAll(Fields fields)
{
for (Field field : fields)
add(field);
}
/**
* Removes the {@link Field} with the given name.
*
* @param name the name of the field to remove
* @return the removed field, or null if no such field existed
*/
public Field remove(String name)
{
return fields.remove(name);
}
/**
* Empties this {@code Fields} instance from all fields.
*
* @see #isEmpty()
*/
public void clear()
{
fields.clear();
}
/**
* @return whether this {@code Fields} instance is empty
*/
public boolean isEmpty()
{
return fields.isEmpty();
}
/**
* Get the number of fields.
* @return the number of fields
*/
public int getSize()
{
return fields.size();
}
/**
* @return an iterator over the {@link Field}s present in this instance
*/
@Override
public Iterator iterator()
{
return fields.values().iterator();
}
/**
* @return the fields (name and values) of this instance copied into a {@code Map}
*/
public Map toStringArrayMap()
{
Map result = new LinkedHashMap<>();
fields.forEach((k, f) -> result.put(f.getName(), f.getValues().toArray(new String[0])));
return result;
}
/**
* @return the fields (name and values) of this instance copied into a {@code MultiMap}
*/
public MultiMap toMultiMap()
{
MultiMap multiMap = new MultiMap<>();
fields.forEach((k, f) -> multiMap.addValues(k, f.getValues()));
return multiMap;
}
@Override
public String toString()
{
return fields.values().stream()
.map(Field::toString)
.collect(Collectors.joining(",", "[", "]"));
}
/**
* A named list of string values.
* The name is case-sensitive and there must be at least one value.
*/
public static class Field
{
private final String name;
private final List values;
public Field(String name, String value)
{
this(name, List.of(value));
}
public Field(String name, List values)
{
this.name = name;
this.values = List.copyOf(values);
}
private Field(String name, List values, String extraValue)
{
this(name, append(values, extraValue));
}
private Field(String name, List values, List moreValues)
{
this(name, append(values, moreValues));
}
private static List append(List values, String extraValue)
{
return switch (values.size())
{
case 0 -> List.of(extraValue);
case 1 -> List.of(values.get(0), extraValue);
case 2 -> List.of(values.get(0), values.get(1), extraValue);
case 3 -> List.of(values.get(0), values.get(1), values.get(2), extraValue);
case 4 -> List.of(values.get(0), values.get(1), values.get(2), values.get(3), extraValue);
case 5 -> List.of(values.get(0), values.get(1), values.get(2), values.get(3), values.get(4), extraValue);
default ->
{
List list = new ArrayList<>(values.size() + 1);
list.addAll(values);
list.add(extraValue);
yield list;
}
};
}
private static List append(List values, List moreValues)
{
if (moreValues == null || moreValues.isEmpty())
return values;
if (moreValues.size() == 1)
return append(values, moreValues.get(0));
return switch (values.size())
{
case 0 -> moreValues;
case 1 -> switch (moreValues.size())
{
case 2 -> List.of(values.get(0), moreValues.get(0), moreValues.get(1));
case 3 -> List.of(values.get(0), moreValues.get(0), moreValues.get(1), moreValues.get(2));
case 4 -> List.of(values.get(0), moreValues.get(0), moreValues.get(1), moreValues.get(2), moreValues.get(3));
case 5 -> List.of(values.get(0), moreValues.get(0), moreValues.get(1), moreValues.get(2), moreValues.get(3), moreValues.get(4));
default ->
{
List list = new ArrayList<>(moreValues.size() + 1);
list.add(values.get(0));
list.addAll(moreValues);
yield list;
}
};
default ->
{
List list = new ArrayList<>(values.size() + moreValues.size());
list.addAll(values);
list.addAll(moreValues);
yield list;
}
};
}
@Override
public boolean equals(Object obj)
{
if (this == obj)
return true;
if (obj == null || getClass() != obj.getClass())
return false;
Field that = (Field)obj;
return name.equals(that.name) && values.equals(that.values);
}
@Override
public int hashCode()
{
return Objects.hash(name, values);
}
/**
* @return the field's name
*/
public String getName()
{
return name;
}
/**
* @return the first field's value
*/
public String getValue()
{
return values.get(0);
}
/**
* Attempts to convert the result of {@link #getValue()} to an integer,
* returning it if the conversion is successful; returns null if the
* result of {@link #getValue()} is null.
*
* @return the result of {@link #getValue()} converted to an integer, or null
* @throws NumberFormatException if the conversion fails
*/
public Integer getValueAsInt()
{
final String value = getValue();
return value == null ? null : (Integer)Integer.parseInt(value);
}
/**
* @return the field's values
*/
public List getValues()
{
return values;
}
/**
* @return whether the field has multiple values
*/
public boolean hasMultipleValues()
{
return values.size() > 1;
}
@Override
public String toString()
{
return String.format("%s=%s", name, values);
}
}
/**
* Combine two Fields
* @param a The base Fields or null
* @param b The overlay Fields or null
* @return Fields, which may be empty, but never null.
*/
public static Fields combine(Fields a, Fields b)
{
if (b == null || b.isEmpty())
return a == null ? EMPTY : a;
if (a == null || a.isEmpty())
return b;
Fields fields = new Fields(a.fields instanceof LinkedHashMap);
fields.addAll(a);
fields.addAll(b);
return fields;
}
private static Map multiMapToMapOfFields(MultiMap params)
{
if (params.isEmpty())
return Collections.emptyMap();
Map fields = new LinkedHashMap<>();
for (Map.Entry> entry : params.entrySet())
fields.put(entry.getKey(), new Field(entry.getKey(), entry.getValue()));
return fields;
}
}