From 2159b8d6cc7e0de418062fecb8e57244184e8820 Mon Sep 17 00:00:00 2001 From: nextdaydelivery <79922345+nxtdaydelivery@users.noreply.github.com> Date: Mon, 25 Jul 2022 11:58:48 +0100 Subject: additional config migrators (#64) * json migrator * *coughs* * casting issues fix * cfg implementation and a couple fixes * reformat * cast fix + javadoc * make the json migrator useful, double parsing, separate annotations --- .../oneconfig/config/migration/CfgMigrator.java | 126 +++++++++++++++++ .../oneconfig/config/migration/CfgName.java | 19 +++ .../oneconfig/config/migration/JsonMigrator.java | 152 +++++++++++++++++++++ .../oneconfig/config/migration/JsonName.java | 19 +++ .../oneconfig/config/migration/Migrator.java | 16 ++- .../config/migration/VigilanceMigrator.java | 25 +++- .../oneconfig/config/migration/VigilanceName.java | 6 + 7 files changed, 353 insertions(+), 10 deletions(-) create mode 100644 src/main/java/cc/polyfrost/oneconfig/config/migration/CfgMigrator.java create mode 100644 src/main/java/cc/polyfrost/oneconfig/config/migration/CfgName.java create mode 100644 src/main/java/cc/polyfrost/oneconfig/config/migration/JsonMigrator.java create mode 100644 src/main/java/cc/polyfrost/oneconfig/config/migration/JsonName.java (limited to 'src') diff --git a/src/main/java/cc/polyfrost/oneconfig/config/migration/CfgMigrator.java b/src/main/java/cc/polyfrost/oneconfig/config/migration/CfgMigrator.java new file mode 100644 index 0000000..d558649 --- /dev/null +++ b/src/main/java/cc/polyfrost/oneconfig/config/migration/CfgMigrator.java @@ -0,0 +1,126 @@ +package cc.polyfrost.oneconfig.config.migration; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class CfgMigrator implements Migrator { + final String filePath; + final boolean fileExists; + HashMap> values; + final Pattern stringOrFloatPattern = Pattern.compile("S:(?\\S+)=(?\\S+)"); + final Pattern intPattern = Pattern.compile("I:(?\\S+)=(?\\S+)"); + final Pattern doublePattern = Pattern.compile("D:(?\\S+)=(?\\S+)"); + final Pattern booleanPattern = Pattern.compile("B:(?\\S+)=(?\\S+)"); + final Pattern listPattern = Pattern.compile("S:(?\\S+)\\s<"); + final Pattern categoryPattern = Pattern.compile("(?\\S+)\\s\\Q{"); + + public CfgMigrator(String filePath) { + this.filePath = filePath; + this.fileExists = new File(filePath).exists(); + } + + + /** + * Get the value from its name, category, and subcategory. The target field is also supplied, which can be used to check for {@link VigilanceName}. + * The returned Object is intended to be a "Duck" object, and should be cast to the correct type. The Migrator should never return ClassCastExceptions. + *
NOTE: .cfg files DO NOT support subcategories! The implementation of this is:
+ *
{@code // if a category and a subcategory is supplied, only the category is used. else:
+     * if(category == null && subcategory != null) category = subcategory;}
+ * + * @param subcategory The subcategory of the option (not supported!) + */ + @Nullable + @Override + public Object getValue(Field field, @NotNull String name, @Nullable String category, @Nullable String subcategory) { + if (!fileExists) return null; + if (values == null) generateValues(); + if (field.isAnnotationPresent(CfgName.class)) { + CfgName annotation = field.getAnnotation(CfgName.class); + name = annotation.name(); + category = annotation.category(); + } + name = parse(name); + if (category == null) { + if (subcategory != null) { + category = parse(subcategory); + } + } else category = parse(category); + return values.getOrDefault(category, new HashMap<>()).getOrDefault(name, null); + } + + protected void generateValues() { + if (values == null) values = new HashMap<>(); + try (BufferedReader reader = new BufferedReader(new FileReader(filePath))) { + String currentCategory = null; + String line; + while ((line = reader.readLine()) != null) { + Matcher categoryMatcher = categoryPattern.matcher(line); + if (categoryMatcher.find()) { + currentCategory = categoryMatcher.group("name"); + if (!values.containsKey(currentCategory)) values.put(currentCategory, new HashMap<>()); + } + if (currentCategory == null) continue; + if (line.contains("\"")) { + line = line.replaceAll("\"", "").replaceAll(" ", ""); + } + Matcher booleanMatcher = booleanPattern.matcher(line); + if (booleanMatcher.find()) { + values.get(currentCategory).put(booleanMatcher.group("name"), Boolean.parseBoolean(booleanMatcher.group("value"))); + continue; + } + Matcher intMatcher = intPattern.matcher(line); + if (intMatcher.find()) { + values.get(currentCategory).put(intMatcher.group("name"), Integer.parseInt(intMatcher.group("value"))); + continue; + } + Matcher doubleMatcher = doublePattern.matcher(line); + if (doubleMatcher.find()) { + values.get(currentCategory).put(doubleMatcher.group("name"), Double.parseDouble(doubleMatcher.group("value"))); + continue; + } + Matcher stringOrFloatMatcher = stringOrFloatPattern.matcher(line.trim()); + if (stringOrFloatMatcher.matches()) { + if (line.contains(".")) { + try { + values.get(currentCategory).put(stringOrFloatMatcher.group("name"), Float.parseFloat(stringOrFloatMatcher.group("value"))); + } catch (Exception ignored) { + values.get(currentCategory).put(stringOrFloatMatcher.group("name"), stringOrFloatMatcher.group("value")); + } + } + values.get(currentCategory).put(stringOrFloatMatcher.group("name"), stringOrFloatMatcher.group("value")); + continue; + } + Matcher listMatcher = listPattern.matcher(line.trim()); + if (listMatcher.matches()) { + String name = listMatcher.group("name"); + ArrayList list = new ArrayList<>(); + while ((line = reader.readLine()) != null && !line.contains(">")) { + list.add(line.trim()); + } + String[] array = new String[list.size()]; + // type casting doesn't work, so you have to do this + list.toArray(array); + values.get(currentCategory).put(name, array); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + @NotNull + protected String parse(@NotNull String value) { + if (value.contains("\"")) { + return value.replaceAll("\"", "").replaceAll(" ", ""); + } else return value; + } +} diff --git a/src/main/java/cc/polyfrost/oneconfig/config/migration/CfgName.java b/src/main/java/cc/polyfrost/oneconfig/config/migration/CfgName.java new file mode 100644 index 0000000..560c374 --- /dev/null +++ b/src/main/java/cc/polyfrost/oneconfig/config/migration/CfgName.java @@ -0,0 +1,19 @@ +package cc.polyfrost.oneconfig.config.migration; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + *
{@code public @interface CfgName}
+ * This interface is used to specify a previous name for an element.
+ * For example, if you changed the name of a variable when you migrated/updated your mod to use OneConfig, + * you can use this annotation to specify the previous name so that the Migrator can grab it. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface CfgName { + String name(); + String category(); +} diff --git a/src/main/java/cc/polyfrost/oneconfig/config/migration/JsonMigrator.java b/src/main/java/cc/polyfrost/oneconfig/config/migration/JsonMigrator.java new file mode 100644 index 0000000..292d583 --- /dev/null +++ b/src/main/java/cc/polyfrost/oneconfig/config/migration/JsonMigrator.java @@ -0,0 +1,152 @@ +package cc.polyfrost.oneconfig.config.migration; + +import com.google.gson.*; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.io.FileReader; +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +/** + *
{@code public class JsonMigrator implements Migrator}
+ *

JsonMigrator is a class that is used to migrate old configs using ANY .json system to the new OneConfig one.

+ * It works using the {@link JsonName} annotation to specify a full, case sensitive, dot (or slash) path to the element;
or if the annotation isn't present, it will assume the target is in the 'master' object, and will use field.getName().

+ * An easy way to get this path is to right-click on any json key in IntelliJ, and select "Copy JSON Pointer" or Copy Special > Copy Reference (Ctrl+Alt+Shift+C)

+ *

Examples

+ * Here is our example .json: + *
{@code
+ * {
+ *   "heartbeatTimeFactor": 100.321,
+ *   "heartbeatVolume": 1.0,
+ *   "hearts": {
+ *     "disabled": false,
+ *     "opacity": 1.0,
+ *     "scale": 1.0,
+ *     "animationSpeed": 0,
+ *     "blur": {
+ *       "disabled": false,
+ *       "opacity": 0.97
+ *     }
+ *   }
+ * }
+ *  }
+ * And here is some examples of how to fetch values, in a config that uses a JsonMigrator: + *
{@code
+ * @JsonName("hearts.blur.disabled")                     // retrieve the value from the old JSON file
+ * @Switch(name = "Disable Blur", category = "Hearts")   // initialize a field like a normal config
+ * public static boolean isHeartsBlurDisabled = false;
+ *
+ * @JsonName("/hearts/blur/opacity")                     // retrieve the value from the old JSON file using slashes instead (either work!)
+ * @Slider(name = "Hearts Opacity", category = "Hearts", min = 0f, max = 1f)   // initialize a field like a normal config
+ * public static float isHeartsBlurDisabled = 1f;
+ *
+ * //@JsonName("heartbeatVolume")                   // This one does not need to have the annotation, as it is in the master object, and the variable name is the same.
+ * @Slider(name = "Heartbeat Volume", category = "Hearts", min = 0f, max = 1f)
+ * public static float heartbeatVolume = 0.5f;      // It's good practice to have it though.
+ *  }
+ */ +public class JsonMigrator implements Migrator { + + protected JsonObject object; + protected HashMap values = null; + + + /** + *

{@link JsonMigrator Click ME} for the full javadoc on how to use JsonMigrator!

+ * Construct a new JsonMigrator for this config file. + * + * @param filePath the full path to where the previous config file should be located. + */ + public JsonMigrator(String filePath) { + File file = new File(filePath); + try { + object = new JsonParser().parse(new FileReader(file)).getAsJsonObject(); + } catch (Exception e) { + e.printStackTrace(); + object = null; + } + } + + /** + * @param field The target field of the option + * @param name IGNORED! Uses field.getName() if the annotation is not present. + * @param category IGNORED! + * @param subcategory IGNORED! + */ + @Override + public Object getValue(Field field, @Nullable String name, @Nullable String category, @Nullable String subcategory) { + if (object == null) return null; + if (values == null) generateValues(); + String key; + if (field.isAnnotationPresent(JsonName.class)) { + JsonName annotation = field.getAnnotation(JsonName.class); + key = annotation.value(); + } else key = field.getName(); + return values.get(parse(key)); + } + + protected String parse(@NotNull String value) { + if (value.startsWith("/") || value.startsWith(".")) value = value.substring(1); + return value.replaceAll("/", "."); + } + + protected void generateValues() { + if (object == null) return; + values = new HashMap<>(); + for (Map.Entry master : object.entrySet()) { + loopThroughChildren(master.getKey(), master.getValue().getAsJsonObject()); + } + } + + protected void loopThroughChildren(String path, JsonObject in) { + for (Map.Entry element : in.entrySet()) { + String thisPath = path + "." + element.getKey(); + if (element.getValue().isJsonObject()) { + loopThroughChildren(thisPath, element.getValue().getAsJsonObject()); + } else put(thisPath, element.getValue()); + } + } + + /** + * Take the JsonElement and add it as the correct type to the hashmap. + * + * @param key "." delimited key + * @param val value to be parsed + */ + protected void put(String key, JsonElement val) { + if (val.isJsonNull()) values.put(key, null); + else if (val.isJsonPrimitive()) values.put(key, cast(val.getAsJsonPrimitive())); + else if (val.isJsonArray()) { + JsonArray array = val.getAsJsonArray(); + Iterator iterator = array.iterator(); + Object[] objects = new Object[array.size()]; + int i = 0; + while (iterator.hasNext()) { + objects[i] = cast(iterator.next().getAsJsonPrimitive()); + } + values.put(key, objects); + } else values.put(key, val); + } + + /** + * Cast the given JsonPrimitive to an appropriate number, boolean, or String. + * + * @param primitive the json primitive + * @return the value in the correct type. + */ + private Object cast(JsonPrimitive primitive) { + if (primitive.isJsonNull()) return null; + else if (primitive.isBoolean()) return primitive.getAsBoolean(); + else if (primitive.isNumber()) { + Number number = primitive.getAsNumber(); + if (number.floatValue() % 1f != 0) { + return number.floatValue(); + } else return number.intValue(); + } else + return primitive.getAsString(); // if is not boolean, null or number return as String + } +} diff --git a/src/main/java/cc/polyfrost/oneconfig/config/migration/JsonName.java b/src/main/java/cc/polyfrost/oneconfig/config/migration/JsonName.java new file mode 100644 index 0000000..98ed2c9 --- /dev/null +++ b/src/main/java/cc/polyfrost/oneconfig/config/migration/JsonName.java @@ -0,0 +1,19 @@ +package cc.polyfrost.oneconfig.config.migration; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + *
{@code public @interface JsonName}
+ * This interface is used to specify a previous name for an element.
+ * For example, if you changed the name of a variable when you migrated/updated your mod to use OneConfig, + * you can use this annotation to specify the previous name so that the Migrator can grab it.
+ * This annotation uses dot notation to specify the full, case sensitive path to the old config field. See {@link JsonMigrator} for more information. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface JsonName { + String value(); +} diff --git a/src/main/java/cc/polyfrost/oneconfig/config/migration/Migrator.java b/src/main/java/cc/polyfrost/oneconfig/config/migration/Migrator.java index abfb2a0..e04becc 100644 --- a/src/main/java/cc/polyfrost/oneconfig/config/migration/Migrator.java +++ b/src/main/java/cc/polyfrost/oneconfig/config/migration/Migrator.java @@ -1,14 +1,24 @@ package cc.polyfrost.oneconfig.config.migration; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + import java.lang.reflect.Field; public interface Migrator { /** - * @param field The field of the option - * @param name The name of the option + * Get the value from its name, category, and subcategory. The target field is also supplied, which can be used to check for migration names. + * The returned Object is intended to be a "Duck" object, and should be cast to the correct type. The Migrator should never return ClassCastExceptions. + * + * @param field The target field of the option + * @param name The name of the option (has to be present) * @param category The category of the option * @param subcategory The subcategory of the option * @return Value of the option, null if not found + * @apiNote The nullability of the subcategory or category depends on the implementation. Please check for @NotNull or @Nullable in your implementation. */ - Object getValue(Field field, String name, String category, String subcategory); + @Nullable + Object getValue(Field field, @NotNull String name, String category, String subcategory); + + } diff --git a/src/main/java/cc/polyfrost/oneconfig/config/migration/VigilanceMigrator.java b/src/main/java/cc/polyfrost/oneconfig/config/migration/VigilanceMigrator.java index 1397ef9..8b03252 100644 --- a/src/main/java/cc/polyfrost/oneconfig/config/migration/VigilanceMigrator.java +++ b/src/main/java/cc/polyfrost/oneconfig/config/migration/VigilanceMigrator.java @@ -1,5 +1,8 @@ package cc.polyfrost.oneconfig.config.migration; +import cc.polyfrost.oneconfig.config.core.OneColor; +import org.jetbrains.annotations.NotNull; + import java.io.BufferedReader; import java.io.File; import java.io.FileReader; @@ -13,6 +16,7 @@ public class VigilanceMigrator implements Migrator { private static final Pattern booleanPattern = Pattern.compile("\"?(?[^\\s\"]+)\"? = (?true|false)"); private static final Pattern numberPattern = Pattern.compile("\"?(?[^\\s\"]+)\"? = (?[\\d.]+)"); private static final Pattern stringPattern = Pattern.compile("\"?(?[^\\s\"]+)\"? = \"(?.+)\""); + private static final Pattern colorPattern = Pattern.compile("\"?(?[^\\s\"]+)\"? = \"(?(\\d{1,3},){3}\\d{1,3})\""); protected final String filePath; protected HashMap>> values = null; protected final boolean fileExists; @@ -23,9 +27,9 @@ public class VigilanceMigrator implements Migrator { } @Override - public Object getValue(Field field, String name, String category, String subcategory) { + public Object getValue(Field field, @NotNull String name, @NotNull String category, @NotNull String subcategory) { if (!fileExists) return null; - if (values == null) getOptions(); + if (values == null) generateValues(); if (field.isAnnotationPresent(VigilanceName.class)) { VigilanceName annotation = field.getAnnotation(VigilanceName.class); name = annotation.name(); @@ -35,16 +39,14 @@ public class VigilanceMigrator implements Migrator { name = parse(name); category = parse(category); subcategory = parse(subcategory); - if (values.containsKey(category) && values.get(category).containsKey(subcategory) && values.get(category).get(subcategory).containsKey(name)) - return values.get(category).get(subcategory).get(name); - return null; + return values.getOrDefault(category, new HashMap<>()).getOrDefault(subcategory, new HashMap<>()).getOrDefault(name, null); } - protected String parse(String value) { + protected @NotNull String parse(@NotNull String value) { return value.toLowerCase().replace(" ", "_"); } - protected void getOptions() { + protected void generateValues() { if (values == null) values = new HashMap<>(); try (BufferedReader reader = new BufferedReader(new FileReader(filePath))) { String currentCategory = null; @@ -74,6 +76,15 @@ public class VigilanceMigrator implements Migrator { else options.put(numberMatcher.group("name"), Integer.parseInt(value)); continue; } + Matcher colorMatcher = colorPattern.matcher(line); + if (colorMatcher.find()) { + String[] strings = colorMatcher.group("value").split(","); + int[] values = new int[4]; + for (int i = 0; i < 4; i++) { + values[i] = Integer.parseInt(strings[i]); + } + options.put(colorMatcher.group("name"), new OneColor(values[0], values[1], values[2], values[3])); + } Matcher stringMatcher = stringPattern.matcher(line); if (stringMatcher.find()) { options.put(stringMatcher.group("name"), stringMatcher.group("value")); diff --git a/src/main/java/cc/polyfrost/oneconfig/config/migration/VigilanceName.java b/src/main/java/cc/polyfrost/oneconfig/config/migration/VigilanceName.java index d9db17e..5e14f9c 100644 --- a/src/main/java/cc/polyfrost/oneconfig/config/migration/VigilanceName.java +++ b/src/main/java/cc/polyfrost/oneconfig/config/migration/VigilanceName.java @@ -5,6 +5,12 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +/** + *
{@code public @interface VigilanceName}
+ * This interface is used to specify a previous name for an element.
+ * For example, if you changed the name of a variable when you migrated/updated your mod to use OneConfig, + * you can use this annotation to specify the previous name so that the Migrator can grab it. + */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface VigilanceName { -- cgit