com.vaadin.client.ResourceLoader Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of vaadin-client Show documentation
Show all versions of vaadin-client Show documentation
Vaadin is a web application framework for Rich Internet Applications (RIA).
Vaadin enables easy development and maintenance of fast and
secure rich web
applications with a stunning look and feel and a wide browser support.
It features a server-side architecture with the majority of the logic
running
on the server. Ajax technology is used at the browser-side to ensure a
rich
and interactive user experience.
/*
* Copyright 2000-2016 Vaadin Ltd.
*
* 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.vaadin.client;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;
import com.google.gwt.core.client.Duration;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.RepeatingCommand;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.LinkElement;
import com.google.gwt.dom.client.NodeList;
import com.google.gwt.dom.client.ScriptElement;
import com.google.gwt.user.client.Timer;
/**
* ResourceLoader lets you dynamically include external scripts and styles on
* the page and lets you know when the resource has been loaded.
*
* @author Vaadin Ltd
* @since 7.0.0
*/
public class ResourceLoader {
/**
* Event fired when a resource has been loaded.
*/
public static class ResourceLoadEvent {
private final ResourceLoader loader;
private final String resourceUrl;
/**
* Creates a new event.
*
* @param loader
* the resource loader that has loaded the resource
* @param resourceUrl
* the url of the loaded resource
*/
public ResourceLoadEvent(ResourceLoader loader, String resourceUrl) {
this.loader = loader;
this.resourceUrl = resourceUrl;
}
/**
* Gets the resource loader that has fired this event.
*
* @return the resource loader
*/
public ResourceLoader getResourceLoader() {
return loader;
}
/**
* Gets the absolute url of the loaded resource.
*
* @return the absolute url of the loaded resource
*/
public String getResourceUrl() {
return resourceUrl;
}
}
/**
* Event listener that gets notified when a resource has been loaded.
*/
public interface ResourceLoadListener {
/**
* Notifies this ResourceLoadListener that a resource has been loaded.
* Some browsers do not support any way of detecting load errors. In
* these cases, onLoad will be called regardless of the status.
*
* @see ResourceLoadEvent
*
* @param event
* a resource load event with information about the loaded
* resource
*/
public void onLoad(ResourceLoadEvent event);
/**
* Notifies this ResourceLoadListener that a resource could not be
* loaded, e.g. because the file could not be found or because the
* server did not respond. Some browsers do not support any way of
* detecting load errors. In these cases, onLoad will be called
* regardless of the status.
*
* @see ResourceLoadEvent
*
* @param event
* a resource load event with information about the resource
* that could not be loaded.
*/
public void onError(ResourceLoadEvent event);
}
private static final ResourceLoader INSTANCE = GWT
.create(ResourceLoader.class);
private ApplicationConnection connection;
private final Set loadedResources = new HashSet<>();
private final Map> loadListeners = new HashMap<>();
private final Element head;
/**
* Creates a new resource loader. You should generally not create you own
* resource loader, but instead use {@link ResourceLoader#get()} to get an
* instance.
*/
protected ResourceLoader() {
Document document = Document.get();
head = document.getElementsByTagName("head").getItem(0);
// detect already loaded scripts, html imports and stylesheets
NodeList scripts = document.getElementsByTagName("script");
for (int i = 0; i < scripts.getLength(); i++) {
ScriptElement element = ScriptElement.as(scripts.getItem(i));
String src = element.getSrc();
if (src != null && src.length() != 0) {
loadedResources.add(src);
}
}
NodeList links = document.getElementsByTagName("link");
for (int i = 0; i < links.getLength(); i++) {
LinkElement linkElement = LinkElement.as(links.getItem(i));
String rel = linkElement.getRel();
String href = linkElement.getHref();
if ("stylesheet".equalsIgnoreCase(rel) && href != null
&& href.length() != 0) {
loadedResources.add(href);
}
if ("import".equalsIgnoreCase(rel) && href != null
&& href.length() != 0) {
loadedResources.add(href);
}
}
}
/**
* Returns the default ResourceLoader
*
* @return the default ResourceLoader
*/
public static ResourceLoader get() {
return INSTANCE;
}
/**
* Load a script and notify a listener when the script is loaded. Calling
* this method when the script is currently loading or already loaded
* doesn't cause the script to be loaded again, but the listener will still
* be notified when appropriate.
*
* @param scriptUrl
* the url of the script to load
* @param resourceLoadListener
* the listener that will get notified when the script is loaded
*/
public void loadScript(final String scriptUrl,
final ResourceLoadListener resourceLoadListener) {
final String url = WidgetUtil.getAbsoluteUrl(scriptUrl);
ResourceLoadEvent event = new ResourceLoadEvent(this, url);
if (loadedResources.contains(url)) {
if (resourceLoadListener != null) {
resourceLoadListener.onLoad(event);
}
return;
}
if (addListener(url, resourceLoadListener, loadListeners)) {
getLogger().info("Loading script from " + url);
ScriptElement scriptTag = Document.get().createScriptElement();
scriptTag.setSrc(url);
scriptTag.setType("text/javascript");
// async=false causes script injected scripts to be executed in the
// injection order. See e.g.
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script
scriptTag.setPropertyBoolean("async", false);
addOnloadHandler(scriptTag, new ResourceLoadListener() {
@Override
public void onLoad(ResourceLoadEvent event) {
fireLoad(event);
}
@Override
public void onError(ResourceLoadEvent event) {
fireError(event);
}
}, event);
head.appendChild(scriptTag);
}
}
/**
* Loads an HTML import and notify a listener when the HTML import is
* loaded. Calling this method when the HTML import is currently loading or
* already loaded doesn't cause the HTML import to be loaded again, but the
* listener will still be notified when appropriate.
*
* @param htmlUrl
* url of HTML import to load
* @param resourceLoadListener
* listener to notify when the HTML import is loaded
*/
public void loadHtmlImport(final String htmlUrl,
final ResourceLoadListener resourceLoadListener) {
final String url = WidgetUtil.getAbsoluteUrl(htmlUrl);
ResourceLoadEvent event = new ResourceLoadEvent(this, url);
if (loadedResources.contains(url)) {
if (resourceLoadListener != null) {
resourceLoadListener.onLoad(event);
}
return;
}
if (addListener(url, resourceLoadListener, loadListeners)) {
LinkElement linkTag = Document.get().createLinkElement();
linkTag.setAttribute("rel", "import");
linkTag.setAttribute("href", url);
addOnloadHandler(linkTag, new ResourceLoadListener() {
@Override
public void onLoad(ResourceLoadEvent event) {
fireLoad(event);
}
@Override
public void onError(ResourceLoadEvent event) {
fireError(event);
}
}, event);
head.appendChild(linkTag);
}
}
/**
* Adds an onload listener to the given element, which should be a link or a
* script tag. The listener is called whenever loading is complete or an
* error occurred.
*
* @since 7.3
* @param element
* the element to attach a listener to
* @param listener
* the listener to call
* @param event
* the event passed to the listener
*/
public static native void addOnloadHandler(Element element,
ResourceLoadListener listener, ResourceLoadEvent event)
/*-{
element.onload = $entry(function() {
element.onload = null;
element.onerror = null;
element.onreadystatechange = null;
[email protected]::onLoad(Lcom/vaadin/client/ResourceLoader$ResourceLoadEvent;)(event);
});
element.onerror = $entry(function() {
element.onload = null;
element.onerror = null;
element.onreadystatechange = null;
[email protected]::onError(Lcom/vaadin/client/ResourceLoader$ResourceLoadEvent;)(event);
});
element.onreadystatechange = function() {
if ("loaded" === element.readyState || "complete" === element.readyState ) {
element.onload(arguments[0]);
}
};
}-*/;
/**
* Load a stylesheet and notify a listener when the stylesheet is loaded.
* Calling this method when the stylesheet is currently loading or already
* loaded doesn't cause the stylesheet to be loaded again, but the listener
* will still be notified when appropriate.
*
* @param stylesheetUrl
* the url of the stylesheet to load
* @param resourceLoadListener
* the listener that will get notified when the stylesheet is
* loaded
*/
public void loadStylesheet(final String stylesheetUrl,
final ResourceLoadListener resourceLoadListener) {
final String url = WidgetUtil.getAbsoluteUrl(stylesheetUrl);
final ResourceLoadEvent event = new ResourceLoadEvent(this, url);
if (loadedResources.contains(url)) {
if (resourceLoadListener != null) {
resourceLoadListener.onLoad(event);
}
return;
}
if (addListener(url, resourceLoadListener, loadListeners)) {
getLogger().info("Loading style sheet from " + url);
LinkElement linkElement = Document.get().createLinkElement();
linkElement.setRel("stylesheet");
linkElement.setType("text/css");
linkElement.setHref(url);
if (BrowserInfo.get().isSafari()) {
// Safari doesn't fire any events for link elements
// See http://www.phpied.com/when-is-a-stylesheet-really-loaded/
Scheduler.get().scheduleFixedPeriod(new RepeatingCommand() {
private final Duration duration = new Duration();
@Override
public boolean execute() {
int styleSheetLength = getStyleSheetLength(url);
if (getStyleSheetLength(url) > 0) {
fireLoad(event);
return false; // Stop repeating
} else if (styleSheetLength == 0) {
// "Loaded" empty sheet -> most likely 404 error
fireError(event);
return true;
} else if (duration.elapsedMillis() > 60 * 1000) {
fireError(event);
return false;
} else {
return true; // Continue repeating
}
}
}, 10);
} else {
addOnloadHandler(linkElement, new ResourceLoadListener() {
@Override
public void onLoad(ResourceLoadEvent event) {
// Chrome, IE, Edge all fire load for errors, must check
// stylesheet data
if (BrowserInfo.get().isChrome()
|| BrowserInfo.get().isIE()
|| BrowserInfo.get().isEdge()) {
int styleSheetLength = getStyleSheetLength(url);
// Error if there's an empty stylesheet
if (styleSheetLength == 0) {
fireError(event);
return;
}
}
fireLoad(event);
}
@Override
public void onError(ResourceLoadEvent event) {
fireError(event);
}
}, event);
if (BrowserInfo.get().isOpera()) {
// Opera onerror never fired, assume error if no onload in x
// seconds
new Timer() {
@Override
public void run() {
if (!loadedResources.contains(url)) {
fireError(event);
}
}
}.schedule(5 * 1000);
}
}
head.appendChild(linkElement);
}
}
private static native int getStyleSheetLength(String url)
/*-{
for(var i = 0; i < $doc.styleSheets.length; i++) {
if ($doc.styleSheets[i].href === url) {
var sheet = $doc.styleSheets[i];
try {
var rules = sheet.cssRules
if (rules === undefined) {
rules = sheet.rules;
}
if (rules === null) {
// Style sheet loaded, but can't access length because of XSS -> assume there's something there
return 1;
}
// Return length so we can distinguish 0 (probably 404 error) from normal case.
return rules.length;
} catch (err) {
return 1;
}
}
}
// No matching stylesheet found -> not yet loaded
return -1;
}-*/;
private static boolean addListener(String url,
ResourceLoadListener listener,
Map> listenerMap) {
Collection listeners = listenerMap.get(url);
if (listeners == null) {
listeners = new HashSet<>();
listeners.add(listener);
listenerMap.put(url, listeners);
return true;
} else {
listeners.add(listener);
return false;
}
}
private void fireError(ResourceLoadEvent event) {
String resource = event.getResourceUrl();
Collection listeners = loadListeners
.remove(resource);
if (listeners != null && !listeners.isEmpty()) {
for (ResourceLoadListener listener : listeners) {
if (listener != null) {
listener.onError(event);
}
}
}
}
private void fireLoad(ResourceLoadEvent event) {
String resource = event.getResourceUrl();
Collection listeners = loadListeners
.remove(resource);
loadedResources.add(resource);
if (listeners != null && !listeners.isEmpty()) {
for (ResourceLoadListener listener : listeners) {
if (listener != null) {
listener.onLoad(event);
}
}
}
}
private static Logger getLogger() {
return Logger.getLogger(ResourceLoader.class.getName());
}
}