Invoking function on Java subclass in Nashorn - javascript

I have a JavaScript script that looks something like this:
function run(database) {
var result = database.query("query", "some resource name");
//operations on result
return result;
}
and I have Java code that executes the script that is something like this:
public Object execute(String script, Database database) {
NashornScriptEngineFactory nsef = new NashornScriptEngineFactory();
ScriptEngine engine = nsef.getScriptEngine();
try {
engine.eval(script);
Invocable invocable = (Invocable) engine;
return invocable.invokeFunction("run", database);
} catch(ScriptException e) {
throw new RuntimeException(e);
}
}
Database is an interface which contains several method definitions, but does not contain the query method. I am calling execute with an implementation of Database, call it DatabaseImpl, that does have the query method. This will be polymorphic, and the script is expected to know what methods are available on the Database instance passed to it. I decided against using generics with this since they are erased at runtime and so the JavaScript has no way of using them anyway, so it's up to the script writer to get the types right.
However, when I run this code, I get the following exception:
javax.script.ScriptException: TypeError: database.query is not a function in <eval> at line number 25
Basically, the gist is, I have an object which implements an interface, and call a method that the particular instance implements, but is not part of the interface definition. My impression is that this should still work, but it does not. It doesn't make much sense to me that I would need to subcast within the script to have access to the query method (is that even possible?), so why am I getting this error? Is it because the method isn't available from the interface definition? Is there a workaround?
Thanks.

This is the main class:
package so;
import java.io.InputStreamReader;
import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
public class Nashorn {
public static void main(String[] args) {
try (InputStreamReader in = resource()) {
ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn");
engine.eval(in);
Invocable invocable = (Invocable) engine;
Database database = new DatabaseImpl();
Object x = invocable.invokeFunction("run", database);
System.out.println(x);
} catch (Exception e) {
e.printStackTrace();
}
}
private static InputStreamReader resource() throws Exception {
return new InputStreamReader(Nashorn.class.getResourceAsStream("db.js"), "utf-8");
}
}
Interface and implementation
package so;
public interface Database {
void connect();
}
package so;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class DatabaseImpl implements Database {
#Override
public void connect() {
System.out.println("Connecting");
}
public List<?> query(String ... stmt){
List<String> lst = new ArrayList<>();
lst.addAll(Arrays.asList(stmt));
lst.addAll(Arrays.asList("A","B","C"));
return lst;
}
}
The javascript file (so/db.js)
function run(database) {
var result = database.query("query", "some resource name");
//operations on result
return result;
}
Running results in:
[query, some resource name, A, B, C]
It basically works.

Related

Call Wicket 6 Code from Javascript and return value

I have managed to call my Wicket 6 Java code from Javascript using option A in this example: https://stackoverflow.com/a/42612027/1047418
However, I have not been able to find examples for returning data from the Java side back to JavaScript (the generated JavaScript callback function does not even include a return statement). How can this be achieved?
Edit: I am not trying to set an attribute in Java and as I've already explained, calling Wicket from JavaScript is not the problem here. I am trying to return a JSON object from Wicket back to the browser as a result of an Ajax request.
Edit2: Following martin-g's examples I cobbled up this working example...
Java
public class MyAjaxBehaviour extends AbstractDefaultAjaxBehavior {
#Override
protected void onComponentTag(ComponentTag tag) {
super.onComponentTag(tag);
tag.put("aprachatcallbackurl", getCallbackUrl());
}
#Override
protected void updateAjaxAttributes(AjaxRequestAttributes attributes) {
super.updateAjaxAttributes(attributes);
attributes.setDataType("json");
attributes.setWicketAjaxResponse(false);
}
#Override
protected void respond(AjaxRequestTarget target) {
getComponent().getRequestCycle().replaceAllRequestHandlers(
new TextRequestHandler("application/json", "UTF-8", "{...JSON GOES HERE...}));
}
}
JavaScript
var mySuccessCallback = function(param1, param2, data, statusText) {
// Data contains the parsed JSON object from MyAjaxBehaviour.respond(...)
...
}
var myFailureCallback = function() {
...
}
Wicket.Ajax.get({
"u": callbackUrl,
"dt": "json",
"wr": false,
"sh": [mySuccessCallback],
"fh": [myFailureCallback]
});
Main problem as that the Wicket 7 Reference incorrectly instructs to use "wr" instead of "dt" in the JavaScript call. :)
I think you can do it in a simpler way!
Wicket Ajax API is just: Wicket.Ajax.ajax({...}). All you need to prepare at the server side is to save the callback url, e.g. by saving it globally in the window object or in HTML element's attributes (data-the-url).
public class CallFromJavascriptBehavior extends AbstractDefaultAjaxBehavior {
#Override
protected void respond(AjaxRequestTarget target) {
final StringValue parameterValue = RequestCycle.get().getRequest().getQueryParameters().getParameterValue("yourName");
System.out.println(String.format("Hello %s", parameterValue.toString()));
// write anything to the WebResponse and then consume it in the JS success handler. See below
}
#Override
public void onComponenntTag(ComponenntTag tag, Component component) {
super.onComponenntTag(tag, component);
tag.put("data-the-url", getCallbackUrl());
}
}
Then in your JS code you can do:
var callbackUrl = jQuery("#theElementId").data("the-url");
Wicket.Ajax.get({"u": callbackUrl, "sh":[successHandler], "fh": [failureHandler] });
Where successHandler and failureHandler are JS functions defined inline (e.g. function(...) {}) or elsewhere.
More documentation you can find at:
https://ci.apache.org/projects/wicket/guide/7.x/single.html#_ajax_request_attributes_and_call_listeners
A blog article with an complete example at http://wicketinaction.com/2012/07/wicket-6-javascript-improvements/
You can just write a Resource and mount it, and get it with your favorite Ajax-approach.
For example:
public class MyResource extends AbstractResource
#Override
protected ResourceResponse newResourceResponse( Attributes attributes )
{
ResourceResponse resourceResponse = new ResourceResponse();
resourceResponse.setContentType( "text/json" );
resourceResponse.setTextEncoding( "utf-8" );
HttpServletRequest request = (HttpServletRequest) attributes.getRequest().getContainerRequest();
try
{
this.json = IOUtils.toString( request.getInputStream() );
}
catch ( IOException e )
{
e.printStackTrace();
}
resourceResponse.setWriteCallback( new WriteCallback()
{
#Override
public void writeData( Attributes attributes ) throws IOException
{
OutputStream outputStream = attributes.getResponse().getOutputStream();
Writer writer = new OutputStreamWriter( outputStream );
writer.write( MyResource.this.json );
writer.close();
}
} );
return resourceResponse;
}
(Copied from my other answer here https://stackoverflow.com/a/17876029/461499)
And see here for mounting it:
https://dzone.com/articles/how-implement-rss-feeds-custom

Jdk nashorn api ScriptEngine returning undefined

I am trying to run a simple javascript function using jdk nashorn script engine. There I am returning one of the passed variable to the function just for simplicity to check if nashorn api working fine or not. After script engine runs the function I get undefined as result.
import jdk.nashorn.api.scripting.ScriptObjectMirror;
import javax.script.ScriptException;
import javax.script.Bindings;
import javax.script.ScriptEngine;
import jdk.nashorn.api.scripting.NashornScriptEngineFactory;
public class JDKNashornScriptRunner {
protected static final ScriptEngine scriptEngine = new NashornScriptEngineFactory().getScriptEngine();
protected final Bindings bindings;
public JDKNashornScriptRunner(String script)
{
bindings = scriptEngine.createBindings();
try
{
scriptEngine.eval(script, bindings);
}
catch( ScriptException e )
{
throw new RuntimeException("Exception while compiling script", e);
}
}
public Object runScript( String v1, String v2, String v3, String v4)
throws NoSuchMethodException, ScriptException
{
bindings.clear();
bindings.put("v1", v1);
bindings.put("v2", v2);
bindings.put("v3", v3);
bindings.put("v4", v4);
return ((ScriptObjectMirror) bindings.get("myFunction")).call(null);
}
}
Main program here -
public class RunMyFunctionScript {
public static void main(String args[])
{
String runScript = "function myFunction(v1, v2, v3, v4) {return v3;}";
JDKNashornScriptRunner scriptRunner = new JDKNashornScriptRunner(runScript);
Object result = scriptRunner.runScript("a","b","c","d"); //Here I am getting undefined as value
String v3Value = String.valueOf(result);
System.out.println(v3Value);
}
}
So the problem here is when scriptEngine is trying to run the script using below given line, I am getting undefined (it should give me c as value)
((ScriptObjectMirror) bindings.get("myFunction")).call(null);
Because you're not passing any argument to your JavaScript function. runScript should be:
public Object runScript( String v1, String v2, String v3, String v4)
throws NoSuchMethodException, ScriptException
{
return ((JSObject) bindings.get("myFunction")).call(null, v1, v2, v3, v4);
}

AngularJS 400 Bad request

I'm trying to implement some post functionality in my app.
I have got the following post method:
restrictLoginAttemptsFromSingleIp: function (id, userId) {
var serviceUri = baseServicesUrlService.getBaseServicesUrl() + "/employee-service/restrict-single-ip";
return $http.post(serviceUri, {restrictLoginAttemptIp: {loginAttemptIds: [id]}, dataOwnerId: userId});
}
My server side is using RESTEasy 3.0.4 with Hibernate validation:
#POST
#Path("/restrict-single-ip")
public Response RestrictSingleIp(#Valid RestrictLoginAttemptIpRequest requestData, #Context HttpRequest request){
return Response.status(200).build();
}
The RestrictLoginAttemptIpRequest class inherits one field (dataOwnerId) of type Long from PostBase:
public class RestrictLoginAttemptIpRequest extends PostBase {
private RestrictLoginAttemptIp restrictLoginAttemptIp;
public RestrictLoginAttemptIp getRestrictLoginAttemptIp() {
return restrictLoginAttemptIp;
}
public void setRestrictLoginAttemptIp(RestrictLoginAttemptIp restrictLoginAttemptIp) {
this.restrictLoginAttemptIp = restrictLoginAttemptIp;
}
}
The RestrictLoginAttemptIp class:
package blah;
import org.hibernate.validator.constraints.NotEmpty;
import java.util.List;
public class RestrictLoginAttemptIp {
#NotEmpty(message = "blah")
private List<Long> loginAttemptIds;
public List<Long> getLoginAttemptIds() {
return loginAttemptIds;
}
public void setLoginAttemptIds(List<Long> loginAttemptIds) {
this.loginAttemptIds = loginAttemptIds;
}
}
I get the following data string from the POST request which seems to be ok:
{restrictLoginAttemptIp={loginAttemptIds=[328]}, dataOwnerId=8}
Can someone please explain me why I get an 400 Bad request error when I invoke that function?
Is this because of Long datatypes? Should I somehow mark them in Javascript to be Longs?
Ok after 4 hours I figured out the problem.
The case is, that I'm reading the POST data (solving permission questions) in a security interceptor. Reading POST data in RESTEasy is a little bit tricky. To create a LinkedHashMap I use Apache IOUtils (https://commons.apache.org/proper/commons-io/javadocs/api-release/org/apache/commons/io/IOUtils.html) like it is figured out in the next code snippet
String result = IOUtils.toString(requestContext.getEntityStream());
ObjectMapper mapper = new ObjectMapper();
Object obj = mapper.readValue(result, Object.class);
I looked up in my AngularJS interceptor (used for example for putting something in the header of every request) and figured out, that the server cannot read the input stream: java.io.ioexception no content to map to object due to end of input.
At the end the problem was, that after I once read the EntityStream of the ContainerRequestContext it became empty. The solution was to repopulate it after reading POST data. Something like this:
private LinkedHashMap getPostData(ContainerRequestContext requestContext) {
Object obj = null;
try {
String result = IOUtils.toString(requestContext.getEntityStream());
ObjectMapper mapper = new ObjectMapper();
obj = mapper.readValue(result, Object.class);
//IMPORTANT: After you can get the entity stream only once. After reading the entity stream is empty
//so the JSON parser cannot convert EMPTY entity stream into any object. To avoid strange errors (like 400 Bad Request)
//you have to convert the string back to input stream and rewrite the empty entity stream.
InputStream stream = IOUtils.toInputStream(result);
requestContext.setEntityStream(stream);
System.out.println(obj);
} catch (IOException e) {
e.printStackTrace();
}
return (LinkedHashMap) obj;
}
P. S. ObjectMapper comes from Jackson

Asynchronous JSON String loading JavaScript

I'm using the d3 force directed graph to display some data I get from an API. Before I can display it, it runs through a java class, which does write it into the right json format.
Since the programm runs in a JavaFX WebView I have a bridge class, that does have a getter method I can call from the JavaScript.
In my Main class I create a WebView and assign the bridge to it. I initialize my JSON translator and pass the bridge to it.
#Override
public void start(Stage stage) {
try {
new JsonTranslator(individual, depth, bridge);
Scene scene = createScene();
[...]
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
launch(args);
}
private Scene createScene() {
[...]
try {
JSObject jsobj = (JSObject) webEngine.executeScript("window");
jsobj.setMember("java", bridge);
} catch (Exception e) {
e.printStackTrace();
}
[...]
}
In my JSONTranslator class I write the json and pass it to the bridge
private void writeFile() {
try {
bridge.setJSONObject(obj.toJSONString());
FileWriter file = new FileWriter(
"C://path/to/some/file.json"
file.write(obj.toJSONString());
file.flush();
file.close();
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("Object:" + obj);
}
I also write it to a file. The data gets printed as expected. Now in my bridge the data is available throughout the getter / setter. In JSToFXBridge.java:
public String getJSONObject(){
System.out.println("get request: " + json);
return json;
}
public void setJSONObject(String string){
this.json = string;
}
Now I call it from my JavaScript
[...]
var draw = function(json, callback) {
[...]
callback.call(data);
};
var data = java.getJSONObject();
draw(data);
However it does print get request: -my json data- on the console, the json string is compleatly fine. If I copy & paste it from the console to be like this var data = -my json data- in the code it works. Only to asign it directly from the method won't work. I can't figure out why since I try to load it asynchronously. Based on this tutorial. Do I make a mistake in laoding the string? Or is it even a wrong way to do so?
Good answer / tutorial to asynchronous JavaScript callbacks can be found here. Solution, which created a new problem [ solved as well ], provided here.
In general think of this pattern:
function addOne(thenRunThisFunction) {
waitAMinuteAsync(function waitedAMinute() {
thenRunThisFunction()
})
}
addOne(function thisGetsRunAfterAddOneFinishes(){})
Explains it very well

Android Phonegap: Notify javascript when an AsyncTask is finished

in my app, when user click on a button in webview, a phonegap plugin will be called to trigger an asynctask to download file from internet. Now i want to send a signal back to javascript part when the asynctask is finished. But i don't know how to do it, because my plugin had already send something back before the asynctask is finished. Does anyone know how i can notify my javascript part without plugin in Phonegap?
I also asked this question in Phonegap Google Group, here is response of Simon Mac Donald. It works perfectly for me:
You can handle this situation by using the Plugin API quite easily. It
is implemented in the core API items Connection and Battery. What you
need to do is:
1) In your execute() method of your plugin save the callbackId you get.
2) Return a NO_RESULT plugin result and set keep callback id to true.
PluginResult pluginResult = new PluginResult(PluginResult.Status.NO_RESULT);
pluginResult.setKeepCallback(true);
return pluginResult;
3) When you async java method finishes return another plugin result like this:
PluginResult result = new PluginResult(PluginResult.Status.OK, data);
result.setKeepCallback(false);
this.success(result, this.myCallbackId);
As I said, you can look at the code in GitHub to see how we are using this for Connection and Battery.
This is how I solve problems like your.
1) Create and associate a JavascriptInterface to your WebView. A JavascriptInterface is simply a class inside which you can declare some Java method you want to use from JS.
public class JSInterface() {
private final CountDownLatch latch = new CountDownLatch(1);
public void waitForProceed() {
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void canProceed() {
latch.countDown();
}
}
2) In your AsyncTask, at the end of onPostExecute() method, you have to call the canProceed() method to notify to JSInterface that it can exit from waitForProceed() method.
public class MyAsyncTask extends AsyncTask<.......> {
private JSInterface jsi;
... // other class property
public MyAsyncTask(JSInterface jsi) {
...
//do what you want with other class property
this.jsi = jsi;
}
#Override
public ... doInBackground(...) {
...
//do something
}
#Override
public void onPostExecute(...) {
...
//do something
jsi.canProceed();
}
}
3) In your Activity you have to associate the JSInterface object to your WebView:
WebView mWebView;
...
mWebView.getSettings().setJavaScriptEnabled(true);
mWebView.addJavascriptInterface(new JSInterface(), "JSIface");
4) Finally, in JS, you can call AsyncTask (I don't know how you call it, but I guess you use somthing like a JSInterface) and after call waitForProceed() method:
startAsyncTask(); //somehow
JSIface.waitForProceed();
I hope it solves your problem ;)
Here is a detailed example:
Lets create some interface that should be called when AsyncTask will finish the stuff, a.e when onPostExecute called.
In my case we fetch some JSONArray data
MyTaskListenerItf.java
public interface GroupTaskListenerItf {
public void onTaskDone(JSONArray groupArray);
}
The AsyncTask template of looks like:
MyBuildTask.java
public class MyBuildTask extends AsyncTask<Void, Void, SomeData>{
private MyTaskListenerItf mTl = null;
public MyBuildTask(Context context, MyTaskListenerItf tl) {
super();
this.mContext = context;
this.mTl = tl;
}
#Override
protected SomeData doInBackground(Void... params) {
/* ... */
}
#Override
protected void onPostExecute(WmTransferItem transferItem) {
// ...
if(this.mTl != null){
JSONArray data = new JSONArray("");
this.mTl.onTaskDone(data);
}
// ..
}
}
So now our CordovaPlugin class should look like:
MyCordovaPlugin.java
public class MyCordovaPlugin extends CordovaPlugin implements GroupTaskListenerItf {
// we need this callback when Task will finish
private CallbackContext mMyCallbackContext = null;
#Override
public boolean execute(String action, JSONArray args,CallbackContext callbackContext) throws JSONException {
if("runMe".equals(action)){
final GroupTaskListenerItf gt = this;
mMyCallbackContext = callbackContext;
// pass our 'GroupTaskListenerItf' interface to async class
MyBuildTask task = new MyBuildTask(cordova.getActivity(), gt);
task.execute();
PluginResult pluginResult = new PluginResult(PluginResult.Status.NO_RESULT);
pluginResult.setKeepCallback(true);
callbackContext.sendPluginResult(pluginResult);
}
else{
this.cordova.getThreadPool().execute( new Runnable() {
public void run() {
// BTW, here you might run something else out of UI Thread
}
});
}
}
/* ... */
#Override
public void onTaskDone(JSONArray data) {
if (this.mGroupCallbackContext != null) {
PluginResult result = new PluginResult(PluginResult.Status.OK, data);
result.setKeepCallback(false);
this.mMyCallbackContext.sendPluginResult(result);
}
}
That's all.
Hope it will help to someone.

Categories

Resources