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