package ru.bclib.config; import com.google.common.collect.Maps; import com.google.common.reflect.TypeToken; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import net.minecraft.util.GsonHelper; import org.jetbrains.annotations.Nullable; import ru.bclib.api.dataexchange.handler.autosync.FileContentWrapper; import ru.bclib.util.JsonFactory; import ru.bclib.util.Pair; import java.io.ByteArrayInputStream; import java.io.File; import java.io.OutputStream; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.function.Consumer; import java.util.function.Supplier; public final class ConfigKeeper { private final Map> configEntries = Maps.newHashMap(); private final ConfigWriter writer; private JsonObject configObject; private boolean changed = false; public ConfigKeeper(String modID, String group) { this.writer = new ConfigWriter(modID, group); this.configObject = writer.load(); } public File getConfigFile() { return this.writer.getConfigFile(); } boolean compareAndUpdateForSync(FileContentWrapper content) { ByteArrayInputStream inputStream = content.getInputStream(); final JsonObject other = JsonFactory.getJsonObject(inputStream); boolean changed = this.compareAndUpdateForSync(other); if (changed) { OutputStream outStream = content.getEmptyOutputStream(); JsonFactory.storeJson(outStream, this.configObject); content.syncWithOutputStream(); } return changed; } boolean compareAndUpdateForSync(JsonObject other) { return compareAndUpdateForSync(this.configObject, other); } private static Pair> find(JsonObject json, Pair key) { for (var entry : json.entrySet()) { final Pair otherKey = ConfigKey.realKey(entry.getKey()); if (otherKey.first.equals(key.first)) return new Pair<>(entry.getValue(), otherKey); } return null; } /** * Called for content based auto-sync. * * @param me - When called in AutoSync this represents the content of the client. * @param other - When called in AutoSync, this represents the content of the server * @return {@code true} if content was changed */ static boolean compareAndUpdateForSync(JsonObject me, JsonObject other) { boolean changed = false; for (var otherEntry : other.entrySet()) { final Pair otherKey = ConfigKey.realKey(otherEntry.getKey()); final JsonElement otherValue = otherEntry.getValue(); Pair> temp = find(me, otherKey); //we already have an entry if (temp != null) { final Pair myKey = temp.second; final JsonElement myValue = temp.first; if ((otherValue.isJsonNull() && !myValue.isJsonNull()) || (otherValue.isJsonPrimitive() && !myValue.isJsonPrimitive()) || (otherValue.isJsonObject() && !myValue.isJsonObject()) || (otherValue.isJsonArray() && !myValue.isJsonArray())) { //types are different => replace with "server"-version in other changed = true; me.add(myKey.first + myKey.second, otherValue); } else if (otherValue.isJsonPrimitive() || otherValue.isJsonArray() || otherValue.isJsonNull()) { if (!otherValue.equals(myValue)) { changed = true; me.add(myKey.first + myKey.second, otherValue); } } else if (otherValue.isJsonObject()) { changed |= compareAndUpdateForSync(myValue.getAsJsonObject(), otherValue.getAsJsonObject()); } } else { //no entry, just copy the value from other if (!otherValue.isJsonNull()) { changed = true; temp = find(me, otherKey); me.add(otherKey.first + otherKey.second, otherValue); } } } return changed; } public void save() { if (!changed) return; this.writer.save(); this.changed = false; } void reload() { this.configObject = this.writer.reload(); this.configEntries.clear(); this.changed = false; } private > void initializeEntry(ConfigKey key, E entry) { if (configObject == null) { return; } String[] path = key.getPath(); JsonObject obj = configObject; if (!key.isRoot()) { for (String group : path) { JsonElement element = obj.get(group); if (element == null || !element.isJsonObject()) { element = new JsonObject(); obj.add(group, element); } obj = element.getAsJsonObject(); } } String paramKey = key.getEntry(); if (entry.hasDefaultInName()) { paramKey += " [default: " + entry.getDefault() + "]"; } this.changed |= entry.setLocation(obj, paramKey); } private > void storeValue(E entry, T value) { if (configObject == null) { return; } T val = entry.getValue(); if (value.equals(val)) return; entry.toJson(value); this.changed = true; } private > T getValue(E entry) { if (!entry.hasLocation()) { return entry.getDefault(); } return entry.fromJson(); } @Nullable public > E getEntry(ConfigKey key, Class type) { Entry entry = this.configEntries.get(key); if (type.isInstance(entry)) { return type.cast(entry); } return null; } @Nullable public > T getValue(ConfigKey key, Class type) { Entry entry = this.getEntry(key, type); if (entry == null) { return null; } return entry.getValue(); } public > E registerEntry(ConfigKey key, E entry) { entry.setWriter(value -> this.storeValue(entry, value)); entry.setReader(() -> { return this.getValue(entry); }); this.initializeEntry(key, entry); this.configEntries.put(key, entry); return entry; } public static class BooleanEntry extends Entry { public BooleanEntry(Boolean defaultValue) { super(defaultValue); } @Override public Boolean fromJson() { return GsonHelper.getAsBoolean(location, key, defaultValue); } @Override public void toJson(Boolean value) { this.location.addProperty(key, value); } } public static class FloatEntry extends Entry { public FloatEntry(Float defaultValue) { super(defaultValue); } @Override public Float fromJson() { return GsonHelper.getAsFloat(location, key, defaultValue); } @Override public void toJson(Float value) { this.location.addProperty(key, value); } } public static class FloatRange extends RangeEntry { public FloatRange(Float defaultValue, float minVal, float maxVal) { super(defaultValue, minVal, maxVal); } @Override public Float fromJson() { return GsonHelper.getAsFloat(location, key, defaultValue); } @Override public void toJson(Float value) { this.location.addProperty(key, value); } } public static class IntegerEntry extends Entry { public IntegerEntry(Integer defaultValue) { super(defaultValue); } @Override public Integer getDefault() { return this.defaultValue; } @Override public Integer fromJson() { return GsonHelper.getAsInt(location, key, defaultValue); } @Override public void toJson(Integer value) { this.location.addProperty(key, value); } } public static class IntegerRange extends RangeEntry { public IntegerRange(Integer defaultValue, int minVal, int maxVal) { super(defaultValue, minVal, maxVal); } @Override public Integer fromJson() { return GsonHelper.getAsInt(location, key, defaultValue); } @Override public void toJson(Integer value) { this.location.addProperty(key, value); } } public static class StringEntry extends Entry { public StringEntry(String defaultValue) { super(defaultValue); } @Override public String fromJson() { return GsonHelper.getAsString(location, key, defaultValue); } @Override public void toJson(String value) { this.location.addProperty(key, value); } } public static abstract class ArrayEntry extends Entry> { public ArrayEntry(List defaultValue) { super(defaultValue); } protected abstract T getValue(JsonElement element); protected abstract void add(JsonArray array, T element); private JsonArray toArray(List input){ final JsonArray array = new JsonArray(); input.forEach(s -> add(array, s)); return array; } @Override public List fromJson() { final JsonArray resArray = GsonHelper.getAsJsonArray(location, key, toArray(defaultValue)); final List res = new ArrayList<>(resArray.size()); resArray.forEach(e -> res.add(getValue(e))); return res; } @Override public void toJson(List value) { this.location.add(key, toArray(value)); } } public static class StringArrayEntry extends ArrayEntry { public StringArrayEntry(List defaultValue) { super(defaultValue); } @Override protected String getValue(JsonElement el){ return el.getAsString(); } protected void add(JsonArray array, String el){ array.add(el); } @Override protected boolean hasDefaultInName() { return false; } } public static class EnumEntry> extends Entry { private final Type type; public EnumEntry(T defaultValue) { super(defaultValue); TypeToken token = new TypeToken() { private static final long serialVersionUID = 1L; }; this.type = token.getType(); } @Override public T getDefault() { return this.defaultValue; } @Override public T fromJson() { return JsonFactory.GSON.fromJson(location.get(key), type); } @Override public void toJson(T value) { location.addProperty(key, JsonFactory.GSON.toJson(value, type)); } } public static abstract class RangeEntry> extends Entry { private final T min, max; public RangeEntry(T defaultValue, T minVal, T maxVal) { super(defaultValue); this.min = minVal; this.max = maxVal; } @Override public void setValue(T value) { super.setValue(value.compareTo(min) < 0 ? min : value.compareTo(max) > 0 ? max : value); } public T minValue() { return this.min; } public T maxValue() { return this.max; } } public static abstract class Entry { protected final T defaultValue; protected Consumer writer; protected Supplier reader; protected JsonObject location; protected String key; public abstract T fromJson(); public abstract void toJson(T value); public Entry(T defaultValue) { this.defaultValue = defaultValue; } protected void setWriter(Consumer writer) { this.writer = writer; } protected void setReader(Supplier reader) { this.reader = reader; } protected boolean setLocation(JsonObject location, String key) { this.location = location; this.key = key; if (!location.has(key)) { this.toJson(defaultValue); return true; } return false; } protected boolean hasLocation() { return this.location != null && this.key != null; } public T getValue() { return this.reader.get(); } public void setValue(T value) { this.writer.accept(value); } public T getDefault() { return this.defaultValue; } public void setDefault() { this.setValue(defaultValue); } protected boolean hasDefaultInName() { return true; } } }