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