• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
buildUrl(String baseUrl, MultiMap<String, String> paramMap)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
buildParameters(MultiMap<String, String> paramMap)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
doGet(String url)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
doGet(String url, OutputStream outputStream)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
doGetIgnore(String url)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
createConnection(URL url, String method, String contentType)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
createXmlConnection(URL url, String method)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
createJsonConnection(URL url, String method)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
doGetWithRetry(String url)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
doGetIgnoreWithRetry(String url)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
doPostWithRetry(String url, String postData, String contentType)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
doPostWithRetry(String url, String postData)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 
RequestRunnable(String url)268         public RequestRunnable(String url) {
269             mUrl = url;
270         }
271 
getUrl()272         public String getUrl() {
273             return mUrl;
274         }
275 
getResponse()276         public String getResponse() {
277             return mResponse;
278         }
279 
setResponse(String response)280         protected void setResponse(String response) {
281             mResponse = response;
282         }
283 
284         /**
285          * Returns the last {@link Exception} that occurred when performing run().
286          */
getException()287         public Exception getException() {
288             return mException;
289         }
290 
setException(Exception e)291         protected void setException(Exception e) {
292             mException = e;
293         }
294 
295         @Override
cancel()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 
GetRequestRunnable(String url, boolean ignoreResult)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
run()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;
PostRequestRunnable(String url, String postData, String contentType)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
run()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      */
getRemoteUrlStream(URL url)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      */
getConnectionInputStream(HttpURLConnection conn)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      */
getConnectionOutputStream(HttpURLConnection conn)451     OutputStream getConnectionOutputStream(HttpURLConnection conn) throws IOException {
452         return conn.getOutputStream();
453     }
454 
455     /**
456      * {@inheritDoc}
457      */
458     @Override
getOpTimeout()459     public int getOpTimeout() {
460         return mQueryTimeout;
461     }
462 
463     /**
464      * {@inheritDoc}
465      */
466     @Override
setOpTimeout(int time)467     public void setOpTimeout(int time) {
468         mQueryTimeout = time;
469     }
470 
471     /**
472      * {@inheritDoc}
473      */
474     @Override
getInitialPollInterval()475     public int getInitialPollInterval() {
476         return mInitialPollInterval;
477     }
478 
479     /**
480      * {@inheritDoc}
481      */
482     @Override
setInitialPollInterval(int time)483     public void setInitialPollInterval(int time) {
484         mInitialPollInterval = time;
485     }
486 
487     /**
488      * {@inheritDoc}
489      */
490     @Override
getMaxPollInterval()491     public int getMaxPollInterval() {
492         return mMaxPollInterval;
493     }
494 
495     /**
496      * {@inheritDoc}
497      */
498     @Override
setMaxPollInterval(int time)499     public void setMaxPollInterval(int time) {
500         mMaxPollInterval = time;
501     }
502 
503     /**
504      * {@inheritDoc}
505      */
506     @Override
getMaxTime()507     public int getMaxTime() {
508         return mMaxTime;
509     }
510 
511     /**
512      * {@inheritDoc}
513      */
514     @Override
setMaxTime(int time)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      */
getRunUtil()522     public IRunUtil getRunUtil() {
523         return RunUtil.getDefault();
524     }
525 }
526