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