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