/* * 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.LoadTerminatedHandler; import com.google.gwt.core.client.impl.AsyncFragmentLoader.LoadingStrategy; import com.google.gwt.core.client.impl.AsyncFragmentLoader.HttpInstallFailure; /** * 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. */ 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 int getFragment() { return fragment; } public String getUrl() { return url; } 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(); /** * 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; } }