Can't call hasOwnProperty on Java List with Nashorn - java

I've got an application that executes a lot of javascript server side and I'm trying to convert from Rhino to Nashorn but I am running into trouble with my scripts. Using Rhino I would always convert whatever arguments I had for a function into a JSON string but that's really slow. With Nashorn I'm trying to just pass in the arguments as Java objects but they don't seem to inherit functions from Javascript's Object type. Here is a sample method that illustrates my problem where hasOwnProperty is not available on my array:
public String printArrayValues() throws ScriptException, NoSuchMethodException {
String script =
"function printArrayValues(objArray) {\n" +
" var result = '';\n" +
" for(var obj in objArray) {\n" +
" if(objArray.hasOwnProperty(obj)) {\n" +
" result = result + ' ' + objArray[obj];\n" +
" }\n" +
" }\n" +
" return result;\n" +
"}";
List<String> data = Arrays.asList(new String[]{ "one", "two", "three"});
ScriptEngine scriptEngine = new NashornScriptEngineFactory().getScriptEngine();
scriptEngine.eval(script);
String result = (String) ((Invocable) scriptEngine).invokeFunction("printArrayValues", data);
}
Here the call to invokeFunction throws an exception:
javax.script.ScriptException: TypeError: [one, two, three] has no such function "hasOwnProperty" in <eval> at line number 4
If I call the same function in a browser, I get what I would expect:
> printArrayValues(["one", "two", "three"]);
> " one two three"
Is there some way I can accomplish this so I really can use these Java objects without turning them into a JSON string and then eval'ing it into a Javascript object?

You can't use Java arrays in this way. Java arrays are "hardwired" objects. Unlike ordinary objects, they don't have methods and they support the [] operator, which objects can't.
This Article about Nashorn at Oracle explains that you need to use the Java.to and Java.from methods in your javascript in order to change a Java array to a Javascript array.

use Java.from() to transform the Java List to Javascript Array, and then operate on it.
String script =
"function printArrayValues(objArray) {\n" +
" var result = '';\n var temp = Java.from(objArray);" +
" for(var obj in temp ) {\n" +
" if(temp .hasOwnProperty(obj)) {\n" +
" result = result + ' ' + temp [obj];\n" +
" }\n" +
" }\n" +
" return result;\n" +
"}";

Related

Inserting String variable into JSON which is in a form of String

I have a JSON payload saved as a String
String jsonBody = “{\n”
+ ” \“example\“: {\n”
+ ” \“example\“: [\n”
+ ” {\n”
+ ” \“example\“: 100,\n”
+ ” \“this_is_example_json_key\“: \“this_is_example_json_value\“,\n”
I created that by copying body from i.e Postman into
String jsonBody = "here I pasted the body";
Unfortunately I cannot have everything hardcoded there, so I have to change some values to variables. The JSON in postman looks like:
"this_is_example_json_key":"x"
And so on. Let's assume that:
String x = “this_is_example_json_value“;
If I just replace it like
+ ” \“this_is_example_json_key\“: \“ + x + \“,\n”
or something like that, the value in the body will be just this_is_example_json_value, where I need "this_is_example_json_value" (the "" marks are part of the value).
So the question is, how to set up those + / " in the String, so in the end in the value of the JSON I will end up with the value inside " ".
I've tried to play with the " / + but nothing of those were working. Variable must be passed with those " " because otherwise, the API is sending back an error.
Since java 15, if you want only use the string, you can also do in this way:
int this_is_example_json_value= 100;
String json = """
{
"this_is_example_json_key": %d
}
""".formatted(this_is_example_json_value);
Here the official jep.
Don't try to build up JSON using strings. Use a proper JSON parser.
import org.json.JSONException;
import org.json.JSONObject;
public class Eg {
public static void main(String[] args) throws JSONException {
String x = "this_is_example_json_value";
JSONObject example = new JSONObject();
example.put("this_is_example_json_key", x);
System.out.println(example.toString());
}
}
Which outputs:
{"this_is_example_json_key":"this_is_example_json_value"}
With no messing around wondering what needs to be escaped.
you can use an extra " \ " "
String x = "this_is_example_json_value";
String jsonBody = "{\n"
+ "\"example\": {\n"
+ " \"example\": [\n"
+ " {\n"
+ " \"example\": 100,\n"
+ "\"this_is_example_json_key\":" + "\"" + x + "\"" + "\n }"
+"\n ]\n }\n }";
in this case you will get a json string
{
"example": {
"example": [
{
"example": 100,
"this_is_example_json_key": "this_is_example_json_value"
}
]
}
}

Convert HOCON string into Java object

One of my webservice return below Java string:
[
{
id=5d93532e77490b00013d8862,
app=null,
manufacturer=pearsonEducation,
bookUid=bookIsbn,
model=2019,
firmware=[1.0],
bookName=devotional,
accountLinking=mandatory
}
]
I have the equivalent Java object for the above string. I would like to typecast or convert the above java string into Java Object.
I couldn't type-cast it since it's a String, not an object. So, I was trying to convert the Java string to JSON string then I can write that string into Java object but no luck getting invalid character "=" exception.
Can you change the web service to return JSON?
That's not possible. They are not changing their contracts. It would be super easy if they returned JSON.
The format your web-service returns has it's own name HOCON. (You can read more about it here)
You do not need your custom parser. Do not try to reinvent the wheel.
Use an existing one instead.
Add this maven dependency to your project:
<dependency>
<groupId>com.typesafe</groupId>
<artifactId>config</artifactId>
<version>1.3.0</version>
</dependency>
Then parse the response as follows:
Config config = ConfigFactory.parseString(text);
String id = config.getString("id");
Long model = config.getLong("model");
There is also an option to parse the whole string into a POJO:
MyResponsePojo response = ConfigBeanFactory.create(config, MyResponsePojo.class);
Unfortunately this parser does not allow null values. So you'll need to handle exceptions of type com.typesafe.config.ConfigException.Null.
Another option is to convert the HOCON string into JSON:
String hoconString = "...";
String jsonString = ConfigFactory.parseString(hoconString)
.root()
.render(ConfigRenderOptions.concise());
Then you can use any JSON-to-POJO mapper.
Well, this is definitely not the best answer to be given here, but it is possible, at least…
Manipulate the String in small steps like this in order to get a Map<String, String> which can be processed. See this example, it's very basic:
public static void main(String[] args) {
String data = "[\r\n"
+ " {\r\n"
+ " id=5d93532e77490b00013d8862, \r\n"
+ " app=null,\r\n"
+ " manufacturer=pearsonEducation, \r\n"
+ " bookUid=bookIsbn, \r\n"
+ " model=2019,\r\n"
+ " firmware=[1.0], \r\n"
+ " bookName=devotional, \r\n"
+ " accountLinking=mandatory\r\n"
+ " }\r\n"
+ "]";
// manipulate the String in order to have
String[] splitData = data
// no leading and trailing [ ] - cut the first and last char
.substring(1, data.length() - 1)
// no linebreaks
.replace("\n", "")
// no windows linebreaks
.replace("\r", "")
// no opening curly brackets
.replace("{", "")
// and no closing curly brackets.
.replace("}", "")
// Then split it by comma
.split(",");
// create a map to store the keys and values
Map<String, String> dataMap = new HashMap<>();
// iterate the key-value pairs connected with '='
for (String s : splitData) {
// split them by the equality symbol
String[] keyVal = s.trim().split("=");
// then take the key
String key = keyVal[0];
// and the value
String val = keyVal[1];
// and store them in the map ——> could be done directly, of course
dataMap.put(key, val);
}
// print the map content
dataMap.forEach((key, value) -> System.out.println(key + " ——> " + value));
}
Please note that I just copied your example String which may have caused the line breaks and I think it is not smart to just replace() all square brackets because the value firmware seems to include those as content.
In my opinion, we split the parse process in two step.
Format the output data to JSON.
Parse text by JSON utils.
In this demo code, i choose regex as format method, and fastjson as JSON tool. you can choose jackson or gson. Furthermore, I remove the [ ], you can put it back, then parse it into array.
import com.alibaba.fastjson.JSON;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class SerializedObject {
private String id;
private String app;
static Pattern compile = Pattern.compile("([a-zA-Z0-9.]+)");
public static void main(String[] args) {
String str =
" {\n" +
" id=5d93532e77490b00013d8862, \n" +
" app=null,\n" +
" manufacturer=pearsonEducation, \n" +
" bookUid=bookIsbn, \n" +
" model=2019,\n" +
" firmware=[1.0], \n" +
" bookName=devotional, \n" +
" accountLinking=mandatory\n" +
" }\n";
String s1 = str.replaceAll("=", ":");
StringBuffer sb = new StringBuffer();
Matcher matcher = compile.matcher(s1);
while (matcher.find()) {
matcher.appendReplacement(sb, "\"" + matcher.group(1) + "\"");
}
matcher.appendTail(sb);
System.out.println(sb.toString());
SerializedObject serializedObject = JSON.parseObject(sb.toString(), SerializedObject.class);
System.out.println(serializedObject);
}
}

Using JSONAssert to check if an item exists in a JSON array

I have a JSONObject that is similar to something like this:
{
"category":"abc"
"staus":""open"
"external":[
{"name":"123", "type":"OTHER"},
{"name":"678", "type":"ALPHA"},
{"name":"890", "type":"DELTA"}
]
}
If I want to use JSONAssert to check if the item {"name":"678"} exists and I don't know the item's order and the number of items in the "external" array, how should I do in Java?
It seems the ArrayValueMatcher should be the way to go but I just cannot get it works.
Please help
You could use JsonPath for this usecase :
JSONArray array = JsonPath.read(json, "$.external[?(#.name == '678')]");
Assertions.assertThat(array).hasSize(1);
Here is a complete example using JsonAssert:
#Test
public void foo() throws Exception {
String jsonString = "{\n" +
" \"category\":\"abc\",\n" +
" \"staus\":\"open\",\n" +
" \"external\":[\n" +
" {\"name\":\"123\", \"type\":\"OTHER\"},\n" +
" {\"name\":\"678\", \"type\":\"ALPHA\"},\n" +
" {\"name\":\"890\", \"type\":\"DELTA\"}\n" +
" ]\n" +
"}";
JsonAssert.with(jsonString).assertThat("$.external[*].name", hasItem(equalTo("678")));
}

Get extended Java class from Nashorn

Is there a way to get and use an extended Java class from JavaScript (Nashorn)?
What I am trying to do is to extend a Java class in Nashorn and then pass it back to Java. For example, we define a class which extends another class in JS:
var SomeClass= Java.type("com.test.SomeClass");
var MySomeClass = Java.extend(SomeClass, {
aMethod1: function() {
print("m1");
},
aMethod2: function() {
print("m2");
},
});
Then I've tried to get this extended object in Java like so: Object mySomeClass = scriptEngine.get("MySomeClass");
However, I am unable to make any method calls on this object in Java. It always throws an exception when I am trying to cast it to a base class - com.test.SomeClass.
Is it possible to use an extended class created in Nashorn (JS) in Java?
Here is a working example extending ArrayList:
String script = "(function() {\n" +
" var ArrayList = Java.type('java.util.ArrayList')\n" +
" var ArrayListExtender = Java.extend(ArrayList)\n" +
" var list = new ArrayListExtender() {\n" +
" get: function(idx) {\n" +
" return idx >= list.size() ? " +
"'no such value' : Java.super(list).get(idx);\n" +
" }\n" +
" }\n" +
" return list;\n" +
"} ());";
ScriptEngine engine = new ScriptEngineManager().getEngineByName("JavaScript");
#SuppressWarnings("unchecked")
List<String> result = (List<String>) engine.eval(script);
result.add("gotcha");
System.out.println(result.get(0));
System.out.println(result.get(10));
You don't state what exception you are getting but the problem is likely in how you consume or use the type. Note that neither Java.type nor Java.extend return a java.lang.Class - these methods return an internal Nashorn type with an undocumented API.

riak mapreduce with limit on response size in java

I'm trying to run a mapReduce query on Riak 1.4 that queries by secondary index, sorts the records by date, and then limits the results to the first record.
I've got the secondary index query working. The sort doesn't seem to do anything. No errors on the sort, just returns the results unsorted. The limit on the number of records returned yields a 'bad_json' error returned by the server.
Here's what I have. It is suppose to query the "cars" bucket for the most recent car owned by "john_doe". (some names have been changed to protect the innocent;) :
JSSourceFunction dateSortFunction = new JSSourceFunction(
"function(v) {" +
"return v.sort(function(a, b) {" +
"return a.issueDate - b.issueDate ;" +
"}" +
");" +
"}");
IndexQuery iq = new BinValueQuery(BinIndex.named("person"), "cars", "john_doe");
MapReduceResult response = session.mapReduce(iq)
.addMapPhase(NamedErlangFunction.MAP_OBJECT_VALUE)
.addReducePhase(dateSortFunction)
.addReducePhase(new NamedJSFunction("Riak.reduceLimit"), 1)
.execute();
I've seen a number of posts on sorting and am hoping to figure it out eventually. However, I haven't seen any help on how the LIMIT function might work.
Thanks in advance!
Update:
Thanks to Joe, he put me on the right track. Here's what ended up working for me. My date format is ISO 8601 (eg. 2011-05-18T17:00:00-07:00). So, I can lexically compare for the correct sorting. Also, I found javascript's array shortening method and updated the code to return up-to the first 5 objects.
JSSourceFunction sortLimitFunction = new JSSourceFunction(
"function(v) {" +
"v.sort(function(a, b) {" +
"return a.issueDate < b.issueDate" +
"}" +
");" +
"if (v.length > " + "5" + ") { " +
"v.length = " + "5" + ";" +
"}" +
"return v;" +
"}");
IndexQuery iq = new BinValueQuery(BinIndex.named("person"), "cars", "john_doe");
MapReduceResult response = session.mapReduce(iq)
.addMapPhase(new NamedJSFunction("Riak.mapValuesJson"))
.addReducePhase(sortLimitFunction)
.execute();
For the sorting, there is a mailing list post that covers this topic. The main difference I see between that implementation and yours is the use of the JavaScript Riak.mapValuesJson function in the map phase.
For the limiting, if you want just the first item from the sorted list, try having your sort function return only the first element. While the reduce function can (and probably is) called multiple times as partial result sets arrive from the various vnodes, the first element in the consolidated list must also be the first element in the partial list where it originated, so this should give you what you are looking for:
JSSourceFunction dateSortFunction = new JSSourceFunction(
"function(v) {" +
"var arr = v.sort(function(a, b) {" +
"return a.issueDate - b.issueDate ;" +
"}" +
");" +
"if (arr.length == 0) { " +
"return [];" +
"} else {"
"return arr[0];" +
"}"
"}"
);

Categories