--- old/src/java.net.http/share/classes/java/net/http/HttpHeaders.java 2018-05-25 15:52:01.597086117 +0100 +++ new/src/java.net.http/share/classes/java/net/http/HttpHeaders.java 2018-05-25 15:52:01.313092158 +0100 @@ -25,62 +25,68 @@ package java.net.http; +import java.util.ArrayList; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.OptionalLong; -import static java.util.Collections.emptyList; -import static java.util.Collections.unmodifiableList; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.function.BiPredicate; +import static java.lang.String.CASE_INSENSITIVE_ORDER; +import static java.util.Collections.unmodifiableMap; import static java.util.Objects.requireNonNull; /** * A read-only view of a set of HTTP headers. * - *

An {@code HttpHeaders} is not created directly, but rather returned from - * an {@link HttpResponse HttpResponse}. Specific HTTP headers can be set for - * {@linkplain HttpRequest requests} through the one of the request builder's - * {@link HttpRequest.Builder#header(String, String) headers} methods. + *

An {@code HttpHeaders} is not typically created directly, but rather + * returned from an {@link HttpRequest#headers() HttpRequest} or an + * {@link HttpResponse#headers() HttpResponse}. Specific HTTP headers can be + * set for a {@linkplain HttpRequest request} through one of the request + * builder's {@link HttpRequest.Builder#header(String, String) headers} methods. * *

The methods of this class ( that accept a String header name ), and the - * Map returned by the {@link #map() map} method, operate without regard to - * case when retrieving the header value. + * {@code Map} returned by the {@link #map() map} method, operate without regard + * to case when retrieving the header value(s). + * + *

An HTTP header name may appear more than once in the HTTP protocol. As + * such, headers are represented as a name and a list of values. Each occurrence + * of a header value is added verbatim, to the appropriate header name list, + * without interpreting its value. In particular, {@code HttpHeaders} does not + * perform any splitting or joining of comma separated header value strings. The + * order of elements in a header value list is preserved when {@link + * HttpRequest.Builder#header(String, String) building} a request. For + * responses, the order of elements in a header value list is the order in which + * they were received. The {@code Map} returned by the {@code map} method, + * however, does not provide any guarantee with regard to the ordering of its + * entries. * *

{@code HttpHeaders} instances are immutable. * * @since 11 */ -public abstract class HttpHeaders { +public final class HttpHeaders { /** - * Creates an HttpHeaders. - */ - protected HttpHeaders() {} - - /** - * Returns an {@link Optional} containing the first value of the given named - * (and possibly multi-valued) header. If the header is not present, then - * the returned {@code Optional} is empty. - * - * @implSpec - * The default implementation invokes - * {@code allValues(name).stream().findFirst()} + * Returns an {@link Optional} containing the first header string value of + * the given named (and possibly multi-valued) header. If the header is not + * present, then the returned {@code Optional} is empty. * * @param name the header name - * @return an {@code Optional} for the first named value + * @return an {@code Optional} containing the first named header + * string value, if present */ public Optional firstValue(String name) { return allValues(name).stream().findFirst(); } /** - * Returns an {@link OptionalLong} containing the first value of the - * named header field. If the header is not present, then the Optional is - * empty. If the header is present but contains a value that does not parse - * as a {@code Long} value, then an exception is thrown. - * - * @implSpec - * The default implementation invokes - * {@code allValues(name).stream().mapToLong(Long::valueOf).findFirst()} + * Returns an {@link OptionalLong} containing the first header string value + * of the named header field. If the header is not present, then the + * Optional is empty. If the header is present but contains a value that + * does not parse as a {@code Long} value, then an exception is thrown. * * @param name the header name * @return an {@code OptionalLong} @@ -92,23 +98,19 @@ } /** - * Returns an unmodifiable List of all of the values of the given named - * header. Always returns a List, which may be empty if the header is not - * present. - * - * @implSpec - * The default implementation invokes, among other things, the - * {@code map().get(name)} to retrieve the list of header values. + * Returns an unmodifiable List of all of the header string values of the + * given named header. Always returns a List, which may be empty if the + * header is not present. * * @param name the header name - * @return a List of String values + * @return a List of headers string values */ public List allValues(String name) { requireNonNull(name); List values = map().get(name); // Making unmodifiable list out of empty in order to make a list which // throws UOE unconditionally - return values != null ? values : unmodifiableList(emptyList()); + return values != null ? values : List.of(); } /** @@ -116,7 +118,9 @@ * * @return the Map */ - public abstract Map> map(); + public Map> map() { + return headers; + } /** * Tests this HTTP headers instance for equality with the given object. @@ -165,4 +169,84 @@ sb.append(" }"); return sb.toString(); } + + /** + * Returns an HTTP headers from the given map. The given map's key + * represents the header name, and its value the list of string header + * values for that header name. + * + *

An HTTP header name may appear more than once in the HTTP protocol. + * Such, multi-valued, headers must be represented by a single entry + * in the given map, whose entry value is a list that represents the + * multiple header string values. Leading and trailing whitespaces are + * removed from all string values retrieved from the given map and its lists + * before processing. Only headers that, after filtering, contain at least + * one, possibly empty string, value will be added to the HTTP headers. + * + * @apiNote The primary purpose of this method is for testing frameworks. + * Per-request headers can be set through one of the {@code HttpRequest} + * {@link HttpRequest.Builder#header(String, String) headers} methods. + * + * @param headerMap the map containing the header names and values + * @param filter a filter that can be used to inspect each + * header-name-and-value pair in the given map to determine if + * it should, or should not, be added to the to the HTTP + * headers + * @return an HTTP headers instance containing the given headers + * @throws NullPointerException if any of: {@code headerMap}, a key or value + * in the given map, or an entry in the map's value list, or + * {@code filter}, is {@code null} + * @throws IllegalArgumentException if the given {@code headerMap} contains + * any two keys that are equal ( without regard to case ); or if the + * given map contains any key whose length, after trimming + * whitespaces, is {@code 0} + */ + public static HttpHeaders of(Map> headerMap, + BiPredicate filter) { + requireNonNull(headerMap); + requireNonNull(filter); + return headersOf(headerMap, filter); + } + + // -- + + private static final HttpHeaders NO_HEADERS = new HttpHeaders(Map.of()); + + private final Map> headers; + + private HttpHeaders(Map> headers) { + this.headers = headers; + } + + // Returns a new HTTP headers after performing a structural copy and filtering. + private static HttpHeaders headersOf(Map> map, + BiPredicate filter) { + TreeMap> other = new TreeMap<>(CASE_INSENSITIVE_ORDER); + TreeSet notAdded = new TreeSet<>(CASE_INSENSITIVE_ORDER); + ArrayList tempList = new ArrayList<>(); + map.forEach((key, value) -> { + String headerName = requireNonNull(key).trim(); + if (headerName.isEmpty()) { + throw new IllegalArgumentException("empty key"); + } + List headerValues = requireNonNull(value); + headerValues.forEach(headerValue -> { + headerValue = requireNonNull(headerValue).trim(); + if (filter.test(headerName, headerValue)) { + tempList.add(headerValue); + } + }); + + if (tempList.isEmpty()) { + if (other.containsKey(headerName) + || notAdded.contains(headerName.toLowerCase(Locale.ROOT))) + throw new IllegalArgumentException("duplicate key: " + headerName); + notAdded.add(headerName.toLowerCase(Locale.ROOT)); + } else if (other.put(headerName, List.copyOf(tempList)) != null) { + throw new IllegalArgumentException("duplicate key: " + headerName); + } + tempList.clear(); + }); + return other.isEmpty() ? NO_HEADERS : new HttpHeaders(unmodifiableMap(other)); + } }