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