javafx.scene.web.WebEngine Maven / Gradle / Ivy
/*
* Copyright (c) 2011, 2021, 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/property
* WebEngine 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();
}
}
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;
}
public Object getBean() {
return WebEngine.this;
}
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();
}
}