Spec-Zone .ru
спецификации, руководства, описания, API
001/*
002 * Copyright (c) 2011, 2013, Oracle and/or its affiliates. All rights reserved.
003 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
004 *
005 * This code is free software; you can redistribute it and/or modify it
006 * under the terms of the GNU General Public License version 2 only, as
007 * published by the Free Software Foundation.  Oracle designates this
008 * particular file as subject to the "Classpath" exception as provided
009 * by Oracle in the LICENSE file that accompanied this code.
010 *
011 * This code is distributed in the hope that it will be useful, but WITHOUT
012 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
013 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
014 * version 2 for more details (a copy is included in the LICENSE file that
015 * accompanied this code).
016 *
017 * You should have received a copy of the GNU General Public License version
018 * 2 along with this work; if not, write to the Free Software Foundation,
019 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
020 *
021 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
022 * or visit www.oracle.com if you need additional information or have any
023 * questions.
024 */
025
026package javafx.scene.web;
027
028import javafx.animation.AnimationTimer;
029import javafx.beans.property.ObjectProperty;
030import javafx.beans.property.ReadOnlyBooleanProperty;
031import javafx.beans.property.ReadOnlyDoubleProperty;
032import javafx.beans.property.ReadOnlyObjectProperty;
033import javafx.beans.property.ReadOnlyStringProperty;
034import javafx.beans.property.ReadOnlyBooleanWrapper;
035import javafx.beans.property.ReadOnlyDoubleWrapper;
036import javafx.beans.property.ReadOnlyObjectWrapper;
037import javafx.beans.property.ReadOnlyStringWrapper;
038import javafx.beans.property.SimpleObjectProperty;
039import javafx.concurrent.Worker;
040import javafx.event.EventHandler;
041import javafx.util.Callback;
042
043import org.w3c.dom.Document;
044
045import com.sun.javafx.tk.TKPulseListener;
046import com.sun.javafx.tk.Toolkit;
047import java.security.AccessController;
048import java.security.PrivilegedAction;
049import javafx.beans.property.*;
050import javafx.geometry.Rectangle2D;
051
052/**
053 * {@code WebEngine} is a non-visual object capable of managing one Web page
054 * at a time. One can load Web pages into an engine, track loading progress,
055 * access document model of a loaded page, and execute JavaScript on the page.
056 * 
057 * <p>Loading is always asynchronous. Methods that initiate loading return
058 * immediately after scheduling a job, so one must not assume loading is
059 * complete by that time. {@link #getLoadWorker} method can be used to track
060 * loading status.
061 * 
062 * <p>A number of JavaScript handlers and callbacks may be registered with a
063 * {@code WebEngine}. These are invoked when a script running on the page
064 * accesses user interface elements that lie beyond the control of the
065 * {@code WebEngine}, such as browser window, toolbar or status line.
066 *
067 * <p>{@code WebEngine} objects must be created and accessed solely from the
068 * FXthread.
069 */
070final public class WebEngine {
071
072    /**
073     * The node associated with this engine. There is a one-to-one correspondance
074     * between the WebView and its WebEngine (although not all WebEngines have
075     * a WebView, every WebView has one and only one WebEngine).
076     */
077    private final ObjectProperty<WebView> view = new SimpleObjectProperty<WebView>(this, "view");
078
079    /**
080     * The Worker which shows progress of the web engine as it loads pages.
081     */
082    private final LoadWorker loadWorker = new LoadWorker();
083    
084    /**
085     * Returns a {@link javafx.concurrent.Worker} object that can be used to
086     * track loading progress.
087     */
088    public final Worker<Void> getLoadWorker() {
089        return loadWorker;
090    }
091    
092    /**
093     * The final document. This may be null if no document has been loaded.
094     */
095    private final DocumentProperty document = new DocumentProperty();
096
097    /**
098     * Returns the document object for the current Web page. If the Web page
099     * failed to load, returns {@code null}.
100     */
101    public final Document getDocument() { return document.getValue(); }
102    
103    /**
104     * Document object for the current Web page. The value is {@code null}
105     * if the Web page failed to load.
106     */   
107    public final ReadOnlyObjectProperty<Document> documentProperty() { 
108        return document;
109    }
110
111    /**
112     * The location of the current page. This may return null.
113     */
114    private final ReadOnlyStringWrapper location = new ReadOnlyStringWrapper(this, "location");
115    
116    /**
117     * Returns URL of the current Web page. If the current page has no URL,
118     * returns an empty String.
119     */   
120    public final String getLocation() { return location.getValue(); }
121    
122    /**
123     * URL of the current Web page. If the current page has no URL,
124     * the value is an empty String.
125     */
126    public final ReadOnlyStringProperty locationProperty() { return location.getReadOnlyProperty(); }
127
128    /**
129     * The page title.
130     */
131    private final ReadOnlyStringWrapper title = new ReadOnlyStringWrapper(this, "title");
132    
133    /**
134     * Returns title of the current Web page. If the current page has no title,
135     * returns {@code null}.
136     */
137    public final String getTitle() { return title.getValue(); }
138    
139    /**
140     * Title of the current Web page. If the current page has no title,
141     * the value is {@code null}.
142     */
143    public final ReadOnlyStringProperty titleProperty() { return title.getReadOnlyProperty(); }
144    
145    //
146    // Settings
147    
148    /**
149     * Specifies whether JavaScript execution is enabled.
150     * 
151     * @defaultValue true
152     * @since 2.2
153     */
154    private BooleanProperty javaScriptEnabled;
155
156    public final void setJavaScriptEnabled(boolean value) {
157        javaScriptEnabledProperty().set(value);
158    }
159
160    public final boolean isJavaScriptEnabled() {
161        return javaScriptEnabled == null ? true : javaScriptEnabled.get();
162    }
163
164    public final BooleanProperty javaScriptEnabledProperty() {
165        if (javaScriptEnabled == null) {
166            javaScriptEnabled = new BooleanPropertyBase(true) {
167                @Override public void invalidated() {
168                    checkThread();
169                }
170
171                @Override public Object getBean() {
172                    return WebEngine.this;
173                }
174
175                @Override public String getName() {
176                    return "javaScriptEnabled";
177                }
178            };
179        }
180        return javaScriptEnabled;
181    }
182    
183    /**
184     * Location of the user stylesheet as a string URL.
185     * 
186     * <p>This should be a local URL, i.e. either {@code 'data:'},
187     * {@code 'file:'}, or {@code 'jar:'}. Remote URLs are not allowed
188     * for security reasons.
189     * 
190     * @defaultValue null
191     * @since 2.2
192     */
193    private StringProperty userStyleSheetLocation;
194
195    public final void setUserStyleSheetLocation(String value) {
196        userStyleSheetLocationProperty().set(value);
197    }
198
199    public final String getUserStyleSheetLocation() {
200        return userStyleSheetLocation == null ? null : userStyleSheetLocation.get();
201    }
202
203    public final StringProperty userStyleSheetLocationProperty() {
204        if (userStyleSheetLocation == null) {
205            userStyleSheetLocation = new StringPropertyBase(null) {
206                private final static String DATA_PREFIX = "data:text/css;charset=utf-8;base64,";
207                
208                @Override public void invalidated() {
209                    checkThread();
210                    String url = get();
211                    String dataUrl;
212                    if (url == null || url.length() <= 0) {
213                        dataUrl = null;
214                    } else if (url.startsWith(DATA_PREFIX)) {
215                        dataUrl = url;
216                    } else if (url.startsWith("file:") ||
217                               url.startsWith("jar:")  ||
218                               url.startsWith("data:")) {
219
220                    } else {
221                        throw new IllegalArgumentException("Invalid stylesheet URL");
222                    }
223                }
224
225                @Override public Object getBean() {
226                    return WebEngine.this;
227                }
228
229                @Override public String getName() {
230                    return "userStyleSheetLocation";
231                }
232            };
233        }
234        return userStyleSheetLocation;
235    }
236    
237    /**
238     * Specifies user agent ID string. This string is the value of the
239     * {@code User-Agent} HTTP header.
240     * 
241     * @defaultValue system dependent
242     * @since 8.0
243     */
244    private StringProperty userAgent;
245
246    public final void setUserAgent(String value) {
247        userAgentProperty().set(value);
248    }
249
250    public final String getUserAgent() {
251        return userAgent == null ? null : userAgent.get();
252    }
253
254    public final StringProperty userAgentProperty() {
255        if (userAgent == null) {
256            userAgent = new StringPropertyBase() {
257                @Override public void invalidated() {
258                    checkThread();
259                }
260
261                @Override public Object getBean() {
262                    return WebEngine.this;
263                }
264
265                @Override public String getName() {
266                    return "userAgent";
267                }
268            };
269        }
270        return userAgent;
271    }
272    
273    private final ObjectProperty<EventHandler<WebEvent<String>>> onAlert
274            = new SimpleObjectProperty<EventHandler<WebEvent<String>>>(this, "onAlert");
275    
276    /**
277     * Returns the JavaScript {@code alert} handler.
278     * @see #onAlertProperty
279     * @see #setOnAlert
280     */  
281    public final EventHandler<WebEvent<String>> getOnAlert() { return onAlert.get(); }
282    
283    /**
284     * Sets the JavaScript {@code alert} handler.
285     * @see #onAlertProperty
286     * @see #getOnAlert
287     */
288    public final void setOnAlert(EventHandler<WebEvent<String>> handler) { onAlert.set(handler); }
289    
290    /**
291     * JavaScript {@code alert} handler property. This handler is invoked
292     * when a script running on the Web page calls the {@code alert} function.
293     */
294    public final ObjectProperty<EventHandler<WebEvent<String>>> onAlertProperty() { return onAlert; }
295
296    
297    private final ObjectProperty<EventHandler<WebEvent<String>>> onStatusChanged
298            = new SimpleObjectProperty<EventHandler<WebEvent<String>>>(this, "onStatusChanged");
299    
300    /**
301     * Returns the JavaScript status handler.
302     * @see #onStatusChangedProperty
303     * @see #setOnStatusChanged
304     */
305    public final EventHandler<WebEvent<String>> getOnStatusChanged() { return onStatusChanged.get(); }
306    
307    /**
308     * Sets the JavaScript status handler.
309     * @see #onStatusChangedProperty
310     * @see #getOnStatusChanged
311     */
312    public final void setOnStatusChanged(EventHandler<WebEvent<String>> handler) { onStatusChanged.set(handler); }
313    
314    /**
315     * JavaScript status handler property. This handler is invoked when
316     * a script running on the Web page sets {@code window.status} property.
317     */
318    public final ObjectProperty<EventHandler<WebEvent<String>>> onStatusChangedProperty() { return onStatusChanged; }
319
320    
321    private final ObjectProperty<EventHandler<WebEvent<Rectangle2D>>> onResized
322            = new SimpleObjectProperty<EventHandler<WebEvent<Rectangle2D>>>(this, "onResized");
323    
324    /**
325     * Returns the JavaScript window resize handler.
326     * @see #onResizedProperty
327     * @see #setOnResized
328     */
329    public final EventHandler<WebEvent<Rectangle2D>> getOnResized() { return onResized.get(); }
330
331    /**
332     * Sets the JavaScript window resize handler.
333     * @see #onResizedProperty
334     * @see #getOnResized
335     */
336    public final void setOnResized(EventHandler<WebEvent<Rectangle2D>> handler) { onResized.set(handler); }
337    
338    /**
339     * JavaScript window resize handler property. This handler is invoked
340     * when a script running on the Web page moves or resizes the
341     * {@code window} object.
342     */
343    public final ObjectProperty<EventHandler<WebEvent<Rectangle2D>>> onResizedProperty() { return onResized; }
344
345    
346    private final ObjectProperty<EventHandler<WebEvent<Boolean>>> onVisibilityChanged
347            = new SimpleObjectProperty<EventHandler<WebEvent<Boolean>>>(this, "onVisibilityChanged");
348    
349    /**
350     * Returns the JavaScript window visibility handler.
351     * @see #onVisibilityChangedProperty
352     * @see #setOnVisibilityChanged
353     */
354    public final EventHandler<WebEvent<Boolean>> getOnVisibilityChanged() { return onVisibilityChanged.get(); }
355
356    /**
357     * Sets the JavaScript window visibility handler.
358     * @see #onVisibilityChangedProperty
359     * @see #getOnVisibilityChanged
360     */
361    public final void setOnVisibilityChanged(EventHandler<WebEvent<Boolean>> handler) { onVisibilityChanged.set(handler); }
362
363    /**
364     * JavaScript window visibility handler property. This handler is invoked
365     * when a script running on the Web page changes visibility of the
366     * {@code window} object.
367     */
368    public final ObjectProperty<EventHandler<WebEvent<Boolean>>> onVisibilityChangedProperty() { return onVisibilityChanged; }
369
370    
371    private ObjectProperty<Callback> createPopupHandler
372            = new SimpleObjectProperty<Callback>(this, "createPopupHandler",
373                new Callback() {
374                    public WebEngine call(Object o) {
375                        return WebEngine.this;
376                    }
377                });
378    
379    /**
380     * Returns the JavaScript popup handler.
381     * @see #createPopupHandlerProperty
382     * @see #setCreatePopupHandler
383     */
384    public final Callback getCreatePopupHandler() { return createPopupHandler.get(); }
385
386    /**
387     * Sets the JavaScript popup handler.
388     * @see #createPopupHandlerProperty
389     * @see #getCreatePopupHandler
390     * @see PopupFeatures
391     */
392    public final void setCreatePopupHandler(Callback handler) { createPopupHandler.set(handler); }
393    
394    /**
395     * JavaScript popup handler property. This handler is invoked when a script
396     * running on the Web page requests a popup to be created.
397     * <p>To satisfy this request a handler may create a new {@code WebEngine},
398     * attach a visibility handler and optionally a resize handler, and return
399     * the newly created engine. To block the popup, a handler should return
400     * {@code null}.
401     * <p>By default, a popup handler is installed that opens popups in this
402     * {@code WebEngine}.
403     * 
404     * @see PopupFeatures
405     */
406    public final ObjectProperty<Callback> createPopupHandlerProperty() { return createPopupHandler; }
407
408    
409    private ObjectProperty<Callback<String, Boolean>> confirmHandler
410            = new SimpleObjectProperty<Callback<String, Boolean>>(this, "confirmHandler");
411    
412    /**
413     * Returns the JavaScript {@code confirm} handler.
414     * @see #confirmHandlerProperty
415     * @see #setConfirmHandler
416     */
417    public final Callback<String, Boolean> getConfirmHandler() { return confirmHandler.get(); }
418    
419    /**
420     * Sets the JavaScript {@code confirm} handler.
421     * @see #confirmHandlerProperty
422     * @see #getConfirmHandler
423     */
424    public final void setConfirmHandler(Callback<String, Boolean> handler) { confirmHandler.set(handler); }
425    
426    /**
427     * JavaScript {@code confirm} handler property. This handler is invoked
428     * when a script running on the Web page calls the {@code confirm} function.
429     * <p>An implementation may display a dialog box with Yes and No options,
430     * and return the user's choice.
431     */
432    public final ObjectProperty<Callback<String, Boolean>> confirmHandlerProperty() { return confirmHandler; }
433
434    
435    private ObjectProperty<Callback> promptHandler
436            = new SimpleObjectProperty<Callback>(this, "promptHandler");
437    
438    /**
439     * Returns the JavaScript {@code prompt} handler.
440     * @see #promptHandlerProperty
441     * @see #setPromptHandler
442     * @see PromptData
443     */
444    public final Callback getPromptHandler() { return promptHandler.get(); }
445
446    /**
447     * Sets the JavaScript {@code prompt} handler.
448     * @see #promptHandlerProperty
449     * @see #getPromptHandler
450     * @see PromptData
451     */
452    public final void setPromptHandler(Callback handler) { promptHandler.set(handler); }
453    
454    /**
455     * JavaScript {@code prompt} handler property. This handler is invoked
456     * when a script running on the Web page calls the {@code prompt} function.
457     * <p>An implementation may display a dialog box with an text field,
458     * and return the user's input.
459     *
460     * @see PromptData
461     */
462    public final ObjectProperty<Callback> promptHandlerProperty() { return promptHandler; }
463
464    /**
465     * Creates a new engine.
466     */
467    public WebEngine() {
468        this(null);
469    }
470
471    /**
472     * Creates a new engine and loads a Web page into it.
473     */
474    public WebEngine(String url) {
475        js2javaBridge = new JS2JavaBridge(this);
476        load(url);
477    }
478
479    /**
480     * Loads a Web page into this engine. This method starts asynchronous
481     * loading and returns immediately.
482     * @param url URL of the web page to load
483     */
484    public void load(String url) {
485        checkThread();
486        
487        if (url == null) {
488            url = "";
489        }
490        
491        if (view.get() != null) {
492            _loadUrl(view.get().getNativeHandle(), url);
493        }
494    }
495    
496    /* Loads a web page */
497    private native void _loadUrl(long handle, String url);
498
499    /**
500     * Loads the given HTML content directly. This method is useful when you have an HTML
501     * String composed in memory, or loaded from some system which cannot be reached via
502     * a URL (for example, the HTML text may have come from a database). As with
503     * {@link #load(String)}, this method is asynchronous.
504     */
505    public void loadContent(String content) {
506        loadContent(content, "text/html");
507    }
508
509    /**
510     * Loads the given content directly. This method is useful when you have content
511     * composed in memory, or loaded from some system which cannot be reached via
512     * a URL (for example, the SVG text may have come from a database). As with
513     * {@link #load(String)}, this method is asynchronous. This method also allows you to
514     * specify the content type of the string being loaded, and so may optionally support
515     * other types besides just HTML.
516     */
517    public void loadContent(String content, String contentType) {
518        checkThread();
519        _loadContent(view.get().getNativeHandle(), content);
520    }
521    
522    /* Loads the given content directly */
523    private native void _loadContent(long handle, String content);
524
525    /**
526     * Reloads the current page, whether loaded from URL or directly from a String in
527     * one of the {@code loadContent} methods.
528     */
529    public void reload() {
530        checkThread();
531    }
532
533    /**
534     * Executes a script in the context of the current page.
535     * @return execution result, converted to a Java object using the following
536     * rules:
537     * <ul>
538     * <li>JavaScript Int32 is converted to {@code java.lang.Integer}
539     * <li>Other JavaScript numbers to {@code java.lang.Double}
540     * <li>JavaScript string to {@code java.lang.String}
541     * <li>JavaScript boolean to {@code java.lang.Boolean}
542     * <li>JavaScript {@code null} to {@code null}
543     * <li>Most JavaScript objects get wrapped as
544     *     {@code netscape.javascript.JSObject}
545     * <li>JavaScript JSNode objects get mapped to instances of
546     *     {@code netscape.javascript.JSObject}, that also implement
547     *     {@code org.w3c.dom.Node}
548     * <li>A special case is the JavaScript class {@code JavaRuntimeObject}
549     *     which is used to wrap a Java object as a JavaScript value - in this
550     *     case we just extract the original Java value.
551     * </ul>
552     */
553    public Object executeScript(String script) {
554        checkThread();
555        
556        StringBuilder b = new StringBuilder("fxEvaluate('");
557        b.append(escapeScript(script));
558        b.append("')");
559        String retVal = _executeScript(view.get().getNativeHandle(), b.toString());
560    
561        try {
562            return js2javaBridge.decode(retVal);
563        } catch (Exception ex) {
564            System.err.println("Couldn't parse arguments. " + ex);
565        }
566        return null;
567    }
568    
569    void executeScriptDirect(String script) {
570        _executeScript(view.get().getNativeHandle(), script);
571    }
572    
573    /* Executes a script in the context of the current page */
574    private native String _executeScript(long handle, String script);
575
576    void setView(WebView view) {
577        this.view.setValue(view);
578    }
579    
580    private void stop() {
581        checkThread();
582    }
583
584    private String escapeScript(String script) {
585        return script.replace((CharSequence) "\\", (CharSequence) "\\\\")
586                .replace((CharSequence) "'", (CharSequence) "\\'")
587                .replace((CharSequence) "\"", (CharSequence) "\\\"")
588                .replace((CharSequence) "\n", (CharSequence) "\\n")
589                .replace((CharSequence) "\r", (CharSequence) "\\r")
590                .replace((CharSequence) "\t", (CharSequence) "\\t");
591    }
592
593    /**
594     * Drives the {@code Timer} when {@code Timer.Mode.PLATFORM_TICKS} is set.
595     */
596    private static class PulseTimer {
597
598        // Used just to guarantee constant pulse activity. See RT-14433.
599        private static AnimationTimer animation =
600            new AnimationTimer() {
601                @Override public void handle(long l) {}
602            };
603
604        private static TKPulseListener listener =
605            new TKPulseListener() {
606                public void pulse() {
607                    // Note, the timer event is executed right in the notifyTick(),
608                    // that is during the pulse event. This makes the timer more
609                    // repsonsive, though prolongs the pulse. So far it causes no
610                    // problems but nevertheless it should be kept in mind.
611                    //Timer.getTimer().notifyTick();
612                }
613            };
614
615        public static void start(){
616            Toolkit.getToolkit().addSceneTkPulseListener(listener);
617            animation.start();
618        }
619
620        public static void stop() {
621            Toolkit.getToolkit().removeSceneTkPulseListener(listener);
622            animation.stop();
623        }
624    }
625
626    static void checkThread() {
627        Toolkit.getToolkit().checkFxUserThread();
628    }
629    
630    private class LoadWorker implements Worker<Void> {
631        
632        private ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<State>(this, "state", State.READY);
633        @Override public final State getState() { checkThread(); return state.get(); }
634        @Override public final ReadOnlyObjectProperty<State> stateProperty() { checkThread(); return state.getReadOnlyProperty(); }
635        private final void updateState(State value) {
636            checkThread();
637            this.state.set(value);
638            running.set(value == State.SCHEDULED || value == State.RUNNING);
639        }
640
641        /**
642         * @InheritDoc
643         */
644        private ReadOnlyObjectWrapper<Void> value = new ReadOnlyObjectWrapper<Void>(this, "value", null);
645        @Override public final Void getValue() { checkThread(); return value.get(); }
646        @Override public final ReadOnlyObjectProperty<Void> valueProperty() { checkThread(); return value.getReadOnlyProperty(); }
647
648        /**
649         * @InheritDoc
650         */
651        private ReadOnlyObjectWrapper<Throwable> exception = new ReadOnlyObjectWrapper<Throwable>(this, "exception");
652        @Override public final Throwable getException() { checkThread(); return exception.get(); }
653        @Override public final ReadOnlyObjectProperty<Throwable> exceptionProperty() { checkThread(); return exception.getReadOnlyProperty(); }
654
655        /**
656         * @InheritDoc
657         */
658        private ReadOnlyDoubleWrapper workDone = new ReadOnlyDoubleWrapper(this, "workDone", -1);
659        @Override public final double getWorkDone() { checkThread(); return workDone.get(); }
660        @Override public final ReadOnlyDoubleProperty workDoneProperty() { checkThread(); return workDone.getReadOnlyProperty(); }
661
662        /**
663         * @InheritDoc
664         */
665        private ReadOnlyDoubleWrapper totalWorkToBeDone = new ReadOnlyDoubleWrapper(this, "totalWork", -1);
666        @Override public final double getTotalWork() { checkThread(); return totalWorkToBeDone.get(); }
667        @Override public final ReadOnlyDoubleProperty totalWorkProperty() { checkThread(); return totalWorkToBeDone.getReadOnlyProperty(); }
668
669        /**
670         * @InheritDoc
671         */
672        private ReadOnlyDoubleWrapper progress = new ReadOnlyDoubleWrapper(this, "progress", -1);
673        @Override public final double getProgress() { checkThread(); return progress.get(); }
674        @Override public final ReadOnlyDoubleProperty progressProperty() { checkThread(); return progress.getReadOnlyProperty(); }
675        private void updateProgress(double p) {
676            totalWorkToBeDone.set(100.0);
677            workDone.set(p * 100.0);
678            progress.set(p);
679        }
680
681        /**
682         * @InheritDoc
683         */
684        private ReadOnlyBooleanWrapper running = new ReadOnlyBooleanWrapper(this, "running", false);
685        @Override public final boolean isRunning() { checkThread(); return running.get(); }
686        @Override public final ReadOnlyBooleanProperty runningProperty() { checkThread(); return running.getReadOnlyProperty(); }
687
688        /**
689         * @InheritDoc
690         */
691        private ReadOnlyStringWrapper message = new ReadOnlyStringWrapper(this, "message", "");
692        @Override public final String getMessage() { return message.get(); }
693        @Override public final ReadOnlyStringProperty messageProperty() { return message.getReadOnlyProperty(); }
694
695        /**
696         * @InheritDoc
697         */
698        private ReadOnlyStringWrapper title = new ReadOnlyStringWrapper(this, "title", "WebEngine Loader");
699        @Override public final String getTitle() { return title.get(); }
700        @Override public final ReadOnlyStringProperty titleProperty() { return title.getReadOnlyProperty(); }
701
702        /**
703         * Cancels the loading of the page. If called after the page has already
704         * been loaded, then this call takes no effect.
705         */
706        @Override public boolean cancel() {
707            if (isRunning()) {
708                stop(); // this call indirectly sets state
709                return true;
710            } else {
711                return false;
712            }
713        }
714
715        private void cancelAndReset() {
716            cancel();
717            exception.set(null);
718            message.set("");
719            totalWorkToBeDone.set(-1);
720            workDone.set(-1);
721            progress.set(-1);
722            updateState(State.READY);
723            running.set(false);
724        }
725
726        private void dispatchLoadEvent(long frame, int state,
727                String url, String contentType, double workDone, int errorCode) {
728        }
729
730        Throwable describeError(int errorCode) {
731            String reason = "Unknown error";
732            
733            return new Throwable(reason);
734        }
735    }
736    
737    private final class DocumentProperty
738            extends ReadOnlyObjectPropertyBase<Document> {
739
740        private boolean available;
741        private Document document;
742
743        private void invalidate(boolean available) {
744            if (this.available || available) {
745                this.available = available;
746                this.document = null;
747                fireValueChangedEvent();
748            }
749        }
750
751        public Document get() {
752            if (!this.available) {
753                return null;
754            }
755            if (this.document == null) {
756                if (this.document == null) {
757                    this.available = false;
758                }
759            }
760            return this.document;
761        }
762
763        public Object getBean() {
764            return WebEngine.this;
765        }
766
767        public String getName() {
768            return "document";
769        }
770    }
771
772    ///////////////////////////////////////////////
773    // JavaScript to Java bridge
774    ///////////////////////////////////////////////
775    
776    private JS2JavaBridge js2javaBridge = null;
777    
778    public void exportObject(String jsName, Object object) {
779        synchronized (loadedLock) {
780            if (js2javaBridge == null) {
781                js2javaBridge = new JS2JavaBridge(this);
782            }
783            js2javaBridge.exportObject(jsName, object);
784        }
785    }
786    
787    
788    interface PageListener {
789        void onLoadStarted();
790        void onLoadFinished();
791        void onLoadFailed();
792        void onJavaCall(String args);
793    }
794    
795    private PageListener pageListener = null;
796    private boolean loaded = false;
797    private final Object loadedLock = new Object();
798    
799    void setPageListener(PageListener listener) {
800        synchronized (loadedLock) {
801            pageListener = listener;
802            if (loaded) {
803                pageListener.onLoadStarted();
804                pageListener.onLoadFinished();
805            }
806        }
807    }
808    
809    boolean isLoaded() {
810        return loaded;
811    }
812    
813    // notifications are called from WebView
814    void notifyLoadStarted() {
815        synchronized (loadedLock) {
816            loaded = false;
817            if (pageListener != null) {
818                pageListener.onLoadStarted();
819            }
820        }
821    }
822
823    void notifyLoadFinished() {
824        synchronized (loadedLock) {
825            loaded = true;
826            if (pageListener != null) {
827                pageListener.onLoadFinished();
828            }
829        }
830    }
831
832    void notifyLoadFailed() {
833        synchronized (loadedLock) {
834            loaded = false;
835            if (pageListener != null) {
836                pageListener.onLoadFailed();
837            }
838        }
839    }
840
841    void notifyJavaCall(String arg) {
842        if (pageListener != null) {
843            pageListener.onJavaCall(arg);
844        }
845    }
846
847    void onAlertNotify(String text) {
848        if (getOnAlert() != null) {
849            dispatchWebEvent(
850                    getOnAlert(),
851                    new WebEvent<String>(this, WebEvent.ALERT, text));
852        }
853    }
854
855    private void dispatchWebEvent(final EventHandler handler, final WebEvent ev) {
856        AccessController.doPrivileged(new PrivilegedAction<Void>() {
857            @Override 
858            public Void run() {
859                handler.handle(ev);
860                return null;
861            }
862        });
863    }
864}