blob: 12608c54222b9251980308db7d4b7f3c60b7bcb6 [file] [log] [blame]
/*
* Copyright 2008 Google Inc.
*
* 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.google.gwt.http.client;
import com.google.gwt.core.shared.GWT;
import com.google.gwt.user.client.Timer;
import com.google.gwt.xhr.client.XMLHttpRequest;
/**
* An HTTP request that is waiting for a response. Requests can be queried for
* their pending status or they can be canceled.
*
* <h3>Required Module</h3> Modules that use this class should inherit
* <code>com.google.gwt.http.HTTP</code>.
*
* {@gwt.include
* com/google/gwt/examples/http/InheritsExample.gwt.xml}
*
*/
public class Request {
/**
* Native implementation associated with {@link Request}. User classes should not use this class
* directly.
*/
static class RequestImpl {
/**
* Creates a {@link Response} instance for the given JavaScript XmlHttpRequest object.
*
* @param xmlHttpRequest xmlHttpRequest object for which we need a response
* @return a {@link Response} object instance
*/
Response createResponse(final XMLHttpRequest xmlHttpRequest) {
return new ResponseImpl(xmlHttpRequest);
}
}
/**
* Special {@link RequestImpl} for IE6-9 to work around some IE specialities.
*/
static class RequestImplIE6To9 extends RequestImpl {
@Override
Response createResponse(XMLHttpRequest xmlHttpRequest) {
return new ResponseImpl(xmlHttpRequest) {
@Override
public int getStatusCode() {
/*
* http://code.google.com/p/google-web-toolkit/issues/detail?id=5031
*
* The XMLHTTPRequest object in IE will return a status code of 1223 and drop some
* response headers if the server returns a HTTP/204.
*
* This issue is fixed in IE10.
*/
int statusCode = super.getStatusCode();
return (statusCode == 1223) ? SC_NO_CONTENT : statusCode;
}
};
}
}
/*
* Although Request is a client-side class, it's a transitive dependency of
* some GWT servlet code. Because GWT.create() isn't safe to call on the
* server, we use the "Initialization On Demand Holder" idiom to lazily
* initialize the RequestImpl.
*/
private static class ImplHolder {
private static final RequestImpl impl = GWT.create(RequestImpl.class);
public static RequestImpl get() {
return impl;
}
}
/**
* Creates a {@link Response} instance for the given JavaScript XmlHttpRequest
* object.
*
* @param xmlHttpRequest xmlHttpRequest object for which we need a response
* @return a {@link Response} object instance
*/
private static Response createResponse(final XMLHttpRequest xmlHttpRequest) {
return ImplHolder.get().createResponse(xmlHttpRequest);
}
/**
* The number of milliseconds to wait for this HTTP request to complete.
*/
private final int timeoutMillis;
/*
* Timer used to force HTTPRequest timeouts. If the user has not requested a
* timeout then this field is null.
*/
private final Timer timer;
/*
* JavaScript XmlHttpRequest object that this Java class wraps. This field is
* not final because we transfer ownership of it to the HTTPResponse object
* and set this field to null.
*/
private XMLHttpRequest xmlHttpRequest;
/**
* Only used for building a
* {@link com.google.gwt.user.client.rpc.impl.FailedRequest}.
*/
protected Request() {
timeoutMillis = 0;
xmlHttpRequest = null;
timer = null;
}
/**
* Constructs an instance of the Request object.
*
* @param xmlHttpRequest JavaScript XmlHttpRequest object instance
* @param timeoutMillis number of milliseconds to wait for a response
* @param callback callback interface to use for notification
*
* @throws IllegalArgumentException if timeoutMillis &lt; 0
* @throws NullPointerException if xmlHttpRequest, or callback are null
*/
Request(XMLHttpRequest xmlHttpRequest, int timeoutMillis,
final RequestCallback callback) {
if (xmlHttpRequest == null) {
throw new NullPointerException();
}
if (callback == null) {
throw new NullPointerException();
}
if (timeoutMillis < 0) {
throw new IllegalArgumentException();
}
this.timeoutMillis = timeoutMillis;
this.xmlHttpRequest = xmlHttpRequest;
if (timeoutMillis > 0) {
// create and start a Timer
timer = new Timer() {
@Override
public void run() {
fireOnTimeout(callback);
}
};
timer.schedule(timeoutMillis);
} else {
// no Timer required
timer = null;
}
}
/**
* Cancels a pending request. If the request has already been canceled or if
* it has timed out no action is taken.
*/
public void cancel() {
/*
* There is a strange race condition that occurs on Mozilla when you cancel
* a request while the response is coming in. It appears that in some cases
* the onreadystatechange handler is still called after the handler function
* has been deleted and during the call to XmlHttpRequest.abort(). So we
* null the xmlHttpRequest here and that will prevent the
* fireOnResponseReceived method from calling the callback function.
*
* Setting the onreadystatechange handler to null gives us the correct
* behavior in Mozilla but crashes IE. That is why we have chosen to fixed
* this in Java by nulling out our reference to the XmlHttpRequest object.
*/
if (xmlHttpRequest != null) {
XMLHttpRequest xmlHttp = xmlHttpRequest;
xmlHttpRequest = null;
xmlHttp.clearOnReadyStateChange();
xmlHttp.abort();
cancelTimer();
}
}
/**
* Returns true if this request is waiting for a response.
*
* @return true if this request is waiting for a response
*/
public boolean isPending() {
if (xmlHttpRequest == null) {
return false;
}
int readyState = xmlHttpRequest.getReadyState();
/*
* Because we are doing asynchronous requests it is possible that we can
* call XmlHttpRequest.send and still have the XmlHttpRequest.getReadyState
* method return the state as XmlHttpRequest.OPEN. That is why we include
* open although it is nottechnically true since open implies that the
* request has not been sent.
*/
switch (readyState) {
case XMLHttpRequest.OPENED:
case XMLHttpRequest.HEADERS_RECEIVED:
case XMLHttpRequest.LOADING:
return true;
}
return false;
}
/*
* Method called when the JavaScript XmlHttpRequest object's readyState
* reaches 4 (LOADED).
*/
void fireOnResponseReceived(RequestCallback callback) {
if (xmlHttpRequest == null) {
// the request has timed out at this point
return;
}
cancelTimer();
/*
* We cannot use cancel here because it would clear the contents of the
* JavaScript XmlHttpRequest object so we manually null out our reference to
* the JavaScriptObject
*/
final XMLHttpRequest xhr = xmlHttpRequest;
xmlHttpRequest = null;
String errorMsg = getBrowserSpecificFailure(xhr);
if (errorMsg != null) {
Throwable exception = new RuntimeException(errorMsg);
callback.onError(this, exception);
} else {
Response response = createResponse(xhr);
callback.onResponseReceived(this, response);
}
}
/*
* Stops the current HTTPRequest timer if there is one.
*/
private void cancelTimer() {
if (timer != null) {
timer.cancel();
}
}
/*
* Method called when this request times out.
*
* NOTE: this method is called from JSNI
*/
private void fireOnTimeout(RequestCallback callback) {
if (xmlHttpRequest == null) {
// the request has been received at this point
return;
}
cancel();
callback.onError(this, new RequestTimeoutException(this, timeoutMillis));
}
/**
* Tests if the JavaScript <code>XmlHttpRequest.status</code> property is
* readable. This can return failure in two different known scenarios:
*
* <ol>
* <li>On Mozilla, after a network error, attempting to read the status code
* results in an exception being thrown. See <a
* href="https://bugzilla.mozilla.org/show_bug.cgi?id=238559"
* >https://bugzilla.mozilla.org/show_bug.cgi?id=238559</a>.</li>
* <li>On Safari, if the HTTP response does not include any response text. See
* <a
* href="http://bugs.webkit.org/show_bug.cgi?id=3810">http://bugs.webkit.org
* /show_bug.cgi?id=3810</a>.</li>
* </ol>
*
* @param xhr the JavaScript <code>XmlHttpRequest</code> object to test
* @return a String message containing an error message if the
* <code>XmlHttpRequest.status</code> code is unreadable or null if
* the status code could be successfully read.
*/
private native String getBrowserSpecificFailure(XMLHttpRequest xhr) /*-{
try {
if (xhr.status === undefined) {
return "XmlHttpRequest.status == undefined, please see Safari bug " +
"http://bugs.webkit.org/show_bug.cgi?id=3810 for more details";
}
return null;
} catch (e) {
return "Unable to read XmlHttpRequest.status; likely causes are a " +
"networking error or bad cross-domain request. Please see " +
"https://bugzilla.mozilla.org/show_bug.cgi?id=238559 for more " +
"details";
}
}-*/;
}