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 com.squareup.okhttp; 19 20 import com.squareup.okhttp.internal.http.HttpDate; 21 import java.util.ArrayList; 22 import java.util.Collections; 23 import java.util.Date; 24 import java.util.LinkedHashMap; 25 import java.util.List; 26 import java.util.Map; 27 import java.util.Set; 28 import java.util.TreeSet; 29 30 /** 31 * The header fields of a single HTTP message. Values are uninterpreted strings; 32 * use {@code Request} and {@code Response} for interpreted headers. This class 33 * maintains the order of the header fields within the HTTP message. 34 * 35 * <p>This class tracks header values 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 * <p>Instances of this class are immutable. Use {@link Builder} to create 46 * instances. 47 */ 48 public final class Headers { 49 private final String[] namesAndValues; 50 51 private Headers(Builder builder) { 52 this.namesAndValues = builder.namesAndValues.toArray(new String[builder.namesAndValues.size()]); 53 } 54 55 private Headers(String[] namesAndValues) { 56 this.namesAndValues = namesAndValues; 57 } 58 59 /** Returns the last value corresponding to the specified field, or null. */ 60 public String get(String name) { 61 return get(namesAndValues, name); 62 } 63 64 /** 65 * Returns the last value corresponding to the specified field parsed as an 66 * HTTP date, or null if either the field is absent or cannot be parsed as a 67 * date. 68 */ 69 public Date getDate(String name) { 70 String value = get(name); 71 return value != null ? HttpDate.parse(value) : null; 72 } 73 74 /** Returns the number of field values. */ 75 public int size() { 76 return namesAndValues.length / 2; 77 } 78 79 /** Returns the field at {@code position} or null if that is out of range. */ 80 public String name(int index) { 81 int nameIndex = index * 2; 82 if (nameIndex < 0 || nameIndex >= namesAndValues.length) { 83 return null; 84 } 85 return namesAndValues[nameIndex]; 86 } 87 88 /** Returns the value at {@code index} or null if that is out of range. */ 89 public String value(int index) { 90 int valueIndex = index * 2 + 1; 91 if (valueIndex < 0 || valueIndex >= namesAndValues.length) { 92 return null; 93 } 94 return namesAndValues[valueIndex]; 95 } 96 97 /** Returns an immutable case-insensitive set of header names. */ 98 public Set<String> names() { 99 TreeSet<String> result = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); 100 for (int i = 0, size = size(); i < size; i++) { 101 result.add(name(i)); 102 } 103 return Collections.unmodifiableSet(result); 104 } 105 106 /** Returns an immutable list of the header values for {@code name}. */ 107 public List<String> values(String name) { 108 List<String> result = null; 109 for (int i = 0, size = size(); i < size; i++) { 110 if (name.equalsIgnoreCase(name(i))) { 111 if (result == null) result = new ArrayList<>(2); 112 result.add(value(i)); 113 } 114 } 115 return result != null 116 ? Collections.unmodifiableList(result) 117 : Collections.<String>emptyList(); 118 } 119 120 public Builder newBuilder() { 121 Builder result = new Builder(); 122 Collections.addAll(result.namesAndValues, namesAndValues); 123 return result; 124 } 125 126 @Override public String toString() { 127 StringBuilder result = new StringBuilder(); 128 for (int i = 0, size = size(); i < size; i++) { 129 result.append(name(i)).append(": ").append(value(i)).append("\n"); 130 } 131 return result.toString(); 132 } 133 134 public Map<String, List<String>> toMultimap() { 135 Map<String, List<String>> result = new LinkedHashMap<String, List<String>>(); 136 for (int i = 0, size = size(); i < size; i++) { 137 String name = name(i); 138 List<String> values = result.get(name); 139 if (values == null) { 140 values = new ArrayList<>(2); 141 result.put(name, values); 142 } 143 values.add(value(i)); 144 } 145 return result; 146 } 147 148 private static String get(String[] namesAndValues, String name) { 149 for (int i = namesAndValues.length - 2; i >= 0; i -= 2) { 150 if (name.equalsIgnoreCase(namesAndValues[i])) { 151 return namesAndValues[i + 1]; 152 } 153 } 154 return null; 155 } 156 157 /** 158 * Returns headers for the alternating header names and values. There must be 159 * an even number of arguments, and they must alternate between header names 160 * and values. 161 */ 162 public static Headers of(String... namesAndValues) { 163 if (namesAndValues == null || namesAndValues.length % 2 != 0) { 164 throw new IllegalArgumentException("Expected alternating header names and values"); 165 } 166 167 // Make a defensive copy and clean it up. 168 namesAndValues = namesAndValues.clone(); 169 for (int i = 0; i < namesAndValues.length; i++) { 170 if (namesAndValues[i] == null) throw new IllegalArgumentException("Headers cannot be null"); 171 namesAndValues[i] = namesAndValues[i].trim(); 172 } 173 174 // Check for malformed headers. 175 for (int i = 0; i < namesAndValues.length; i += 2) { 176 String name = namesAndValues[i]; 177 String value = namesAndValues[i + 1]; 178 if (name.length() == 0 || name.indexOf('\0') != -1 || value.indexOf('\0') != -1) { 179 throw new IllegalArgumentException("Unexpected header: " + name + ": " + value); 180 } 181 } 182 183 return new Headers(namesAndValues); 184 } 185 186 /** 187 * Returns headers for the header names and values in the {@link Map}. 188 */ 189 public static Headers of(Map<String, String> headers) { 190 if (headers == null) { 191 throw new IllegalArgumentException("Expected map with header names and values"); 192 } 193 194 // Make a defensive copy and clean it up. 195 String[] namesAndValues = new String[headers.size() * 2]; 196 int i = 0; 197 for (Map.Entry<String, String> header : headers.entrySet()) { 198 if (header.getKey() == null || header.getValue() == null) { 199 throw new IllegalArgumentException("Headers cannot be null"); 200 } 201 String name = header.getKey().trim(); 202 String value = header.getValue().trim(); 203 if (name.length() == 0 || name.indexOf('\0') != -1 || value.indexOf('\0') != -1) { 204 throw new IllegalArgumentException("Unexpected header: " + name + ": " + value); 205 } 206 namesAndValues[i] = name; 207 namesAndValues[i + 1] = value; 208 i += 2; 209 } 210 211 return new Headers(namesAndValues); 212 } 213 214 public static final class Builder { 215 private final List<String> namesAndValues = new ArrayList<>(20); 216 217 /** 218 * Add a header line without any validation. Only appropriate for headers from the remote peer 219 * or cache. 220 */ 221 Builder addLenient(String line) { 222 int index = line.indexOf(":", 1); 223 if (index != -1) { 224 return addLenient(line.substring(0, index), line.substring(index + 1)); 225 } else if (line.startsWith(":")) { 226 // Work around empty header names and header names that start with a 227 // colon (created by old broken SPDY versions of the response cache). 228 return addLenient("", line.substring(1)); // Empty header name. 229 } else { 230 return addLenient("", line); // No header name. 231 } 232 } 233 234 /** Add an header line containing a field name, a literal colon, and a value. */ 235 public Builder add(String line) { 236 int index = line.indexOf(":"); 237 if (index == -1) { 238 throw new IllegalArgumentException("Unexpected header: " + line); 239 } 240 return add(line.substring(0, index).trim(), line.substring(index + 1)); 241 } 242 243 /** Add a field with the specified value. */ 244 public Builder add(String name, String value) { 245 checkNameAndValue(name, value); 246 return addLenient(name, value); 247 } 248 249 /** 250 * Add a field with the specified value without any validation. Only 251 * appropriate for headers from the remote peer or cache. 252 */ 253 Builder addLenient(String name, String value) { 254 namesAndValues.add(name); 255 namesAndValues.add(value.trim()); 256 return this; 257 } 258 259 public Builder removeAll(String name) { 260 for (int i = 0; i < namesAndValues.size(); i += 2) { 261 if (name.equalsIgnoreCase(namesAndValues.get(i))) { 262 namesAndValues.remove(i); // name 263 namesAndValues.remove(i); // value 264 i -= 2; 265 } 266 } 267 return this; 268 } 269 270 /** 271 * Set a field with the specified value. If the field is not found, it is 272 * added. If the field is found, the existing values are replaced. 273 */ 274 public Builder set(String name, String value) { 275 checkNameAndValue(name, value); 276 removeAll(name); 277 addLenient(name, value); 278 return this; 279 } 280 281 private void checkNameAndValue(String name, String value) { 282 if (name == null) throw new IllegalArgumentException("name == null"); 283 if (name.isEmpty()) throw new IllegalArgumentException("name is empty"); 284 for (int i = 0, length = name.length(); i < length; i++) { 285 char c = name.charAt(i); 286 if (c <= '\u001f' || c >= '\u007f') { 287 throw new IllegalArgumentException(String.format( 288 "Unexpected char %#04x at %d in header name: %s", (int) c, i, name)); 289 } 290 } 291 if (value == null) throw new IllegalArgumentException("value == null"); 292 293 // Workaround for applications that set trailing "\r", "\n" or "\r\n" on header values. 294 // http://b/26422335, http://b/26889631 Android used to allow anything except '\0'. 295 int valueLen = value.length(); 296 if (valueLen >= 2 && value.charAt(valueLen - 2) == '\r' 297 && value.charAt(valueLen - 1) == '\n') { 298 value = value.substring(0, value.length() - 2); 299 } else if (valueLen > 0 300 && (value.charAt(valueLen - 1) == '\n' 301 || value.charAt(valueLen - 1) == '\r')) { 302 value = value.substring(0, valueLen - 1); 303 } 304 // End of workaround. 305 306 for (int i = 0, length = value.length(); i < length; i++) { 307 char c = value.charAt(i); 308 // ANDROID-BEGIN 309 // http://b/28867041 - keep things working for apps that rely on Android's (out of spec) 310 // UTF-8 header encoding behavior. 311 // if ((c <= '\u001f' && c != '\u0009' /* htab */) || c >= '\u007f') { 312 if ((c <= '\u001f' && c != '\u0009' /* htab */) || c == '\u007f') { 313 // ANDROID-END 314 throw new IllegalArgumentException(String.format( 315 "Unexpected char %#04x at %d in header value: %s", (int) c, i, value)); 316 } 317 } 318 } 319 320 /** Equivalent to {@code build().get(name)}, but potentially faster. */ 321 public String get(String name) { 322 for (int i = namesAndValues.size() - 2; i >= 0; i -= 2) { 323 if (name.equalsIgnoreCase(namesAndValues.get(i))) { 324 return namesAndValues.get(i + 1); 325 } 326 } 327 return null; 328 } 329 330 public Headers build() { 331 return new Headers(this); 332 } 333 } 334 } 335