• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  *  Licensed to the Apache Software Foundation (ASF) under one or more
3  *  contributor license agreements.  See the NOTICE file distributed with
4  *  this work for additional information regarding copyright ownership.
5  *  The ASF licenses this file to You under the Apache License, Version 2.0
6  *  (the "License"); you may not use this file except in compliance with
7  *  the License.  You may obtain a copy of the License at
8  *
9  *     http://www.apache.org/licenses/LICENSE-2.0
10  *
11  *  Unless required by applicable law or agreed to in writing, software
12  *  distributed under the License is distributed on an "AS IS" BASIS,
13  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  *  See the License for the specific language governing permissions and
15  *  limitations under the License.
16  */
17 
18 package libcore.net.http;
19 
20 import java.util.ArrayList;
21 import java.util.Collections;
22 import java.util.Comparator;
23 import java.util.List;
24 import java.util.Map;
25 import java.util.Map.Entry;
26 import java.util.Set;
27 import java.util.TreeMap;
28 
29 /**
30  * The HTTP status and unparsed header fields of a single HTTP message. Values
31  * are represented as uninterpreted strings; use {@link RequestHeaders} and
32  * {@link ResponseHeaders} for interpreted headers. This class maintains the
33  * order of the header fields within the HTTP message.
34  *
35  * <p>This class tracks fields line-by-line. A field with multiple comma-
36  * separated values on the same line will be treated as a field with a single
37  * value by this class. It is the caller's responsibility to detect and split
38  * on commas if their field permits multiple values. This simplifies use of
39  * single-valued fields whose values routinely contain commas, such as cookies
40  * or dates.
41  *
42  * <p>This class trims whitespace from values. It never returns values with
43  * leading or trailing whitespace.
44  */
45 public final class RawHeaders {
46     private static final Comparator<String> FIELD_NAME_COMPARATOR = new Comparator<String>() {
47         @FindBugsSuppressWarnings("ES_COMPARING_PARAMETER_STRING_WITH_EQ")
48         @Override public int compare(String a, String b) {
49             if (a == b) {
50                 return 0;
51             } else if (a == null) {
52                 return -1;
53             } else if (b == null) {
54                 return 1;
55             } else {
56                 return String.CASE_INSENSITIVE_ORDER.compare(a, b);
57             }
58         }
59     };
60 
61     private final List<String> namesAndValues = new ArrayList<String>(20);
62     private String statusLine;
63     private int httpMinorVersion = 1;
64     private int responseCode = -1;
65     private String responseMessage;
66 
RawHeaders()67     public RawHeaders() {}
68 
RawHeaders(RawHeaders copyFrom)69     public RawHeaders(RawHeaders copyFrom) {
70         namesAndValues.addAll(copyFrom.namesAndValues);
71         statusLine = copyFrom.statusLine;
72         httpMinorVersion = copyFrom.httpMinorVersion;
73         responseCode = copyFrom.responseCode;
74         responseMessage = copyFrom.responseMessage;
75     }
76 
77     /**
78      * Sets the response status line (like "HTTP/1.0 200 OK") or request line
79      * (like "GET / HTTP/1.1").
80      */
setStatusLine(String statusLine)81     public void setStatusLine(String statusLine) {
82         statusLine = statusLine.trim();
83         this.statusLine = statusLine;
84 
85         if (statusLine == null || !statusLine.startsWith("HTTP/")) {
86             return;
87         }
88         statusLine = statusLine.trim();
89         int mark = statusLine.indexOf(" ") + 1;
90         if (mark == 0) {
91             return;
92         }
93         if (statusLine.charAt(mark - 2) != '1') {
94             this.httpMinorVersion = 0;
95         }
96         int last = mark + 3;
97         if (last > statusLine.length()) {
98             last = statusLine.length();
99         }
100         this.responseCode = Integer.parseInt(statusLine.substring(mark, last));
101         if (last + 1 <= statusLine.length()) {
102             this.responseMessage = statusLine.substring(last + 1);
103         }
104     }
105 
getStatusLine()106     public String getStatusLine() {
107         return statusLine;
108     }
109 
110     /**
111      * Returns the status line's HTTP minor version. This returns 0 for HTTP/1.0
112      * and 1 for HTTP/1.1. This returns 1 if the HTTP version is unknown.
113      */
getHttpMinorVersion()114     public int getHttpMinorVersion() {
115         return httpMinorVersion != -1 ? httpMinorVersion : 1;
116     }
117 
118     /**
119      * Returns the HTTP status code or -1 if it is unknown.
120      */
getResponseCode()121     public int getResponseCode() {
122         return responseCode;
123     }
124 
125     /**
126      * Returns the HTTP status message or null if it is unknown.
127      */
getResponseMessage()128     public String getResponseMessage() {
129         return responseMessage;
130     }
131 
132     /**
133      * Add an HTTP header line containing a field name, a literal colon, and a
134      * value.
135      */
addLine(String line)136     public void addLine(String line) {
137         int index = line.indexOf(":");
138         if (index == -1) {
139             add("", line);
140         } else {
141             add(line.substring(0, index), line.substring(index + 1));
142         }
143     }
144 
145     /**
146      * Add a field with the specified value.
147      */
add(String fieldName, String value)148     public void add(String fieldName, String value) {
149         if (fieldName == null) {
150             throw new IllegalArgumentException("fieldName == null");
151         }
152         if (value == null) {
153             /*
154              * Given null values, the RI sends a malformed field line like
155              * "Accept\r\n". For platform compatibility and HTTP compliance, we
156              * print a warning and ignore null values.
157              */
158             System.logW("Ignoring HTTP header field '" + fieldName + "' because its value is null");
159             return;
160         }
161         namesAndValues.add(fieldName);
162         namesAndValues.add(value.trim());
163     }
164 
removeAll(String fieldName)165     public void removeAll(String fieldName) {
166         for (int i = 0; i < namesAndValues.size(); i += 2) {
167             if (fieldName.equalsIgnoreCase(namesAndValues.get(i))) {
168                 namesAndValues.remove(i); // field name
169                 namesAndValues.remove(i); // value
170             }
171         }
172     }
173 
addAll(String fieldName, List<String> headerFields)174     public void addAll(String fieldName, List<String> headerFields) {
175         for (String value : headerFields) {
176             add(fieldName, value);
177         }
178     }
179 
180     /**
181      * Set a field with the specified value. If the field is not found, it is
182      * added. If the field is found, the existing values are replaced.
183      */
set(String fieldName, String value)184     public void set(String fieldName, String value) {
185         removeAll(fieldName);
186         add(fieldName, value);
187     }
188 
189     /**
190      * Returns the number of field values.
191      */
length()192     public int length() {
193         return namesAndValues.size() / 2;
194     }
195 
196     /**
197      * Returns the field at {@code position} or null if that is out of range.
198      */
getFieldName(int index)199     public String getFieldName(int index) {
200         int fieldNameIndex = index * 2;
201         if (fieldNameIndex < 0 || fieldNameIndex >= namesAndValues.size()) {
202             return null;
203         }
204         return namesAndValues.get(fieldNameIndex);
205     }
206 
207     /**
208      * Returns the value at {@code index} or null if that is out of range.
209      */
getValue(int index)210     public String getValue(int index) {
211         int valueIndex = index * 2 + 1;
212         if (valueIndex < 0 || valueIndex >= namesAndValues.size()) {
213             return null;
214         }
215         return namesAndValues.get(valueIndex);
216     }
217 
218     /**
219      * Returns the last value corresponding to the specified field, or null.
220      */
get(String fieldName)221     public String get(String fieldName) {
222         for (int i = namesAndValues.size() - 2; i >= 0; i -= 2) {
223             if (fieldName.equalsIgnoreCase(namesAndValues.get(i))) {
224                 return namesAndValues.get(i + 1);
225             }
226         }
227         return null;
228     }
229 
230     /**
231      * @param fieldNames a case-insensitive set of HTTP header field names.
232      */
getAll(Set<String> fieldNames)233     public RawHeaders getAll(Set<String> fieldNames) {
234         RawHeaders result = new RawHeaders();
235         for (int i = 0; i < namesAndValues.size(); i += 2) {
236             String fieldName = namesAndValues.get(i);
237             if (fieldNames.contains(fieldName)) {
238                 result.add(fieldName, namesAndValues.get(i + 1));
239             }
240         }
241         return result;
242     }
243 
toHeaderString()244     public String toHeaderString() {
245         StringBuilder result = new StringBuilder(256);
246         result.append(statusLine).append("\r\n");
247         for (int i = 0; i < namesAndValues.size(); i += 2) {
248             result.append(namesAndValues.get(i)).append(": ")
249                     .append(namesAndValues.get(i + 1)).append("\r\n");
250         }
251         result.append("\r\n");
252         return result.toString();
253     }
254 
255     /**
256      * Returns an immutable map containing each field to its list of values. The
257      * status line is mapped to null.
258      */
toMultimap()259     public Map<String, List<String>> toMultimap() {
260         Map<String, List<String>> result = new TreeMap<String, List<String>>(FIELD_NAME_COMPARATOR);
261         for (int i = 0; i < namesAndValues.size(); i += 2) {
262             String fieldName = namesAndValues.get(i);
263             String value = namesAndValues.get(i + 1);
264 
265             List<String> allValues = new ArrayList<String>();
266             List<String> otherValues = result.get(fieldName);
267             if (otherValues != null) {
268                 allValues.addAll(otherValues);
269             }
270             allValues.add(value);
271             result.put(fieldName, Collections.unmodifiableList(allValues));
272         }
273         if (statusLine != null) {
274             result.put(null, Collections.unmodifiableList(Collections.singletonList(statusLine)));
275         }
276         return Collections.unmodifiableMap(result);
277     }
278 
279     /**
280      * Creates a new instance from the given map of fields to values. If
281      * present, the null field's last element will be used to set the status
282      * line.
283      */
fromMultimap(Map<String, List<String>> map)284     public static RawHeaders fromMultimap(Map<String, List<String>> map) {
285         RawHeaders result = new RawHeaders();
286         for (Entry<String, List<String>> entry : map.entrySet()) {
287             String fieldName = entry.getKey();
288             List<String> values = entry.getValue();
289             if (fieldName != null) {
290                 result.addAll(fieldName, values);
291             } else if (!values.isEmpty()) {
292                 result.setStatusLine(values.get(values.size() - 1));
293             }
294         }
295         return result;
296     }
297 }
298