blob: 29e82d199a684113f38870b214606068e4074cdd [file] [log] [blame]
/*
* Copyright 2011 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.JavaScriptObject;
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;
/**
* Base for a standard loading strategy used in a web browser. Subclasses
* provide an implementation of DownloadStrategy, thereby controlling how
* the download of the code will be done, while the base class controls how
* to interact with the linker, handle download failures, etc.
*
* The linker it is used with should provide JavaScript-level functions to
* indicate how to handle downloading and installing code.
*
* Linkers should always provide a function
* <code>__gwtStartLoadingFragment</code>. This function is called by
* this class 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>, and the linker should handle
* installing the code as well. Note that in this case, all the code in this
* class is pretty much moot.
*
* 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 LoadingStrategyBase implements LoadingStrategy {
/**
* Subclasses will need to implement this and pass it in in the constructor.
* This is how they control how the download will be done (XHR, Script tag, etc.)
* If the download succeeds, the DownloadStrategy should call it's
* RequestData's tryInstall() function, and if it fails, it should call it's
* RequestData's onLoadError() function.
*/
protected interface DownloadStrategy {
void tryDownload(final RequestData request);
}
/**
* A trivial JavaScript map from ints to ints. Used to keep a global count of
* how many times a user has manually retried fetching a fragment.
*/
private static final class FragmentReloadTracker extends JavaScriptObject {
public static FragmentReloadTracker create() {
return (FragmentReloadTracker) JavaScriptObject.createArray();
}
protected FragmentReloadTracker() { }
public native int get(int x) /*-{
return this[x] ? this[x] : 0;
}-*/;
public native void put(int x, int y) /*-{
this[x] = y;
}-*/;
}
/**
* Since LoadingStrategy must support concurrent requests, we keep most of the
* relevant info in the RequestData, and pass it around. Once created, a
* RequestData interacts primarily with it's DownloadStrategy, which will
* attempt call out to the RequestData's tryInstall function if the download
* succeeds, or it's onLoadError if the download fails.
*/
protected static class RequestData {
private static final int MAX_LOG_LENGTH = 200;
private DownloadStrategy downloadStrategy;
private LoadTerminatedHandler errorHandler = null;
private int fragment;
private int maxRetryCount;
private String originalUrl;
private int retryCount;
private String url;
public RequestData(String url, LoadTerminatedHandler errorHandler,
int fragment, DownloadStrategy downloadStrategy, int maxRetryCount) {
this.url = url;
this.originalUrl = url;
this.errorHandler = errorHandler;
this.maxRetryCount = maxRetryCount;
this.retryCount = 0;
this.fragment = fragment;
this.downloadStrategy = downloadStrategy;
}
public LoadTerminatedHandler getErrorHandler() { return errorHandler; }
public int getFragment() { return fragment; }
public int getRetryCount() { return retryCount; }
protected void setRetryCount(int retryCount) {
this.retryCount = retryCount;
}
public String getUrl() { return url; }
protected void setUrl(String url) {
this.url = url;
}
public String getOriginalUrl() { return originalUrl; }
public void onLoadError(Throwable e, boolean mayRetry) {
if (mayRetry) {
retryCount++;
if (retryCount <= maxRetryCount) {
char connector = originalUrl.contains("?") ? '&' : '?';
url = originalUrl + connector + "autoRetry=" + retryCount;
downloadStrategy.tryDownload(this);
return;
}
}
errorHandler.loadTerminated(e);
}
public void tryDownload() {
downloadStrategy.tryDownload(this);
}
public void tryInstall(String code) {
try {
gwtInstallCode(code);
} catch (RuntimeException e) {
String textIntro = code;
if (textIntro != null && textIntro.length() > MAX_LOG_LENGTH) {
textIntro = textIntro.substring(0, MAX_LOG_LENGTH) + "...";
}
onLoadError(new HttpInstallFailure(url, textIntro, e), false);
}
}
}
/**
* The number of times that we will retry a download. Note that if the install
* fails, we do not retry, since there's no reason to expect a different result.
*/
public static int MAX_AUTO_RETRY_COUNT = 3;
/**
* Call the linker-supplied <code>__gwtInstallCode</code> method. This method
* will attempt to install the code, and throw a runtime exception if it fails,
* which will get caught by the RequestData.tryInstall() function.
*/
protected static 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 static native String gwtStartLoadingFragment(int fragment,
LoadTerminatedHandler loadErrorHandler) /*-{
function loadFailed(e) {
loadErrorHandler.@com.google.gwt.core.client.impl.AsyncFragmentLoader$LoadTerminatedHandler::loadTerminated(Ljava/lang/Throwable;)(e);
}
return __gwtStartLoadingFragment(fragment, $entry(loadFailed));
}-*/;
private DownloadStrategy downloadStrategy;
private final FragmentReloadTracker manualRetryNumbers = FragmentReloadTracker.create();
public DownloadStrategy getDownloadStrategy() {
return downloadStrategy;
}
/**
* Subclasses should create a DownloadStrategy and pass it into this constructor.
*/
public LoadingStrategyBase(DownloadStrategy downloadStrategy) {
this.downloadStrategy = downloadStrategy;
}
@Override
public void startLoadingFragment(int fragment,
final LoadTerminatedHandler loadErrorHandler) {
String url = gwtStartLoadingFragment(fragment, loadErrorHandler);
if (url == null) {
// The linker is going to handle this fetch - nothing more to do
return;
}
// Browsers will ignore too many script tags if it has previously failed
// to download that url, so we add a parameter to the url if
// this is not the first time we've tried to download this fragment.
int manualRetry = getManualRetryNum(fragment);
if (manualRetry > 0) {
char connector = url.contains("?") ? '&' : '?';
url += connector + "manualRetry=" + manualRetry;
}
RequestData request = new RequestData(url, loadErrorHandler,
fragment, downloadStrategy, getMaxAutoRetryCount());
request.tryDownload();
}
protected int getMaxAutoRetryCount() { return MAX_AUTO_RETRY_COUNT; }
private int getManualRetryNum(int fragment) {
int ser = manualRetryNumbers.get(fragment);
manualRetryNumbers.put(fragment, ser + 1);
return ser;
}
}