/* * 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 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.ObjectElement; 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. * * You can also preload resources, allowing them to get cached by the browser * without being evaluated. This enables downloading multiple resources at once * while still controlling in which order e.g. scripts are executed. * * @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; private final boolean preload; /** * Creates a new event. * * @param loader * the resource loader that has loaded the resource * @param resourceUrl * the url of the loaded resource * @param preload * true if the resource has only been preloaded, false if * it's fully loaded */ public ResourceLoadEvent(ResourceLoader loader, String resourceUrl, boolean preload) { this.loader = loader; this.resourceUrl = resourceUrl; this.preload = preload; } /** * 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; } /** * Returns true if the resource has been preloaded, false if it's fully * loaded * * @see ResourceLoader#preloadResource(String, ResourceLoadListener) * * @return true if the resource has been preloaded, false if it's fully * loaded */ public boolean isPreload() { return preload; } } /** * 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 Set preloadedResources = new HashSet(); private final Map> loadListeners = new HashMap>(); private final Map> preloadListeners = 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 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); } } } /** * 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) { loadScript(scriptUrl, resourceLoadListener, !supportsInOrderScriptExecution()); } /** * 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 * url of script to load * @param resourceLoadListener * listener to notify when script is loaded * @param async * What mode the script.async attribute should be set to * @since 7.2.4 */ public void loadScript(final String scriptUrl, final ResourceLoadListener resourceLoadListener, boolean async) { final String url = WidgetUtil.getAbsoluteUrl(scriptUrl); ResourceLoadEvent event = new ResourceLoadEvent(this, url, false); if (loadedResources.contains(url)) { if (resourceLoadListener != null) { resourceLoadListener.onLoad(event); } return; } if (preloadListeners.containsKey(url)) { // Preload going on, continue when preloaded preloadResource(url, new ResourceLoadListener() { @Override public void onLoad(ResourceLoadEvent event) { loadScript(url, resourceLoadListener); } @Override public void onError(ResourceLoadEvent event) { // Preload failed -> signal error to own listener if (resourceLoadListener != null) { resourceLoadListener.onError(event); } } }); return; } if (addListener(url, resourceLoadListener, loadListeners)) { ScriptElement scriptTag = Document.get().createScriptElement(); scriptTag.setSrc(url); scriptTag.setType("text/javascript"); scriptTag.setPropertyBoolean("async", async); addOnloadHandler(scriptTag, new ResourceLoadListener() { @Override public void onLoad(ResourceLoadEvent event) { fireLoad(event); } @Override public void onError(ResourceLoadEvent event) { fireError(event); } }, event); head.appendChild(scriptTag); } } /** * The current browser supports script.async='false' for maintaining * execution order for dynamically-added scripts. * * @return Browser supports script.async='false' * @since 7.2.4 */ public static boolean supportsInOrderScriptExecution() { return BrowserInfo.get().isIE11() || BrowserInfo.get().isEdge(); } /** * Download a resource and notify a listener when the resource is loaded * without attempting to interpret the resource. When a resource has been * preloaded, it will be present in the browser's cache (provided the HTTP * headers allow caching), making a subsequent load operation complete * without having to wait for the resource to be downloaded again. * * Calling this method when the resource is currently loading, currently * preloading, already preloaded or already loaded doesn't cause the * resource to be preloaded again, but the listener will still be notified * when appropriate. * * @param url * the url of the resource to preload * @param resourceLoadListener * the listener that will get notified when the resource is * preloaded */ public void preloadResource(String url, ResourceLoadListener resourceLoadListener) { url = WidgetUtil.getAbsoluteUrl(url); ResourceLoadEvent event = new ResourceLoadEvent(this, url, true); if (loadedResources.contains(url) || preloadedResources.contains(url)) { // Already loaded or preloaded -> just fire listener if (resourceLoadListener != null) { resourceLoadListener.onLoad(event); } return; } if (addListener(url, resourceLoadListener, preloadListeners) && !loadListeners.containsKey(url)) { // Inject loader element if this is the first time this is preloaded // AND the resources isn't already being loaded in the normal way final Element element = getPreloadElement(url); addOnloadHandler(element, new ResourceLoadListener() { @Override public void onLoad(ResourceLoadEvent event) { fireLoad(event); Document.get().getBody().removeChild(element); } @Override public void onError(ResourceLoadEvent event) { fireError(event); Document.get().getBody().removeChild(element); } }, event); Document.get().getBody().appendChild(element); } } private static Element getPreloadElement(String url) { /*- * TODO * In Chrome, FF: * does not fire event if resource is 404 -> eternal spinner. * always fires onerror -> no way to know if it loaded -> eternal spinner *