Home | History | Annotate | Download | only in net
      1 /*
      2  * Copyright (C) 2010 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.tradefed.util.net;
     18 
     19 import com.android.tradefed.log.LogUtil.CLog;
     20 import com.android.tradefed.util.IRunUtil;
     21 import com.android.tradefed.util.IRunUtil.IRunnableResult;
     22 import com.android.tradefed.util.MultiMap;
     23 import com.android.tradefed.util.RunUtil;
     24 import com.android.tradefed.util.StreamUtil;
     25 import com.android.tradefed.util.VersionParser;
     26 
     27 import java.io.IOException;
     28 import java.io.InputStream;
     29 import java.io.OutputStream;
     30 import java.io.OutputStreamWriter;
     31 import java.io.UnsupportedEncodingException;
     32 import java.net.HttpURLConnection;
     33 import java.net.URL;
     34 import java.net.URLEncoder;
     35 
     36 /**
     37  * Contains helper methods for making http requests
     38  */
     39 public class HttpHelper implements IHttpHelper {
     40     // Note: max int timeout, expressed in millis, is 24 days
     41     /** Time before timing out a request in ms. */
     42     private int mQueryTimeout = 1 * 60 * 1000;
     43     /** Initial poll interval in ms. */
     44     private int mInitialPollInterval = 1 * 1000;
     45     /** Max poll interval in ms. */
     46     private int mMaxPollInterval = 10 * 60 * 1000;
     47     /** Max time for retrying request in ms. */
     48     private int mMaxTime = 10 * 60 * 1000;
     49     /** Max number of redirects to follow */
     50     private int mMaxRedirects = 5;
     51 
     52     private final static String USER_AGENT = "TradeFederation";
     53 
     54     /**
     55      * {@inheritDoc}
     56      */
     57     @Override
     58     public String buildUrl(String baseUrl, MultiMap<String, String> paramMap) {
     59         StringBuilder urlBuilder = new StringBuilder(baseUrl);
     60         if (paramMap != null && !paramMap.isEmpty()) {
     61             urlBuilder.append("?");
     62             urlBuilder.append(buildParameters(paramMap));
     63         }
     64         return urlBuilder.toString();
     65     }
     66 
     67     /**
     68      * {@inheritDoc}
     69      */
     70     @Override
     71     public String buildParameters(MultiMap<String, String> paramMap) {
     72         StringBuilder urlBuilder = new StringBuilder("");
     73         boolean first = true;
     74         for (String key : paramMap.keySet()) {
     75             for (String value : paramMap.get(key)) {
     76                 if (!first) {
     77                     urlBuilder.append("&");
     78                 } else {
     79                     first = false;
     80                 }
     81                 try {
     82                     urlBuilder.append(URLEncoder.encode(key, "UTF-8"));
     83                     urlBuilder.append("=");
     84                     urlBuilder.append(URLEncoder.encode(value, "UTF-8"));
     85                 } catch (UnsupportedEncodingException e) {
     86                     throw new IllegalArgumentException(e);
     87                 }
     88             }
     89         }
     90 
     91         return urlBuilder.toString();
     92     }
     93 
     94     /**
     95      * {@inheritDoc}
     96      */
     97     @SuppressWarnings("resource")
     98     @Override
     99     public String doGet(String url) throws IOException, DataSizeException {
    100         CLog.d("Performing GET request for %s", url);
    101         InputStream remote = null;
    102         byte[] bufResult = new byte[MAX_DATA_SIZE];
    103         int currBufPos = 0;
    104 
    105         try {
    106             remote = getRemoteUrlStream(new URL(url));
    107             int bytesRead;
    108             // read data from stream into temporary buffer
    109             while ((bytesRead = remote.read(bufResult, currBufPos,
    110                     bufResult.length - currBufPos)) != -1) {
    111                 currBufPos += bytesRead;
    112                 if (currBufPos >= bufResult.length) {
    113                     // Eclipse compiler incorrectly flags this statement as not 'remote
    114                     // is not closed at this location'.
    115                     // So add @SuppressWarnings('resource') to shut it up.
    116                     throw new DataSizeException();
    117                 }
    118             }
    119 
    120             return new String(bufResult, 0, currBufPos);
    121         } finally {
    122             StreamUtil.close(remote);
    123         }
    124     }
    125 
    126     /**
    127      * {@inheritDoc}
    128      */
    129     @Override
    130     public void doGet(String url, OutputStream outputStream) throws IOException {
    131         CLog.d("Performing GET download request for %s", url);
    132         InputStream remote = null;
    133         try {
    134             remote = getRemoteUrlStream(new URL(url));
    135             StreamUtil.copyStreams(remote, outputStream);
    136         } finally {
    137             StreamUtil.close(remote);
    138         }
    139     }
    140 
    141     /**
    142      * {@inheritDoc}
    143      */
    144     @Override
    145     public void doGetIgnore(String url) throws IOException {
    146         CLog.d("Performing GET request for %s. Ignoring result.", url);
    147         InputStream remote = null;
    148         try {
    149             remote = getRemoteUrlStream(new URL(url));
    150         } finally {
    151             StreamUtil.close(remote);
    152         }
    153     }
    154 
    155     /**
    156      * {@inheritDoc}
    157      */
    158     @Override
    159     public HttpURLConnection createConnection(URL url, String method, String contentType)
    160             throws IOException {
    161         HttpURLConnection connection = (HttpURLConnection) url.openConnection();
    162         connection.setRequestMethod(method);
    163         if (contentType != null) {
    164             connection.setRequestProperty("Content-Type", contentType);
    165         }
    166         connection.setDoInput(true);
    167         connection.setDoOutput(true);
    168         connection.setConnectTimeout(getOpTimeout());  // timeout for establishing the connection
    169         connection.setReadTimeout(getOpTimeout());  // timeout for receiving a read() response
    170         connection.setRequestProperty("User-Agent",
    171                 String.format("%s/%s", USER_AGENT, VersionParser.fetchVersion()));
    172         return connection;
    173     }
    174 
    175     /**
    176      * {@inheritDoc}
    177      */
    178     @Override
    179     public HttpURLConnection createXmlConnection(URL url, String method) throws IOException {
    180         return createConnection(url, method, "text/xml");
    181     }
    182 
    183     /**
    184      * {@inheritDoc}
    185      */
    186     @Override
    187     public HttpURLConnection createJsonConnection(URL url, String method) throws IOException {
    188         return createConnection(url, method, "application/json");
    189     }
    190 
    191     /**
    192      * {@inheritDoc}
    193      */
    194     @Override
    195     public String doGetWithRetry(String url) throws IOException, DataSizeException {
    196         GetRequestRunnable runnable = new GetRequestRunnable(url, false);
    197         if (getRunUtil().runEscalatingTimedRetry(getOpTimeout(), getInitialPollInterval(),
    198                 getMaxPollInterval(), getMaxTime(), runnable)) {
    199             return runnable.getResponse();
    200         } else if (runnable.getException() instanceof IOException) {
    201             throw (IOException) runnable.getException();
    202         } else if (runnable.getException() instanceof DataSizeException) {
    203             throw (DataSizeException) runnable.getException();
    204         } else if (runnable.getException() instanceof RuntimeException) {
    205             throw (RuntimeException) runnable.getException();
    206         } else {
    207             throw new IOException("GET request could not be completed");
    208         }
    209     }
    210 
    211     /**
    212      * {@inheritDoc}
    213      */
    214     @Override
    215     public void doGetIgnoreWithRetry(String url) throws IOException {
    216         GetRequestRunnable runnable = new GetRequestRunnable(url, true);
    217         if (getRunUtil().runEscalatingTimedRetry(getOpTimeout(), getInitialPollInterval(),
    218                 getMaxPollInterval(), getMaxTime(), runnable)) {
    219             return;
    220         } else if (runnable.getException() instanceof IOException) {
    221             throw (IOException) runnable.getException();
    222         } else if (runnable.getException() instanceof RuntimeException) {
    223             throw (RuntimeException) runnable.getException();
    224         } else {
    225             throw new IOException("GET request could not be completed");
    226         }
    227     }
    228 
    229     /**
    230      * {@inheritDoc}
    231      */
    232     @Override
    233     public String doPostWithRetry(String url, String postData, String contentType)
    234             throws IOException, DataSizeException {
    235         PostRequestRunnable runnable = new PostRequestRunnable(url, postData, contentType);
    236         if (getRunUtil().runEscalatingTimedRetry(getOpTimeout(), getInitialPollInterval(),
    237                 getMaxPollInterval(), getMaxTime(), runnable)) {
    238             return runnable.getResponse();
    239         } else if (runnable.getException() instanceof IOException) {
    240             throw (IOException) runnable.getException();
    241         } else if (runnable.getException() instanceof DataSizeException) {
    242             throw (DataSizeException) runnable.getException();
    243         } else if (runnable.getException() instanceof RuntimeException) {
    244             throw (RuntimeException) runnable.getException();
    245         } else {
    246             throw new IOException("POST request could not be completed");
    247         }
    248     }
    249 
    250     /**
    251      * {@inheritDoc}
    252      */
    253     @Override
    254     public String doPostWithRetry(String url, String postData) throws IOException,
    255             DataSizeException {
    256         return doPostWithRetry(url, postData, null);
    257     }
    258 
    259     /**
    260      * Runnable for making requests with
    261      * {@link IRunUtil#runEscalatingTimedRetry(long, long, long, long, IRunnableResult)}.
    262      */
    263     public abstract class RequestRunnable implements IRunnableResult {
    264         private String mResponse = null;
    265         private Exception mException = null;
    266         private final String mUrl;
    267 
    268         public RequestRunnable(String url) {
    269             mUrl = url;
    270         }
    271 
    272         public String getUrl() {
    273             return mUrl;
    274         }
    275 
    276         public String getResponse() {
    277             return mResponse;
    278         }
    279 
    280         protected void setResponse(String response) {
    281             mResponse = response;
    282         }
    283 
    284         /**
    285          * Returns the last {@link Exception} that occurred when performing run().
    286          */
    287         public Exception getException() {
    288             return mException;
    289         }
    290 
    291         protected void setException(Exception e) {
    292             mException = e;
    293         }
    294 
    295         @Override
    296         public void cancel() {
    297             // ignore
    298         }
    299     }
    300 
    301     /**
    302      * Runnable for making GET requests with
    303      * {@link IRunUtil#runEscalatingTimedRetry(long, long, long, long, IRunnableResult)}.
    304      */
    305     private class GetRequestRunnable extends RequestRunnable {
    306         private boolean mIgnoreResult;
    307 
    308         public GetRequestRunnable(String url, boolean ignoreResult) {
    309             super(url);
    310             mIgnoreResult = ignoreResult;
    311         }
    312 
    313         /**
    314          * Perform a single GET request, storing the response or the associated exception in case of
    315          * error.
    316          */
    317         @Override
    318         public boolean run() {
    319             try {
    320                 if (mIgnoreResult) {
    321                     doGetIgnore(getUrl());
    322                 } else {
    323                     setResponse(doGet(getUrl()));
    324                 }
    325                 return true;
    326             } catch (IOException e) {
    327                 CLog.i("IOException %s from %s", e.getMessage(), getUrl());
    328                 setException(e);
    329             } catch (DataSizeException e) {
    330                 CLog.i("Unexpected oversized response from %s", getUrl());
    331                 setException(e);
    332             } catch (RuntimeException e) {
    333                 CLog.i("RuntimeException %s", e.getMessage());
    334                 setException(e);
    335             }
    336 
    337             return false;
    338         }
    339     }
    340 
    341     /**
    342      * Runnable for making POST requests with
    343      * {@link IRunUtil#runEscalatingTimedRetry(long, long, long, long, IRunnableResult)}.
    344      */
    345     private class PostRequestRunnable extends RequestRunnable {
    346         String mPostData;
    347         String mContentType;
    348         public PostRequestRunnable(String url, String postData, String contentType) {
    349             super(url);
    350             mPostData = postData;
    351             mContentType = contentType;
    352         }
    353 
    354         /**
    355          * Perform a single POST request, storing the response or the associated exception in case
    356          * of error.
    357          */
    358         @SuppressWarnings("resource")
    359         @Override
    360         public boolean run() {
    361             InputStream inputStream = null;
    362             OutputStream outputStream = null;
    363             OutputStreamWriter outputStreamWriter = null;
    364             try {
    365                 HttpURLConnection conn = createConnection(new URL(getUrl()), "POST", mContentType);
    366 
    367                 outputStream = getConnectionOutputStream(conn);
    368                 outputStreamWriter = new OutputStreamWriter(outputStream);
    369                 outputStreamWriter.write(mPostData);
    370                 outputStreamWriter.flush();
    371 
    372                 inputStream = getConnectionInputStream(conn);
    373                 byte[] bufResult = new byte[MAX_DATA_SIZE];
    374                 int currBufPos = 0;
    375                 int bytesRead;
    376                 // read data from stream into temporary buffer
    377                 while ((bytesRead = inputStream.read(bufResult, currBufPos,
    378                         bufResult.length - currBufPos)) != -1) {
    379                     currBufPos += bytesRead;
    380                     if (currBufPos >= bufResult.length) {
    381                         // Eclipse compiler incorrectly flags this statement as not 'stream
    382                         // is not closed at this location'.
    383                         // So add @SuppressWarnings('resource') to shut it up.
    384                         throw new DataSizeException();
    385                     }
    386                 }
    387                 setResponse(new String(bufResult, 0, currBufPos));
    388                 return true;
    389             } catch (IOException e) {
    390                 CLog.i("IOException %s from %s", e.getMessage(), getUrl());
    391                 setException(e);
    392             } catch (DataSizeException e) {
    393                 CLog.i("Unexpected oversized response from %s", getUrl());
    394                 setException(e);
    395             } catch (RuntimeException e) {
    396                 CLog.i("RuntimeException %s", e.getMessage());
    397                 setException(e);
    398             } finally {
    399                 StreamUtil.close(outputStream);
    400                 StreamUtil.close(inputStream);
    401                 StreamUtil.close(outputStreamWriter);
    402             }
    403 
    404             return false;
    405         }
    406     }
    407 
    408     /**
    409      * Factory method for opening an input stream to a remote url. Exposed for unit testing.
    410      *
    411      * @param url the {@link URL}
    412      * @return the {@link InputStream}
    413      * @throws IOException if stream could not be opened.
    414      */
    415     InputStream getRemoteUrlStream(URL url) throws IOException {
    416         // Redirects are handle by HttpURLConnection, except when the protocol changes.
    417         // e.g.: http to https, and vice versa.
    418         boolean redirect;
    419         int redirectCount = 0;
    420         HttpURLConnection conn = createConnection(url, "GET", null);
    421         do {
    422             redirect = false;
    423             int status = conn.getResponseCode();
    424             if (status != HttpURLConnection.HTTP_OK) {
    425                 if (status == HttpURLConnection.HTTP_MOVED_PERM
    426                         || status == HttpURLConnection.HTTP_MOVED_TEMP
    427                         || status == HttpURLConnection.HTTP_SEE_OTHER) {
    428                     redirect = true;
    429                 }
    430             }
    431             if (redirect) {
    432                 String location = conn.getHeaderField("Location");
    433                 URL newURL = new URL(location);
    434                 CLog.d("Redirect occured during GET, new url %s", location);
    435                 conn = createConnection(newURL, "GET", null);
    436             }
    437         } while(redirect && redirectCount < mMaxRedirects);
    438         return conn.getInputStream();
    439     }
    440 
    441     /**
    442      * Factory method for getting connection input stream. Exposed for unit testing.
    443      */
    444     InputStream getConnectionInputStream(HttpURLConnection conn) throws IOException {
    445         return conn.getInputStream();
    446     }
    447 
    448     /**
    449      * Factory method for getting connection output stream. Exposed for unit testing.
    450      */
    451     OutputStream getConnectionOutputStream(HttpURLConnection conn) throws IOException {
    452         return conn.getOutputStream();
    453     }
    454 
    455     /**
    456      * {@inheritDoc}
    457      */
    458     @Override
    459     public int getOpTimeout() {
    460         return mQueryTimeout;
    461     }
    462 
    463     /**
    464      * {@inheritDoc}
    465      */
    466     @Override
    467     public void setOpTimeout(int time) {
    468         mQueryTimeout = time;
    469     }
    470 
    471     /**
    472      * {@inheritDoc}
    473      */
    474     @Override
    475     public int getInitialPollInterval() {
    476         return mInitialPollInterval;
    477     }
    478 
    479     /**
    480      * {@inheritDoc}
    481      */
    482     @Override
    483     public void setInitialPollInterval(int time) {
    484         mInitialPollInterval = time;
    485     }
    486 
    487     /**
    488      * {@inheritDoc}
    489      */
    490     @Override
    491     public int getMaxPollInterval() {
    492         return mMaxPollInterval;
    493     }
    494 
    495     /**
    496      * {@inheritDoc}
    497      */
    498     @Override
    499     public void setMaxPollInterval(int time) {
    500         mMaxPollInterval = time;
    501     }
    502 
    503     /**
    504      * {@inheritDoc}
    505      */
    506     @Override
    507     public int getMaxTime() {
    508         return mMaxTime;
    509     }
    510 
    511     /**
    512      * {@inheritDoc}
    513      */
    514     @Override
    515     public void setMaxTime(int time) {
    516         mMaxTime = time;
    517     }
    518 
    519     /**
    520      * Get {@link IRunUtil} to use. Exposed so unit tests can mock.
    521      */
    522     public IRunUtil getRunUtil() {
    523         return RunUtil.getDefault();
    524     }
    525 }
    526