/**
* Copyright 2010 The ForPlay Authors
*
* 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 forplay.html;
import java.util.HashMap;
import java.util.Map;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.JavaScriptException;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.ImageElement;
import com.google.gwt.regexp.shared.RegExp;
import com.google.gwt.resources.client.DataResource;
import com.google.gwt.resources.client.ImageResource;
import com.google.gwt.resources.client.ResourcePrototype;
import com.google.gwt.user.client.Window;
import com.google.gwt.xhr.client.ReadyStateChangeHandler;
import com.google.gwt.xhr.client.XMLHttpRequest;
import forplay.core.AbstractCachingAssetManager;
import forplay.core.AutoClientBundleWithLookup;
import forplay.core.ForPlay;
import forplay.core.Image;
import forplay.core.ResourceCallback;
import forplay.core.Sound;
import forplay.html.XDomainRequest.Handler;
public class HtmlAssetManager extends AbstractCachingAssetManager {
/**
* Whether or not to log successful progress of {@code XMLHTTPRequest} and
* {@code XDomainRequest} requests.
*/
private static final boolean LOG_XHR_SUCCESS = false;
private String pathPrefix = "";
public void setPathPrefix(String prefix) {
pathPrefix = prefix;
}
private Map<String, AutoClientBundleWithLookup> clientBundles = new HashMap<String, AutoClientBundleWithLookup>();
public void addClientBundle(String regExp, AutoClientBundleWithLookup clientBundle) {
clientBundles.put(regExp, clientBundle);
}
@Override
protected void doGetText(final String path, final ResourceCallback<String> callback) {
final String fullPath = pathPrefix + path;
/*
* Except for IE, all browsers support on-domain and cross-domain XHR via
* {@code XMLHTTPRequest}. IE, on the other hand, not only requires the use
* of a non-standard {@code XDomainRequest} for cross-domain requests, but
* doesn't allow on-domain requests to be issued via {@code XMLHTTPRequest},
* even when {@code Access-Control-Allow-Origin} includes the current
* document origin. Since we here don't always know if the current request
* will be cross domain, we try XHR, and then fall back to XDR if the we're
* running on IE.
*/
try {
doXhr(fullPath, callback);
} catch (JavaScriptException e) {
if (Window.Navigator.getUserAgent().indexOf("MSIE") != -1) {
doXdr(fullPath, callback);
} else {
throw e;
}
}
}
private void doXdr(final String fullPath, final ResourceCallback<String> callback) {
XDomainRequest xdr = XDomainRequest.create();
xdr.setHandler(new Handler() {
@Override
public void onTimeout(XDomainRequest xdr) {
ForPlay.log().error("xdr::onTimeout[" + fullPath + "]()");
callback.error(new RuntimeException("Error getting " + fullPath + " : " + xdr.getStatus()));
}
@Override
public void onProgress(XDomainRequest xdr) {
if (LOG_XHR_SUCCESS) {
ForPlay.log().debug("xdr::onProgress[" + fullPath + "]()");
}
}
@Override
public void onLoad(XDomainRequest xdr) {
if (LOG_XHR_SUCCESS) {
ForPlay.log().debug("xdr::onLoad[" + fullPath + "]()");
}
callback.done(xdr.getResponseText());
}
@Override
public void onError(XDomainRequest xdr) {
ForPlay.log().error("xdr::onError[" + fullPath + "]()");
callback.error(new RuntimeException("Error getting " + fullPath + " : " + xdr.getStatus()));
}
});
if (LOG_XHR_SUCCESS) {
ForPlay.log().debug("xdr.open('GET', '" + fullPath + "')...");
}
xdr.open("GET", fullPath);
if (LOG_XHR_SUCCESS) {
ForPlay.log().debug("xdr.send()...");
}
xdr.send();
}
private void doXhr(final String fullPath, final ResourceCallback<String> callback) {
XMLHttpRequest xhr = XMLHttpRequest.create();
xhr.setOnReadyStateChange(new ReadyStateChangeHandler() {
@Override
public void onReadyStateChange(XMLHttpRequest xhr) {
int readyState = xhr.getReadyState();
if (readyState == XMLHttpRequest.DONE) {
int status = xhr.getStatus();
// status code 0 will be returned for non-http requests, e.g. file://
if (status != 0 && (status < 200 || status >= 400)) {
ForPlay.log().error(
"xhr::onReadyStateChange[" + fullPath + "](readyState = " + readyState
+ "; status = " + status + ")");
callback.error(new RuntimeException("Error getting " + fullPath + " : "
+ xhr.getStatusText()));
} else {
if (LOG_XHR_SUCCESS) {
ForPlay.log().debug(
"xhr::onReadyStateChange[" + fullPath + "](readyState = " + readyState
+ "; status = " + status + ")");
}
// TODO(fredsa): Remove try-catch and materialized exception once issue 6562 is fixed
// http://code.google.com/p/google-web-toolkit/issues/detail?id=6562
try {
callback.done(xhr.getResponseText());
} catch(JavaScriptException e) {
if (GWT.isProdMode()) {
throw e;
} else {
JavaScriptException materialized = new JavaScriptException(e.getName(),
e.getDescription());
materialized.setStackTrace(e.getStackTrace());
throw materialized;
}
}
}
}
}
});
if (LOG_XHR_SUCCESS) {
ForPlay.log().debug("xhr.open('GET', '" + fullPath + "')...");
}
xhr.open("GET", fullPath);
if (LOG_XHR_SUCCESS) {
ForPlay.log().debug("xhr.send()...");
}
xhr.send();
}
@Override
protected Sound loadSound(String path) {
String url = pathPrefix + path;
AutoClientBundleWithLookup clientBundle = getBundle(path);
if (clientBundle != null) {
String key = getKey(path);
DataResource resource = (DataResource) getResource(key, clientBundle);
if (resource != null) {
url = resource.getUrl();
}
} else {
url += ".mp3";
}
return adaptSound(url);
}
private Sound adaptSound(String url) {
HtmlAudio audio = (HtmlAudio) ForPlay.audio();
HtmlSound sound = audio.createSound(url);
return sound;
}
@Override
protected Image loadImage(String path) {
String url = pathPrefix + path;
AutoClientBundleWithLookup clientBundle = getBundle(path);
if (clientBundle != null) {
String key = getKey(path);
ImageResource resource = (ImageResource) getResource(key, clientBundle);
if (resource != null) {
url = resource.getURL();
}
}
return adaptImage(url);
}
/**
* Determine the resource key from a given path.
*
* @param fullPath full path, with or without a file extension
* @return the key by which the resource can be looked up
*/
private String getKey(String fullPath) {
String key = fullPath.substring(fullPath.lastIndexOf('/') + 1);
int dotCharIdx = key.indexOf('.');
return dotCharIdx != -1 ? key.substring(0, dotCharIdx) : key;
}
private ResourcePrototype getResource(String key, AutoClientBundleWithLookup clientBundle) {
ResourcePrototype resource = clientBundle.getResource(key);
return resource;
}
private AutoClientBundleWithLookup getBundle(String collection) {
AutoClientBundleWithLookup clientBundle = null;
for (Map.Entry<String, AutoClientBundleWithLookup> entry : clientBundles.entrySet()) {
String regExp = entry.getKey();
if (RegExp.compile(regExp).exec(collection) != null) {
clientBundle = entry.getValue();
}
}
return clientBundle;
}
private Image adaptImage(String url) {
ImageElement img = Document.get().createImageElement();
/*
* When the server provides an appropriate {@literal
* Access-Control-Allow-Origin} response header, allow images to be served
* cross origin on supported, CORS enabled, browsers.
*/
setCrossOrigin(img, "anonymous");
img.setSrc(url);
return new HtmlImage(img);
}
/**
* Set the state of the {@code crossOrigin} attribute for CORS.
*
* @param elem the DOM element on which to set the {@code crossOrigin}
* attribute
* @param state one of {@code "anonymous"} or {@code "use-credentials"}
*/
private native void setCrossOrigin(Element elem, String state) /*-{
if ('crossOrigin' in elem) {
elem.setAttribute('crossOrigin', state);
}
}-*/;
}