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.NetworkResponse;
     21 
     22 import org.apache.http.impl.cookie.DateParseException;
     23 import org.apache.http.impl.cookie.DateUtils;
     24 import org.apache.http.protocol.HTTP;
     25 
     26 import java.util.Map;
     27 
     28 /**
     29  * Utility methods for parsing HTTP headers.
     30  */
     31 public class HttpHeaderParser {
     32 
     33     /**
     34      * Extracts a {@link Cache.Entry} from a {@link NetworkResponse}.
     35      *
     36      * @param response The network response to parse headers from
     37      * @return a cache entry for the given response, or null if the response is not cacheable.
     38      */
     39     public static Cache.Entry parseCacheHeaders(NetworkResponse response) {
     40         long now = System.currentTimeMillis();
     41 
     42         Map<String, String> headers = response.headers;
     43 
     44         long serverDate = 0;
     45         long lastModified = 0;
     46         long serverExpires = 0;
     47         long softExpire = 0;
     48         long finalExpire = 0;
     49         long maxAge = 0;
     50         long staleWhileRevalidate = 0;
     51         boolean hasCacheControl = false;
     52         boolean mustRevalidate = false;
     53 
     54         String serverEtag = null;
     55         String headerValue;
     56 
     57         headerValue = headers.get("Date");
     58         if (headerValue != null) {
     59             serverDate = parseDateAsEpoch(headerValue);
     60         }
     61 
     62         headerValue = headers.get("Cache-Control");
     63         if (headerValue != null) {
     64             hasCacheControl = true;
     65             String[] tokens = headerValue.split(",");
     66             for (int i = 0; i < tokens.length; i++) {
     67                 String token = tokens[i].trim();
     68                 if (token.equals("no-cache") || token.equals("no-store")) {
     69                     return null;
     70                 } else if (token.startsWith("max-age=")) {
     71                     try {
     72                         maxAge = Long.parseLong(token.substring(8));
     73                     } catch (Exception e) {
     74                     }
     75                 } else if (token.startsWith("stale-while-revalidate=")) {
     76                     try {
     77                         staleWhileRevalidate = Long.parseLong(token.substring(23));
     78                     } catch (Exception e) {
     79                     }
     80                 } else if (token.equals("must-revalidate") || token.equals("proxy-revalidate")) {
     81                     mustRevalidate = true;
     82                 }
     83             }
     84         }
     85 
     86         headerValue = headers.get("Expires");
     87         if (headerValue != null) {
     88             serverExpires = parseDateAsEpoch(headerValue);
     89         }
     90 
     91         headerValue = headers.get("Last-Modified");
     92         if (headerValue != null) {
     93             lastModified = parseDateAsEpoch(headerValue);
     94         }
     95 
     96         serverEtag = headers.get("ETag");
     97 
     98         // Cache-Control takes precedence over an Expires header, even if both exist and Expires
     99         // is more restrictive.
    100         if (hasCacheControl) {
    101             softExpire = now + maxAge * 1000;
    102             finalExpire = mustRevalidate
    103                     ? softExpire
    104                     : softExpire + staleWhileRevalidate * 1000;
    105         } else if (serverDate > 0 && serverExpires >= serverDate) {
    106             // Default semantic for Expire header in HTTP specification is softExpire.
    107             softExpire = now + (serverExpires - serverDate);
    108             finalExpire = softExpire;
    109         }
    110 
    111         Cache.Entry entry = new Cache.Entry();
    112         entry.data = response.data;
    113         entry.etag = serverEtag;
    114         entry.softTtl = softExpire;
    115         entry.ttl = finalExpire;
    116         entry.serverDate = serverDate;
    117         entry.lastModified = lastModified;
    118         entry.responseHeaders = headers;
    119 
    120         return entry;
    121     }
    122 
    123     /**
    124      * Parse date in RFC1123 format, and return its value as epoch
    125      */
    126     public static long parseDateAsEpoch(String dateStr) {
    127         try {
    128             // Parse date in RFC1123 format if this header contains one
    129             return DateUtils.parseDate(dateStr).getTime();
    130         } catch (DateParseException e) {
    131             // Date in invalid format, fallback to 0
    132             return 0;
    133         }
    134     }
    135 
    136     /**
    137      * Retrieve a charset from headers
    138      *
    139      * @param headers An {@link java.util.Map} of headers
    140      * @param defaultCharset Charset to return if none can be found
    141      * @return Returns the charset specified in the Content-Type of this header,
    142      * or the defaultCharset if none can be found.
    143      */
    144     public static String parseCharset(Map<String, String> headers, String defaultCharset) {
    145         String contentType = headers.get(HTTP.CONTENT_TYPE);
    146         if (contentType != null) {
    147             String[] params = contentType.split(";");
    148             for (int i = 1; i < params.length; i++) {
    149                 String[] pair = params[i].trim().split("=");
    150                 if (pair.length == 2) {
    151                     if (pair[0].equals("charset")) {
    152                         return pair[1];
    153                     }
    154                 }
    155             }
    156         }
    157 
    158         return defaultCharset;
    159     }
    160 
    161     /**
    162      * Returns the charset specified in the Content-Type of this header,
    163      * or the HTTP default (ISO-8859-1) if none can be found.
    164      */
    165     public static String parseCharset(Map<String, String> headers) {
    166         return parseCharset(headers, HTTP.DEFAULT_CONTENT_CHARSET);
    167     }
    168 }
    169