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 22KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599
  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. loadScript(scriptUrl, resourceLoadListener,
  188. !supportsInOrderScriptExecution());
  189. }
  190. /**
  191. * Load a script and notify a listener when the script is loaded. Calling
  192. * this method when the script is currently loading or already loaded
  193. * doesn't cause the script to be loaded again, but the listener will still
  194. * be notified when appropriate.
  195. *
  196. *
  197. * @param scriptUrl
  198. * url of script to load
  199. * @param resourceLoadListener
  200. * listener to notify when script is loaded
  201. * @param async
  202. * What mode the script.async attribute should be set to
  203. * @since 7.2.4
  204. */
  205. public void loadScript(final String scriptUrl,
  206. final ResourceLoadListener resourceLoadListener, boolean async) {
  207. final String url = Util.getAbsoluteUrl(scriptUrl);
  208. ResourceLoadEvent event = new ResourceLoadEvent(this, url, false);
  209. if (loadedResources.contains(url)) {
  210. if (resourceLoadListener != null) {
  211. resourceLoadListener.onLoad(event);
  212. }
  213. return;
  214. }
  215. if (preloadListeners.containsKey(url)) {
  216. // Preload going on, continue when preloaded
  217. preloadResource(url, new ResourceLoadListener() {
  218. @Override
  219. public void onLoad(ResourceLoadEvent event) {
  220. loadScript(url, resourceLoadListener);
  221. }
  222. @Override
  223. public void onError(ResourceLoadEvent event) {
  224. // Preload failed -> signal error to own listener
  225. if (resourceLoadListener != null) {
  226. resourceLoadListener.onError(event);
  227. }
  228. }
  229. });
  230. return;
  231. }
  232. if (addListener(url, resourceLoadListener, loadListeners)) {
  233. ScriptElement scriptTag = Document.get().createScriptElement();
  234. scriptTag.setSrc(url);
  235. scriptTag.setType("text/javascript");
  236. scriptTag.setPropertyBoolean("async", async);
  237. addOnloadHandler(scriptTag, new ResourceLoadListener() {
  238. @Override
  239. public void onLoad(ResourceLoadEvent event) {
  240. fireLoad(event);
  241. }
  242. @Override
  243. public void onError(ResourceLoadEvent event) {
  244. fireError(event);
  245. }
  246. }, event);
  247. head.appendChild(scriptTag);
  248. }
  249. }
  250. /**
  251. * The current browser supports script.async='false' for maintaining
  252. * execution order for dynamically-added scripts.
  253. *
  254. * @return Browser supports script.async='false'
  255. * @since 7.2.4
  256. */
  257. public static boolean supportsInOrderScriptExecution() {
  258. return BrowserInfo.get().isIE()
  259. && BrowserInfo.get().getBrowserMajorVersion() >= 11;
  260. }
  261. /**
  262. * Download a resource and notify a listener when the resource is loaded
  263. * without attempting to interpret the resource. When a resource has been
  264. * preloaded, it will be present in the browser's cache (provided the HTTP
  265. * headers allow caching), making a subsequent load operation complete
  266. * without having to wait for the resource to be downloaded again.
  267. *
  268. * Calling this method when the resource is currently loading, currently
  269. * preloading, already preloaded or already loaded doesn't cause the
  270. * resource to be preloaded again, but the listener will still be notified
  271. * when appropriate.
  272. *
  273. * @param url
  274. * the url of the resource to preload
  275. * @param resourceLoadListener
  276. * the listener that will get notified when the resource is
  277. * preloaded
  278. */
  279. public void preloadResource(String url,
  280. ResourceLoadListener resourceLoadListener) {
  281. url = Util.getAbsoluteUrl(url);
  282. ResourceLoadEvent event = new ResourceLoadEvent(this, url, true);
  283. if (loadedResources.contains(url) || preloadedResources.contains(url)) {
  284. // Already loaded or preloaded -> just fire listener
  285. if (resourceLoadListener != null) {
  286. resourceLoadListener.onLoad(event);
  287. }
  288. return;
  289. }
  290. if (addListener(url, resourceLoadListener, preloadListeners)
  291. && !loadListeners.containsKey(url)) {
  292. // Inject loader element if this is the first time this is preloaded
  293. // AND the resources isn't already being loaded in the normal way
  294. final Element element = getPreloadElement(url);
  295. addOnloadHandler(element, new ResourceLoadListener() {
  296. @Override
  297. public void onLoad(ResourceLoadEvent event) {
  298. fireLoad(event);
  299. Document.get().getBody().removeChild(element);
  300. }
  301. @Override
  302. public void onError(ResourceLoadEvent event) {
  303. fireError(event);
  304. Document.get().getBody().removeChild(element);
  305. }
  306. }, event);
  307. Document.get().getBody().appendChild(element);
  308. }
  309. }
  310. private static Element getPreloadElement(String url) {
  311. /*-
  312. * TODO
  313. * In Chrome, FF:
  314. * <object> does not fire event if resource is 404 -> eternal spinner.
  315. * <img> always fires onerror -> no way to know if it loaded -> eternal spinner
  316. * <script type="text/javascript> fires, but also executes -> not preloading
  317. * <script type="text/cache"> does not fire events
  318. * XHR not tested - should work, probably causes other issues
  319. -*/
  320. if (BrowserInfo.get().isIE()) {
  321. // If ie11+ for some reason gets a preload request
  322. if (BrowserInfo.get().getBrowserMajorVersion() >= 11) {
  323. throw new RuntimeException(
  324. "Browser doesn't support preloading with text/cache");
  325. }
  326. ScriptElement element = Document.get().createScriptElement();
  327. element.setSrc(url);
  328. element.setType("text/cache");
  329. return element;
  330. } else {
  331. ObjectElement element = Document.get().createObjectElement();
  332. element.setData(url);
  333. if (BrowserInfo.get().isChrome()) {
  334. element.setType("text/cache");
  335. } else {
  336. element.setType("text/plain");
  337. }
  338. element.setHeight("0px");
  339. element.setWidth("0px");
  340. return element;
  341. }
  342. }
  343. private native void addOnloadHandler(Element element,
  344. ResourceLoadListener listener, ResourceLoadEvent event)
  345. /*-{
  346. element.onload = $entry(function() {
  347. element.onload = null;
  348. element.onerror = null;
  349. element.onreadystatechange = null;
  350. listener.@com.vaadin.client.ResourceLoader.ResourceLoadListener::onLoad(Lcom/vaadin/client/ResourceLoader$ResourceLoadEvent;)(event);
  351. });
  352. element.onerror = $entry(function() {
  353. element.onload = null;
  354. element.onerror = null;
  355. element.onreadystatechange = null;
  356. listener.@com.vaadin.client.ResourceLoader.ResourceLoadListener::onError(Lcom/vaadin/client/ResourceLoader$ResourceLoadEvent;)(event);
  357. });
  358. element.onreadystatechange = function() {
  359. if ("loaded" === element.readyState || "complete" === element.readyState ) {
  360. element.onload(arguments[0]);
  361. }
  362. };
  363. }-*/;
  364. /**
  365. * Load a stylesheet and notify a listener when the stylesheet is loaded.
  366. * Calling this method when the stylesheet is currently loading or already
  367. * loaded doesn't cause the stylesheet to be loaded again, but the listener
  368. * will still be notified when appropriate.
  369. *
  370. * @param stylesheetUrl
  371. * the url of the stylesheet to load
  372. * @param resourceLoadListener
  373. * the listener that will get notified when the stylesheet is
  374. * loaded
  375. */
  376. public void loadStylesheet(final String stylesheetUrl,
  377. final ResourceLoadListener resourceLoadListener) {
  378. final String url = Util.getAbsoluteUrl(stylesheetUrl);
  379. final ResourceLoadEvent event = new ResourceLoadEvent(this, url, false);
  380. if (loadedResources.contains(url)) {
  381. if (resourceLoadListener != null) {
  382. resourceLoadListener.onLoad(event);
  383. }
  384. return;
  385. }
  386. if (preloadListeners.containsKey(url)) {
  387. // Preload going on, continue when preloaded
  388. preloadResource(url, new ResourceLoadListener() {
  389. @Override
  390. public void onLoad(ResourceLoadEvent event) {
  391. loadStylesheet(url, resourceLoadListener);
  392. }
  393. @Override
  394. public void onError(ResourceLoadEvent event) {
  395. // Preload failed -> signal error to own listener
  396. if (resourceLoadListener != null) {
  397. resourceLoadListener.onError(event);
  398. }
  399. }
  400. });
  401. return;
  402. }
  403. if (addListener(url, resourceLoadListener, loadListeners)) {
  404. LinkElement linkElement = Document.get().createLinkElement();
  405. linkElement.setRel("stylesheet");
  406. linkElement.setType("text/css");
  407. linkElement.setHref(url);
  408. if (BrowserInfo.get().isSafari()) {
  409. // Safari doesn't fire any events for link elements
  410. // See http://www.phpied.com/when-is-a-stylesheet-really-loaded/
  411. Scheduler.get().scheduleFixedPeriod(new RepeatingCommand() {
  412. private final Duration duration = new Duration();
  413. @Override
  414. public boolean execute() {
  415. int styleSheetLength = getStyleSheetLength(url);
  416. if (getStyleSheetLength(url) > 0) {
  417. fireLoad(event);
  418. return false; // Stop repeating
  419. } else if (styleSheetLength == 0) {
  420. // "Loaded" empty sheet -> most likely 404 error
  421. fireError(event);
  422. return true;
  423. } else if (duration.elapsedMillis() > 60 * 1000) {
  424. fireError(event);
  425. return false;
  426. } else {
  427. return true; // Continue repeating
  428. }
  429. }
  430. }, 10);
  431. } else {
  432. addOnloadHandler(linkElement, new ResourceLoadListener() {
  433. @Override
  434. public void onLoad(ResourceLoadEvent event) {
  435. // Chrome && IE fires load for errors, must check
  436. // stylesheet data
  437. if (BrowserInfo.get().isChrome()
  438. || BrowserInfo.get().isIE()) {
  439. int styleSheetLength = getStyleSheetLength(url);
  440. // Error if there's an empty stylesheet
  441. if (styleSheetLength == 0) {
  442. fireError(event);
  443. return;
  444. }
  445. }
  446. fireLoad(event);
  447. }
  448. @Override
  449. public void onError(ResourceLoadEvent event) {
  450. fireError(event);
  451. }
  452. }, event);
  453. if (BrowserInfo.get().isOpera()) {
  454. // Opera onerror never fired, assume error if no onload in x
  455. // seconds
  456. new Timer() {
  457. @Override
  458. public void run() {
  459. if (!loadedResources.contains(url)) {
  460. fireError(event);
  461. }
  462. }
  463. }.schedule(5 * 1000);
  464. }
  465. }
  466. head.appendChild(linkElement);
  467. }
  468. }
  469. private static native int getStyleSheetLength(String url)
  470. /*-{
  471. for(var i = 0; i < $doc.styleSheets.length; i++) {
  472. if ($doc.styleSheets[i].href === url) {
  473. var sheet = $doc.styleSheets[i];
  474. try {
  475. var rules = sheet.cssRules
  476. if (rules === undefined) {
  477. rules = sheet.rules;
  478. }
  479. if (rules === null) {
  480. // Style sheet loaded, but can't access length because of XSS -> assume there's something there
  481. return 1;
  482. }
  483. // Return length so we can distinguish 0 (probably 404 error) from normal case.
  484. return rules.length;
  485. } catch (err) {
  486. return 1;
  487. }
  488. }
  489. }
  490. // No matching stylesheet found -> not yet loaded
  491. return -1;
  492. }-*/;
  493. private static boolean addListener(String url,
  494. ResourceLoadListener listener,
  495. Map<String, Collection<ResourceLoadListener>> listenerMap) {
  496. Collection<ResourceLoadListener> listeners = listenerMap.get(url);
  497. if (listeners == null) {
  498. listeners = new HashSet<ResourceLoader.ResourceLoadListener>();
  499. listeners.add(listener);
  500. listenerMap.put(url, listeners);
  501. return true;
  502. } else {
  503. listeners.add(listener);
  504. return false;
  505. }
  506. }
  507. private void fireError(ResourceLoadEvent event) {
  508. String resource = event.getResourceUrl();
  509. Collection<ResourceLoadListener> listeners;
  510. if (event.isPreload()) {
  511. // Also fire error for load listeners
  512. fireError(new ResourceLoadEvent(this, resource, false));
  513. listeners = preloadListeners.remove(resource);
  514. } else {
  515. listeners = loadListeners.remove(resource);
  516. }
  517. if (listeners != null && !listeners.isEmpty()) {
  518. for (ResourceLoadListener listener : listeners) {
  519. if (listener != null) {
  520. listener.onError(event);
  521. }
  522. }
  523. }
  524. }
  525. private void fireLoad(ResourceLoadEvent event) {
  526. String resource = event.getResourceUrl();
  527. Collection<ResourceLoadListener> listeners;
  528. if (event.isPreload()) {
  529. preloadedResources.add(resource);
  530. listeners = preloadListeners.remove(resource);
  531. } else {
  532. if (preloadListeners.containsKey(resource)) {
  533. // Also fire preload events for potential listeners
  534. fireLoad(new ResourceLoadEvent(this, resource, true));
  535. }
  536. preloadedResources.remove(resource);
  537. loadedResources.add(resource);
  538. listeners = loadListeners.remove(resource);
  539. }
  540. if (listeners != null && !listeners.isEmpty()) {
  541. for (ResourceLoadListener listener : listeners) {
  542. if (listener != null) {
  543. listener.onLoad(event);
  544. }
  545. }
  546. }
  547. }
  548. }