JavaScript

Este guia descreve como acessar o JavaScript numa página Web carregada, executar código JavaScript, injetar objetos Java para chamar Java a partir do JavaScript, etc.

Executando JavaScript

O JxBrowser permite acessar e executar código JavaScript numa página Web carregada.

Para acessar o JavaScript, certifique-se de que a página Web está completamente carregada e que o JavaScript está habilitado.

Para executar código JavaScript, use o método Frame.executeJavaScript(String). Este método bloqueia a execução da thread atual e aguarda até que o código fornecido seja executado. O método retorna um java.lang.Object que representa o resultado da execução. O método devolve null se o resultado da execução for null ou undefined.

O exemplo a seguir executa o código JavaScript que retorna um título do document:

String title = frame.executeJavaScript("document.title");
val title = frame.executeJavaScript<String>("document.title")

Você pode executar qualquer código JavaScript:

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']])")

Se você não quiser bloquear a execução da thread atual, então você pode usar o método Frame.executeJavaScript(String javaScript, Consumer<?> callback). Este método executa o código JavaScript fornecido de forma assíncrona e fornece o resultado da execução através do callback fornecido:

frame.executeJavaScript("document.body", (Consumer<Element>) body -> {
    String html = body.innerHtml();
});
frame.executeJavaScript("document.body", Consumer<Element> { body -> 
    val html = body.innerHtml()
})

Conversão de Tipo

O JavaScript e o Java trabalham com tipos primitivos diferentes. O JxBrowser implementa uma conversão automática de tipos JavaScript para tipos Java e vice-versa.

JavaScript para Java

As seguintes regras são utilizadas para converter JavaScript em tipos Java:

  • Os números JavaScript são convertidos para java.lang.Double
  • JavaScript string para java.lang.String
  • JavaScript boolean para java.lang.Boolean
  • JavaScript null ou undefined para null
  • JavaScript Promise para JsPromise
  • Os objetos JavaScript são agrupados como JsObject
  • As funções JavaScript são agrupadas como JsFunction
  • Os objetos JavaScript DOM Node são envolvidos como JsObject e EventTarget
  • JavaScript ArrayBuffer é envolvido como JsArrayBuffer
  • JavaScript Array é envolvido como JsArray
  • JavaScript Set é envolvido como JsSet
  • JavaScript Map é envolvido como JsMap

No exemplo acima, sabemos que document.title é uma string, então definimos o valor de retorno como java.lang.String.

Java para JavaScript

As seguintes regras são utilizadas para converter tipos Java em JavaScript:

  • java.lang.Double é convertido para JavaScript Number
  • java.lang.String para JavaScript string
  • java.lang.Boolean para JavaScript boolean
  • Java null para JavaScript null
  • JsObject para um objeto JavaScript apropriado
  • JsPromise para JavaScript Promise
  • EventTarget para um objeto JavaScript DOM Node apropriado
  • O java.lang.Object será envolvido num objeto proxy JavaScript
  • java.util.List<?> para JavaScript Array ou objeto proxy
  • JsArray para JavaScript Array
  • java.util.Set<?> para JavaScript Set ou objeto proxy
  • JsSet para JavaScript Set
  • java.util.Map<?,?> para JavaScript Map ou objeto proxy
  • JsMap para JavaScript Map
  • byte[] para JavaScript ArrayBuffer
  • JsArrayBuffer para JavaScript ArrayBuffer

Se passar um objeto Java não primitivo para o JavaScript, este será convertido num objeto proxy. As chamadas de métodos e propriedades para este objeto serão delegadas no objeto Java. Por razões de segurança, o JavaScript pode acessar apenas os métodos e campos do objeto Java injetado que são explicitamente marcados como acessíveis, seja usando a anotação @JsAccessible ou através da classe JsAccessibleTypes.

As coleções Java que não são criadas acessíveis ao JavaScript utilizando a anotação @JsAccessible ou através da classe JsAccessibleTypes são convertidas em coleções JavaScript. O conteúdo da coleção convertida é uma cópia profunda da coleção Java. As modificações da coleção convertida em JavaScript não afetam a coleção em Java.

As colecções Java que são feitas acessíveis ao JavaScript utilizando a anotação @JsAccessible ou através da classe JsAccessibleTypes são agrupadas num objeto proxy JavaScript. Estes objetos proxy podem ser utilizados para modificar a coleção em Java.

Wrappers DOM

Pelas regras da conversão automática de tipos, os objetos DOM do JavaScript são agrupados como JsObject e EventTarget. Ela te permite trabalhar com os objetos DOM do JavaScript através da API DOM do JxBrowser.

No exemplo a seguir, retornamos o document que representa o objeto DOM do JavaScript. Neste caso, o valor de retorno pode ser definido como JsObject ou Document:

Document document = frame.executeJavaScript("document");
val document = frame.executeJavaScript<Document>("document")
JsObject document = frame.executeJavaScript("document");
val document = frame.executeJavaScript<JsObject>("document")

Trabalhando com JsObject

Para trabalhar com objetos JavaScript a partir de código Java, utilize a classe JsObject. Ela permite trabalhar com as propriedades do objeto e chamar as suas funções.

Propriedades

Para obter os nomes das propriedades de um objeto JavaScript, incluindo as propriedades dos objetos protótipos, utilize o método propertyNames():

List<String> propertyNames = jsObject.propertyNames();
val propertyNames = jsObject.propertyNames()

Para verificar se o objeto JavaScript possui uma propriedade especificada, utilize o método hasProperty(String):

boolean has = jsObject.hasProperty("<property-name>");
val has = jsObject.hasProperty("<property-name>")

Para obter o valor da propriedade de um objeto JavaScript pelo seu nome, utilize property(String). Por exemplo:

JsObject document = frame.executeJavaScript("document");
document.property("title").ifPresent(title -> {});
val document = frame.executeJavaScript<JsObject>("document")!!
document.property<String>("title").ifPresent { title -> }

O valor de retorno representa java.lang.Object que pode ser definido para o tipo necessário. Veja Conversão de Tipos.

É possível remover uma propriedade utilizando a seguinte abordagem:

boolean success = jsObject.removeProperty("<property-name>");
val success = jsObject.removeProperty("<property-name>")

Funções

Para chamar uma função com o nome e os argumentos necessários, utilize a função call(String methodName, Object... args) method. O exemplo a seguir demonstra como chamar a função document.getElementById() do JavaScript:

JsObject element = document.call("getElementById", "elementId");
val element: JsObject = document.call("getElementById", "elementId")

que é equivalente ao seguinte código em JavaScript:

var element = document.getElementById("demo");

O método lança JsException se ocorrer um erro durante a execução da função.

Encerramento

Os objetos V8 que possuem uma contraparte JsObject não estão sujeitos à coleta de lixo do V8. Por padrão, mantemos estes objetos na memória até a página ser descarregada.

Para otimizar a utilização da memória, você pode ativar a recolha de lixo por objeto:

jsObject.close();
jsObject.close()

O fechamento de JsObject marca o objeto V8 correspondente como coletável, mas não libera o objeto imediatamente. Após a chamada do método close(), as tentativas de utilização do JsObject darão origem à ObjectClosedException.

JsFunctionCallback

A outra maneira de chamar Java a partir de JavaScript é usando JsFunctionCallback.

A ponte JavaScript-Java te permite associar JsFunctionCallback a uma propriedade JavaScript que será tratada como uma função que pode ser invocada no código JavaScript.

Por exemplo, você pode registrar uma função JavaScript associada à instância JsFunctionCallback utilizando o seguinte código:

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]}" })

Agora, em JavaScript, você pode invocar esta função da seguinte forma:

window.sayHello('John');

JsFunction

Desde 7.7, você pode trabalhar com as funções JavaScript diretamente a partir do código Java e passar a referência a uma função do JavaScript para o Java. Por exemplo:

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

Desde 7.17 você pode trabalhar com JavaScript Promises diretamente a partir do código Java. Por exemplo:

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
}

Chamando Java a partir de JavaScript

Quando você passa um java.lang.Object como um valor de propriedade, ou um argumento ao chamar a função JavaScript, o objeto Java será automaticamente envolvido em um objeto JavaScript.

Ela te permite injetar objetos Java no JavaScript e invocar os seus métodos e campos públicos a partir do JavaScript.

Por razões de segurança, apenas os métodos e campos públicos não estáticos anotados com @JsAccessible ou declarados na classe anotada com @JsAccessible podem ser acessados a partir do JavaScript. Os métodos e campos protegidos, privados ou privados de pacote anotados, ou os métodos e campos declarados numa classe com esses modificadores, permanecem inacessíveis a partir do JavaScript.

Para injetar um objeto Java no JavaScript, defina a classe do objeto Java e marque o método público que deve ser acessível a partir do JavaScript com @JsAccessible:

public final class JavaObject {
    @JsAccessible
    public String sayHelloTo(String firstName) {
        return "Hello " + firstName + "!";
    }
}
class JavaObject {
    @JsAccessible
    fun sayHelloTo(firstName: String) = "Hello $firstName!"
}

Injeta uma instância do objeto Java no JavaScript antes de o JavaScript ser executado na página Web carregada:

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()
})

Agora você pode fazer referência ao objeto e chamar o seu método a partir do JavaScript:

window.java.sayHelloTo("John");

Regras de anotação

A anotação @JsAccessible permite expor métodos e campos de um objeto Java injetado ao JavaScript.

Você pode tornar acessíveis apenas tipos, métodos e campos públicos. A lista completa dos casos suportados é a seguinte:

  • Uma classe de nível superior ou uma interface
  • Uma classe estática aninhada ou uma interface
  • Um método não estático de uma classe ou de uma interface
  • Um campo não estático de uma classe

A anotação não pode ser aplicada a tipos, métodos e campos não-públicos. Os métodos e campos públicos de um tipo não-público são considerados não-públicos. Quando se anota um tipo, todos os seus métodos e campos públicos tornam-se acessíveis ao JavaScript. Quando se anota um método ou um campo de um tipo não-anotado, apenas o membro anotado se torna acessível ao JavaScript.

Um método acessível continua a sê-lo quando é substituído numa subclasse. Isto significa que você pode tornar uma interface acessível e passar qualquer uma das suas implementações para o JavaScript: todos os métodos declarados na interface serão acessíveis a partir do JavaScript. Outros métodos e campos declarados na classe de implementação permanecerão inacessíveis a menos que você os marque explicitamente ou a todo o tipo com esta anotação.

Outra forma de tornar um tipo acessível a partir do JavaScript é utilizando JsAccessibleTypes. Isto é particularmente útil quando se quer tornar acessível um dos tipos principais do Java (por exemplo, java.util.List), ou um tipo de uma biblioteca de terceiros que não pode ser tornado acessível usando esta anotação.

Exemplos:

Os métodos e campos anotados de uma classe pública de nível superior são acessíveis:

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

Os métodos e campos anotados de uma classe pública estática aninhada são acessíveis:

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() {}
    }
}

Os métodos e campos não-anotados de uma classe anotada são acessíveis:

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

Os métodos e campos de uma classe anotada de base são acessíveis aos herdeiros:

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() {}
    }
}

Os métodos e campos herdados não são acessíveis se eles ou a classe em que são declarados não estiverem anotados:

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() {}
    }
}

Os métodos sobrescritos da classe são acessíveis:

public final class TopClass {
   public static class BaseNestedClass {
       @JsAccessible
       public void method() {}
   }
   public static class NestedClass extends BaseNestedClass {
       @Override
       public void method() {} // acessível
   }
}
class TopClass {
    open class BaseNestedClass {
        @JsAccessible
        open fun method() {
        }
    }
    class NestedClass : BaseNestedClass() {
        override fun method() {} // acessível
    }
}

Os métodos implementados da interface são acessíveis:

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() {} // acessível
    }
}

Se a assinatura de um método Java acessível tiver um parâmetro numérico primitivo, o número transferido do JavaScript será verificado quanto à possibilidade de conversão para o tipo de parâmetro Java. Se a conversão puder ser efetuada sem perda de dados e não forem encontrados outros métodos sobrecarregados adequados, o método será invocado.

Se existir mais do que um método que possa aceitar os parâmetros passados, o JavaScript lança uma exceção para indicar que a chamada de método solicitada é ambígua e não pode ser executada.

Se não forem encontrados métodos ou campos que correspondam ao nome solicitado, o JavaScript lança uma exceção indicando que o membro solicitado não existe.

Se tanto o método como o campo com o mesmo nome solicitado pelo JavaScript existirem, o JavaScript lança uma exceção para indicar que o membro solicitado é ambíguo e não pode ser acessado.

Conversão Automática de Tipos

A ponte JavaScript-Java fornece a funcionalidade de conversão automática de tipos quando chama um método público do objeto Java injetado a partir do JavaScript.

A biblioteca converte automaticamente o Number JavaScript fornecido para o tipo Java necessário se for possível. Se detectarmos que o número dado não pode ser convertido para, por exemplo, um Java byte sem perda de dados, então a biblioteca lança uma exceção e notifica o JavaScript que não existe nenhum método Java apropriado. Se o valor fornecido puder ser convertido sem perda de dados, a biblioteca o converte e invoca o método Java adequado.

Por exemplo, se você injetar o seguinte objeto Java no JavaScript:

public final class JavaObject {
    @JsAccessible
    public int method(int intValue) {
        return intValue;
    }
}
class JavaObject {
    @JsAccessible
    fun method(intValue: Int) = intValue
}

Depois pode chamá-lo a partir do JavaScript e passar o valor Number do JavaScript que pode ser convertido para e Integer sem perda de dados:

window.javaObject.method(123);

Mas, se você passar um valor Double que não pode ser convertido para um Integer sem perda de dados, você receberá um erro:

window.javaObject.method(3.14); // <- erro

Chamada a Partir de Bibliotecas

Os objetos Java injetados são um tipo especial de objetos. Eles se comportam de forma diferente dos objetos JavaScript normais e não se destinam a ser passados diretamente para as bibliotecas JavaScript.

Antes de as utilizar em bibliotecas JavaScript, recomendamos que as envolva no Proxy. Neste exemplo, criamos um objeto proxy que implementa o acesso de leitura aos membros acessíveis do JS:

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);
    },
    ...
});

Mensagens de Console

O JxBrowser permite receber todas as mensagens de saída enviadas para o console através da função JavaScript console.log(). Você pode escutar as mensagens com os seguintes níveis:

  • DEBUG
  • LOG
  • WARNING
  • ERROR

Para receber uma notificação quando o Console receber uma mensagem, utilize o evento ConsoleMessageReceived. Por exemplo:

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