]> source.dussan.org Git - vaadin-framework.git/commitdiff
Add error handling to ResourceLoader (#9044)
authorLeif Åstrand <leif@vaadin.com>
Thu, 28 Jun 2012 08:47:55 +0000 (11:47 +0300)
committerLeif Åstrand <leif@vaadin.com>
Thu, 28 Jun 2012 08:52:13 +0000 (11:52 +0300)
src/com/vaadin/terminal/gwt/client/ApplicationConnection.java
src/com/vaadin/terminal/gwt/client/ResourceLoader.java

index ab9520240c09ef68deb98fbe50682144056eda10..c6320f941b9bf7dca1fb16ee2037231c367cfd23 100644 (file)
@@ -1624,9 +1624,16 @@ public class ApplicationConnection {
     private static void loadStyleDependencies(JsArrayString dependencies) {
         // Assuming no reason to interpret in a defined order
         ResourceLoadListener resourceLoadListener = new ResourceLoadListener() {
-            public void onResourceLoad(ResourceLoadEvent event) {
+            public void onLoad(ResourceLoadEvent event) {
                 ApplicationConfiguration.endDependencyLoading();
             }
+
+            public void onError(ResourceLoadEvent event) {
+                VConsole.error(event.getResourceUrl()
+                        + " could not be loaded, or the load detection failed because the stylesheet is empty.");
+                // The show must go on
+                onLoad(event);
+            }
         };
         ResourceLoader loader = ResourceLoader.get();
         for (int i = 0; i < dependencies.length(); i++) {
@@ -1642,7 +1649,7 @@ public class ApplicationConnection {
 
         // Listener that loads the next when one is completed
         ResourceLoadListener resourceLoadListener = new ResourceLoadListener() {
-            public void onResourceLoad(ResourceLoadEvent event) {
+            public void onLoad(ResourceLoadEvent event) {
                 if (dependencies.length() != 0) {
                     ApplicationConfiguration.startDependencyLoading();
                     // Load next in chain (hopefully already preloaded)
@@ -1652,6 +1659,12 @@ public class ApplicationConnection {
                 // Call start for next before calling end for current
                 ApplicationConfiguration.endDependencyLoading();
             }
+
+            public void onError(ResourceLoadEvent event) {
+                VConsole.error(event.getResourceUrl() + " could not be loaded.");
+                // The show must go on
+                onLoad(event);
+            }
         };
 
         ResourceLoader loader = ResourceLoader.get();
index 7abafbc216920948b3c244b3c1b3eb0934726732..8c481e03566b25282dca251a0eaa9ee5dbc0b952 100644 (file)
@@ -10,6 +10,7 @@ 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;
@@ -20,6 +21,7 @@ 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
@@ -97,7 +99,9 @@ public class ResourceLoader {
      */
     public interface ResourceLoadListener {
         /**
-         * Notified this ResourceLoadListener that a resource has been loaded
+         * 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
          * 
@@ -105,7 +109,22 @@ public class ResourceLoader {
          *            a resource load event with information about the loaded
          *            resource
          */
-        public void onResourceLoad(ResourceLoadEvent event);
+        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
@@ -176,19 +195,27 @@ public class ResourceLoader {
     public void loadScript(final String scriptUrl,
             final ResourceLoadListener resourceLoadListener) {
         final String url = getAbsoluteUrl(scriptUrl);
+        ResourceLoadEvent event = new ResourceLoadEvent(this, url, false);
         if (loadedResources.contains(url)) {
             if (resourceLoadListener != null) {
-                resourceLoadListener.onResourceLoad(new ResourceLoadEvent(this,
-                        url, false));
+                resourceLoadListener.onLoad(event);
             }
             return;
         }
 
         if (preloadListeners.containsKey(url)) {
+            // Preload going on, continue when preloaded
             preloadResource(url, new ResourceLoadListener() {
-                public void onResourceLoad(ResourceLoadEvent event) {
+                public void onLoad(ResourceLoadEvent event) {
                     loadScript(url, resourceLoadListener);
                 }
+
+                public void onError(ResourceLoadEvent event) {
+                    // Preload failed -> signal error to own listener
+                    if (resourceLoadListener != null) {
+                        resourceLoadListener.onError(event);
+                    }
+                }
             });
             return;
         }
@@ -197,7 +224,15 @@ public class ResourceLoader {
             ScriptElement scriptTag = Document.get().createScriptElement();
             scriptTag.setSrc(url);
             scriptTag.setType("text/javascript");
-            addOnloadHandler(scriptTag, url, false);
+            addOnloadHandler(scriptTag, new ResourceLoadListener() {
+                public void onLoad(ResourceLoadEvent event) {
+                    fireLoad(event);
+                }
+
+                public void onError(ResourceLoadEvent event) {
+                    fireError(event);
+                }
+            }, event);
             head.appendChild(scriptTag);
         }
     }
@@ -229,10 +264,11 @@ public class ResourceLoader {
     public void preloadResource(String url,
             ResourceLoadListener resourceLoadListener) {
         url = 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.onResourceLoad(new ResourceLoadEvent(this,
-                        url, !loadedResources.contains(url)));
+                resourceLoadListener.onLoad(event);
             }
             return;
         }
@@ -243,7 +279,15 @@ public class ResourceLoader {
             // AND the resources isn't already being loaded in the normal way
 
             Element element = getPreloadElement(url);
-            addOnloadHandler(element, url, true);
+            addOnloadHandler(element, new ResourceLoadListener() {
+                public void onLoad(ResourceLoadEvent event) {
+                    fireLoad(event);
+                }
+
+                public void onError(ResourceLoadEvent event) {
+                    fireError(event);
+                }
+            }, event);
 
             // TODO Remove object when loaded (without causing spinner in FF)
             Document.get().getBody().appendChild(element);
@@ -266,24 +310,24 @@ public class ResourceLoader {
         }
     }
 
-    private native void addOnloadHandler(Element element, String url,
-            boolean preload)
+    private native void addOnloadHandler(Element element,
+            ResourceLoadListener listener, ResourceLoadEvent event)
     /*-{
-        var self = this;
-        var done = $entry(function() {
-            element.onloadDone = true;
+        element.onload = $entry(function() {
             element.onload = null;
+            element.onerror = null;
             element.onreadystatechange = null;
-            self.@com.vaadin.terminal.gwt.client.ResourceLoader::onResourceLoad(Ljava/lang/String;Z)(url, preload);
+            listener.@com.vaadin.terminal.gwt.client.ResourceLoader.ResourceLoadListener::onLoad(Lcom/vaadin/terminal/gwt/client/ResourceLoader$ResourceLoadEvent;)(event);
+        });
+        element.onerror = $entry(function() {
+            element.onload = null;
+            element.onerror = null;
+            element.onreadystatechange = null;
+            listener.@com.vaadin.terminal.gwt.client.ResourceLoader.ResourceLoadListener::onError(Lcom/vaadin/terminal/gwt/client/ResourceLoader$ResourceLoadEvent;)(event);
         });
-        element.onload = function() {
-            if (!element.onloadDone) {
-                done(); 
-            }
-        };
         element.onreadystatechange = function() { 
-            if (("loaded" === element.readyState || "complete" === element.readyState) && !element.onloadDone ) {
-                done();
+            if ("loaded" === element.readyState || "complete" === element.readyState ) {
+                element.onload(arguments[0]);
             }
         };       
     }-*/;
@@ -303,19 +347,27 @@ public class ResourceLoader {
     public void loadStylesheet(final String stylesheetUrl,
             final ResourceLoadListener resourceLoadListener) {
         final String url = getAbsoluteUrl(stylesheetUrl);
+        final ResourceLoadEvent event = new ResourceLoadEvent(this, url, false);
         if (loadedResources.contains(url)) {
             if (resourceLoadListener != null) {
-                resourceLoadListener.onResourceLoad(new ResourceLoadEvent(this,
-                        url, false));
+                resourceLoadListener.onLoad(event);
             }
             return;
         }
 
         if (preloadListeners.containsKey(url)) {
+            // Preload going on, continue when preloaded
             preloadResource(url, new ResourceLoadListener() {
-                public void onResourceLoad(ResourceLoadEvent event) {
+                public void onLoad(ResourceLoadEvent event) {
                     loadStylesheet(url, resourceLoadListener);
                 }
+
+                public void onError(ResourceLoadEvent event) {
+                    // Preload failed -> signal error to own listener
+                    if (resourceLoadListener != null) {
+                        resourceLoadListener.onError(event);
+                    }
+                }
             });
             return;
         }
@@ -327,35 +379,92 @@ public class ResourceLoader {
             linkElement.setHref(url);
 
             if (BrowserInfo.get().isSafari()) {
-                // Safari doesn't fire onload events for link elements
+                // Safari doesn't fire any events for link elements
                 // See http://www.phpied.com/when-is-a-stylesheet-really-loaded/
-                // TODO Stop checking after some timeout
                 Scheduler.get().scheduleFixedPeriod(new RepeatingCommand() {
+                    private final Duration duration = new Duration();
+
                     public boolean execute() {
-                        if (isStyleSheetPresent(url)) {
-                            onResourceLoad(url, false);
+                        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, url, false);
+                addOnloadHandler(linkElement, new ResourceLoadListener() {
+                    public void onLoad(ResourceLoadEvent event) {
+                        // Chrome && IE fires load for errors, must check
+                        // stylesheet data
+                        if (BrowserInfo.get().isChrome()
+                                || BrowserInfo.get().isIE()) {
+                            int styleSheetLength = getStyleSheetLength(url);
+                            // Error if there's an empty stylesheet
+                            if (styleSheetLength == 0) {
+                                fireError(event);
+                                return;
+                            }
+                        }
+                        fireLoad(event);
+                    }
+
+                    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 boolean isStyleSheetPresent(String url)
+    private static native int getStyleSheetLength(String url)
     /*-{
         for(var i = 0; i < $doc.styleSheets.length; i++) {
             if ($doc.styleSheets[i].href === url) {
-                return true;
+                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;
+                }
             }
         }
-        return false;
+        // No matching stylesheet found -> not yet loaded
+        return -1;
     }-*/;
 
     private static boolean addListener(String url,
@@ -373,26 +482,45 @@ public class ResourceLoader {
         }
     }
 
-    private void onResourceLoad(String resource, boolean preload) {
+    private void fireError(ResourceLoadEvent event) {
+        String resource = event.getResourceUrl();
+
+        Collection<ResourceLoadListener> listeners;
+        if (event.isPreload()) {
+            // Also fire error for load listeners
+            fireError(new ResourceLoadEvent(this, resource, false));
+            listeners = preloadListeners.remove(resource);
+        } else {
+            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<ResourceLoadListener> listeners;
-        if (preload) {
+        if (event.isPreload()) {
             preloadedResources.add(resource);
             listeners = preloadListeners.remove(resource);
         } else {
             if (preloadListeners.containsKey(resource)) {
                 // Also fire preload events for potential listeners
-                onResourceLoad(resource, true);
+                fireLoad(new ResourceLoadEvent(this, resource, true));
             }
             preloadedResources.remove(resource);
             loadedResources.add(resource);
             listeners = loadListeners.remove(resource);
         }
         if (listeners != null && !listeners.isEmpty()) {
-            ResourceLoadEvent event = new ResourceLoadEvent(this, resource,
-                    preload);
             for (ResourceLoadListener listener : listeners) {
                 if (listener != null) {
-                    listener.onResourceLoad(event);
+                    listener.onLoad(event);
                 }
             }
         }