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 67 public RawHeaders() {} 68 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 */ 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 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 */ 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 */ 121 public int getResponseCode() { 122 return responseCode; 123 } 124 125 /** 126 * Returns the HTTP status message or null if it is unknown. 127 */ 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 */ 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 */ 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 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 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 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 */ 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 */ 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