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