Home | History | Annotate | Download | only in toolbox
      1 /*
      2  * Copyright (C) 2011 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.volley.toolbox;
     18 
     19 import com.android.volley.Cache;
     20 import com.android.volley.Header;
     21 import com.android.volley.NetworkResponse;
     22 import com.android.volley.VolleyLog;
     23 
     24 import java.text.ParseException;
     25 import java.text.SimpleDateFormat;
     26 import java.util.ArrayList;
     27 import java.util.Date;
     28 import java.util.List;
     29 import java.util.Locale;
     30 import java.util.Map;
     31 import java.util.TimeZone;
     32 import java.util.TreeMap;
     33 
     34 /**
     35  * Utility methods for parsing HTTP headers.
     36  */
     37 public class HttpHeaderParser {
     38 
     39     static final String HEADER_CONTENT_TYPE = "Content-Type";
     40 
     41     private static final String DEFAULT_CONTENT_CHARSET = "ISO-8859-1";
     42 
     43     private static final String RFC1123_FORMAT = "EEE, dd MMM yyyy HH:mm:ss zzz";
     44 
     45     /**
     46      * Extracts a {@link com.android.volley.Cache.Entry} from a {@link NetworkResponse}.
     47      *
     48      * @param response The network response to parse headers from
     49      * @return a cache entry for the given response, or null if the response is not cacheable.
     50      */
     51     public static Cache.Entry parseCacheHeaders(NetworkResponse response) {
     52         long now = System.currentTimeMillis();
     53 
     54         Map<String, String> headers = response.headers;
     55 
     56         long serverDate = 0;
     57         long lastModified = 0;
     58         long serverExpires = 0;
     59         long softExpire = 0;
     60         long finalExpire = 0;
     61         long maxAge = 0;
     62         long staleWhileRevalidate = 0;
     63         boolean hasCacheControl = false;
     64         boolean mustRevalidate = false;
     65 
     66         String serverEtag = null;
     67         String headerValue;
     68 
     69         headerValue = headers.get("Date");
     70         if (headerValue != null) {
     71             serverDate = parseDateAsEpoch(headerValue);
     72         }
     73 
     74         headerValue = headers.get("Cache-Control");
     75         if (headerValue != null) {
     76             hasCacheControl = true;
     77             String[] tokens = headerValue.split(",");
     78             for (int i = 0; i < tokens.length; i++) {
     79                 String token = tokens[i].trim();
     80                 if (token.equals("no-cache") || token.equals("no-store")) {
     81                     return null;
     82                 } else if (token.startsWith("max-age=")) {
     83                     try {
     84                         maxAge = Long.parseLong(token.substring(8));
     85                     } catch (Exception e) {
     86                     }
     87                 } else if (token.startsWith("stale-while-revalidate=")) {
     88                     try {
     89                         staleWhileRevalidate = Long.parseLong(token.substring(23));
     90                     } catch (Exception e) {
     91                     }
     92                 } else if (token.equals("must-revalidate") || token.equals("proxy-revalidate")) {
     93                     mustRevalidate = true;
     94                 }
     95             }
     96         }
     97 
     98         headerValue = headers.get("Expires");
     99         if (headerValue != null) {
    100             serverExpires = parseDateAsEpoch(headerValue);
    101         }
    102 
    103         headerValue = headers.get("Last-Modified");
    104         if (headerValue != null) {
    105             lastModified = parseDateAsEpoch(headerValue);
    106         }
    107 
    108         serverEtag = headers.get("ETag");
    109 
    110         // Cache-Control takes precedence over an Expires header, even if both exist and Expires
    111         // is more restrictive.
    112         if (hasCacheControl) {
    113             softExpire = now + maxAge * 1000;
    114             finalExpire = mustRevalidate
    115                     ? softExpire
    116                     : softExpire + staleWhileRevalidate * 1000;
    117         } else if (serverDate > 0 && serverExpires >= serverDate) {
    118             // Default semantic for Expire header in HTTP specification is softExpire.
    119             softExpire = now + (serverExpires - serverDate);
    120             finalExpire = softExpire;
    121         }
    122 
    123         Cache.Entry entry = new Cache.Entry();
    124         entry.data = response.data;
    125         entry.etag = serverEtag;
    126         entry.softTtl = softExpire;
    127         entry.ttl = finalExpire;
    128         entry.serverDate = serverDate;
    129         entry.lastModified = lastModified;
    130         entry.responseHeaders = headers;
    131         entry.allResponseHeaders = response.allHeaders;
    132 
    133         return entry;
    134     }
    135 
    136     /**
    137      * Parse date in RFC1123 format, and return its value as epoch
    138      */
    139     public static long parseDateAsEpoch(String dateStr) {
    140         try {
    141             // Parse date in RFC1123 format if this header contains one
    142             return newRfc1123Formatter().parse(dateStr).getTime();
    143         } catch (ParseException e) {
    144             // Date in invalid format, fallback to 0
    145             VolleyLog.e(e, "Unable to parse dateStr: %s, falling back to 0", dateStr);
    146             return 0;
    147         }
    148     }
    149 
    150     /** Format an epoch date in RFC1123 format. */
    151     static String formatEpochAsRfc1123(long epoch) {
    152         return newRfc1123Formatter().format(new Date(epoch));
    153     }
    154 
    155     private static SimpleDateFormat newRfc1123Formatter() {
    156         SimpleDateFormat formatter =
    157                 new SimpleDateFormat(RFC1123_FORMAT, Locale.US);
    158         formatter.setTimeZone(TimeZone.getTimeZone("GMT"));
    159         return formatter;
    160     }
    161 
    162     /**
    163      * Retrieve a charset from headers
    164      *
    165      * @param headers An {@link java.util.Map} of headers
    166      * @param defaultCharset Charset to return if none can be found
    167      * @return Returns the charset specified in the Content-Type of this header,
    168      * or the defaultCharset if none can be found.
    169      */
    170     public static String parseCharset(Map<String, String> headers, String defaultCharset) {
    171         String contentType = headers.get(HEADER_CONTENT_TYPE);
    172         if (contentType != null) {
    173             String[] params = contentType.split(";");
    174             for (int i = 1; i < params.length; i++) {
    175                 String[] pair = params[i].trim().split("=");
    176                 if (pair.length == 2) {
    177                     if (pair[0].equals("charset")) {
    178                         return pair[1];
    179                     }
    180                 }
    181             }
    182         }
    183 
    184         return defaultCharset;
    185     }
    186 
    187     /**
    188      * Returns the charset specified in the Content-Type of this header,
    189      * or the HTTP default (ISO-8859-1) if none can be found.
    190      */
    191     public static String parseCharset(Map<String, String> headers) {
    192         return parseCharset(headers, DEFAULT_CONTENT_CHARSET);
    193     }
    194 
    195     // Note - these are copied from NetworkResponse to avoid making them public (as needed to access
    196     // them from the .toolbox package), which would mean they'd become part of the Volley API.
    197     // TODO: Consider obfuscating official releases so we can share utility methods between Volley
    198     // and Toolbox without making them public APIs.
    199 
    200     static Map<String, String> toHeaderMap(List<Header> allHeaders) {
    201         Map<String, String> headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
    202         // Later elements in the list take precedence.
    203         for (Header header : allHeaders) {
    204             headers.put(header.getName(), header.getValue());
    205         }
    206         return headers;
    207     }
    208 
    209     static List<Header> toAllHeaderList(Map<String, String> headers) {
    210         List<Header> allHeaders = new ArrayList<>(headers.size());
    211         for (Map.Entry<String, String> header : headers.entrySet()) {
    212             allHeaders.add(new Header(header.getKey(), header.getValue()));
    213         }
    214         return allHeaders;
    215     }
    216 }
    217