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