/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tradefed.util.net;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.util.IRunUtil;
import com.android.tradefed.util.IRunUtil.IRunnableResult;
import com.android.tradefed.util.MultiMap;
import com.android.tradefed.util.RunUtil;
import com.android.tradefed.util.StreamUtil;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
/**
* Contains helper methods for making http requests
*/
public class HttpHelper implements IHttpHelper {
/**
* Time before timing out a request in ms.
*/
private long mQueryTimeout = 1 * 60 * 1000;
/**
* Initial poll interval in ms.
*/
private long mInitialPollInterval = 1 * 1000;
/**
* Max poll interval in ms.
*/
private long mMaxPollInterval = 10 * 60 * 1000;
/**
* Max time for retrying request in ms.
*/
private long mMaxTime = 10 * 60 * 1000;
/**
* {@inheritDoc}
*/
@Override
public String buildUrl(String baseUrl, MultiMap<String, String> paramMap) {
StringBuilder urlBuilder = new StringBuilder(baseUrl);
if (paramMap != null && !paramMap.isEmpty()) {
urlBuilder.append("?");
urlBuilder.append(buildParameters(paramMap));
}
return urlBuilder.toString();
}
/**
* {@inheritDoc}
*/
@Override
public String buildParameters(MultiMap<String, String> paramMap) {
StringBuilder urlBuilder = new StringBuilder("");
boolean first = true;
for (String key : paramMap.keySet()) {
for (String value : paramMap.get(key)) {
if (!first) {
urlBuilder.append("&");
} else {
first = false;
}
try {
urlBuilder.append(URLEncoder.encode(key, "UTF-8"));
urlBuilder.append("=");
urlBuilder.append(URLEncoder.encode(value, "UTF-8"));
} catch (UnsupportedEncodingException e) {
throw new IllegalArgumentException(e);
}
}
}
return urlBuilder.toString();
}
/**
* {@inheritDoc}
*/
@SuppressWarnings("resource")
@Override
public String doGet(String url) throws IOException, DataSizeException {
CLog.d("Performing GET request for %s", url);
InputStream remote = null;
byte[] bufResult = new byte[MAX_DATA_SIZE];
int currBufPos = 0;
try {
remote = getRemoteUrlStream(new URL(url));
int bytesRead;
// read data from stream into temporary buffer
while ((bytesRead = remote.read(bufResult, currBufPos,
bufResult.length - currBufPos)) != -1) {
currBufPos += bytesRead;
if (currBufPos >= bufResult.length) {
// Eclipse compiler incorrectly flags this statement as not 'remote
// is not closed at this location'.
// So add @SuppressWarnings('resource') to shut it up.
throw new DataSizeException();
}
}
return new String(bufResult, 0, currBufPos);
} finally {
StreamUtil.close(remote);
}
}
public void doGet(String url,File destFile) throws IOException {
CLog.d("Performing GET request for %s", url);
BufferedInputStream remote = null;
BufferedOutputStream bof = new BufferedOutputStream(new FileOutputStream(destFile));
byte[] buf = new byte[8192];
int c = 0;
try {
remote = new BufferedInputStream(getRemoteUrlStream(new URL(url)));
while((c = remote.read(buf)) != -1) {
bof.write(buf, 0, c);
}
} finally {
StreamUtil.close(bof);
StreamUtil.close(remote);
}
}
/**
* {@inheritDoc}
*/
@Override
public void doGetIgnore(String url) throws IOException {
CLog.d("Performing GET request for %s. Ignoring result.", url);
InputStream remote = null;
try {
remote = getRemoteUrlStream(new URL(url));
} finally {
StreamUtil.close(remote);
}
}
/**
* {@inheritDoc}
*/
@Override
public HttpURLConnection createConnection(URL url, String method, String contentType)
throws IOException {
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod(method);
if (contentType != null) {
connection.setRequestProperty("Content-Type", contentType);
}
connection.setDoInput(true);
connection.setDoOutput(true);
return connection;
}
/**
* {@inheritDoc}
*/
@Override
public HttpURLConnection createXmlConnection(URL url, String method) throws IOException {
return createConnection(url, method, "text/xml");
}
/**
* {@inheritDoc}
*/
@Override
public HttpURLConnection createJsonConnection(URL url, String method) throws IOException {
return createConnection(url, method, "text/json");
}
/**
* {@inheritDoc}
*/
@Override
public String doGetWithRetry(String url) throws IOException, DataSizeException {
GetRequestRunnable runnable = new GetRequestRunnable(url, false);
if (getRunUtil().runEscalatingTimedRetry(getOpTimeout(), getInitialPollInterval(),
getMaxPollInterval(), getMaxTime(), runnable)) {
return runnable.getResponse();
} else if (runnable.getException() instanceof IOException) {
throw (IOException) runnable.getException();
} else if (runnable.getException() instanceof DataSizeException) {
throw (DataSizeException) runnable.getException();
} else if (runnable.getException() instanceof RuntimeException) {
throw (RuntimeException) runnable.getException();
} else {
throw new IOException("GET request could not be completed");
}
}
public void doGetWithRetry(String url, File destFile) throws IOException {
DownloadRequestRunnable runnable = new DownloadRequestRunnable(url,destFile);
if(getRunUtil().runEscalatingTimedRetry(getOpTimeout(), getInitialPollInterval(),
getMaxPollInterval(), getMaxTime(), runnable)) {
} else if (runnable.getException() instanceof IOException) {
throw (IOException) runnable.getException();
} else {
throw new IOException("GET request could not be completed");
}
}
/**
* {@inheritDoc}
*/
@Override
public void doGetIgnoreWithRetry(String url) throws IOException {
GetRequestRunnable runnable = new GetRequestRunnable(url, true);
if (getRunUtil().runEscalatingTimedRetry(getOpTimeout(), getInitialPollInterval(),
getMaxPollInterval(), getMaxTime(), runnable)) {
return;
} else if (runnable.getException() instanceof IOException) {
throw (IOException) runnable.getException();
} else if (runnable.getException() instanceof RuntimeException) {
throw (RuntimeException) runnable.getException();
} else {
throw new IOException("GET request could not be completed");
}
}
/**
* {@inheritDoc}
*/
@Override
public String doPostWithRetry(String url, String postData, String contentType)
throws IOException, DataSizeException {
PostRequestRunnable runnable = new PostRequestRunnable(url, postData, contentType);
if (getRunUtil().runEscalatingTimedRetry(getOpTimeout(), getInitialPollInterval(),
getMaxPollInterval(), getMaxTime(), runnable)) {
return runnable.getResponse();
} else if (runnable.getException() instanceof IOException) {
throw (IOException) runnable.getException();
} else if (runnable.getException() instanceof DataSizeException) {
throw (DataSizeException) runnable.getException();
} else if (runnable.getException() instanceof RuntimeException) {
throw (RuntimeException) runnable.getException();
} else {
throw new IOException("POST request could not be completed");
}
}
/**
* {@inheritDoc}
*/
@Override
public String doPostWithRetry(String url, String postData) throws IOException,
DataSizeException {
return doPostWithRetry(url, postData, null);
}
/**
* Runnable for making requests with
* {@link IRunUtil#runEscalatingTimedRetry(long, long, long, long, IRunnableResult)}.
*/
private abstract class RequestRunnable implements IRunnableResult {
private String mResponse = null;
private Exception mException = null;
private final String mUrl;
public RequestRunnable(String url) {
mUrl = url;
}
public String getUrl() {
return mUrl;
}
public String getResponse() {
return mResponse;
}
protected void setResponse(String response) {
mResponse = response;
}
/**
* Returns the last {@link Exception} that occurred when performing run().
*/
public Exception getException() {
return mException;
}
protected void setException(Exception e) {
mException = e;
}
@Override
public void cancel() {
// ignore
}
}
/**
* Runnable for making GET requests with
* {@link IRunUtil#runEscalatingTimedRetry(long, long, long, long, IRunnableResult)}.
*/
private class GetRequestRunnable extends RequestRunnable {
private boolean mIgnoreResult;
public GetRequestRunnable(String url, boolean ignoreResult) {
super(url);
mIgnoreResult = ignoreResult;
}
/**
* Perform a single GET request, storing the response or the associated exception in case of
* error.
*/
@Override
public boolean run() {
try {
if (mIgnoreResult) {
doGetIgnore(getUrl());
} else {
setResponse(doGet(getUrl()));
}
return true;
} catch (IOException e) {
CLog.i("IOException %s from %s", e.getMessage(), getUrl());
setException(e);
} catch (DataSizeException e) {
CLog.i("Unexpected oversized response from %s", getUrl());
setException(e);
} catch (RuntimeException e) {
CLog.i("RuntimeException %s", e.getMessage());
setException(e);
}
return false;
}
}
private class DownloadRequestRunnable extends RequestRunnable {
private File mDestFile;
public DownloadRequestRunnable(String url, File destFile) {
super(url);
mDestFile = destFile;
}
@Override
public boolean run() {
try {
doGet(getUrl(),mDestFile);
return true;
} catch (IOException e) {
CLog.i("IOException %s from %s", e.getMessage(), getUrl());
setException(e);
}
return false;
}
}
/**
* Runnable for making POST requests with
* {@link IRunUtil#runEscalatingTimedRetry(long, long, long, long, IRunnableResult)}.
*/
private class PostRequestRunnable extends RequestRunnable {
String mPostData;
String mContentType;
public PostRequestRunnable(String url, String postData, String contentType) {
super(url);
mPostData = postData;
mContentType = contentType;
}
/**
* Perform a single POST request, storing the response or the associated exception in case
* of error.
*/
@SuppressWarnings("resource")
@Override
public boolean run() {
InputStream inputStream = null;
OutputStream outputStream = null;
OutputStreamWriter outputStreamWriter = null;
try {
HttpURLConnection conn = createConnection(new URL(getUrl()), "POST", mContentType);
outputStream = getConnectionOutputStream(conn);
outputStreamWriter = new OutputStreamWriter(outputStream);
outputStreamWriter.write(mPostData);
outputStreamWriter.flush();
inputStream = getConnectionInputStream(conn);
byte[] bufResult = new byte[MAX_DATA_SIZE];
int currBufPos = 0;
int bytesRead;
// read data from stream into temporary buffer
while ((bytesRead = inputStream.read(bufResult, currBufPos,
bufResult.length - currBufPos)) != -1) {
currBufPos += bytesRead;
if (currBufPos >= bufResult.length) {
// Eclipse compiler incorrectly flags this statement as not 'stream
// is not closed at this location'.
// So add @SuppressWarnings('resource') to shut it up.
throw new DataSizeException();
}
}
setResponse(new String(bufResult, 0, currBufPos));
return true;
} catch (IOException e) {
CLog.i("IOException %s from %s", e.getMessage(), getUrl());
setException(e);
} catch (DataSizeException e) {
CLog.i("Unexpected oversized response from %s", getUrl());
setException(e);
} catch (RuntimeException e) {
CLog.i("RuntimeException %s", e.getMessage());
setException(e);
} finally {
StreamUtil.close(outputStream);
StreamUtil.close(inputStream);
StreamUtil.close(outputStreamWriter);
}
return false;
}
}
/**
* Factory method for opening an input stream to a remote url. Exposed for unit testing.
*
* @param url the {@link URL}
* @return the {@link InputStream}
* @throws IOException if stream could not be opened.
*/
InputStream getRemoteUrlStream(URL url) throws IOException {
return url.openStream();
}
/**
* Factory method for getting connection input stream. Exposed for unit testing.
*/
InputStream getConnectionInputStream(HttpURLConnection conn) throws IOException {
return conn.getInputStream();
}
/**
* Factory method for getting connection output stream. Exposed for unit testing.
*/
OutputStream getConnectionOutputStream(HttpURLConnection conn) throws IOException {
return conn.getOutputStream();
}
/**
* {@inheritDoc}
*/
@Override
public long getOpTimeout() {
return mQueryTimeout;
}
/**
* {@inheritDoc}
*/
@Override
public void setOpTimeout(long time) {
mQueryTimeout = time;
}
/**
* {@inheritDoc}
*/
@Override
public long getInitialPollInterval() {
return mInitialPollInterval;
}
/**
* {@inheritDoc}
*/
@Override
public void setInitialPollInterval(long time) {
mInitialPollInterval = time;
}
/**
* {@inheritDoc}
*/
@Override
public long getMaxPollInterval() {
return mMaxPollInterval;
}
/**
* {@inheritDoc}
*/
@Override
public void setMaxPollInterval(long time) {
mMaxPollInterval = time;
}
/**
* {@inheritDoc}
*/
@Override
public long getMaxTime() {
return mMaxTime;
}
/**
* {@inheritDoc}
*/
@Override
public void setMaxTime(long time) {
mMaxTime = time;
}
/**
* Get {@link IRunUtil} to use. Exposed so unit tests can mock.
*/
IRunUtil getRunUtil() {
return RunUtil.getDefault();
}
}