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