// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.jdisc; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentSkipListMap; /** * This is an encapsulation of the header fields that belong to either a {@link Request} or a {@link Response}. It is * a multimap from String to String, with some additional methods for convenience. The keys of this map are compared by * ignoring their case, so that get("foo") returns the same entry as get("FOO"). * * @author Simon Thoresen Hult */ public class HeaderFields implements Map> { private final ConcurrentSkipListMap> content = new ConcurrentSkipListMap<>(String::compareToIgnoreCase); @Override public int size() { return content.size(); } @Override public boolean isEmpty() { return content.isEmpty(); } @Override public boolean containsKey(Object key) { return content.containsKey(key); } @Override public boolean containsValue(Object value) { return content.containsValue(value); } /** *

Convenience method for checking whether or not a named header contains a specific value. If the named header * is not set, or if the given value is not contained within that header's value list, this method returns * false.

* *

NOTE: This method is case-SENSITIVE.

* * @param key The key whose values to search in. * @param value The values to search for. * @return True if the given value was found in the named header. * @see #containsIgnoreCase */ public boolean contains(String key, String value) { List lst = content.get(key); if (lst == null) { return false; } return lst.contains(value); } /** *

Convenience method for checking whether or not a named header contains a specific value, regardless of case. * If the named header is not set, or if the given value is not contained within that header's value list, this * method returns false.

* *

NOTE: This method is case-INSENSITIVE.

* * @param key The key whose values to search in. * @param value The values to search for, ignoring case. * @return True if the given value was found in the named header. * @see #contains */ public boolean containsIgnoreCase(String key, String value) { List lst = content.get(key); if (lst == null) { return false; } for (String val : lst) { if (value.equalsIgnoreCase(val)) { return true; } } return false; } /** * Adds the given value to the entry of the specified key. If no entry exists for the given key, a new one is * created containing only the given value. * * @param key The key with which the specified value is to be associated. * @param value The value to be added to the list associated with the specified key. */ public void add(String key, String value) { List lst = content.get(key); if (lst != null) { lst.add(value); } else { put(key, value); } } /** * Adds the given values to the entry of the specified key. If no entry exists for the given key, a new one is * created containing only the given values. * * @param key the key with which the specified value is to be associated. * @param values the values to be added to the list associated with the specified key. */ public void add(String key, List values) { List lst = content.get(key); if (lst != null) { lst.addAll(values); } else { put(key, values); } } /** * Adds all the entries of the given map to this. This is the same as calling {@link #add(String, List)} for each * entry in values. * * @param values the values to be added to this. */ public void addAll(Map> values) { for (Entry> entry : values.entrySet()) { add(entry.getKey(), entry.getValue()); } } /** *

Convenience method to call {@link #put(String, List)} with a singleton list that contains the specified * value.

* * @param key The key of the entry to put. * @param value The value to put. * @return The previous value associated with key, or null if there was no mapping for * key. */ public List put(String key, String value) { List list = Collections.synchronizedList(new ArrayList<>(1)); list.add(value); return content.put(key, list); } @Override public List put(String key, List value) { return content.put(key, Collections.synchronizedList(new ArrayList<>(value))); } @Override public void putAll(Map> values) { for (Entry> entry : values.entrySet()) { put(entry.getKey(), entry.getValue()); } } @Override public List remove(Object key) { return content.remove(key); } /** *

Removes the given value from the entry of the specified key.

* * @param key The key of the entry to remove from. * @param value The value to remove from the entry. * @return True if the value was removed. */ public boolean remove(String key, String value) { List lst = content.get(key); if (lst == null) { return false; } if (!lst.remove(value)) { return false; } if (lst.isEmpty()) { content.remove(key); } return true; } @Override public void clear() { content.clear(); } @Override public List get(Object key) { return content.get(key); } /** * Convenience method for retrieving the first value of a named header field. If the header is not set, or if the * value list is empty, this method returns null. * * @param key the key whose first value to return * @return the first value of the named header, or null */ public String getFirst(String key) { List lst = get(key); if (lst == null || lst.isEmpty()) { return null; } return lst.get(0); } /** * Convenience method for checking whether a named header field is true. To satisfy this, the * header field needs to have at least 1 entry, and Boolean.valueOf() of all its values must parse as * true. * * @param key the key whose values to parse as a boolean. * @return the boolean value of the named header. */ public boolean isTrue(String key) { List lst = content.get(key); if (lst == null) { return false; } for (String value : lst) { if (!Boolean.valueOf(value)) { return false; } } return true; } @Override public Set keySet() { return content.keySet(); } @Override public Collection> values() { return content.values(); } @Override public Set>> entrySet() { return content.entrySet(); } @Override public String toString() { return content.toString(); } /** * Returns an unmodifiable list of all key-value pairs of this. This provides a flattened view on the content of * this map. * * @return the collection of entries */ public List> entries() { List> list = new ArrayList<>(content.size()); for (Entry> entry : content.entrySet()) { String key = entry.getKey(); for (String value : entry.getValue()) { list.add(new MyEntry(key, value)); } } return List.copyOf(list); } @Override public boolean equals(Object obj) { return obj instanceof HeaderFields && content.equals(((HeaderFields)obj).content); } @Override public int hashCode() { return content.hashCode(); } private static class MyEntry implements Map.Entry { final String key; final String value; private MyEntry(String key, String value) { this.key = key; this.value = value; } @Override public String getKey() { return key; } @Override public String getValue() { return value; } @Override public String setValue(String value) { throw new UnsupportedOperationException(); } @Override public String toString() { return key + '=' + value; } } }