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