1 /*
2  * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved.
3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4  *
5  * This code is free software; you can redistribute it and/or modify it
6  * under the terms of the GNU General Public License version 2 only, as
7  * published by the Free Software Foundation.  Oracle designates this
8  * particular file as subject to the "Classpath" exception as provided
9  * by Oracle in the LICENSE file that accompanied this code.
10  *
11  * This code is distributed in the hope that it will be useful, but WITHOUT
12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14  * version 2 for more details (a copy is included in the LICENSE file that
15  * accompanied this code).
16  *
17  * You should have received a copy of the GNU General Public License version
18  * 2 along with this work; if not, write to the Free Software Foundation,
19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20  *
21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22  * or visit www.oracle.com if you need additional information or have any
23  * questions.
24  */
25 
26 package java.net.http;
27 
28 import java.util.ArrayList;
29 import java.util.List;
30 import java.util.Locale;
31 import java.util.Map;
32 import java.util.Optional;
33 import java.util.OptionalLong;
34 import java.util.TreeMap;
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;
39 import static java.util.Objects.requireNonNull;
40 
41 /**
42  * A read-only view of a set of HTTP headers.
43  *
44  * <p> An {@code HttpHeaders} is not typically created directly, but rather
45  * returned from an {@link HttpRequest#headers() HttpRequest} or an
46  * {@link HttpResponse#headers() HttpResponse}. Specific HTTP headers can be
47  * set for a {@linkplain HttpRequest request} through one of the request
48  * builder's {@link HttpRequest.Builder#header(String, String) headers} methods.
49  *
50  * <p> The methods of this class ( that accept a String header name ), and the
51  * {@code Map} returned by the {@link #map() map} method, operate without regard
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.
65  *
66  * <p> {@code HttpHeaders} instances are immutable.
67  *
68  * @since 11
69  */
70 public final class HttpHeaders {
71 
72     /**
73      * Returns an {@link Optional} containing the first header string value of
74      * the given named (and possibly multi-valued) header. If the header is not
75      * present, then the returned {@code Optional} is empty.
76      *
77      * @param name the header name
78      * @return an {@code Optional<String>} containing the first named header
79      *         string value, if present
80      */
firstValue(String name)81     public Optional<String> firstValue(String name) {
82         return allValues(name).stream().findFirst();
83     }
84 
85     /**
86      * Returns an {@link OptionalLong} containing the first header string value
87      * of the named header field. If the header is not present, then the
88      * Optional is empty. If the header is present but contains a value that
89      * does not parse as a {@code Long} value, then an exception is thrown.
90      *
91      * @param name the header name
92      * @return  an {@code OptionalLong}
93      * @throws NumberFormatException if a value is found, but does not parse as
94      *                               a Long
95      */
firstValueAsLong(String name)96     public OptionalLong firstValueAsLong(String name) {
97         return allValues(name).stream().mapToLong(Long::valueOf).findFirst();
98     }
99 
100     /**
101      * Returns an unmodifiable List of all of the header string values of the
102      * given named header. Always returns a List, which may be empty if the
103      * header is not present.
104      *
105      * @param name the header name
106      * @return a List of headers string values
107      */
allValues(String name)108     public List<String> allValues(String name) {
109         requireNonNull(name);
110         List<String> values = map().get(name);
111         // Making unmodifiable list out of empty in order to make a list which
112         // throws UOE unconditionally
113         return values != null ? values : List.of();
114     }
115 
116     /**
117      * Returns an unmodifiable multi Map view of this HttpHeaders.
118      *
119      * @return the Map
120      */
map()121     public Map<String,List<String>> map() {
122         return headers;
123     }
124 
125     /**
126      * Tests this HTTP headers instance for equality with the given object.
127      *
128      * <p> If the given object is not an {@code HttpHeaders} then this
129      * method returns {@code false}. Two HTTP headers are equal if each
130      * of their corresponding {@linkplain #map() maps} are equal.
131      *
132      * <p> This method satisfies the general contract of the {@link
133      * Object#equals(Object) Object.equals} method.
134      *
135      * @param obj the object to which this object is to be compared
136      * @return {@code true} if, and only if, the given object is an {@code
137      *         HttpHeaders} that is equal to this HTTP headers
138      */
equals(Object obj)139     public final boolean equals(Object obj) {
140         if (!(obj instanceof HttpHeaders))
141             return false;
142         HttpHeaders that = (HttpHeaders)obj;
143         return this.map().equals(that.map());
144     }
145 
146     /**
147      * Computes a hash code for this HTTP headers instance.
148      *
149      * <p> The hash code is based upon the components of the HTTP headers
150      * {@link #map() map}, and satisfies the general contract of the
151      * {@link Object#hashCode Object.hashCode} method.
152      *
153      * @return the hash-code value for this HTTP headers
154      */
hashCode()155     public final int hashCode() {
156         int h = 0;
157         for (Map.Entry<String, List<String>> e : map().entrySet()) {
158             h += entryHash(e);
159         }
160         return h;
161     }
162 
163     /**
164      * Returns this HTTP headers as a string.
165      *
166      * @return a string describing the HTTP headers
167      */
168     @Override
toString()169     public String toString() {
170         StringBuilder sb = new StringBuilder();
171         sb.append(super.toString()).append(" { ");
172         sb.append(map());
173         sb.append(" }");
174         return sb.toString();
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      */
of(Map<String,List<String>> headerMap, BiPredicate<String,String> filter)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 
HttpHeaders(Map<String,List<String>> headers)221     private HttpHeaders(Map<String,List<String>> headers) {
222         this.headers = headers;
223     }
224 
entryHash(Map.Entry<String, List<String>> e)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.
headersOf(Map<String,List<String>> map, BiPredicate<String,String> filter)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     }
265 }
266