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