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