/* * Copyright (c) 2011, 2013, 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.scene.web.Debugger; import com.sun.javafx.tk.TKPulseListener; import com.sun.javafx.tk.Toolkit; import com.sun.javafx.webkit.Accessor; import com.sun.webkit.Disposer; import com.sun.webkit.DisposerRecord; import com.sun.webkit.InspectorClient; import com.sun.webkit.Invoker; import com.sun.javafx.webkit.prism.PrismInvoker; import static com.sun.webkit.LoadListenerClient.*; import com.sun.webkit.LoadListenerClient; import com.sun.webkit.Timer; import com.sun.webkit.WebPage; import com.sun.webkit.network.URLs; import com.sun.webkit.network.Util; import java.io.BufferedInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.lang.ref.WeakReference; import java.net.MalformedURLException; import java.net.URLConnection; import java.security.AccessController; import java.security.PrivilegedAction; import javafx.animation.AnimationTimer; import javafx.beans.InvalidationListener; import javafx.beans.property.BooleanProperty; import javafx.beans.property.BooleanPropertyBase; import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyBooleanProperty; import javafx.beans.property.ReadOnlyBooleanWrapper; import javafx.beans.property.ReadOnlyDoubleProperty; import javafx.beans.property.ReadOnlyDoubleWrapper; import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.ReadOnlyObjectPropertyBase; import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.beans.property.ReadOnlyStringProperty; import javafx.beans.property.ReadOnlyStringWrapper; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.StringProperty; import javafx.beans.property.StringPropertyBase; import javafx.concurrent.Worker; import javafx.event.EventHandler; import javafx.geometry.Rectangle2D; import javafx.scene.Node; import javafx.util.Callback; import org.w3c.dom.Document; /** * {@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: *

*

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: *


import javafx.concurrent.Worker.State;
final Stage stage;
webEngine.getLoadWorker().stateProperty().addListener(
        new ChangeListener<State>() {
            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 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: *


Stage stage;
webEngine.setOnResized(
        new EventHandler<WebEvent<Rectangle2D>>() {
            public void handle(WebEvent<Rectangle2D> 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: *


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: *


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. * *

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();
    }
}
...
JSObject window = (JSObject) webEngine.executeScript("window");
window.setMember("app", new JavaApplication());
 * 
* You can then refer to the object and the method from your HTML page: *

<a href="" onclick="app.exit()">Click here to exit application</a>
 * 
*

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

* 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)
 * 
* *

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. */ final public class WebEngine { static { Accessor.setPageAccessor(new Accessor.PageAccessor() { @Override public WebPage getPage(WebEngine w) { return 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()); } /** * 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; /** * The debugger associated with this web engine. */ private final DebuggerImpl debugger = new DebuggerImpl(); /** * Returns a {@link javafx.concurrent.Worker} object that can be used to * track loading progress. */ 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(); /** * Returns the document object for the current Web page. If the Web page * failed to load, returns {@code null}. */ 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. */ public final ReadOnlyObjectProperty documentProperty() { return document; } /** * The location of the current page. This may return null. */ private final ReadOnlyStringWrapper location = new ReadOnlyStringWrapper(this, "location"); /** * Returns URL of the current Web page. If the current page has no URL, * returns an empty String. */ 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. */ 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"); /** * Returns title of the current Web page. If the current page has no title, * returns {@code null}. */ 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}. */ 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 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:'}, or {@code 'jar:'}. Remote URLs are not allowed * for security reasons. * * @defaultValue null * @since 2.2 */ private StringProperty userStyleSheetLocation; public final void setUserStyleSheetLocation(String value) { userStyleSheetLocationProperty().set(value); } public final String getUserStyleSheetLocation() { return userStyleSheetLocation == null ? null : userStyleSheetLocation.get(); } 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("data:")) { try { URLConnection conn = URLs.newURL(url).openConnection(); conn.connect(); BufferedInputStream in = new BufferedInputStream(conn.getInputStream()); ByteArrayOutputStream out = new ByteArrayOutputStream(); new sun.misc.BASE64Encoder().encodeBuffer(in, out); dataUrl = DATA_PREFIX + out.toString(); } 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 user agent ID string. This string is the value of the * {@code User-Agent} HTTP header. * * @defaultValue system dependent * @since 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"); /** * Returns the JavaScript {@code alert} handler. * @see #onAlertProperty * @see #setOnAlert */ public final EventHandler> getOnAlert() { return onAlert.get(); } /** * Sets the JavaScript {@code alert} handler. * @see #onAlertProperty * @see #getOnAlert */ 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. */ public final ObjectProperty>> onAlertProperty() { return onAlert; } private final ObjectProperty>> onStatusChanged = new SimpleObjectProperty>>(this, "onStatusChanged"); /** * Returns the JavaScript status handler. * @see #onStatusChangedProperty * @see #setOnStatusChanged */ public final EventHandler> getOnStatusChanged() { return onStatusChanged.get(); } /** * Sets the JavaScript status handler. * @see #onStatusChangedProperty * @see #getOnStatusChanged */ 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. */ public final ObjectProperty>> onStatusChangedProperty() { return onStatusChanged; } private final ObjectProperty>> onResized = new SimpleObjectProperty>>(this, "onResized"); /** * Returns the JavaScript window resize handler. * @see #onResizedProperty * @see #setOnResized */ public final EventHandler> getOnResized() { return onResized.get(); } /** * Sets the JavaScript window resize handler. * @see #onResizedProperty * @see #getOnResized */ 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. */ public final ObjectProperty>> onResizedProperty() { return onResized; } private final ObjectProperty>> onVisibilityChanged = new SimpleObjectProperty>>(this, "onVisibilityChanged"); /** * Returns the JavaScript window visibility handler. * @see #onVisibilityChangedProperty * @see #setOnVisibilityChanged */ public final EventHandler> getOnVisibilityChanged() { return onVisibilityChanged.get(); } /** * Sets the JavaScript window visibility handler. * @see #onVisibilityChangedProperty * @see #getOnVisibilityChanged */ 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. */ public final ObjectProperty>> onVisibilityChangedProperty() { return onVisibilityChanged; } private final ObjectProperty> createPopupHandler = new SimpleObjectProperty>(this, "createPopupHandler", new Callback() { public WebEngine call(PopupFeatures p) { return WebEngine.this; } }); /** * Returns the JavaScript popup handler. * @see #createPopupHandlerProperty * @see #setCreatePopupHandler */ public final Callback getCreatePopupHandler() { return createPopupHandler.get(); } /** * Sets the JavaScript popup handler. * @see #createPopupHandlerProperty * @see #getCreatePopupHandler * @see PopupFeatures */ 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}. * * @see PopupFeatures */ public final ObjectProperty> createPopupHandlerProperty() { return createPopupHandler; } private final ObjectProperty> confirmHandler = new SimpleObjectProperty>(this, "confirmHandler"); /** * Returns the JavaScript {@code confirm} handler. * @see #confirmHandlerProperty * @see #setConfirmHandler */ public final Callback getConfirmHandler() { return confirmHandler.get(); } /** * Sets the JavaScript {@code confirm} handler. * @see #confirmHandlerProperty * @see #getConfirmHandler */ 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. */ public final ObjectProperty> confirmHandlerProperty() { return confirmHandler; } private final ObjectProperty> promptHandler = new SimpleObjectProperty>(this, "promptHandler"); /** * Returns the JavaScript {@code prompt} handler. * @see #promptHandlerProperty * @see #setPromptHandler * @see PromptData */ public final Callback getPromptHandler() { return promptHandler.get(); } /** * Sets the JavaScript {@code prompt} handler. * @see #promptHandlerProperty * @see #getPromptHandler * @see PromptData */ 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. * * @see PromptData */ public final ObjectProperty> promptHandlerProperty() { return promptHandler; } /** * Creates a new engine. */ public WebEngine() { this(null); } /** * Creates a new engine and loads a Web page into it. */ public WebEngine(String url) { if (instanceCount == 0 && Timer.getMode() == Timer.Mode.PLATFORM_TICKS) { PulseTimer.start(); } Accessor accessor = new AccessorImpl(this); page = new WebPage(this // new WebPageClientImpl(accessor), // new UIClientImpl(accessor), // new PolicyClientImpl(), // new InspectorClientImpl(this), // new ThemeClientImpl(accessor), // false); ); page.addLoadListenerClient(new PageLoadListener(this)); history = new WebHistory(page); Disposer.addRecord(this, new SelfDisposer(page)); load(url); 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 = ""; } 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); } } 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. */ 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. */ public void loadContent(String content, String contentType) { checkThread(); loadWorker.cancelAndReset(); 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 2.2 */ public WebHistory getHistory() { return history; } /** * Executes a script in the context of the current page. * @return execution result, converted to a Java object using the following * rules: *

*/ public Object executeScript(String script) { checkThread(); return page.executeScript(page.getMainFrame(), script); } private long getMainFrame() { return page.getMainFrame(); } WebPage getPage() { return page; } void setView(WebView view) { this.view.setValue(view); } public WebView getView() { return this.view.getValue(); } private void stop() { checkThread(); page.stop(page.getMainFrame()); } private static final class SelfDisposer implements DisposerRecord { private final WebPage page; private SelfDisposer(WebPage page) { this.page = page; } @Override public void dispose() { page.dispose(); 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 = new TKPulseListener() { public void pulse() { // 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. // 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_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: 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); } @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(new PrivilegedAction() { @Override public Void run() { messageCallback.call(message); return null; } }, webEngine.page.getAccessControlContext()); result = true; } } return result; } } }