From 454d7cd52c8995232d873d2db895d5de33b8e372 Mon Sep 17 00:00:00 2001 From: Reza Date: Thu, 22 May 2025 16:20:42 +0330 Subject: [PATCH 1/2] Implement a library to work with json data - Parses JSON string into JSONObject (JSONParser.java) - Interface for serializing and deserializing objects (JSONSerializable.java & JSONSerializableFactory.java) --- src/jsonlib/JSONParser.java | 181 +++++++++++++++++++++++ src/jsonlib/JSONSerializable.java | 7 + src/jsonlib/JSONSerializableFactory.java | 32 ++++ src/jsonlib/types/JSONDict.java | 74 +++++++++ src/jsonlib/types/JSONList.java | 54 +++++++ src/jsonlib/types/JSONNumber.java | 18 +++ src/jsonlib/types/JSONObject.java | 27 ++++ src/jsonlib/types/JSONString.java | 36 +++++ 8 files changed, 429 insertions(+) create mode 100644 src/jsonlib/JSONParser.java create mode 100644 src/jsonlib/JSONSerializable.java create mode 100644 src/jsonlib/JSONSerializableFactory.java create mode 100644 src/jsonlib/types/JSONDict.java create mode 100644 src/jsonlib/types/JSONList.java create mode 100644 src/jsonlib/types/JSONNumber.java create mode 100644 src/jsonlib/types/JSONObject.java create mode 100644 src/jsonlib/types/JSONString.java diff --git a/src/jsonlib/JSONParser.java b/src/jsonlib/JSONParser.java new file mode 100644 index 0000000..565d8b3 --- /dev/null +++ b/src/jsonlib/JSONParser.java @@ -0,0 +1,181 @@ +package jsonlib; + +import jsonlib.types.JSONDict; +import jsonlib.types.JSONList; +import jsonlib.types.JSONNumber; +import jsonlib.types.JSONObject; +import jsonlib.types.JSONString; + +public class JSONParser { + private String content; + private int pos; + + public JSONParser(String content) { + this.content = content; + this.pos = 0; + } + + private char peek() { + if (pos >= 0 && pos < content.length()) + return content.charAt(pos); + else + return '\0'; + } + + private char consume() { + char c = peek(); + + if (c != '\0') + pos++; + + return c; + } + + private void consumeWhitespace() { + while (peek() != '\0' && Character.isWhitespace(peek())) + consume(); + } + + private void expect(String needed) throws JSONExpectFailedException { + if (needed == null || needed.length() == 0) + return; + + int i = 0; + while (peek() != '\0' && i < needed.length()) { + if (peek() != needed.charAt(i)) { + throw new JSONExpectFailedException(String.format("Expected '%c', but found '%c'", needed.charAt(i), peek())); + } + + i++; + consume(); + } + + // it means we reached EOF before finishing the expected string + // so we should throw an exception + if (i < needed.length()) + throw new JSONExpectFailedException(String.format("Expected '%c', but reached EOF", needed.charAt(i))); + } + + private JSONString parseString() throws JSONExpectFailedException { + expect("\""); + + StringBuilder sb = new StringBuilder(); + + while (peek() != '\0' && peek() != '"') + sb.append(consume()); + + expect("\""); + + return new JSONString(sb.toString()); + } + + private JSONNumber parseNumber() throws JSONExpectFailedException { + int sign = 1; + + if (peek() == '-') { + expect("-"); + sign = -1; + } + + StringBuilder integerPartStr = new StringBuilder(); + StringBuilder floatingPartStr = new StringBuilder(); + boolean isFloat = false; + + while (peek() != '\0' && Character.isDigit(peek())) + integerPartStr.append(consume()); + + if (integerPartStr.length() == 0) + throw new JSONExpectFailedException(String.format("Expected digit but found '%c'", peek())); + + if (peek() == '.') { + expect("."); + isFloat = true; + while (peek() != '\0' && Character.isDigit(peek())) + floatingPartStr.append(consume()); + + if (floatingPartStr.length() == 0) + throw new JSONExpectFailedException(String.format("Expected digit but found '%c'", peek())); + } + + if (isFloat) { + double number = Double.valueOf(integerPartStr + "." + floatingPartStr); + return new JSONNumber(sign * number); + } else { + int number = Integer.valueOf(integerPartStr.toString()); + return new JSONNumber(sign * number); + } + } + + private JSONList parseList() throws JSONExpectFailedException { + expect("["); + JSONList list = new JSONList(); + + while (peek() != '\0' && peek() != ']') { + JSONObject item = parse(); + list.add(item); + + consumeWhitespace(); + + if (peek() != ']') + expect(","); + } + + expect("]"); + return list; + } + + private JSONDict parseDict() throws JSONExpectFailedException { + expect("{"); + JSONDict dict = new JSONDict(); + + while (peek() != '\0' && peek() != '}') { + consumeWhitespace(); + JSONString key = parseString(); + consumeWhitespace(); + expect(":"); + JSONObject value = parse(); + + dict.put(key, value); + + consumeWhitespace(); + if (peek() != '}') + expect(","); + } + + expect("}"); + return dict; + } + + public JSONObject parse() throws JSONExpectFailedException { + while (peek() != '\0') { + switch (peek()) { + case '[': + return parseList(); + case '{': + return parseDict(); + case '"': + return parseString(); + default: + if (Character.isDigit(peek()) || peek() == '-') + return parseNumber(); + else if (Character.isWhitespace(peek())) + consume(); + else + // TODO: handle invalid characters properly. + throw new UnsupportedOperationException("not implemented yet"); + + // TODO: handle json boolean and null + break; + } + } + return null; + } + +} + +class JSONExpectFailedException extends Exception { + + public JSONExpectFailedException(String msg) { + super(msg); + } +} diff --git a/src/jsonlib/JSONSerializable.java b/src/jsonlib/JSONSerializable.java new file mode 100644 index 0000000..5a95aa9 --- /dev/null +++ b/src/jsonlib/JSONSerializable.java @@ -0,0 +1,7 @@ +package jsonlib; + +import jsonlib.types.JSONObject; + +public interface JSONSerializable { + public JSONObject serialize(); +} diff --git a/src/jsonlib/JSONSerializableFactory.java b/src/jsonlib/JSONSerializableFactory.java new file mode 100644 index 0000000..3d17768 --- /dev/null +++ b/src/jsonlib/JSONSerializableFactory.java @@ -0,0 +1,32 @@ +package jsonlib; + +import jsonlib.types.JSONDict; + +import java.util.HashMap; +import java.util.Map; + +public class JSONSerializableFactory { + private static final Map> typeRegistry = new HashMap<>(); + + public static void registerType(String type, Class clazz) { + typeRegistry.put(type, clazz); + } + + public static JSONSerializable deserialize(JSONDict json) { + if (json == null) { + throw new IllegalArgumentException("Invalid JSON"); + } + + String type = json.getString("class"); + Class clazz = typeRegistry.get(type); + if (clazz == null) + throw new IllegalArgumentException("Unknown type: " + type); + + try { + java.lang.reflect.Method deserializeMethod = clazz.getMethod("deserialize", JSONDict.class); + return (JSONSerializable) deserializeMethod.invoke(null, json); + } catch (Exception e) { + throw new IllegalArgumentException("Failed to deserialize: " + type, e); + } + } +} diff --git a/src/jsonlib/types/JSONDict.java b/src/jsonlib/types/JSONDict.java new file mode 100644 index 0000000..3650ffb --- /dev/null +++ b/src/jsonlib/types/JSONDict.java @@ -0,0 +1,74 @@ +package jsonlib.types; + +import jsonlib.JSONSerializable; + +import java.util.HashMap; +import java.util.Map; + +public class JSONDict extends JSONObject { + public Map value; + + public JSONDict() { + this.value = new HashMap<>(); + } + + public JSONDict(Map value) { + this.value = new HashMap<>(); + + if (value == null) + return; + + for (Map.Entry entry : value.entrySet()) { + this.value.put(new JSONString(entry.getKey()), entry.getValue().serialize()); + } + } + + public void put(JSONString key, JSONObject value) { + this.value.put(key, value); + } + + public void put(String key, JSONObject value) { + this.value.put(new JSONString(key), value); + } + + public JSONObject get(String key) { + return this.value.get(new JSONString(key)); + } + + public String getString(String key) { + JSONObject obj = this.get(key); + if (!(obj instanceof JSONString)) + throw new IllegalArgumentException(String.format("dict[%s] is not an string", key)); + + JSONString jsonString = (JSONString) obj; + return jsonString.getValue(); + } + + public int getInteger(String key) { + JSONObject obj = this.get(key); + if (!(obj instanceof JSONNumber)) + throw new IllegalArgumentException(String.format("dict[%s] is not a number", key)); + + JSONNumber jsonNumber = (JSONNumber) obj; + return jsonNumber.getValue(); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("{"); + + for (Map.Entry entry : value.entrySet()) { + sb.append(entry.getKey().toString()); + sb.append(": "); + sb.append(entry.getValue().toString()); + sb.append(", "); + } + + // ignore the last ", " + sb.delete(sb.length() - 2, sb.length()); + + sb.append("}"); + return sb.toString(); + } +} diff --git a/src/jsonlib/types/JSONList.java b/src/jsonlib/types/JSONList.java new file mode 100644 index 0000000..092397e --- /dev/null +++ b/src/jsonlib/types/JSONList.java @@ -0,0 +1,54 @@ +package jsonlib.types; + +import jsonlib.JSONSerializable; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +public class JSONList extends JSONObject implements Iterable { + public List value; + + public JSONList() { + this.value = new ArrayList<>(); + } + + public JSONList(List value) { + this.value = new ArrayList<>(); + + if (value == null) + return; + + for (T item : value) { + this.value.add(item.serialize()); + } + } + + public void add(JSONObject data) { + this.value.add(data); + } + + public void add(T data) { + this.value.add(data.serialize()); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + + sb.append("["); + for (int i = 0; i < value.size(); i++) { + sb.append(value.get(i).toString()); + if (i + 1 < value.size()) + sb.append(", "); + } + sb.append("]"); + + return sb.toString(); + } + + @Override + public Iterator iterator() { + return value.iterator(); + } +} diff --git a/src/jsonlib/types/JSONNumber.java b/src/jsonlib/types/JSONNumber.java new file mode 100644 index 0000000..f28c451 --- /dev/null +++ b/src/jsonlib/types/JSONNumber.java @@ -0,0 +1,18 @@ +package jsonlib.types; + +public class JSONNumber extends JSONObject { + public T value; + + public JSONNumber(T value) { + this.value = value; + } + + @Override + public String toString() { + return this.value.toString(); + } + + public T getValue() { + return this.value; + } +} diff --git a/src/jsonlib/types/JSONObject.java b/src/jsonlib/types/JSONObject.java new file mode 100644 index 0000000..8abc580 --- /dev/null +++ b/src/jsonlib/types/JSONObject.java @@ -0,0 +1,27 @@ +package jsonlib.types; + +import jsonlib.JSONSerializable; + +import java.util.List; +import java.util.Map; + +public abstract class JSONObject { + public static JSONObject fromString(String data) { + return new JSONString(data); + } + + public static JSONObject fromNumber(T data) { + return new JSONNumber(data); + } + + public static JSONObject fromList(List data) { + return new JSONList(data); + } + + public static JSONObject fromDict(Map data) { + return new JSONDict(data); + } + + @Override + public abstract String toString(); +} diff --git a/src/jsonlib/types/JSONString.java b/src/jsonlib/types/JSONString.java new file mode 100644 index 0000000..a54d589 --- /dev/null +++ b/src/jsonlib/types/JSONString.java @@ -0,0 +1,36 @@ +package jsonlib.types; + +public class JSONString extends JSONObject { + public String value; + + public JSONString(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + @Override + public String toString() { + return String.format("%s", value); + } + + @Override + public int hashCode() { + return this.value.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) + return false; + + if (!(obj instanceof JSONString)) + return false; + + JSONString other = (JSONString) obj; + + return this.value.equals(other.getValue()); + } +} From 00c4c45985bf0244027b63d6c9c64eb554e83e23 Mon Sep 17 00:00:00 2001 From: Reza Date: Thu, 22 May 2025 16:41:26 +0330 Subject: [PATCH 2/2] fix JSONString: wrap the data in "" in toString --- src/jsonlib/types/JSONString.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/jsonlib/types/JSONString.java b/src/jsonlib/types/JSONString.java index a54d589..1d4eebc 100644 --- a/src/jsonlib/types/JSONString.java +++ b/src/jsonlib/types/JSONString.java @@ -13,7 +13,7 @@ public String getValue() { @Override public String toString() { - return String.format("%s", value); + return String.format("\"%s\"", value); } @Override