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