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