23 * questions. |
23 * questions. |
24 */ |
24 */ |
25 |
25 |
26 package java.net.http; |
26 package java.net.http; |
27 |
27 |
|
28 import java.util.ArrayList; |
28 import java.util.List; |
29 import java.util.List; |
|
30 import java.util.Locale; |
29 import java.util.Map; |
31 import java.util.Map; |
30 import java.util.Optional; |
32 import java.util.Optional; |
31 import java.util.OptionalLong; |
33 import java.util.OptionalLong; |
32 import static java.util.Collections.emptyList; |
34 import java.util.TreeMap; |
33 import static java.util.Collections.unmodifiableList; |
35 import java.util.TreeSet; |
|
36 import java.util.function.BiPredicate; |
|
37 import static java.lang.String.CASE_INSENSITIVE_ORDER; |
|
38 import static java.util.Collections.unmodifiableMap; |
34 import static java.util.Objects.requireNonNull; |
39 import static java.util.Objects.requireNonNull; |
35 |
40 |
36 /** |
41 /** |
37 * A read-only view of a set of HTTP headers. |
42 * A read-only view of a set of HTTP headers. |
38 * |
43 * |
39 * <p> An {@code HttpHeaders} is not created directly, but rather returned from |
44 * <p> An {@code HttpHeaders} is not typically created directly, but rather |
40 * an {@link HttpResponse HttpResponse}. Specific HTTP headers can be set for |
45 * returned from an {@link HttpRequest#headers() HttpRequest} or an |
41 * {@linkplain HttpRequest requests} through the one of the request builder's |
46 * {@link HttpResponse#headers() HttpResponse}. Specific HTTP headers can be |
42 * {@link HttpRequest.Builder#header(String, String) headers} methods. |
47 * set for a {@linkplain HttpRequest request} through one of the request |
|
48 * builder's {@link HttpRequest.Builder#header(String, String) headers} methods. |
43 * |
49 * |
44 * <p> The methods of this class ( that accept a String header name ), and the |
50 * <p> The methods of this class ( that accept a String header name ), and the |
45 * Map returned by the {@link #map() map} method, operate without regard to |
51 * {@code Map} returned by the {@link #map() map} method, operate without regard |
46 * case when retrieving the header value. |
52 * to case when retrieving the header value(s). |
|
53 * |
|
54 * <p> An HTTP header name may appear more than once in the HTTP protocol. As |
|
55 * such, headers are represented as a name and a list of values. Each occurrence |
|
56 * of a header value is added verbatim, to the appropriate header name list, |
|
57 * without interpreting its value. In particular, {@code HttpHeaders} does not |
|
58 * perform any splitting or joining of comma separated header value strings. The |
|
59 * order of elements in a header value list is preserved when {@link |
|
60 * HttpRequest.Builder#header(String, String) building} a request. For |
|
61 * responses, the order of elements in a header value list is the order in which |
|
62 * they were received. The {@code Map} returned by the {@code map} method, |
|
63 * however, does not provide any guarantee with regard to the ordering of its |
|
64 * entries. |
47 * |
65 * |
48 * <p> {@code HttpHeaders} instances are immutable. |
66 * <p> {@code HttpHeaders} instances are immutable. |
49 * |
67 * |
50 * @since 11 |
68 * @since 11 |
51 */ |
69 */ |
52 public abstract class HttpHeaders { |
70 public final class HttpHeaders { |
53 |
71 |
54 /** |
72 /** |
55 * Creates an HttpHeaders. |
73 * Returns an {@link Optional} containing the first header string value of |
56 */ |
74 * the given named (and possibly multi-valued) header. If the header is not |
57 protected HttpHeaders() {} |
75 * present, then the returned {@code Optional} is empty. |
58 |
|
59 /** |
|
60 * Returns an {@link Optional} containing the first value of the given named |
|
61 * (and possibly multi-valued) header. If the header is not present, then |
|
62 * the returned {@code Optional} is empty. |
|
63 * |
|
64 * @implSpec |
|
65 * The default implementation invokes |
|
66 * {@code allValues(name).stream().findFirst()} |
|
67 * |
76 * |
68 * @param name the header name |
77 * @param name the header name |
69 * @return an {@code Optional<String>} for the first named value |
78 * @return an {@code Optional<String>} containing the first named header |
|
79 * string value, if present |
70 */ |
80 */ |
71 public Optional<String> firstValue(String name) { |
81 public Optional<String> firstValue(String name) { |
72 return allValues(name).stream().findFirst(); |
82 return allValues(name).stream().findFirst(); |
73 } |
83 } |
74 |
84 |
75 /** |
85 /** |
76 * Returns an {@link OptionalLong} containing the first value of the |
86 * Returns an {@link OptionalLong} containing the first header string value |
77 * named header field. If the header is not present, then the Optional is |
87 * of the named header field. If the header is not present, then the |
78 * empty. If the header is present but contains a value that does not parse |
88 * Optional is empty. If the header is present but contains a value that |
79 * as a {@code Long} value, then an exception is thrown. |
89 * does not parse as a {@code Long} value, then an exception is thrown. |
80 * |
|
81 * @implSpec |
|
82 * The default implementation invokes |
|
83 * {@code allValues(name).stream().mapToLong(Long::valueOf).findFirst()} |
|
84 * |
90 * |
85 * @param name the header name |
91 * @param name the header name |
86 * @return an {@code OptionalLong} |
92 * @return an {@code OptionalLong} |
87 * @throws NumberFormatException if a value is found, but does not parse as |
93 * @throws NumberFormatException if a value is found, but does not parse as |
88 * a Long |
94 * a Long |
90 public OptionalLong firstValueAsLong(String name) { |
96 public OptionalLong firstValueAsLong(String name) { |
91 return allValues(name).stream().mapToLong(Long::valueOf).findFirst(); |
97 return allValues(name).stream().mapToLong(Long::valueOf).findFirst(); |
92 } |
98 } |
93 |
99 |
94 /** |
100 /** |
95 * Returns an unmodifiable List of all of the values of the given named |
101 * Returns an unmodifiable List of all of the header string values of the |
96 * header. Always returns a List, which may be empty if the header is not |
102 * given named header. Always returns a List, which may be empty if the |
97 * present. |
103 * header is not present. |
98 * |
|
99 * @implSpec |
|
100 * The default implementation invokes, among other things, the |
|
101 * {@code map().get(name)} to retrieve the list of header values. |
|
102 * |
104 * |
103 * @param name the header name |
105 * @param name the header name |
104 * @return a List of String values |
106 * @return a List of headers string values |
105 */ |
107 */ |
106 public List<String> allValues(String name) { |
108 public List<String> allValues(String name) { |
107 requireNonNull(name); |
109 requireNonNull(name); |
108 List<String> values = map().get(name); |
110 List<String> values = map().get(name); |
109 // Making unmodifiable list out of empty in order to make a list which |
111 // Making unmodifiable list out of empty in order to make a list which |
110 // throws UOE unconditionally |
112 // throws UOE unconditionally |
111 return values != null ? values : unmodifiableList(emptyList()); |
113 return values != null ? values : List.of(); |
112 } |
114 } |
113 |
115 |
114 /** |
116 /** |
115 * Returns an unmodifiable multi Map view of this HttpHeaders. |
117 * Returns an unmodifiable multi Map view of this HttpHeaders. |
116 * |
118 * |
117 * @return the Map |
119 * @return the Map |
118 */ |
120 */ |
119 public abstract Map<String, List<String>> map(); |
121 public Map<String,List<String>> map() { |
|
122 return headers; |
|
123 } |
120 |
124 |
121 /** |
125 /** |
122 * Tests this HTTP headers instance for equality with the given object. |
126 * Tests this HTTP headers instance for equality with the given object. |
123 * |
127 * |
124 * <p> If the given object is not an {@code HttpHeaders} then this |
128 * <p> If the given object is not an {@code HttpHeaders} then this |
163 sb.append(super.toString()).append(" { "); |
171 sb.append(super.toString()).append(" { "); |
164 sb.append(map()); |
172 sb.append(map()); |
165 sb.append(" }"); |
173 sb.append(" }"); |
166 return sb.toString(); |
174 return sb.toString(); |
167 } |
175 } |
|
176 |
|
177 /** |
|
178 * Returns an HTTP headers from the given map. The given map's key |
|
179 * represents the header name, and its value the list of string header |
|
180 * values for that header name. |
|
181 * |
|
182 * <p> An HTTP header name may appear more than once in the HTTP protocol. |
|
183 * Such, <i>multi-valued</i>, headers must be represented by a single entry |
|
184 * in the given map, whose entry value is a list that represents the |
|
185 * multiple header string values. Leading and trailing whitespaces are |
|
186 * removed from all string values retrieved from the given map and its lists |
|
187 * before processing. Only headers that, after filtering, contain at least |
|
188 * one, possibly empty string, value will be added to the HTTP headers. |
|
189 * |
|
190 * @apiNote The primary purpose of this method is for testing frameworks. |
|
191 * Per-request headers can be set through one of the {@code HttpRequest} |
|
192 * {@link HttpRequest.Builder#header(String, String) headers} methods. |
|
193 * |
|
194 * @param headerMap the map containing the header names and values |
|
195 * @param filter a filter that can be used to inspect each |
|
196 * header-name-and-value pair in the given map to determine if |
|
197 * it should, or should not, be added to the to the HTTP |
|
198 * headers |
|
199 * @return an HTTP headers instance containing the given headers |
|
200 * @throws NullPointerException if any of: {@code headerMap}, a key or value |
|
201 * in the given map, or an entry in the map's value list, or |
|
202 * {@code filter}, is {@code null} |
|
203 * @throws IllegalArgumentException if the given {@code headerMap} contains |
|
204 * any two keys that are equal ( without regard to case ); or if the |
|
205 * given map contains any key whose length, after trimming |
|
206 * whitespaces, is {@code 0} |
|
207 */ |
|
208 public static HttpHeaders of(Map<String,List<String>> headerMap, |
|
209 BiPredicate<String,String> filter) { |
|
210 requireNonNull(headerMap); |
|
211 requireNonNull(filter); |
|
212 return headersOf(headerMap, filter); |
|
213 } |
|
214 |
|
215 // -- |
|
216 |
|
217 private static final HttpHeaders NO_HEADERS = new HttpHeaders(Map.of()); |
|
218 |
|
219 private final Map<String,List<String>> headers; |
|
220 |
|
221 private HttpHeaders(Map<String,List<String>> headers) { |
|
222 this.headers = headers; |
|
223 } |
|
224 |
|
225 private static final int entryHash(Map.Entry<String, List<String>> e) { |
|
226 String key = e.getKey(); |
|
227 List<String> value = e.getValue(); |
|
228 // we know that by construction key and values can't be null |
|
229 int keyHash = key.toLowerCase(Locale.ROOT).hashCode(); |
|
230 int valueHash = value.hashCode(); |
|
231 return keyHash ^ valueHash; |
|
232 } |
|
233 |
|
234 // Returns a new HTTP headers after performing a structural copy and filtering. |
|
235 private static HttpHeaders headersOf(Map<String,List<String>> map, |
|
236 BiPredicate<String,String> filter) { |
|
237 TreeMap<String,List<String>> other = new TreeMap<>(CASE_INSENSITIVE_ORDER); |
|
238 TreeSet<String> notAdded = new TreeSet<>(CASE_INSENSITIVE_ORDER); |
|
239 ArrayList<String> tempList = new ArrayList<>(); |
|
240 map.forEach((key, value) -> { |
|
241 String headerName = requireNonNull(key).trim(); |
|
242 if (headerName.isEmpty()) { |
|
243 throw new IllegalArgumentException("empty key"); |
|
244 } |
|
245 List<String> headerValues = requireNonNull(value); |
|
246 headerValues.forEach(headerValue -> { |
|
247 headerValue = requireNonNull(headerValue).trim(); |
|
248 if (filter.test(headerName, headerValue)) { |
|
249 tempList.add(headerValue); |
|
250 } |
|
251 }); |
|
252 |
|
253 if (tempList.isEmpty()) { |
|
254 if (other.containsKey(headerName) |
|
255 || notAdded.contains(headerName.toLowerCase(Locale.ROOT))) |
|
256 throw new IllegalArgumentException("duplicate key: " + headerName); |
|
257 notAdded.add(headerName.toLowerCase(Locale.ROOT)); |
|
258 } else if (other.put(headerName, List.copyOf(tempList)) != null) { |
|
259 throw new IllegalArgumentException("duplicate key: " + headerName); |
|
260 } |
|
261 tempList.clear(); |
|
262 }); |
|
263 return other.isEmpty() ? NO_HEADERS : new HttpHeaders(unmodifiableMap(other)); |
|
264 } |
168 } |
265 } |