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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502
  1. /*
  2. * Copyright 2000-2018 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.ArrayList;
  18. import java.util.Collection;
  19. import java.util.HashMap;
  20. import java.util.HashSet;
  21. import java.util.Map;
  22. import java.util.Set;
  23. import java.util.logging.Logger;
  24. import com.google.gwt.core.client.Duration;
  25. import com.google.gwt.core.client.GWT;
  26. import com.google.gwt.core.client.Scheduler;
  27. import com.google.gwt.core.client.Scheduler.RepeatingCommand;
  28. import com.google.gwt.dom.client.Document;
  29. import com.google.gwt.dom.client.Element;
  30. import com.google.gwt.dom.client.LinkElement;
  31. import com.google.gwt.dom.client.NodeList;
  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. * @author Vaadin Ltd
  39. * @since 7.0.0
  40. */
  41. public class ResourceLoader {
  42. /**
  43. * Event fired when a resource has been loaded.
  44. */
  45. public static class ResourceLoadEvent {
  46. private final ResourceLoader loader;
  47. private final String resourceUrl;
  48. /**
  49. * Creates a new event.
  50. *
  51. * @param loader
  52. * the resource loader that has loaded the resource
  53. * @param resourceUrl
  54. * the url of the loaded resource
  55. */
  56. public ResourceLoadEvent(ResourceLoader loader, String resourceUrl) {
  57. this.loader = loader;
  58. this.resourceUrl = resourceUrl;
  59. }
  60. /**
  61. * Gets the resource loader that has fired this event.
  62. *
  63. * @return the resource loader
  64. */
  65. public ResourceLoader getResourceLoader() {
  66. return loader;
  67. }
  68. /**
  69. * Gets the absolute url of the loaded resource.
  70. *
  71. * @return the absolute url of the loaded resource
  72. */
  73. public String getResourceUrl() {
  74. return resourceUrl;
  75. }
  76. }
  77. /**
  78. * Event listener that gets notified when a resource has been loaded.
  79. */
  80. public interface ResourceLoadListener {
  81. /**
  82. * Notifies this ResourceLoadListener that a resource has been loaded.
  83. * Some browsers do not support any way of detecting load errors. In
  84. * these cases, onLoad will be called regardless of the status.
  85. *
  86. * @see ResourceLoadEvent
  87. *
  88. * @param event
  89. * a resource load event with information about the loaded
  90. * resource
  91. */
  92. public void onLoad(ResourceLoadEvent event);
  93. /**
  94. * Notifies this ResourceLoadListener that a resource could not be
  95. * loaded, e.g. because the file could not be found or because the
  96. * server did not respond. Some browsers do not support any way of
  97. * detecting load errors. In these cases, onLoad will be called
  98. * regardless of the status.
  99. *
  100. * @see ResourceLoadEvent
  101. *
  102. * @param event
  103. * a resource load event with information about the resource
  104. * that could not be loaded.
  105. */
  106. public void onError(ResourceLoadEvent event);
  107. }
  108. private static final ResourceLoader INSTANCE = GWT
  109. .create(ResourceLoader.class);
  110. private ApplicationConnection connection;
  111. private final Set<String> loadedResources = new HashSet<>();
  112. private final Map<String, Collection<ResourceLoadListener>> loadListeners = new HashMap<>();
  113. private final Element head;
  114. /**
  115. * Creates a new resource loader. You should generally not create you own
  116. * resource loader, but instead use {@link ResourceLoader#get()} to get an
  117. * instance.
  118. */
  119. protected ResourceLoader() {
  120. Document document = Document.get();
  121. head = document.getElementsByTagName("head").getItem(0);
  122. // detect already loaded scripts, html imports and stylesheets
  123. NodeList<Element> scripts = document.getElementsByTagName("script");
  124. for (int i = 0; i < scripts.getLength(); i++) {
  125. ScriptElement element = ScriptElement.as(scripts.getItem(i));
  126. String src = element.getSrc();
  127. if (src != null && !src.isEmpty()) {
  128. loadedResources.add(src);
  129. }
  130. }
  131. NodeList<Element> links = document.getElementsByTagName("link");
  132. for (int i = 0; i < links.getLength(); i++) {
  133. LinkElement linkElement = LinkElement.as(links.getItem(i));
  134. String rel = linkElement.getRel();
  135. String href = linkElement.getHref();
  136. if ("stylesheet".equalsIgnoreCase(rel) && href != null
  137. && !href.isEmpty()) {
  138. loadedResources.add(href);
  139. }
  140. if ("import".equalsIgnoreCase(rel) && href != null
  141. && !href.isEmpty()) {
  142. loadedResources.add(href);
  143. }
  144. }
  145. }
  146. /**
  147. * Returns the default ResourceLoader.
  148. *
  149. * @return the default ResourceLoader
  150. */
  151. public static ResourceLoader get() {
  152. return INSTANCE;
  153. }
  154. /**
  155. * Load a script and notify a listener when the script is loaded. Calling
  156. * this method when the script is currently loading or already loaded
  157. * doesn't cause the script to be loaded again, but the listener will still
  158. * be notified when appropriate.
  159. *
  160. * @param scriptUrl
  161. * the url of the script to load
  162. * @param resourceLoadListener
  163. * the listener that will get notified when the script is loaded
  164. */
  165. public void loadScript(final String scriptUrl,
  166. final ResourceLoadListener resourceLoadListener) {
  167. final String url = WidgetUtil.getAbsoluteUrl(scriptUrl);
  168. ResourceLoadEvent event = new ResourceLoadEvent(this, url);
  169. if (loadedResources.contains(url)) {
  170. if (resourceLoadListener != null) {
  171. resourceLoadListener.onLoad(event);
  172. }
  173. return;
  174. }
  175. if (addListener(url, resourceLoadListener, loadListeners)) {
  176. getLogger().info("Loading script from " + url);
  177. ScriptElement scriptTag = Document.get().createScriptElement();
  178. scriptTag.setSrc(url);
  179. scriptTag.setType("text/javascript");
  180. // async=false causes script injected scripts to be executed in the
  181. // injection order. See e.g.
  182. // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script
  183. scriptTag.setPropertyBoolean("async", false);
  184. addOnloadHandler(scriptTag, new ResourceLoadListener() {
  185. @Override
  186. public void onLoad(ResourceLoadEvent event) {
  187. fireLoad(event);
  188. }
  189. @Override
  190. public void onError(ResourceLoadEvent event) {
  191. fireError(event);
  192. }
  193. }, event);
  194. head.appendChild(scriptTag);
  195. }
  196. }
  197. /**
  198. * Loads an HTML import and notify a listener when the HTML import is
  199. * loaded. Calling this method when the HTML import is currently loading or
  200. * already loaded doesn't cause the HTML import to be loaded again, but the
  201. * listener will still be notified when appropriate.
  202. *
  203. * @param htmlUrl
  204. * url of HTML import to load
  205. * @param resourceLoadListener
  206. * listener to notify when the HTML import is loaded
  207. */
  208. public void loadHtmlImport(final String htmlUrl,
  209. final ResourceLoadListener resourceLoadListener) {
  210. final String url = WidgetUtil.getAbsoluteUrl(htmlUrl);
  211. ResourceLoadEvent event = new ResourceLoadEvent(this, url);
  212. if (loadedResources.contains(url)) {
  213. if (resourceLoadListener != null) {
  214. resourceLoadListener.onLoad(event);
  215. }
  216. return;
  217. }
  218. if (addListener(url, resourceLoadListener, loadListeners)) {
  219. LinkElement linkTag = Document.get().createLinkElement();
  220. linkTag.setAttribute("rel", "import");
  221. linkTag.setAttribute("href", url);
  222. addOnloadHandler(linkTag, new ResourceLoadListener() {
  223. @Override
  224. public void onLoad(ResourceLoadEvent event) {
  225. // Must wait for all HTML imports to finish
  226. // processing to ensure that e.g. the template is
  227. // parsed when calling the element constructor.
  228. runWhenHtmlImportsReady(() -> fireLoad(event));
  229. }
  230. @Override
  231. public void onError(ResourceLoadEvent event) {
  232. fireError(event);
  233. }
  234. }, event);
  235. head.appendChild(linkTag);
  236. }
  237. }
  238. /**
  239. * Adds an onload listener to the given element, which should be a link or a
  240. * script tag. The listener is called whenever loading is complete or an
  241. * error occurred.
  242. *
  243. * @since 7.3
  244. * @param element
  245. * the element to attach a listener to
  246. * @param listener
  247. * the listener to call
  248. * @param event
  249. * the event passed to the listener
  250. */
  251. public static native void addOnloadHandler(Element element,
  252. ResourceLoadListener listener, ResourceLoadEvent event)
  253. /*-{
  254. element.onload = $entry(function() {
  255. element.onload = null;
  256. element.onerror = null;
  257. element.onreadystatechange = null;
  258. listener.@com.vaadin.client.ResourceLoader.ResourceLoadListener::onLoad(Lcom/vaadin/client/ResourceLoader$ResourceLoadEvent;)(event);
  259. });
  260. element.onerror = $entry(function() {
  261. element.onload = null;
  262. element.onerror = null;
  263. element.onreadystatechange = null;
  264. listener.@com.vaadin.client.ResourceLoader.ResourceLoadListener::onError(Lcom/vaadin/client/ResourceLoader$ResourceLoadEvent;)(event);
  265. });
  266. element.onreadystatechange = function() {
  267. if ("loaded" === element.readyState || "complete" === element.readyState ) {
  268. element.onload(arguments[0]);
  269. }
  270. };
  271. }-*/;
  272. /**
  273. * Load a stylesheet and notify a listener when the stylesheet is loaded.
  274. * Calling this method when the stylesheet is currently loading or already
  275. * loaded doesn't cause the stylesheet to be loaded again, but the listener
  276. * will still be notified when appropriate.
  277. *
  278. * @param stylesheetUrl
  279. * the url of the stylesheet to load
  280. * @param resourceLoadListener
  281. * the listener that will get notified when the stylesheet is
  282. * loaded
  283. */
  284. public void loadStylesheet(final String stylesheetUrl,
  285. final ResourceLoadListener resourceLoadListener) {
  286. final String url = WidgetUtil.getAbsoluteUrl(stylesheetUrl);
  287. final ResourceLoadEvent event = new ResourceLoadEvent(this, url);
  288. if (loadedResources.contains(url)) {
  289. if (resourceLoadListener != null) {
  290. resourceLoadListener.onLoad(event);
  291. }
  292. return;
  293. }
  294. if (addListener(url, resourceLoadListener, loadListeners)) {
  295. getLogger().info("Loading style sheet from " + url);
  296. LinkElement linkElement = Document.get().createLinkElement();
  297. linkElement.setRel("stylesheet");
  298. linkElement.setType("text/css");
  299. linkElement.setHref(url);
  300. if (BrowserInfo.get().isSafariOrIOS()) {
  301. // Safari doesn't fire any events for link elements
  302. // See http://www.phpied.com/when-is-a-stylesheet-really-loaded/
  303. Scheduler.get().scheduleFixedPeriod(new RepeatingCommand() {
  304. private final Duration duration = new Duration();
  305. @Override
  306. public boolean execute() {
  307. int styleSheetLength = getStyleSheetLength(url);
  308. if (getStyleSheetLength(url) > 0) {
  309. fireLoad(event);
  310. return false; // Stop repeating
  311. } else if (styleSheetLength == 0) {
  312. // "Loaded" empty sheet -> most likely 404 error
  313. fireError(event);
  314. return true;
  315. } else if (duration.elapsedMillis() > 60 * 1000) {
  316. fireError(event);
  317. return false;
  318. } else {
  319. return true; // Continue repeating
  320. }
  321. }
  322. }, 10);
  323. } else {
  324. addOnloadHandler(linkElement, new ResourceLoadListener() {
  325. @Override
  326. public void onLoad(ResourceLoadEvent event) {
  327. // Chrome, IE, Edge all fire load for errors, must check
  328. // stylesheet data
  329. if (BrowserInfo.get().isChrome()
  330. || BrowserInfo.get().isIE()
  331. || BrowserInfo.get().isEdge()) {
  332. int styleSheetLength = getStyleSheetLength(url);
  333. // Error if there's an empty stylesheet
  334. if (styleSheetLength == 0) {
  335. fireError(event);
  336. return;
  337. }
  338. }
  339. fireLoad(event);
  340. }
  341. @Override
  342. public void onError(ResourceLoadEvent event) {
  343. fireError(event);
  344. }
  345. }, event);
  346. if (BrowserInfo.get().isOpera()) {
  347. // Opera onerror never fired, assume error if no onload in x
  348. // seconds
  349. new Timer() {
  350. @Override
  351. public void run() {
  352. if (!loadedResources.contains(url)) {
  353. fireError(event);
  354. }
  355. }
  356. }.schedule(5 * 1000);
  357. }
  358. }
  359. head.appendChild(linkElement);
  360. }
  361. }
  362. private static native int getStyleSheetLength(String url)
  363. /*-{
  364. for (var i = 0; i < $doc.styleSheets.length; i++) {
  365. if ($doc.styleSheets[i].href === url) {
  366. var sheet = $doc.styleSheets[i];
  367. try {
  368. var rules = sheet.cssRules
  369. if (rules === undefined) {
  370. rules = sheet.rules;
  371. }
  372. if (rules === null) {
  373. // Style sheet loaded, but can't access length because of XSS -> assume there's something there
  374. return 1;
  375. }
  376. // Return length so we can distinguish 0 (probably 404 error) from normal case.
  377. return rules.length;
  378. } catch (err) {
  379. return 1;
  380. }
  381. }
  382. }
  383. // No matching stylesheet found -> not yet loaded
  384. return -1;
  385. }-*/;
  386. private static boolean addListener(String url,
  387. ResourceLoadListener listener,
  388. Map<String, Collection<ResourceLoadListener>> listenerMap) {
  389. Collection<ResourceLoadListener> listeners = listenerMap.get(url);
  390. if (listeners == null) {
  391. listeners = new ArrayList<>();
  392. listeners.add(listener);
  393. listenerMap.put(url, listeners);
  394. return true;
  395. } else {
  396. listeners.add(listener);
  397. return false;
  398. }
  399. }
  400. private void fireError(ResourceLoadEvent event) {
  401. String resource = event.getResourceUrl();
  402. Collection<ResourceLoadListener> listeners = loadListeners
  403. .remove(resource);
  404. if (listeners != null && !listeners.isEmpty()) {
  405. for (ResourceLoadListener listener : listeners) {
  406. if (listener != null) {
  407. listener.onError(event);
  408. }
  409. }
  410. }
  411. }
  412. private void fireLoad(ResourceLoadEvent event) {
  413. String resource = event.getResourceUrl();
  414. Collection<ResourceLoadListener> listeners = loadListeners
  415. .remove(resource);
  416. loadedResources.add(resource);
  417. if (listeners != null && !listeners.isEmpty()) {
  418. for (ResourceLoadListener listener : listeners) {
  419. if (listener != null) {
  420. listener.onLoad(event);
  421. }
  422. }
  423. }
  424. }
  425. private static Logger getLogger() {
  426. return Logger.getLogger(ResourceLoader.class.getName());
  427. }
  428. private static native boolean supportsHtmlWhenReady()
  429. /*-{
  430. return !!($wnd.HTMLImports && $wnd.HTMLImports.whenReady);
  431. }-*/;
  432. private static native void addHtmlImportsReadyHandler(Runnable handler)
  433. /*-{
  434. $wnd.HTMLImports.whenReady($entry(function() {
  435. handler.@Runnable::run()();
  436. }));
  437. }-*/;
  438. /**
  439. * Executes a Runnable when all HTML imports are ready. If the browser does
  440. * not support triggering an event when HTML imports are ready, the Runnable
  441. * is executed immediately.
  442. *
  443. * @param runnable
  444. * the code to execute
  445. * @since 8.1
  446. */
  447. protected void runWhenHtmlImportsReady(Runnable runnable) {
  448. if (GWT.isClient() && supportsHtmlWhenReady()) {
  449. addHtmlImportsReadyHandler(() -> runnable.run());
  450. } else {
  451. runnable.run();
  452. }
  453. }
  454. }