Files
TetrisClone/src/main/java/eu/midnightdust/yaytris/util/json/NightJson.java
2025-09-09 15:03:24 +02:00

268 lines
9.8 KiB
Java

package eu.midnightdust.yaytris.util.json;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.nio.file.Files;
import java.util.*;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* NightJson v0.3 by Martin Prokoph
* Extremely lightweight JSON library
* Concept inspired by GSON
*/
public class NightJson {
private static final String KEY_PATTERN = "\"(.*)\":";
Class<?> jsonClass;
Field jsonMap;
String fileLocation;
/**
* Initialize the NightJSON json reader & writer.
*
* @param jsonClass the java class that should be linked to the json file
* @param fileLocation the location the json file is read from and written to
*/
public NightJson(Class<?> jsonClass, String fileLocation) {
this.jsonClass = jsonClass;
this.fileLocation = fileLocation;
getField("jsonMap").ifPresent(f -> {
if (f.getType() == Map.class && getTypeArgument(f, 0) == String.class) jsonMap = f;
});
}
/**
* Convert the current state of the java json class to actual json and save it to disk.
*/
public void writeJson() {
if (fileLocation == null) return;
try {
FileWriter jsonFile = new FileWriter(fileLocation);
writeJsonToFile(jsonFile);
} catch (IOException | IllegalAccessException e) {
System.out.println("Oh no! An Error occurred whilst writing the JSON file :(");
//noinspection CallToPrintStackTrace
e.printStackTrace();
}
}
@SuppressWarnings("unchecked")
private void writeJsonToFile(FileWriter jsonFile) throws IOException, IllegalAccessException {
jsonFile.write("{\n");
Iterator<Field> it = Arrays.stream(jsonClass.getFields()).iterator();
while (it.hasNext()) {
Field field = it.next();
if (field != jsonMap) writeElement(jsonFile, field.get(null), field.getType(), field.getName(), it.hasNext());
}
if (jsonMap != null) {
Iterator<String> mapIt = ((Map<String,?>)jsonMap.get(null)).keySet().iterator();
while (mapIt.hasNext()) {
String key = mapIt.next();
Object value = jsonMap.get(key);
writeElement(jsonFile, value, value.getClass(), key, mapIt.hasNext());
}
}
jsonFile.write("}");
jsonFile.close();
}
/**
* Write the desired element into the file.
*/
private void writeElement(FileWriter jsonFile, Object value, Class<?> type, String name, boolean hasNext) throws IOException {
jsonFile.write("\t");
if (type == Comment.class) {
jsonFile.write(String.format("// %s\n", ((Comment) value).commentString));
return;
}
jsonFile.write(String.format("\"%s\": ", name));
jsonFile.write(objToString(value, type));
jsonFile.write(hasNext ? ",\n" : "\n");
}
/**
* Converts the specified value object to its json representation.
*/
private String objToString(Object value, Class<?> type) {
if (type == Map.class) {
StringBuilder mapPairs = new StringBuilder();
Map<?, ?> map = (Map<?, ?>) value;
Iterator<?> it = map.keySet().iterator();
mapPairs.append(it.hasNext() ? "{" : "{}");
while (it.hasNext()) {
Object key = it.next();
Object val = map.get(key);
mapPairs.append("\n\t\t");
mapPairs.append(String.format("%s: %s", objToString(key, key.getClass()), objToString(val, val.getClass())));
mapPairs.append(it.hasNext() ? "," : "\n\t}");
}
return mapPairs.toString();
}
return String.format(type == String.class || type.isEnum() ? "\"%s\"" : "%s", value);
}
public void readJson() {
if (fileLocation == null) return;
try {
File file = new File(fileLocation);
if (!file.exists()) {
writeJson();
return;
}
readJsonFromString(Files.readString(file.toPath()));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* Read the json file from disk and overwrite the json class's field values.
*/
public void readJsonFromString(String jsonString) {
try {
readJsonString(jsonString);
} catch (IllegalAccessException | NoSuchElementException | ClassCastException e) {
System.out.println("Oh no! An Error occurred whilst reading the JSON file :(");
//noinspection CallToPrintStackTrace
e.printStackTrace();
}
}
@SuppressWarnings("unchecked")
private void readJsonString(String jsonString) throws IllegalAccessException {
Map<String, Object> asMap = jsonToMap(
jsonString.replaceAll("(//)+.*\n", ""), // Replace comment lines (Json5)
(key) -> getField(key).isPresent() ? getField(key).get().getType() : String.class); // Determine data type
for (String key : asMap.keySet()) {
Object value = asMap.get(key);
Optional<Field> field = getField(key);
if (field.isPresent()) {
field.get().set(null, value);
}
else if (jsonMap != null) {
((Map<String, Object>)jsonMap.get(null)).put(key, value);
}
}
}
/**
* Read the json file as key-value pairs and save it as a map.
*/
private Map<String, Object> jsonToMap(String jsonString, Function<String, Class<?>> keyToType) {
Map<String, Object> map = new HashMap<>();
Iterator<String> pairIterator = Arrays.stream(jsonString.replaceAll("(//)+.*\n", "").replaceFirst("[{]", "").split(",")).iterator();
while (pairIterator.hasNext()) {
String s = pairIterator.next();
Matcher matcher = Pattern.compile(KEY_PATTERN).matcher(s);
if (matcher.find()) {
String key = matcher.group().replaceAll("([\":])", "");
map.put(key, getValue(s, key, keyToType, pairIterator));
}
}
return map;
}
/**
* Read the current key in the json file.
*/
private Object getValue(String s, String key, Function<String, Class<?>> keyToType, Iterator<String> pairIterator) {
String val = s.split(KEY_PATTERN, 2)[1];
if (s.contains("{")) {
return readJsonMap(key, pairIterator, val);
}
else {
while (val.startsWith(" ")) val = val.substring(1);
val = val.replaceAll("[\"}\n]", "");
while (val.endsWith(",") || val.endsWith("\n") || val.endsWith("\t")) val = val.substring(0, val.length() - 1);
return stringToObj(val, keyToType.apply(key));
}
}
/**
* Handle maps recursively
*/
private Map<String, Object> readJsonMap(String key, Iterator<String> pairIterator, String val) {
StringBuilder submapString = new StringBuilder();
String next = val;
int level = 0;
while (next != null) {
submapString.append(next);
level += charAmount(next, '{');
level -= charAmount(next, '}');
if (level <= 0) break;
if (!pairIterator.hasNext()) {
submapString.append(",");
next = pairIterator.next();
} else next = null;
}
Optional<Field> field = getField(key);
return jsonToMap(String.valueOf(submapString), k -> field.isPresent() ? getTypeArgument(field.get(), 1) : String.class);
}
/**
* Count the amount of appearances of a char in a string.
*
* @param input the string to search in
* @param c the char to count
*/
private int charAmount(String input, char c) {
return (int) input.chars().filter(ch -> ch == c).count();
}
/**
* Converts the value from a json string to the actual field type.
*/
private Object stringToObj(String value, Class<?> type) {
switch (type.getName()) {
case "byte": return Byte.parseByte(value);
case "int": return Integer.parseInt(value);
case "long": return Long.parseLong(value);
case "float": return Float.parseFloat(value);
case "double": return Double.parseDouble(value);
case "boolean": return Boolean.parseBoolean(value);
}
if (type.isEnum()) return Arrays.stream(type.getEnumConstants())
.filter(enumConstant -> Objects.equals(enumConstant.toString(), value)).findFirst().orElseThrow();
else return value;
}
/**
* Gets the type arguments of typed data structures, such as lists or maps.
* @param field the associated field
* @param index the type index (relevant for maps)
*/
private static Class<?> getTypeArgument(Field field, int index) {
return getPrimitiveType((Class<?>) ((ParameterizedType) field.getGenericType()).getActualTypeArguments()[index]);
}
/**
* Tries to get primitive types from non-primitives (e.g. Boolean -> boolean)
*/
public static Class<?> getPrimitiveType(Class<?> rawType) {
try { return (Class<?>) rawType.getField("TYPE").get(null);
} catch (NoSuchFieldException | IllegalAccessException ignored) { return rawType; }
}
/**
* Get a field from a class by its name.
* @param name the field specifier
* @return an optional representation of the field
*/
private Optional<Field> getField(String name) {
try {
return Optional.of(jsonClass.getField(name));
} catch (NoSuchFieldException e) {
return Optional.empty();
}
}
}