| /* |
| * 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. |
| */ |
| 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 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(); |
| } |
| } |