diff options
author | nextdaydelivery <79922345+nxtdaydelivery@users.noreply.github.com> | 2022-07-25 11:58:48 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-07-25 11:58:48 +0100 |
commit | 2159b8d6cc7e0de418062fecb8e57244184e8820 (patch) | |
tree | cb3928cc4f027138319b5a7e16a691785c88d703 /src/main/java/cc/polyfrost/oneconfig/config | |
parent | 580fd1d5c4ec5625c813f6d593928a401a500869 (diff) | |
download | OneConfig-2159b8d6cc7e0de418062fecb8e57244184e8820.tar.gz OneConfig-2159b8d6cc7e0de418062fecb8e57244184e8820.tar.bz2 OneConfig-2159b8d6cc7e0de418062fecb8e57244184e8820.zip |
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
Diffstat (limited to 'src/main/java/cc/polyfrost/oneconfig/config')
7 files changed, 353 insertions, 10 deletions
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<String, HashMap<String, Object>> values; + final Pattern stringOrFloatPattern = Pattern.compile("S:(?<name>\\S+)=(?<value>\\S+)"); + final Pattern intPattern = Pattern.compile("I:(?<name>\\S+)=(?<value>\\S+)"); + final Pattern doublePattern = Pattern.compile("D:(?<name>\\S+)=(?<value>\\S+)"); + final Pattern booleanPattern = Pattern.compile("B:(?<name>\\S+)=(?<value>\\S+)"); + final Pattern listPattern = Pattern.compile("S:(?<name>\\S+)\\s<"); + final Pattern categoryPattern = Pattern.compile("(?<name>\\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. + * <br><b>NOTE: .cfg files DO NOT support subcategories! The implementation of this is:</b> <br> + * <pre>{@code // if a category and a subcategory is supplied, only the category is used. else: + * if(category == null && subcategory != null) category = subcategory;}</pre> + * + * @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<String> 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; + +/** + * <pre style="font-size: 12px">{@code public @interface CfgName}</pre> + * This interface is used to specify a previous name for an element.<br> + * 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; + +/** + * <pre>{@code public class JsonMigrator implements Migrator}</pre> + * <h2>JsonMigrator is a class that is used to migrate old configs using ANY .json system to the new OneConfig one.</h2> + * It works using the {@link JsonName} annotation to specify a full, case sensitive, dot (or slash) path to the element; <br>or if the annotation isn't present, it will assume the target is in the 'master' object, and will use field.getName(). <br><br> + * 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) <br><br> + * <h2>Examples</h2> + * Here is our example .json: + * <pre>{@code + * { + * "heartbeatTimeFactor": 100.321, + * "heartbeatVolume": 1.0, + * "hearts": { + * "disabled": false, + * "opacity": 1.0, + * "scale": 1.0, + * "animationSpeed": 0, + * "blur": { + * "disabled": false, + * "opacity": 0.97 + * } + * } + * } + * }</pre> + * And here is some examples of how to fetch values, in a config that uses a JsonMigrator: + * <pre>{@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. + * }</pre> + */ +public class JsonMigrator implements Migrator { + + protected JsonObject object; + protected HashMap<String, Object> values = null; + + + /** + * <h2>{@link JsonMigrator Click ME} for the full javadoc on how to use JsonMigrator!</h2> + * 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 <b>IGNORED!</b> Uses field.getName() if the annotation is not present. + * @param category <b>IGNORED!</b> + * @param subcategory <b>IGNORED!</b> + */ + @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<String, JsonElement> master : object.entrySet()) { + loopThroughChildren(master.getKey(), master.getValue().getAsJsonObject()); + } + } + + protected void loopThroughChildren(String path, JsonObject in) { + for (Map.Entry<String, JsonElement> 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<JsonElement> 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; + +/** + * <pre style="font-size: 12px">{@code public @interface JsonName}</pre> + * This interface is used to specify a previous name for an element.<br> + * 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. <br> + * <b>This annotation uses dot notation to specify the full, case sensitive path to the old config field. See {@link JsonMigrator} for more information.</b> + */ +@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 <b>The nullability of the subcategory or category depends on the implementation. Please check for @NotNull or @Nullable in your implementation.</b> */ - 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("\"?(?<name>[^\\s\"]+)\"? = (?<value>true|false)"); private static final Pattern numberPattern = Pattern.compile("\"?(?<name>[^\\s\"]+)\"? = (?<value>[\\d.]+)"); private static final Pattern stringPattern = Pattern.compile("\"?(?<name>[^\\s\"]+)\"? = \"(?<value>.+)\""); + private static final Pattern colorPattern = Pattern.compile("\"?(?<name>[^\\s\"]+)\"? = \"(?<value>(\\d{1,3},){3}\\d{1,3})\""); protected final String filePath; protected HashMap<String, HashMap<String, HashMap<String, Object>>> 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; +/** + * <pre style="font-size: 12px">{@code public @interface VigilanceName}</pre> + * This interface is used to specify a previous name for an element.<br> + * 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 { |