Skip to content

Commit

Permalink
Use latest lucidjson
Browse files Browse the repository at this point in the history
  • Loading branch information
lavajuno committed Feb 6, 2024
1 parent d896765 commit 13e0edf
Show file tree
Hide file tree
Showing 7 changed files with 198 additions and 209 deletions.
48 changes: 37 additions & 11 deletions src/main/java/org/lavajuno/lucidjson/JsonArray.java
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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);
}

/**
Expand All @@ -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);
}

/**
Expand Down Expand Up @@ -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<JsonEntity> parseValues(String text) throws ParseException {
private static Vector<JsonEntity> parseValues(String text, Index i) throws ParseException {
Vector<JsonEntity> values = new Vector<>();
Vector<String> 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;
}

Expand Down
210 changes: 38 additions & 172 deletions src/main/java/org/lavajuno/lucidjson/JsonEntity.java
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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<String> splitValues(String text) {
Vector<String> 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<String, JsonEntity> 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<String, JsonEntity> 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++; }
}

/**
Expand Down
22 changes: 17 additions & 5 deletions src/main/java/org/lavajuno/lucidjson/JsonLiteral.java
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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");
}
}

Expand Down
Loading

0 comments on commit 13e0edf

Please sign in to comment.