You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ResourceLoader.java 21KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551
  1. /*
  2. * Copyright 2011 Vaadin Ltd.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  5. * use this file except in compliance with the License. You may obtain a copy of
  6. * the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  12. * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  13. * License for the specific language governing permissions and limitations under
  14. * the License.
  15. */
  16. package com.vaadin.terminal.gwt.client;
  17. import java.util.Collection;
  18. import java.util.HashMap;
  19. import java.util.HashSet;
  20. import java.util.Map;
  21. import java.util.Set;
  22. import com.google.gwt.core.client.Duration;
  23. import com.google.gwt.core.client.GWT;
  24. import com.google.gwt.core.client.Scheduler;
  25. import com.google.gwt.core.client.Scheduler.RepeatingCommand;
  26. import com.google.gwt.dom.client.AnchorElement;
  27. import com.google.gwt.dom.client.Document;
  28. import com.google.gwt.dom.client.Element;
  29. import com.google.gwt.dom.client.LinkElement;
  30. import com.google.gwt.dom.client.NodeList;
  31. import com.google.gwt.dom.client.ObjectElement;
  32. import com.google.gwt.dom.client.ScriptElement;
  33. import com.google.gwt.user.client.Timer;
  34. /**
  35. * ResourceLoader lets you dynamically include external scripts and styles on
  36. * the page and lets you know when the resource has been loaded.
  37. *
  38. * You can also preload resources, allowing them to get cached by the browser
  39. * without being evaluated. This enables downloading multiple resources at once
  40. * while still controlling in which order e.g. scripts are executed.
  41. *
  42. * @author Vaadin Ltd
  43. * @since 7.0.0
  44. */
  45. public class ResourceLoader {
  46. /**
  47. * Event fired when a resource has been loaded.
  48. */
  49. public static class ResourceLoadEvent {
  50. private ResourceLoader loader;
  51. private String resourceUrl;
  52. private final boolean preload;
  53. /**
  54. * Creates a new event.
  55. *
  56. * @param loader
  57. * the resource loader that has loaded the resource
  58. * @param resourceUrl
  59. * the url of the loaded resource
  60. * @param preload
  61. * true if the resource has only been preloaded, false if
  62. * it's fully loaded
  63. */
  64. public ResourceLoadEvent(ResourceLoader loader, String resourceUrl,
  65. boolean preload) {
  66. this.loader = loader;
  67. this.resourceUrl = resourceUrl;
  68. this.preload = preload;
  69. }
  70. /**
  71. * Gets the resource loader that has fired this event
  72. *
  73. * @return the resource loader
  74. */
  75. public ResourceLoader getResourceLoader() {
  76. return loader;
  77. }
  78. /**
  79. * Gets the absolute url of the loaded resource.
  80. *
  81. * @return the absolute url of the loaded resource
  82. */
  83. public String getResourceUrl() {
  84. return resourceUrl;
  85. }
  86. /**
  87. * Returns true if the resource has been preloaded, false if it's fully
  88. * loaded
  89. *
  90. * @see ResourceLoader#preloadResource(String, ResourceLoadListener)
  91. *
  92. * @return true if the resource has been preloaded, false if it's fully
  93. * loaded
  94. */
  95. public boolean isPreload() {
  96. return preload;
  97. }
  98. }
  99. /**
  100. * Event listener that gets notified when a resource has been loaded
  101. */
  102. public interface ResourceLoadListener {
  103. /**
  104. * Notifies this ResourceLoadListener that a resource has been loaded.
  105. * Some browsers do not support any way of detecting load errors. In
  106. * these cases, onLoad will be called regardless of the status.
  107. *
  108. * @see ResourceLoadEvent
  109. *
  110. * @param event
  111. * a resource load event with information about the loaded
  112. * resource
  113. */
  114. public void onLoad(ResourceLoadEvent event);
  115. /**
  116. * Notifies this ResourceLoadListener that a resource could not be
  117. * loaded, e.g. because the file could not be found or because the
  118. * server did not respond. Some browsers do not support any way of
  119. * detecting load errors. In these cases, onLoad will be called
  120. * regardless of the status.
  121. *
  122. * @see ResourceLoadEvent
  123. *
  124. * @param event
  125. * a resource load event with information about the resource
  126. * that could not be loaded.
  127. */
  128. public void onError(ResourceLoadEvent event);
  129. }
  130. private static final ResourceLoader INSTANCE = GWT
  131. .create(ResourceLoader.class);
  132. private ApplicationConnection connection;
  133. private final Set<String> loadedResources = new HashSet<String>();
  134. private final Set<String> preloadedResources = new HashSet<String>();
  135. private final Map<String, Collection<ResourceLoadListener>> loadListeners = new HashMap<String, Collection<ResourceLoadListener>>();
  136. private final Map<String, Collection<ResourceLoadListener>> preloadListeners = new HashMap<String, Collection<ResourceLoadListener>>();
  137. private final Element head;
  138. /**
  139. * Creates a new resource loader. You should generally not create you own
  140. * resource loader, but instead use {@link ResourceLoader#get()} to get an
  141. * instance.
  142. */
  143. protected ResourceLoader() {
  144. Document document = Document.get();
  145. head = document.getElementsByTagName("head").getItem(0);
  146. // detect already loaded scripts and stylesheets
  147. NodeList<Element> scripts = document.getElementsByTagName("script");
  148. for (int i = 0; i < scripts.getLength(); i++) {
  149. ScriptElement element = ScriptElement.as(scripts.getItem(i));
  150. String src = element.getSrc();
  151. if (src != null && src.length() != 0) {
  152. loadedResources.add(src);
  153. }
  154. }
  155. NodeList<Element> links = document.getElementsByTagName("link");
  156. for (int i = 0; i < links.getLength(); i++) {
  157. LinkElement linkElement = LinkElement.as(links.getItem(i));
  158. String rel = linkElement.getRel();
  159. String href = linkElement.getHref();
  160. if ("stylesheet".equalsIgnoreCase(rel) && href != null
  161. && href.length() != 0) {
  162. loadedResources.add(href);
  163. }
  164. }
  165. }
  166. /**
  167. * Returns the default ResourceLoader
  168. *
  169. * @return the default ResourceLoader
  170. */
  171. public static ResourceLoader get() {
  172. return INSTANCE;
  173. }
  174. /**
  175. * Load a script and notify a listener when the script is loaded. Calling
  176. * this method when the script is currently loading or already loaded
  177. * doesn't cause the script to be loaded again, but the listener will still
  178. * be notified when appropriate.
  179. *
  180. *
  181. * @param scriptUrl
  182. * the url of the script to load
  183. * @param resourceLoadListener
  184. * the listener that will get notified when the script is loaded
  185. */
  186. public void loadScript(final String scriptUrl,
  187. final ResourceLoadListener resourceLoadListener) {
  188. final String url = getAbsoluteUrl(scriptUrl);
  189. ResourceLoadEvent event = new ResourceLoadEvent(this, url, false);
  190. if (loadedResources.contains(url)) {
  191. if (resourceLoadListener != null) {
  192. resourceLoadListener.onLoad(event);
  193. }
  194. return;
  195. }
  196. if (preloadListeners.containsKey(url)) {
  197. // Preload going on, continue when preloaded
  198. preloadResource(url, new ResourceLoadListener() {
  199. @Override
  200. public void onLoad(ResourceLoadEvent event) {
  201. loadScript(url, resourceLoadListener);
  202. }
  203. @Override
  204. public void onError(ResourceLoadEvent event) {
  205. // Preload failed -> signal error to own listener
  206. if (resourceLoadListener != null) {
  207. resourceLoadListener.onError(event);
  208. }
  209. }
  210. });
  211. return;
  212. }
  213. if (addListener(url, resourceLoadListener, loadListeners)) {
  214. ScriptElement scriptTag = Document.get().createScriptElement();
  215. scriptTag.setSrc(url);
  216. scriptTag.setType("text/javascript");
  217. addOnloadHandler(scriptTag, new ResourceLoadListener() {
  218. @Override
  219. public void onLoad(ResourceLoadEvent event) {
  220. fireLoad(event);
  221. }
  222. @Override
  223. public void onError(ResourceLoadEvent event) {
  224. fireError(event);
  225. }
  226. }, event);
  227. head.appendChild(scriptTag);
  228. }
  229. }
  230. private static String getAbsoluteUrl(String url) {
  231. AnchorElement a = Document.get().createAnchorElement();
  232. a.setHref(url);
  233. return a.getHref();
  234. }
  235. /**
  236. * Download a resource and notify a listener when the resource is loaded
  237. * without attempting to interpret the resource. When a resource has been
  238. * preloaded, it will be present in the browser's cache (provided the HTTP
  239. * headers allow caching), making a subsequent load operation complete
  240. * without having to wait for the resource to be downloaded again.
  241. *
  242. * Calling this method when the resource is currently loading, currently
  243. * preloading, already preloaded or already loaded doesn't cause the
  244. * resource to be preloaded again, but the listener will still be notified
  245. * when appropriate.
  246. *
  247. * @param url
  248. * the url of the resource to preload
  249. * @param resourceLoadListener
  250. * the listener that will get notified when the resource is
  251. * preloaded
  252. */
  253. public void preloadResource(String url,
  254. ResourceLoadListener resourceLoadListener) {
  255. url = getAbsoluteUrl(url);
  256. ResourceLoadEvent event = new ResourceLoadEvent(this, url, true);
  257. if (loadedResources.contains(url) || preloadedResources.contains(url)) {
  258. // Already loaded or preloaded -> just fire listener
  259. if (resourceLoadListener != null) {
  260. resourceLoadListener.onLoad(event);
  261. }
  262. return;
  263. }
  264. if (addListener(url, resourceLoadListener, preloadListeners)
  265. && !loadListeners.containsKey(url)) {
  266. // Inject loader element if this is the first time this is preloaded
  267. // AND the resources isn't already being loaded in the normal way
  268. Element element = getPreloadElement(url);
  269. addOnloadHandler(element, new ResourceLoadListener() {
  270. @Override
  271. public void onLoad(ResourceLoadEvent event) {
  272. fireLoad(event);
  273. }
  274. @Override
  275. public void onError(ResourceLoadEvent event) {
  276. fireError(event);
  277. }
  278. }, event);
  279. // TODO Remove object when loaded (without causing spinner in FF)
  280. Document.get().getBody().appendChild(element);
  281. }
  282. }
  283. private static Element getPreloadElement(String url) {
  284. if (BrowserInfo.get().isIE()) {
  285. ScriptElement element = Document.get().createScriptElement();
  286. element.setSrc(url);
  287. element.setType("text/cache");
  288. return element;
  289. } else {
  290. ObjectElement element = Document.get().createObjectElement();
  291. element.setData(url);
  292. element.setType("text/plain");
  293. element.setHeight("0px");
  294. element.setWidth("0px");
  295. return element;
  296. }
  297. }
  298. private native void addOnloadHandler(Element element,
  299. ResourceLoadListener listener, ResourceLoadEvent event)
  300. /*-{
  301. element.onload = $entry(function() {
  302. element.onload = null;
  303. element.onerror = null;
  304. element.onreadystatechange = null;
  305. listener.@com.vaadin.terminal.gwt.client.ResourceLoader.ResourceLoadListener::onLoad(Lcom/vaadin/terminal/gwt/client/ResourceLoader$ResourceLoadEvent;)(event);
  306. });
  307. element.onerror = $entry(function() {
  308. element.onload = null;
  309. element.onerror = null;
  310. element.onreadystatechange = null;
  311. listener.@com.vaadin.terminal.gwt.client.ResourceLoader.ResourceLoadListener::onError(Lcom/vaadin/terminal/gwt/client/ResourceLoader$ResourceLoadEvent;)(event);
  312. });
  313. element.onreadystatechange = function() {
  314. if ("loaded" === element.readyState || "complete" === element.readyState ) {
  315. element.onload(arguments[0]);
  316. }
  317. };
  318. }-*/;
  319. /**
  320. * Load a stylesheet and notify a listener when the stylesheet is loaded.
  321. * Calling this method when the stylesheet is currently loading or already
  322. * loaded doesn't cause the stylesheet to be loaded again, but the listener
  323. * will still be notified when appropriate.
  324. *
  325. * @param stylesheetUrl
  326. * the url of the stylesheet to load
  327. * @param resourceLoadListener
  328. * the listener that will get notified when the stylesheet is
  329. * loaded
  330. */
  331. public void loadStylesheet(final String stylesheetUrl,
  332. final ResourceLoadListener resourceLoadListener) {
  333. final String url = getAbsoluteUrl(stylesheetUrl);
  334. final ResourceLoadEvent event = new ResourceLoadEvent(this, url, false);
  335. if (loadedResources.contains(url)) {
  336. if (resourceLoadListener != null) {
  337. resourceLoadListener.onLoad(event);
  338. }
  339. return;
  340. }
  341. if (preloadListeners.containsKey(url)) {
  342. // Preload going on, continue when preloaded
  343. preloadResource(url, new ResourceLoadListener() {
  344. @Override
  345. public void onLoad(ResourceLoadEvent event) {
  346. loadStylesheet(url, resourceLoadListener);
  347. }
  348. @Override
  349. public void onError(ResourceLoadEvent event) {
  350. // Preload failed -> signal error to own listener
  351. if (resourceLoadListener != null) {
  352. resourceLoadListener.onError(event);
  353. }
  354. }
  355. });
  356. return;
  357. }
  358. if (addListener(url, resourceLoadListener, loadListeners)) {
  359. LinkElement linkElement = Document.get().createLinkElement();
  360. linkElement.setRel("stylesheet");
  361. linkElement.setType("text/css");
  362. linkElement.setHref(url);
  363. if (BrowserInfo.get().isSafari()) {
  364. // Safari doesn't fire any events for link elements
  365. // See http://www.phpied.com/when-is-a-stylesheet-really-loaded/
  366. Scheduler.get().scheduleFixedPeriod(new RepeatingCommand() {
  367. private final Duration duration = new Duration();
  368. @Override
  369. public boolean execute() {
  370. int styleSheetLength = getStyleSheetLength(url);
  371. if (getStyleSheetLength(url) > 0) {
  372. fireLoad(event);
  373. return false; // Stop repeating
  374. } else if (styleSheetLength == 0) {
  375. // "Loaded" empty sheet -> most likely 404 error
  376. fireError(event);
  377. return true;
  378. } else if (duration.elapsedMillis() > 60 * 1000) {
  379. fireError(event);
  380. return false;
  381. } else {
  382. return true; // Continue repeating
  383. }
  384. }
  385. }, 10);
  386. } else {
  387. addOnloadHandler(linkElement, new ResourceLoadListener() {
  388. @Override
  389. public void onLoad(ResourceLoadEvent event) {
  390. // Chrome && IE fires load for errors, must check
  391. // stylesheet data
  392. if (BrowserInfo.get().isChrome()
  393. || BrowserInfo.get().isIE()) {
  394. int styleSheetLength = getStyleSheetLength(url);
  395. // Error if there's an empty stylesheet
  396. if (styleSheetLength == 0) {
  397. fireError(event);
  398. return;
  399. }
  400. }
  401. fireLoad(event);
  402. }
  403. @Override
  404. public void onError(ResourceLoadEvent event) {
  405. fireError(event);
  406. }
  407. }, event);
  408. if (BrowserInfo.get().isOpera()) {
  409. // Opera onerror never fired, assume error if no onload in x
  410. // seconds
  411. new Timer() {
  412. @Override
  413. public void run() {
  414. if (!loadedResources.contains(url)) {
  415. fireError(event);
  416. }
  417. }
  418. }.schedule(5 * 1000);
  419. }
  420. }
  421. head.appendChild(linkElement);
  422. }
  423. }
  424. private static native int getStyleSheetLength(String url)
  425. /*-{
  426. for(var i = 0; i < $doc.styleSheets.length; i++) {
  427. if ($doc.styleSheets[i].href === url) {
  428. var sheet = $doc.styleSheets[i];
  429. try {
  430. var rules = sheet.cssRules
  431. if (rules === undefined) {
  432. rules = sheet.rules;
  433. }
  434. if (rules === null) {
  435. // Style sheet loaded, but can't access length because of XSS -> assume there's something there
  436. return 1;
  437. }
  438. // Return length so we can distinguish 0 (probably 404 error) from normal case.
  439. return rules.length;
  440. } catch (err) {
  441. return 1;
  442. }
  443. }
  444. }
  445. // No matching stylesheet found -> not yet loaded
  446. return -1;
  447. }-*/;
  448. private static boolean addListener(String url,
  449. ResourceLoadListener listener,
  450. Map<String, Collection<ResourceLoadListener>> listenerMap) {
  451. Collection<ResourceLoadListener> listeners = listenerMap.get(url);
  452. if (listeners == null) {
  453. listeners = new HashSet<ResourceLoader.ResourceLoadListener>();
  454. listeners.add(listener);
  455. listenerMap.put(url, listeners);
  456. return true;
  457. } else {
  458. listeners.add(listener);
  459. return false;
  460. }
  461. }
  462. private void fireError(ResourceLoadEvent event) {
  463. String resource = event.getResourceUrl();
  464. Collection<ResourceLoadListener> listeners;
  465. if (event.isPreload()) {
  466. // Also fire error for load listeners
  467. fireError(new ResourceLoadEvent(this, resource, false));
  468. listeners = preloadListeners.remove(resource);
  469. } else {
  470. listeners = loadListeners.remove(resource);
  471. }
  472. if (listeners != null && !listeners.isEmpty()) {
  473. for (ResourceLoadListener listener : listeners) {
  474. if (listener != null) {
  475. listener.onError(event);
  476. }
  477. }
  478. }
  479. }
  480. private void fireLoad(ResourceLoadEvent event) {
  481. String resource = event.getResourceUrl();
  482. Collection<ResourceLoadListener> listeners;
  483. if (event.isPreload()) {
  484. preloadedResources.add(resource);
  485. listeners = preloadListeners.remove(resource);
  486. } else {
  487. if (preloadListeners.containsKey(resource)) {
  488. // Also fire preload events for potential listeners
  489. fireLoad(new ResourceLoadEvent(this, resource, true));
  490. }
  491. preloadedResources.remove(resource);
  492. loadedResources.add(resource);
  493. listeners = loadListeners.remove(resource);
  494. }
  495. if (listeners != null && !listeners.isEmpty()) {
  496. for (ResourceLoadListener listener : listeners) {
  497. if (listener != null) {
  498. listener.onLoad(event);
  499. }
  500. }
  501. }
  502. }
  503. }