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

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