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 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 mapIt = ((Map)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 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 = getField(key); if (field.isPresent()) { field.get().set(null, value); } else if (jsonMap != null) { ((Map)jsonMap.get(null)).put(key, value); } } } /** * Read the json file as key-value pairs and save it as a map. */ private Map jsonToMap(String jsonString, Function> keyToType) { Map map = new HashMap<>(); Iterator 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> keyToType, Iterator 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 readJsonMap(String key, Iterator 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 = 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 getField(String name) { try { return Optional.of(jsonClass.getField(name)); } catch (NoSuchFieldException e) { return Optional.empty(); } } }