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 to java.lang.String
  • JavaScript boolean to java.lang.Boolean
  • JavaScript null or undefined to null
  • JavaScript Promise to JsPromise
  • JavaScript objects get wrapped as JsObject
  • JavaScript functions get wrapped as JsFunction
  • JavaScript DOM Node objects get wrapped as both JsObject and EventTarget
  • JavaScript ArrayBuffer get wrapped as JsArrayBuffer
  • JavaScript Array get wrapped as JsArray
  • JavaScript Set get wrapped as JsSet
  • JavaScript Map get wrapped as JsMap

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 JavaScript Number
  • java.lang.String to JavaScript string
  • java.lang.Boolean to JavaScript boolean
  • Java null to JavaScript null
  • JsObject to an appropriate JavaScript object
  • JsPromise to JavaScript Promise
  • EventTarget to an appropriate JavaScript DOM Node object
  • java.lang.Object will be wrapped into a JavaScript proxy object
  • java.util.List<?> to JavaScript Array or proxy object
  • JsArray to JavaScript Array
  • java.util.Set<?> to JavaScript Set or proxy object
  • JsSet to JavaScript Set
  • java.util.Map<?,?> to JavaScript Map or proxy object
  • JsMap to JavaScript Map
  • byte[] to JavaScript ArrayBuffer
  • JsArrayBuffer to JavaScript ArrayBuffer

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.set(InjectJsCallback::class.java, 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 TopClass {
    @JsAccessible
    public Object accessibleField;
    @JsAccessible
    public void accessibleMethod() {}
}
class TopClass {
    @JsAccessible
    var accessibleField: Any? = null
    @JsAccessible
    fun accessibleMethod() {}
}

Annotated methods and fields of a public static nested class are accessible:

public final class TopClass {
   public static class NestedClass {
       @JsAccessible
       public Object accessibleField;
       @JsAccessible
       public void accessibleMethod() {}
   }
}
class TopClass {
    class NestedClass {
        @JsAccessible
        var accessibleField: Any? = null
        @JsAccessible
        fun accessibleMethod() {}
    }
}

Unannotated methods and fields of an annotated class are accessible:

@JsAccessible
public final class TopClass {
    public Object accessibleField;
    public void accessibleMethod() {}
}
@JsAccessible
class TopClass {
    var accessibleField: Any? = null
    fun accessibleMethod() {}
}

Methods and fields of a base annotated class are accessible from inheritors:

public final class TopClass {
   @JsAccessible
   public static class BaseNestedClass {
       public Object accessibleFieldFromInheritor;
       public void accessibleMethodFromInheritor() {}
   }
   public static class NestedClass extends BaseNestedClass {
       public Object inaccessibleField;
       public void inaccessibleMethod() {}
   }
}
class TopClass {
    @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 TopClass {
   public static class BaseNestedClass {
       public Object inaccessibleField;
       public void inaccessibleMethod() {}
   }
   @JsAccessible
   public static class NestedClass extends BaseNestedClass {
       public Object accessibleField;
       public void accessibleMethod() {}
   }
}
class TopClass {
    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 TopClass {
   public static class BaseNestedClass {
       @JsAccessible
       public void method() {}
   }
   public static class NestedClass extends BaseNestedClass {
       @Override
       public void method() {} // accessible
   }
}
class TopClass {
    open class BaseNestedClass {
        @JsAccessible
        open fun method() {
        }
    }
    class NestedClass : BaseNestedClass() {
        override fun method() {} // accessible
    }
}

Implemented interface methods are accessible:

public static class TopClass {
   public interface NestedInterface {
       @JsAccessible
       void method();
   }
   public static class AccessibleImplementor implements NestedInterface {
       @Override
       public void method() { } // accessible
   }
}
class TopClass {
    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 JavaObject {
    @JsAccessible
    public int method(int intValue) {
        return intValue;
    }
}
class JavaObject {
    @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.on(ConsoleMessageReceived::class.java) { event ->
    val consoleMessage = event.consoleMessage()
    val level = consoleMessage.level()
    val message = consoleMessage.message()
}
Go Top