All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.vaadin.client.ResourceLoader Maven / Gradle / Ivy

Go to download

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.

There is a newer version: 8.27.1
Show newest version
/*
 * Copyright 2000-2014 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().isIE()
                && BrowserInfo.get().getBrowserMajorVersion() >= 11;
    }

    /**
     * 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
         *