
org.netbeans.html.boot.spi.Fn Maven / Gradle / Ivy
Show all versions of net.java.html.boot Show documentation
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.netbeans.html.boot.spi;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import java.net.URL;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executor;
import net.java.html.js.JavaScriptBody;
import org.netbeans.html.boot.impl.FnContext;
/** Represents single JavaScript function that can be invoked.
* Created via {@link Presenter#defineFn(java.lang.String, java.lang.String...)}.
*
* @author Jaroslav Tulach
*/
public abstract class Fn {
private final Ref presenter;
/**
* @deprecated Ineffective as of 0.6.
* Provide a presenter via {@link #Fn(org.netbeans.html.boot.spi.Fn.Presenter)}
* constructor
*/
@Deprecated
protected Fn() {
this(null);
}
/** Creates new function object and associates it with given presenter.
*
* @param presenter the browser presenter associated with this function
* @since 0.6
*/
protected Fn(Presenter presenter) {
this.presenter = ref(presenter);
}
/** True, if currently active presenter is the same as presenter this
* function has been created for via {@link #Fn(org.netbeans.html.boot.spi.Fn.Presenter)}.
*
* @return true, if proper presenter is used
*/
public final boolean isValid() {
return FnContext.currentPresenter(false) == presenter();
}
/** Helper method to check if the provided instance is valid function.
* Checks if the parameter is non-null and if so, does {@link #isValid()}
* check.
*
* @param fnOrNull function or null
* @return true if the parameter is non-null and valid
* @since 0.7
*/
public static boolean isValid(Fn fnOrNull) {
return fnOrNull != null && fnOrNull.isValid();
}
/** Helper method to find current presenter and ask it to define new
* function by calling {@link Presenter#defineFn(java.lang.String, java.lang.String...)}.
*
* @param caller the class who wishes to define the function
* @param code the body of the function (can reference this
and names
variables)
* @param names names of individual parameters
* @return the function object that can be {@link Fn#invoke(java.lang.Object, java.lang.Object...) invoked}
* - can return null
if there is {@link #activePresenter() no presenter}
* @since 0.7
*/
public static Fn define(Class> caller, String code, String... names) {
return define(caller, false, code, names);
}
/** Helper method to find current presenter and ask it to define new
* function.
*
* @param caller the class who wishes to define the function
* @param keepParametersAlive whether Java parameters should survive in JavaScript
* after the method invocation is over
* @param code the body of the function (can reference this
and names
variables)
* @param names names of individual parameters
* @return the function object that can be {@link Fn#invoke(java.lang.Object, java.lang.Object...) invoked}
* - can return null
if there is {@link #activePresenter() no presenter}
* @since 1.1
*/
public static Fn define(Class> caller, boolean keepParametersAlive, String code, String... names) {
final Presenter p = FnContext.currentPresenter(false);
if (p == null) {
return null;
}
if (p instanceof KeepAlive) {
boolean[] arr;
if (!keepParametersAlive && names.length > 0) {
arr = new boolean[names.length];
for (int i = 0; i < arr.length; i++) {
arr[i] = false;
}
} else {
arr = null;
}
return ((KeepAlive)p).defineFn(code, names, arr);
}
return p.defineFn(code, names);
}
/** Wraps function to ensure that the script represented by resource
* gets loaded into the browser environment before the function fn
* is executed.
*
* @param fn original function to call (if null
returns null
)
* @param caller the class who wishes to define/call the function
* @param resource resources (accessible via {@link ClassLoader#getResource(java.lang.String)})
* with a JavaScript that is supposed to loaded into the browser
* environment
* @return function that ensures the script is loaded and then delegates
* to fn
. Returns null
if the input fn
is null
* @since 0.7
*/
public static Fn preload(final Fn fn, final Class> caller, final String resource) {
if (fn == null) {
return null;
}
return new Preload(fn.presenter(), fn, resource, caller);
}
/** The currently active presenter.
*
* @return the currently active presenter or null
* @since 0.7
*/
public static Presenter activePresenter() {
return FnContext.currentPresenter(false);
}
/** Activates given presenter. Used to associate the native
* JavaScript code specified by
* {@link JavaScriptBody} annotation with certain presenter:
*
* try ({@link Closeable} c = Fn.activate(presenter)) {
* doCallsInPresenterContext();
* }
*
*
* @param p the presenter that should be active until closable is closed
* @return the closable to close
* @throws NullPointerException if the {@code p} is {@code null}
* @since 0.7
*/
public static Closeable activate(Presenter p) {
if (p == null) {
throw new NullPointerException();
}
return FnContext.activate(p);
}
/** Obtains a (usually {@linkplain WeakReference weak}) reference to
* the presenter. Such reference is suitable for embedding in various long
* living structures with a life-cycle that may outspan the one of presenter.
*
* @param p the presenter
* @return reference to the presenter, {@code null} only if {@code p} is {@code null}
* @since 1.6.1
* @see Ref
*/
public static Ref> ref(final Presenter p) {
if (p == null) {
return null;
}
if (p instanceof Ref>) {
Ref> r = ((Ref>) p).reference();
if (r == null) {
throw new NullPointerException();
}
return r;
}
return new FallbackIdentity(p);
}
/** Invokes the defined function with specified this
and
* appropriate arguments.
*
* @param thiz the meaning of this
inside of the JavaScript
* function - can be null
* @param args arguments for the function
* @return return value from the function
* @throws Exception if something goes wrong, as exception may be thrown
*/
public abstract Object invoke(Object thiz, Object... args) throws Exception;
/** Invokes the defined function with specified this
and
* appropriate arguments asynchronously. The invocation may be
* happen "later".
*
* @param thiz the meaning of this
inside of the JavaScript
* function - can be null
* @param args arguments for the function
* @throws Exception if something goes wrong, as exception may be thrown
* @since 0.7.6
*/
public void invokeLater(Object thiz, Object... args) throws Exception {
invoke(thiz, args);
}
/** Provides the function implementation access to the presenter provided
* in {@link #Fn(org.netbeans.html.boot.spi.Fn.Presenter) the constructor}.
*
* @return presenter passed in the constructor (may be, but should not be null
)
* @since 0.7
*/
protected final Presenter presenter() {
return presenter == null ? null : presenter.presenter();
}
/** The representation of a presenter - usually a browser window.
* Should be provided by a library included in the application and registered
* in META-INF/services
, for example with
* @ServiceProvider(service = Fn.Presenter.class)
annotation.
* To verify the implementation of the presenter is correct, implement
* associated TCK (e.g. test compatibility kit) at least the headless
* one as illustrated at:
*
* {@codesnippet net.java.html.boot.script.ScriptEngineJavaScriptTCK}
*
* Since 0.7 a presenter may implement {@link Executor} interface, in case
* it supports single threaded execution environment. The executor's
* {@link Executor#execute(java.lang.Runnable)} method is then supposed
* to invoke the runnable immediately (in case we are on the right thread
* already) or return and asynchronously invoke the runnable later on the
* right thread (if we are on wrong thread).
*/
public interface Presenter {
/** Creates new function with given parameter names and provided body.
*
* @param code the body of the function. Can refer to variables named
* as names
* @param names names of parameters of the function - these will be
* available when the code
body executes
*
* @return function that can be later invoked
*/
public Fn defineFn(String code, String... names);
/** Opens the browser, loads provided page and when the
* page is ready, it calls back to the provider runnable.
*
* @param page the URL for the page to display
* @param onPageLoad callback when the page is ready
*/
public void displayPage(URL page, Runnable onPageLoad);
/** Loads a script into the browser JavaScript interpreter and
* executes it.
* @param code the script to execute
* @throws Exception if something goes wrong, throw an exception
*/
public void loadScript(Reader code) throws Exception;
}
/** Additional interface to be implemented by {@link Presenter}s that
* wish to control what objects are passed into the JavaScript virtual
* machine.
*
* If a JavaScript engine makes callback to Java method that returns
* a value, the {@link #toJavaScript(java.lang.Object)} method is
* consulted to convert the Java value to something reasonable inside
* JavaScript VM.
*
* Note: The implementation based on JavaFX WebView
* uses this interface to convert Java arrays to JavaScript ones.
*
* @see Presenter
* @since 0.7
*/
public interface ToJavaScript {
/** Convert a Java return value into some object suitable for
* JavaScript virtual machine.
*
* @param toReturn the Java object to be returned
* @return the replacement value to return instead
*/
public Object toJavaScript(Object toReturn);
}
/** Additional interface to be implemented by {@link Presenter}s that
* need to convert JavaScript object (usually array) to Java object
* when calling back from JavaScript to Java.
*
* Note: The implementation based on JavaFX
* WebView
uses this interface to convert JavaScript arrays to
* Java ones.
*
* @since 0.7
*/
public interface FromJavaScript {
/** Convert a JavaScript object into suitable Java representation
* before a Java method is called with this object as an argument.
*
* @param js the JavaScript object
* @return replacement object for
*/
public Object toJava(Object js);
}
/** Additional interface to {@link Presenter} to control more precisely
* garbage collection behavior of individual parameters. See
* {@link JavaScriptBody#keepAlive()} attribute for description of the
* actual behavior of the interface.
*
* @since 1.1
*/
public interface KeepAlive {
/** Creates new function with given parameter names and provided body.
*
* @param code the body of the function. Can refer to variables named
* as names
* @param names names of parameters of the function - these will be
* available when the code
body executes
* @param keepAlive array of booleans describing for each parameter
* whether it should be kept alive or not. Length of the array
* must be the same as length of names
array. The
* array may be null
to signal that all parameters
* should be kept alive.
*
* @return function that can be later invoked
*/
public Fn defineFn(String code, String[] names, boolean[] keepAlive);
}
/** Represents a JavaScript Promise
* in Java. A promise is a microtask to be executed as soon as current
* "runnable" finishes. At that moment the {@link #compute()} method
* is invoked and the result is remebered.
*
* @since 1.8
*/
public static abstract class Promise {
private static Reference last = new WeakReference<>(null);
private static Fn[] lastFn;
private static synchronized Fn[] wrapAndResolve() {
final Presenter ap = Fn.activePresenter();
if (ap == null) {
throw new NullPointerException("No presenter!");
}
if (lastFn != null && last.get() == ap) {
return lastFn;
}
last = new WeakReference<>(ap);
return lastFn = new Fn[] {
Fn.define(Promise.class, """
var arr = [null, null, null];
arr[0] = new Promise(function (success, failure) {
arr[1] = success;
arr[2] = failure;
});
return arr;
"""),
Fn.define(Promise.class, "fn(value);", "fn", "value")
};
}
private final Fn.Presenter presenter;
private final Object promise;
private final Object success;
private final Object failure;
private boolean resolved;
/**
* Constructor for subclasses.
* @param p the presenter to resolve promise with
* @since 1.8
*/
protected Promise(Fn.Presenter p) {
this.presenter = p;
Object[] promiseSuccessFailure;
try (var ctx = Fn.activate(p)) {
promiseSuccessFailure = (Object[]) wrapAndResolve()[0].invoke(null);
} catch (Exception ex) {
throw new IllegalStateException(ex);
}
this.promise = toJava(promiseSuccessFailure[0]);
this.success = toJava(promiseSuccessFailure[1]);
this.failure = toJava(promiseSuccessFailure[2]);
}
/**
* Schedules the promise invocation and returns JavaScript representation
* of the promise.
*
* @return JavaScript {@code Promise} object
* @since 1.8
*/
public final Object schedule() {
try (var ctx = Fn.activate(presenter)) {
FnContext.registerMicrotask(this::resolve);
} catch (IOException ex) {
throw new IllegalStateException(ex);
}
return promise;
}
/** Implement in subclasses to perform the "promised" Java operation.
* Once this method finishes the system resolves the
* {@link #schedule() scheduled promise}.
*
* @return the value to resolve the promise with
* @throws Throwable the exception to resolve the promise with
* @since 1.8
*/
protected abstract Object compute() throws Throwable;
private void resolve() {
if (!resolved) {
try (var ctx = Fn.activate(presenter)) {
try {
Object result = compute();
wrapAndResolve()[1].invoke(null, success, result);
} catch (Throwable ex) {
wrapAndResolve()[1].invoke(null, failure, ex);
} finally {
resolved = true;
}
} catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
}
private Object toJava(Object js) {
if (Fn.activePresenter() instanceof Fn.FromJavaScript p) {
return p.toJava(js);
}
return js;
}
}
/**
* Reference to a {@link Presenter}.Each implementation of a {@link Presenter}
* may choose a way to reference itself (usually in a {@linkplain WeakReference weak way})
* effectively. Various code that needs to hold a reference to a presenter
* is then encouraged to obtain such reference via {@link Fn#ref(org.netbeans.html.boot.spi.Fn.Presenter)}
* call and hold on to it. Holding a reference to an instance of {@link Presenter}
* is discouraged as it may lead to memory leaks.
*
* Presenters willing to to represent a reference to itself effectively shall
* also implement the {@link Ref} interface and return reasonable reference
* from the {@link #reference()} method.
*
* @param
the type of the presenter
* @see Fn#ref(org.netbeans.html.boot.spi.Fn.Presenter)
* @since 1.6.1
*/
public interface Ref
{
/** Creates a reference to a presenter.
* Rather than calling this method directly, call {@link Fn#ref(org.netbeans.html.boot.spi.Fn.Presenter)}.
* @return a (weak) reference to the associated presenter
*/
public Ref
reference();
/** The associated presenter.
*
* @return the presenter or {@code null}, if it has been GCed meanwhile
*/
public P presenter();
/** Reference must properly implement {@link #hashCode} and {@link #equals}.
*
* @return proper hashcode
*/
@Override
public int hashCode();
/** Reference must properly implement {@link #hashCode} and {@link #equals}.
*
* @return proper equals result
*/
@Override
public boolean equals(Object obj);
}
private static class Preload extends Fn {
private static Map> LOADED;
private final Fn fn;
private final String resource;
private final Class> caller;
Preload(Presenter presenter, Fn fn, String resource, Class> caller) {
super(presenter);
this.fn = fn;
this.resource = resource;
this.caller = caller;
}
@Override
public Object invoke(Object thiz, Object... args) throws Exception {
loadResource();
return fn.invoke(thiz, args);
}
@Override
public void invokeLater(Object thiz, Object... args) throws Exception {
loadResource();
fn.invokeLater(thiz, args);
}
private void loadResource() throws Exception {
Ref id = super.presenter;
if (id == null) {
id = ref(FnContext.currentPresenter(false));
}
Fn.Presenter realPresenter = id == null ? null : id.presenter();
if (realPresenter != null) {
if (LOADED == null) {
LOADED = new HashMap>();
}
Set there = LOADED.get(resource);
if (there == null) {
there = new HashSet();
LOADED.put(resource, there);
}
if (there.add(id)) {
final ClassLoader l = caller.getClassLoader();
InputStream is = l.getResourceAsStream(resource);
if (is == null && resource.startsWith("/")) {
is = l.getResourceAsStream(resource.substring(1));
}
if (is == null) {
throw new IOException("Cannot find " + resource + " in " + l);
}
try {
InputStreamReader r = new InputStreamReader(is, "UTF-8");
realPresenter.loadScript(r);
} finally {
is.close();
}
}
}
}
}
}