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