16
\$\begingroup\$

Some time ago I found a website about the Java Nashorn API which allows you to run JavaScript from Java code. I thought about creating a JSON parser in pure Java (without external libraries) which makes use of this API and the JavaScript function JSON.parse() My goal is to make this parser as fast, resource efficient and easy as possible. So I would like to know if the following code is efficient compared to JSON libraries and how I could improve it in terms of performance and accuracy.

Code

JSONFile.java

package com.example.json;

import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;

public class JSONFile {

    public static final JSONObject parseFromPath(String path) {
        return parseFromString(TextFile.readFromPath(path));
    }

    public static final JSONObject parseFromResource(String resource) {
        return parseFromString(TextFile.readFromResource(resource));
    }

    public static final JSONObject parseFromURL(String url) {
        return parseFromString(TextFile.readFromURL(url));
    }

    public static final JSONObject parseFromString(String json) {
        try {
            ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn");
            engine.eval(TextFile.readFromResource("/js/jsonparser.js"));
            return new JSONObject(((Invocable) engine).invokeFunction("parseJSON", json));
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (ScriptException e) {
            e.printStackTrace();
        }
        return null;
    }

}

JSONObject.java

package com.example.json;

import java.util.Map;

public class JSONObject {

    private Map<String, Object> map;

    @SuppressWarnings("unchecked")
    public JSONObject(Object object) {
        map = (Map<String, Object>) object;
    }

    public Object get(String key) {
        return map.get(key);
    }

    public String getString(String key) {
        return (String) map.get(key);
    }

    public boolean getBoolean(String key) {
        return (boolean) map.get(key);
    }

    public int getInteger(String key) {
        return (int) map.get(key);
    }

    public float getFloat(String key) {
        return (float) map.get(key);
    }

    public JSONObject getObject(String key) {
        return new JSONObject(map.get(key));
    }

    public JSONArray getArray(String key) {
        return new JSONArray((Object[]) map.get(key));
    }

}

JSONArray.java

package com.example.json;

public class JSONArray {

    private Object[] array;

    public JSONArray(Object[] array) {
        this.array = array;
    }

    public int length() {
        return array.length;
    }

    public Object get(int index) {
        return array[index];
    }

    public String getString(int index) {
        return (String) array[index];
    }

    public boolean getBoolean(int index) {
        return (boolean) array[index];
    }

    public int getInteger(int index) {
        return (int) array[index];
    }

    public float getFloat(int index) {
        return (float) array[index];
    }

    public JSONObject getObject(int index) {
        return new JSONObject(array[index]);
    }

    public JSONArray getArray(int index) {
        return new JSONArray((Object[]) array[index]);
    }

}

TextFile.java

package com.example.json;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

public class TextFile {

    public static final String LINE_SEPARATOR = System.getProperty("line.separator");

    public static final String readFromPath(String path) {
        try {
            return read(Files.newInputStream(Paths.get(path), StandardOpenOption.READ));
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    public static final String readFromResource(String resource) {
        return read(TextFile.class.getResourceAsStream(resource));
    }

    public static final String readFromURL(String url) {
        try {
            return read(new URL(url).openStream());
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    private static final String read(InputStream is) {
        try {
            BufferedReader reader = new BufferedReader(new InputStreamReader(is));
            StringBuilder builder = new StringBuilder();
            String line = null;
            while ((line = reader.readLine()) != null) builder.append(line + LINE_SEPARATOR);
            reader.close();
            System.gc();
            return builder.toString().trim();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

}

/js/sonparser.js

function parseJSON(json) { return convert(JSON.parse(json)); }

function convert(object) {
    if (isObject(object)) return toHashMap(object);
    if (isArray(object)) return toArray(object);
    return object;
}

function toHashMap(object) {
    var map = new java.util.HashMap();
    for (key in object) map.put(key, convert(object[key]));
    return map;
}

function toArray(object) {
    var array = Java.to(object, "java.lang.Object[]");
    for (var index = 0, len = array.length; index < len; index++) array[index] = convert(array[index]);
    return array;
}

function isObject(object) { return Object.prototype.toString.call(object) === "[object Object]"; }
function isArray(object) { return Object.prototype.toString.call(object) === "[object Array]"; }

Example

example.json (Source: http://www.w3schools.com/json/)

{"employees":[
    {"firstName":"John", "lastName":"Doe"},
    {"firstName":"Anna", "lastName":"Smith"},
    {"firstName":"Peter", "lastName":"Jones"}
]}

JSONTest.java

public class JSONTest {

    public static void main(String[] args) {
        JSONArray employees = JSONFile.parseFromResource("/json/example.json").getArray("employees");
        for (int index = 0, len = employees.length(); index < len; index++) {
            JSONObject employee = employees.getObject(index);
            System.out.println(employee.getString("lastName") + ", " + employee.getString("firstName"));
        }
    }

}

Result

Doe, John
Smith, Anna
Jones, Peter

I made some heavy changes to the code since the last edit. (Added JSONObject.java & JSONArray.java & TextFile.java and small improvements to JSONFile.java) So now it is much easier to use the code, as seen in the example.

\$\endgroup\$
1
  • 1
    \$\begingroup\$ This can be done more easily using a ScriptObjectMirror \$\endgroup\$
    – dramzy
    Commented Dec 18, 2018 at 16:16

2 Answers 2

5
\$\begingroup\$

Method names and argument types

public static final JSONObject parseFromPath(String path) { ... }

public static final JSONObject parseFromResource(String resource) { ... }

public static final JSONObject parseFromURL(String url) { ... }

public static final JSONObject parseFromString(String json) { .. }

A school of thought is that one can also use the argument type(s) to simplify the method names. In other words, if the method name says it is parsing from Path, why not accept a Path object then?

public static final JSONObject parse(Path path) { ... }

public static final JSONObject parse(InputStream resource) { ... }

public static final JSONObject parse(URL url) { ... }

public static final JSONObject parse(String json) { .. }

An additional benefit of this is that you eliminate to risk of calling parseFromString() accidentally, when the argument is really a URL.

Auto-boxing

You rely on auto-boxing for your JSONObject and JSONArray classes. The problem with this is that you get a nasty NullPointerException if there is no mapped/array value, and the JVM is trying to unbox the null to one of the primitive types. Also, if the value is not the desired primitive type wrapper object, you will get a ClassCastException too. You should consider checking for that, for example:

public class JSONArray {

    // ...

    public int getInteger(int index) {
        Object value = array[index];
        if (value instanceof Integer) {
            return (Integer) value;
        }
        throw new IllegalArgumentException("Non-integer value at array index: " + index);
    }
}
\$\endgroup\$
2
\$\begingroup\$

Positive points:

  1. It might seem obvious, but adding the length method was a good choice.

  2. I agree with h.j.k in general. I just want to emphasize that your choice might make your API more user-friendly, so they can simply pass the String. But again, I understand that in general, separation of concerns and abstraction are good practices and I would make the same comment.

  3. Most importantly, the fact that you created something that the world needed and didn't exist (please correct me if I'm wrong).

Other potential option:

I emphasize it's a grey area and depends on the user:

  1. Adding toString method.
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.