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

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