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?
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" +
"};");
});
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 + "]");
});
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.
Related
I'm trying to update an HTML5 table in real-time with some data from the database. Here is my code:
HTML page:
<script type="text/javascript">
//check for browser support
if(typeof(EventSource)!=="undefined") {
//create an object, passing it the name and location of the server side script
var eSource = new EventSource("[some address]/api/sse");
//detect message receipt
eSource.onmessage = function(event) {
//write the received data to the page
document.getElementById("placeholder").innerHTML=table;
};
}
else {
[erro message]
}
</script>
And my Java Restful service:
#Path("/sse")
public class SSEResource {
#Context
private UriInfo context;
public SSEResource() {
}
#GET
#Produces(SseFeature.SERVER_SENT_EVENTS)
public String getServerSentEvents() throws Exception {
SomeObject o = new SomeObject();
final String myString = o.someQuery().getEntity().toString();
return "data: " + myString + "\n\n";
}
}
This someQuery() method queries from database and returns what I want to put on my table. Everythings looks great. But I want to know if it's right or wrong, because if I put some log on someQuery() method, I see that every 3 seconds the query is executed. This may cause heavy duty, right? Is this normal or is my code wrong?
When developing Cordova plugins, all of the tutorials I have found go something like this:
File: AwesomePlugin.js
var AwesomePlugin = {
kungfuGripAction = function(target, successCallback, failureCallback) {
return cordova.exec(
successCallback,
failureCallback,
'AwesomePluginClass',
'kungfuGripAction',
[target]
);
}
};
module.exports = AwesomePlugin;
File: AwesomePluginClass.java
#Override
public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
if (ACTION_KUNGFU_GRIP.equals(action)) {
JSONObject target = args.getJSONObject(0);
if (gripTarget(target)) {
callbackContext.success("Target successfully gripped.");
return true;
} else {
callbackContext.error("Could not grip target.");
return false;
}
}
Log.d(LOG_TAG, "INVALID ACTION! " + action);
callbackContext.error("Invalid action: " + action);
return false;
}
File: clientCode.js
AwesomePlugin.kungfuGripAction(cobraEnemy, function(ok) { }, function(err) { });
In the above code, the callbacks can only be called once and are then disposed. If you attempt to call the .success() or .error() method of the callback context object, it will not work and you will get a log message:
Attempted to send a second callback for ID: AwesomePlugin2982699494<BR>W/CordovaPlugin(976) Result was: "Target successfully gripped."
It seems like it is not possible to write a method with a callback that can be called repeatedly seeing as .success() and .error() are the only documented ways to invoke a callback from within native plugin code. While this is mostly what we want, there are times when we want to have the plugin execute a callback repeatedly. For example:
AwesomePlugin.kungfuGripAction(cobraEnemy, function(ok) {
// After successful grip, punch repeatedly and update life meter.
AwesomePlugin.punchRepeatedly(cobraEnemy, function(hits) {
updateLifeMeter(cobraEnemy, hits);
}, function(err) { });
}, function(err) { });
AwesomePlugin.punchRepeatedly() above will execute repeatedly (maybe in a separate thread) and call function(hits) with each successful execution. If implemented in the de-facto way (using single-use callbacks), you have to either use a loop (which is bad as it is non-async) or tail-call AwesomePlugin.punchRepeatedly() in the callback (error-prone).
What would be the correct way to implement punchRepeatedly() in native code so that it is able register the callback once and then execute it repeatedly?
I think, you can use a pluginResult with the keepCallback property set to true.
PluginResult result = new PluginResult(PluginResult.Status.OK, "YOUR_MESSAGE");
// PluginResult result = new PluginResult(PluginResult.Status.ERROR, "YOUR_ERROR_MESSAGE");
result.setKeepCallback(true);
callbackContext.sendPluginResult(result);
You should be able to invoke the callback several times this way.
In jonas's answer, every time you call sendPluginResult you have to send the same value. So I changed the PluginResult class to add a method like this:
public void setStrMessage(String strMessage){
this.strMessage = strMessage;
}
This way, I can set the message I want to send to the JavaScript side.
Are there any ways to debug javascript and html that is executed within a Javafx WebView? Something similar to Firebug or Chrome's developer console?
I have an application that renders fine in Firefox and Chrome, but does not render correctly inside a WebView. It really could be any number of things, but without some debugging tools I don't know how to track down the root cause.
Thanks.
Here is some Java code to make use of Firebug Lite in a JavaFX WebView without modifying the html of the target page.
webView.getEngine().executeScript("if (!document.getElementById('FirebugLite')){E = document['createElement' + 'NS'] && document.documentElement.namespaceURI;E = E ? document['createElement' + 'NS'](E, 'script') : document['createElement']('script');E['setAttribute']('id', 'FirebugLite');E['setAttribute']('src', 'https://getfirebug.com/' + 'firebug-lite.js' + '#startOpened');E['setAttribute']('FirebugLite', '4');(document['getElementsByTagName']('head')[0] || document['getElementsByTagName']('body')[0]).appendChild(E);E = new Image;E['setAttribute']('src', 'https://getfirebug.com/' + '#startOpened');}");
You can trigger the code using a JavaFX Button or any other mechanism you wish.
I am debugging JavaFx WebView with chrome DevTools and safari Web Inspector.
I created minimal project to help people debug with DevTools. Get it on GitHub. You can find there:
runnable javaFXWebKitDebugger.jar
source code of created javaFXWebKitDebugger.jar
The sample opens WebView and enables WebSocket Servlet. When you run javaFXWebKitDebugger.jar open Chrome browser and load: dev tools url
You can try Firebug Lite, which can be incorporated into any web-browser. See http://www.makeuseof.com/tag/install-firebug-for-browsers-other-than-firefox/
Maybe a bit late to answer, but I think this way is quite simple.
add javascript listener in java
Java :
webengine.getLoadWorker().stateProperty().addListener(new ChangeListener<State>() {
#Override
public void changed(ObservableValue<? extends State> observable,
State oldValue, State newValue) {
JSObject jsobj = (JSObject) webengine.executeScript("window");
jsobj.setMember("java", new JSListener());
}
});
Then create the JS listener class in java.
JSListener java:
public class JSListener {
public void log(String text){
System.out.println(text);
}
}
add javascript in html file
Javascript:
var javaReady = function(callback){
if(typeof callback =='function'){
if(typeof java !='undefined'){
callback();
} else {
var javaTimeout = 0;
var readycall = setInterval(function(){
javaTimeout++;
if(typeof java !='undefined' || javaTimeout > 1000){
try{
callback();
} catch(s){};
clearInterval(readycall);
}
},1);
}
}
};
var errorlistener = function(msg, url, line){
javaReady(function(){
java.log(msg +", url: "+url+ ", line:" + line);
});
};
//overide onerror
var onerror = errorlistener;
If you want to load html from the outside, you can not to change it, you can use code like this.
var testsss = window.open("http://testError.com");
testsss.onerror = errorlistener;
but first you need to add setCreatePopupHandler in java, to make it you can see here: webview not opening the popup window in javafx
How do we call javascript from Android? I have this javascript library which I would like to use, I want to call the javascript function and pass the result value to the android java code. Haven't found the answer from now. i managed to call android code from javascript, but I want the other way around.
There is a hack:
Bind some Java object so that it can be called from Javascript with WebView:
addJavascriptInterface(javaObjectCallback, "JavaCallback")
Force execute javascript within an existing page by
WebView.loadUrl("javascript: var result = window.YourJSLibrary.callSomeFunction();
window.JavaCallback.returnResult(result)");
(in this case your java class JavaObjectCallback should have a method returnResult(..))
Note: this is a security risk - any JS code in this web page could access/call your binded Java object. Best to pass some one-time cookies to loadUrl() and pass them back your Java object to check that it's your code making the call.
You can use Rhino library to execute JavaScript without WebView.
Download Rhino first, unzip it, put the js.jar file under libs folder. It is very small, so you don't need to worry your apk file will be ridiculously large because of this one external jar.
Here is some simple code to execute JavaScript code.
Object[] params = new Object[] { "javaScriptParam" };
// Every Rhino VM begins with the enter()
// This Context is not Android's Context
Context rhino = Context.enter();
// Turn off optimization to make Rhino Android compatible
rhino.setOptimizationLevel(-1);
try {
Scriptable scope = rhino.initStandardObjects();
// Note the forth argument is 1, which means the JavaScript source has
// been compressed to only one line using something like YUI
rhino.evaluateString(scope, javaScriptCode, "JavaScript", 1, null);
// Get the functionName defined in JavaScriptCode
Object obj = scope.get(functionNameInJavaScriptCode, scope);
if (obj instanceof Function) {
Function jsFunction = (Function) obj;
// Call the function with params
Object jsResult = jsFunction.call(rhino, scope, scope, params);
// Parse the jsResult object to a String
String result = Context.toString(jsResult);
}
} finally {
Context.exit();
}
You can see more details at my post.
In order to match the method calls of the iOS WebviewJavascriptBridge ( https://github.com/marcuswestin/WebViewJavascriptBridge ), I made some proxy for the calls of register_handle and call_handle. Please note I am not a Javascript-guru therefore there is probably a better solution.
javascriptBridge = (function() {
var handlers = {};
return {
init: function () {
},
getHandlers : function() {
return handlers;
},
callHandler : function(name, param) {
if(param !== null && param !== undefined) {
JSInterface[name](param);
} else {
JSInterface[name]();
}
},
registerHandler : function(name, method) {
if(handlers === undefined) {
handlers = {};
}
if(handlers[name] === undefined) {
handlers[name] = method;
}
}
};
}());
This way you can send from Javascript to Java calls that can have a String parameter
javascriptBridge.callHandler("login", JSON.stringify(jsonObj));
calls down to
#JavascriptInterface
public void login(String credentialsJSON)
{
Log.d(getClass().getName(), "Login: " + credentialsJSON);
new Thread(new Runnable() {
public void run() {
Gson gson = new Gson();
LoginObject credentials = gson.fromJson(credentialsJSON, LoginObject.class);
SingletonBus.INSTANCE.getBus().post(new Events.Login.LoginEvent(credentials));
}
}).start();
}
and you can call back to Javascript with
javascriptBridge.registerHandler('successfullAuthentication', function () {
alert('hello');
})
and
private Handler webViewHandler = new Handler(Looper.myLooper());
webViewHandler.post(
new Runnable()
{
public void run()
{
webView.loadUrl("javascript: javascriptBridge.getHandlers().successfullAuthentication();"
}
}
);
If you need to pass a parameter, serialize to JSON string then call StringEscapeUtils.escapeEcmaScript(json), otherwise you get unexpected identifier: source (1) error.
A bit tacky and hacky, but it works. You just have to remove the following.
connectWebViewJavascriptBridge(function(bridge) {
}
EDIT:
in order to change the global variable to an actual property, I changed the above code to the following:
(function(root) {
root.bridge = (function() {
var handlers = {};
return {
init: function () {
},
getHandlers : function() {
return handlers;
},
callHandler : function(name, param) {
if(param !== null && param !== undefined) {
Android[name](param);
} else {
Android[name]();
}
},
registerHandler : function(name, method) {
if(handlers === undefined) {
handlers = {};
}
if(handlers[name] === undefined) {
handlers[name] = method;
}
}
};
}());
})(this);
I got the idea from Javascript global module or global variable .
For a full implementation of JavaScript that doesn't require using a slow WebView, please see AndroidJSCore, which is a full port of Webkit's JavaScriptCore for Android.
UPDATE 2018: AndroidJSCore is deprecated. However, its successor, LiquidCore has all of the same functionality and more.
Calling functions from Android is very simple:
JSContext context = new JSContext();
String script =
"function factorial(x) { var f = 1; for(; x > 1; x--) f *= x; return f; }\n" +
"var fact_a = factorial(a);\n";
context.evaluateScript("var a = 10;");
context.evaluateScript(script);
JSValue fact_a = context.property("fact_a");
System.out.println(df.format(fact_a.toNumber())); // 3628800.0
I have a web application that uses java applet defined in a <applet> tag. Is it possible to add a javascript event that is triggered after the applet is fully loaded? This is some initialization javascript that is dependent on that the applet is fully loaded and valid.
javascript invoking is rather simple:
Your init() method can include the jsObject declaration and javascript invoking:
#Override
public void init() {
// some code
JSObject jsObject = JSObject.getWindow(this);
jsObject.eval("your javascript");
}
If you don't have source code control over the applet, you can poll for the applet to be loaded before calling methods on it. Here is a utility function I wrote that does just that:
/* Attempt to load the applet up to "X" times with a delay. If it succeeds, then execute the callback function. */
function WaitForAppletLoad(applet_id, attempts, delay, onSuccessCallback, onFailCallback) {
//Test
var to = typeof (document.getElementById(applet_id));
if (to == 'function' || to == 'object') {
onSuccessCallback(); //Go do it.
return true;
} else {
if (attempts == 0) {
onFailCallback();
return false;
} else {
//Put it back in the hopper.
setTimeout(function () {
WaitForAppletLoad(applet_id, --attempts, delay, onSuccessCallback, onFailCallback);
}, delay);
}
}
}
Call it like this:
WaitForAppletLoad("fileapplet", 10, 2000, function () {
BuildTree("c:/");
}, function () {
alert("Sorry, unable to load the local file browser.");
});
You have an initializer function (i think it is run) in java applet. From there you can call a javascript in the web page after initialization work.
To work you must add the MAYSCRIPT attribute to your applet definition
<applet id="someId" code="JavaApplet.class" codebase="/foo" archive="Applet.jar" MAYSCRIPT>
</applet>
Code example to invoke a JavaScript:
public String invokeJavaScript(Object caller, String cmd) throws TiNT4Exception {
printDebug(2, "Start JavaScript >>" + cmd + "<<");
try {
// declare variables
Method getw = null;
Method eval = null;
Object jswin = null;
// create new instance of class netscape.javascript.JSObject
Class c = Class.forName("netscape.javascript.JSObject"); // , true, this.getClass().getClassLoader()); // does it in IE too
// evaluate methods
Method ms[] = c.getMethods();
for (int i = 0; i < ms.length; i ++) {
if (ms[i].getName().compareTo("getWindow") == 0) { getw = ms[i]; }
else if (ms[i].getName().compareTo("eval") == 0) { eval = ms[i]; }
} // for every method
printDebug(3, "start invokings");
Object a[] = new Object[1];
a[0] = caller;
jswin = getw.invoke(c, a);
a[0] = cmd;
Object result = eval.invoke(jswin, a);
if (result == null) {
printDebug(3, "no return value from invokeJavaScript");
return "";
}
if (result instanceof String) {
return (String)result;
} else {
return result.toString();
}
} catch (InvocationTargetException ite) {
throw new TiNT4Exception(ite.getTargetException() + "");
} catch (Exception e) {
throw new TiNT4Exception(e + "");
}
} // invokeJavaScript
You can use the param tag to pass the name of a JS function into your applet:
<applet id="myapplet" code="JavaApplet.class" codebase="/foo"
archive="Applet.jar" MAYSCRIPT>
<param name="applet_ready_callback" value="myJSfunction"/>
</applet>
In your applet, get the value of the param and call the function when ready:
#Override
public void init() {
String jsCallbackName = getParameter("applet_ready_callback");
JSObject jsObject = JSObject.getWindow(this);
jsObject.eval(jsCallbackName + "()");
}
I used another way to call a JavaScript function from an applet.
try {
getAppletContext().showDocument(new URL("javascript:appletLoaded()"));
} catch (MalformedURLException e) {
System.err.println("Failed to call JavaScript function appletLoaded()");
}
...must be called in the applet class which extends Applet or JApplet. I called the JavaScript function at the end of my start() method.
It is possible with Java 7 SE. You can register onLoad() event or just poll status, see Handling Initialization Status With Event Handlers for an example.
In order to use this functionality, you should deploy the applet with java_status_events parameter set to true.
The article Applet Status and Event Handlers outlines the status and events:
Status
LOADING = 1 - Applet is loading
READY = 2 - Applet has loaded completely and is ready to receive JavaScript calls
ERROR = 3 - Error while loading applet
Events
onLoad: Occurs when applet status is READY. Applet has finished loading and is ready to receive JavaScript calls.
onStop: Occurs when applet has stopped.
onError: Occurs when applet status is ERROR. An error has occurred while loading the applet.
You can register or determine an event handler in the JavaScript code of a web page as shown in the following code snippets.
// use an anonymous function
applet.onLoad(function() {
//event handler for ready state
}
// use a regular function
function onLoadHandler() {
// event handler for ready state
}
// Use method form
applet.onLoad(onLoadHandler);
// Use attribute form
applet.onLoad = onLoadHandler;