--- 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)); + } } --- /dev/null 2018-04-23 10:23:09.668448234 +0100 +++ new/test/jdk/java/net/httpclient/HttpHeadersOf.java 2018-05-25 15:52:11.580873726 +0100 @@ -0,0 +1,396 @@ +/* + * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @summary Tests for HttpHeaders.of factory method + * @run testng HttpHeadersOf + */ + +import java.net.http.HttpHeaders; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.function.BiPredicate; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertThrows; +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.fail; + +public class HttpHeadersOf { + + static final Class NPE = NullPointerException.class; + static final Class NFE = NumberFormatException.class; + static final Class UOE = UnsupportedOperationException.class; + + static final BiPredicate ACCEPT_ALL = + new BiPredicate<>() { + @Override public boolean test(String name, String value) { return true; } + @Override public String toString() { return "ACCEPT_ALL"; } + }; + + static final BiPredicate REJECT_ALL = + new BiPredicate<>() { + @Override public boolean test(String name, String value) { return false; } + @Override public String toString() { return "REJECT_ALL"; } + }; + + @DataProvider(name = "predicates") + public Object[][] predicates() { + return new Object[][] { { ACCEPT_ALL }, { REJECT_ALL } }; + } + + @Test(dataProvider = "predicates") + public void testNull(BiPredicate filter) { + assertThrows(NPE, () -> HttpHeaders.of(null, null)); + assertThrows(NPE, () -> HttpHeaders.of(null, filter)); + assertThrows(NPE, () -> HttpHeaders.of(Map.of(), null)); + assertThrows(NPE, () -> HttpHeaders.of(Map.of("name", List.of("value")), null)); + + // nulls in the Map + assertThrows(NPE, () -> HttpHeaders.of(Map.of(null, List.of("value)")), filter)); + assertThrows(NPE, () -> HttpHeaders.of(Map.of("name", null), filter)); + assertThrows(NPE, () -> HttpHeaders.of(Map.of("name", List.of(null)), filter)); + assertThrows(NPE, () -> HttpHeaders.of(Map.of("name", List.of("aValue", null)), filter)); + assertThrows(NPE, () -> HttpHeaders.of(Map.of("name", List.of(null, "aValue")), filter)); + } + + + @DataProvider(name = "filterMaps") + public Object[][] filterMaps() { + List>> maps = List.of( + Map.of("A", List.of("B"), "X", List.of("Y", "Z")), + Map.of("A", List.of("B", "C"), "X", List.of("Y", "Z")), + Map.of("A", List.of("B", "C", "D"), "X", List.of("Y", "Z")) + ); + return maps.stream().map(p -> new Object[] { p }).toArray(Object[][]::new); + } + + @Test(dataProvider = "filterMaps") + public void testFilter(Map> map) { + HttpHeaders headers = HttpHeaders.of(map, REJECT_ALL); + assertEquals(headers.map().size(), 0); + assertFalse(headers.firstValue("A").isPresent()); + assertEquals(headers.allValues("A").size(), 0); + + headers = HttpHeaders.of(map, (name, value) -> { + if (name.equals("A")) return true; else return false; }); + assertEquals(headers.map().size(), 1); + assertTrue(headers.firstValue("A").isPresent()); + assertEquals(headers.allValues("A"), map.get("A")); + assertEquals(headers.allValues("A").size(), map.get("A").size()); + assertFalse(headers.firstValue("X").isPresent()); + + headers = HttpHeaders.of(map, (name, value) -> { + if (name.equals("X")) return true; else return false; }); + assertEquals(headers.map().size(), 1); + assertTrue(headers.firstValue("X").isPresent()); + assertEquals(headers.allValues("X"), map.get("X")); + assertEquals(headers.allValues("X").size(), map.get("X").size()); + assertFalse(headers.firstValue("A").isPresent()); + } + + + @DataProvider(name = "mapValues") + public Object[][] mapValues() { + List>> maps = List.of( + Map.of("A", List.of("B")), + Map.of("A", List.of("B", "C")), + Map.of("A", List.of("B", "C", "D")), + + Map.of("A", List.of("B"), "X", List.of("Y", "Z")), + Map.of("A", List.of("B", "C"), "X", List.of("Y", "Z")), + Map.of("A", List.of("B", "C", "D"), "X", List.of("Y", "Z")), + + Map.of("A", List.of("B"), "X", List.of("Y", "Z")), + Map.of("A", List.of("B", "C"), "X", List.of("Y", "Z")), + Map.of("A", List.of("B", "C", "D"), "X", List.of("Y", "Z")), + + Map.of("X", List.of("Y", "Z"), "A", List.of("B")), + Map.of("X", List.of("Y", "Z"), "A", List.of("B", "C")), + Map.of("X", List.of("Y", "Z"), "A", List.of("B", "C", "D")) + ); + return maps.stream().map(p -> new Object[] { p }).toArray(Object[][]::new); + } + + @Test(dataProvider = "mapValues") + public void testMapValues(Map> map) { + HttpHeaders headers = HttpHeaders.of(map, ACCEPT_ALL); + + assertEquals(headers.map().size(), map.size()); + assertTrue(headers.firstValue("A").isPresent()); + assertTrue(headers.firstValue("a").isPresent()); + assertEquals(headers.firstValue("A").get(), "B"); + assertEquals(headers.firstValue("a").get(), "B"); + assertEquals(headers.allValues("A"), map.get("A")); + assertEquals(headers.allValues("a"), map.get("A")); + assertEquals(headers.allValues("F").size(), 0); + assertTrue(headers.map().get("A").contains("B")); + assertFalse(headers.map().get("A").contains("F")); + assertThrows(NFE, () -> headers.firstValueAsLong("A")); + + // a non-exhaustive list of mutators + assertThrows(UOE, () -> headers.map().put("Z", List.of("Z"))); + assertThrows(UOE, () -> headers.map().remove("A")); + assertThrows(UOE, () -> headers.map().remove("A", "B")); + assertThrows(UOE, () -> headers.map().clear()); + assertThrows(UOE, () -> headers.allValues("A").remove("B")); + assertThrows(UOE, () -> headers.allValues("A").remove(1)); + assertThrows(UOE, () -> headers.allValues("A").clear()); + assertThrows(UOE, () -> headers.allValues("A").add("Z")); + assertThrows(UOE, () -> headers.allValues("A").addAll(List.of("Z"))); + assertThrows(UOE, () -> headers.allValues("A").add(1, "Z")); + } + + + @DataProvider(name = "caseInsensitivity") + public Object[][] caseInsensitivity() { + List>> maps = List.of( + Map.of("Accept-Encoding", List.of("gzip, deflate")), + Map.of("accept-encoding", List.of("gzip, deflate")), + Map.of("AccePT-ENCoding", List.of("gzip, deflate")), + Map.of("ACCept-EncodING", List.of("gzip, deflate")), + Map.of("ACCEPT-ENCODING", List.of("gzip, deflate")) + ); + return maps.stream().map(p -> new Object[] { p }).toArray(Object[][]::new); + } + + @Test(dataProvider = "caseInsensitivity") + public void testCaseInsensitivity(Map> map) { + HttpHeaders headers = HttpHeaders.of(map, ACCEPT_ALL); + + for (String name : List.of("Accept-Encoding", "accept-encoding", + "aCCept-EnCODing", "accepT-encodinG")) { + assertTrue(headers.firstValue(name).isPresent()); + assertTrue(headers.allValues(name).contains("gzip, deflate")); + assertEquals(headers.firstValue(name).get(), "gzip, deflate"); + assertEquals(headers.allValues(name).size(), 1); + assertEquals(headers.map().size(), 1); + assertEquals(headers.map().get(name).size(), 1); + assertEquals(headers.map().get(name).get(0), "gzip, deflate"); + } + } + + @DataProvider(name = "valueAsLong") + public Object[][] valueAsLong() { + return new Object[][] { + new Object[] { Map.of("Content-Length", List.of("10")), 10l }, + new Object[] { Map.of("Content-Length", List.of("101")), 101l }, + new Object[] { Map.of("Content-Length", List.of("56789")), 56789l }, + new Object[] { Map.of("Content-Length", List.of(Long.toString(Long.MAX_VALUE))), Long.MAX_VALUE }, + new Object[] { Map.of("Content-Length", List.of(Long.toString(Long.MIN_VALUE))), Long.MIN_VALUE } + }; + } + + @Test(dataProvider = "valueAsLong") + public void testValueAsLong(Map> map, long expected) { + HttpHeaders headers = HttpHeaders.of(map, ACCEPT_ALL); + assertEquals(headers.firstValueAsLong("Content-Length").getAsLong(), expected); + } + + + @DataProvider(name = "duplicateNames") + public Object[][] duplicateNames() { + List>> maps = List.of( + Map.of("X-name", List.of(), + "x-name", List.of()), + Map.of("X-name", List.of(""), + "x-name", List.of("")), + Map.of("X-name", List.of("C"), + "x-name", List.of("D")), + Map.of("X-name", List.of("E"), + "Y-name", List.of("F"), + "X-Name", List.of("G")), + Map.of("X-chegar", List.of("H"), + "y-dfuchs", List.of("I"), + "Y-dfuchs", List.of("J")), + Map.of("X-name ", List.of("K"), + "X-Name", List.of("L")), + Map.of("X-name", List.of("M"), + "\rX-Name", List.of("N")) + ); + return maps.stream().map(p -> new Object[] { p }).toArray(Object[][]::new); + } + + @Test(dataProvider = "duplicateNames") + public void testDuplicates(Map> map) { + HttpHeaders headers; + try { + headers = HttpHeaders.of(map, ACCEPT_ALL); + fail("UNEXPECTED: " + headers); + } catch (IllegalArgumentException iae) { + System.out.println("caught EXPECTED IAE:" + iae); + assertTrue(iae.getMessage().contains("duplicate")); + } + } + + + @DataProvider(name = "noSplittingJoining") + public Object[][] noSplittingJoining() { + List>> maps = List.of( + Map.of("A", List.of("B")), + Map.of("A", List.of("B", "C")), + Map.of("A", List.of("B", "C", "D")), + Map.of("A", List.of("B", "C", "D", "E")), + Map.of("A", List.of("B", "C", "D", "E", "F")), + Map.of("A", List.of("B, C")), + Map.of("A", List.of("B, C, D")), + Map.of("A", List.of("B, C, D, E")), + Map.of("A", List.of("B, C, D, E, F")), + Map.of("A", List.of("B, C", "D", "E", "F")), + Map.of("A", List.of("B", "C, D", "E", "F")), + Map.of("A", List.of("B", "C, D", "E, F")), + Map.of("A", List.of("B", "C, D, E", "F")), + Map.of("A", List.of("B", "C, D, E, F")) + ); + return maps.stream().map(p -> new Object[] { p }).toArray(Object[][]::new); + } + + @Test(dataProvider = "noSplittingJoining") + public void testNoSplittingJoining(Map> map) { + HttpHeaders headers = HttpHeaders.of(map, ACCEPT_ALL); + Map> headersMap = headers.map(); + + assertEquals(headers.map().size(), map.size()); + for (Map.Entry> entry : map.entrySet()) { + String headerName = entry.getKey(); + List headerValues = entry.getValue(); + assertEquals(headerValues, headersMap.get(headerName)); + assertEquals(headerValues, headers.allValues(headerName)); + assertEquals(headerValues.get(0), headers.firstValue(headerName).get()); + } + } + + + @DataProvider(name = "trimming") + public Object[][] trimming() { + List>> maps = List.of( + Map.of("A", List.of("B")), + Map.of(" A", List.of("B")), + Map.of("A ", List.of("B")), + Map.of("A", List.of(" B")), + Map.of("A", List.of("B ")), + Map.of("\tA", List.of("B")), + Map.of("A\t", List.of("B")), + Map.of("A", List.of("\tB")), + Map.of("A", List.of("B\t")), + Map.of("A\r", List.of("B")), + Map.of("A\n", List.of("B")), + Map.of("A\r\n", List.of("B")) + ); + return maps.stream().map(p -> new Object[] { p }).toArray(Object[][]::new); + } + + @Test(dataProvider = "trimming") + public void testTrimming(Map> map) { + HttpHeaders headers = HttpHeaders.of(map, (name, value) -> { + assertEquals(name, "A"); + assertEquals(value, "B"); + return true; + }); + + assertEquals(headers.map().size(), 1); + assertEquals(headers.firstValue("A").get(), "B"); + assertEquals(headers.allValues("A"), List.of("B")); + assertTrue(headers.map().get("A").equals(List.of("B"))); + } + + + @DataProvider(name = "emptyKey") + public Object[][] emptyKey() { + List>> maps = List.of( + Map.of("", List.of("B")), + Map.of(" ", List.of("B")), + Map.of(" ", List.of("B")), + Map.of("\t", List.of("B")), + Map.of("\t\t", List.of("B")) + ); + return maps.stream().map(p -> new Object[] { p }).toArray(Object[][]::new); + } + + @Test(dataProvider = "emptyKey") + public void testEmptyKey(Map> map) { + HttpHeaders headers; + try { + headers = HttpHeaders.of(map, ACCEPT_ALL); + fail("UNEXPECTED: " + headers); + } catch (IllegalArgumentException iae) { + System.out.println("caught EXPECTED IAE:" + iae); + assertTrue(iae.getMessage().contains("empty")); + } + } + + + @DataProvider(name = "emptyValue") + public Object[][] emptyValue() { + List>> maps = List.of( + Map.of("A", List.of("")), + Map.of("A", List.of("", "")), + Map.of("A", List.of("", "", " ")), + Map.of("A", List.of("\t")), + Map.of("A", List.of("\t\t")) + ); + return maps.stream().map(p -> new Object[] { p }).toArray(Object[][]::new); + } + + @Test(dataProvider = "emptyValue") + public void testEmptyValue(Map> map) { + HttpHeaders headers = HttpHeaders.of(map, (name, value) -> { + assertEquals(value, ""); + return true; + }); + assertEquals(headers.map().size(), map.size()); + assertEquals(headers.map().get("A").get(0), ""); + headers.allValues("A").forEach(v -> assertEquals(v, "")); + assertEquals(headers.firstValue("A").get(), ""); + } + + + @DataProvider(name = "noValues") + public Object[][] noValues() { + List>> maps = List.of( + Map.of("A", List.of()), + Map.of("A", List.of(), "B", List.of()), + Map.of("A", List.of(), "B", List.of(), "C", List.of()), + Map.of("A", new ArrayList()), + Map.of("A", new LinkedList()) + ); + return maps.stream().map(p -> new Object[] { p }).toArray(Object[][]::new); + } + + @Test(dataProvider = "noValues") + public void testNoValues(Map> map) { + HttpHeaders headers = HttpHeaders.of(map, (name, value) -> { + fail("UNEXPECTED call to filter"); + return true; + }); + assertEquals(headers.map().size(), 0); + assertEquals(headers.map().get("A"), null); + assertEquals(headers.allValues("A").size(), 0); + assertFalse(headers.firstValue("A").isPresent()); + } +} --- old/src/java.net.http/share/classes/jdk/internal/net/http/ImmutableHeaders.java 2018-05-25 15:52:21.444663884 +0100 +++ /dev/null 2018-04-23 10:23:09.668448234 +0100 @@ -1,116 +0,0 @@ -/* - * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - -package jdk.internal.net.http; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.TreeMap; -import java.util.function.BiPredicate; -import java.util.function.Predicate; -import java.net.http.HttpHeaders; -import jdk.internal.net.http.common.HttpHeadersImpl; -import jdk.internal.net.http.common.Utils; -import static java.util.Collections.emptyMap; -import static java.util.Collections.unmodifiableList; -import static java.util.Collections.unmodifiableMap; -import static java.util.Objects.requireNonNull; - -final class ImmutableHeaders extends HttpHeaders { - - private final Map> map; - - public static ImmutableHeaders empty() { - return of(emptyMap()); - } - - public static ImmutableHeaders of(Map> src) { - return of(src, x -> true); - } - - public static ImmutableHeaders of(HttpHeaders headers) { - return (headers instanceof ImmutableHeaders) - ? (ImmutableHeaders)headers - : of(headers.map()); - } - - static ImmutableHeaders validate(HttpHeaders headers) { - if (headers instanceof ImmutableHeaders) { - return of(headers); - } - if (headers instanceof HttpHeadersImpl) { - return of(headers); - } - Map> map = headers.map(); - return new ImmutableHeaders(map, Utils.VALIDATE_USER_HEADER); - } - - public static ImmutableHeaders of(Map> src, - Predicate keyAllowed) { - requireNonNull(src, "src"); - requireNonNull(keyAllowed, "keyAllowed"); - return new ImmutableHeaders(src, headerAllowed(keyAllowed)); - } - - public static ImmutableHeaders of(Map> src, - BiPredicate> headerAllowed) { - requireNonNull(src, "src"); - requireNonNull(headerAllowed, "headerAllowed"); - return new ImmutableHeaders(src, headerAllowed); - } - - private ImmutableHeaders(Map> src, - BiPredicate> headerAllowed) { - Map> m = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); - src.entrySet().stream() - .forEach(e -> addIfAllowed(e, headerAllowed, m)); - this.map = unmodifiableMap(m); - } - - private static void addIfAllowed(Map.Entry> e, - BiPredicate> headerAllowed, - Map> map) { - String key = e.getKey(); - List values = unmodifiableValues(e.getValue()); - if (headerAllowed.test(key, values)) { - map.put(key, values); - } - } - - private static List unmodifiableValues(List values) { - return unmodifiableList(new ArrayList<>(Objects.requireNonNull(values))); - } - - private static BiPredicate> headerAllowed(Predicate keyAllowed) { - return (n,v) -> keyAllowed.test(n); - } - - @Override - public Map> map() { - return map; - } -} --- old/src/java.net.http/share/classes/jdk/internal/net/http/common/HttpHeadersImpl.java 2018-05-25 15:52:26.292560749 +0100 +++ /dev/null 2018-04-23 10:23:09.668448234 +0100 @@ -1,89 +0,0 @@ -/* - * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - -package jdk.internal.net.http.common; - -import java.net.http.HttpHeaders; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.TreeMap; - -/** - * Implementation of HttpHeaders. - * - * The public HttpHeaders API provides a read-only view, while the - * non-HttpHeaders members allow for implementation specific mutation, e.g. - * during creation, etc. - */ -public class HttpHeadersImpl extends HttpHeaders { - - private final TreeMap> headers; - - public HttpHeadersImpl() { - headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); - } - - @Override - public Map> map() { - return Collections.unmodifiableMap(headersMap()); - } - - // non-HttpHeaders private mutators - - public HttpHeadersImpl deepCopy() { - HttpHeadersImpl h1 = newDeepCopy(); - for (Map.Entry> entry : headersMap().entrySet()) { - List valuesCopy = new ArrayList<>(entry.getValue()); - h1.headersMap().put(entry.getKey(), valuesCopy); - } - return h1; - } - - public void addHeader(String name, String value) { - headersMap().computeIfAbsent(name, k -> new ArrayList<>(1)) - .add(value); - } - - public void setHeader(String name, String value) { - // headers typically have one value - List values = new ArrayList<>(1); - values.add(value); - headersMap().put(name, values); - } - - public void clear() { - headersMap().clear(); - } - - protected HttpHeadersImpl newDeepCopy() { - return new HttpHeadersImpl(); - } - - protected Map> headersMap() { - return headers; - } -}