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> 301 * The Java class and method must both be declared public. If the class is in a 302 * named module, then the module must {@link Module#isOpen(String,Module) open} 303 * the containing package to at least the {@code javafx.web} module 304 * (or {@link Module#isExported(String) export} the containing package 305 * unconditionally). Otherwise, the method will not be called, and no error or 306 * warning will be produced. 307 * </p> 308 * 309 * <p><b>Threading</b></p> 310 * <p>{@code WebEngine} objects must be created and accessed solely from the 311 * JavaFX Application thread. This rule also applies to any DOM and JavaScript 312 * objects obtained from the {@code WebEngine} object. 313 * @since JavaFX 2.0 314 */ 315 final public class WebEngine { 316 static { 317 Accessor.setPageAccessor(w -> w == null ? null : w.getPage()); 318 319 Invoker.setInvoker(new PrismInvoker()); 320 Renderer.setRenderer(new PrismRenderer()); 321 WCGraphicsManager.setGraphicsManager(new PrismGraphicsManager()); 322 CursorManager.setCursorManager(new CursorManagerImpl()); 323 com.sun.webkit.EventLoop.setEventLoop(new EventLoopImpl()); 324 ThemeClient.setDefaultRenderTheme(new RenderThemeImpl()); 325 Utilities.setUtilities(new UtilitiesImpl()); 326 } 327 328 private static final Logger logger = 329 Logger.getLogger(WebEngine.class.getName()); 330 331 /** 332 * The number of instances of this class. 333 * Used to start and stop the pulse timer. 334 */ 335 private static int instanceCount = 0; 336 337 /** 338 * The node associated with this engine. There is a one-to-one correspondence 339 * between the WebView and its WebEngine (although not all WebEngines have 340 * a WebView, every WebView has one and only one WebEngine). 341 */ 342 private final ObjectProperty<WebView> view = new SimpleObjectProperty<WebView>(this, "view"); 343 344 /** 345 * The Worker which shows progress of the web engine as it loads pages. 346 */ 347 private final LoadWorker loadWorker = new LoadWorker(); 348 349 /** 350 * The object that provides interaction with the native webkit core. 351 */ 352 private final WebPage page; 353 354 private final SelfDisposer disposer; 355 356 private final DebuggerImpl debugger = new DebuggerImpl(); 357 358 private boolean userDataDirectoryApplied = false; 359 360 361 /** 362 * Returns a {@link javafx.concurrent.Worker} object that can be used to 363 * track loading progress. 364 * 365 * @return the {@code Worker} object 366 */ 367 public final Worker<Void> getLoadWorker() { 368 return loadWorker; 369 } 370 371 372 /* 373 * The final document. This may be null if no document has been loaded. 374 */ 375 private final DocumentProperty document = new DocumentProperty(); 376 377 public final Document getDocument() { return document.getValue(); } 378 379 /** 380 * Document object for the current Web page. The value is {@code null} 381 * if the Web page failed to load. 382 * 383 * @return the document property 384 */ 385 public final ReadOnlyObjectProperty<Document> documentProperty() { 386 return document; 387 } 388 389 390 /* 391 * The location of the current page. This may return null. 392 */ 393 private final ReadOnlyStringWrapper location = new ReadOnlyStringWrapper(this, "location"); 394 395 public final String getLocation() { return location.getValue(); } 396 397 /** 398 * URL of the current Web page. If the current page has no URL, 399 * the value is an empty String. 400 * 401 * @return the location property 402 */ 403 public final ReadOnlyStringProperty locationProperty() { return location.getReadOnlyProperty(); } 404 405 private void updateLocation(String value) { 406 this.location.set(value); 407 this.document.invalidate(false); 408 this.title.set(null); 409 } 410 411 412 /* 413 * The page title. 414 */ 415 private final ReadOnlyStringWrapper title = new ReadOnlyStringWrapper(this, "title"); 416 417 public final String getTitle() { return title.getValue(); } 418 419 /** 420 * Title of the current Web page. If the current page has no title, 421 * the value is {@code null}. 422 * 423 * @return the title property 424 */ 425 public final ReadOnlyStringProperty titleProperty() { return title.getReadOnlyProperty(); } 426 427 private void updateTitle() { 428 title.set(page.getTitle(page.getMainFrame())); 429 } 430 431 // 432 // Settings 433 434 /** 435 * Specifies whether JavaScript execution is enabled. 436 * 437 * @defaultValue true 438 * @since JavaFX 2.2 439 */ 440 private BooleanProperty javaScriptEnabled; 441 442 public final void setJavaScriptEnabled(boolean value) { 443 javaScriptEnabledProperty().set(value); 444 } 445 446 public final boolean isJavaScriptEnabled() { 447 return javaScriptEnabled == null ? true : javaScriptEnabled.get(); 448 } 449 450 public final BooleanProperty javaScriptEnabledProperty() { 451 if (javaScriptEnabled == null) { 452 javaScriptEnabled = new BooleanPropertyBase(true) { 453 @Override public void invalidated() { 454 checkThread(); 455 page.setJavaScriptEnabled(get()); 456 } 457 458 @Override public Object getBean() { 459 return WebEngine.this; 460 } 461 462 @Override public String getName() { 463 return "javaScriptEnabled"; 464 } 465 }; 466 } 467 return javaScriptEnabled; 468 } 469 470 /** 471 * Location of the user stylesheet as a string URL. 472 * 473 * <p>This should be a local URL, i.e. either {@code 'data:'}, 474 * {@code 'file:'}, or {@code 'jar:'}. Remote URLs are not allowed 475 * for security reasons. 476 * 477 * @defaultValue null 478 * @since JavaFX 2.2 479 */ 480 private StringProperty userStyleSheetLocation; 481 482 public final void setUserStyleSheetLocation(String value) { 483 userStyleSheetLocationProperty().set(value); 484 } 485 486 public final String getUserStyleSheetLocation() { 487 return userStyleSheetLocation == null ? null : userStyleSheetLocation.get(); 488 } 489 490 private byte[] readFully(BufferedInputStream in) throws IOException { 491 final int BUF_SIZE = 4096; 492 int outSize = 0; 493 final List<byte[]> outList = new ArrayList<>(); 494 byte[] buffer = new byte[BUF_SIZE]; 495 496 while (true) { 497 int nBytes = in.read(buffer); 498 if (nBytes < 0) break; 499 500 byte[] chunk; 501 if (nBytes == buffer.length) { 502 chunk = buffer; 503 buffer = new byte[BUF_SIZE]; 504 } else { 505 chunk = new byte[nBytes]; 506 System.arraycopy(buffer, 0, chunk, 0, nBytes); 507 } 508 outList.add(chunk); 509 outSize += nBytes; 510 } 511 512 final byte[] out = new byte[outSize]; 513 int outPos = 0; 514 for (byte[] chunk : outList) { 515 System.arraycopy(chunk, 0, out, outPos, chunk.length); 516 outPos += chunk.length; 517 } 518 519 return out; 520 } 521 522 public final StringProperty userStyleSheetLocationProperty() { 523 if (userStyleSheetLocation == null) { 524 userStyleSheetLocation = new StringPropertyBase(null) { 525 private final static String DATA_PREFIX = "data:text/css;charset=utf-8;base64,"; 526 527 @Override public void invalidated() { 528 checkThread(); 529 String url = get(); 530 String dataUrl; 531 if (url == null || url.length() <= 0) { 532 dataUrl = null; 533 } else if (url.startsWith(DATA_PREFIX)) { 534 dataUrl = url; 535 } else if (url.startsWith("file:") || 536 url.startsWith("jar:") || 537 url.startsWith("data:")) 538 { 539 try { 540 URLConnection conn = URLs.newURL(url).openConnection(); 541 conn.connect(); 542 543 BufferedInputStream in = 544 new BufferedInputStream(conn.getInputStream()); 545 byte[] inBytes = readFully(in); 546 String out = Base64.getMimeEncoder().encodeToString(inBytes); 547 dataUrl = DATA_PREFIX + out; 548 } catch (IOException e) { 549 throw new RuntimeException(e); 550 } 551 } else { 552 throw new IllegalArgumentException("Invalid stylesheet URL"); 553 } 554 page.setUserStyleSheetLocation(dataUrl); 555 } 556 557 @Override public Object getBean() { 558 return WebEngine.this; 559 } 560 561 @Override public String getName() { 562 return "userStyleSheetLocation"; 563 } 564 }; 565 } 566 return userStyleSheetLocation; 567 } 568 569 /** 570 * Specifies the directory to be used by this {@code WebEngine} 571 * to store local user data. 572 * 573 * <p>If the value of this property is not {@code null}, 574 * the {@code WebEngine} will attempt to store local user data 575 * in the respective directory. 576 * If the value of this property is {@code null}, 577 * the {@code WebEngine} will attempt to store local user data 578 * in an automatically selected system-dependent user- and 579 * application-specific directory. 580 * 581 * <p>When a {@code WebEngine} is about to start loading a web 582 * page or executing a script for the first time, it checks whether 583 * it can actually use the directory specified by this property. 584 * If the check fails for some reason, the {@code WebEngine} invokes 585 * the {@link WebEngine#onErrorProperty WebEngine.onError} event handler, 586 * if any, with a {@link WebErrorEvent} describing the reason. 587 * If the invoked event handler modifies the {@code userDataDirectory} 588 * property, the {@code WebEngine} retries with the new value as soon 589 * as the handler returns. If the handler does not modify the 590 * {@code userDataDirectory} property (which is the default), 591 * the {@code WebEngine} continues without local user data. 592 * 593 * <p>Once the {@code WebEngine} has started loading a web page or 594 * executing a script, changes made to this property have no effect 595 * on where the {@code WebEngine} stores or will store local user 596 * data. 597 * 598 * <p>Currently, the directory specified by this property is used 599 * only to store the data that backs the {@code window.localStorage} 600 * objects. In the future, more types of data can be added. 601 * 602 * @defaultValue {@code null} 603 * @since JavaFX 8.0 604 */ 605 private final ObjectProperty<File> userDataDirectory = 606 new SimpleObjectProperty<>(this, "userDataDirectory"); 607 608 public final File getUserDataDirectory() { 609 return userDataDirectory.get(); 610 } 611 612 public final void setUserDataDirectory(File value) { 613 userDataDirectory.set(value); 614 } 615 616 public final ObjectProperty<File> userDataDirectoryProperty() { 617 return userDataDirectory; 618 } 619 620 /** 621 * Specifies user agent ID string. This string is the value of the 622 * {@code User-Agent} HTTP header. 623 * 624 * @defaultValue system dependent 625 * @since JavaFX 8.0 626 */ 627 private StringProperty userAgent; 628 629 public final void setUserAgent(String value) { 630 userAgentProperty().set(value); 631 } 632 633 public final String getUserAgent() { 634 return userAgent == null ? page.getUserAgent() : userAgent.get(); 635 } 636 637 public final StringProperty userAgentProperty() { 638 if (userAgent == null) { 639 userAgent = new StringPropertyBase(page.getUserAgent()) { 640 @Override public void invalidated() { 641 checkThread(); 642 page.setUserAgent(get()); 643 } 644 645 @Override public Object getBean() { 646 return WebEngine.this; 647 } 648 649 @Override public String getName() { 650 return "userAgent"; 651 } 652 }; 653 } 654 return userAgent; 655 } 656 657 private final ObjectProperty<EventHandler<WebEvent<String>>> onAlert 658 = new SimpleObjectProperty<EventHandler<WebEvent<String>>>(this, "onAlert"); 659 660 public final EventHandler<WebEvent<String>> getOnAlert() { return onAlert.get(); } 661 662 public final void setOnAlert(EventHandler<WebEvent<String>> handler) { onAlert.set(handler); } 663 664 /** 665 * JavaScript {@code alert} handler property. This handler is invoked 666 * when a script running on the Web page calls the {@code alert} function. 667 * @return the onAlert property 668 */ 669 public final ObjectProperty<EventHandler<WebEvent<String>>> onAlertProperty() { return onAlert; } 670 671 672 private final ObjectProperty<EventHandler<WebEvent<String>>> onStatusChanged 673 = new SimpleObjectProperty<EventHandler<WebEvent<String>>>(this, "onStatusChanged"); 674 675 public final EventHandler<WebEvent<String>> getOnStatusChanged() { return onStatusChanged.get(); } 676 677 public final void setOnStatusChanged(EventHandler<WebEvent<String>> handler) { onStatusChanged.set(handler); } 678 679 /** 680 * JavaScript status handler property. This handler is invoked when 681 * a script running on the Web page sets {@code window.status} property. 682 * @return the onStatusChanged property 683 */ 684 public final ObjectProperty<EventHandler<WebEvent<String>>> onStatusChangedProperty() { return onStatusChanged; } 685 686 687 private final ObjectProperty<EventHandler<WebEvent<Rectangle2D>>> onResized 688 = new SimpleObjectProperty<EventHandler<WebEvent<Rectangle2D>>>(this, "onResized"); 689 690 public final EventHandler<WebEvent<Rectangle2D>> getOnResized() { return onResized.get(); } 691 692 public final void setOnResized(EventHandler<WebEvent<Rectangle2D>> handler) { onResized.set(handler); } 693 694 /** 695 * JavaScript window resize handler property. This handler is invoked 696 * when a script running on the Web page moves or resizes the 697 * {@code window} object. 698 * @return the onResized property 699 */ 700 public final ObjectProperty<EventHandler<WebEvent<Rectangle2D>>> onResizedProperty() { return onResized; } 701 702 703 private final ObjectProperty<EventHandler<WebEvent<Boolean>>> onVisibilityChanged 704 = new SimpleObjectProperty<EventHandler<WebEvent<Boolean>>>(this, "onVisibilityChanged"); 705 706 public final EventHandler<WebEvent<Boolean>> getOnVisibilityChanged() { return onVisibilityChanged.get(); } 707 708 public final void setOnVisibilityChanged(EventHandler<WebEvent<Boolean>> handler) { onVisibilityChanged.set(handler); } 709 710 /** 711 * JavaScript window visibility handler property. This handler is invoked 712 * when a script running on the Web page changes visibility of the 713 * {@code window} object. 714 * @return the onVisibilityChanged property 715 */ 716 public final ObjectProperty<EventHandler<WebEvent<Boolean>>> onVisibilityChangedProperty() { return onVisibilityChanged; } 717 718 719 private final ObjectProperty<Callback<PopupFeatures, WebEngine>> createPopupHandler 720 = new SimpleObjectProperty<Callback<PopupFeatures, WebEngine>>(this, "createPopupHandler", 721 p -> WebEngine.this); 722 723 public final Callback<PopupFeatures, WebEngine> getCreatePopupHandler() { return createPopupHandler.get(); } 724 725 public final void setCreatePopupHandler(Callback<PopupFeatures, WebEngine> handler) { createPopupHandler.set(handler); } 726 727 /** 728 * JavaScript popup handler property. This handler is invoked when a script 729 * running on the Web page requests a popup to be created. 730 * <p>To satisfy this request a handler may create a new {@code WebEngine}, 731 * attach a visibility handler and optionally a resize handler, and return 732 * the newly created engine. To block the popup, a handler should return 733 * {@code null}. 734 * <p>By default, a popup handler is installed that opens popups in this 735 * {@code WebEngine}. 736 * 737 * @return the createPopupHandler property 738 * 739 * @see PopupFeatures 740 */ 741 public final ObjectProperty<Callback<PopupFeatures, WebEngine>> createPopupHandlerProperty() { return createPopupHandler; } 742 743 744 private final ObjectProperty<Callback<String, Boolean>> confirmHandler 745 = new SimpleObjectProperty<Callback<String, Boolean>>(this, "confirmHandler"); 746 747 public final Callback<String, Boolean> getConfirmHandler() { return confirmHandler.get(); } 748 749 public final void setConfirmHandler(Callback<String, Boolean> handler) { confirmHandler.set(handler); } 750 751 /** 752 * JavaScript {@code confirm} handler property. This handler is invoked 753 * when a script running on the Web page calls the {@code confirm} function. 754 * <p>An implementation may display a dialog box with Yes and No options, 755 * and return the user's choice. 756 * 757 * @return the confirmHandler property 758 */ 759 public final ObjectProperty<Callback<String, Boolean>> confirmHandlerProperty() { return confirmHandler; } 760 761 762 private final ObjectProperty<Callback<PromptData, String>> promptHandler 763 = new SimpleObjectProperty<Callback<PromptData, String>>(this, "promptHandler"); 764 765 public final Callback<PromptData, String> getPromptHandler() { return promptHandler.get(); } 766 767 public final void setPromptHandler(Callback<PromptData, String> handler) { promptHandler.set(handler); } 768 769 /** 770 * JavaScript {@code prompt} handler property. This handler is invoked 771 * when a script running on the Web page calls the {@code prompt} function. 772 * <p>An implementation may display a dialog box with an text field, 773 * and return the user's input. 774 * 775 * @return the promptHandler property 776 * @see PromptData 777 */ 778 public final ObjectProperty<Callback<PromptData, String>> promptHandlerProperty() { return promptHandler; } 779 780 /** 781 * The event handler called when an error occurs. 782 * 783 * @defaultValue {@code null} 784 * @since JavaFX 8.0 785 */ 786 private final ObjectProperty<EventHandler<WebErrorEvent>> onError = 787 new SimpleObjectProperty<>(this, "onError"); 788 789 public final EventHandler<WebErrorEvent> getOnError() { 790 return onError.get(); 791 } 792 793 public final void setOnError(EventHandler<WebErrorEvent> handler) { 794 onError.set(handler); 795 } 796 797 public final ObjectProperty<EventHandler<WebErrorEvent>> onErrorProperty() { 798 return onError; 799 } 800 801 802 /** 803 * Creates a new engine. 804 */ 805 public WebEngine() { 806 this(null, false); 807 } 808 809 /** 810 * Creates a new engine and loads a Web page into it. 811 * 812 * @param url the URL of the web page to load 813 */ 814 public WebEngine(String url) { 815 this(url, true); 816 } 817 818 private WebEngine(String url, boolean callLoad) { 819 checkThread(); 820 Accessor accessor = new AccessorImpl(this); 821 page = new WebPage( 822 new WebPageClientImpl(accessor), 823 new UIClientImpl(accessor), 824 null, 825 new InspectorClientImpl(this), 826 new ThemeClientImpl(accessor), 827 false); 828 page.addLoadListenerClient(new PageLoadListener(this)); 829 830 history = new WebHistory(page); 831 832 disposer = new SelfDisposer(page); 833 Disposer.addRecord(this, disposer); 834 835 if (callLoad) { 836 load(url); 837 } 838 839 if (instanceCount == 0 && 840 Timer.getMode() == Timer.Mode.PLATFORM_TICKS) 841 { 842 PulseTimer.start(); 843 } 844 instanceCount++; 845 } 846 847 /** 848 * Loads a Web page into this engine. This method starts asynchronous 849 * loading and returns immediately. 850 * @param url URL of the web page to load 851 */ 852 public void load(String url) { 853 checkThread(); 854 loadWorker.cancelAndReset(); 855 856 if (url == null || url.equals("") || url.equals("about:blank")) { 857 url = ""; 858 } else { 859 // verify and, if possible, adjust the url on the Java 860 // side, otherwise it may crash native code 861 try { 862 url = Util.adjustUrlForWebKit(url); 863 } catch (MalformedURLException e) { 864 loadWorker.dispatchLoadEvent(getMainFrame(), 865 PAGE_STARTED, url, null, 0.0, 0); 866 loadWorker.dispatchLoadEvent(getMainFrame(), 867 LOAD_FAILED, url, null, 0.0, MALFORMED_URL); 868 return; 869 } 870 } 871 applyUserDataDirectory(); 872 page.open(page.getMainFrame(), url); 873 } 874 875 /** 876 * Loads the given HTML content directly. This method is useful when you have an HTML 877 * String composed in memory, or loaded from some system which cannot be reached via 878 * a URL (for example, the HTML text may have come from a database). As with 879 * {@link #load(String)}, this method is asynchronous. 880 * 881 * @param content the HTML content to load 882 */ 883 public void loadContent(String content) { 884 loadContent(content, "text/html"); 885 } 886 887 /** 888 * Loads the given content directly. This method is useful when you have content 889 * composed in memory, or loaded from some system which cannot be reached via 890 * a URL (for example, the SVG text may have come from a database). As with 891 * {@link #load(String)}, this method is asynchronous. This method also allows you to 892 * specify the content type of the string being loaded, and so may optionally support 893 * other types besides just HTML. 894 * 895 * @param content the HTML content to load 896 * @param contentType the type of content to load 897 */ 898 public void loadContent(String content, String contentType) { 899 checkThread(); 900 loadWorker.cancelAndReset(); 901 applyUserDataDirectory(); 902 page.load(page.getMainFrame(), content, contentType); 903 } 904 905 /** 906 * Reloads the current page, whether loaded from URL or directly from a String in 907 * one of the {@code loadContent} methods. 908 */ 909 public void reload() { 910 // TODO what happens if this is called while currently loading a page? 911 checkThread(); 912 page.refresh(page.getMainFrame()); 913 } 914 915 private final WebHistory history; 916 917 /** 918 * Returns the session history object. 919 * 920 * @return history object 921 * @since JavaFX 2.2 922 */ 923 public WebHistory getHistory() { 924 return history; 925 } 926 927 /** 928 * Executes a script in the context of the current page. 929 * 930 * @param script the script 931 * @return execution result, converted to a Java object using the following 932 * rules: 933 * <ul> 934 * <li>JavaScript Int32 is converted to {@code java.lang.Integer} 935 * <li>Other JavaScript numbers to {@code java.lang.Double} 936 * <li>JavaScript string to {@code java.lang.String} 937 * <li>JavaScript boolean to {@code java.lang.Boolean} 938 * <li>JavaScript {@code null} to {@code null} 939 * <li>Most JavaScript objects get wrapped as 940 * {@code netscape.javascript.JSObject} 941 * <li>JavaScript JSNode objects get mapped to instances of 942 * {@code netscape.javascript.JSObject}, that also implement 943 * {@code org.w3c.dom.Node} 944 * <li>A special case is the JavaScript class {@code JavaRuntimeObject} 945 * which is used to wrap a Java object as a JavaScript value - in this 946 * case we just extract the original Java value. 947 * </ul> 948 */ 949 public Object executeScript(String script) { 950 checkThread(); 951 applyUserDataDirectory(); 952 return page.executeScript(page.getMainFrame(), script); 953 } 954 955 private long getMainFrame() { 956 return page.getMainFrame(); 957 } 958 959 WebPage getPage() { 960 return page; 961 } 962 963 void setView(WebView view) { 964 this.view.setValue(view); 965 } 966 967 private void stop() { 968 checkThread(); 969 page.stop(page.getMainFrame()); 970 } 971 972 private void applyUserDataDirectory() { 973 if (userDataDirectoryApplied) { 974 return; 975 } 976 userDataDirectoryApplied = true; 977 File nominalUserDataDir = getUserDataDirectory(); 978 while (true) { 979 File userDataDir; 980 String displayString; 981 if (nominalUserDataDir == null) { 982 userDataDir = defaultUserDataDirectory(); 983 displayString = format("null (%s)", userDataDir); 984 } else { 985 userDataDir = nominalUserDataDir; 986 displayString = userDataDir.toString(); 987 } 988 logger.log(Level.FINE, "Trying to apply user data " 989 + "directory [{0}]", displayString); 990 String errorMessage; 991 EventType<WebErrorEvent> errorType; 992 Throwable error; 993 try { 994 userDataDir = DirectoryLock.canonicalize(userDataDir); 995 File localStorageDir = new File(userDataDir, "localstorage"); 996 File[] dirs = new File[] { 997 userDataDir, 998 localStorageDir, 999 }; 1000 for (File dir : dirs) { 1001 createDirectories(dir); 1002 // Additional security check to make sure the caller 1003 // has permission to write to the target directory 1004 File test = new File(dir, ".test"); 1005 if (test.createNewFile()) { 1006 test.delete(); 1007 } 1008 } 1009 disposer.userDataDirectoryLock = new DirectoryLock(userDataDir); 1010 1011 page.setLocalStorageDatabasePath(localStorageDir.getPath()); 1012 page.setLocalStorageEnabled(true); 1013 1014 logger.log(Level.FINE, "User data directory [{0}] has " 1015 + "been applied successfully", displayString); 1016 return; 1017 1018 } catch (DirectoryLock.DirectoryAlreadyInUseException ex) { 1019 errorMessage = "User data directory [%s] is already in use"; 1020 errorType = WebErrorEvent.USER_DATA_DIRECTORY_ALREADY_IN_USE; 1021 error = ex; 1022 } catch (IOException ex) { 1023 errorMessage = "An I/O error occurred while setting up " 1024 + "user data directory [%s]"; 1025 errorType = WebErrorEvent.USER_DATA_DIRECTORY_IO_ERROR; 1026 error = ex; 1027 } catch (SecurityException ex) { 1028 errorMessage = "A security error occurred while setting up " 1029 + "user data directory [%s]"; 1030 errorType = WebErrorEvent.USER_DATA_DIRECTORY_SECURITY_ERROR; 1031 error = ex; 1032 } 1033 1034 errorMessage = format(errorMessage, displayString); 1035 logger.log(Level.FINE, "{0}, calling error handler", errorMessage); 1036 File oldNominalUserDataDir = nominalUserDataDir; 1037 fireError(errorType, errorMessage, error); 1038 nominalUserDataDir = getUserDataDirectory(); 1039 if (Objects.equals(nominalUserDataDir, oldNominalUserDataDir)) { 1040 logger.log(Level.FINE, "Error handler did not " 1041 + "modify user data directory, continuing " 1042 + "without user data directory"); 1043 return; 1044 } else { 1045 logger.log(Level.FINE, "Error handler has set " 1046 + "user data directory to [{0}], " 1047 + "retrying", nominalUserDataDir); 1048 continue; 1049 } 1050 } 1051 } 1052 1053 private static File defaultUserDataDirectory() { 1054 return new File( 1055 com.sun.glass.ui.Application.GetApplication() 1056 .getDataDirectory(), 1057 "webview"); 1058 } 1059 1060 private static void createDirectories(File directory) throws IOException { 1061 Path path = directory.toPath(); 1062 try { 1063 Files.createDirectories(path, PosixFilePermissions.asFileAttribute( 1064 PosixFilePermissions.fromString("rwx------"))); 1065 } catch (UnsupportedOperationException ex) { 1066 Files.createDirectories(path); 1067 } 1068 } 1069 1070 private void fireError(EventType<WebErrorEvent> eventType, String message, 1071 Throwable exception) 1072 { 1073 EventHandler<WebErrorEvent> handler = getOnError(); 1074 if (handler != null) { 1075 handler.handle(new WebErrorEvent(this, eventType, 1076 message, exception)); 1077 } 1078 } 1079 1080 // for testing purposes only 1081 void dispose() { 1082 disposer.dispose(); 1083 } 1084 1085 private static final class SelfDisposer implements DisposerRecord { 1086 private WebPage page; 1087 private DirectoryLock userDataDirectoryLock; 1088 1089 private SelfDisposer(WebPage page) { 1090 this.page = page; 1091 } 1092 1093 @Override public void dispose() { 1094 if (page == null) { 1095 return; 1096 } 1097 page.dispose(); 1098 page = null; 1099 if (userDataDirectoryLock != null) { 1100 userDataDirectoryLock.close(); 1101 } 1102 instanceCount--; 1103 if (instanceCount == 0 && 1104 Timer.getMode() == Timer.Mode.PLATFORM_TICKS) 1105 { 1106 PulseTimer.stop(); 1107 } 1108 } 1109 } 1110 1111 private static final class AccessorImpl extends Accessor { 1112 private final WeakReference<WebEngine> engine; 1113 1114 private AccessorImpl(WebEngine w) { 1115 this.engine = new WeakReference<WebEngine>(w); 1116 } 1117 1118 @Override public WebEngine getEngine() { 1119 return engine.get(); 1120 } 1121 1122 @Override public WebPage getPage() { 1123 WebEngine w = getEngine(); 1124 return w == null ? null : w.page; 1125 } 1126 1127 @Override public WebView getView() { 1128 WebEngine w = getEngine(); 1129 return w == null ? null : w.view.get(); 1130 } 1131 1132 @Override public void addChild(Node child) { 1133 WebView view = getView(); 1134 if (view != null) { 1135 view.getChildren().add(child); 1136 } 1137 } 1138 1139 @Override public void removeChild(Node child) { 1140 WebView view = getView(); 1141 if (view != null) { 1142 view.getChildren().remove(child); 1143 } 1144 } 1145 1146 @Override public void addViewListener(InvalidationListener l) { 1147 WebEngine w = getEngine(); 1148 if (w != null) { 1149 w.view.addListener(l); 1150 } 1151 } 1152 } 1153 1154 /** 1155 * Drives the {@code Timer} when {@code Timer.Mode.PLATFORM_TICKS} is set. 1156 */ 1157 private static final class PulseTimer { 1158 1159 // Used just to guarantee constant pulse activity. See RT-14433. 1160 private static final AnimationTimer animation = 1161 new AnimationTimer() { 1162 @Override public void handle(long l) {} 1163 }; 1164 1165 private static final TKPulseListener listener = 1166 () -> { 1167 // Note, the timer event is executed right in the notifyTick(), 1168 // that is during the pulse event. This makes the timer more 1169 // repsonsive, though prolongs the pulse. So far it causes no 1170 // problems but nevertheless it should be kept in mind. 1171 1172 // Execute notifyTick in runLater to run outside of pulse so 1173 // that events will run in order and be able to display dialogs 1174 // or call other methods that require a nested event loop. 1175 Platform.runLater(() -> Timer.getTimer().notifyTick()); 1176 }; 1177 1178 private static void start(){ 1179 Toolkit.getToolkit().addSceneTkPulseListener(listener); 1180 animation.start(); 1181 } 1182 1183 private static void stop() { 1184 Toolkit.getToolkit().removeSceneTkPulseListener(listener); 1185 animation.stop(); 1186 } 1187 } 1188 1189 static void checkThread() { 1190 Toolkit.getToolkit().checkFxUserThread(); 1191 } 1192 1193 1194 /** 1195 * The page load event listener. This object references the owner 1196 * WebEngine weakly so as to avoid referencing WebEngine from WebPage 1197 * strongly. 1198 */ 1199 private static final class PageLoadListener implements LoadListenerClient { 1200 1201 private final WeakReference<WebEngine> engine; 1202 1203 1204 private PageLoadListener(WebEngine engine) { 1205 this.engine = new WeakReference<WebEngine>(engine); 1206 } 1207 1208 1209 @Override public void dispatchLoadEvent(long frame, int state, 1210 String url, String contentType, double progress, int errorCode) 1211 { 1212 WebEngine w = engine.get(); 1213 if (w != null) { 1214 w.loadWorker.dispatchLoadEvent(frame, state, url, 1215 contentType, progress, errorCode); 1216 } 1217 } 1218 1219 @Override public void dispatchResourceLoadEvent(long frame, 1220 int state, String url, String contentType, double progress, 1221 int errorCode) 1222 { 1223 } 1224 } 1225 1226 1227 private final class LoadWorker implements Worker<Void> { 1228 1229 private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<State>(this, "state", State.READY); 1230 @Override public final State getState() { checkThread(); return state.get(); } 1231 @Override public final ReadOnlyObjectProperty<State> stateProperty() { checkThread(); return state.getReadOnlyProperty(); } 1232 private void updateState(State value) { 1233 checkThread(); 1234 this.state.set(value); 1235 running.set(value == State.SCHEDULED || value == State.RUNNING); 1236 } 1237 1238 /** 1239 * @InheritDoc 1240 */ 1241 private final ReadOnlyObjectWrapper<Void> value = new ReadOnlyObjectWrapper<Void>(this, "value", null); 1242 @Override public final Void getValue() { checkThread(); return value.get(); } 1243 @Override public final ReadOnlyObjectProperty<Void> valueProperty() { checkThread(); return value.getReadOnlyProperty(); } 1244 1245 /** 1246 * @InheritDoc 1247 */ 1248 private final ReadOnlyObjectWrapper<Throwable> exception = new ReadOnlyObjectWrapper<Throwable>(this, "exception"); 1249 @Override public final Throwable getException() { checkThread(); return exception.get(); } 1250 @Override public final ReadOnlyObjectProperty<Throwable> exceptionProperty() { checkThread(); return exception.getReadOnlyProperty(); } 1251 1252 /** 1253 * @InheritDoc 1254 */ 1255 private final ReadOnlyDoubleWrapper workDone = new ReadOnlyDoubleWrapper(this, "workDone", -1); 1256 @Override public final double getWorkDone() { checkThread(); return workDone.get(); } 1257 @Override public final ReadOnlyDoubleProperty workDoneProperty() { checkThread(); return workDone.getReadOnlyProperty(); } 1258 1259 /** 1260 * @InheritDoc 1261 */ 1262 private final ReadOnlyDoubleWrapper totalWorkToBeDone = new ReadOnlyDoubleWrapper(this, "totalWork", -1); 1263 @Override public final double getTotalWork() { checkThread(); return totalWorkToBeDone.get(); } 1264 @Override public final ReadOnlyDoubleProperty totalWorkProperty() { checkThread(); return totalWorkToBeDone.getReadOnlyProperty(); } 1265 1266 /** 1267 * @InheritDoc 1268 */ 1269 private final ReadOnlyDoubleWrapper progress = new ReadOnlyDoubleWrapper(this, "progress", -1); 1270 @Override public final double getProgress() { checkThread(); return progress.get(); } 1271 @Override public final ReadOnlyDoubleProperty progressProperty() { checkThread(); return progress.getReadOnlyProperty(); } 1272 private void updateProgress(double p) { 1273 totalWorkToBeDone.set(100.0); 1274 workDone.set(p * 100.0); 1275 progress.set(p); 1276 } 1277 1278 /** 1279 * @InheritDoc 1280 */ 1281 private final ReadOnlyBooleanWrapper running = new ReadOnlyBooleanWrapper(this, "running", false); 1282 @Override public final boolean isRunning() { checkThread(); return running.get(); } 1283 @Override public final ReadOnlyBooleanProperty runningProperty() { checkThread(); return running.getReadOnlyProperty(); } 1284 1285 /** 1286 * @InheritDoc 1287 */ 1288 private final ReadOnlyStringWrapper message = new ReadOnlyStringWrapper(this, "message", ""); 1289 @Override public final String getMessage() { return message.get(); } 1290 @Override public final ReadOnlyStringProperty messageProperty() { return message.getReadOnlyProperty(); } 1291 1292 /** 1293 * @InheritDoc 1294 */ 1295 private final ReadOnlyStringWrapper title = new ReadOnlyStringWrapper(this, "title", "WebEngine Loader"); 1296 @Override public final String getTitle() { return title.get(); } 1297 @Override public final ReadOnlyStringProperty titleProperty() { return title.getReadOnlyProperty(); } 1298 1299 /** 1300 * Cancels the loading of the page. If called after the page has already 1301 * been loaded, then this call takes no effect. 1302 */ 1303 @Override public boolean cancel() { 1304 if (isRunning()) { 1305 stop(); // this call indirectly sets state 1306 return true; 1307 } else { 1308 return false; 1309 } 1310 } 1311 1312 private void cancelAndReset() { 1313 cancel(); 1314 exception.set(null); 1315 message.set(""); 1316 totalWorkToBeDone.set(-1); 1317 workDone.set(-1); 1318 progress.set(-1); 1319 updateState(State.READY); 1320 running.set(false); 1321 } 1322 1323 private void dispatchLoadEvent(long frame, int state, 1324 String url, String contentType, double workDone, int errorCode) 1325 { 1326 if (frame != getMainFrame()) { 1327 return; 1328 } 1329 1330 switch (state) { 1331 case PAGE_STARTED: 1332 message.set("Loading " + url); 1333 updateLocation(url); 1334 updateProgress(0.0); 1335 updateState(State.SCHEDULED); 1336 updateState(State.RUNNING); 1337 break; 1338 case PAGE_REDIRECTED: 1339 message.set("Loading " + url); 1340 updateLocation(url); 1341 break; 1342 case PAGE_FINISHED: 1343 message.set("Loading complete"); 1344 updateProgress(1.0); 1345 updateState(State.SUCCEEDED); 1346 break; 1347 case LOAD_FAILED: 1348 message.set("Loading failed"); 1349 exception.set(describeError(errorCode)); 1350 updateState(State.FAILED); 1351 break; 1352 case LOAD_STOPPED: 1353 message.set("Loading stopped"); 1354 updateState(State.CANCELLED); 1355 break; 1356 case PROGRESS_CHANGED: 1357 updateProgress(workDone); 1358 break; 1359 case TITLE_RECEIVED: 1360 updateTitle(); 1361 break; 1362 case DOCUMENT_AVAILABLE: 1363 if (this.state.get() != State.RUNNING) { 1364 // We have empty load; send a synthetic event (RT-32097) 1365 dispatchLoadEvent(frame, PAGE_STARTED, url, contentType, workDone, errorCode); 1366 } 1367 document.invalidate(true); 1368 break; 1369 } 1370 } 1371 1372 private Throwable describeError(int errorCode) { 1373 String reason = "Unknown error"; 1374 1375 switch (errorCode) { 1376 case UNKNOWN_HOST: 1377 reason = "Unknown host"; 1378 break; 1379 case MALFORMED_URL: 1380 reason = "Malformed URL"; 1381 break; 1382 case SSL_HANDSHAKE: 1383 reason = "SSL handshake failed"; 1384 break; 1385 case CONNECTION_REFUSED: 1386 reason = "Connection refused by server"; 1387 break; 1388 case CONNECTION_RESET: 1389 reason = "Connection reset by server"; 1390 break; 1391 case NO_ROUTE_TO_HOST: 1392 reason = "No route to host"; 1393 break; 1394 case CONNECTION_TIMED_OUT: 1395 reason = "Connection timed out"; 1396 break; 1397 case PERMISSION_DENIED: 1398 reason = "Permission denied"; 1399 break; 1400 case INVALID_RESPONSE: 1401 reason = "Invalid response from server"; 1402 break; 1403 case TOO_MANY_REDIRECTS: 1404 reason = "Too many redirects"; 1405 break; 1406 case FILE_NOT_FOUND: 1407 reason = "File not found"; 1408 break; 1409 } 1410 return new Throwable(reason); 1411 } 1412 } 1413 1414 1415 private final class DocumentProperty 1416 extends ReadOnlyObjectPropertyBase<Document> { 1417 1418 private boolean available; 1419 private Document document; 1420 1421 private void invalidate(boolean available) { 1422 if (this.available || available) { 1423 this.available = available; 1424 this.document = null; 1425 fireValueChangedEvent(); 1426 } 1427 } 1428 1429 public Document get() { 1430 if (!this.available) { 1431 return null; 1432 } 1433 if (this.document == null) { 1434 this.document = page.getDocument(page.getMainFrame()); 1435 if (this.document == null) { 1436 this.available = false; 1437 } 1438 } 1439 return this.document; 1440 } 1441 1442 public Object getBean() { 1443 return WebEngine.this; 1444 } 1445 1446 public String getName() { 1447 return "document"; 1448 } 1449 } 1450 1451 1452 /* 1453 * Returns the debugger associated with this web engine. 1454 * The debugger is an object that can be used to debug 1455 * the web page currently loaded into the web engine. 1456 * <p> 1457 * All methods of the debugger must be called on 1458 * the JavaFX Application Thread. 1459 * The message callback object registered with the debugger 1460 * is always called on the JavaFX Application Thread. 1461 * @return the debugger associated with this web engine. 1462 * The return value cannot be {@code null}. 1463 */ 1464 Debugger getDebugger() { 1465 return debugger; 1466 } 1467 1468 /** 1469 * The debugger implementation. 1470 */ 1471 private final class DebuggerImpl implements Debugger { 1472 1473 private boolean enabled; 1474 private Callback<String,Void> messageCallback; 1475 1476 1477 @Override 1478 public boolean isEnabled() { 1479 checkThread(); 1480 return enabled; 1481 } 1482 1483 @Override 1484 public void setEnabled(boolean enabled) { 1485 checkThread(); 1486 if (enabled != this.enabled) { 1487 if (enabled) { 1488 page.setDeveloperExtrasEnabled(true); 1489 page.connectInspectorFrontend(); 1490 } else { 1491 page.disconnectInspectorFrontend(); 1492 page.setDeveloperExtrasEnabled(false); 1493 } 1494 this.enabled = enabled; 1495 } 1496 } 1497 1498 @Override 1499 public void sendMessage(String message) { 1500 checkThread(); 1501 if (!enabled) { 1502 throw new IllegalStateException("Debugger is not enabled"); 1503 } 1504 if (message == null) { 1505 throw new NullPointerException("message is null"); 1506 } 1507 page.dispatchInspectorMessageFromFrontend(message); 1508 } 1509 1510 @Override 1511 public Callback<String,Void> getMessageCallback() { 1512 checkThread(); 1513 return messageCallback; 1514 } 1515 1516 @Override 1517 public void setMessageCallback(Callback<String,Void> callback) { 1518 checkThread(); 1519 messageCallback = callback; 1520 } 1521 } 1522 1523 /** 1524 * The inspector client implementation. This object references the owner 1525 * WebEngine weakly so as to avoid referencing WebEngine from WebPage 1526 * strongly. 1527 */ 1528 private static final class InspectorClientImpl implements InspectorClient { 1529 1530 private final WeakReference<WebEngine> engine; 1531 1532 1533 private InspectorClientImpl(WebEngine engine) { 1534 this.engine = new WeakReference<WebEngine>(engine); 1535 } 1536 1537 1538 @Override 1539 public boolean sendMessageToFrontend(final String message) { 1540 boolean result = false; 1541 WebEngine webEngine = engine.get(); 1542 if (webEngine != null) { 1543 final Callback<String,Void> messageCallback = 1544 webEngine.debugger.messageCallback; 1545 if (messageCallback != null) { 1546 AccessController.doPrivileged((PrivilegedAction<Void>) () -> { 1547 messageCallback.call(message); 1548 return null; 1549 }, webEngine.page.getAccessControlContext()); 1550 result = true; 1551 } 1552 } 1553 return result; 1554 } 1555 } 1556 1557 private static final boolean printStatusOK(PrinterJob job) { 1558 switch (job.getJobStatus()) { 1559 case NOT_STARTED: 1560 case PRINTING: 1561 return true; 1562 default: 1563 return false; 1564 } 1565 } 1566 1567 /** 1568 * Prints the current Web page using the given printer job. 1569 * <p>This method does not modify the state of the job, nor does it call 1570 * {@link PrinterJob#endJob}, so the job may be safely reused afterwards. 1571 * 1572 * @param job printer job used for printing 1573 * @since JavaFX 8.0 1574 */ 1575 public void print(PrinterJob job) { 1576 if (!printStatusOK(job)) { 1577 return; 1578 } 1579 1580 PageLayout pl = job.getJobSettings().getPageLayout(); 1581 float width = (float) pl.getPrintableWidth(); 1582 float height = (float) pl.getPrintableHeight(); 1583 int pageCount = page.beginPrinting(width, height); 1584 1585 for (int i = 0; i < pageCount; i++) { 1586 if (printStatusOK(job)) { 1587 Node printable = new Printable(page, i, width); 1588 job.printPage(printable); 1589 } 1590 } 1591 page.endPrinting(); 1592 } 1593 }