1 /* 2 * Copyright (c) 2011, 2017, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package javafx.scene.web; 27 28 import com.sun.javafx.scene.web.Debugger; 29 import com.sun.javafx.scene.web.Printable; 30 import com.sun.javafx.tk.TKPulseListener; 31 import com.sun.javafx.tk.Toolkit; 32 import com.sun.javafx.webkit.*; 33 import com.sun.javafx.webkit.prism.PrismGraphicsManager; 34 import com.sun.javafx.webkit.prism.PrismInvoker; 35 import com.sun.javafx.webkit.prism.theme.PrismRenderer; 36 import com.sun.javafx.webkit.theme.RenderThemeImpl; 37 import com.sun.javafx.webkit.theme.Renderer; 38 import com.sun.webkit.*; 39 import com.sun.webkit.graphics.WCGraphicsManager; 40 import com.sun.webkit.network.URLs; 41 import com.sun.webkit.network.Util; 42 import javafx.animation.AnimationTimer; 43 import javafx.application.Platform; 44 import javafx.beans.InvalidationListener; 45 import javafx.beans.property.*; 46 import javafx.concurrent.Worker; 47 import javafx.event.EventHandler; 48 import javafx.event.EventType; 49 import javafx.geometry.Rectangle2D; 50 import javafx.print.PageLayout; 51 import javafx.print.PrinterJob; 52 import javafx.scene.Node; 53 import javafx.util.Callback; 54 import org.w3c.dom.Document; 55 56 import java.io.BufferedInputStream; 57 import java.io.File; 58 import java.io.IOException; 59 import static java.lang.String.format; 60 import java.lang.ref.WeakReference; 61 import java.net.MalformedURLException; 62 import java.net.URLConnection; 63 import java.nio.file.Files; 64 import java.nio.file.Path; 65 import java.nio.file.attribute.PosixFilePermissions; 66 import java.security.AccessController; 67 import java.security.PrivilegedAction; 68 import java.util.ArrayList; 69 import java.util.Base64; 70 import java.util.List; 71 import java.util.Objects; 72 import java.util.logging.Level; 73 import java.util.logging.Logger; 74 75 import static com.sun.webkit.LoadListenerClient.*; 76 77 /** 78 * {@code WebEngine} is a non-visual object capable of managing one Web page 79 * at a time. It loads Web pages, creates their document models, applies 80 * styles as necessary, and runs JavaScript on pages. It provides access 81 * to the document model of the current page, and enables two-way 82 * communication between a Java application and JavaScript code of the page. 83 * 84 * <p><b>Loading Web Pages</b></p> 85 * <p>The {@code WebEngine} class provides two ways to load content into a 86 * {@code WebEngine} object: 87 * <ul> 88 * <li>From an arbitrary URL using the {@link #load} method. This method uses 89 * the {@code java.net} package for network access and protocol handling. 90 * <li>From an in-memory String using the 91 * {@link #loadContent(java.lang.String, java.lang.String)} and 92 * {@link #loadContent(java.lang.String)} methods. 93 * </ul> 94 * <p>Loading always happens on a background thread. Methods that initiate 95 * loading return immediately after scheduling a background job. To track 96 * progress and/or cancel a job, use the {@link javafx.concurrent.Worker} 97 * instance available from the {@link #getLoadWorker} method. 98 * 99 * <p>The following example changes the stage title when loading completes 100 * successfully: 101 * <pre>{@code 102 import javafx.concurrent.Worker.State; 103 final Stage stage; 104 webEngine.getLoadWorker().stateProperty().addListener( 105 new ChangeListener<State>() { 106 public void changed(ObservableValue ov, State oldState, State newState) { 107 if (newState == State.SUCCEEDED) { 108 stage.setTitle(webEngine.getLocation()); 109 } 110 } 111 }); 112 webEngine.load("http://javafx.com"); 113 * }</pre> 114 * 115 * <p><b>User Interface Callbacks</b></p> 116 * <p>A number of user interface callbacks may be registered with a 117 * {@code WebEngine} object. These callbacks are invoked when a script running 118 * on the page requests a user interface operation to be performed, for 119 * example, opens a popup window or changes status text. A {@code WebEngine} 120 * object cannot handle such requests internally, so it passes the request to 121 * the corresponding callbacks. If no callback is defined for a specific 122 * operation, the request is silently ignored. 123 * 124 * <p>The table below shows JavaScript user interface methods and properties 125 * with their corresponding {@code WebEngine} callbacks: 126 * <table border="1"> 127 * <caption>JavaScript Callback Table</caption> 128 * <tr> 129 * <th>JavaScript method/property</th> 130 * <th>WebEngine callback</th> 131 * </tr> 132 * <tr><td>{@code window.alert()}</td><td>{@code onAlert}</td></tr> 133 * <tr><td>{@code window.confirm()}</td><td>{@code confirmHandler}</td></tr> 134 * <tr><td>{@code window.open()}</td><td>{@code createPopupHandler}</td></tr> 135 * <tr><td>{@code window.open()} and<br> 136 * {@code window.close()}</td><td>{@code onVisibilityChanged}</td></tr> 137 * <tr><td>{@code window.prompt()}</td><td>{@code promptHandler}</td></tr> 138 * <tr><td>Setting {@code window.status}</td><td>{@code onStatusChanged}</td></tr> 139 * <tr><td>Setting any of the following:<br> 140 * {@code window.innerWidth}, {@code window.innerHeight},<br> 141 * {@code window.outerWidth}, {@code window.outerHeight},<br> 142 * {@code window.screenX}, {@code window.screenY},<br> 143 * {@code window.screenLeft}, {@code window.screenTop} 144 * <td>{@code onResized}</td></tr> 145 * </table> 146 * 147 * <p>The following example shows a callback that resizes a browser window: 148 * <pre>{@code 149 Stage stage; 150 webEngine.setOnResized( 151 new EventHandler<WebEvent<Rectangle2D>>() { 152 public void handle(WebEvent<Rectangle2D> ev) { 153 Rectangle2D r = ev.getData(); 154 stage.setWidth(r.getWidth()); 155 stage.setHeight(r.getHeight()); 156 } 157 }); 158 * }</pre> 159 * 160 * <p><b>Access to Document Model</b></p> 161 * <p>The {@code WebEngine} objects create and manage a Document Object Model 162 * (DOM) for their Web pages. The model can be accessed and modified using 163 * Java DOM Core classes. The {@link #getDocument()} method provides access 164 * to the root of the model. Additionally DOM Event specification is supported 165 * to define event handlers in Java code. 166 * 167 * <p>The following example attaches a Java event listener to an element of 168 * a Web page. Clicking on the element causes the application to exit: 169 * <pre>{@code 170 EventListener listener = new EventListener() { 171 public void handleEvent(Event ev) { 172 Platform.exit(); 173 } 174 }; 175 176 Document doc = webEngine.getDocument(); 177 Element el = doc.getElementById("exit-app"); 178 ((EventTarget) el).addEventListener("click", listener, false); 179 * }</pre> 180 * 181 * <p><b>Evaluating JavaScript expressions</b></p> 182 * <p>It is possible to execute arbitrary JavaScript code in the context of 183 * the current page using the {@link #executeScript} method. For example: 184 * <pre>{@code 185 webEngine.executeScript("history.back()"); 186 * }</pre> 187 * 188 * <p>The execution result is returned to the caller, 189 * as described in the next section. 190 * 191 * <p><b>Mapping JavaScript values to Java objects</b></p> 192 * 193 * JavaScript values are represented using the obvious Java classes: 194 * null becomes Java null; a boolean becomes a {@code java.lang.Boolean}; 195 * and a string becomes a {@code java.lang.String}. 196 * A number can be {@code java.lang.Double} or a {@code java.lang.Integer}, 197 * depending. 198 * The undefined value maps to a specific unique String 199 * object whose value is {@code "undefined"}. 200 * <p> 201 * If the result is a 202 * JavaScript object, it is wrapped as an instance of the 203 * {@link netscape.javascript.JSObject} class. 204 * (As a special case, if the JavaScript object is 205 * a {@code JavaRuntimeObject} as discussed in the next section, 206 * then the original Java object is extracted instead.) 207 * The {@code JSObject} class is a proxy that provides access to 208 * methods and properties of its underlying JavaScript object. 209 * The most commonly used {@code JSObject} methods are 210 * {@link netscape.javascript.JSObject#getMember getMember} 211 * (to read a named property), 212 * {@link netscape.javascript.JSObject#setMember setMember} 213 * (to set or define a property), 214 * and {@link netscape.javascript.JSObject#call call} 215 * (to call a function-valued property). 216 * <p> 217 * A DOM {@code Node} is mapped to an object that both extends 218 * {@code JSObject} and implements the appropriate DOM interfaces. 219 * To get a {@code JSObject} object for a {@code Node} just do a cast: 220 * <pre> 221 * JSObject jdoc = (JSObject) webEngine.getDocument(); 222 * </pre> 223 * <p> 224 * In some cases the context provides a specific Java type that guides 225 * the conversion. 226 * For example if setting a Java {@code String} field from a JavaScript 227 * expression, then the JavaScript value is converted to a string. 228 * 229 * <p><b>Mapping Java objects to JavaScript values</b></p> 230 * 231 * The arguments of the {@code JSObject} methods {@code setMember} and 232 * {@code call} pass Java objects to the JavaScript environment. 233 * This is roughly the inverse of the JavaScript-to-Java mapping 234 * described above: 235 * Java {@code String}, {@code Number}, or {@code Boolean} objects 236 * are converted to the obvious JavaScript values. A {@code JSObject} 237 * object is converted to the original wrapped JavaScript object. 238 * Otherwise a {@code JavaRuntimeObject} is created. This is 239 * a JavaScript object that acts as a proxy for the Java object, 240 * in that accessing properties of the {@code JavaRuntimeObject} 241 * causes the Java field or method with the same name to be accessed. 242 * <p> Note that the Java objects bound using 243 * {@link netscape.javascript.JSObject#setMember JSObject.setMember}, 244 * {@link netscape.javascript.JSObject#setSlot JSObject.setSlot}, and 245 * {@link netscape.javascript.JSObject#call JSObject.call} 246 * are implemented using weak references. This means that the Java object 247 * can be garbage collected, causing subsequent accesses to the JavaScript 248 * objects to have no effect. 249 * 250 * <p><b>Calling back to Java from JavaScript</b></p> 251 * 252 * <p>The {@link netscape.javascript.JSObject#setMember JSObject.setMember} 253 * method is useful to enable upcalls from JavaScript 254 * into Java code, as illustrated by the following example. The Java code 255 * establishes a new JavaScript object named {@code app}. This object has one 256 * public member, the method {@code exit}. 257 * <pre><code> 258 public class JavaApplication { 259 public void exit() { 260 Platform.exit(); 261 } 262 } 263 ... 264 JavaApplication javaApp = new JavaApplication(); 265 JSObject window = (JSObject) webEngine.executeScript("window"); 266 window.setMember("app", javaApp); 267 * </code></pre> 268 * You can then refer to the object and the method from your HTML page: 269 * <pre>{@code 270 <a href="" onclick="app.exit()">Click here to exit application</a> 271 * }</pre> 272 * <p>When a user clicks the link the application is closed. 273 * <p> 274 * Note that in the above example, the application holds a reference 275 * to the {@code JavaApplication} instance. This is required for the callback 276 * from JavaScript to execute the desired method. 277 * <p> In the following example, the application does not hold a reference 278 * to the Java object: 279 * <pre><code> 280 * JSObject window = (JSObject) webEngine.executeScript("window"); 281 * window.setMember("app", new JavaApplication()); 282 * </code></pre> 283 * <p> In this case, since the property value is a local object, {@code "new JavaApplication()"}, 284 * the value may be garbage collected in next GC cycle. 285 * <p> 286 * When a user clicks the link, it does not guarantee to execute the callback method {@code exit}. 287 * <p> 288 * If there are multiple Java methods with the given name, 289 * then the engine selects one matching the number of parameters 290 * in the call. (Varargs are not handled.) An unspecified one is 291 * chosen if there are multiple ones with the correct number of parameters. 292 * <p> 293 * You can pick a specific overloaded method by listing the 294 * parameter types in an "extended method name", which has the 295 * form <code>"<var>method_name</var>(<var>param_type1</var>,...,<var>param_typen</var>)"</code>. Typically you'd write the JavaScript expression: 296 * <pre> 297 * <code><var>receiver</var>["<var>method_name</var>(<var>param_type1</var>,...,<var>param_typeN</var>)"](<var>arg1</var>,...,<var>argN</var>)</code> 298 * </pre> 299 * 300 * <p><b>Threading</b></p> 301 * <p>{@code WebEngine} objects must be created and accessed solely from the 302 * JavaFX Application thread. This rule also applies to any DOM and JavaScript 303 * objects obtained from the {@code WebEngine} object. 304 * @since JavaFX 2.0 305 */ 306 final public class WebEngine { 307 static { 308 Accessor.setPageAccessor(w -> w == null ? null : w.getPage()); 309 310 Invoker.setInvoker(new PrismInvoker()); 311 Renderer.setRenderer(new PrismRenderer()); 312 WCGraphicsManager.setGraphicsManager(new PrismGraphicsManager()); 313 CursorManager.setCursorManager(new CursorManagerImpl()); 314 com.sun.webkit.EventLoop.setEventLoop(new EventLoopImpl()); 315 ThemeClient.setDefaultRenderTheme(new RenderThemeImpl()); 316 Utilities.setUtilities(new UtilitiesImpl()); 317 } 318 319 private static final Logger logger = 320 Logger.getLogger(WebEngine.class.getName()); 321 322 /** 323 * The number of instances of this class. 324 * Used to start and stop the pulse timer. 325 */ 326 private static int instanceCount = 0; 327 328 /** 329 * The node associated with this engine. There is a one-to-one correspondence 330 * between the WebView and its WebEngine (although not all WebEngines have 331 * a WebView, every WebView has one and only one WebEngine). 332 */ 333 private final ObjectProperty<WebView> view = new SimpleObjectProperty<WebView>(this, "view"); 334 335 /** 336 * The Worker which shows progress of the web engine as it loads pages. 337 */ 338 private final LoadWorker loadWorker = new LoadWorker(); 339 340 /** 341 * The object that provides interaction with the native webkit core. 342 */ 343 private final WebPage page; 344 345 private final SelfDisposer disposer; 346 347 private final DebuggerImpl debugger = new DebuggerImpl(); 348 349 private boolean userDataDirectoryApplied = false; 350 351 352 /** 353 * Returns a {@link javafx.concurrent.Worker} object that can be used to 354 * track loading progress. 355 * 356 * @return the {@code Worker} object 357 */ 358 public final Worker<Void> getLoadWorker() { 359 return loadWorker; 360 } 361 362 363 /* 364 * The final document. This may be null if no document has been loaded. 365 */ 366 private final DocumentProperty document = new DocumentProperty(); 367 368 public final Document getDocument() { return document.getValue(); } 369 370 /** 371 * Document object for the current Web page. The value is {@code null} 372 * if the Web page failed to load. 373 * 374 * @return the document property 375 */ 376 public final ReadOnlyObjectProperty<Document> documentProperty() { 377 return document; 378 } 379 380 381 /* 382 * The location of the current page. This may return null. 383 */ 384 private final ReadOnlyStringWrapper location = new ReadOnlyStringWrapper(this, "location"); 385 386 public final String getLocation() { return location.getValue(); } 387 388 /** 389 * URL of the current Web page. If the current page has no URL, 390 * the value is an empty String. 391 * 392 * @return the location property 393 */ 394 public final ReadOnlyStringProperty locationProperty() { return location.getReadOnlyProperty(); } 395 396 private void updateLocation(String value) { 397 this.location.set(value); 398 this.document.invalidate(false); 399 this.title.set(null); 400 } 401 402 403 /* 404 * The page title. 405 */ 406 private final ReadOnlyStringWrapper title = new ReadOnlyStringWrapper(this, "title"); 407 408 public final String getTitle() { return title.getValue(); } 409 410 /** 411 * Title of the current Web page. If the current page has no title, 412 * the value is {@code null}. 413 * 414 * @return the title property 415 */ 416 public final ReadOnlyStringProperty titleProperty() { return title.getReadOnlyProperty(); } 417 418 private void updateTitle() { 419 title.set(page.getTitle(page.getMainFrame())); 420 } 421 422 // 423 // Settings 424 425 /** 426 * Specifies whether JavaScript execution is enabled. 427 * 428 * @defaultValue true 429 * @since JavaFX 2.2 430 */ 431 private BooleanProperty javaScriptEnabled; 432 433 public final void setJavaScriptEnabled(boolean value) { 434 javaScriptEnabledProperty().set(value); 435 } 436 437 public final boolean isJavaScriptEnabled() { 438 return javaScriptEnabled == null ? true : javaScriptEnabled.get(); 439 } 440 441 public final BooleanProperty javaScriptEnabledProperty() { 442 if (javaScriptEnabled == null) { 443 javaScriptEnabled = new BooleanPropertyBase(true) { 444 @Override public void invalidated() { 445 checkThread(); 446 page.setJavaScriptEnabled(get()); 447 } 448 449 @Override public Object getBean() { 450 return WebEngine.this; 451 } 452 453 @Override public String getName() { 454 return "javaScriptEnabled"; 455 } 456 }; 457 } 458 return javaScriptEnabled; 459 } 460 461 /** 462 * Location of the user stylesheet as a string URL. 463 * 464 * <p>This should be a local URL, i.e. either {@code 'data:'}, 465 * {@code 'file:'}, or {@code 'jar:'}. Remote URLs are not allowed 466 * for security reasons. 467 * 468 * @defaultValue null 469 * @since JavaFX 2.2 470 */ 471 private StringProperty userStyleSheetLocation; 472 473 public final void setUserStyleSheetLocation(String value) { 474 userStyleSheetLocationProperty().set(value); 475 } 476 477 public final String getUserStyleSheetLocation() { 478 return userStyleSheetLocation == null ? null : userStyleSheetLocation.get(); 479 } 480 481 private byte[] readFully(BufferedInputStream in) throws IOException { 482 final int BUF_SIZE = 4096; 483 int outSize = 0; 484 final List<byte[]> outList = new ArrayList<>(); 485 byte[] buffer = new byte[BUF_SIZE]; 486 487 while (true) { 488 int nBytes = in.read(buffer); 489 if (nBytes < 0) break; 490 491 byte[] chunk; 492 if (nBytes == buffer.length) { 493 chunk = buffer; 494 buffer = new byte[BUF_SIZE]; 495 } else { 496 chunk = new byte[nBytes]; 497 System.arraycopy(buffer, 0, chunk, 0, nBytes); 498 } 499 outList.add(chunk); 500 outSize += nBytes; 501 } 502 503 final byte[] out = new byte[outSize]; 504 int outPos = 0; 505 for (byte[] chunk : outList) { 506 System.arraycopy(chunk, 0, out, outPos, chunk.length); 507 outPos += chunk.length; 508 } 509 510 return out; 511 } 512 513 public final StringProperty userStyleSheetLocationProperty() { 514 if (userStyleSheetLocation == null) { 515 userStyleSheetLocation = new StringPropertyBase(null) { 516 private final static String DATA_PREFIX = "data:text/css;charset=utf-8;base64,"; 517 518 @Override public void invalidated() { 519 checkThread(); 520 String url = get(); 521 String dataUrl; 522 if (url == null || url.length() <= 0) { 523 dataUrl = null; 524 } else if (url.startsWith(DATA_PREFIX)) { 525 dataUrl = url; 526 } else if (url.startsWith("file:") || 527 url.startsWith("jar:") || 528 url.startsWith("data:")) 529 { 530 try { 531 URLConnection conn = URLs.newURL(url).openConnection(); 532 conn.connect(); 533 534 BufferedInputStream in = 535 new BufferedInputStream(conn.getInputStream()); 536 byte[] inBytes = readFully(in); 537 String out = Base64.getMimeEncoder().encodeToString(inBytes); 538 dataUrl = DATA_PREFIX + out; 539 } catch (IOException e) { 540 throw new RuntimeException(e); 541 } 542 } else { 543 throw new IllegalArgumentException("Invalid stylesheet URL"); 544 } 545 page.setUserStyleSheetLocation(dataUrl); 546 } 547 548 @Override public Object getBean() { 549 return WebEngine.this; 550 } 551 552 @Override public String getName() { 553 return "userStyleSheetLocation"; 554 } 555 }; 556 } 557 return userStyleSheetLocation; 558 } 559 560 /** 561 * Specifies the directory to be used by this {@code WebEngine} 562 * to store local user data. 563 * 564 * <p>If the value of this property is not {@code null}, 565 * the {@code WebEngine} will attempt to store local user data 566 * in the respective directory. 567 * If the value of this property is {@code null}, 568 * the {@code WebEngine} will attempt to store local user data 569 * in an automatically selected system-dependent user- and 570 * application-specific directory. 571 * 572 * <p>When a {@code WebEngine} is about to start loading a web 573 * page or executing a script for the first time, it checks whether 574 * it can actually use the directory specified by this property. 575 * If the check fails for some reason, the {@code WebEngine} invokes 576 * the {@link WebEngine#onErrorProperty WebEngine.onError} event handler, 577 * if any, with a {@link WebErrorEvent} describing the reason. 578 * If the invoked event handler modifies the {@code userDataDirectory} 579 * property, the {@code WebEngine} retries with the new value as soon 580 * as the handler returns. If the handler does not modify the 581 * {@code userDataDirectory} property (which is the default), 582 * the {@code WebEngine} continues without local user data. 583 * 584 * <p>Once the {@code WebEngine} has started loading a web page or 585 * executing a script, changes made to this property have no effect 586 * on where the {@code WebEngine} stores or will store local user 587 * data. 588 * 589 * <p>Currently, the directory specified by this property is used 590 * only to store the data that backs the {@code window.localStorage} 591 * objects. In the future, more types of data can be added. 592 * 593 * @defaultValue {@code null} 594 * @since JavaFX 8.0 595 */ 596 private final ObjectProperty<File> userDataDirectory = 597 new SimpleObjectProperty<>(this, "userDataDirectory"); 598 599 public final File getUserDataDirectory() { 600 return userDataDirectory.get(); 601 } 602 603 public final void setUserDataDirectory(File value) { 604 userDataDirectory.set(value); 605 } 606 607 public final ObjectProperty<File> userDataDirectoryProperty() { 608 return userDataDirectory; 609 } 610 611 /** 612 * Specifies user agent ID string. This string is the value of the 613 * {@code User-Agent} HTTP header. 614 * 615 * @defaultValue system dependent 616 * @since JavaFX 8.0 617 */ 618 private StringProperty userAgent; 619 620 public final void setUserAgent(String value) { 621 userAgentProperty().set(value); 622 } 623 624 public final String getUserAgent() { 625 return userAgent == null ? page.getUserAgent() : userAgent.get(); 626 } 627 628 public final StringProperty userAgentProperty() { 629 if (userAgent == null) { 630 userAgent = new StringPropertyBase(page.getUserAgent()) { 631 @Override public void invalidated() { 632 checkThread(); 633 page.setUserAgent(get()); 634 } 635 636 @Override public Object getBean() { 637 return WebEngine.this; 638 } 639 640 @Override public String getName() { 641 return "userAgent"; 642 } 643 }; 644 } 645 return userAgent; 646 } 647 648 private final ObjectProperty<EventHandler<WebEvent<String>>> onAlert 649 = new SimpleObjectProperty<EventHandler<WebEvent<String>>>(this, "onAlert"); 650 651 public final EventHandler<WebEvent<String>> getOnAlert() { return onAlert.get(); } 652 653 public final void setOnAlert(EventHandler<WebEvent<String>> handler) { onAlert.set(handler); } 654 655 /** 656 * JavaScript {@code alert} handler property. This handler is invoked 657 * when a script running on the Web page calls the {@code alert} function. 658 * @return the onAlert property 659 */ 660 public final ObjectProperty<EventHandler<WebEvent<String>>> onAlertProperty() { return onAlert; } 661 662 663 private final ObjectProperty<EventHandler<WebEvent<String>>> onStatusChanged 664 = new SimpleObjectProperty<EventHandler<WebEvent<String>>>(this, "onStatusChanged"); 665 666 public final EventHandler<WebEvent<String>> getOnStatusChanged() { return onStatusChanged.get(); } 667 668 public final void setOnStatusChanged(EventHandler<WebEvent<String>> handler) { onStatusChanged.set(handler); } 669 670 /** 671 * JavaScript status handler property. This handler is invoked when 672 * a script running on the Web page sets {@code window.status} property. 673 * @return the onStatusChanged property 674 */ 675 public final ObjectProperty<EventHandler<WebEvent<String>>> onStatusChangedProperty() { return onStatusChanged; } 676 677 678 private final ObjectProperty<EventHandler<WebEvent<Rectangle2D>>> onResized 679 = new SimpleObjectProperty<EventHandler<WebEvent<Rectangle2D>>>(this, "onResized"); 680 681 public final EventHandler<WebEvent<Rectangle2D>> getOnResized() { return onResized.get(); } 682 683 public final void setOnResized(EventHandler<WebEvent<Rectangle2D>> handler) { onResized.set(handler); } 684 685 /** 686 * JavaScript window resize handler property. This handler is invoked 687 * when a script running on the Web page moves or resizes the 688 * {@code window} object. 689 * @return the onResized property 690 */ 691 public final ObjectProperty<EventHandler<WebEvent<Rectangle2D>>> onResizedProperty() { return onResized; } 692 693 694 private final ObjectProperty<EventHandler<WebEvent<Boolean>>> onVisibilityChanged 695 = new SimpleObjectProperty<EventHandler<WebEvent<Boolean>>>(this, "onVisibilityChanged"); 696 697 public final EventHandler<WebEvent<Boolean>> getOnVisibilityChanged() { return onVisibilityChanged.get(); } 698 699 public final void setOnVisibilityChanged(EventHandler<WebEvent<Boolean>> handler) { onVisibilityChanged.set(handler); } 700 701 /** 702 * JavaScript window visibility handler property. This handler is invoked 703 * when a script running on the Web page changes visibility of the 704 * {@code window} object. 705 * @return the onVisibilityChanged property 706 */ 707 public final ObjectProperty<EventHandler<WebEvent<Boolean>>> onVisibilityChangedProperty() { return onVisibilityChanged; } 708 709 710 private final ObjectProperty<Callback<PopupFeatures, WebEngine>> createPopupHandler 711 = new SimpleObjectProperty<Callback<PopupFeatures, WebEngine>>(this, "createPopupHandler", 712 p -> WebEngine.this); 713 714 public final Callback<PopupFeatures, WebEngine> getCreatePopupHandler() { return createPopupHandler.get(); } 715 716 public final void setCreatePopupHandler(Callback<PopupFeatures, WebEngine> handler) { createPopupHandler.set(handler); } 717 718 /** 719 * JavaScript popup handler property. This handler is invoked when a script 720 * running on the Web page requests a popup to be created. 721 * <p>To satisfy this request a handler may create a new {@code WebEngine}, 722 * attach a visibility handler and optionally a resize handler, and return 723 * the newly created engine. To block the popup, a handler should return 724 * {@code null}. 725 * <p>By default, a popup handler is installed that opens popups in this 726 * {@code WebEngine}. 727 * 728 * @return the createPopupHandler property 729 * 730 * @see PopupFeatures 731 */ 732 public final ObjectProperty<Callback<PopupFeatures, WebEngine>> createPopupHandlerProperty() { return createPopupHandler; } 733 734 735 private final ObjectProperty<Callback<String, Boolean>> confirmHandler 736 = new SimpleObjectProperty<Callback<String, Boolean>>(this, "confirmHandler"); 737 738 public final Callback<String, Boolean> getConfirmHandler() { return confirmHandler.get(); } 739 740 public final void setConfirmHandler(Callback<String, Boolean> handler) { confirmHandler.set(handler); } 741 742 /** 743 * JavaScript {@code confirm} handler property. This handler is invoked 744 * when a script running on the Web page calls the {@code confirm} function. 745 * <p>An implementation may display a dialog box with Yes and No options, 746 * and return the user's choice. 747 * 748 * @return the confirmHandler property 749 */ 750 public final ObjectProperty<Callback<String, Boolean>> confirmHandlerProperty() { return confirmHandler; } 751 752 753 private final ObjectProperty<Callback<PromptData, String>> promptHandler 754 = new SimpleObjectProperty<Callback<PromptData, String>>(this, "promptHandler"); 755 756 public final Callback<PromptData, String> getPromptHandler() { return promptHandler.get(); } 757 758 public final void setPromptHandler(Callback<PromptData, String> handler) { promptHandler.set(handler); } 759 760 /** 761 * JavaScript {@code prompt} handler property. This handler is invoked 762 * when a script running on the Web page calls the {@code prompt} function. 763 * <p>An implementation may display a dialog box with an text field, 764 * and return the user's input. 765 * 766 * @return the promptHandler property 767 * @see PromptData 768 */ 769 public final ObjectProperty<Callback<PromptData, String>> promptHandlerProperty() { return promptHandler; } 770 771 /** 772 * The event handler called when an error occurs. 773 * 774 * @defaultValue {@code null} 775 * @since JavaFX 8.0 776 */ 777 private final ObjectProperty<EventHandler<WebErrorEvent>> onError = 778 new SimpleObjectProperty<>(this, "onError"); 779 780 public final EventHandler<WebErrorEvent> getOnError() { 781 return onError.get(); 782 } 783 784 public final void setOnError(EventHandler<WebErrorEvent> handler) { 785 onError.set(handler); 786 } 787 788 public final ObjectProperty<EventHandler<WebErrorEvent>> onErrorProperty() { 789 return onError; 790 } 791 792 793 /** 794 * Creates a new engine. 795 */ 796 public WebEngine() { 797 this(null, false); 798 } 799 800 /** 801 * Creates a new engine and loads a Web page into it. 802 * 803 * @param url the URL of the web page to load 804 */ 805 public WebEngine(String url) { 806 this(url, true); 807 } 808 809 private WebEngine(String url, boolean callLoad) { 810 checkThread(); 811 Accessor accessor = new AccessorImpl(this); 812 page = new WebPage( 813 new WebPageClientImpl(accessor), 814 new UIClientImpl(accessor), 815 null, 816 new InspectorClientImpl(this), 817 new ThemeClientImpl(accessor), 818 false); 819 page.addLoadListenerClient(new PageLoadListener(this)); 820 821 history = new WebHistory(page); 822 823 disposer = new SelfDisposer(page); 824 Disposer.addRecord(this, disposer); 825 826 if (callLoad) { 827 load(url); 828 } 829 830 if (instanceCount == 0 && 831 Timer.getMode() == Timer.Mode.PLATFORM_TICKS) 832 { 833 PulseTimer.start(); 834 } 835 instanceCount++; 836 } 837 838 /** 839 * Loads a Web page into this engine. This method starts asynchronous 840 * loading and returns immediately. 841 * @param url URL of the web page to load 842 */ 843 public void load(String url) { 844 checkThread(); 845 loadWorker.cancelAndReset(); 846 847 if (url == null || url.equals("") || url.equals("about:blank")) { 848 url = ""; 849 } else { 850 // verify and, if possible, adjust the url on the Java 851 // side, otherwise it may crash native code 852 try { 853 url = Util.adjustUrlForWebKit(url); 854 } catch (MalformedURLException e) { 855 loadWorker.dispatchLoadEvent(getMainFrame(), 856 PAGE_STARTED, url, null, 0.0, 0); 857 loadWorker.dispatchLoadEvent(getMainFrame(), 858 LOAD_FAILED, url, null, 0.0, MALFORMED_URL); 859 return; 860 } 861 } 862 applyUserDataDirectory(); 863 page.open(page.getMainFrame(), url); 864 } 865 866 /** 867 * Loads the given HTML content directly. This method is useful when you have an HTML 868 * String composed in memory, or loaded from some system which cannot be reached via 869 * a URL (for example, the HTML text may have come from a database). As with 870 * {@link #load(String)}, this method is asynchronous. 871 * 872 * @param content the HTML content to load 873 */ 874 public void loadContent(String content) { 875 loadContent(content, "text/html"); 876 } 877 878 /** 879 * Loads the given content directly. This method is useful when you have content 880 * composed in memory, or loaded from some system which cannot be reached via 881 * a URL (for example, the SVG text may have come from a database). As with 882 * {@link #load(String)}, this method is asynchronous. This method also allows you to 883 * specify the content type of the string being loaded, and so may optionally support 884 * other types besides just HTML. 885 * 886 * @param content the HTML content to load 887 * @param contentType the type of content to load 888 */ 889 public void loadContent(String content, String contentType) { 890 checkThread(); 891 loadWorker.cancelAndReset(); 892 applyUserDataDirectory(); 893 page.load(page.getMainFrame(), content, contentType); 894 } 895 896 /** 897 * Reloads the current page, whether loaded from URL or directly from a String in 898 * one of the {@code loadContent} methods. 899 */ 900 public void reload() { 901 // TODO what happens if this is called while currently loading a page? 902 checkThread(); 903 page.refresh(page.getMainFrame()); 904 } 905 906 private final WebHistory history; 907 908 /** 909 * Returns the session history object. 910 * 911 * @return history object 912 * @since JavaFX 2.2 913 */ 914 public WebHistory getHistory() { 915 return history; 916 } 917 918 /** 919 * Executes a script in the context of the current page. 920 * 921 * @param script the script 922 * @return execution result, converted to a Java object using the following 923 * rules: 924 * <ul> 925 * <li>JavaScript Int32 is converted to {@code java.lang.Integer} 926 * <li>Other JavaScript numbers to {@code java.lang.Double} 927 * <li>JavaScript string to {@code java.lang.String} 928 * <li>JavaScript boolean to {@code java.lang.Boolean} 929 * <li>JavaScript {@code null} to {@code null} 930 * <li>Most JavaScript objects get wrapped as 931 * {@code netscape.javascript.JSObject} 932 * <li>JavaScript JSNode objects get mapped to instances of 933 * {@code netscape.javascript.JSObject}, that also implement 934 * {@code org.w3c.dom.Node} 935 * <li>A special case is the JavaScript class {@code JavaRuntimeObject} 936 * which is used to wrap a Java object as a JavaScript value - in this 937 * case we just extract the original Java value. 938 * </ul> 939 */ 940 public Object executeScript(String script) { 941 checkThread(); 942 applyUserDataDirectory(); 943 return page.executeScript(page.getMainFrame(), script); 944 } 945 946 private long getMainFrame() { 947 return page.getMainFrame(); 948 } 949 950 WebPage getPage() { 951 return page; 952 } 953 954 void setView(WebView view) { 955 this.view.setValue(view); 956 } 957 958 private void stop() { 959 checkThread(); 960 page.stop(page.getMainFrame()); 961 } 962 963 private void applyUserDataDirectory() { 964 if (userDataDirectoryApplied) { 965 return; 966 } 967 userDataDirectoryApplied = true; 968 File nominalUserDataDir = getUserDataDirectory(); 969 while (true) { 970 File userDataDir; 971 String displayString; 972 if (nominalUserDataDir == null) { 973 userDataDir = defaultUserDataDirectory(); 974 displayString = format("null (%s)", userDataDir); 975 } else { 976 userDataDir = nominalUserDataDir; 977 displayString = userDataDir.toString(); 978 } 979 logger.log(Level.FINE, "Trying to apply user data " 980 + "directory [{0}]", displayString); 981 String errorMessage; 982 EventType<WebErrorEvent> errorType; 983 Throwable error; 984 try { 985 userDataDir = DirectoryLock.canonicalize(userDataDir); 986 File localStorageDir = new File(userDataDir, "localstorage"); 987 File[] dirs = new File[] { 988 userDataDir, 989 localStorageDir, 990 }; 991 for (File dir : dirs) { 992 createDirectories(dir); 993 // Additional security check to make sure the caller 994 // has permission to write to the target directory 995 File test = new File(dir, ".test"); 996 if (test.createNewFile()) { 997 test.delete(); 998 } 999 } 1000 disposer.userDataDirectoryLock = new DirectoryLock(userDataDir); 1001 1002 page.setLocalStorageDatabasePath(localStorageDir.getPath()); 1003 page.setLocalStorageEnabled(true); 1004 1005 logger.log(Level.FINE, "User data directory [{0}] has " 1006 + "been applied successfully", displayString); 1007 return; 1008 1009 } catch (DirectoryLock.DirectoryAlreadyInUseException ex) { 1010 errorMessage = "User data directory [%s] is already in use"; 1011 errorType = WebErrorEvent.USER_DATA_DIRECTORY_ALREADY_IN_USE; 1012 error = ex; 1013 } catch (IOException ex) { 1014 errorMessage = "An I/O error occurred while setting up " 1015 + "user data directory [%s]"; 1016 errorType = WebErrorEvent.USER_DATA_DIRECTORY_IO_ERROR; 1017 error = ex; 1018 } catch (SecurityException ex) { 1019 errorMessage = "A security error occurred while setting up " 1020 + "user data directory [%s]"; 1021 errorType = WebErrorEvent.USER_DATA_DIRECTORY_SECURITY_ERROR; 1022 error = ex; 1023 } 1024 1025 errorMessage = format(errorMessage, displayString); 1026 logger.log(Level.FINE, "{0}, calling error handler", errorMessage); 1027 File oldNominalUserDataDir = nominalUserDataDir; 1028 fireError(errorType, errorMessage, error); 1029 nominalUserDataDir = getUserDataDirectory(); 1030 if (Objects.equals(nominalUserDataDir, oldNominalUserDataDir)) { 1031 logger.log(Level.FINE, "Error handler did not " 1032 + "modify user data directory, continuing " 1033 + "without user data directory"); 1034 return; 1035 } else { 1036 logger.log(Level.FINE, "Error handler has set " 1037 + "user data directory to [{0}], " 1038 + "retrying", nominalUserDataDir); 1039 continue; 1040 } 1041 } 1042 } 1043 1044 private static File defaultUserDataDirectory() { 1045 return new File( 1046 com.sun.glass.ui.Application.GetApplication() 1047 .getDataDirectory(), 1048 "webview"); 1049 } 1050 1051 private static void createDirectories(File directory) throws IOException { 1052 Path path = directory.toPath(); 1053 try { 1054 Files.createDirectories(path, PosixFilePermissions.asFileAttribute( 1055 PosixFilePermissions.fromString("rwx------"))); 1056 } catch (UnsupportedOperationException ex) { 1057 Files.createDirectories(path); 1058 } 1059 } 1060 1061 private void fireError(EventType<WebErrorEvent> eventType, String message, 1062 Throwable exception) 1063 { 1064 EventHandler<WebErrorEvent> handler = getOnError(); 1065 if (handler != null) { 1066 handler.handle(new WebErrorEvent(this, eventType, 1067 message, exception)); 1068 } 1069 } 1070 1071 // for testing purposes only 1072 void dispose() { 1073 disposer.dispose(); 1074 } 1075 1076 private static final class SelfDisposer implements DisposerRecord { 1077 private WebPage page; 1078 private DirectoryLock userDataDirectoryLock; 1079 1080 private SelfDisposer(WebPage page) { 1081 this.page = page; 1082 } 1083 1084 @Override public void dispose() { 1085 if (page == null) { 1086 return; 1087 } 1088 page.dispose(); 1089 page = null; 1090 if (userDataDirectoryLock != null) { 1091 userDataDirectoryLock.close(); 1092 } 1093 instanceCount--; 1094 if (instanceCount == 0 && 1095 Timer.getMode() == Timer.Mode.PLATFORM_TICKS) 1096 { 1097 PulseTimer.stop(); 1098 } 1099 } 1100 } 1101 1102 private static final class AccessorImpl extends Accessor { 1103 private final WeakReference<WebEngine> engine; 1104 1105 private AccessorImpl(WebEngine w) { 1106 this.engine = new WeakReference<WebEngine>(w); 1107 } 1108 1109 @Override public WebEngine getEngine() { 1110 return engine.get(); 1111 } 1112 1113 @Override public WebPage getPage() { 1114 WebEngine w = getEngine(); 1115 return w == null ? null : w.page; 1116 } 1117 1118 @Override public WebView getView() { 1119 WebEngine w = getEngine(); 1120 return w == null ? null : w.view.get(); 1121 } 1122 1123 @Override public void addChild(Node child) { 1124 WebView view = getView(); 1125 if (view != null) { 1126 view.getChildren().add(child); 1127 } 1128 } 1129 1130 @Override public void removeChild(Node child) { 1131 WebView view = getView(); 1132 if (view != null) { 1133 view.getChildren().remove(child); 1134 } 1135 } 1136 1137 @Override public void addViewListener(InvalidationListener l) { 1138 WebEngine w = getEngine(); 1139 if (w != null) { 1140 w.view.addListener(l); 1141 } 1142 } 1143 } 1144 1145 /** 1146 * Drives the {@code Timer} when {@code Timer.Mode.PLATFORM_TICKS} is set. 1147 */ 1148 private static final class PulseTimer { 1149 1150 // Used just to guarantee constant pulse activity. See RT-14433. 1151 private static final AnimationTimer animation = 1152 new AnimationTimer() { 1153 @Override public void handle(long l) {} 1154 }; 1155 1156 private static final TKPulseListener listener = 1157 () -> { 1158 // Note, the timer event is executed right in the notifyTick(), 1159 // that is during the pulse event. This makes the timer more 1160 // repsonsive, though prolongs the pulse. So far it causes no 1161 // problems but nevertheless it should be kept in mind. 1162 1163 // Execute notifyTick in runLater to run outside of pulse so 1164 // that events will run in order and be able to display dialogs 1165 // or call other methods that require a nested event loop. 1166 Platform.runLater(() -> Timer.getTimer().notifyTick()); 1167 }; 1168 1169 private static void start(){ 1170 Toolkit.getToolkit().addSceneTkPulseListener(listener); 1171 animation.start(); 1172 } 1173 1174 private static void stop() { 1175 Toolkit.getToolkit().removeSceneTkPulseListener(listener); 1176 animation.stop(); 1177 } 1178 } 1179 1180 static void checkThread() { 1181 Toolkit.getToolkit().checkFxUserThread(); 1182 } 1183 1184 1185 /** 1186 * The page load event listener. This object references the owner 1187 * WebEngine weakly so as to avoid referencing WebEngine from WebPage 1188 * strongly. 1189 */ 1190 private static final class PageLoadListener implements LoadListenerClient { 1191 1192 private final WeakReference<WebEngine> engine; 1193 1194 1195 private PageLoadListener(WebEngine engine) { 1196 this.engine = new WeakReference<WebEngine>(engine); 1197 } 1198 1199 1200 @Override public void dispatchLoadEvent(long frame, int state, 1201 String url, String contentType, double progress, int errorCode) 1202 { 1203 WebEngine w = engine.get(); 1204 if (w != null) { 1205 w.loadWorker.dispatchLoadEvent(frame, state, url, 1206 contentType, progress, errorCode); 1207 } 1208 } 1209 1210 @Override public void dispatchResourceLoadEvent(long frame, 1211 int state, String url, String contentType, double progress, 1212 int errorCode) 1213 { 1214 } 1215 } 1216 1217 1218 private final class LoadWorker implements Worker<Void> { 1219 1220 private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<State>(this, "state", State.READY); 1221 @Override public final State getState() { checkThread(); return state.get(); } 1222 @Override public final ReadOnlyObjectProperty<State> stateProperty() { checkThread(); return state.getReadOnlyProperty(); } 1223 private void updateState(State value) { 1224 checkThread(); 1225 this.state.set(value); 1226 running.set(value == State.SCHEDULED || value == State.RUNNING); 1227 } 1228 1229 /** 1230 * @InheritDoc 1231 */ 1232 private final ReadOnlyObjectWrapper<Void> value = new ReadOnlyObjectWrapper<Void>(this, "value", null); 1233 @Override public final Void getValue() { checkThread(); return value.get(); } 1234 @Override public final ReadOnlyObjectProperty<Void> valueProperty() { checkThread(); return value.getReadOnlyProperty(); } 1235 1236 /** 1237 * @InheritDoc 1238 */ 1239 private final ReadOnlyObjectWrapper<Throwable> exception = new ReadOnlyObjectWrapper<Throwable>(this, "exception"); 1240 @Override public final Throwable getException() { checkThread(); return exception.get(); } 1241 @Override public final ReadOnlyObjectProperty<Throwable> exceptionProperty() { checkThread(); return exception.getReadOnlyProperty(); } 1242 1243 /** 1244 * @InheritDoc 1245 */ 1246 private final ReadOnlyDoubleWrapper workDone = new ReadOnlyDoubleWrapper(this, "workDone", -1); 1247 @Override public final double getWorkDone() { checkThread(); return workDone.get(); } 1248 @Override public final ReadOnlyDoubleProperty workDoneProperty() { checkThread(); return workDone.getReadOnlyProperty(); } 1249 1250 /** 1251 * @InheritDoc 1252 */ 1253 private final ReadOnlyDoubleWrapper totalWorkToBeDone = new ReadOnlyDoubleWrapper(this, "totalWork", -1); 1254 @Override public final double getTotalWork() { checkThread(); return totalWorkToBeDone.get(); } 1255 @Override public final ReadOnlyDoubleProperty totalWorkProperty() { checkThread(); return totalWorkToBeDone.getReadOnlyProperty(); } 1256 1257 /** 1258 * @InheritDoc 1259 */ 1260 private final ReadOnlyDoubleWrapper progress = new ReadOnlyDoubleWrapper(this, "progress", -1); 1261 @Override public final double getProgress() { checkThread(); return progress.get(); } 1262 @Override public final ReadOnlyDoubleProperty progressProperty() { checkThread(); return progress.getReadOnlyProperty(); } 1263 private void updateProgress(double p) { 1264 totalWorkToBeDone.set(100.0); 1265 workDone.set(p * 100.0); 1266 progress.set(p); 1267 } 1268 1269 /** 1270 * @InheritDoc 1271 */ 1272 private final ReadOnlyBooleanWrapper running = new ReadOnlyBooleanWrapper(this, "running", false); 1273 @Override public final boolean isRunning() { checkThread(); return running.get(); } 1274 @Override public final ReadOnlyBooleanProperty runningProperty() { checkThread(); return running.getReadOnlyProperty(); } 1275 1276 /** 1277 * @InheritDoc 1278 */ 1279 private final ReadOnlyStringWrapper message = new ReadOnlyStringWrapper(this, "message", ""); 1280 @Override public final String getMessage() { return message.get(); } 1281 @Override public final ReadOnlyStringProperty messageProperty() { return message.getReadOnlyProperty(); } 1282 1283 /** 1284 * @InheritDoc 1285 */ 1286 private final ReadOnlyStringWrapper title = new ReadOnlyStringWrapper(this, "title", "WebEngine Loader"); 1287 @Override public final String getTitle() { return title.get(); } 1288 @Override public final ReadOnlyStringProperty titleProperty() { return title.getReadOnlyProperty(); } 1289 1290 /** 1291 * Cancels the loading of the page. If called after the page has already 1292 * been loaded, then this call takes no effect. 1293 */ 1294 @Override public boolean cancel() { 1295 if (isRunning()) { 1296 stop(); // this call indirectly sets state 1297 return true; 1298 } else { 1299 return false; 1300 } 1301 } 1302 1303 private void cancelAndReset() { 1304 cancel(); 1305 exception.set(null); 1306 message.set(""); 1307 totalWorkToBeDone.set(-1); 1308 workDone.set(-1); 1309 progress.set(-1); 1310 updateState(State.READY); 1311 running.set(false); 1312 } 1313 1314 private void dispatchLoadEvent(long frame, int state, 1315 String url, String contentType, double workDone, int errorCode) 1316 { 1317 if (frame != getMainFrame()) { 1318 return; 1319 } 1320 1321 switch (state) { 1322 case PAGE_STARTED: 1323 message.set("Loading " + url); 1324 updateLocation(url); 1325 updateProgress(0.0); 1326 updateState(State.SCHEDULED); 1327 updateState(State.RUNNING); 1328 break; 1329 case PAGE_REDIRECTED: 1330 message.set("Loading " + url); 1331 updateLocation(url); 1332 break; 1333 case PAGE_FINISHED: 1334 message.set("Loading complete"); 1335 updateProgress(1.0); 1336 updateState(State.SUCCEEDED); 1337 break; 1338 case LOAD_FAILED: 1339 message.set("Loading failed"); 1340 exception.set(describeError(errorCode)); 1341 updateState(State.FAILED); 1342 break; 1343 case LOAD_STOPPED: 1344 message.set("Loading stopped"); 1345 updateState(State.CANCELLED); 1346 break; 1347 case PROGRESS_CHANGED: 1348 updateProgress(workDone); 1349 break; 1350 case TITLE_RECEIVED: 1351 updateTitle(); 1352 break; 1353 case DOCUMENT_AVAILABLE: 1354 if (this.state.get() != State.RUNNING) { 1355 // We have empty load; send a synthetic event (RT-32097) 1356 dispatchLoadEvent(frame, PAGE_STARTED, url, contentType, workDone, errorCode); 1357 } 1358 document.invalidate(true); 1359 break; 1360 } 1361 } 1362 1363 private Throwable describeError(int errorCode) { 1364 String reason = "Unknown error"; 1365 1366 switch (errorCode) { 1367 case UNKNOWN_HOST: 1368 reason = "Unknown host"; 1369 break; 1370 case MALFORMED_URL: 1371 reason = "Malformed URL"; 1372 break; 1373 case SSL_HANDSHAKE: 1374 reason = "SSL handshake failed"; 1375 break; 1376 case CONNECTION_REFUSED: 1377 reason = "Connection refused by server"; 1378 break; 1379 case CONNECTION_RESET: 1380 reason = "Connection reset by server"; 1381 break; 1382 case NO_ROUTE_TO_HOST: 1383 reason = "No route to host"; 1384 break; 1385 case CONNECTION_TIMED_OUT: 1386 reason = "Connection timed out"; 1387 break; 1388 case PERMISSION_DENIED: 1389 reason = "Permission denied"; 1390 break; 1391 case INVALID_RESPONSE: 1392 reason = "Invalid response from server"; 1393 break; 1394 case TOO_MANY_REDIRECTS: 1395 reason = "Too many redirects"; 1396 break; 1397 case FILE_NOT_FOUND: 1398 reason = "File not found"; 1399 break; 1400 } 1401 return new Throwable(reason); 1402 } 1403 } 1404 1405 1406 private final class DocumentProperty 1407 extends ReadOnlyObjectPropertyBase<Document> { 1408 1409 private boolean available; 1410 private Document document; 1411 1412 private void invalidate(boolean available) { 1413 if (this.available || available) { 1414 this.available = available; 1415 this.document = null; 1416 fireValueChangedEvent(); 1417 } 1418 } 1419 1420 public Document get() { 1421 if (!this.available) { 1422 return null; 1423 } 1424 if (this.document == null) { 1425 this.document = page.getDocument(page.getMainFrame()); 1426 if (this.document == null) { 1427 this.available = false; 1428 } 1429 } 1430 return this.document; 1431 } 1432 1433 public Object getBean() { 1434 return WebEngine.this; 1435 } 1436 1437 public String getName() { 1438 return "document"; 1439 } 1440 } 1441 1442 1443 /* 1444 * Returns the debugger associated with this web engine. 1445 * The debugger is an object that can be used to debug 1446 * the web page currently loaded into the web engine. 1447 * <p> 1448 * All methods of the debugger must be called on 1449 * the JavaFX Application Thread. 1450 * The message callback object registered with the debugger 1451 * is always called on the JavaFX Application Thread. 1452 * @return the debugger associated with this web engine. 1453 * The return value cannot be {@code null}. 1454 */ 1455 Debugger getDebugger() { 1456 return debugger; 1457 } 1458 1459 /** 1460 * The debugger implementation. 1461 */ 1462 private final class DebuggerImpl implements Debugger { 1463 1464 private boolean enabled; 1465 private Callback<String,Void> messageCallback; 1466 1467 1468 @Override 1469 public boolean isEnabled() { 1470 checkThread(); 1471 return enabled; 1472 } 1473 1474 @Override 1475 public void setEnabled(boolean enabled) { 1476 checkThread(); 1477 if (enabled != this.enabled) { 1478 if (enabled) { 1479 page.setDeveloperExtrasEnabled(true); 1480 page.connectInspectorFrontend(); 1481 } else { 1482 page.disconnectInspectorFrontend(); 1483 page.setDeveloperExtrasEnabled(false); 1484 } 1485 this.enabled = enabled; 1486 } 1487 } 1488 1489 @Override 1490 public void sendMessage(String message) { 1491 checkThread(); 1492 if (!enabled) { 1493 throw new IllegalStateException("Debugger is not enabled"); 1494 } 1495 if (message == null) { 1496 throw new NullPointerException("message is null"); 1497 } 1498 page.dispatchInspectorMessageFromFrontend(message); 1499 } 1500 1501 @Override 1502 public Callback<String,Void> getMessageCallback() { 1503 checkThread(); 1504 return messageCallback; 1505 } 1506 1507 @Override 1508 public void setMessageCallback(Callback<String,Void> callback) { 1509 checkThread(); 1510 messageCallback = callback; 1511 } 1512 } 1513 1514 /** 1515 * The inspector client implementation. This object references the owner 1516 * WebEngine weakly so as to avoid referencing WebEngine from WebPage 1517 * strongly. 1518 */ 1519 private static final class InspectorClientImpl implements InspectorClient { 1520 1521 private final WeakReference<WebEngine> engine; 1522 1523 1524 private InspectorClientImpl(WebEngine engine) { 1525 this.engine = new WeakReference<WebEngine>(engine); 1526 } 1527 1528 1529 @Override 1530 public boolean sendMessageToFrontend(final String message) { 1531 boolean result = false; 1532 WebEngine webEngine = engine.get(); 1533 if (webEngine != null) { 1534 final Callback<String,Void> messageCallback = 1535 webEngine.debugger.messageCallback; 1536 if (messageCallback != null) { 1537 AccessController.doPrivileged((PrivilegedAction<Void>) () -> { 1538 messageCallback.call(message); 1539 return null; 1540 }, webEngine.page.getAccessControlContext()); 1541 result = true; 1542 } 1543 } 1544 return result; 1545 } 1546 } 1547 1548 private static final boolean printStatusOK(PrinterJob job) { 1549 switch (job.getJobStatus()) { 1550 case NOT_STARTED: 1551 case PRINTING: 1552 return true; 1553 default: 1554 return false; 1555 } 1556 } 1557 1558 /** 1559 * Prints the current Web page using the given printer job. 1560 * <p>This method does not modify the state of the job, nor does it call 1561 * {@link PrinterJob#endJob}, so the job may be safely reused afterwards. 1562 * 1563 * @param job printer job used for printing 1564 * @since JavaFX 8.0 1565 */ 1566 public void print(PrinterJob job) { 1567 if (!printStatusOK(job)) { 1568 return; 1569 } 1570 1571 PageLayout pl = job.getJobSettings().getPageLayout(); 1572 float width = (float) pl.getPrintableWidth(); 1573 float height = (float) pl.getPrintableHeight(); 1574 int pageCount = page.beginPrinting(width, height); 1575 1576 for (int i = 0; i < pageCount; i++) { 1577 if (printStatusOK(job)) { 1578 Node printable = new Printable(page, i, width); 1579 job.printPage(printable); 1580 } 1581 } 1582 page.endPrinting(); 1583 } 1584 }