From 13e0edfb6def5bee9d33558e34fd3b04f646720a Mon Sep 17 00:00:00 2001 From: Juno Date: Mon, 5 Feb 2024 22:04:50 -0500 Subject: [PATCH] Use latest lucidjson --- .../org/lavajuno/lucidjson/JsonArray.java | 48 +++- .../org/lavajuno/lucidjson/JsonEntity.java | 210 ++++-------------- .../org/lavajuno/lucidjson/JsonLiteral.java | 22 +- .../org/lavajuno/lucidjson/JsonNumber.java | 34 ++- .../org/lavajuno/lucidjson/JsonObject.java | 51 +++-- .../org/lavajuno/lucidjson/JsonString.java | 26 ++- .../org/lavajuno/lucidjson/util/Index.java | 16 ++ 7 files changed, 198 insertions(+), 209 deletions(-) create mode 100644 src/main/java/org/lavajuno/lucidjson/util/Index.java diff --git a/src/main/java/org/lavajuno/lucidjson/JsonArray.java b/src/main/java/org/lavajuno/lucidjson/JsonArray.java index 579b9f8..5cfdd3b 100644 --- a/src/main/java/org/lavajuno/lucidjson/JsonArray.java +++ b/src/main/java/org/lavajuno/lucidjson/JsonArray.java @@ -1,5 +1,7 @@ package org.lavajuno.lucidjson; +import org.lavajuno.lucidjson.util.Index; + import java.io.FileInputStream; import java.io.FileNotFoundException; import java.text.ParseException; @@ -29,10 +31,11 @@ public class JsonArray extends JsonEntity { /** * Constructs a JsonArray by parsing the input. * @param text JSON to parse + * @param i Index of next character to parse * @throws ParseException If an error is encountered while parsing the input */ - protected JsonArray(String text) throws ParseException { - values = parseValues(text.strip()); + protected JsonArray(String text, Index i) throws ParseException { + values = parseValues(text, i); } /** @@ -43,11 +46,8 @@ protected JsonArray(String text) throws ParseException { */ public static JsonArray from(String text) throws ParseException { String line = text.replace("\n", ""); - if(!line.matches(ARRAY_RGX)) { - printError(line, "Expected an array."); - throw new ParseException("Expected an array.", 0); - } - return new JsonArray(line); + Index i = new Index(0); + return new JsonArray(line, i); } /** @@ -79,15 +79,41 @@ public static JsonArray fromFile(String file_path) throws FileNotFoundException, /** * @param text JSON to parse + * @param i Index of next character to parse * @return Vector created from the input * @throws ParseException If an error is encountered while parsing the input */ - private static Vector parseValues(String text) throws ParseException { + private static Vector parseValues(String text, Index i) throws ParseException { Vector values = new Vector<>(); - Vector raw_values = splitValues(text); - for(String i : raw_values) { - if(!i.isEmpty()) { values.add(parseEntity(i.strip())); } + skipSpace(text, i); + if(text.charAt(i.pos) != '[') { + throwParseError(text, i.pos, "Parsing array, expected a '['."); + } + i.pos++; + if(i.pos >= text.length()) { + // Handle end of input after opening { + throwParseError(text, i.pos, "Parsing array, reached end of input."); } + if(text.charAt(i.pos) == ']') { + // Handle empty arrays + i.pos++; + return new Vector<>(); + } + skipSpace(text, i); + // Parse this JsonArray's values + while(i.pos < text.length()) { + values.add(parseEntity(text, i)); + skipSpace(text, i); + if(text.charAt(i.pos) == ']') { + i.pos++; + break; + } + if(text.charAt(i.pos) != ',') { + throwParseError(text , i.pos, "Parsing array, expected a ','."); + } + i.pos++; + } + return values; } diff --git a/src/main/java/org/lavajuno/lucidjson/JsonEntity.java b/src/main/java/org/lavajuno/lucidjson/JsonEntity.java index 081a495..9015ec7 100644 --- a/src/main/java/org/lavajuno/lucidjson/JsonEntity.java +++ b/src/main/java/org/lavajuno/lucidjson/JsonEntity.java @@ -1,9 +1,8 @@ package org.lavajuno.lucidjson; +import org.lavajuno.lucidjson.util.Index; import org.lavajuno.lucidjson.util.Pair; - import java.text.ParseException; -import java.util.Vector; /** * Abstract representation of a single JSON entity. @@ -12,198 +11,65 @@ */ @SuppressWarnings("unused") public abstract class JsonEntity { - protected static final String PAIR_RGX = "^\".+\" *: *.+$"; - protected static final String OBJECT_RGX = "^\\{.*}$"; - protected static final String ARRAY_RGX = "^\\[.*]$"; - protected static final String STRING_RGX = "^ *\".*\"$"; - protected static final String NUMBER_RGX = "^-?[0-9]+(\\.[0-9]+)?([Ee][0-9]+)?$"; - protected static final String LITERAL_RGX = "^(true|false|null)$"; - - /** - * Possible matches for a segment of text - */ - protected enum Matches { - OBJECT, ARRAY, STRING, NUMBER, LITERAL, NONE - } - - /** - * Matches the input text to a predefined category. - * @param text Text to match - * @return The category that the text is matched to. Can be NONE. - */ - protected static Matches match(String text) { - if(text.matches(OBJECT_RGX)) { return Matches.OBJECT; } - if(text.matches(ARRAY_RGX)) { return Matches.ARRAY; } - if(text.matches(STRING_RGX)) { return Matches.STRING; } - if(text.matches(NUMBER_RGX)) { return Matches.NUMBER; } - if(text.matches(LITERAL_RGX)) { return Matches.LITERAL; } - return Matches.NONE; - } - - /** - * Scans a line and finds the index of the character that closes - * an entity in a scoped structure. ( ex. open->{<- { { } } { } close->}<- ) - * @param line Line to scan - * @param open Opening character - * @param close Closing character - * @param start Index to start scanning at - * @return Index of the closing character. -1 if no character is found. - */ - protected static int findClosing(String line, char open, char close, int start) { - int scope = 0; - boolean enclosed = false; - for(int i = start + 1; i < line.length(); i++) { - if(line.charAt(i) == '"' && line.charAt(i - 1) != '\\') { - enclosed = !enclosed; // double quotes, not escaped - } else if(!enclosed && line.charAt(i) == open && line.charAt(i - 1) != '\\') { - scope++; // not enclosed, opening character, not escaped - } else if(!enclosed && line.charAt(i) == close && line.charAt(i - 1) != '\\') { - // not enclosed, closing character, not escaped - if(scope == 0) { return i; } - scope--; - } - } - return -1; // fail - } - - /** - * Scans the input text and finds the index of the next instance of a - * character that is not enclosed by a pair of another given character. - * @param text Text to scan - * @param c Character to find - * @param start Index to start scanning at - * @return Index of the found character. -1 if not character is found. - */ - protected static int findNext(String text, char c, int start) { - boolean enclosed = text.charAt(start) == '\"'; - for(int i = start + 1; i < text.length(); i++) { - if(text.charAt(i) == '"' && text.charAt(i - 1) != '\\') { - enclosed = !enclosed; // double quotes, not escaped - } else if(!enclosed && text.charAt(i) == c && text.charAt(i - 1) != '\\') { - return i; // not enclosed, target character, not escaped - } - } - return -1; // fail - } - - /** - * Scans the input text and finds the index of the next instance of a - * delimiter character that is not enclosed by an object or array. - * @param text Text to scan - * @param c Delimiter character to find - * @param start Index to start scanning at - * @return Index of the found character. -1 if not character is found. - */ - protected static int findDelimiter(String text, char c, int start) { - boolean enclosed = false; - int i = start; - for(; i < text.length(); i++) { - if(text.charAt(i) == '"' && text.charAt(i - 1) != '\\') { - - enclosed = !enclosed; // double quotes, not escaped - } else if(!enclosed && text.charAt(i) == '{') { - i = findClosing(text, '{', '}', i); // ignore commas in objects - } else if(!enclosed && text.charAt(i) == '[') { - i = findClosing(text, '[', ']', i); // ignore commas in arrays - } else if(!enclosed && text.charAt(i) == c) { - return i; // found comma - } - } - return -1; // fail - } - - /** - * Scans the input and splits comma-delimited values of a JSON - * object or array into a Vector of Strings. - * @param text Text to scan - * @return Elements of the JSON entity as a Vector of Strings. - */ - protected static Vector splitValues(String text) { - Vector values = new Vector<>(); - int e_start = 1; // ignore opening '[' / '{' - int e_end = 0; - while(true) { - e_end = findDelimiter(text, ',', e_start); - if(e_end == -1) { - // last element (no trailing comma) - values.add(text.substring(e_start, text.length() - 1)); // ignore closing ']' / '}' - break; - } - values.add(text.substring(e_start, e_end)); - e_start = e_end + 1; // next element, skip comma - } - return values; - } - /** - * Scans text containing a key-value pair and returns the key. - * @param text Text to scan - * @param end Index of the end of the key - * @return Key found in the pair - * @throws ParseException If the input does not contain a key. - */ - protected static String parseKey(String text, int end) throws ParseException { - String key_raw = text.substring(0, end).strip(); - if(!key_raw.matches(STRING_RGX)) { - printError(text.substring(0, end), "Expected a string."); - throw new ParseException("Expected a string.", 0); - } - return key_raw.substring(1, key_raw.length() - 1); - } - - /** - * Prints a parse error to stderr. - * @param text The text that caused the parse error + * Prints a parse error to stderr, then throws a ParseException + * @param text The input currently being parsed + * @param pos Index of the character that caused error * @param explanation Why the parse error happened */ - protected static void printError(String text, String explanation) { - System.err.println("vvv JSON - Parse error on input: vvv"); - System.err.println(text); - System.err.println("^^^ ---------------------------- ^^^"); + protected static void throwParseError(String text, int pos, String explanation) + throws ParseException { + System.err.println("JSON - Parse error at index " + pos + " of input:"); + System.err.print(text.substring(pos, Math.min(pos + 12, text.length()))); + System.err.println("..."); + System.err.println("^"); System.err.println(explanation + "\n"); /* extra newline */ + throw new ParseException(explanation, pos); } /** * Constructs a single JsonEntity by parsing the input. * @param text Text to parse + * @param i Index of next character to parse * @return JsonEntity created from the input. * @throws ParseException If the input does not match any type of entity */ - protected static JsonEntity parseEntity(String text) throws ParseException { - JsonEntity entity = switch(match(text)) { - case OBJECT -> new JsonObject(text); - case ARRAY -> new JsonArray(text); - case STRING -> new JsonString(text); - case NUMBER -> new JsonNumber(text); - case LITERAL -> new JsonLiteral(text); - case NONE -> null; + protected static JsonEntity parseEntity(String text, Index i) throws ParseException { + while(text.charAt(i.pos) == ' ' || text.charAt(i.pos) == '\t') { i.pos++; } + return switch(text.charAt(i.pos)) { + case '{' -> new JsonObject(text, i); + case '[' -> new JsonArray(text, i); + case '"' -> new JsonString(text, i); + case 't', 'f', 'n' -> new JsonLiteral(text, i); + default -> new JsonNumber(text, i); }; - if(entity == null) { - printError(text, "Expected an entity."); - throw new ParseException("Expected an entity.", 0); - } - return entity; } /** * Constructs a key-value pair (String : JsonEntity) by parsing the input. * @param text Text to parse + * @param i Index of next character to parse * @return Key-value pair created from the input. * @throws ParseException If the input does not match a pair containing a String and JsonEntity */ - protected static Pair parsePair(String text) throws ParseException { - if(text.matches(PAIR_RGX)) { - int split_index = findNext(text, ':', 0); - if(split_index == -1) { - printError(text, "Expected a key-value pair."); - throw new ParseException("Expected a key-value pair.", 0); - } - String key = parseKey(text, split_index); - JsonEntity value = parseEntity(text.substring(split_index + 1).strip()); - return new Pair<>(key, value); + protected static Pair parsePair(String text, Index i) throws ParseException { + while(text.charAt(i.pos) == ' ' || text.charAt(i.pos) == '\t') { i.pos++; } + String key = (new JsonString(text, i)).getValue(); + if(text.charAt(i.pos) != ':') { + throwParseError(text, i.pos, "Parsing pair, expected a ':'."); } - printError(text, "Expected a key-value pair. (Not matched)"); - throw new ParseException("Expected a key-value pair. (Not matched)", 0); + i.pos++; + JsonEntity value = parseEntity(text, i); + return new Pair<>(key, value); + } + + /** + * Advances the index past any whitespace + * @param text Text to scan + * @param i Index of next character to parse + */ + protected static void skipSpace(String text, Index i) { + while(text.charAt(i.pos) == ' ' || text.charAt(i.pos) == '\t') { i.pos++; } } /** diff --git a/src/main/java/org/lavajuno/lucidjson/JsonLiteral.java b/src/main/java/org/lavajuno/lucidjson/JsonLiteral.java index 4df7b2c..280b4c4 100644 --- a/src/main/java/org/lavajuno/lucidjson/JsonLiteral.java +++ b/src/main/java/org/lavajuno/lucidjson/JsonLiteral.java @@ -1,5 +1,9 @@ package org.lavajuno.lucidjson; +import org.lavajuno.lucidjson.util.Index; + +import java.text.ParseException; + /** * Represents a JSON literal value (true/false/null). * Provides functionality for getting and setting the value. @@ -21,13 +25,21 @@ public class JsonLiteral extends JsonEntity { /** * Constructs a JsonLiteral by parsing the input. + * @param i Index of next character to parse * @param text JSON to parse */ - protected JsonLiteral(String text) { - switch (text) { - case "null" -> value = null; - case "true" -> value = true; - case "false" -> value = false; + protected JsonLiteral(String text, Index i) throws ParseException { + if(text.startsWith("true", i.pos)) { + i.pos += 4; + value = true; + } else if(text.startsWith("false", i.pos)) { + i.pos += 5; + value = false; + } else if(text.startsWith("null", i.pos)) { + i.pos += 4; + value = null; + } else { + throwParseError(text, i.pos, "Parsing literal, unknown value"); } } diff --git a/src/main/java/org/lavajuno/lucidjson/JsonNumber.java b/src/main/java/org/lavajuno/lucidjson/JsonNumber.java index 23f96f9..2280152 100644 --- a/src/main/java/org/lavajuno/lucidjson/JsonNumber.java +++ b/src/main/java/org/lavajuno/lucidjson/JsonNumber.java @@ -1,5 +1,9 @@ package org.lavajuno.lucidjson; +import org.lavajuno.lucidjson.util.Index; + +import java.text.ParseException; + /** * Represents a JSON number value. * Provides functionality for getting and setting the value as an int, long, float, or double. @@ -34,10 +38,21 @@ public class JsonNumber extends JsonEntity { /** * Constructs a JsonNumber from the input. - * @param value JSON number + * @param text JSON number + * @param i Index of next character to parse */ - protected JsonNumber(String value) { - this.value = value; + protected JsonNumber(String text, Index i) throws ParseException { + int begin = i.pos; + while(i.pos < text.length()) { + if(!isNumber(text.charAt(i.pos))) { + break; + } + i.pos++; + } + if(i.pos == text.length()) { + throwParseError(text, i.pos, "Parsing number, reached end of input."); + } + this.value = text.substring(begin, i.pos); } /** @@ -92,6 +107,19 @@ protected JsonNumber(String value) { */ public void set(double value) { this.value = Double.toString(value); } + /** + * Returns true if the character is part of a valid JSON number + * @param c Character to check + * @return True if the character is part of a valid JSON number + */ + private static boolean isNumber(char c) { + return switch(c) { + case '0', '1', '2', '3', '4', '5', '6', + '7', '8', '9', '.', '-', 'e', 'E' -> true; + default -> false; + }; + } + @Override public String toString() { return value; } diff --git a/src/main/java/org/lavajuno/lucidjson/JsonObject.java b/src/main/java/org/lavajuno/lucidjson/JsonObject.java index d9cf612..5bcaa97 100644 --- a/src/main/java/org/lavajuno/lucidjson/JsonObject.java +++ b/src/main/java/org/lavajuno/lucidjson/JsonObject.java @@ -1,5 +1,6 @@ package org.lavajuno.lucidjson; +import org.lavajuno.lucidjson.util.Index; import org.lavajuno.lucidjson.util.Pair; import java.io.FileInputStream; @@ -30,10 +31,11 @@ public class JsonObject extends JsonEntity { /** * Constructs a JsonObject by parsing the input. * @param text JSON to parse + * @param i Index of next character to parse * @throws ParseException If an error is encountered while parsing the input */ - protected JsonObject(String text) throws ParseException { - values = parseValues(text.strip()); + protected JsonObject(String text, Index i) throws ParseException { + values = parseValues(text, i); } /** @@ -44,11 +46,8 @@ protected JsonObject(String text) throws ParseException { */ public static JsonObject from(String text) throws ParseException { String line = text.replace("\n", ""); - if(!line.matches(OBJECT_RGX)) { - printError(line, "Expected an object."); - throw new ParseException("Expected an object.", 0); - } - return new JsonObject(line); + Index i = new Index(0); + return new JsonObject(line, i); } /** @@ -80,18 +79,44 @@ public static JsonObject fromFile(String file_path) throws FileNotFoundException /** * @param text JSON to parse + * @param i Index of next character to parse * @return Key-value map created from the input * @throws ParseException If an error is encountered while parsing the input */ - private static TreeMap parseValues(String text) throws ParseException { + private static TreeMap parseValues(String text, Index i) throws ParseException { TreeMap values = new TreeMap<>(); - Vector raw_values = splitValues(text); - for(String i : raw_values) { - if(!i.isEmpty()) { - Pair p = parsePair(i.strip()); - values.put(p.first, p.second); + skipSpace(text, i); + if(text.charAt(i.pos) != '{') { + throwParseError(text, i.pos, "Parsing object, expected a '{'."); + } + i.pos++; + if(i.pos >= text.length()) { + // Handle end of input after opening { + throwParseError(text, i.pos, "Parsing object, reached end of input."); + } + if(text.charAt(i.pos) == '}') { + // Handle empty objects + i.pos++; + return new TreeMap<>(); + } + skipSpace(text, i); + // Parse this JsonObject's values + while(i.pos < text.length()) { + Pair p = parsePair(text, i); + values.put(p.first, p.second); + skipSpace(text, i); + if(text.charAt(i.pos) == '}') { + // Object close + i.pos++; + break; + } + if(text.charAt(i.pos) != ',') { + // Not the last item, but no comma + throwParseError(text , i.pos, "Parsing object, expected a ','."); } + i.pos++; } + return values; } diff --git a/src/main/java/org/lavajuno/lucidjson/JsonString.java b/src/main/java/org/lavajuno/lucidjson/JsonString.java index 3b51ed5..30f8240 100644 --- a/src/main/java/org/lavajuno/lucidjson/JsonString.java +++ b/src/main/java/org/lavajuno/lucidjson/JsonString.java @@ -1,5 +1,9 @@ package org.lavajuno.lucidjson; +import org.lavajuno.lucidjson.util.Index; + +import java.text.ParseException; + /** * Represents a JSON string value. * Provides functionality for getting and setting the value. @@ -12,12 +16,24 @@ public class JsonString extends JsonEntity { * Constructs a JsonValue by parsing the input. * @param text JSON string to parse */ - public JsonString(String text) { - if(text.charAt(0) == '"' && text.charAt(text.length() - 1) == '"') { - value = text.substring(1, text.length() - 1); - } else { - value = text; + public JsonString(String text, Index i) throws ParseException { + skipSpace(text, i); + if(text.charAt(i.pos) != '"') { + throwParseError(text, i.pos, "Parsing string, expected a '\"'."); + } + i.pos++; + int begin = i.pos; + while(i.pos < text.length()) { + if(text.charAt(i.pos) == '"' && text.charAt(i.pos - 1) != '\\') { + break; + } + i.pos++; + } + if(i.pos == text.length()) { + throwParseError(text, i.pos, "Parsing string, reached end of input."); } + value = text.substring(begin, i.pos); + i.pos++; } /** diff --git a/src/main/java/org/lavajuno/lucidjson/util/Index.java b/src/main/java/org/lavajuno/lucidjson/util/Index.java new file mode 100644 index 0000000..2a0fc3d --- /dev/null +++ b/src/main/java/org/lavajuno/lucidjson/util/Index.java @@ -0,0 +1,16 @@ +package org.lavajuno.lucidjson.util; + +/** + * Stores the index of the next character to be parsed. + * Passed between JsonEntities as the input is parsed + * and they are constructed. + */ +public class Index { + public int pos; + + /** + * Constructs an Index. + * @param pos Initial character pointed to by this Index. + */ + public Index(int pos) { this.pos = pos; } +}