JavaFX 8 WebEngine: How to get console.log() from javascript to System.out in java?
Asked Answered
P

3

21

I'm using both JavaFX and the javascript engine inside JavaFX WebEngine to develop an application. I'd like to get feedback from javascript for debugging purposes. What happens to the console output inside the WebEngine? Is there any way I can access it, or redirect to System.out in java?

Prisoner answered 24/2, 2015 at 3:26 Comment(0)
K
27

The following code redirects console.log() to JavaBridge.log():

import netscape.javascript.JSObject;

[...]

public class JavaBridge
{
    public void log(String text)
    {
        System.out.println(text);
    }
}

// Maintain a strong reference to prevent garbage collection:
// https://bugs.openjdk.java.net/browse/JDK-8154127
private final JavaBridge bridge = new JavaBridge();

[...]

webEngine.getLoadWorker().stateProperty().addListener((observable, oldValue, newValue) ->
{
    JSObject window = (JSObject) webEngine.executeScript("window");
    window.setMember("java", bridge);
    webEngine.executeScript("console.log = function(message)\n" +
        "{\n" +
        "    java.log(message);\n" +
        "};");
});
Kelm answered 22/3, 2015 at 6:6 Comment(9)
what is "window" here?Capernaum
@Florian can you please help here? I'm getting a null pointer exception if i declare window as JSObjectCapernaum
@naveenbharadwaj you need to post a separate question.Kelm
Doesn't seem to be working. My calls to console.log() isn't being picked up for some reason.Upheld
Not sure if we need to do this every time state changes - perhaps just when newValue == State.SCHEDULED or something? otherwise we are overriding console.log multiple times for no reason, in the same request.Guck
If this silently fails for you like it did for me, one thing to try is initializing bridge in a persistent scope (e.g., as an member variable of an object, instead of a local variable). I think my bridge was garbage collected before it could be called from JavaScript (also see stackoverflow.com/a/41903154 for details about setMember and garbage collection)Belovo
@AndrewHead I fixed the answer. Thank you for pointing his out!Kelm
@Guck State.SCHEDULED doesn't work for me, it fails when I load a second page in the same WebView. However, State.SUCCEEDED works.Toweling
I was stumped since import netscape.javascript.JSObject; is deprecated in Java 9 and import jdk.nashorn.api.scripting.JSObject; is deprecated in Java 15, with no replacements mentioned anywhere - I am using Java 20. Finally discovered that if you add requires jdk.jsobject; to your src/main/java/module-info.java then you can access import netscape.javascript.JSObject;. Not sure if this is kosher but it works.Illsuited
N
13

You can just add message listener to see what's happening in your output. You don't have to inject js bridge with redefinition of functions like console.log for every single loaded page

WebConsoleListener.setDefaultListener((webView, message, lineNumber, sourceId) -> {
    System.out.println(message + "[at " + lineNumber + "]");
});
Nineteen answered 2/3, 2018 at 21:7 Comment(6)
Yes, but you don't get the ability to expand and view the arguments passed to console methods or jump to source like you get from the debugger console.Denigrate
He just asked about getting output from console.log, so it's better alternative to Green Man's answer. Dedicated debugging tools belong to another case and it is not always helping - for example if we want to catch output in our final distributed app.Nineteen
Sadly WebConsoleListener is not available in recent versions of Java.Toweling
@GuillaumeF. package com.sun.javafx.webkit; but it is not event from java, I'm using the lastest javaFX version, it is hereNubile
Avoid! It uses internal API which might be removed in any release.Ciapha
As the comments predicted.... sadly... this stopped working in newer FX versions :(Drongo
O
6

I like to go the other direction. We use log4j so I created a javascript wrapper like the following:

module.exports = {

    levels:[ "ALL", "TRACE", "DEBUG", "INFO", "WARN", "ERROR", "OFF"],

    level:"INFO",

    error:function(msg){
      if(this.isErrorEnabled()){
        console.error(msg)
      }
    },
    warn:function(msg){
      if(this.isWarnEnabled()){
        console.warn(msg)
      }
    },
    info:function(msg){
      if(this.isInfoEnabled()){
        console.log("INFO: "+msg)
      }
    },
    debug:function(msg){
      if(this.isDebugEnabled()){
        console.log("DEBUG: "+msg)
      }
    },
    trace:function(msg){
      if(this.isTraceEnabled()){
        console.log("TRACE: "+msg)
      }
    },

    isErrorEnabled:function(){
      return this.isEnabled("ERROR");
    },
    isWarnEnabled:function(){
      return this.isEnabled("WARN");
    },
    isInfoEnabled:function(){
      return this.isEnabled("INFO");
    },
    isDebugEnabled:function(){
      return this.isEnabled("DEBUG");
    },
    isTraceEnabled:function(){
      return this.isEnabled("TRACE");
    },
    isEnabled:function(statementLevel){
      return this.levels.indexOf(this.level)<=this.levels.indexOf(statementLevel);
    }
  }

Then at the beginning of the javascript I check to see if the log is present and set it:

if(window.log == undefined){
  window.log = require("./utils/log4j-wrapper")
  window.log.level = "INFO"
}

And that way if you set the Log4j logger directly on the engine before you even load the url like:

WebEngine webEngine = webView.getEngine()
JSObject win = (JSObject) webEngine.executeScript("window")
win.setMember("log", log)  //log being the java log4j logger

This way I can get logging in if I am opening directly in a browser or it is being run from a WebView in a JavaFX program. And has the added benefit of having levels to the logging in javascript that match your packages of the WebView controller. Just an alternative for larger javascript views.

Oleaceous answered 7/12, 2015 at 17:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.