blob: 84abb3a22ce39fd1f9e19fa7f69c7768c021a451 [file] [log] [blame]
/*
* Copyright 2009 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.core.client.impl;
import com.google.gwt.core.client.impl.AsyncFragmentLoader.HttpDownloadFailure;
import com.google.gwt.core.client.impl.AsyncFragmentLoader.HttpInstallFailure;
import com.google.gwt.core.client.impl.AsyncFragmentLoader.LoadTerminatedHandler;
import com.google.gwt.core.client.impl.AsyncFragmentLoader.LoadingStrategy;
import com.google.gwt.xhr.client.ReadyStateChangeHandler;
import com.google.gwt.xhr.client.XMLHttpRequest;
/**
* The standard loading strategy used in a web browser. The linker it is used
* with should provide JavaScript-level functions to indicate how to handle
* downloading and installing code. There is support to use XHR for the
* download.
*
* Linkers should always provide a function
* <code>__gwtStartLoadingFragment</code>. This function is called by
* AsyncFragmentLoader with two arguments: an integer fragment number that needs
* to be downloaded, and a one-argument loadFinished function. If the load
* fails, that function should be called with a descriptive exception as the
* argument. If the load succeeds, that function may also be called, so long as
* it isn't called until the downloaded code has been installed.
*
*
* If the mechanism for loading the contents of fragments is provided by the
* linker, the <code>__gwtStartLoadingFragment</code> function should return
* <code>null</code> or <code>undefined</code>.
*
* Alternatively, the function can return a URL designating from where the code
* for the requested fragment can be downloaded. In that case, the linker should
* also provide a function <code>__gwtInstallCode</code> for actually installing
* the code once it is downloaded. That function will be passed the loaded code
* once it has been downloaded.
*/
public class XhrLoadingStrategy implements LoadingStrategy {
/**
* A {@link MockableXMLHttpRequest} that is really just a vanilla
* XMLHttpRequest. This wrapper (and thus {@code MockableXMLHttpRequest} is
* needed because so much of {@link XMLHttpRequest} is final, which in turn
* is because it extends {@code JavaScriptObject} and is subject to its
* restrictions.
*
* It is important that these methods be simple enough to be inlined away.
*/
static class DelegatingXMLHttpRequest implements MockableXMLHttpRequest {
private final XMLHttpRequest delegate;
public DelegatingXMLHttpRequest(XMLHttpRequest xmlHttpRequest) {
delegate = xmlHttpRequest;
}
public void clearOnReadyStateChange() {
delegate.clearOnReadyStateChange();
}
public int getReadyState() {
return delegate.getReadyState();
}
public String getResponseText() {
return delegate.getResponseText();
}
public int getStatus() {
return delegate.getStatus();
}
public String getStatusText() {
return delegate.getStatusText();
}
public void open(String method, String url) {
delegate.open(method, url);
}
public void send() {
delegate.send();
}
public void setOnReadyStateChange(ReadyStateChangeHandler handler) {
delegate.setOnReadyStateChange(handler);
}
public void setRequestHeader(String header, String value) {
delegate.setRequestHeader(header, value);
}
}
/**
* Delegates to the real XMLHttpRequest, except in test when we make a mock
* to jump through error/retry hoops.
*/
interface MockableXMLHttpRequest {
void clearOnReadyStateChange();
int getReadyState();
String getResponseText();
int getStatus();
String getStatusText();
void open(String method, String url);
void send();
void setOnReadyStateChange(ReadyStateChangeHandler handler);
void setRequestHeader(String header, String value);
}
/**
* Since LoadingStrategy must support concurrent requests, including figuring
* which is which in the onLoadError handling, we need to keep track of this
* data for each outstanding request, which we index by xhr object.
*/
protected static class RequestData {
String url;
int retryCount;
LoadTerminatedHandler errorHandler = null;
public RequestData(String url, LoadTerminatedHandler errorHandler) {
this.url = url;
this.errorHandler = errorHandler;
this.retryCount = 0;
}
}
static final String HTTP_GET = "GET";
/**
* Some UA's like Safari will have a "0" status code when loading from file:
* URLs. Additionally, the "0" status code is used sometimes if the server
* does not respond, e.g. if there is a connection refused.
*/
static final int HTTP_STATUS_NON_HTTP = 0;
static final int HTTP_STATUS_OK = 200;
/**
* For error logging, max length of fragment response text to include in
* failed-to-install exception message.
*/
private static final int MAX_LOG_LENGTH = 200;
/**
* Number of retry attempts for a single fragment. If a fragment download
* fails, we try again this many times before "really" failing out to user
* error-handling code. If a fragment downloads but doesn't install, we
* don't retry at all.
*/
private static final int MAX_RETRY_COUNT = 3;
public void startLoadingFragment(int fragment,
final LoadTerminatedHandler loadErrorHandler) {
String url = gwtStartLoadingFragment(fragment, loadErrorHandler);
if (url == null) {
// The download has already started; nothing more to do
return;
}
RequestData request = new RequestData(url, loadErrorHandler);
tryLoad(request);
}
/**
* Overridable for tests.
*/
protected MockableXMLHttpRequest createXhr() {
return new DelegatingXMLHttpRequest(XMLHttpRequest.create());
}
/**
* Call the linker-supplied <code>__gwtInstallCode</code> method. See the
* {@link AsyncFragmentLoader class comment} for more details.
*/
protected native void gwtInstallCode(String text) /*-{
__gwtInstallCode(text);
}-*/;
/**
* Call the linker-supplied __gwtStartLoadingFragment function. It should
* either start the download and return null or undefined, or it should
* return a URL that should be downloaded to get the code. If it starts the
* download itself, it can synchronously load it, e.g. from cache, if that
* makes sense.
*/
protected native String gwtStartLoadingFragment(int fragment,
LoadTerminatedHandler loadErrorHandler) /*-{
function loadFailed(e) {
loadErrorHandler.@com.google.gwt.core.client.impl.AsyncFragmentLoader$LoadTerminatedHandler::loadTerminated(*)(e);
}
return __gwtStartLoadingFragment(fragment, $entry(loadFailed));
}-*/;
/**
* Error recovery from loading or installing code.
* @param request the requestData of this request
* @param e exception of the error
* @param mayRetry {@code true} if retrying might be helpful
*/
protected void onLoadError(RequestData request, Throwable e, boolean mayRetry) {
if (mayRetry) {
request.retryCount++;
if (request.retryCount < MAX_RETRY_COUNT) {
tryLoad(request);
return;
}
}
request.errorHandler.loadTerminated(e);
}
/**
* Makes a single load-and-install attempt.
*/
protected void tryLoad(final RequestData request) {
final MockableXMLHttpRequest xhr = createXhr();
xhr.open(HTTP_GET, request.url);
if (request.retryCount > 0) {
// disable caching if we have to retry; one cause could be bad cache
xhr.setRequestHeader("Cache-Control", "no-cache");
}
xhr.setOnReadyStateChange(new ReadyStateChangeHandler() {
public void onReadyStateChange(XMLHttpRequest ignored) {
if (xhr.getReadyState() == XMLHttpRequest.DONE) {
xhr.clearOnReadyStateChange();
if ((xhr.getStatus() == HTTP_STATUS_OK || xhr.getStatus() == HTTP_STATUS_NON_HTTP)
&& xhr.getResponseText() != null
&& xhr.getResponseText().length() != 0) {
try {
gwtInstallCode(xhr.getResponseText());
} catch (RuntimeException e) {
String textIntro = xhr.getResponseText();
if (textIntro != null && textIntro.length() > MAX_LOG_LENGTH) {
textIntro = textIntro.substring(0, MAX_LOG_LENGTH) + "...";
}
onLoadError(request,
new HttpInstallFailure(request.url, textIntro, e), false);
}
} else {
onLoadError(request,
new HttpDownloadFailure(request.url, xhr.getStatus(),
xhr.getStatusText()), true);
}
}
}
});
xhr.send();
}
}