Home | History | Annotate | Download | only in okhttp
      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