Home | History | Annotate | Download | only in retriever
      1 /*
      2  * Copyright (C) 2015 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.statementservice.retriever;
     18 
     19 import android.util.Log;
     20 
     21 import com.android.volley.Cache;
     22 import com.android.volley.NetworkResponse;
     23 import com.android.volley.toolbox.HttpHeaderParser;
     24 
     25 import java.io.BufferedInputStream;
     26 import java.io.ByteArrayOutputStream;
     27 import java.io.IOException;
     28 import java.io.InputStream;
     29 import java.net.HttpURLConnection;
     30 import java.net.URL;
     31 import java.util.HashMap;
     32 import java.util.List;
     33 import java.util.Locale;
     34 import java.util.Map;
     35 
     36 /**
     37  * Helper class for fetching HTTP or HTTPS URL.
     38  *
     39  * Visible for testing.
     40  *
     41  * @hide
     42  */
     43 public class URLFetcher {
     44     private static final String TAG = URLFetcher.class.getSimpleName();
     45 
     46     private static final long DO_NOT_CACHE_RESULT = 0L;
     47     private static final int INPUT_BUFFER_SIZE_IN_BYTES = 1024;
     48 
     49     /**
     50      * Fetches the specified url and returns the content and ttl.
     51      *
     52      * <p>
     53      * Retry {@code retry} times if the connection failed or timed out for any reason.
     54      * HTTP error code (e.g. 404/500) won't be retried.
     55      *
     56      * @throws IOException if it can't retrieve the content due to a network problem.
     57      * @throws AssociationServiceException if the URL scheme is not http or https or the content
     58      * length exceeds {code fileSizeLimit}.
     59      */
     60     public WebContent getWebContentFromUrlWithRetry(URL url, long fileSizeLimit,
     61             int connectionTimeoutMillis, int backoffMillis, int retry)
     62                     throws AssociationServiceException, IOException, InterruptedException {
     63         if (retry <= 0) {
     64             throw new IllegalArgumentException("retry should be a postive inetger.");
     65         }
     66         while (retry > 0) {
     67             try {
     68                 return getWebContentFromUrl(url, fileSizeLimit, connectionTimeoutMillis);
     69             } catch (IOException e) {
     70                 retry--;
     71                 if (retry == 0) {
     72                     throw e;
     73                 }
     74             }
     75 
     76             Thread.sleep(backoffMillis);
     77         }
     78 
     79         // Should never reach here.
     80         return null;
     81     }
     82 
     83     /**
     84      * Fetches the specified url and returns the content and ttl.
     85      *
     86      * @throws IOException if it can't retrieve the content due to a network problem.
     87      * @throws AssociationServiceException if the URL scheme is not http or https or the content
     88      * length exceeds {code fileSizeLimit}.
     89      */
     90     public WebContent getWebContentFromUrl(URL url, long fileSizeLimit, int connectionTimeoutMillis)
     91             throws AssociationServiceException, IOException {
     92         final String scheme = url.getProtocol().toLowerCase(Locale.US);
     93         if (!scheme.equals("http") && !scheme.equals("https")) {
     94             throw new IllegalArgumentException("The url protocol should be on http or https.");
     95         }
     96 
     97         HttpURLConnection connection = null;
     98         try {
     99             connection = (HttpURLConnection) url.openConnection();
    100             connection.setInstanceFollowRedirects(true);
    101             connection.setConnectTimeout(connectionTimeoutMillis);
    102             connection.setReadTimeout(connectionTimeoutMillis);
    103             connection.setUseCaches(true);
    104             connection.setInstanceFollowRedirects(false);
    105             connection.addRequestProperty("Cache-Control", "max-stale=60");
    106 
    107             if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
    108                 Log.e(TAG, "The responses code is not 200 but "  + connection.getResponseCode());
    109                 return new WebContent("", DO_NOT_CACHE_RESULT);
    110             }
    111 
    112             if (connection.getContentLength() > fileSizeLimit) {
    113                 Log.e(TAG, "The content size of the url is larger than "  + fileSizeLimit);
    114                 return new WebContent("", DO_NOT_CACHE_RESULT);
    115             }
    116 
    117             Long expireTimeMillis = getExpirationTimeMillisFromHTTPHeader(
    118                     connection.getHeaderFields());
    119 
    120             return new WebContent(inputStreamToString(
    121                     connection.getInputStream(), connection.getContentLength(), fileSizeLimit),
    122                 expireTimeMillis);
    123         } finally {
    124             if (connection != null) {
    125                 connection.disconnect();
    126             }
    127         }
    128     }
    129 
    130     /**
    131      * Visible for testing.
    132      * @hide
    133      */
    134     public static String inputStreamToString(InputStream inputStream, int length, long sizeLimit)
    135             throws IOException, AssociationServiceException {
    136         if (length < 0) {
    137             length = 0;
    138         }
    139         ByteArrayOutputStream baos = new ByteArrayOutputStream(length);
    140         BufferedInputStream bis = new BufferedInputStream(inputStream);
    141         byte[] buffer = new byte[INPUT_BUFFER_SIZE_IN_BYTES];
    142         int len = 0;
    143         while ((len = bis.read(buffer)) != -1) {
    144             baos.write(buffer, 0, len);
    145             if (baos.size() > sizeLimit) {
    146                 throw new AssociationServiceException("The content size of the url is larger than "
    147                         + sizeLimit);
    148             }
    149         }
    150         return baos.toString("UTF-8");
    151     }
    152 
    153     /**
    154      * Parses the HTTP headers to compute the ttl.
    155      *
    156      * @param headers a map that map the header key to the header values. Can be null.
    157      * @return the ttl in millisecond or null if the ttl is not specified in the header.
    158      */
    159     private Long getExpirationTimeMillisFromHTTPHeader(Map<String, List<String>> headers) {
    160         if (headers == null) {
    161             return null;
    162         }
    163         Map<String, String> joinedHeaders = joinHttpHeaders(headers);
    164 
    165         NetworkResponse response = new NetworkResponse(null, joinedHeaders);
    166         Cache.Entry cachePolicy = HttpHeaderParser.parseCacheHeaders(response);
    167 
    168         if (cachePolicy == null) {
    169             // Cache is disabled, set the expire time to 0.
    170             return DO_NOT_CACHE_RESULT;
    171         } else if (cachePolicy.ttl == 0) {
    172             // Cache policy is not specified, set the expire time to 0.
    173             return DO_NOT_CACHE_RESULT;
    174         } else {
    175             // cachePolicy.ttl is actually the expire timestamp in millisecond.
    176             return cachePolicy.ttl;
    177         }
    178     }
    179 
    180     /**
    181      * Converts an HTTP header map of the format provided by {@linkHttpUrlConnection} to a map of
    182      * the format accepted by {@link HttpHeaderParser}. It does this by joining all the entries for
    183      * a given header key with ", ".
    184      */
    185     private Map<String, String> joinHttpHeaders(Map<String, List<String>> headers) {
    186         Map<String, String> joinedHeaders = new HashMap<String, String>();
    187         for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
    188             List<String> values = entry.getValue();
    189             if (values.size() == 1) {
    190                 joinedHeaders.put(entry.getKey(), values.get(0));
    191             } else {
    192                 joinedHeaders.put(entry.getKey(), Utils.joinStrings(", ", values));
    193             }
    194         }
    195         return joinedHeaders;
    196     }
    197 }
    198