JavaScript
This guide describes how to access JavaScript on a loaded web page, execute JavaScript code, inject Java objects to call Java from JavaScript, etc.
Executing JavaScript
JxBrowser allows accessing and executing JavaScript code on a loaded web page.
To access JavaScript make sure that web page is loaded completely and JavaScript is enabled.
To execute JavaScript code use the Frame.executeJavaScript(String)
method. This method blocks the current thread execution and waits until the given code is executed. The method returns a java.lang.Object
that represents the result of the execution. The method returns null
if the result of the execution is null
or undefined
.
The following example executes the JavaScript code that returns a title of the document
:
String title = frame.executeJavaScript("document.title");
val title = frame.executeJavaScript<String>("document.title")
You can execute any JavaScript code:
double number = frame.executeJavaScript("123");
boolean bool = frame.executeJavaScript("true");
String string = frame.executeJavaScript("'Hello'");
JsFunction alert = frame.executeJavaScript("window.alert");
JsObject window = frame.executeJavaScript("window");
Element body = frame.executeJavaScript("document.body");
JsPromise promise = frame.executeJavaScript("Promise.resolve('Success')");
JsArray array = frame.executeJavaScript("['Apple', 'Banana']");
JsArrayBuffer arrayBuffer = frame.executeJavaScript("new ArrayBuffer(8)");
JsSet set = frame.executeJavaScript("new Set([1, 2, 3, 4])");
JsMap map = frame.executeJavaScript("new Map([['John', '32'], ['Mary', '26']])");
val number = frame.executeJavaScript<Double>("123")
val bool = frame.executeJavaScript<Boolean>("true")
val string = frame.executeJavaScript<String>("'Hello'")
val alert = frame.executeJavaScript<JsFunction>("window.alert")
val window = frame.executeJavaScript<JsObject>("window")
val body = frame.executeJavaScript<Element>("document.body")
val promise = frame.executeJavaScript<JsPromise>("Promise.resolve('Success')")
val array = frame.executeJavaScript<JsArray>("['Apple', 'Banana']")
val arrayBuffer = frame.executeJavaScript<JsArrayBuffer>("new ArrayBuffer(8)")
val set = frame.executeJavaScript<JsSet>("new Set([1, 2, 3, 4])")
val map = frame.executeJavaScript<JsMap>("new Map([['John', '32'], ['Mary', '26']])")
If you would like do not block the current thread execution, then you can use the Frame.executeJavaScript(String javaScript, Consumer<?> callback)
method. This method executes the given JavaScript code asynchronously and provides the result of the execution through the given callback
:
frame.executeJavaScript("document.body", (Consumer<Element>) body -> {
String html = body.innerHtml();
});
frame.executeJavaScript("document.body", Consumer<Element> { body ->
val html = body.innerHtml()
})
Type conversion
JavaScript and Java work with different primitive types. JxBrowser implements an automatic type conversion from JavaScript to Java types and vice versa.
JavaScript to Java
The following rules are used to convert JavaScript into Java types:
- JavaScript numbers are converted to
java.lang.Double
- JavaScript
string
tojava.lang.String
- JavaScript
boolean
tojava.lang.Boolean
- JavaScript
null
orundefined
tonull
- JavaScript
Promise
toJsPromise
- JavaScript objects get wrapped as
JsObject
- JavaScript functions get wrapped as
JsFunction
- JavaScript DOM Node objects get wrapped as both
JsObject
andEventTarget
- JavaScript
ArrayBuffer
get wrapped asJsArrayBuffer
- JavaScript
Array
get wrapped asJsArray
- JavaScript
Set
get wrapped asJsSet
- JavaScript
Map
get wrapped asJsMap
In the example above we know that the document.title
is a string, so we set the return value to java.lang.String
.
Java to JavaScript
The following rules are used to convert Java into JavaScript types:
java.lang.Double
is converted to JavaScriptNumber
java.lang.String
to JavaScriptstring
java.lang.Boolean
to JavaScriptboolean
- Java
null
to JavaScriptnull
JsObject
to an appropriate JavaScript objectJsPromise
to JavaScriptPromise
EventTarget
to an appropriate JavaScript DOM Node objectjava.lang.Object
will be wrapped into a JavaScript proxy objectjava.util.List<?>
to JavaScriptArray
or proxy objectJsArray
to JavaScriptArray
java.util.Set<?>
to JavaScriptSet
or proxy objectJsSet
to JavaScriptSet
java.util.Map<?,?>
to JavaScriptMap
or proxy objectJsMap
to JavaScriptMap
byte[]
to JavaScriptArrayBuffer
JsArrayBuffer
to JavaScriptArrayBuffer
If you pass a non-primitive Java object to JavaScript, it will be converted into a proxy object. Method and property calls to this object will be delegated to the Java object. For security reasons, JavaScript can access only those methods and fields of the injected Java object that are explicitly marked as accessible either using the @JsAccessible
annotation or via the JsAccessibleTypes
class.
Java collections that are not made accessible to JavaScript using the @JsAccessible
annotation or via the JsAccessibleTypes
class are converted to JavaScript collections. The content of the converted collection is a deep copy of the Java collection. Modifications of the converted collection in JavaScript do not affect the collection in Java.
Java collections that are made accessible to JavaScript using the @JsAccessible
annotation or via the JsAccessibleTypes
class are wrapped into a JavaScript proxy object. Such proxy objects can be used to modify the collection in Java.
DOM wrappers
By the rules of the automatic type conversion JavaScript DOM objects get wrapped as both JsObject
and EventTarget
. It allows you to work with the JavaScript DOM objects through JxBrowser DOM API.
In the following example we return the document
that represents the JavaScript DOM object. In this case the return value can be set to JsObject
or Document
:
Document document = frame.executeJavaScript("document");
val document = frame.executeJavaScript<Document>("document")
JsObject document = frame.executeJavaScript("document");
val document = frame.executeJavaScript<JsObject>("document")
Working with JsObject
To work with JavaScript objects from Java code please use the JsObject
class. It allows working with the object properties and calling its functions.
Properties
To get property names of a JavaScript object, including properties from the prototype objects, please use the propertyNames()
method:
List<String> propertyNames = jsObject.propertyNames();
val propertyNames = jsObject.propertyNames()
To check whether JavaScript object has a specified property, please use the hasProperty(String)
method:
boolean has = jsObject.hasProperty("<property-name>");
val has = jsObject.hasProperty("<property-name>")
To get value of the JavaScript object property by its name please use property(String)
. For example:
JsObject document = frame.executeJavaScript("document");
document.property("title").ifPresent(title -> {});
val document = frame.executeJavaScript<JsObject>("document")!!
document.property<String>("title").ifPresent { title -> }
The return value represents java.lang.Object
that can be set to the required type. See the Type Conversion.
You can remove a property using the following approach:
boolean success = jsObject.removeProperty("<property-name>");
val success = jsObject.removeProperty("<property-name>")
Functions
To call a function with the required name and arguments use the call(String methodName, Object... args)
method. The following example demonstrates how to call the document.getElementById()
JavaScript function:
JsObject element = document.call("getElementById", "elementId");
val element: JsObject = document.call("getElementById", "elementId")
which is equivalent to the following code in JavaScript:
var element = document.getElementById("demo");
The method throws JsException
if an error occurs during the function execution.
Closing
V8 objects that have a JsObject
counterpart are not subjects to V8 garbage collection. By default, we keep these
objects in memory until the page is unloaded.
To optimize the memory usage, you can enable garbage collection on the per-object basis:
jsObject.close();
jsObject.close()
Closing JsObject
marks the corresponding V8 object as collectable, but it does not release the object immediately.
After calling the close()
method, the attempts to use JsObject
will lead to ObjectClosedException
.
JsFunctionCallback
The other way to call Java from JavaScript is using JsFunctionCallback
.
JavaScript-Java bridge allows you to associate JsFunctionCallback
with a JavaScript property that
will be treated as a function that can be invoked in JavaScript code.
For example, you can register a JavaScript function associated with the JsFunctionCallback
instance using the following code:
JsObject window = frame.executeJavaScript("window");
if (window != null) {
window.putProperty("sayHello", (JsFunctionCallback) args ->
"Hello, " + args[0]);
}
val window = frame.executeJavaScript<JsObject>("window")
window?.putProperty("sayHello", JsFunctionCallback { args -> "Hello, ${args[0]}" })
Now, in JavaScript you can invoke this function in the following way:
window.sayHello('John');
JsFunction
Since 7.7 you can work with the JavaScript functions directly from Java code, and pass the reference to a function from JavaScript to Java. For example:
JsObject window = frame.executeJavaScript("window");
if (window != null) {
JsFunction alert = frame.executeJavaScript("window.alert");
if (alert != null) {
alert.invoke(window, "Hello world!");
}
}
val window = frame.executeJavaScript<JsObject>("window")
if (window != null) {
val alert = frame.executeJavaScript<JsFunction>("window.alert")
alert?.invoke<Any>(window, "Hello world!")
}
JsPromise
Since 7.17 you can work with JavaScript Promises directly from Java code. For example:
JsPromise promise = frame.executeJavaScript(
"new Promise(function(resolve, reject) {\n"
+ " setTimeout(function() {\n"
+ " resolve('Hello Java!');\n"
+ " }, 2000);"
+ "})");
promise.then(results -> {
System.out.println(results[0]);
return promise;
}).then(results -> {
System.out.println(results[0]);
return promise;
}).catchError(errors -> {
System.out.println(errors[0]);
return promise;
});
val promise = frame.executeJavaScript<JsPromise>(
"""new Promise(function(resolve, reject) {
setTimeout(function() {
resolve('Hello Java!');
}, 2000);
})
"""
)!!
promise.then { results ->
println(results[0])
promise
}.then { results ->
println(results[0])
promise
}.catchError { errors ->
println(errors[0])
promise
}
Calling Java from JavaScript
When you pass a java.lang.Object
as a property value, or an argument when calling JavaScript
function, the Java object will automatically be wrapped into a JavaScript object.
It allows injecting Java objects into JavaScript and invoking its public methods and fields from JavaScript.
For security reasons only public non-static methods and fields annotated with @JsAccessible
or declared in the class annotated with @JsAccessible
can be accessed from JavaScript. Annotated protected, private, or package-private methods and fields, or methods and fields declared in a class with such modifiers, remain inaccessible from JavaScript.
To inject Java object into JavaScript, define the Java object class and mark the public method that should be accessible from JavaScript with @JsAccessible
:
public final class JavaObject {
@JsAccessible
public String sayHelloTo(String firstName) {
return "Hello " + firstName + "!";
}
}
class JavaObject {
@JsAccessible
fun sayHelloTo(firstName: String) = "Hello $firstName!"
}
Inject an instance of the Java object into JavaScript before JavaScript is executed on the loaded web page:
browser.set(InjectJsCallback.class, params -> {
JsObject window = params.frame().executeJavaScript("window");
window.putProperty("java", new JavaObject());
return InjectJsCallback.Response.proceed();
});
browser.register(InjectJsCallback { params ->
val window = params.frame().executeJavaScript<JsObject>("window")
window?.putProperty("java", JavaObject())
InjectJsCallback.Response.proceed()
})
Now you can refer to the object and call its method from JavaScript:
window.java.sayHelloTo("John");
Annotating rules
The @JsAccessible
annotation allows exposing methods and fields of an injected Java object to JavaScript.
You can make accessible only public types, methods, and fields. The full list of the supported cases is the following:
- A top-level class or an interface
- A nested static class or an interface
- A non-static method of a class or an interface
- A non-static field of a class
The annotation cannot be applied to non-public types, methods, and fields. Public methods and fields of a non-public type are considered non-public. When you annotate a type, all its public methods and fields become accessible to JavaScript. When you annotate a method or a field of an unannotated type, only the annotated member becomes accessible to JavaScript.
An accessible method remains so when it is overridden in a subclass. It means that you can make an interface accessible and pass any of its implementations to JavaScript: all the methods declared in the interface will be accessible from JavaScript. Other methods and fields declared in the implementing class will remain inaccessible unless you explicitly mark them or the entire type with this annotation.
Another way to make a type accessible from JavaScript is using JsAccessibleTypes
. This is particularly useful when you want to make accessible one of the core Java types (e.g. java.util.List
), or a type from a third-party library that you cannot make accessible using this annotation.
Examples:
Annotated methods and fields of a public top level class are accessible:
public final class TopClassDemo {
@JsAccessible
public Object accessibleField;
@JsAccessible
public void accessibleMethod() {}
}
class TopClassDemo {
@JsAccessible
var accessibleField: Any? = null
@JsAccessible
fun accessibleMethod() {}
}
Annotated methods and fields of a public static nested class are accessible:
public final class TopClassWithNestedDemo {
public static class NestedClass {
@JsAccessible
public Object accessibleField;
@JsAccessible
public void accessibleMethod() {}
}
}
class TopClassWithNestedDemo {
class NestedClass {
@JsAccessible
var accessibleField: Any? = null
@JsAccessible
fun accessibleMethod() {}
}
}
Unannotated methods and fields of an annotated class are accessible:
@JsAccessible
public final class ClassWithUnannotatedMembersDemo {
public Object accessibleField;
public void accessibleMethod() {}
}
@JsAccessible
class ClassWithUnannotatedMembersDemo {
var accessibleField: Any? = null
fun accessibleMethod() {}
}
Methods and fields of a base annotated class are accessible from inheritors:
public final class AccessFromInheritorsDemo {
@JsAccessible
public static class BaseNestedClass {
public Object accessibleFieldFromInheritor;
public void accessibleMethodFromInheritor() {}
}
public static class NestedClass extends BaseNestedClass {
public Object inaccessibleField;
public void inaccessibleMethod() {}
}
}
class AccessFromInheritorsDemo {
@JsAccessible
open class BaseNestedClass {
var accessibleFieldFromInheritor: Any? = null
fun accessibleMethodFromInheritor() {}
}
class NestedClass : BaseNestedClass() {
var inaccessibleField: Any? = null
fun inaccessibleMethod() {}
}
}
Inherited methods and fields are not accessible if they or the class they are declared in are not annotated:
public final class UnannotatedClassAccessDemo {
public static class BaseNestedClass {
public Object inaccessibleField;
public void inaccessibleMethod() {}
}
@JsAccessible
public static class NestedClass extends BaseNestedClass {
public Object accessibleField;
public void accessibleMethod() {}
}
}
class UnannotatedClassAccessDemo {
open class BaseNestedClass {
var inaccessibleField: Any? = null
fun inaccessibleMethod() {}
}
@JsAccessible
class NestedClass : BaseNestedClass() {
var accessibleField: Any? = null
fun accessibleMethod() {}
}
}
Overridden class methods are accessible:
public final class OverriddenMethodsAccessDemo {
public static class BaseNestedClass {
@JsAccessible
public void method() {}
}
public static class NestedClass extends BaseNestedClass {
@Override
public void method() {} // Accessible.
}
}
class OverriddenMethodsAccessDemo {
open class BaseNestedClass {
@JsAccessible
open fun method() {
}
}
class NestedClass : BaseNestedClass() {
override fun method() {} // Accessible.
}
}
Implemented interface methods are accessible:
public final class ImplInterfaceMethodsAccessDemo {
public interface NestedInterface {
@JsAccessible
void method();
}
public static class AccessibleImplementor implements NestedInterface {
@Override
public void method() { } // Accessible.
}
}
class ImplInterfaceMethodsAccessDemo {
interface NestedInterface {
@JsAccessible
fun method()
}
class AccessibleImplementor : NestedInterface {
override fun method() {} // Accessible.
}
}
If the signature of an accessible Java method has a primitive numeric parameter, then the number transferred from JavaScript will be checked for the possibility of converting to the Java parameter type. If the conversion can be performed without data loss and no other suitable overloaded methods were found, the method will be invoked.
If more than one method that can accept the passed parameters does exist, JavaScript throws an exception to indicate that the requested method call is ambiguous and cannot be performed.
If no methods or fields that correspond to the requested name found, JavaScript throws an exception indicating that the requested member does not exist.
If both method and field with the same name requested by JavaScript do exist, JavaScript throws an exception to indicate that the requested member is ambiguous and cannot be accessed.
Automatic type conversion
The JavaScript-Java bridge provides automatic types conversion functionality when calling a public method of the injected Java object from JavaScript.
The library automatically convert the given JavaScript Number
to the required Java type if
it is possible. If we detect that the given number cannot be converted to, for example, a Java
byte
without data loss, then the library throws an exception and notifies JavaScript that there is
no appropriate Java method. If the given value can be converted without data loss, then the library
converts it and invokes the appropriate Java method.
For example, if you inject the following Java object into JavaScript:
public final class JavaObjectWithTypeConversion {
@JsAccessible
public int method(int intValue) {
return intValue;
}
}
class JavaObjectWithTypeConversion {
@JsAccessible
fun method(intValue: Int) = intValue
}
Then you can call it from JavaScript and pass the JavaScript Number
value that can be converted to
an Integer
without data loss:
window.javaObject.method(123);
But, if you pass Double
value that cannot be converted to an Integer
without data loss, you will
get an error:
window.javaObject.method(3.14); // <- error
Calling from libraries
The injected Java objects are special kind of objects. They behave differently from regular JavaScript objects and are not intended to be passed directly to JavaScript libraries.
Before using them in JavaScript libraries, we recommend wrapping them into the Proxy. In this example, we create a proxy object that implements read access to JS accessible members:
const proxy = new Proxy({__java: myJavaObject}, {
get(target, prop, receiver) {
for (let javaMemberName in target.__java) {
if (prop === javaMemberName) {
return target.__java[prop]
}
}
return Reflect.get(...arguments);
},
...
});
Console messages
JxBrowser allows receiving all output messages sent to the Console via the console.log()
JavaScript function. You can listen to the messages with the following levels:
DEBUG
LOG
WARNING
ERROR
To get a notification when the Console gets a message please use the ConsoleMessageReceived
event. For example:
browser.on(ConsoleMessageReceived.class, event -> {
ConsoleMessage consoleMessage = event.consoleMessage();
ConsoleMessageLevel level = consoleMessage.level();
String message = consoleMessage.message();
});
browser.subscribe<ConsoleMessageReceived> { event ->
val consoleMessage = event.consoleMessage()
val level = consoleMessage.level()
val message = consoleMessage.message()
}