blob: 2ed5d4cff99666e811b7710dbd1f7e286a5cbad0 [file] [log] [blame]
/*
* Copyright (C) 2006 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 android.net.http;
import java.io.EOFException;
import java.io.InputStream;
import java.io.IOException;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.zip.GZIPInputStream;
import org.apache.http.entity.InputStreamEntity;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpException;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.HttpStatus;
import org.apache.http.ParseException;
import org.apache.http.ProtocolVersion;
import org.apache.http.StatusLine;
import org.apache.http.message.BasicHttpRequest;
import org.apache.http.message.BasicHttpEntityEnclosingRequest;
import org.apache.http.protocol.RequestContent;
/**
* Represents an HTTP request for a given host.
*/
public class Request {
/** The eventhandler to call as the request progresses */
EventHandler mEventHandler;
private Connection mConnection;
/** The Apache http request */
BasicHttpRequest mHttpRequest;
/** The path component of this request */
String mPath;
/** Host serving this request */
HttpHost mHost;
/** Set if I'm using a proxy server */
HttpHost mProxyHost;
/** True if request has been cancelled */
volatile boolean mCancelled = false;
int mFailCount = 0;
// This will be used to set the Range field if we retry a connection. This
// is http/1.1 feature.
private int mReceivedBytes = 0;
private InputStream mBodyProvider;
private int mBodyLength;
private final static String HOST_HEADER = "Host";
private final static String ACCEPT_ENCODING_HEADER = "Accept-Encoding";
private final static String CONTENT_LENGTH_HEADER = "content-length";
/* Used to synchronize waitUntilComplete() requests */
private final Object mClientResource = new Object();
/** True if loading should be paused **/
private boolean mLoadingPaused = false;
/**
* Processor used to set content-length and transfer-encoding
* headers.
*/
private static RequestContent requestContentProcessor =
new RequestContent();
/**
* Instantiates a new Request.
* @param method GET/POST/PUT
* @param host The server that will handle this request
* @param path path part of URI
* @param bodyProvider InputStream providing HTTP body, null if none
* @param bodyLength length of body, must be 0 if bodyProvider is null
* @param eventHandler request will make progress callbacks on
* this interface
* @param headers reqeust headers
*/
Request(String method, HttpHost host, HttpHost proxyHost, String path,
InputStream bodyProvider, int bodyLength,
EventHandler eventHandler,
Map<String, String> headers) {
mEventHandler = eventHandler;
mHost = host;
mProxyHost = proxyHost;
mPath = path;
mBodyProvider = bodyProvider;
mBodyLength = bodyLength;
if (bodyProvider == null && !"POST".equalsIgnoreCase(method)) {
mHttpRequest = new BasicHttpRequest(method, getUri());
} else {
mHttpRequest = new BasicHttpEntityEnclosingRequest(
method, getUri());
// it is ok to have null entity for BasicHttpEntityEnclosingRequest.
// By using BasicHttpEntityEnclosingRequest, it will set up the
// correct content-length, content-type and content-encoding.
if (bodyProvider != null) {
setBodyProvider(bodyProvider, bodyLength);
}
}
addHeader(HOST_HEADER, getHostPort());
/* FIXME: if webcore will make the root document a
high-priority request, we can ask for gzip encoding only on
high priority reqs (saving the trouble for images, etc) */
addHeader(ACCEPT_ENCODING_HEADER, "gzip");
addHeaders(headers);
}
/**
* @param pause True if the load should be paused.
*/
synchronized void setLoadingPaused(boolean pause) {
mLoadingPaused = pause;
// Wake up the paused thread if we're unpausing the load.
if (!mLoadingPaused) {
notify();
}
}
/**
* @param connection Request served by this connection
*/
void setConnection(Connection connection) {
mConnection = connection;
}
/* package */ EventHandler getEventHandler() {
return mEventHandler;
}
/**
* Add header represented by given pair to request. Header will
* be formatted in request as "name: value\r\n".
* @param name of header
* @param value of header
*/
void addHeader(String name, String value) {
if (name == null) {
String damage = "Null http header name";
HttpLog.e(damage);
throw new NullPointerException(damage);
}
if (value == null || value.length() == 0) {
String damage = "Null or empty value for header \"" + name + "\"";
HttpLog.e(damage);
throw new RuntimeException(damage);
}
mHttpRequest.addHeader(name, value);
}
/**
* Add all headers in given map to this request. This is a helper
* method: it calls addHeader for each pair in the map.
*/
void addHeaders(Map<String, String> headers) {
if (headers == null) {
return;
}
Entry<String, String> entry;
Iterator<Entry<String, String>> i = headers.entrySet().iterator();
while (i.hasNext()) {
entry = i.next();
addHeader(entry.getKey(), entry.getValue());
}
}
/**
* Send the request line and headers
*/
void sendRequest(AndroidHttpClientConnection httpClientConnection)
throws HttpException, IOException {
if (mCancelled) return; // don't send cancelled requests
if (HttpLog.LOGV) {
HttpLog.v("Request.sendRequest() " + mHost.getSchemeName() + "://" + getHostPort());
// HttpLog.v(mHttpRequest.getRequestLine().toString());
if (false) {
Iterator i = mHttpRequest.headerIterator();
while (i.hasNext()) {
Header header = (Header)i.next();
HttpLog.v(header.getName() + ": " + header.getValue());
}
}
}
requestContentProcessor.process(mHttpRequest,
mConnection.getHttpContext());
httpClientConnection.sendRequestHeader(mHttpRequest);
if (mHttpRequest instanceof HttpEntityEnclosingRequest) {
httpClientConnection.sendRequestEntity(
(HttpEntityEnclosingRequest) mHttpRequest);
}
if (HttpLog.LOGV) {
HttpLog.v("Request.requestSent() " + mHost.getSchemeName() + "://" + getHostPort() + mPath);
}
}
/**
* Receive a single http response.
*
* @param httpClientConnection the request to receive the response for.
*/
void readResponse(AndroidHttpClientConnection httpClientConnection)
throws IOException, ParseException {
if (mCancelled) return; // don't send cancelled requests
StatusLine statusLine = null;
boolean hasBody = false;
httpClientConnection.flush();
int statusCode = 0;
Headers header = new Headers();
do {
statusLine = httpClientConnection.parseResponseHeader(header);
statusCode = statusLine.getStatusCode();
} while (statusCode < HttpStatus.SC_OK);
if (HttpLog.LOGV) HttpLog.v(
"Request.readResponseStatus() " +
statusLine.toString().length() + " " + statusLine);
ProtocolVersion v = statusLine.getProtocolVersion();
mEventHandler.status(v.getMajor(), v.getMinor(),
statusCode, statusLine.getReasonPhrase());
mEventHandler.headers(header);
HttpEntity entity = null;
hasBody = canResponseHaveBody(mHttpRequest, statusCode);
if (hasBody)
entity = httpClientConnection.receiveResponseEntity(header);
// restrict the range request to the servers claiming that they are
// accepting ranges in bytes
boolean supportPartialContent = "bytes".equalsIgnoreCase(header
.getAcceptRanges());
if (entity != null) {
InputStream is = entity.getContent();
// process gzip content encoding
Header contentEncoding = entity.getContentEncoding();
InputStream nis = null;
byte[] buf = null;
int count = 0;
try {
if (contentEncoding != null &&
contentEncoding.getValue().equals("gzip")) {
nis = new GZIPInputStream(is);
} else {
nis = is;
}
/* accumulate enough data to make it worth pushing it
* up the stack */
buf = mConnection.getBuf();
int len = 0;
int lowWater = buf.length / 2;
while (len != -1) {
synchronized(this) {
while (mLoadingPaused) {
// Put this (network loading) thread to sleep if WebCore
// has asked us to. This can happen with plugins for
// example, if we are streaming data but the plugin has
// filled its internal buffers.
try {
wait();
} catch (InterruptedException e) {
HttpLog.e("Interrupted exception whilst "
+ "network thread paused at WebCore's request."
+ " " + e.getMessage());
}
}
}
len = nis.read(buf, count, buf.length - count);
if (len != -1) {
count += len;
if (supportPartialContent) mReceivedBytes += len;
}
if (len == -1 || count >= lowWater) {
if (HttpLog.LOGV) HttpLog.v("Request.readResponse() " + count);
mEventHandler.data(buf, count);
count = 0;
}
}
} catch (EOFException e) {
/* InflaterInputStream throws an EOFException when the
server truncates gzipped content. Handle this case
as we do truncated non-gzipped content: no error */
if (count > 0) {
// if there is uncommited content, we should commit them
mEventHandler.data(buf, count);
}
if (HttpLog.LOGV) HttpLog.v( "readResponse() handling " + e);
} catch(IOException e) {
// don't throw if we have a non-OK status code
if (statusCode == HttpStatus.SC_OK
|| statusCode == HttpStatus.SC_PARTIAL_CONTENT) {
if (supportPartialContent && count > 0) {
// if there is uncommited content, we should commit them
// as we will continue the request
mEventHandler.data(buf, count);
}
throw e;
}
} finally {
if (nis != null) {
nis.close();
}
}
}
mConnection.setCanPersist(entity, statusLine.getProtocolVersion(),
header.getConnectionType());
mEventHandler.endData();
complete();
if (HttpLog.LOGV) HttpLog.v("Request.readResponse(): done " +
mHost.getSchemeName() + "://" + getHostPort() + mPath);
}
/**
* Data will not be sent to or received from server after cancel()
* call. Does not close connection--use close() below for that.
*
* Called by RequestHandle from non-network thread
*/
synchronized void cancel() {
if (HttpLog.LOGV) {
HttpLog.v("Request.cancel(): " + getUri());
}
// Ensure that the network thread is not blocked by a hanging request from WebCore to
// pause the load.
mLoadingPaused = false;
notify();
mCancelled = true;
if (mConnection != null) {
mConnection.cancel();
}
}
String getHostPort() {
String myScheme = mHost.getSchemeName();
int myPort = mHost.getPort();
// Only send port when we must... many servers can't deal with it
if (myPort != 80 && myScheme.equals("http") ||
myPort != 443 && myScheme.equals("https")) {
return mHost.toHostString();
} else {
return mHost.getHostName();
}
}
String getUri() {
if (mProxyHost == null ||
mHost.getSchemeName().equals("https")) {
return mPath;
}
return mHost.getSchemeName() + "://" + getHostPort() + mPath;
}
/**
* for debugging
*/
public String toString() {
return mPath;
}
/**
* If this request has been sent once and failed, it must be reset
* before it can be sent again.
*/
void reset() {
/* clear content-length header */
mHttpRequest.removeHeaders(CONTENT_LENGTH_HEADER);
if (mBodyProvider != null) {
try {
mBodyProvider.reset();
} catch (IOException ex) {
if (HttpLog.LOGV) HttpLog.v(
"failed to reset body provider " +
getUri());
}
setBodyProvider(mBodyProvider, mBodyLength);
}
if (mReceivedBytes > 0) {
// reset the fail count as we continue the request
mFailCount = 0;
// set the "Range" header to indicate that the retry will continue
// instead of restarting the request
HttpLog.v("*** Request.reset() to range:" + mReceivedBytes);
mHttpRequest.setHeader("Range", "bytes=" + mReceivedBytes + "-");
}
}
/**
* Pause thread request completes. Used for synchronous requests,
* and testing
*/
void waitUntilComplete() {
synchronized (mClientResource) {
try {
if (HttpLog.LOGV) HttpLog.v("Request.waitUntilComplete()");
mClientResource.wait();
if (HttpLog.LOGV) HttpLog.v("Request.waitUntilComplete() done waiting");
} catch (InterruptedException e) {
}
}
}
void complete() {
synchronized (mClientResource) {
mClientResource.notifyAll();
}
}
/**
* Decide whether a response comes with an entity.
* The implementation in this class is based on RFC 2616.
* Unknown methods and response codes are supposed to
* indicate responses with an entity.
* <br/>
* Derived executors can override this method to handle
* methods and response codes not specified in RFC 2616.
*
* @param request the request, to obtain the executed method
* @param response the response, to obtain the status code
*/
private static boolean canResponseHaveBody(final HttpRequest request,
final int status) {
if ("HEAD".equalsIgnoreCase(request.getRequestLine().getMethod())) {
return false;
}
return status >= HttpStatus.SC_OK
&& status != HttpStatus.SC_NO_CONTENT
&& status != HttpStatus.SC_NOT_MODIFIED;
}
/**
* Supply an InputStream that provides the body of a request. It's
* not great that the caller must also provide the length of the data
* returned by that InputStream, but the client needs to know up
* front, and I'm not sure how to get this out of the InputStream
* itself without a costly readthrough. I'm not sure skip() would
* do what we want. If you know a better way, please let me know.
*/
private void setBodyProvider(InputStream bodyProvider, int bodyLength) {
if (!bodyProvider.markSupported()) {
throw new IllegalArgumentException(
"bodyProvider must support mark()");
}
// Mark beginning of stream
bodyProvider.mark(Integer.MAX_VALUE);
((BasicHttpEntityEnclosingRequest)mHttpRequest).setEntity(
new InputStreamEntity(bodyProvider, bodyLength));
}
/**
* Handles SSL error(s) on the way down from the user (the user
* has already provided their feedback).
*/
void handleSslErrorResponse(boolean proceed) {
HttpsConnection connection = (HttpsConnection)(mConnection);
if (connection != null) {
connection.restartConnection(proceed);
}
}
/**
* Helper: calls error() on eventhandler with appropriate message
* This should not be called before the mConnection is set.
*/
void error(int errorId, String errorMessage) {
mEventHandler.error(errorId, errorMessage);
}
}