From cdeb8836a666ea6b47b843be8c5a229c6aef1bb6 Mon Sep 17 00:00:00 2001 From: Juno Date: Wed, 3 Apr 2024 11:55:26 -0400 Subject: [PATCH] JSON and config refactoring --- .../org/lavajuno/lucidjson/JsonArray.java | 102 +++------ .../org/lavajuno/lucidjson/JsonEntity.java | 63 +++--- .../org/lavajuno/lucidjson/JsonLiteral.java | 13 +- .../org/lavajuno/lucidjson/JsonNumber.java | 27 ++- .../org/lavajuno/lucidjson/JsonObject.java | 69 ++---- .../lavajuno/lucidjson/JsonSerializable.java | 49 ++++ .../org/lavajuno/lucidjson/JsonString.java | 33 ++- .../lucidjson/error/JsonParseException.java | 15 ++ .../org/lavajuno/lucidjson/util/Index.java | 3 + .../lavajuno/lucidjson/util/StringUtils.java | 95 ++++++++ .../mirrorlog/config/ApplicationConfig.java | 213 +++++++----------- .../org/lavajuno/mirrorlog/io/LogEvent.java | 5 +- .../org/lavajuno/mirrorlog/io/LogFile.java | 9 +- .../mirrorlog/io/OutputController.java | 18 +- .../mirrorlog/server/ServerController.java | 13 +- .../mirrorlog/server/ServerThread.java | 13 +- 16 files changed, 390 insertions(+), 350 deletions(-) create mode 100644 src/main/java/org/lavajuno/lucidjson/JsonSerializable.java create mode 100644 src/main/java/org/lavajuno/lucidjson/error/JsonParseException.java create mode 100644 src/main/java/org/lavajuno/lucidjson/util/StringUtils.java diff --git a/src/main/java/org/lavajuno/lucidjson/JsonArray.java b/src/main/java/org/lavajuno/lucidjson/JsonArray.java index 48093a6..6deb258 100644 --- a/src/main/java/org/lavajuno/lucidjson/JsonArray.java +++ b/src/main/java/org/lavajuno/lucidjson/JsonArray.java @@ -1,13 +1,9 @@ package org.lavajuno.lucidjson; import org.lavajuno.lucidjson.util.Index; +import org.lavajuno.lucidjson.error.JsonParseException; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.text.ParseException; -import java.util.List; -import java.util.Scanner; -import java.util.Vector; +import java.util.ArrayList; /** * Represents a JSON array. @@ -15,26 +11,26 @@ */ @SuppressWarnings("unused") public class JsonArray extends JsonEntity { - private final Vector values; + private final ArrayList values; /** * Constructs an empty JsonArray. */ - public JsonArray() { values = new Vector<>(); } + public JsonArray() { values = new ArrayList<>(); } /** * Constructs a JsonArray from the given vector of elements. * @param values Values to initialize array with */ - public JsonArray(Vector values) { this.values = values; } + public JsonArray(ArrayList values) { this.values = values; } /** * 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 + * @throws JsonParseException If an error is encountered while parsing the input */ - protected JsonArray(String text, Index i) throws ParseException { + protected JsonArray(String text, Index i) throws JsonParseException { values = parseValues(text, i); } @@ -42,62 +38,34 @@ protected JsonArray(String text, Index i) throws ParseException { * Deserializes a JSON array from a String. * @param text Input string * @return Deserialized JSON array - * @throws ParseException if parsing fails; + * @throws JsonParseException if parsing fails; */ - public static JsonArray from(String text) throws ParseException { - String line = text.replace("\n", ""); + public static JsonArray from(String text) throws JsonParseException { Index i = new Index(0); - return new JsonArray(line, i); - } - - /** - * Deserializes a JSON array from a list of lines (Strings). - * @param lines Input lines - * @return Deserialized JSON array - * @throws ParseException If parsing fails - */ - public static JsonArray from(List lines) throws ParseException { - StringBuilder sb = new StringBuilder(); - for(String i : lines) { sb.append(i); } - return from(sb.toString()); - } - - /** - * Deserializes a JSON array from a file. - * @param file_path Path to the input file - * @return Deserialized JSON array - * @throws FileNotFoundException If the file could not be read - * @throws ParseException If parsing fails - */ - public static JsonArray fromFile(String file_path) throws FileNotFoundException, ParseException { - Scanner file = new Scanner(new FileInputStream(file_path)); - StringBuilder lines = new StringBuilder(); - while(file.hasNextLine()) { lines.append(file.nextLine()); } - file.close(); - return from(lines.toString()); + return new JsonArray(text, i); } /** * @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 + * @throws JsonParseException If an error is encountered while parsing the input */ - private static Vector parseValues(String text, Index i) throws ParseException { - Vector values = new Vector<>(); + private static ArrayList parseValues(String text, Index i) throws JsonParseException { + ArrayList values = new ArrayList<>(); skipSpace(text, i); if(text.charAt(i.pos) != '[') { - throwParseError(text, i.pos, "Parsing array, expected a '['."); + throw new JsonParseException(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."); + throw new JsonParseException(text, i.pos, "Parsing array, reached end of input."); } if(text.charAt(i.pos) == ']') { // Handle empty arrays i.pos++; - return new Vector<>(); + return new ArrayList<>(); } skipSpace(text, i); // Parse this JsonArray's values @@ -109,7 +77,7 @@ private static Vector parseValues(String text, Index i) throws Parse break; } if(text.charAt(i.pos) != ',') { - throwParseError(text , i.pos, "Parsing array, expected a ','."); + throw new JsonParseException(text , i.pos, "Parsing array, expected a ','."); } i.pos++; } @@ -119,21 +87,21 @@ private static Vector parseValues(String text, Index i) throws Parse /** * @param index Index of the target JsonEntity - * @return JsonEntity at the given index (null if it does not exist) + * @return JsonEntity at the given index + * @throws IndexOutOfBoundsException If the index is larger than the size of the array */ - public JsonEntity get(int index) { - try { - return values.get(index); - } catch(ArrayIndexOutOfBoundsException e) { - return null; - } + public JsonEntity get(int index) throws IndexOutOfBoundsException { + return values.get(index); } /** * @param index Index of the target JsonEntity * @param value New value for the target JsonEntity + * @throws IndexOutOfBoundsException If the index is larger than the size of the array */ - public void set(int index, JsonEntity value) { values.set(index, value); } + public void set(int index, JsonEntity value) throws IndexOutOfBoundsException { + values.set(index, value); + } /** * @param value JsonEntity to be added to this JsonArray @@ -142,8 +110,11 @@ public JsonEntity get(int index) { /** * @param index Index of the JsonEntity to remove + * @throws IndexOutOfBoundsException If the index is larger than the size of the array */ - public void remove(int index) { values.remove(index); } + public void remove(int index) throws IndexOutOfBoundsException { + values.remove(index); + } /** * Clears this JsonArray @@ -158,21 +129,16 @@ public JsonEntity get(int index) { /** * @return This JsonArray's elements */ - public Vector getValues() { return values; } + public ArrayList values() { return values; } - /** - * Serializes this JsonArray to a String, with indentation and newlines. - * @param indent Indent of this JsonEntity (0) - * @return Returns this JsonEntity as a string. - */ @Override - protected String toString(int indent) { + protected String toJsonString(int indent) { StringBuilder sb = new StringBuilder(); String pad_elem = " ".repeat(indent + 4); String pad_close = " ".repeat(indent); sb.append("[\n"); for(int i = 0; i < values.size(); i++) { - sb.append(pad_elem).append(values.get(i).toString(indent + 4)); + sb.append(pad_elem).append(values.get(i).toJsonString(indent + 4)); if(i < values.size() - 1) { sb.append(","); } sb.append("\n"); } @@ -181,11 +147,11 @@ protected String toString(int indent) { } @Override - public String toString() { + public String toJsonString() { StringBuilder sb = new StringBuilder(); sb.append("["); for(int i = 0; i < values.size() - 1; i++) { - sb.append(values.get(i)).append(",\n"); + sb.append(values.get(i).toJsonString()).append(",\n"); } if(!values.isEmpty()) { sb.append(values.get(values.size() - 1)); diff --git a/src/main/java/org/lavajuno/lucidjson/JsonEntity.java b/src/main/java/org/lavajuno/lucidjson/JsonEntity.java index 0063487..b9b3298 100644 --- a/src/main/java/org/lavajuno/lucidjson/JsonEntity.java +++ b/src/main/java/org/lavajuno/lucidjson/JsonEntity.java @@ -2,7 +2,7 @@ import org.lavajuno.lucidjson.util.Index; import org.lavajuno.lucidjson.util.Pair; -import java.text.ParseException; +import org.lavajuno.lucidjson.error.JsonParseException; /** * Abstract representation of a single JSON entity. @@ -11,31 +11,15 @@ */ @SuppressWarnings("unused") public abstract class JsonEntity { - /** - * 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 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 + * @throws JsonParseException If the input does not match any type of entity */ - protected static JsonEntity parseEntity(String text, Index i) throws ParseException { - while(text.charAt(i.pos) == ' ' || text.charAt(i.pos) == '\t') { i.pos++; } + protected static JsonEntity parseEntity(String text, Index i) throws JsonParseException { + skipSpace(text, i); return switch(text.charAt(i.pos)) { case '{' -> new JsonObject(text, i); case '[' -> new JsonArray(text, i); @@ -50,49 +34,56 @@ protected static JsonEntity parseEntity(String text, Index i) throws ParseExcept * @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 + * @throws JsonParseException If the input does not match a pair containing a String and JsonEntity */ - 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(); + protected static Pair parsePair(String text, Index i) throws JsonParseException { + skipSpace(text, i); + String key = (new JsonString(text, i)).value(); skipSpace(text, i); if(text.charAt(i.pos) != ':') { - throwParseError(text, i.pos, "Parsing pair, expected a ':'."); + throw new JsonParseException(text, i.pos, "Parsing pair, expected a ':'."); } i.pos++; JsonEntity value = parseEntity(text, i); return new Pair<>(key, value); } + /** + * @param c Character to check + * @return True if the character is whitespace (space, tab, or newline) + */ + protected static boolean isWhitespace(char c) { + return c == ' ' || c == '\t' || c == '\n' || c == '\r'; + } + /** * 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++; } + while(isWhitespace(text.charAt(i.pos))) { i.pos++; } } /** - * Serializes this JsonEntity to a String with newlines and indentation. + * Serializes this JsonEntity to a JSON string with newlines and indentation. * @param indent Indent of this JsonEntity * @return This JsonEntity as a String */ - protected abstract String toString(int indent); + protected abstract String toJsonString(int indent); /** - * Serializes this JsonEntity to a String with optional formatting. - * @param pretty Whether to use newlines and indents in the output + * Serializes this JsonEntity to a JSON string. + * @param pretty false to minify, true to use newlines and indents * @return This JsonEntity as a String */ - public String toString(boolean pretty) { - return pretty ? this.toString(0) : this.toString(); + public String toJsonString(boolean pretty) { + return pretty ? this.toJsonString(0) : this.toJsonString(); } /** - * Serializes this JsonEntity to a String without any formatting. - * @return This JsonEntity as a String + * Serializes this JsonEntity to a minified JSON string. + * @return This JsonEntity as a JSON string */ - @Override - public abstract String toString(); + public abstract String toJsonString(); } diff --git a/src/main/java/org/lavajuno/lucidjson/JsonLiteral.java b/src/main/java/org/lavajuno/lucidjson/JsonLiteral.java index 280b4c4..2c73f4e 100644 --- a/src/main/java/org/lavajuno/lucidjson/JsonLiteral.java +++ b/src/main/java/org/lavajuno/lucidjson/JsonLiteral.java @@ -1,8 +1,7 @@ package org.lavajuno.lucidjson; import org.lavajuno.lucidjson.util.Index; - -import java.text.ParseException; +import org.lavajuno.lucidjson.error.JsonParseException; /** * Represents a JSON literal value (true/false/null). @@ -28,7 +27,7 @@ public class JsonLiteral extends JsonEntity { * @param i Index of next character to parse * @param text JSON to parse */ - protected JsonLiteral(String text, Index i) throws ParseException { + protected JsonLiteral(String text, Index i) throws JsonParseException { if(text.startsWith("true", i.pos)) { i.pos += 4; value = true; @@ -39,7 +38,7 @@ protected JsonLiteral(String text, Index i) throws ParseException { i.pos += 4; value = null; } else { - throwParseError(text, i.pos, "Parsing literal, unknown value"); + throw new JsonParseException(text, i.pos, "Parsing literal, unknown value"); } } @@ -53,14 +52,14 @@ protected JsonLiteral(String text, Index i) throws ParseException { * Gets the value of this JsonLiteral. * @return Value of this JsonLiteral (true/false/null) */ - public Boolean getValue() { return value; } + public Boolean value() { return value; } @Override - public String toString() { + public String toJsonString() { if(value == null) { return "null"; } return value ? "true" : "false"; } @Override - protected String toString(int indent) { return this.toString(); } + protected String toJsonString(int indent) { return this.toJsonString(); } } diff --git a/src/main/java/org/lavajuno/lucidjson/JsonNumber.java b/src/main/java/org/lavajuno/lucidjson/JsonNumber.java index 2280152..0e6691f 100644 --- a/src/main/java/org/lavajuno/lucidjson/JsonNumber.java +++ b/src/main/java/org/lavajuno/lucidjson/JsonNumber.java @@ -1,8 +1,7 @@ package org.lavajuno.lucidjson; import org.lavajuno.lucidjson.util.Index; - -import java.text.ParseException; +import org.lavajuno.lucidjson.error.JsonParseException; /** * Represents a JSON number value. @@ -41,7 +40,7 @@ public class JsonNumber extends JsonEntity { * @param text JSON number * @param i Index of next character to parse */ - protected JsonNumber(String text, Index i) throws ParseException { + protected JsonNumber(String text, Index i) throws JsonParseException { int begin = i.pos; while(i.pos < text.length()) { if(!isNumber(text.charAt(i.pos))) { @@ -50,7 +49,7 @@ protected JsonNumber(String text, Index i) throws ParseException { i.pos++; } if(i.pos == text.length()) { - throwParseError(text, i.pos, "Parsing number, reached end of input."); + throw new JsonParseException(text, i.pos, "Parsing number, reached end of input."); } this.value = text.substring(begin, i.pos); } @@ -60,52 +59,52 @@ protected JsonNumber(String text, Index i) throws ParseException { * @return Value of this JsonNumber as an int * @throws NumberFormatException If this JsonNumber cannot be parsed as an int */ - public int getInt() throws NumberFormatException { return Integer.parseInt(value); } + public int toInt() throws NumberFormatException { return Integer.parseInt(value); } /** * Gets the value of this JsonNumber. * @return Value of this JsonNumber as a long * @throws NumberFormatException If this JsonNumber cannot be parsed as a long */ - public long getLong() throws NumberFormatException { return Long.parseLong(value); } + public long toLong() throws NumberFormatException { return Long.parseLong(value); } /** * Gets the value of this JsonNumber. * @return Value of this JsonNumber as a float * @throws NumberFormatException If this JsonNumber cannot be parsed as a float */ - public float getFloat() throws NumberFormatException { return Float.parseFloat(value); } + public float toFloat() throws NumberFormatException { return Float.parseFloat(value); } /** * Gets the value of this JsonNumber. * @return Value of this JsonNumber as a double * @throws NumberFormatException If this JsonNumber cannot be parsed as a double */ - public double getDouble() throws NumberFormatException { return Double.parseDouble(value); } + public double toDouble() throws NumberFormatException { return Double.parseDouble(value); } /** * Sets the value of this JsonNumber. * @param value Int value of this JsonNumber */ - public void set(int value) { this.value = Integer.toString(value); } + public void setValue(int value) { this.value = Integer.toString(value); } /** * Sets the value of this JsonNumber. * @param value Long value of this JsonNumber */ - public void set(long value) { this.value = Long.toString(value); } + public void setValue(long value) { this.value = Long.toString(value); } /** * Sets the value of this JsonNumber. * @param value Float value of this JsonNumber */ - public void set(float value) { this.value = Float.toString(value); } + public void setValue(float value) { this.value = Float.toString(value); } /** * Sets the value of this JsonNumber. * @param value Double value of this JsonNumber */ - public void set(double value) { this.value = Double.toString(value); } + public void setValue(double value) { this.value = Double.toString(value); } /** * Returns true if the character is part of a valid JSON number @@ -121,8 +120,8 @@ private static boolean isNumber(char c) { } @Override - public String toString() { return value; } + public String toJsonString() { return value; } @Override - protected String toString(int indent) { return this.toString(); } + protected String toJsonString(int indent) { return this.toJsonString(); } } diff --git a/src/main/java/org/lavajuno/lucidjson/JsonObject.java b/src/main/java/org/lavajuno/lucidjson/JsonObject.java index 5bcaa97..d77251c 100644 --- a/src/main/java/org/lavajuno/lucidjson/JsonObject.java +++ b/src/main/java/org/lavajuno/lucidjson/JsonObject.java @@ -2,16 +2,15 @@ import org.lavajuno.lucidjson.util.Index; import org.lavajuno.lucidjson.util.Pair; +import org.lavajuno.lucidjson.error.JsonParseException; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.text.ParseException; -import java.util.*; +import java.util.TreeMap; +import java.util.Collection; +import java.util.Set; /** * Represents a JSON object. * Provides functionality for accessing and modifying its values. - * LucidJSON v0.0.1 (Experimental) */ @SuppressWarnings("unused") public class JsonObject extends JsonEntity { @@ -32,9 +31,9 @@ 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 + * @throws JsonParseException If an error is encountered while parsing the input */ - protected JsonObject(String text, Index i) throws ParseException { + protected JsonObject(String text, Index i) throws JsonParseException { values = parseValues(text, i); } @@ -42,57 +41,29 @@ protected JsonObject(String text, Index i) throws ParseException { * Deserializes a JSON object from a String. * @param text Input string * @return Deserialized JSON object - * @throws ParseException if parsing fails; + * @throws JsonParseException if parsing fails; */ - public static JsonObject from(String text) throws ParseException { - String line = text.replace("\n", ""); + public static JsonObject from(String text) throws JsonParseException { Index i = new Index(0); - return new JsonObject(line, i); - } - - /** - * Deserializes a JSON object from a list of lines (Strings). - * @param lines Input lines - * @return Deserialized JSON object - * @throws ParseException If parsing fails - */ - public static JsonObject from(List lines) throws ParseException { - StringBuilder sb = new StringBuilder(); - for(String i : lines) { sb.append(i); } - return from(sb.toString()); - } - - /** - * Deserializes a JSON object from a file. - * @param file_path Path to the input file - * @return Deserialized JSON object - * @throws FileNotFoundException If the file could not be read - * @throws ParseException If parsing fails - */ - public static JsonObject fromFile(String file_path) throws FileNotFoundException, ParseException { - Scanner file = new Scanner(new FileInputStream(file_path)); - StringBuilder lines = new StringBuilder(); - while(file.hasNextLine()) { lines.append(file.nextLine()); } - file.close(); - return from(lines.toString()); + return new JsonObject(text, i); } /** * @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 + * @throws JsonParseException If an error is encountered while parsing the input */ - private static TreeMap parseValues(String text, Index i) throws ParseException { + private static TreeMap parseValues(String text, Index i) throws JsonParseException { TreeMap values = new TreeMap<>(); skipSpace(text, i); if(text.charAt(i.pos) != '{') { - throwParseError(text, i.pos, "Parsing object, expected a '{'."); + throw new JsonParseException(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."); + throw new JsonParseException(text, i.pos, "Parsing object, reached end of input."); } if(text.charAt(i.pos) == '}') { // Handle empty objects @@ -112,7 +83,7 @@ private static TreeMap parseValues(String text, Index i) thr } if(text.charAt(i.pos) != ',') { // Not the last item, but no comma - throwParseError(text , i.pos, "Parsing object, expected a ','."); + throw new JsonParseException(text , i.pos, "Parsing object, expected a ','."); } i.pos++; } @@ -150,13 +121,13 @@ private static TreeMap parseValues(String text, Index i) thr * Gets a collection of all the keys in this JsonObject * @return This JsonObject's keys */ - public Collection getKeys() { return values.keySet(); } + public Collection keys() { return values.keySet(); } /** * Gets a collection of all the values in this JsonObject * @return This JsonObject's values */ - public Collection getValues() { return values.values(); } + public Collection values() { return values.values(); } /** * Gets ths size of this JsonObject @@ -165,7 +136,7 @@ private static TreeMap parseValues(String text, Index i) thr public int size() { return values.size(); } @Override - protected String toString(int indent) { + protected String toJsonString(int indent) { StringBuilder sb = new StringBuilder(); String pad_elem = " ".repeat(indent + 4); String pad_close = " ".repeat(indent); @@ -175,7 +146,7 @@ protected String toString(int indent) { for(String j : keys) { i++; sb.append(pad_elem).append("\"").append(j).append("\": "); - sb.append(values.get(j).toString(indent + 4)); + sb.append(values.get(j).toJsonString(indent + 4)); if(i < keys.size()) { sb.append(","); } sb.append("\n"); } @@ -184,14 +155,14 @@ protected String toString(int indent) { } @Override - public String toString() { + public String toJsonString() { StringBuilder sb = new StringBuilder(); sb.append("{"); Set keys = values.keySet(); int i = 0; for(String j : keys) { i++; - sb.append("\"").append(j).append("\":").append(values.get(j)); + sb.append("\"").append(j).append("\":").append(values.get(j).toJsonString()); if(i < keys.size()) { sb.append(","); } } sb.append("}"); diff --git a/src/main/java/org/lavajuno/lucidjson/JsonSerializable.java b/src/main/java/org/lavajuno/lucidjson/JsonSerializable.java new file mode 100644 index 0000000..ef69338 --- /dev/null +++ b/src/main/java/org/lavajuno/lucidjson/JsonSerializable.java @@ -0,0 +1,49 @@ +package org.lavajuno.lucidjson; + +import org.lavajuno.lucidjson.error.JsonParseException; + +/** + * Classes can implement JsonSerializable to enable them + * to be easily serialized/deserialized to/from JSON strings. + * Only toJsonObject() and fromJsonObject() need to be implemented. + */ +@SuppressWarnings("unused") +public interface JsonSerializable { + /** + * Saves data from this instance into a new JSON object. + * @return JSON object created from this instance + */ + JsonObject toJsonObject(); + + /** + * Loads data from a JSON object into this instance. + * @param o JSON object to load data from + */ + void fromJsonObject(JsonObject o); + + /** + * Serializes this instance to a JSON string. + * @param pretty false to minify, true to use newlines and indents + * @return This instance as a JSON string + */ + default String toJsonString(boolean pretty) { + return this.toJsonObject().toJsonString(pretty); + } + + /** + * Serializes this instance to a minified JSON string. + * @return This object as a minified JSON string + */ + default String toJsonString() { + return this.toJsonObject().toJsonString(false); + } + + /** + * Deserializes a JSON string into this instance. + * @param s JSON string to deserialize + * @throws JsonParseException If an error is encountered while parsing the input + */ + default void fromJsonString(String s) throws JsonParseException { + this.fromJsonObject(JsonObject.from(s)); + } +} diff --git a/src/main/java/org/lavajuno/lucidjson/JsonString.java b/src/main/java/org/lavajuno/lucidjson/JsonString.java index 30f8240..ed372c1 100644 --- a/src/main/java/org/lavajuno/lucidjson/JsonString.java +++ b/src/main/java/org/lavajuno/lucidjson/JsonString.java @@ -1,25 +1,38 @@ package org.lavajuno.lucidjson; import org.lavajuno.lucidjson.util.Index; +import org.lavajuno.lucidjson.util.StringUtils; +import org.lavajuno.lucidjson.error.JsonParseException; -import java.text.ParseException; /** * Represents a JSON string value. * Provides functionality for getting and setting the value. + * Handles escaping strings automatically. */ @SuppressWarnings("unused") public class JsonString extends JsonEntity { private String value; /** - * Constructs a JsonValue by parsing the input. + * Constructs a JsonString with a value of "". + */ + public JsonString() { value = ""; } + + /** + * Constructs a JsonString with the given value. + * @param value Value for the new JsonString + */ + public JsonString(String value) { this.value = value; } + + /** + * Constructs a JsonString by parsing the input. * @param text JSON string to parse */ - public JsonString(String text, Index i) throws ParseException { + protected JsonString(String text, Index i) throws JsonParseException { skipSpace(text, i); if(text.charAt(i.pos) != '"') { - throwParseError(text, i.pos, "Parsing string, expected a '\"'."); + throw new JsonParseException(text, i.pos, "Parsing string, expected a '\"'."); } i.pos++; int begin = i.pos; @@ -30,9 +43,9 @@ public JsonString(String text, Index i) throws ParseException { i.pos++; } if(i.pos == text.length()) { - throwParseError(text, i.pos, "Parsing string, reached end of input."); + throw new JsonParseException(text, i.pos, "Parsing string, reached end of input."); } - value = text.substring(begin, i.pos); + value = StringUtils.unescape(text.substring(begin, i.pos)); i.pos++; } @@ -40,7 +53,7 @@ public JsonString(String text, Index i) throws ParseException { * Gets the value of this JsonString. * @return Value of this JsonString */ - public String getValue() { return value; } + public String value() { return value; } /** * Sets the value of this JsonString @@ -49,10 +62,10 @@ public JsonString(String text, Index i) throws ParseException { public void setValue(String value) { this.value = value; } @Override - public String toString() { - return "\"" + value + "\""; + public String toJsonString() { + return "\"" + StringUtils.escape(value) + "\""; } @Override - protected String toString(int indent) { return this.toString(); } + protected String toJsonString(int indent) { return this.toJsonString(); } } diff --git a/src/main/java/org/lavajuno/lucidjson/error/JsonParseException.java b/src/main/java/org/lavajuno/lucidjson/error/JsonParseException.java new file mode 100644 index 0000000..9698559 --- /dev/null +++ b/src/main/java/org/lavajuno/lucidjson/error/JsonParseException.java @@ -0,0 +1,15 @@ +package org.lavajuno.lucidjson.error; + +import java.text.ParseException; + +public class JsonParseException extends ParseException { + public JsonParseException(String text, int pos, String cause) { + super(getMessage(text, pos, cause), pos); + } + + private static String getMessage(String text, int pos, String cause) { + return "At index " + pos + " of input:\n-->" + + text.substring(pos, Math.min(pos + 12, text.length())) + + "\n" + cause; + } +} diff --git a/src/main/java/org/lavajuno/lucidjson/util/Index.java b/src/main/java/org/lavajuno/lucidjson/util/Index.java index 2a0fc3d..678b5f0 100644 --- a/src/main/java/org/lavajuno/lucidjson/util/Index.java +++ b/src/main/java/org/lavajuno/lucidjson/util/Index.java @@ -6,6 +6,9 @@ * and they are constructed. */ public class Index { + /** + * Integer index of the next character be parsed. + */ public int pos; /** diff --git a/src/main/java/org/lavajuno/lucidjson/util/StringUtils.java b/src/main/java/org/lavajuno/lucidjson/util/StringUtils.java new file mode 100644 index 0000000..dbc9102 --- /dev/null +++ b/src/main/java/org/lavajuno/lucidjson/util/StringUtils.java @@ -0,0 +1,95 @@ +package org.lavajuno.lucidjson.util; + +import org.lavajuno.lucidjson.error.JsonParseException; + +/** + * StringUtils provides helper functions for working with strings + * that are used by other classes. + */ +public class StringUtils { + /** + * Escapes a string. + * Currently does not support unicode escapes (uXXXX) + * @param s String that may contain characters that need to be escaped + * @return Escaped string + */ + public static String escape(String s) { + StringBuilder sb = new StringBuilder(); + for(int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + switch(c) { + case '"': + sb.append("\\\""); + break; + case '\\': + sb.append("\\\\"); + break; + case '\b': + sb.append("\\b"); + break; + case '\f': + sb.append("\\f"); + break; + case '\n': + sb.append("\\n"); + break; + case '\r': + sb.append("\\r"); + break; + case '\t': + sb.append("\\t"); + break; + default: + sb.append(c); + } + } + return sb.toString(); + } + + /** + * Unescapes a string. + * Currently does not support unicode escapes (uXXXX) + * @param s String that may contain escape sequences + * @return Unescaped strings + * @throws JsonParseException If an invalid escape sequence is found + */ + public static String unescape(String s) throws JsonParseException { + StringBuilder sb = new StringBuilder(); + for(int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if(c == '\\' && i < s.length() - 1) { + // Escape sequence + switch(s.charAt(i + 1)) { + case '"': + sb.append('"'); + break; + case '\\': + sb.append('\\'); + break; + case 'b': + sb.append('\b'); + break; + case 'f': + sb.append('\f'); + break; + case 'n': + sb.append('\n'); + break; + case 'r': + sb.append('\r'); + break; + case 't': + sb.append("\t"); + break; + default: + throw new JsonParseException(s, i, "Invalid escape sequence"); + } + i++; + } else { + // Not part of escape sequence + sb.append(c); + } + } + return sb.toString(); + } +} diff --git a/src/main/java/org/lavajuno/mirrorlog/config/ApplicationConfig.java b/src/main/java/org/lavajuno/mirrorlog/config/ApplicationConfig.java index 45e88af..3a53d35 100644 --- a/src/main/java/org/lavajuno/mirrorlog/config/ApplicationConfig.java +++ b/src/main/java/org/lavajuno/mirrorlog/config/ApplicationConfig.java @@ -1,126 +1,60 @@ package org.lavajuno.mirrorlog.config; import org.lavajuno.lucidjson.*; +import org.lavajuno.mirrorlog.main.LogMap; import java.io.IOException; -import java.net.InetAddress; -import java.net.UnknownHostException; +import java.nio.file.Files; +import java.nio.file.Path; import java.text.ParseException; -import java.util.Vector; +import java.util.*; /** * ApplicationConfig loads and stores the program configuration, and * provides functionality for retrieving configuration values. */ @SuppressWarnings("unused") -public class ApplicationConfig { - private final int threads; - private final int port; - private final int timeout; - private final int revision; - private final boolean restricted; - private final Vector allowed_addresses; - private final int component_pad; - private final boolean log_to_file; - private final int file_duration; - private final int file_history; +public class ApplicationConfig implements JsonSerializable { + private static ApplicationConfig instance; + + private int threads; + private int port; + private int timeout; + private int revision; + private boolean restricted; + private Set allowed_addresses; + private int component_pad; + private boolean log_to_file; + private int file_duration; + private int file_history; /** - * Instantiates an ApplicationConfig. - * Loads program configuration from the specified configuration file, - * and stores the resulting configuration in the instance. - * @param config_file_path Path to the configuration file to load - * @throws IOException If loading the configuration fails. + * Gets the instance of ApplicationConfig. Will create it if it does not already exist. + * @return Instance of ApplicationConfig. */ - public ApplicationConfig(String config_file_path) throws IOException, ParseException { - /* Parse configuration file */ - final JsonObject config_root = JsonObject.fromFile(config_file_path); - - /* Get configuration revision */ - final JsonNumber config_revision = (JsonNumber) config_root.get("revision"); - - /* Get server configuration */ - final JsonObject config_server = (JsonObject) config_root.get("server"); - final JsonNumber config_threads = (JsonNumber) config_server.get("threads"); - final JsonNumber config_port = (JsonNumber) config_server.get("port"); - final JsonNumber config_timeout = (JsonNumber) config_server.get("timeout"); - final JsonLiteral config_restricted = (JsonLiteral) config_server.get("restricted"); - final JsonArray config_addresses = (JsonArray) config_server.get("allowed_addresses"); - - /* Get log file configuration */ - final JsonObject config_output = (JsonObject) config_root.get("output"); - final JsonNumber config_component_pad = (JsonNumber) config_output.get("component_pad"); - final JsonLiteral config_log_to_file = (JsonLiteral) config_output.get("log_to_file"); - final JsonNumber config_file_duration = (JsonNumber) config_output.get("file_duration"); - final JsonNumber config_file_history = (JsonNumber) config_output.get("file_history"); - - /* Read in and set configuration values */ - try { - this.revision = config_revision.getInt(); - } catch(NumberFormatException e) { - System.err.println("Illegal value for key \"version\"."); - throw new IOException("Illegal value for key \"version\"."); - } - - try { - this.threads = config_threads.getInt(); - } catch(NumberFormatException e) { - System.err.println("Illegal value for key \"threads\"."); - throw new IOException("Illegal value for key \"threads\"."); - } - - try { - this.port = config_port.getInt(); - } catch(NumberFormatException e) { - System.err.println("Illegal value for key \"port\"."); - throw new IOException("Illegal value for key \"port\"."); - } - - try { - this.timeout = config_timeout.getInt(); - } catch(NumberFormatException e) { - System.err.println("Illegal value for key \"timeout\"."); - throw new IOException("Illegal value for key \"timeout\"."); - } - - this.restricted = config_restricted.getValue(); - - allowed_addresses = new Vector<>(); - try { - for(JsonEntity i : config_addresses.getValues()) { - JsonString j = (JsonString) i; - allowed_addresses.add(InetAddress.getByName(j.getValue()) ); - } - } catch(UnknownHostException e) { - System.err.println("Illegal value for key \"allowed_addresses\"."); - throw new IOException("Illegal value for key \"allowed_addresses\"."); - } - - try { - this.component_pad = config_component_pad.getInt(); - } catch(NumberFormatException e) { - System.err.println("Illegal value for key \"component_pad\"."); - throw new IOException("Illegal value for key \"component_pad\"."); - } - - this.log_to_file = config_log_to_file.getValue(); + public static ApplicationConfig getInstance() { + if(instance == null) { instance = new ApplicationConfig(); } + return instance; + } + /** + * Constructs ApplicationConfig. + * Loads program configuration from the specified configuration file, + * and stores the resulting configuration in the new instance. + * If reading or parsing the configuration fails, a runtime + * exception will be thrown. + */ + private ApplicationConfig() { try { - this.file_duration = config_file_duration.getInt(); - } catch(NumberFormatException e) { - System.err.println("Illegal value for key \"file_duration\"."); - throw new IOException("Illegal value for key \"file_duration\"."); - } + JsonObject config = JsonObject.from( + Files.readString(Path.of(LogMap.CONFIG_FILE_PATH)) + ); + this.fromJsonObject(config); - try { - this.file_history = config_file_history.getInt(); - } catch(NumberFormatException e) { - System.err.println("Illegal value for key \"file_history\"."); - throw new IOException("Illegal value for key \"file_history\"."); + } catch(IOException | ParseException e) { + throw new RuntimeException(e); } - - /* Finished loading configuration */ - System.out.println("Application configuration created:\n" + this); + System.out.println("Application configuration created:\n" + this.toJsonString(true)); } /** @@ -151,7 +85,7 @@ public ApplicationConfig(String config_file_path) throws IOException, ParseExcep * The list of allowed IP addresses * @return The value of "allowed_addresses" */ - public Vector getAllowedAddresses() { return allowed_addresses; } + public Set getAllowedAddresses() { return allowed_addresses; } /** * The length that component names should be padded to @@ -178,31 +112,48 @@ public ApplicationConfig(String config_file_path) throws IOException, ParseExcep public int getFileHistory() { return file_history; } @Override - public String toString() { - return String.format( - """ - { - Revision: %s - Threads: %s - Port: %s - Timeout: %s - Restricted: %s - Allowed addresses: %s - Component padding: %s - Log to file: %s - File duration: %s - File history: %s - }""", - revision, - threads, - port, - timeout, - restricted, - allowed_addresses, - component_pad, - log_to_file, - file_duration, - file_history - ); + public JsonObject toJsonObject() { + JsonObject server = new JsonObject(); + server.put("threads", new JsonNumber(threads)); + server.put("port", new JsonNumber(port)); + server.put("timeout", new JsonNumber(timeout)); + server.put("restricted", new JsonLiteral(restricted)); + JsonArray addrs = new JsonArray(); + for(String i : allowed_addresses) { addrs.add(new JsonString(i)); } + server.put("allowed_addresses", addrs); + + JsonObject output = new JsonObject(); + output.put("component_pad", new JsonNumber(component_pad)); + output.put("log_to_file", new JsonLiteral(log_to_file)); + output.put("file_duration", new JsonNumber(file_duration)); + output.put("file_history", new JsonNumber(file_history)); + + JsonObject root = new JsonObject(); + root.put("revision", new JsonNumber(revision)); + root.put("server", server); + root.put("output", output); + + return root; + } + + @Override + public void fromJsonObject(JsonObject o) { + revision = ((JsonNumber) o.get("revision")).toInt(); + + final JsonObject config_server = (JsonObject) o.get("server"); + threads = ((JsonNumber) config_server.get("threads")).toInt(); + port = ((JsonNumber) config_server.get("port")).toInt(); + timeout = ((JsonNumber) config_server.get("timeout")).toInt(); + restricted = ((JsonLiteral) config_server.get("restricted")).value(); + allowed_addresses = new TreeSet<>(); + for(JsonEntity i : ((JsonArray) config_server.get("allowed_addresses")).values()) { + allowed_addresses.add(((JsonString) i).value()); + } + + final JsonObject config_output = (JsonObject) o.get("output"); + component_pad = ((JsonNumber) config_output.get("component_pad")).toInt(); + log_to_file = ((JsonLiteral) config_output.get("log_to_file")).value(); + file_duration = ((JsonNumber) config_output.get("file_duration")).toInt(); + file_history = ((JsonNumber) config_output.get("file_history")).toInt(); } } diff --git a/src/main/java/org/lavajuno/mirrorlog/io/LogEvent.java b/src/main/java/org/lavajuno/mirrorlog/io/LogEvent.java index 3e0af06..b7e04fa 100644 --- a/src/main/java/org/lavajuno/mirrorlog/io/LogEvent.java +++ b/src/main/java/org/lavajuno/mirrorlog/io/LogEvent.java @@ -35,13 +35,12 @@ public class LogEvent { * @param component_name The component name to be logged * @param severity The severity of the event * @param message The message to be logged - * @param application_config ApplicationConfig to use */ - public LogEvent(String component_name, int severity, String message, ApplicationConfig application_config) { + public LogEvent(String component_name, int severity, String message) { this.component_name = component_name; this.severity = severity; this.message = message; - this.COMPONENT_PAD = application_config.getComponentPad(); + this.COMPONENT_PAD = ApplicationConfig.getInstance().getComponentPad(); } /** diff --git a/src/main/java/org/lavajuno/mirrorlog/io/LogFile.java b/src/main/java/org/lavajuno/mirrorlog/io/LogFile.java index df1c88d..61525a5 100644 --- a/src/main/java/org/lavajuno/mirrorlog/io/LogFile.java +++ b/src/main/java/org/lavajuno/mirrorlog/io/LogFile.java @@ -23,19 +23,18 @@ public class LogFile { private final PrintWriter log_print_writer; /** - * Instantiates a LogFile. + * Constructs a LogFile. * Creates new files and cleans up old ones if needed. - * @param application_config ApplicationConfiguration to use * @throws IOException Passes along IOExceptions from file accessors */ - public LogFile(ApplicationConfig application_config) throws IOException { + public LogFile() throws IOException { final String DATE_TAG = FILE_DATE_FORMAT.format(new Date()); final Calendar calendar = Calendar.getInstance(); calendar.set(Calendar.MINUTE, 0); calendar.set(Calendar.SECOND, 0); - calendar.add(Calendar.HOUR, application_config.getFileDuration()); + calendar.add(Calendar.HOUR, ApplicationConfig.getInstance().getFileDuration()); DATE_EXPIRY = Long.parseLong(FILE_DATE_FORMAT.format(calendar.getTime())); - cleanupLogs(LOGS_PATH, application_config.getFileHistory()); + cleanupLogs(LOGS_PATH, ApplicationConfig.getInstance().getFileHistory()); Files.createDirectories(Paths.get(LOGS_PATH)); final String FILE_PATH = LOGS_PATH + DATE_TAG + ".log"; diff --git a/src/main/java/org/lavajuno/mirrorlog/io/OutputController.java b/src/main/java/org/lavajuno/mirrorlog/io/OutputController.java index d1ab57a..07d1986 100644 --- a/src/main/java/org/lavajuno/mirrorlog/io/OutputController.java +++ b/src/main/java/org/lavajuno/mirrorlog/io/OutputController.java @@ -12,19 +12,16 @@ */ public class OutputController extends Thread { private final BlockingQueue output_queue; - private final ApplicationConfig application_config; private LogFile logFile; private final boolean LOG_TO_FILE; /** - * Instantiates an OutputController. - * @param application_config ApplicationConfig to use + * Constructs an OutputController. */ - public OutputController(ApplicationConfig application_config) throws IOException { - this.application_config = application_config; - this.output_queue = new LinkedBlockingQueue<>(); - this.LOG_TO_FILE = application_config.getLogToFile(); - if(LOG_TO_FILE) { logFile = new LogFile(application_config); } + public OutputController() throws IOException { + output_queue = new LinkedBlockingQueue<>(); + LOG_TO_FILE = ApplicationConfig.getInstance().getLogToFile(); + if(LOG_TO_FILE) { logFile = new LogFile(); } } /** @@ -34,7 +31,7 @@ public OutputController(ApplicationConfig application_config) throws IOException * @param message Message to be logged */ public void submitEvent(String component_name, int severity, String message) { - output_queue.add(new LogEvent(component_name, severity, message, application_config)); + output_queue.add(new LogEvent(component_name, severity, message)); } /** @@ -47,7 +44,6 @@ public void run() { while(true) { event = output_queue.take(); write(event); - } } catch(InterruptedException e) { System.out.println("Flushing output queue..."); @@ -66,7 +62,7 @@ private void write(LogEvent event) { if(logFile.isExpired()) { logFile.close(); try { - logFile = new LogFile(application_config); + logFile = new LogFile(); } catch(IOException e) { System.err.println("Failed to create new log file!"); } diff --git a/src/main/java/org/lavajuno/mirrorlog/server/ServerController.java b/src/main/java/org/lavajuno/mirrorlog/server/ServerController.java index 56a048d..8953301 100644 --- a/src/main/java/org/lavajuno/mirrorlog/server/ServerController.java +++ b/src/main/java/org/lavajuno/mirrorlog/server/ServerController.java @@ -26,10 +26,6 @@ public class ServerController extends Thread { */ private final ExecutorService threadPool; - /** - * This ServerController's application configuration - */ - final ApplicationConfig application_config; /** * This ServerController's OutputController @@ -41,10 +37,9 @@ public class ServerController extends Thread { * @throws IOException if the socket cannot be created */ public ServerController() throws IOException, ParseException { - application_config = new ApplicationConfig(LogMap.CONFIG_FILE_PATH); - output_controller = new OutputController(application_config); - threadPool = Executors.newFixedThreadPool(application_config.getThreads()); - socket = new ServerSocket(application_config.getPort()); + output_controller = new OutputController(); + threadPool = Executors.newFixedThreadPool(ApplicationConfig.getInstance().getThreads()); + socket = new ServerSocket(ApplicationConfig.getInstance().getPort()); Runtime.getRuntime().addShutdownHook(new Thread(this::interrupt)); } @@ -58,7 +53,7 @@ public void run() { ); while(true) { try { - threadPool.submit(new ServerThread(socket.accept(), output_controller, application_config)); + threadPool.submit(new ServerThread(socket.accept(), output_controller)); } catch(IOException e) { if (socket.isClosed()) { return; } System.err.println("Failed to accept a connection. (IOException)"); diff --git a/src/main/java/org/lavajuno/mirrorlog/server/ServerThread.java b/src/main/java/org/lavajuno/mirrorlog/server/ServerThread.java index 98db736..7a2bdf5 100644 --- a/src/main/java/org/lavajuno/mirrorlog/server/ServerThread.java +++ b/src/main/java/org/lavajuno/mirrorlog/server/ServerThread.java @@ -16,21 +16,20 @@ */ public class ServerThread extends Thread { private final Socket socket; - private final InetAddress client_address; private final OutputController outputController; private final ApplicationConfig application_config; + private final String client_address; /** * Instantiates a ServerThread. * @param socket Socket to communicate with client over * @param outputController OutputController to queue events in - * @param application_config ApplicationConfig to use */ - public ServerThread(Socket socket, OutputController outputController, ApplicationConfig application_config) { + public ServerThread(Socket socket, OutputController outputController) { this.socket = socket; + client_address = socket.getInetAddress().toString().split("/", 2)[1]; this.outputController = outputController; - this.client_address = socket.getInetAddress(); - this.application_config = application_config; + this.application_config = ApplicationConfig.getInstance(); Runtime.getRuntime().addShutdownHook(new Thread(this::interrupt)); } @@ -93,10 +92,10 @@ public void run() { @Override public void interrupt() { try { - System.out.println("Connection to " + socket.getInetAddress() + " terminated. (Shutdown)"); + System.out.println("Connection to " + client_address + " terminated. (Shutdown)"); this.socket.close(); } catch(IOException e) { - System.err.println("Failed to close connection to " + socket.getInetAddress()); + System.err.println("Failed to close connection to " + client_address); } } }