All Downloads are FREE. Search and download functionalities are using the official Maven repository.

javafx.scene.web.WebEngine Maven / Gradle / Ivy

There is a newer version: 24-ea+15
Show newest version
/*
 * Copyright (c) 2011, 2022, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package javafx.scene.web;

import com.sun.javafx.logging.PlatformLogger;
import com.sun.javafx.scene.web.Debugger;
import com.sun.javafx.scene.web.Printable;
import com.sun.javafx.tk.TKPulseListener;
import com.sun.javafx.tk.Toolkit;
import com.sun.javafx.webkit.*;
import com.sun.javafx.webkit.prism.PrismGraphicsManager;
import com.sun.javafx.webkit.prism.PrismInvoker;
import com.sun.javafx.webkit.prism.theme.PrismRenderer;
import com.sun.javafx.webkit.theme.RenderThemeImpl;
import com.sun.javafx.webkit.theme.Renderer;
import com.sun.webkit.*;
import com.sun.webkit.graphics.WCGraphicsManager;
import com.sun.webkit.network.URLs;
import com.sun.webkit.network.Util;
import javafx.animation.AnimationTimer;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.property.*;
import javafx.concurrent.Worker;
import javafx.event.EventHandler;
import javafx.event.EventType;
import javafx.geometry.Rectangle2D;
import javafx.print.JobSettings;
import javafx.print.PageLayout;
import javafx.print.PageRange;
import javafx.print.PrinterJob;
import javafx.scene.Node;
import javafx.util.Callback;
import org.w3c.dom.Document;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import static java.lang.String.format;
import java.lang.ref.WeakReference;
import java.net.MalformedURLException;
import java.net.URLConnection;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.PosixFilePermissions;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Objects;

import static com.sun.webkit.LoadListenerClient.*;

/**
 * {@code WebEngine} is a non-visual object capable of managing one Web page
 * at a time. It loads Web pages, creates their document models, applies
 * styles as necessary, and runs JavaScript on pages. It provides access
 * to the document model of the current page, and enables two-way
 * communication between a Java application and JavaScript code of the page.
 *
 * 

Loading Web Pages

*

The {@code WebEngine} class provides two ways to load content into a * {@code WebEngine} object: *

    *
  • From an arbitrary URL using the {@link #load} method. This method uses * the {@code java.net} package for network access and protocol handling. *
  • From an in-memory String using the * {@link #loadContent(java.lang.String, java.lang.String)} and * {@link #loadContent(java.lang.String)} methods. *
*

Loading always happens on a background thread. Methods that initiate * loading return immediately after scheduling a background job. To track * progress and/or cancel a job, use the {@link javafx.concurrent.Worker} * instance available from the {@link #getLoadWorker} method. * *

The following example changes the stage title when loading completes * successfully: *

{@code
    import javafx.concurrent.Worker.State;
    final Stage stage;
    webEngine.getLoadWorker().stateProperty().addListener(
        new ChangeListener() {
            public void changed(ObservableValue ov, State oldState, State newState) {
                if (newState == State.SUCCEEDED) {
                    stage.setTitle(webEngine.getLocation());
                }
            }
        });
    webEngine.load("http://javafx.com");
 * }
* *

User Interface Callbacks

*

A number of user interface callbacks may be registered with a * {@code WebEngine} object. These callbacks are invoked when a script running * on the page requests a user interface operation to be performed, for * example, opens a popup window or changes status text. A {@code WebEngine} * object cannot handle such requests internally, so it passes the request to * the corresponding callbacks. If no callback is defined for a specific * operation, the request is silently ignored. * *

The table below shows JavaScript user interface methods and properties * with their corresponding {@code WebEngine} callbacks: *

* * * * * * * * * * * * * *
JavaScript Callback Table
JavaScript method/propertyWebEngine callback
{@code window.alert()}{@code onAlert}
{@code window.confirm()}{@code confirmHandler}
{@code window.open()}{@code createPopupHandler}
{@code window.open()} and
* {@code window.close()}
{@code onVisibilityChanged}
{@code window.prompt()}{@code promptHandler}
Setting {@code window.status}{@code onStatusChanged}
Setting any of the following:
* {@code window.innerWidth}, {@code window.innerHeight},
* {@code window.outerWidth}, {@code window.outerHeight},
* {@code window.screenX}, {@code window.screenY},
* {@code window.screenLeft}, {@code window.screenTop}
{@code onResized}
* *

The following example shows a callback that resizes a browser window: *

{@code
    Stage stage;
    webEngine.setOnResized(
        new EventHandler>() {
            public void handle(WebEvent ev) {
                Rectangle2D r = ev.getData();
                stage.setWidth(r.getWidth());
                stage.setHeight(r.getHeight());
            }
        });
 * }
* *

Access to Document Model

*

The {@code WebEngine} objects create and manage a Document Object Model * (DOM) for their Web pages. The model can be accessed and modified using * Java DOM Core classes. The {@link #getDocument()} method provides access * to the root of the model. Additionally DOM Event specification is supported * to define event handlers in Java code. * *

The following example attaches a Java event listener to an element of * a Web page. Clicking on the element causes the application to exit: *

{@code
    EventListener listener = new EventListener() {
        public void handleEvent(Event ev) {
            Platform.exit();
        }
    };

    Document doc = webEngine.getDocument();
    Element el = doc.getElementById("exit-app");
    ((EventTarget) el).addEventListener("click", listener, false);
 * }
* *

Evaluating JavaScript expressions

*

It is possible to execute arbitrary JavaScript code in the context of * the current page using the {@link #executeScript} method. For example: *

{@code
    webEngine.executeScript("history.back()");
 * }
* *

The execution result is returned to the caller, * as described in the next section. * *

Mapping JavaScript values to Java objects

* * JavaScript values are represented using the obvious Java classes: * null becomes Java null; a boolean becomes a {@code java.lang.Boolean}; * and a string becomes a {@code java.lang.String}. * A number can be {@code java.lang.Double} or a {@code java.lang.Integer}, * depending. * The undefined value maps to a specific unique String * object whose value is {@code "undefined"}. *

* If the result is a * JavaScript object, it is wrapped as an instance of the * {@link netscape.javascript.JSObject} class. * (As a special case, if the JavaScript object is * a {@code JavaRuntimeObject} as discussed in the next section, * then the original Java object is extracted instead.) * The {@code JSObject} class is a proxy that provides access to * methods and properties of its underlying JavaScript object. * The most commonly used {@code JSObject} methods are * {@link netscape.javascript.JSObject#getMember getMember} * (to read a named property), * {@link netscape.javascript.JSObject#setMember setMember} * (to set or define a property), * and {@link netscape.javascript.JSObject#call call} * (to call a function-valued property). *

* A DOM {@code Node} is mapped to an object that both extends * {@code JSObject} and implements the appropriate DOM interfaces. * To get a {@code JSObject} object for a {@code Node} just do a cast: *

 * JSObject jdoc = (JSObject) webEngine.getDocument();
 * 
*

* In some cases the context provides a specific Java type that guides * the conversion. * For example if setting a Java {@code String} field from a JavaScript * expression, then the JavaScript value is converted to a string. * *

Mapping Java objects to JavaScript values

* * The arguments of the {@code JSObject} methods {@code setMember} and * {@code call} pass Java objects to the JavaScript environment. * This is roughly the inverse of the JavaScript-to-Java mapping * described above: * Java {@code String}, {@code Number}, or {@code Boolean} objects * are converted to the obvious JavaScript values. A {@code JSObject} * object is converted to the original wrapped JavaScript object. * Otherwise a {@code JavaRuntimeObject} is created. This is * a JavaScript object that acts as a proxy for the Java object, * in that accessing properties of the {@code JavaRuntimeObject} * causes the Java field or method with the same name to be accessed. *

Note that the Java objects bound using * {@link netscape.javascript.JSObject#setMember JSObject.setMember}, * {@link netscape.javascript.JSObject#setSlot JSObject.setSlot}, and * {@link netscape.javascript.JSObject#call JSObject.call} * are implemented using weak references. This means that the Java object * can be garbage collected, causing subsequent accesses to the JavaScript * objects to have no effect. * *

Calling back to Java from JavaScript

* *

The {@link netscape.javascript.JSObject#setMember JSObject.setMember} * method is useful to enable upcalls from JavaScript * into Java code, as illustrated by the following example. The Java code * establishes a new JavaScript object named {@code app}. This object has one * public member, the method {@code exit}. *


public class JavaApplication {
    public void exit() {
        Platform.exit();
    }
}
...
JavaApplication javaApp = new JavaApplication();
JSObject window = (JSObject) webEngine.executeScript("window");
window.setMember("app", javaApp);
 * 
* You can then refer to the object and the method from your HTML page: *
{@code
    Click here to exit application
 * }
*

When a user clicks the link the application is closed. *

* Note that in the above example, the application holds a reference * to the {@code JavaApplication} instance. This is required for the callback * from JavaScript to execute the desired method. *

In the following example, the application does not hold a reference * to the Java object: *


 * JSObject window = (JSObject) webEngine.executeScript("window");
 * window.setMember("app", new JavaApplication());
 * 
*

In this case, since the property value is a local object, {@code "new JavaApplication()"}, * the value may be garbage collected in next GC cycle. *

* When a user clicks the link, it does not guarantee to execute the callback method {@code exit}. *

* If there are multiple Java methods with the given name, * then the engine selects one matching the number of parameters * in the call. (Varargs are not handled.) An unspecified one is * chosen if there are multiple ones with the correct number of parameters. *

* You can pick a specific overloaded method by listing the * parameter types in an "extended method name", which has the * form "method_name(param_type1,...,param_typen)". Typically you'd write the JavaScript expression: *

 * receiver["method_name(param_type1,...,param_typeN)"](arg1,...,argN)
 * 
* *

* The Java class and method must both be declared public. *

* *

Deploying an Application as a Module

*

* If any Java class passed to JavaScript is in a named module, then it must * be reflectively accessible to the {@code javafx.web} module. * A class is reflectively accessible if the module * {@link Module#isOpen(String,Module) opens} the containing package to at * least the {@code javafx.web} module. * Otherwise, the method will not be called, and no error or * warning will be produced. *

*

* For example, if {@code com.foo.MyClass} is in the {@code foo.app} module, * the {@code module-info.java} might * look like this: *

*
{@code module foo.app {
    opens com.foo to javafx.web;
}}
* *

* Alternatively, a class is reflectively accessible if the module * {@link Module#isExported(String) exports} the containing package * unconditionally. *

* *

* Starting with JavaFX 14, HTTP/2 support has been added to WebEngine. * This is achieved by using {@link java.net.http.HttpClient} instead of {@link URLConnection}. HTTP/2 is activated * by default when JavaFX 14 (or later) is used with JDK 12 (or later). *

* *

Threading

*

{@code WebEngine} objects must be created and accessed solely from the * JavaFX Application thread. This rule also applies to any DOM and JavaScript * objects obtained from the {@code WebEngine} object. * @since JavaFX 2.0 */ final public class WebEngine { static { Accessor.setPageAccessor(w -> w == null ? null : w.getPage()); Invoker.setInvoker(new PrismInvoker()); Renderer.setRenderer(new PrismRenderer()); WCGraphicsManager.setGraphicsManager(new PrismGraphicsManager()); CursorManager.setCursorManager(new CursorManagerImpl()); com.sun.webkit.EventLoop.setEventLoop(new EventLoopImpl()); ThemeClient.setDefaultRenderTheme(new RenderThemeImpl()); Utilities.setUtilities(new UtilitiesImpl()); } private static final PlatformLogger logger = PlatformLogger.getLogger(WebEngine.class.getName()); /** * The number of instances of this class. * Used to start and stop the pulse timer. */ private static int instanceCount = 0; /** * The node associated with this engine. There is a one-to-one correspondence * between the WebView and its WebEngine (although not all WebEngines have * a WebView, every WebView has one and only one WebEngine). */ private final ObjectProperty view = new SimpleObjectProperty<>(this, "view"); /** * The Worker which shows progress of the web engine as it loads pages. */ private final LoadWorker loadWorker = new LoadWorker(); /** * The object that provides interaction with the native webkit core. */ private final WebPage page; private final SelfDisposer disposer; private final DebuggerImpl debugger = new DebuggerImpl(); private boolean userDataDirectoryApplied = false; /** * Returns a {@link javafx.concurrent.Worker} object that can be used to * track loading progress. * * @return the {@code Worker} object */ public final Worker getLoadWorker() { return loadWorker; } /* * The final document. This may be null if no document has been loaded. */ private final DocumentProperty document = new DocumentProperty(); public final Document getDocument() { return document.getValue(); } /** * Document object for the current Web page. The value is {@code null} * if the Web page failed to load. * * @return the document property */ public final ReadOnlyObjectProperty documentProperty() { return document; } /* * The location of the current page. This may return null. */ private final ReadOnlyStringWrapper location = new ReadOnlyStringWrapper(this, "location"); public final String getLocation() { return location.getValue(); } /** * URL of the current Web page. If the current page has no URL, * the value is an empty String. * * @return the location property */ public final ReadOnlyStringProperty locationProperty() { return location.getReadOnlyProperty(); } private void updateLocation(String value) { this.location.set(value); this.document.invalidate(false); this.title.set(null); } /* * The page title. */ private final ReadOnlyStringWrapper title = new ReadOnlyStringWrapper(this, "title"); public final String getTitle() { return title.getValue(); } /** * Title of the current Web page. If the current page has no title, * the value is {@code null}. This property will be updated * asynchronously some time after the page is loaded. Applications * should not rely on any particular timing, but should listen for * changes to this property, or bind to it, to know when it has * been updated. * * @return the title property */ public final ReadOnlyStringProperty titleProperty() { return title.getReadOnlyProperty(); } private void updateTitle() { title.set(page.getTitle(page.getMainFrame())); } // // Settings /** * Specifies whether JavaScript execution is enabled. * * @defaultValue true * @since JavaFX 2.2 */ private BooleanProperty javaScriptEnabled; public final void setJavaScriptEnabled(boolean value) { javaScriptEnabledProperty().set(value); } public final boolean isJavaScriptEnabled() { return javaScriptEnabled == null ? true : javaScriptEnabled.get(); } public final BooleanProperty javaScriptEnabledProperty() { if (javaScriptEnabled == null) { javaScriptEnabled = new BooleanPropertyBase(true) { @Override public void invalidated() { checkThread(); page.setJavaScriptEnabled(get()); } @Override public Object getBean() { return WebEngine.this; } @Override public String getName() { return "javaScriptEnabled"; } }; } return javaScriptEnabled; } /** * Location of the user stylesheet as a string URL. * *

This should be a local URL, i.e. either {@code 'data:'}, * {@code 'file:'}, {@code 'jar:'}, or {@code 'jrt:'}. Remote URLs are not allowed * for security reasons. * * @defaultValue null * @since JavaFX 2.2 */ private StringProperty userStyleSheetLocation; public final void setUserStyleSheetLocation(String value) { userStyleSheetLocationProperty().set(value); } public final String getUserStyleSheetLocation() { return userStyleSheetLocation == null ? null : userStyleSheetLocation.get(); } private byte[] readFully(BufferedInputStream in) throws IOException { final int BUF_SIZE = 4096; int outSize = 0; final List outList = new ArrayList<>(); byte[] buffer = new byte[BUF_SIZE]; while (true) { int nBytes = in.read(buffer); if (nBytes < 0) break; byte[] chunk; if (nBytes == buffer.length) { chunk = buffer; buffer = new byte[BUF_SIZE]; } else { chunk = new byte[nBytes]; System.arraycopy(buffer, 0, chunk, 0, nBytes); } outList.add(chunk); outSize += nBytes; } final byte[] out = new byte[outSize]; int outPos = 0; for (byte[] chunk : outList) { System.arraycopy(chunk, 0, out, outPos, chunk.length); outPos += chunk.length; } return out; } public final StringProperty userStyleSheetLocationProperty() { if (userStyleSheetLocation == null) { userStyleSheetLocation = new StringPropertyBase(null) { private final static String DATA_PREFIX = "data:text/css;charset=utf-8;base64,"; @Override public void invalidated() { checkThread(); String url = get(); String dataUrl; if (url == null || url.length() <= 0) { dataUrl = null; } else if (url.startsWith(DATA_PREFIX)) { dataUrl = url; } else if (url.startsWith("file:") || url.startsWith("jar:") || url.startsWith("jrt:") || url.startsWith("data:")) { try { URLConnection conn = URLs.newURL(url).openConnection(); conn.connect(); BufferedInputStream in = new BufferedInputStream(conn.getInputStream()); byte[] inBytes = readFully(in); String out = Base64.getMimeEncoder().encodeToString(inBytes); dataUrl = DATA_PREFIX + out; } catch (IOException e) { throw new RuntimeException(e); } } else { throw new IllegalArgumentException("Invalid stylesheet URL"); } page.setUserStyleSheetLocation(dataUrl); } @Override public Object getBean() { return WebEngine.this; } @Override public String getName() { return "userStyleSheetLocation"; } }; } return userStyleSheetLocation; } /** * Specifies the directory to be used by this {@code WebEngine} * to store local user data. * *

If the value of this property is not {@code null}, * the {@code WebEngine} will attempt to store local user data * in the respective directory. * If the value of this property is {@code null}, * the {@code WebEngine} will attempt to store local user data * in an automatically selected system-dependent user- and * application-specific directory. * *

When a {@code WebEngine} is about to start loading a web * page or executing a script for the first time, it checks whether * it can actually use the directory specified by this property. * If the check fails for some reason, the {@code WebEngine} invokes * the {@link WebEngine#onErrorProperty WebEngine.onError} event handler, * if any, with a {@link WebErrorEvent} describing the reason. * If the invoked event handler modifies the {@code userDataDirectory} * property, the {@code WebEngine} retries with the new value as soon * as the handler returns. If the handler does not modify the * {@code userDataDirectory} property (which is the default), * the {@code WebEngine} continues without local user data. * *

Once the {@code WebEngine} has started loading a web page or * executing a script, changes made to this property have no effect * on where the {@code WebEngine} stores or will store local user * data. * *

Currently, the directory specified by this property is used * only to store the data that backs the {@code window.localStorage} * objects. In the future, more types of data can be added. * * @defaultValue {@code null} * @since JavaFX 8.0 */ private final ObjectProperty userDataDirectory = new SimpleObjectProperty<>(this, "userDataDirectory"); public final File getUserDataDirectory() { return userDataDirectory.get(); } public final void setUserDataDirectory(File value) { userDataDirectory.set(value); } public final ObjectProperty userDataDirectoryProperty() { return userDataDirectory; } /** * Specifies user agent ID string. This string is the value of the * {@code User-Agent} HTTP header. * * @defaultValue system dependent * @since JavaFX 8.0 */ private StringProperty userAgent; public final void setUserAgent(String value) { userAgentProperty().set(value); } public final String getUserAgent() { return userAgent == null ? page.getUserAgent() : userAgent.get(); } public final StringProperty userAgentProperty() { if (userAgent == null) { userAgent = new StringPropertyBase(page.getUserAgent()) { @Override public void invalidated() { checkThread(); page.setUserAgent(get()); } @Override public Object getBean() { return WebEngine.this; } @Override public String getName() { return "userAgent"; } }; } return userAgent; } private final ObjectProperty>> onAlert = new SimpleObjectProperty<>(this, "onAlert"); public final EventHandler> getOnAlert() { return onAlert.get(); } public final void setOnAlert(EventHandler> handler) { onAlert.set(handler); } /** * JavaScript {@code alert} handler property. This handler is invoked * when a script running on the Web page calls the {@code alert} function. * @return the onAlert property */ public final ObjectProperty>> onAlertProperty() { return onAlert; } private final ObjectProperty>> onStatusChanged = new SimpleObjectProperty<>(this, "onStatusChanged"); public final EventHandler> getOnStatusChanged() { return onStatusChanged.get(); } public final void setOnStatusChanged(EventHandler> handler) { onStatusChanged.set(handler); } /** * JavaScript status handler property. This handler is invoked when * a script running on the Web page sets {@code window.status} property. * @return the onStatusChanged property */ public final ObjectProperty>> onStatusChangedProperty() { return onStatusChanged; } private final ObjectProperty>> onResized = new SimpleObjectProperty<>(this, "onResized"); public final EventHandler> getOnResized() { return onResized.get(); } public final void setOnResized(EventHandler> handler) { onResized.set(handler); } /** * JavaScript window resize handler property. This handler is invoked * when a script running on the Web page moves or resizes the * {@code window} object. * @return the onResized property */ public final ObjectProperty>> onResizedProperty() { return onResized; } private final ObjectProperty>> onVisibilityChanged = new SimpleObjectProperty<>(this, "onVisibilityChanged"); public final EventHandler> getOnVisibilityChanged() { return onVisibilityChanged.get(); } public final void setOnVisibilityChanged(EventHandler> handler) { onVisibilityChanged.set(handler); } /** * JavaScript window visibility handler property. This handler is invoked * when a script running on the Web page changes visibility of the * {@code window} object. * @return the onVisibilityChanged property */ public final ObjectProperty>> onVisibilityChangedProperty() { return onVisibilityChanged; } private final ObjectProperty> createPopupHandler = new SimpleObjectProperty<>(this, "createPopupHandler", p -> WebEngine.this); public final Callback getCreatePopupHandler() { return createPopupHandler.get(); } public final void setCreatePopupHandler(Callback handler) { createPopupHandler.set(handler); } /** * JavaScript popup handler property. This handler is invoked when a script * running on the Web page requests a popup to be created. *

To satisfy this request a handler may create a new {@code WebEngine}, * attach a visibility handler and optionally a resize handler, and return * the newly created engine. To block the popup, a handler should return * {@code null}. *

By default, a popup handler is installed that opens popups in this * {@code WebEngine}. * * @return the createPopupHandler property * * @see PopupFeatures */ public final ObjectProperty> createPopupHandlerProperty() { return createPopupHandler; } private final ObjectProperty> confirmHandler = new SimpleObjectProperty<>(this, "confirmHandler"); public final Callback getConfirmHandler() { return confirmHandler.get(); } public final void setConfirmHandler(Callback handler) { confirmHandler.set(handler); } /** * JavaScript {@code confirm} handler property. This handler is invoked * when a script running on the Web page calls the {@code confirm} function. *

An implementation may display a dialog box with Yes and No options, * and return the user's choice. * * @return the confirmHandler property */ public final ObjectProperty> confirmHandlerProperty() { return confirmHandler; } private final ObjectProperty> promptHandler = new SimpleObjectProperty<>(this, "promptHandler"); public final Callback getPromptHandler() { return promptHandler.get(); } public final void setPromptHandler(Callback handler) { promptHandler.set(handler); } /** * JavaScript {@code prompt} handler property. This handler is invoked * when a script running on the Web page calls the {@code prompt} function. *

An implementation may display a dialog box with an text field, * and return the user's input. * * @return the promptHandler property * @see PromptData */ public final ObjectProperty> promptHandlerProperty() { return promptHandler; } /** * The event handler called when an error occurs. * * @defaultValue {@code null} * @since JavaFX 8.0 */ private final ObjectProperty> onError = new SimpleObjectProperty<>(this, "onError"); public final EventHandler getOnError() { return onError.get(); } public final void setOnError(EventHandler handler) { onError.set(handler); } public final ObjectProperty> onErrorProperty() { return onError; } /** * Creates a new engine. */ public WebEngine() { this(null, false); } /** * Creates a new engine and loads a Web page into it. * * @param url the URL of the web page to load */ public WebEngine(String url) { this(url, true); } private WebEngine(String url, boolean callLoad) { checkThread(); Accessor accessor = new AccessorImpl(this); page = new WebPage( new WebPageClientImpl(accessor), new UIClientImpl(accessor), null, new InspectorClientImpl(this), new ThemeClientImpl(accessor), false); page.addLoadListenerClient(new PageLoadListener(this)); history = new WebHistory(page); disposer = new SelfDisposer(page); Disposer.addRecord(this, disposer); if (callLoad) { load(url); } if (instanceCount == 0 && Timer.getMode() == Timer.Mode.PLATFORM_TICKS) { PulseTimer.start(); } instanceCount++; } /** * Loads a Web page into this engine. This method starts asynchronous * loading and returns immediately. * @param url URL of the web page to load */ public void load(String url) { checkThread(); loadWorker.cancelAndReset(); if (url == null || url.equals("") || url.equals("about:blank")) { url = ""; } else { // verify and, if possible, adjust the url on the Java // side, otherwise it may crash native code try { url = Util.adjustUrlForWebKit(url); } catch (MalformedURLException e) { loadWorker.dispatchLoadEvent(getMainFrame(), PAGE_STARTED, url, null, 0.0, 0); loadWorker.dispatchLoadEvent(getMainFrame(), LOAD_FAILED, url, null, 0.0, MALFORMED_URL); return; } } applyUserDataDirectory(); page.open(page.getMainFrame(), url); } /** * Loads the given HTML content directly. This method is useful when you have an HTML * String composed in memory, or loaded from some system which cannot be reached via * a URL (for example, the HTML text may have come from a database). As with * {@link #load(String)}, this method is asynchronous. * * @param content the HTML content to load */ public void loadContent(String content) { loadContent(content, "text/html"); } /** * Loads the given content directly. This method is useful when you have content * composed in memory, or loaded from some system which cannot be reached via * a URL (for example, the SVG text may have come from a database). As with * {@link #load(String)}, this method is asynchronous. This method also allows you to * specify the content type of the string being loaded, and so may optionally support * other types besides just HTML. * * @param content the HTML content to load * @param contentType the type of content to load */ public void loadContent(String content, String contentType) { checkThread(); loadWorker.cancelAndReset(); applyUserDataDirectory(); page.load(page.getMainFrame(), content, contentType); } /** * Reloads the current page, whether loaded from URL or directly from a String in * one of the {@code loadContent} methods. */ public void reload() { // TODO what happens if this is called while currently loading a page? checkThread(); page.refresh(page.getMainFrame()); } private final WebHistory history; /** * Returns the session history object. * * @return history object * @since JavaFX 2.2 */ public WebHistory getHistory() { return history; } /** * Executes a script in the context of the current page. * * @param script the script * @return execution result, converted to a Java object using the following * rules: *

    *
  • JavaScript Int32 is converted to {@code java.lang.Integer} *
  • Other JavaScript numbers to {@code java.lang.Double} *
  • JavaScript string to {@code java.lang.String} *
  • JavaScript boolean to {@code java.lang.Boolean} *
  • JavaScript {@code null} to {@code null} *
  • Most JavaScript objects get wrapped as * {@code netscape.javascript.JSObject} *
  • JavaScript JSNode objects get mapped to instances of * {@code netscape.javascript.JSObject}, that also implement * {@code org.w3c.dom.Node} *
  • A special case is the JavaScript class {@code JavaRuntimeObject} * which is used to wrap a Java object as a JavaScript value - in this * case we just extract the original Java value. *
*/ public Object executeScript(String script) { checkThread(); applyUserDataDirectory(); return page.executeScript(page.getMainFrame(), script); } private long getMainFrame() { return page.getMainFrame(); } WebPage getPage() { return page; } void setView(WebView view) { this.view.setValue(view); } private void stop() { checkThread(); page.stop(page.getMainFrame()); } private void applyUserDataDirectory() { if (userDataDirectoryApplied) { return; } userDataDirectoryApplied = true; File nominalUserDataDir = getUserDataDirectory(); while (true) { File userDataDir; String displayString; if (nominalUserDataDir == null) { userDataDir = defaultUserDataDirectory(); displayString = format("null (%s)", userDataDir); } else { userDataDir = nominalUserDataDir; displayString = userDataDir.toString(); } logger.fine("Trying to apply user data directory [{0}]", displayString); String errorMessage; EventType errorType; Throwable error; try { userDataDir = DirectoryLock.canonicalize(userDataDir); File localStorageDir = new File(userDataDir, "localstorage"); File[] dirs = new File[] { userDataDir, localStorageDir, }; for (File dir : dirs) { createDirectories(dir); // Additional security check to make sure the caller // has permission to write to the target directory File test = new File(dir, ".test"); if (test.createNewFile()) { test.delete(); } } disposer.userDataDirectoryLock = new DirectoryLock(userDataDir); page.setLocalStorageDatabasePath(localStorageDir.getPath()); page.setLocalStorageEnabled(true); logger.fine("User data directory [{0}] has " + "been applied successfully", displayString); return; } catch (DirectoryLock.DirectoryAlreadyInUseException ex) { errorMessage = "User data directory [%s] is already in use"; errorType = WebErrorEvent.USER_DATA_DIRECTORY_ALREADY_IN_USE; error = ex; } catch (IOException ex) { errorMessage = "An I/O error occurred while setting up " + "user data directory [%s]"; errorType = WebErrorEvent.USER_DATA_DIRECTORY_IO_ERROR; error = ex; } catch (SecurityException ex) { errorMessage = "A security error occurred while setting up " + "user data directory [%s]"; errorType = WebErrorEvent.USER_DATA_DIRECTORY_SECURITY_ERROR; error = ex; } errorMessage = format(errorMessage, displayString); logger.fine("{0}, calling error handler", errorMessage); File oldNominalUserDataDir = nominalUserDataDir; fireError(errorType, errorMessage, error); nominalUserDataDir = getUserDataDirectory(); if (Objects.equals(nominalUserDataDir, oldNominalUserDataDir)) { logger.fine("Error handler did not modify user data directory, " + "continuing without user data directory"); return; } else { logger.fine("Error handler has set user data directory to [{0}], " + "retrying", nominalUserDataDir); continue; } } } private static File defaultUserDataDirectory() { return new File( com.sun.glass.ui.Application.GetApplication() .getDataDirectory(), "webview"); } private static void createDirectories(File directory) throws IOException { Path path = directory.toPath(); try { Files.createDirectories(path, PosixFilePermissions.asFileAttribute( PosixFilePermissions.fromString("rwx------"))); } catch (UnsupportedOperationException ex) { Files.createDirectories(path); } } private void fireError(EventType eventType, String message, Throwable exception) { EventHandler handler = getOnError(); if (handler != null) { handler.handle(new WebErrorEvent(this, eventType, message, exception)); } } // for testing purposes only void dispose() { disposer.dispose(); } private static final class SelfDisposer implements DisposerRecord { private WebPage page; private DirectoryLock userDataDirectoryLock; private SelfDisposer(WebPage page) { this.page = page; } @Override public void dispose() { if (page == null) { return; } page.dispose(); page = null; if (userDataDirectoryLock != null) { userDataDirectoryLock.close(); } instanceCount--; if (instanceCount == 0 && Timer.getMode() == Timer.Mode.PLATFORM_TICKS) { PulseTimer.stop(); } } } private static final class AccessorImpl extends Accessor { private final WeakReference engine; private AccessorImpl(WebEngine w) { this.engine = new WeakReference<>(w); } @Override public WebEngine getEngine() { return engine.get(); } @Override public WebPage getPage() { WebEngine w = getEngine(); return w == null ? null : w.page; } @Override public WebView getView() { WebEngine w = getEngine(); return w == null ? null : w.view.get(); } @Override public void addChild(Node child) { WebView view = getView(); if (view != null) { view.getChildren().add(child); } } @Override public void removeChild(Node child) { WebView view = getView(); if (view != null) { view.getChildren().remove(child); } } @Override public void addViewListener(InvalidationListener l) { WebEngine w = getEngine(); if (w != null) { w.view.addListener(l); } } } /** * Drives the {@code Timer} when {@code Timer.Mode.PLATFORM_TICKS} is set. */ private static final class PulseTimer { // Used just to guarantee constant pulse activity. See RT-14433. private static final AnimationTimer animation = new AnimationTimer() { @Override public void handle(long l) {} }; private static final TKPulseListener listener = () -> { // Note, the timer event is executed right in the notifyTick(), // that is during the pulse event. This makes the timer more // repsonsive, though prolongs the pulse. So far it causes no // problems but nevertheless it should be kept in mind. // Execute notifyTick in runLater to run outside of pulse so // that events will run in order and be able to display dialogs // or call other methods that require a nested event loop. Platform.runLater(() -> Timer.getTimer().notifyTick()); }; private static void start(){ Toolkit.getToolkit().addSceneTkPulseListener(listener); animation.start(); } private static void stop() { Toolkit.getToolkit().removeSceneTkPulseListener(listener); animation.stop(); } } static void checkThread() { Toolkit.getToolkit().checkFxUserThread(); } /** * The page load event listener. This object references the owner * WebEngine weakly so as to avoid referencing WebEngine from WebPage * strongly. */ private static final class PageLoadListener implements LoadListenerClient { private final WeakReference engine; private PageLoadListener(WebEngine engine) { this.engine = new WeakReference<>(engine); } @Override public void dispatchLoadEvent(long frame, int state, String url, String contentType, double progress, int errorCode) { WebEngine w = engine.get(); if (w != null) { w.loadWorker.dispatchLoadEvent(frame, state, url, contentType, progress, errorCode); } } @Override public void dispatchResourceLoadEvent(long frame, int state, String url, String contentType, double progress, int errorCode) { } } private final class LoadWorker implements Worker { private final ReadOnlyObjectWrapper state = new ReadOnlyObjectWrapper<>(this, "state", State.READY); @Override public final State getState() { checkThread(); return state.get(); } @Override public final ReadOnlyObjectProperty stateProperty() { checkThread(); return state.getReadOnlyProperty(); } private void updateState(State value) { checkThread(); this.state.set(value); running.set(value == State.SCHEDULED || value == State.RUNNING); } /** * @InheritDoc */ private final ReadOnlyObjectWrapper value = new ReadOnlyObjectWrapper<>(this, "value", null); @Override public final Void getValue() { checkThread(); return value.get(); } @Override public final ReadOnlyObjectProperty valueProperty() { checkThread(); return value.getReadOnlyProperty(); } /** * @InheritDoc */ private final ReadOnlyObjectWrapper exception = new ReadOnlyObjectWrapper<>(this, "exception"); @Override public final Throwable getException() { checkThread(); return exception.get(); } @Override public final ReadOnlyObjectProperty exceptionProperty() { checkThread(); return exception.getReadOnlyProperty(); } /** * @InheritDoc */ private final ReadOnlyDoubleWrapper workDone = new ReadOnlyDoubleWrapper(this, "workDone", -1); @Override public final double getWorkDone() { checkThread(); return workDone.get(); } @Override public final ReadOnlyDoubleProperty workDoneProperty() { checkThread(); return workDone.getReadOnlyProperty(); } /** * @InheritDoc */ private final ReadOnlyDoubleWrapper totalWorkToBeDone = new ReadOnlyDoubleWrapper(this, "totalWork", -1); @Override public final double getTotalWork() { checkThread(); return totalWorkToBeDone.get(); } @Override public final ReadOnlyDoubleProperty totalWorkProperty() { checkThread(); return totalWorkToBeDone.getReadOnlyProperty(); } /** * @InheritDoc */ private final ReadOnlyDoubleWrapper progress = new ReadOnlyDoubleWrapper(this, "progress", -1); @Override public final double getProgress() { checkThread(); return progress.get(); } @Override public final ReadOnlyDoubleProperty progressProperty() { checkThread(); return progress.getReadOnlyProperty(); } private void updateProgress(double p) { totalWorkToBeDone.set(100.0); workDone.set(p * 100.0); progress.set(p); } /** * @InheritDoc */ private final ReadOnlyBooleanWrapper running = new ReadOnlyBooleanWrapper(this, "running", false); @Override public final boolean isRunning() { checkThread(); return running.get(); } @Override public final ReadOnlyBooleanProperty runningProperty() { checkThread(); return running.getReadOnlyProperty(); } /** * @InheritDoc */ private final ReadOnlyStringWrapper message = new ReadOnlyStringWrapper(this, "message", ""); @Override public final String getMessage() { return message.get(); } @Override public final ReadOnlyStringProperty messageProperty() { return message.getReadOnlyProperty(); } /** * @InheritDoc */ private final ReadOnlyStringWrapper title = new ReadOnlyStringWrapper(this, "title", "WebEngine Loader"); @Override public final String getTitle() { return title.get(); } @Override public final ReadOnlyStringProperty titleProperty() { return title.getReadOnlyProperty(); } /** * Cancels the loading of the page. If called after the page has already * been loaded, then this call takes no effect. */ @Override public boolean cancel() { if (isRunning()) { stop(); // this call indirectly sets state return true; } else { return false; } } private void cancelAndReset() { cancel(); exception.set(null); message.set(""); totalWorkToBeDone.set(-1); workDone.set(-1); progress.set(-1); updateState(State.READY); running.set(false); } private void dispatchLoadEvent(long frame, int state, String url, String contentType, double workDone, int errorCode) { if (frame != getMainFrame()) { return; } switch (state) { case PAGE_STARTED: message.set("Loading " + url); updateLocation(url); updateProgress(0.0); updateState(State.SCHEDULED); updateState(State.RUNNING); break; case PAGE_REDIRECTED: message.set("Loading " + url); updateLocation(url); break; case PAGE_REPLACED: message.set("Replaced " + url); // Update only the location, don't change title or document. WebEngine.this.location.set(url); break; case PAGE_FINISHED: message.set("Loading complete"); updateProgress(1.0); updateState(State.SUCCEEDED); break; case LOAD_FAILED: message.set("Loading failed"); exception.set(describeError(errorCode)); updateState(State.FAILED); break; case LOAD_STOPPED: message.set("Loading stopped"); updateState(State.CANCELLED); break; case PROGRESS_CHANGED: updateProgress(workDone); break; case TITLE_RECEIVED: updateTitle(); break; case DOCUMENT_AVAILABLE: if (this.state.get() != State.RUNNING) { // We have empty load; send a synthetic event (RT-32097) dispatchLoadEvent(frame, PAGE_STARTED, url, contentType, workDone, errorCode); } document.invalidate(true); break; } } private Throwable describeError(int errorCode) { String reason = "Unknown error"; switch (errorCode) { case UNKNOWN_HOST: reason = "Unknown host"; break; case MALFORMED_URL: reason = "Malformed URL"; break; case SSL_HANDSHAKE: reason = "SSL handshake failed"; break; case CONNECTION_REFUSED: reason = "Connection refused by server"; break; case CONNECTION_RESET: reason = "Connection reset by server"; break; case NO_ROUTE_TO_HOST: reason = "No route to host"; break; case CONNECTION_TIMED_OUT: reason = "Connection timed out"; break; case PERMISSION_DENIED: reason = "Permission denied"; break; case INVALID_RESPONSE: reason = "Invalid response from server"; break; case TOO_MANY_REDIRECTS: reason = "Too many redirects"; break; case FILE_NOT_FOUND: reason = "File not found"; break; } return new Throwable(reason); } } private final class DocumentProperty extends ReadOnlyObjectPropertyBase { private boolean available; private Document document; private void invalidate(boolean available) { if (this.available || available) { this.available = available; this.document = null; fireValueChangedEvent(); } } @Override public Document get() { if (!this.available) { return null; } if (this.document == null) { this.document = page.getDocument(page.getMainFrame()); if (this.document == null) { this.available = false; } } return this.document; } @Override public Object getBean() { return WebEngine.this; } @Override public String getName() { return "document"; } } /* * Returns the debugger associated with this web engine. * The debugger is an object that can be used to debug * the web page currently loaded into the web engine. *

* All methods of the debugger must be called on * the JavaFX Application Thread. * The message callback object registered with the debugger * is always called on the JavaFX Application Thread. * @return the debugger associated with this web engine. * The return value cannot be {@code null}. */ Debugger getDebugger() { return debugger; } /** * The debugger implementation. */ private final class DebuggerImpl implements Debugger { private boolean enabled; private Callback messageCallback; @Override public boolean isEnabled() { checkThread(); return enabled; } @Override public void setEnabled(boolean enabled) { checkThread(); if (enabled != this.enabled) { if (enabled) { page.setDeveloperExtrasEnabled(true); page.connectInspectorFrontend(); } else { page.disconnectInspectorFrontend(); page.setDeveloperExtrasEnabled(false); } this.enabled = enabled; } } @Override public void sendMessage(String message) { checkThread(); if (!enabled) { throw new IllegalStateException("Debugger is not enabled"); } if (message == null) { throw new NullPointerException("message is null"); } page.dispatchInspectorMessageFromFrontend(message); } @Override public Callback getMessageCallback() { checkThread(); return messageCallback; } @Override public void setMessageCallback(Callback callback) { checkThread(); messageCallback = callback; } } /** * The inspector client implementation. This object references the owner * WebEngine weakly so as to avoid referencing WebEngine from WebPage * strongly. */ private static final class InspectorClientImpl implements InspectorClient { private final WeakReference engine; private InspectorClientImpl(WebEngine engine) { this.engine = new WeakReference<>(engine); } @SuppressWarnings("removal") @Override public boolean sendMessageToFrontend(final String message) { boolean result = false; WebEngine webEngine = engine.get(); if (webEngine != null) { final Callback messageCallback = webEngine.debugger.messageCallback; if (messageCallback != null) { AccessController.doPrivileged((PrivilegedAction) () -> { messageCallback.call(message); return null; }, webEngine.page.getAccessControlContext()); result = true; } } return result; } } private static final boolean printStatusOK(PrinterJob job) { switch (job.getJobStatus()) { case NOT_STARTED: case PRINTING: return true; default: return false; } } /** * Prints the current Web page using the given printer job. *

This method does not modify the state of the job, nor does it call * {@link PrinterJob#endJob}, so the job may be safely reused afterwards. * * @param job printer job used for printing * @since JavaFX 8.0 */ public void print(PrinterJob job) { if (!printStatusOK(job)) { return; } PageLayout pl = job.getJobSettings().getPageLayout(); float width = (float) pl.getPrintableWidth(); float height = (float) pl.getPrintableHeight(); int pageCount = page.beginPrinting(width, height); JobSettings jobSettings = job.getJobSettings(); if (jobSettings.getPageRanges() != null) { PageRange[] pageRanges = jobSettings.getPageRanges(); for (PageRange p : pageRanges) { for (int i = p.getStartPage(); i <= p.getEndPage() && i <= pageCount; ++i) { if (printStatusOK(job)) { Node printable = new Printable(page, i - 1, width); job.printPage(printable); } } } } else { for (int i = 0; i < pageCount; i++) { if (printStatusOK(job)) { Node printable = new Printable(page, i, width); job.printPage(printable); } } } page.endPrinting(); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy