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