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