From 16c9e9cca06c59e56d2fdfee5ebc3daec08e8b9b Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 7 Sep 2026 19:39:18 -0600 Subject: [PATCH] Consolidate SimpleAPI into one Maven project with core and Bukkit packages Remove the parent and child module builds. Attach shared and shared-sources JARs from the existing POM, retain public compatibility facades, and move all module tests into the normal project. Preserve dependency scopes, versions and shading; verify the packaged shared classpath without adding a workflow. --- SimpleAPI/pom.xml | 92 ++++- .../bukkit/config/AnnotationHandler.java | 13 + .../bukkit/config/BukkitConfigView.java | 29 ++ .../simpleapi/bukkit/package-info.java | 6 + .../core/config/AnnotationBinder.java | 311 +++++++++++++++++ .../core/config/ConfigurateConfigView.java | 143 ++++++++ .../core/config/YamlConfigDocument.java | 270 +++++++++++++++ .../bencodez/simpleapi/core/package-info.java | 7 + .../simpleapi/core/sql/MysqlConfigView.java | 40 +++ .../file/annotation/AnnotationBinder.java | 308 +---------------- .../file/annotation/AnnotationHandler.java | 26 +- .../file/config/bukkit/BukkitConfigView.java | 89 +---- .../configurate/ConfigurateConfigView.java | 136 +------- .../configurate/YamlConfigDocument.java | 268 +-------------- .../sql/mysql/config/MysqlConfigView.java | 36 +- .../simpleapi/core/CoreClasspathTest.java | 23 ++ .../ConfigurateConfigViewTest.java | 11 +- .../configurate/YamlConfigDocumentTest.java | 0 .../simpleapi/sql/mysql/SharedSqlTest.java | 16 +- .../tests/shared/ConfigParityTest.java | 62 ++++ .../tests/shared/NativeConfigFixture.java | 47 +++ .../shared/PackageCompatibilityTest.java | 58 ++++ .../tests/shared/SharedArtifactTest.java | 62 ++++ .../tests/shared/SharedRuntimeClasspath.java | 47 +++ docs/platform-neutral-configuration.md | 91 +---- docs/shared-libraries.md | 324 +++++++++--------- pom.xml | 47 --- simpleapi-configurate/pom.xml | 31 -- simpleapi-core/pom.xml | 50 --- .../simpleapi/core/CoreClasspathTest.java | 17 - simpleapi-sql/pom.xml | 42 --- tools/ConfigParityProbe.java | 54 --- tools/NativeConfigSmoke.java | 36 -- tools/SharedArtifactProbe.java | 41 --- tools/verify-shared-artifacts.py | 62 ---- 35 files changed, 1436 insertions(+), 1459 deletions(-) create mode 100644 SimpleAPI/src/main/java/com/bencodez/simpleapi/bukkit/config/AnnotationHandler.java create mode 100644 SimpleAPI/src/main/java/com/bencodez/simpleapi/bukkit/config/BukkitConfigView.java create mode 100644 SimpleAPI/src/main/java/com/bencodez/simpleapi/bukkit/package-info.java create mode 100644 SimpleAPI/src/main/java/com/bencodez/simpleapi/core/config/AnnotationBinder.java create mode 100644 SimpleAPI/src/main/java/com/bencodez/simpleapi/core/config/ConfigurateConfigView.java create mode 100644 SimpleAPI/src/main/java/com/bencodez/simpleapi/core/config/YamlConfigDocument.java create mode 100644 SimpleAPI/src/main/java/com/bencodez/simpleapi/core/package-info.java create mode 100644 SimpleAPI/src/main/java/com/bencodez/simpleapi/core/sql/MysqlConfigView.java create mode 100644 SimpleAPI/src/test/java/com/bencodez/simpleapi/core/CoreClasspathTest.java rename {simpleapi-configurate => SimpleAPI}/src/test/java/com/bencodez/simpleapi/file/config/configurate/ConfigurateConfigViewTest.java (97%) rename {simpleapi-configurate => SimpleAPI}/src/test/java/com/bencodez/simpleapi/file/config/configurate/YamlConfigDocumentTest.java (100%) rename {simpleapi-sql => SimpleAPI}/src/test/java/com/bencodez/simpleapi/sql/mysql/SharedSqlTest.java (72%) create mode 100644 SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/shared/ConfigParityTest.java create mode 100644 SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/shared/NativeConfigFixture.java create mode 100644 SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/shared/PackageCompatibilityTest.java create mode 100644 SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/shared/SharedArtifactTest.java create mode 100644 SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/shared/SharedRuntimeClasspath.java delete mode 100644 pom.xml delete mode 100644 simpleapi-configurate/pom.xml delete mode 100644 simpleapi-core/pom.xml delete mode 100644 simpleapi-core/src/test/java/com/bencodez/simpleapi/core/CoreClasspathTest.java delete mode 100644 simpleapi-sql/pom.xml delete mode 100644 tools/ConfigParityProbe.java delete mode 100644 tools/NativeConfigSmoke.java delete mode 100644 tools/SharedArtifactProbe.java delete mode 100644 tools/verify-shared-artifacts.py diff --git a/SimpleAPI/pom.xml b/SimpleAPI/pom.xml index 0bb2b188..3b9eae8e 100644 --- a/SimpleAPI/pom.xml +++ b/SimpleAPI/pom.xml @@ -19,6 +19,7 @@ 1.85 + ${project.name} src/main/java @@ -68,14 +69,63 @@ org.apache.maven.plugins maven-jar-plugin 3.5.0 - - ${project.name} - - - - org.apache.maven.plugins - maven-surefire-plugin - 3.5.4 + + + shared-library + package + jar + + shared + + com/bencodez/simpleapi/core/** + com/bencodez/simpleapi/file/config/*.class + com/bencodez/simpleapi/file/config/configurate/*.class + com/bencodez/simpleapi/file/annotation/AnnotationBinder*.class + com/bencodez/simpleapi/file/annotation/ConfigData*.class + com/bencodez/simpleapi/time/ParsedDuration*.class + com/bencodez/simpleapi/debug/DebugLevel.class + com/bencodez/simpleapi/sql/Column.class + com/bencodez/simpleapi/sql/DataType.class + com/bencodez/simpleapi/sql/data/*.class + com/bencodez/simpleapi/sql/mysql/AbstractSqlTable*.class + com/bencodez/simpleapi/sql/mysql/ConnectionManager*.class + com/bencodez/simpleapi/sql/mysql/DbType.class + com/bencodez/simpleapi/sql/mysql/MySQL*.class + com/bencodez/simpleapi/sql/mysql/config/MysqlConfig.class + com/bencodez/simpleapi/sql/mysql/config/MysqlConfigView.class + com/bencodez/simpleapi/sql/mysql/queries/*.class + + + + + shared-library-sources + package + jar + + shared-sources + ${project.build.sourceDirectory} + + com/bencodez/simpleapi/core/** + com/bencodez/simpleapi/file/config/*.java + com/bencodez/simpleapi/file/config/configurate/*.java + com/bencodez/simpleapi/file/annotation/AnnotationBinder.java + com/bencodez/simpleapi/file/annotation/ConfigData*.java + com/bencodez/simpleapi/time/ParsedDuration.java + com/bencodez/simpleapi/debug/DebugLevel.java + com/bencodez/simpleapi/sql/Column.java + com/bencodez/simpleapi/sql/DataType.java + com/bencodez/simpleapi/sql/data/*.java + com/bencodez/simpleapi/sql/mysql/AbstractSqlTable.java + com/bencodez/simpleapi/sql/mysql/ConnectionManager.java + com/bencodez/simpleapi/sql/mysql/DbType.java + com/bencodez/simpleapi/sql/mysql/MySQL.java + com/bencodez/simpleapi/sql/mysql/config/MysqlConfig.java + com/bencodez/simpleapi/sql/mysql/config/MysqlConfigView.java + com/bencodez/simpleapi/sql/mysql/queries/*.java + + + + org.apache.maven.plugins @@ -124,6 +174,32 @@ + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.4 + + + default-test + + **/SharedArtifactTest.java + + + + shared-artifact-test + package + test + + **/SharedArtifactTest.java + ${project.build.directory}/shared-artifact-reports + + ${project.build.directory}/${project.build.finalName}-shared.jar + ${project.build.directory}/${project.build.finalName}.jar + + + + + diff --git a/SimpleAPI/src/main/java/com/bencodez/simpleapi/bukkit/config/AnnotationHandler.java b/SimpleAPI/src/main/java/com/bencodez/simpleapi/bukkit/config/AnnotationHandler.java new file mode 100644 index 00000000..218be012 --- /dev/null +++ b/SimpleAPI/src/main/java/com/bencodez/simpleapi/bukkit/config/AnnotationHandler.java @@ -0,0 +1,13 @@ +package com.bencodez.simpleapi.bukkit.config; + +import org.bukkit.configuration.ConfigurationSection; +import com.bencodez.simpleapi.core.config.AnnotationBinder; + +/** Bukkit entry point; native section-typed fields continue receiving the original section. */ +public class AnnotationHandler { + private final AnnotationBinder binder = new AnnotationBinder(view -> ((BukkitConfigView) view).getSection()); + public AnnotationHandler() { } + public void load(ConfigurationSection config, Object target) { + binder.load(config == null ? null : new BukkitConfigView(config), target); + } +} diff --git a/SimpleAPI/src/main/java/com/bencodez/simpleapi/bukkit/config/BukkitConfigView.java b/SimpleAPI/src/main/java/com/bencodez/simpleapi/bukkit/config/BukkitConfigView.java new file mode 100644 index 00000000..fe9fc9ab --- /dev/null +++ b/SimpleAPI/src/main/java/com/bencodez/simpleapi/bukkit/config/BukkitConfigView.java @@ -0,0 +1,29 @@ +package com.bencodez.simpleapi.bukkit.config; + +import java.util.List; +import java.util.Objects; +import java.util.Set; +import org.bukkit.configuration.ConfigurationSection; +import com.bencodez.simpleapi.file.config.ConfigView; + +/** Live Bukkit adapter. Defaults, coercions, native section identity and path rules are delegated. */ +public class BukkitConfigView implements ConfigView { + private final ConfigurationSection section; + public BukkitConfigView(ConfigurationSection section) { this.section = Objects.requireNonNull(section, "section"); } + public ConfigurationSection getSection() { return section; } + @Override public boolean contains(String path) { return section.contains(path); } + @Override public String getString(String path, String fallback) { return section.getString(path, fallback); } + @Override public boolean getBoolean(String path, boolean fallback) { return section.getBoolean(path, fallback); } + @Override public int getInt(String path, int fallback) { return section.getInt(path, fallback); } + @Override public long getLong(String path, long fallback) { return section.getLong(path, fallback); } + @Override public double getDouble(String path, double fallback) { return section.getDouble(path, fallback); } + @Override public List getStringList(String path) { return section.getStringList(path); } + @Override public List getIntegerList(String path) { return section.getIntegerList(path); } + @Override public boolean isConfigurationSection(String path) { return section.isConfigurationSection(path); } + @Override public BukkitConfigView getConfigurationSection(String path) { + ConfigurationSection child = section.getConfigurationSection(path); + return child == null ? null : sectionView(child); + } + protected BukkitConfigView sectionView(ConfigurationSection child) { return new BukkitConfigView(child); } + @Override public Set getKeys(boolean deep) { return section.getKeys(deep); } +} diff --git a/SimpleAPI/src/main/java/com/bencodez/simpleapi/bukkit/package-info.java b/SimpleAPI/src/main/java/com/bencodez/simpleapi/bukkit/package-info.java new file mode 100644 index 00000000..df3f0e5e --- /dev/null +++ b/SimpleAPI/src/main/java/com/bencodez/simpleapi/bukkit/package-info.java @@ -0,0 +1,6 @@ +/** + * Bukkit adapters for the shared implementations. Legacy public entry points + * delegate here without requiring consumers to change imports. Forge, Fabric and + * other loader adapters will get sibling packages only when they are implemented. + */ +package com.bencodez.simpleapi.bukkit; diff --git a/SimpleAPI/src/main/java/com/bencodez/simpleapi/core/config/AnnotationBinder.java b/SimpleAPI/src/main/java/com/bencodez/simpleapi/core/config/AnnotationBinder.java new file mode 100644 index 00000000..789f7787 --- /dev/null +++ b/SimpleAPI/src/main/java/com/bencodez/simpleapi/core/config/AnnotationBinder.java @@ -0,0 +1,311 @@ +package com.bencodez.simpleapi.core.config; + +import com.bencodez.simpleapi.file.annotation.*; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import java.util.Objects; +import java.util.function.Function; + +import com.bencodez.simpleapi.file.config.ConfigView; + +import com.bencodez.simpleapi.time.ParsedDuration; + +/** + * Platform-neutral implementation of the existing configuration annotations. + * + *

This is a compatibility extraction, not a change to the annotation rules. + * Field fallback values, empty-list handling, declared-field-only traversal, + * annotation order and per-field exception isolation are intentionally retained. + * In particular, the legacy zero-default long reflection behavior is unchanged.

+ */ +public class AnnotationBinder { + + private final Function sectionValue; + + /** Creates a binder that assigns ConfigView values to section fields. */ + public AnnotationBinder() { + this(view -> view); + } + + /** + * Creates a binder with a platform-specific section projection. Only the + * compatibility adapter should unwrap a view to a native section. The + * projection is invoked for present sections, never for an absent section. + * + * @param sectionValue projection used for ConfigDataConfigurationSection + */ + public AnnotationBinder(Function sectionValue) { + this.sectionValue = Objects.requireNonNull(sectionValue, "sectionValue"); + } + + @SuppressWarnings("unchecked") + public void load(ConfigView config, Object classToLoad) { + Class clazz = classToLoad.getClass(); + + for (Field field : clazz.getDeclaredFields()) { + try { + field.setAccessible(true); + + ConfigDataString stringAnnotation = field.getAnnotation(ConfigDataString.class); + if (stringAnnotation != null) { + + String defaultValue = stringAnnotation.defaultValue(); + if (defaultValue.isEmpty()) { + try { + String v = (String) field.get(classToLoad); + defaultValue = v; + } catch (Exception e) { + + } + } + String value = ""; + if (!stringAnnotation.secondPath().isEmpty()) { + value = config.getString(stringAnnotation.path(), + config.getString(stringAnnotation.secondPath(), defaultValue)); + } else { + value = config.getString(stringAnnotation.path(), defaultValue); + } + + field.set(classToLoad, value); + + } + + ConfigDataBoolean booleanAnnotation = field.getAnnotation(ConfigDataBoolean.class); + if (booleanAnnotation != null) { + boolean defaultValue = booleanAnnotation.defaultValue(); + if (!defaultValue) { + try { + boolean v = field.getBoolean(classToLoad); + defaultValue = v; + } catch (Exception e) { + + } + + } + + boolean value = defaultValue; + if (config.contains(booleanAnnotation.path())) { + value = config.getBoolean(booleanAnnotation.path(), defaultValue); + } else if (!booleanAnnotation.secondPath().isEmpty() + && config.contains(booleanAnnotation.secondPath())) { + value = config.getBoolean(booleanAnnotation.secondPath(), defaultValue); + + if (booleanAnnotation.secondPathInvert()) { + value = !value; + } + } else { + value = config.getBoolean(booleanAnnotation.path(), defaultValue); + } + + field.set(classToLoad, value); + } + + ConfigDataInt intAnnotation = field.getAnnotation(ConfigDataInt.class); + if (intAnnotation != null) { + int defaultValue = intAnnotation.defaultValue(); + if (defaultValue == 0) { + try { + int v = field.getInt(classToLoad); + defaultValue = v; + } catch (Exception e) { + + } + } + int value = 0; + if (!intAnnotation.secondPath().isEmpty()) { + value = config.getInt(intAnnotation.path(), + config.getInt(intAnnotation.secondPath(), defaultValue)); + } else { + value = config.getInt(intAnnotation.path(), defaultValue); + } + + field.set(classToLoad, value); + } + + ConfigDataLong longAnnotation = field.getAnnotation(ConfigDataLong.class); + if (longAnnotation != null) { + long defaultValue = longAnnotation.defaultValue(); + if (defaultValue == 0) { + try { + int v = field.getInt(classToLoad); + defaultValue = v; + } catch (Exception e) { + + } + } + long value = 0; + if (!longAnnotation.secondPath().isEmpty()) { + value = config.getLong(longAnnotation.path(), + config.getLong(longAnnotation.secondPath(), defaultValue)); + } else { + value = config.getLong(longAnnotation.path(), defaultValue); + } + + field.set(classToLoad, value); + } + + ConfigDataDouble doubleAnnotation = field.getAnnotation(ConfigDataDouble.class); + if (doubleAnnotation != null) { + double defaultValue = doubleAnnotation.defaultValue(); + if (defaultValue == 0) { + try { + double v = field.getDouble(classToLoad); + defaultValue = v; + } catch (Exception e) { + + } + } + double value = 0; + if (!doubleAnnotation.secondPath().isEmpty()) { + value = config.getDouble(doubleAnnotation.path(), + config.getDouble(doubleAnnotation.secondPath(), defaultValue)); + } else { + value = config.getDouble(doubleAnnotation.path(), defaultValue); + } + + field.set(classToLoad, value); + } + + ConfigDataListString listAnnotation = field.getAnnotation(ConfigDataListString.class); + if (listAnnotation != null) { + ArrayList defaultValue = new ArrayList<>(); + try { + ArrayList v = (ArrayList) field.get(classToLoad); + defaultValue = v; + } catch (Exception e) { + + } + + List list = config.getStringList(listAnnotation.path()); + + if (list.isEmpty()) { + list = config.getStringList(listAnnotation.secondPath()); + } + + ArrayList list1 = new ArrayList<>(list); + // use default value + if (list.isEmpty()) { + list1 = defaultValue; + } + + field.set(classToLoad, list1); + + /* + * ArrayList value = null; if (!listAnnotation.secondPath().isEmpty()) { + * value = (ArrayList) config.getList(listAnnotation.path(), + * config.getList(listAnnotation.secondPath(), defaultValue)); } else { value = + * (ArrayList) config.getList(listAnnotation.path(), defaultValue); } + * + * field.set(classToLoad, value); + */ + } + + ConfigDataListInt intListAnnotation = field.getAnnotation(ConfigDataListInt.class); + if (intListAnnotation != null) { + ArrayList defaultValue = new ArrayList<>(); + try { + ArrayList v = (ArrayList) field.get(classToLoad); + defaultValue = v; + } catch (Exception e) { + + } + + List list = config.getIntegerList(intListAnnotation.path()); + + if (list.isEmpty()) { + list = config.getIntegerList(intListAnnotation.secondPath()); + } + + ArrayList list1 = new ArrayList<>(list); + // use default value + if (list.isEmpty()) { + list1 = defaultValue; + } + + field.set(classToLoad, list1); + + /* + * ArrayList value = null; if (!listAnnotation.secondPath().isEmpty()) { + * value = (ArrayList) config.getList(listAnnotation.path(), + * config.getList(listAnnotation.secondPath(), defaultValue)); } else { value = + * (ArrayList) config.getList(listAnnotation.path(), defaultValue); } + * + * field.set(classToLoad, value); + */ + } + + ConfigDataKeys setAnnotation = field.getAnnotation(ConfigDataKeys.class); + if (setAnnotation != null) { + Set value = new HashSet<>(); + if (config.isConfigurationSection(setAnnotation.path())) { + value = config.getConfigurationSection(setAnnotation.path()).getKeys(false); + } else if (config.isConfigurationSection(setAnnotation.secondPath()) + && setAnnotation.secondPath().length() > 0) { + value = config.getConfigurationSection(setAnnotation.secondPath()).getKeys(false); + } + if (value != null) { + field.set(classToLoad, value); + } + } + + ConfigDataConfigurationSection confAnnotation = field + .getAnnotation(ConfigDataConfigurationSection.class); + if (confAnnotation != null) { + ConfigView value = null; + if (config.isConfigurationSection(confAnnotation.path())) { + value = config.getConfigurationSection(confAnnotation.path()); + } else if (config.isConfigurationSection(confAnnotation.secondPath()) + && !confAnnotation.secondPath().isEmpty()) { + value = config.getConfigurationSection(confAnnotation.secondPath()); + } + + field.set(classToLoad, value == null ? null : sectionValue.apply(value)); + } + + ConfigDataParsedDuration durationAnnotation = field.getAnnotation(ConfigDataParsedDuration.class); + if (durationAnnotation != null) { + + String defaultValue = durationAnnotation.defaultValue(); + + if (defaultValue.isEmpty()) { + try { + Object v = field.get(classToLoad); + if (v != null) { + defaultValue = v.toString(); + } + } catch (Exception e) { + + } + } + + String value = ""; + + if (!durationAnnotation.secondPath().isEmpty()) { + value = config.getString(durationAnnotation.path(), + config.getString(durationAnnotation.secondPath(), defaultValue)); + } else { + value = config.getString(durationAnnotation.path(), defaultValue); + } + + try { + // Assumes ParsedDuration has a constructor or static parse method + Object parsedDuration = ParsedDuration.parse(value, durationAnnotation.defaultTimeUnit()); + field.set(classToLoad, parsedDuration); + } catch (Exception e) { + e.printStackTrace(); + } + } + + } catch (Exception e) { + e.printStackTrace(); + } + } + + } + +} diff --git a/SimpleAPI/src/main/java/com/bencodez/simpleapi/core/config/ConfigurateConfigView.java b/SimpleAPI/src/main/java/com/bencodez/simpleapi/core/config/ConfigurateConfigView.java new file mode 100644 index 00000000..61553957 --- /dev/null +++ b/SimpleAPI/src/main/java/com/bencodez/simpleapi/core/config/ConfigurateConfigView.java @@ -0,0 +1,143 @@ +package com.bencodez.simpleapi.core.config; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.regex.Pattern; + +import org.spongepowered.configurate.ConfigurationNode; + +import com.bencodez.simpleapi.file.config.ConfigView; + +/** + * Read-only view of a Configurate section. Scalar numbers/booleans are read + * strictly, rather than using Configurate's more permissive string coercions. + * This matches the Bukkit scalar/list rules used by the existing binder. + * + *

Paths use '.' by default; at(String...) uses literal segments for identifiers + * containing dots. An externally supplied node is a live view and the caller must + * coordinate its mutation. YamlConfigDocument supplies detached snapshots.

+ * + *

Getter/annotation defaults are supported. Bukkit's mutable default-section + * overlay and native serialized Bukkit objects are deliberately not emulated.

+ */ +public class ConfigurateConfigView implements ConfigView { + protected final ConfigurationNode node; + private final char separator; + + public ConfigurateConfigView(ConfigurationNode node) { this(node, '.'); } + + public ConfigurateConfigView(ConfigurationNode node, char separator) { + this.node = Objects.requireNonNull(node, "node"); + this.separator = separator; + } + + /** Shares the same read view; used only by legacy package facades. */ + protected ConfigurateConfigView(ConfigurateConfigView source) { + this(source.node, source.separator); + } + + /** Factory keeps covariant legacy section views in their original package. */ + protected ConfigurateConfigView sectionView(ConfigurationNode child) { + return new ConfigurateConfigView(child, separator); + } + + protected final String[] segments(String path) { + Objects.requireNonNull(path, "path"); + return path.isEmpty() ? new String[0] : path.split(Pattern.quote(String.valueOf(separator)), -1); + } + + protected final ConfigurationNode resolve(String... keys) { + Objects.requireNonNull(keys, "keys"); + ConfigurationNode current = node; + for (String key : keys) { + Objects.requireNonNull(key, "key"); + ConfigurationNode match = null; + // YAML numeric keys remain addressable as strings, without replacing + // them with a second, differently typed key when an editor writes. + for (Map.Entry entry : current.childrenMap().entrySet()) { + if (String.valueOf(entry.getKey()).equals(key)) { + if (match != null) throw new IllegalArgumentException("Ambiguous configuration key"); + match = entry.getValue(); + } + } + current = match == null ? current.node(key) : match; + } + return current; + } + + /** Returns a section using literal key segments, or null for an absent/non-map node. */ + public ConfigurateConfigView at(String... keys) { + ConfigurationNode child = resolve(keys); + return child.isMap() || child == node && child.isNull() + ? sectionView(child) : null; + } + + @Override public boolean contains(String path) { + return path.isEmpty() || !resolve(segments(path)).isNull(); + } + @Override public String getString(String path, String fallback) { + ConfigurationNode child = resolve(segments(path)); + if (child.isMap()) return fallback; // no Bukkit section toString emulation + Object value = child.raw(); + return value == null ? fallback : value.toString(); + } + @Override public boolean getBoolean(String path, boolean fallback) { + Object value = resolve(segments(path)).rawScalar(); + return value instanceof Boolean bool ? bool : fallback; + } + @Override public int getInt(String path, int fallback) { + Object value = resolve(segments(path)).rawScalar(); + return value instanceof Number number ? number.intValue() : fallback; + } + @Override public long getLong(String path, long fallback) { + Object value = resolve(segments(path)).rawScalar(); + return value instanceof Number number ? number.longValue() : fallback; + } + @Override public double getDouble(String path, double fallback) { + Object value = resolve(segments(path)).rawScalar(); + return value instanceof Number number ? number.doubleValue() : fallback; + } + @Override public List getStringList(String path) { + List result = new ArrayList<>(); + for (ConfigurationNode child : resolve(segments(path)).childrenList()) { + Object value = child.rawScalar(); + if (value instanceof String || value instanceof Number || value instanceof Boolean || value instanceof Character) + result.add(value.toString()); + } + return result; + } + @Override public List getIntegerList(String path) { + List result = new ArrayList<>(); + for (ConfigurationNode child : resolve(segments(path)).childrenList()) { + Object value = child.rawScalar(); + if (value instanceof Number number) result.add(number.intValue()); + else if (value instanceof Character character) result.add((int) character); + else if (value instanceof String string) { + try { result.add(Integer.parseInt(string)); } catch (NumberFormatException ignored) { } + } + } + return result; + } + @Override public boolean isConfigurationSection(String path) { return at(segments(path)) != null; } + @Override public ConfigurateConfigView getConfigurationSection(String path) { return at(segments(path)); } + @Override public Set getKeys(boolean deep) { + LinkedHashSet result = new LinkedHashSet<>(); + collectKeys(node, "", deep, result, 0); + return result; + } + private void collectKeys(ConfigurationNode parent, String prefix, boolean deep, Set keys, int depth) { + if (depth > 64) throw new IllegalArgumentException("Configuration nesting is too deep"); + Set siblings = new LinkedHashSet<>(); + for (Map.Entry entry : parent.childrenMap().entrySet()) { + String key = String.valueOf(entry.getKey()); + if (!siblings.add(key)) throw new IllegalArgumentException("Ambiguous configuration key"); + String path = prefix + key; + keys.add(path); + if (deep && entry.getValue().isMap()) collectKeys(entry.getValue(), path + separator, true, keys, depth + 1); + } + } +} diff --git a/SimpleAPI/src/main/java/com/bencodez/simpleapi/core/config/YamlConfigDocument.java b/SimpleAPI/src/main/java/com/bencodez/simpleapi/core/config/YamlConfigDocument.java new file mode 100644 index 00000000..f1b42e0c --- /dev/null +++ b/SimpleAPI/src/main/java/com/bencodez/simpleapi/core/config/YamlConfigDocument.java @@ -0,0 +1,270 @@ +package com.bencodez.simpleapi.core.config; + +import java.io.BufferedWriter; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.charset.CodingErrorAction; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.PosixFilePermission; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.HexFormat; +import java.util.IdentityHashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Consumer; + +import org.spongepowered.configurate.ConfigurationNode; +import org.spongepowered.configurate.yaml.NodeStyle; +import org.spongepowered.configurate.yaml.YamlConfigurationLoader; + +import com.bencodez.simpleapi.file.config.ConfigDocument; +import com.bencodez.simpleapi.file.config.ConfigEditor; +import com.bencodez.simpleapi.file.config.ConfigSnapshot; + +/** + * YAML document for administrator-owned local configuration. No file is created + * by open/reload. Updates use private copies, content revisions, a bounded staged + * write and an atomic replacement; there is no truncate-in-place fallback. + * + *

The parent directory must exist and be trusted. Target symlinks/non-regular + * files are rejected. The caller owns exclusive cross-process writes: revision + * checks detect observed edits but cannot make an atomic filesystem compare/swap. + * File bytes are forced before rename; power-loss durability of directory entries + * and preservation of non-POSIX ACLs are not promised.

+ * + *

Parsing uses Configurate's YAML loader and its parser policies. This is not + * a hostile YAML upload endpoint. Formatting/inline comments and native Bukkit + * serialized objects are not a lossless migration contract. Existing Bukkit and + * Velocity file implementations are not changed by this class.

+ */ +public final class YamlConfigDocument implements ConfigDocument { + public static final int DEFAULT_MAX_BYTES = 1024 * 1024; + private static final int MAX_NODES = 100_000; + private static final int MAX_DEPTH = 64; + private final Path path; + private final int maxBytes; + private ConfigurationNode root; + private String revision; + private boolean editing; + + private YamlConfigDocument(Path path, int maxBytes) { + this.path = path; + this.maxBytes = maxBytes; + } + + public static YamlConfigDocument open(Path path) throws IOException { return open(path, DEFAULT_MAX_BYTES); } + + public static YamlConfigDocument open(Path path, int maxBytes) throws IOException { + Objects.requireNonNull(path, "path"); + if (maxBytes < 1 || maxBytes > 16 * DEFAULT_MAX_BYTES) + throw new IllegalArgumentException("maxBytes must be between 1 byte and 16 MiB"); + Path absolute = path.toAbsolutePath().normalize(); + if (absolute.getFileName() == null) throw new IOException("A configuration filename is required"); + // Resolve the trusted parent once. Never create parent directories as a side effect of reading. + Path canonical = absolute.getParent().toRealPath().resolve(absolute.getFileName()); + YamlConfigDocument document = new YamlConfigDocument(canonical, maxBytes); + document.reload(); + return document; + } + + @Override public Path path() { return path; } + + @Override public synchronized ConfigSnapshot snapshot() { + return new ConfigSnapshot(revision, new ConfigurateConfigView(root.copy())); + } + + @Override public synchronized ConfigSnapshot reload() throws IOException { + if (editing) throw new IllegalStateException("Cannot reload inside an edit callback"); + byte[] bytes = readCurrent(); + ConfigurationNode candidate = parse(bytes == null ? new byte[0] : bytes); + ConfigSnapshot result = new ConfigSnapshot(revisionOf(bytes), new ConfigurateConfigView(candidate.copy())); + root = candidate; + revision = result.revision(); + return result; + } + + @Override public synchronized ConfigSnapshot update(String expectedRevision, Consumer edit) throws IOException { + Objects.requireNonNull(expectedRevision, "expectedRevision"); + Objects.requireNonNull(edit, "edit"); + if (editing) throw new IllegalStateException("Cannot nest document updates"); + if (!revision.equals(expectedRevision)) throw new IOException("Stale in-memory configuration revision"); + requireUnchanged(); + editing = true; + Editor editor = new Editor(root.copy()); + try { + try { edit.accept(editor); } finally { editor.active = false; } + validate(editor.node, 0, new int[1]); + ConfigurationNode candidate = editor.node.copy(); + byte[] encoded = serialize(candidate); + String nextRevision = revisionOf(encoded); + ConfigSnapshot result = new ConfigSnapshot(nextRevision, new ConfigurateConfigView(candidate.copy())); + replace(encoded); + root = candidate; + revision = nextRevision; + return result; + } finally { + editing = false; + } + } + + private void requireUnchanged() throws IOException { + if (!revision.equals(revisionOf(readCurrent()))) + throw new IOException("Configuration changed on disk; reload before applying an edit"); + } + + private byte[] readCurrent() throws IOException { + BasicFileAttributes attributes; + try { attributes = Files.readAttributes(path, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS); } + catch (NoSuchFileException missing) { return null; } + if (!attributes.isRegularFile() || attributes.isSymbolicLink()) + throw new IOException("Configuration target must be a regular file, not a symbolic link"); + if (attributes.size() > maxBytes) throw new IOException("Configuration exceeds byte limit"); + try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ, LinkOption.NOFOLLOW_LINKS)) { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + ByteBuffer buffer = ByteBuffer.allocate(8192); + while (channel.read(buffer) != -1) { + buffer.flip(); + int count = buffer.remaining(); + if (count > maxBytes - bytes.size()) throw new IOException("Configuration exceeds byte limit"); + bytes.write(buffer.array(), 0, count); + buffer.clear(); + } + return bytes.toByteArray(); + } + } + + private ConfigurationNode parse(byte[] bytes) throws IOException { + String text = StandardCharsets.UTF_8.newDecoder().onMalformedInput(CodingErrorAction.REPORT) + .onUnmappableCharacter(CodingErrorAction.REPORT).decode(ByteBuffer.wrap(bytes)).toString(); + ConfigurationNode candidate = YamlConfigurationLoader.builder().buildAndLoadString(text); + if (candidate.isNull()) candidate.raw(new LinkedHashMap()); + if (!candidate.isMap()) throw new IOException("Configuration root must be a mapping"); + try { validate(candidate, 0, new int[1]); } + catch (IllegalArgumentException invalid) { throw new IOException("Unsupported configuration structure", invalid); } + return candidate; + } + + private byte[] serialize(ConfigurationNode candidate) throws IOException { + LimitedOutput output = new LimitedOutput(maxBytes); + try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(output, StandardCharsets.UTF_8))) { + YamlConfigurationLoader.builder().nodeStyle(NodeStyle.BLOCK).sink(() -> writer).build().save(candidate); + } + return output.bytes.toByteArray(); + } + + private void replace(byte[] encoded) throws IOException { + requireUnchanged(); + Set permissions = null; + if (Files.getFileAttributeView(path.getParent(), java.nio.file.attribute.PosixFileAttributeView.class) != null) { + permissions = revision.equals("missing") + ? Set.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE) + : Files.getPosixFilePermissions(path, LinkOption.NOFOLLOW_LINKS); + } + Path temporary = Files.createTempFile(path.getParent(), ".simpleapi-config-", ".tmp"); + try { + if (permissions != null) Files.setPosixFilePermissions(temporary, permissions); + try (FileChannel channel = FileChannel.open(temporary, StandardOpenOption.WRITE, LinkOption.NOFOLLOW_LINKS)) { + ByteBuffer buffer = ByteBuffer.wrap(encoded); + while (buffer.hasRemaining()) channel.write(buffer); + channel.force(true); + } + requireUnchanged(); + Files.move(temporary, path, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); + } finally { + Files.deleteIfExists(temporary); + } + } + + private static String revisionOf(byte[] bytes) { + if (bytes == null) return "missing"; + try { return "sha256:" + HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(bytes)); } + catch (NoSuchAlgorithmException impossible) { throw new IllegalStateException("SHA-256 unavailable", impossible); } + } + + private static void validate(ConfigurationNode node, int depth, int[] count) { + if (depth > MAX_DEPTH || ++count[0] > MAX_NODES) throw new IllegalArgumentException("Configuration structure exceeds limits"); + if (node.isMap()) { + Set keys = new LinkedHashSet<>(); + for (Map.Entry entry : node.childrenMap().entrySet()) { + Object key = entry.getKey(); + if (!(key instanceof String || key instanceof Number || key instanceof Boolean || key instanceof Character) + || !keys.add(key.toString())) throw new IllegalArgumentException("Unsupported or ambiguous configuration key"); + validate(entry.getValue(), depth + 1, count); + } + } else if (node.isList()) { + for (ConfigurationNode child : node.childrenList()) validate(child, depth + 1, count); + } + } + + private static Object copyValue(Object value, int depth, int[] count, IdentityHashMap ancestors) { + if (depth > MAX_DEPTH || ++count[0] > MAX_NODES) throw new IllegalArgumentException("Configuration value exceeds structure limits"); + if (value == null || value instanceof String || value instanceof Boolean + || value instanceof Byte || value instanceof Short || value instanceof Integer || value instanceof Long + || value instanceof Float || value instanceof Double || value instanceof java.math.BigInteger + || value instanceof java.math.BigDecimal) return value; + if (value instanceof Character character) return character.toString(); + if (ancestors.put(value, Boolean.TRUE) != null) throw new IllegalArgumentException("Cyclic configuration values are not supported"); + try { + if (value instanceof Map map) { + Map copy = new LinkedHashMap<>(); + for (Map.Entry entry : map.entrySet()) { + if (!(entry.getKey() instanceof String key)) throw new IllegalArgumentException("Edited map keys must be strings"); + copy.put(key, copyValue(entry.getValue(), depth + 1, count, ancestors)); + } + return copy; + } + if (value instanceof List list) { + List copy = new ArrayList<>(); + for (Object child : list) copy.add(copyValue(child, depth + 1, count, ancestors)); + return copy; + } + throw new IllegalArgumentException("Only scalar, list and string-keyed map values are supported"); + } finally { ancestors.remove(value); } + } + + private static final class Editor extends ConfigurateConfigView implements ConfigEditor { + private final Thread owner = Thread.currentThread(); + private boolean active = true; + private Editor(ConfigurationNode node) { super(node); } + @Override public void set(String path, Object value) { setAt(value, segments(path)); } + @Override public void setAt(Object value, String... keys) { + if (!active || Thread.currentThread() != owner) throw new IllegalStateException("Editor is no longer active on this thread"); + Objects.requireNonNull(keys, "keys"); + if (keys.length == 0 || keys.length > MAX_DEPTH) throw new IllegalArgumentException("A nonempty bounded key path is required"); + for (String key : keys) if (key == null || key.isEmpty()) throw new IllegalArgumentException("Empty/null key segment"); + Object copy = copyValue(value, 0, new int[1], new IdentityHashMap<>()); + resolve(keys).raw(copy); + } + } + + private static final class LimitedOutput extends OutputStream { + private final int limit; + private final ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + private LimitedOutput(int limit) { this.limit = limit; } + @Override public void write(int value) throws IOException { + if (bytes.size() >= limit) throw new IOException("Serialized configuration exceeds byte limit"); + bytes.write(value); + } + @Override public void write(byte[] value, int offset, int length) throws IOException { + if (length > limit - bytes.size()) throw new IOException("Serialized configuration exceeds byte limit"); + bytes.write(value, offset, length); + } + } +} diff --git a/SimpleAPI/src/main/java/com/bencodez/simpleapi/core/package-info.java b/SimpleAPI/src/main/java/com/bencodez/simpleapi/core/package-info.java new file mode 100644 index 00000000..edb47c11 --- /dev/null +++ b/SimpleAPI/src/main/java/com/bencodez/simpleapi/core/package-info.java @@ -0,0 +1,7 @@ +/** + * Platform-independent implementations for the shared artifact. This package must + * not reference Bukkit, proxy, Minecraft or mod-loader APIs. Existing public + * configuration contracts, annotations and value types retain their old package + * names for compatibility; they are selected explicitly into the shared JAR. + */ +package com.bencodez.simpleapi.core; diff --git a/SimpleAPI/src/main/java/com/bencodez/simpleapi/core/sql/MysqlConfigView.java b/SimpleAPI/src/main/java/com/bencodez/simpleapi/core/sql/MysqlConfigView.java new file mode 100644 index 00000000..c807ea36 --- /dev/null +++ b/SimpleAPI/src/main/java/com/bencodez/simpleapi/core/sql/MysqlConfigView.java @@ -0,0 +1,40 @@ +package com.bencodez.simpleapi.core.sql; + +import com.bencodez.simpleapi.sql.mysql.config.MysqlConfig; + +import java.util.Objects; +import com.bencodez.simpleapi.file.config.ConfigView; +import com.bencodez.simpleapi.sql.mysql.DbType; + +/** Platform-neutral snapshot of the existing MySQL section keys and defaults. */ +public class MysqlConfigView extends MysqlConfig { + public MysqlConfigView(ConfigView section) { + Objects.requireNonNull(section, "section"); + setTablePrefix(section.getString("Prefix", null)); + String tableName = section.getString("Name", ""); + if (tableName != null && !tableName.isEmpty()) setTableName(tableName); + setHostName(section.getString("Host", null)); + setPort(section.getInt("Port", 0)); + setUser(section.getString("Username", null)); + setPass(section.getString("Password", null)); + setDatabase(section.getString("Database", null)); + setLifeTime(section.getLong("MaxLifeTime", -1)); + setMaxThreads(Math.max(1, section.getInt("MaxConnections", 1))); + setMinimumIdle(section.getInt("MinimumIdle", 2)); + setIdleTimeoutMs(section.getLong("IdleTimeoutMs", 10 * 60_000L)); + setKeepaliveMs(section.getLong("KeepaliveMs", 5 * 60_000L)); + setValidationMs(section.getLong("ValidationMs", 5_000L)); + setLeakDetectMs(section.getLong("LeakDetectMs", 20_000L)); + setConnectionTimeout(section.getInt("ConnectionTimeout", 50_000)); + String type = section.getString("DbType", ""); + setDbType(type != null && !type.isEmpty() ? DbType.fromString(type) + : section.getBoolean("UseMariaDB", false) ? DbType.MARIADB : DbType.MYSQL); + setDriver(section.getString("Driver", "")); + setUseSSL(section.getBoolean("UseSSL", false)); + setPublicKeyRetrieval(section.getBoolean("PublicKeyRetrieval", false)); + setUseMariaDB(section.getBoolean("UseMariaDB", false)); + setLine(section.getString("Line", "")); + setDebug(section.getBoolean("Debug", false)); + setPoolName(section.getString("PoolName", "")); + } +} diff --git a/SimpleAPI/src/main/java/com/bencodez/simpleapi/file/annotation/AnnotationBinder.java b/SimpleAPI/src/main/java/com/bencodez/simpleapi/file/annotation/AnnotationBinder.java index 682ebedc..d85bf519 100644 --- a/SimpleAPI/src/main/java/com/bencodez/simpleapi/file/annotation/AnnotationBinder.java +++ b/SimpleAPI/src/main/java/com/bencodez/simpleapi/file/annotation/AnnotationBinder.java @@ -1,309 +1,11 @@ package com.bencodez.simpleapi.file.annotation; -import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import java.util.Objects; import java.util.function.Function; - import com.bencodez.simpleapi.file.config.ConfigView; -import com.bencodez.simpleapi.time.ParsedDuration; - -/** - * Platform-neutral implementation of the existing configuration annotations. - * - *

This is a compatibility extraction, not a change to the annotation rules. - * Field fallback values, empty-list handling, declared-field-only traversal, - * annotation order and per-field exception isolation are intentionally retained. - * In particular, the legacy zero-default long reflection behavior is unchanged.

- */ -public class AnnotationBinder { - - private final Function sectionValue; - - /** Creates a binder that assigns ConfigView values to section fields. */ - public AnnotationBinder() { - this(view -> view); - } - - /** - * Creates a binder with a platform-specific section projection. Only the - * compatibility adapter should unwrap a view to a native section. The - * projection is invoked for present sections, never for an absent section. - * - * @param sectionValue projection used for ConfigDataConfigurationSection - */ - public AnnotationBinder(Function sectionValue) { - this.sectionValue = Objects.requireNonNull(sectionValue, "sectionValue"); - } - - @SuppressWarnings("unchecked") - public void load(ConfigView config, Object classToLoad) { - Class clazz = classToLoad.getClass(); - - for (Field field : clazz.getDeclaredFields()) { - try { - field.setAccessible(true); - - ConfigDataString stringAnnotation = field.getAnnotation(ConfigDataString.class); - if (stringAnnotation != null) { - - String defaultValue = stringAnnotation.defaultValue(); - if (defaultValue.isEmpty()) { - try { - String v = (String) field.get(classToLoad); - defaultValue = v; - } catch (Exception e) { - - } - } - String value = ""; - if (!stringAnnotation.secondPath().isEmpty()) { - value = config.getString(stringAnnotation.path(), - config.getString(stringAnnotation.secondPath(), defaultValue)); - } else { - value = config.getString(stringAnnotation.path(), defaultValue); - } - - field.set(classToLoad, value); - - } - - ConfigDataBoolean booleanAnnotation = field.getAnnotation(ConfigDataBoolean.class); - if (booleanAnnotation != null) { - boolean defaultValue = booleanAnnotation.defaultValue(); - if (!defaultValue) { - try { - boolean v = field.getBoolean(classToLoad); - defaultValue = v; - } catch (Exception e) { - - } - - } - - boolean value = defaultValue; - if (config.contains(booleanAnnotation.path())) { - value = config.getBoolean(booleanAnnotation.path(), defaultValue); - } else if (!booleanAnnotation.secondPath().isEmpty() - && config.contains(booleanAnnotation.secondPath())) { - value = config.getBoolean(booleanAnnotation.secondPath(), defaultValue); - - if (booleanAnnotation.secondPathInvert()) { - value = !value; - } - } else { - value = config.getBoolean(booleanAnnotation.path(), defaultValue); - } - - field.set(classToLoad, value); - } - - ConfigDataInt intAnnotation = field.getAnnotation(ConfigDataInt.class); - if (intAnnotation != null) { - int defaultValue = intAnnotation.defaultValue(); - if (defaultValue == 0) { - try { - int v = field.getInt(classToLoad); - defaultValue = v; - } catch (Exception e) { - - } - } - int value = 0; - if (!intAnnotation.secondPath().isEmpty()) { - value = config.getInt(intAnnotation.path(), - config.getInt(intAnnotation.secondPath(), defaultValue)); - } else { - value = config.getInt(intAnnotation.path(), defaultValue); - } - - field.set(classToLoad, value); - } - - ConfigDataLong longAnnotation = field.getAnnotation(ConfigDataLong.class); - if (longAnnotation != null) { - long defaultValue = longAnnotation.defaultValue(); - if (defaultValue == 0) { - try { - int v = field.getInt(classToLoad); - defaultValue = v; - } catch (Exception e) { - - } - } - long value = 0; - if (!longAnnotation.secondPath().isEmpty()) { - value = config.getLong(longAnnotation.path(), - config.getLong(longAnnotation.secondPath(), defaultValue)); - } else { - value = config.getLong(longAnnotation.path(), defaultValue); - } - - field.set(classToLoad, value); - } - - ConfigDataDouble doubleAnnotation = field.getAnnotation(ConfigDataDouble.class); - if (doubleAnnotation != null) { - double defaultValue = doubleAnnotation.defaultValue(); - if (defaultValue == 0) { - try { - double v = field.getDouble(classToLoad); - defaultValue = v; - } catch (Exception e) { - - } - } - double value = 0; - if (!doubleAnnotation.secondPath().isEmpty()) { - value = config.getDouble(doubleAnnotation.path(), - config.getDouble(doubleAnnotation.secondPath(), defaultValue)); - } else { - value = config.getDouble(doubleAnnotation.path(), defaultValue); - } - - field.set(classToLoad, value); - } - - ConfigDataListString listAnnotation = field.getAnnotation(ConfigDataListString.class); - if (listAnnotation != null) { - ArrayList defaultValue = new ArrayList<>(); - try { - ArrayList v = (ArrayList) field.get(classToLoad); - defaultValue = v; - } catch (Exception e) { - - } - - List list = config.getStringList(listAnnotation.path()); - - if (list.isEmpty()) { - list = config.getStringList(listAnnotation.secondPath()); - } - - ArrayList list1 = new ArrayList<>(list); - // use default value - if (list.isEmpty()) { - list1 = defaultValue; - } - - field.set(classToLoad, list1); - - /* - * ArrayList value = null; if (!listAnnotation.secondPath().isEmpty()) { - * value = (ArrayList) config.getList(listAnnotation.path(), - * config.getList(listAnnotation.secondPath(), defaultValue)); } else { value = - * (ArrayList) config.getList(listAnnotation.path(), defaultValue); } - * - * field.set(classToLoad, value); - */ - } - - ConfigDataListInt intListAnnotation = field.getAnnotation(ConfigDataListInt.class); - if (intListAnnotation != null) { - ArrayList defaultValue = new ArrayList<>(); - try { - ArrayList v = (ArrayList) field.get(classToLoad); - defaultValue = v; - } catch (Exception e) { - - } - - List list = config.getIntegerList(intListAnnotation.path()); - - if (list.isEmpty()) { - list = config.getIntegerList(intListAnnotation.secondPath()); - } - - ArrayList list1 = new ArrayList<>(list); - // use default value - if (list.isEmpty()) { - list1 = defaultValue; - } - - field.set(classToLoad, list1); - - /* - * ArrayList value = null; if (!listAnnotation.secondPath().isEmpty()) { - * value = (ArrayList) config.getList(listAnnotation.path(), - * config.getList(listAnnotation.secondPath(), defaultValue)); } else { value = - * (ArrayList) config.getList(listAnnotation.path(), defaultValue); } - * - * field.set(classToLoad, value); - */ - } - - ConfigDataKeys setAnnotation = field.getAnnotation(ConfigDataKeys.class); - if (setAnnotation != null) { - Set value = new HashSet<>(); - if (config.isConfigurationSection(setAnnotation.path())) { - value = config.getConfigurationSection(setAnnotation.path()).getKeys(false); - } else if (config.isConfigurationSection(setAnnotation.secondPath()) - && setAnnotation.secondPath().length() > 0) { - value = config.getConfigurationSection(setAnnotation.secondPath()).getKeys(false); - } - if (value != null) { - field.set(classToLoad, value); - } - } - - ConfigDataConfigurationSection confAnnotation = field - .getAnnotation(ConfigDataConfigurationSection.class); - if (confAnnotation != null) { - ConfigView value = null; - if (config.isConfigurationSection(confAnnotation.path())) { - value = config.getConfigurationSection(confAnnotation.path()); - } else if (config.isConfigurationSection(confAnnotation.secondPath()) - && !confAnnotation.secondPath().isEmpty()) { - value = config.getConfigurationSection(confAnnotation.secondPath()); - } - - field.set(classToLoad, value == null ? null : sectionValue.apply(value)); - } - - ConfigDataParsedDuration durationAnnotation = field.getAnnotation(ConfigDataParsedDuration.class); - if (durationAnnotation != null) { - - String defaultValue = durationAnnotation.defaultValue(); - - if (defaultValue.isEmpty()) { - try { - Object v = field.get(classToLoad); - if (v != null) { - defaultValue = v.toString(); - } - } catch (Exception e) { - - } - } - - String value = ""; - - if (!durationAnnotation.secondPath().isEmpty()) { - value = config.getString(durationAnnotation.path(), - config.getString(durationAnnotation.secondPath(), defaultValue)); - } else { - value = config.getString(durationAnnotation.path(), defaultValue); - } - - try { - // Assumes ParsedDuration has a constructor or static parse method - Object parsedDuration = ParsedDuration.parse(value, durationAnnotation.defaultTimeUnit()); - field.set(classToLoad, parsedDuration); - } catch (Exception e) { - e.printStackTrace(); - } - } - - } catch (Exception e) { - e.printStackTrace(); - } - } - - } - +/** Compatibility name for the shared binder. Existing annotations and rules are unchanged. */ +public class AnnotationBinder extends com.bencodez.simpleapi.core.config.AnnotationBinder { + public AnnotationBinder() { super(); } + public AnnotationBinder(Function sectionValue) { super(sectionValue); } + @Override public void load(ConfigView config, Object target) { super.load(config, target); } } diff --git a/SimpleAPI/src/main/java/com/bencodez/simpleapi/file/annotation/AnnotationHandler.java b/SimpleAPI/src/main/java/com/bencodez/simpleapi/file/annotation/AnnotationHandler.java index 551b7dd5..b1ca1cba 100644 --- a/SimpleAPI/src/main/java/com/bencodez/simpleapi/file/annotation/AnnotationHandler.java +++ b/SimpleAPI/src/main/java/com/bencodez/simpleapi/file/annotation/AnnotationHandler.java @@ -2,26 +2,8 @@ import org.bukkit.configuration.ConfigurationSection; -import com.bencodez.simpleapi.file.config.bukkit.BukkitConfigView; - -/** Bukkit-compatible entry point for the shared annotation binder. */ -public class AnnotationHandler { - - private final AnnotationBinder binder = new AnnotationBinder( - view -> ((BukkitConfigView) view).getSection()); - - public AnnotationHandler() { - } - - /** - * Loads the existing annotations without changing their Bukkit behavior. - * This signature remains unchanged; a ConfigView overload is deliberately - * not added, so existing calls such as load(null, target) stay unambiguous. - * - * @param config Bukkit configuration (legacy null handling is preserved) - * @param classToLoad object whose declared fields should be populated - */ - public void load(ConfigurationSection config, Object classToLoad) { - binder.load(config == null ? null : new BukkitConfigView(config), classToLoad); - } +/** Original public API retained for source and binary compatibility. */ +public class AnnotationHandler extends com.bencodez.simpleapi.bukkit.config.AnnotationHandler { + public AnnotationHandler() { super(); } + @Override public void load(ConfigurationSection config, Object target) { super.load(config, target); } } diff --git a/SimpleAPI/src/main/java/com/bencodez/simpleapi/file/config/bukkit/BukkitConfigView.java b/SimpleAPI/src/main/java/com/bencodez/simpleapi/file/config/bukkit/BukkitConfigView.java index 0990bd23..7eb342de 100644 --- a/SimpleAPI/src/main/java/com/bencodez/simpleapi/file/config/bukkit/BukkitConfigView.java +++ b/SimpleAPI/src/main/java/com/bencodez/simpleapi/file/config/bukkit/BukkitConfigView.java @@ -1,89 +1,12 @@ package com.bencodez.simpleapi.file.config.bukkit; -import java.util.List; -import java.util.Objects; -import java.util.Set; - import org.bukkit.configuration.ConfigurationSection; -import com.bencodez.simpleapi.file.config.ConfigView; - -/** - * Live, read-only facade over a Bukkit configuration section. Reads deliberately - * preserve the backing section's defaults, coercions, key order and path options. - */ -public final class BukkitConfigView implements ConfigView { - - private final ConfigurationSection section; - - public BukkitConfigView(ConfigurationSection section) { - this.section = Objects.requireNonNull(section, "section"); - } - - /** - * Returns the original section for legacy Bukkit-typed annotated fields. - * No copy is made: existing callers retain section identity and mutability. - * This method belongs to the Bukkit adapter, not the platform-neutral API. - * - * @return the backing Bukkit section - */ - public ConfigurationSection getSection() { - return section; - } - - @Override - public boolean contains(String path) { - return section.contains(path); - } - - @Override - public String getString(String path, String defaultValue) { - return section.getString(path, defaultValue); - } - - @Override - public boolean getBoolean(String path, boolean defaultValue) { - return section.getBoolean(path, defaultValue); - } - - @Override - public int getInt(String path, int defaultValue) { - return section.getInt(path, defaultValue); - } - - @Override - public long getLong(String path, long defaultValue) { - return section.getLong(path, defaultValue); - } - - @Override - public double getDouble(String path, double defaultValue) { - return section.getDouble(path, defaultValue); - } - - @Override - public List getStringList(String path) { - return section.getStringList(path); - } - - @Override - public List getIntegerList(String path) { - return section.getIntegerList(path); - } - - @Override - public boolean isConfigurationSection(String path) { - return section.isConfigurationSection(path); - } - - @Override - public BukkitConfigView getConfigurationSection(String path) { - ConfigurationSection child = section.getConfigurationSection(path); - return child == null ? null : new BukkitConfigView(child); - } - - @Override - public Set getKeys(boolean deep) { - return section.getKeys(deep); +/** Compatibility name. New Bukkit integration code belongs in simpleapi.bukkit. */ +public final class BukkitConfigView extends com.bencodez.simpleapi.bukkit.config.BukkitConfigView { + public BukkitConfigView(ConfigurationSection section) { super(section); } + @Override protected BukkitConfigView sectionView(ConfigurationSection child) { return new BukkitConfigView(child); } + @Override public BukkitConfigView getConfigurationSection(String path) { + return (BukkitConfigView) super.getConfigurationSection(path); } } diff --git a/SimpleAPI/src/main/java/com/bencodez/simpleapi/file/config/configurate/ConfigurateConfigView.java b/SimpleAPI/src/main/java/com/bencodez/simpleapi/file/config/configurate/ConfigurateConfigView.java index 8c5f44b2..3ca3c633 100644 --- a/SimpleAPI/src/main/java/com/bencodez/simpleapi/file/config/configurate/ConfigurateConfigView.java +++ b/SimpleAPI/src/main/java/com/bencodez/simpleapi/file/config/configurate/ConfigurateConfigView.java @@ -1,133 +1,31 @@ package com.bencodez.simpleapi.file.config.configurate; -import java.util.ArrayList; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.regex.Pattern; - import org.spongepowered.configurate.ConfigurationNode; -import com.bencodez.simpleapi.file.config.ConfigView; - -/** - * Read-only view of a Configurate section. Scalar numbers/booleans are read - * strictly, rather than using Configurate's more permissive string coercions. - * This matches the Bukkit scalar/list rules used by the existing binder. - * - *

Paths use '.' by default; at(String...) uses literal segments for identifiers - * containing dots. An externally supplied node is a live view and the caller must - * coordinate its mutation. YamlConfigDocument supplies detached snapshots.

- * - *

Getter/annotation defaults are supported. Bukkit's mutable default-section - * overlay and native serialized Bukkit objects are deliberately not emulated.

- */ -public class ConfigurateConfigView implements ConfigView { - protected final ConfigurationNode node; - private final char separator; +/** Compatibility facade retaining the original constructors and covariant return types. */ +public class ConfigurateConfigView extends com.bencodez.simpleapi.core.config.ConfigurateConfigView { + private final char legacySeparator; public ConfigurateConfigView(ConfigurationNode node) { this(node, '.'); } - public ConfigurateConfigView(ConfigurationNode node, char separator) { - this.node = Objects.requireNonNull(node, "node"); - this.separator = separator; - } - - protected final String[] segments(String path) { - Objects.requireNonNull(path, "path"); - return path.isEmpty() ? new String[0] : path.split(Pattern.quote(String.valueOf(separator)), -1); - } - - protected final ConfigurationNode resolve(String... keys) { - Objects.requireNonNull(keys, "keys"); - ConfigurationNode current = node; - for (String key : keys) { - Objects.requireNonNull(key, "key"); - ConfigurationNode match = null; - // YAML numeric keys remain addressable as strings, without replacing - // them with a second, differently typed key when an editor writes. - for (Map.Entry entry : current.childrenMap().entrySet()) { - if (String.valueOf(entry.getKey()).equals(key)) { - if (match != null) throw new IllegalArgumentException("Ambiguous configuration key"); - match = entry.getValue(); - } - } - current = match == null ? current.node(key) : match; - } - return current; - } - - /** Returns a section using literal key segments, or null for an absent/non-map node. */ - public ConfigurateConfigView at(String... keys) { - ConfigurationNode child = resolve(keys); - return child.isMap() || child == node && child.isNull() - ? new ConfigurateConfigView(child, separator) : null; - } - - @Override public boolean contains(String path) { - return path.isEmpty() || !resolve(segments(path)).isNull(); - } - @Override public String getString(String path, String fallback) { - ConfigurationNode child = resolve(segments(path)); - if (child.isMap()) return fallback; // no Bukkit section toString emulation - Object value = child.raw(); - return value == null ? fallback : value.toString(); - } - @Override public boolean getBoolean(String path, boolean fallback) { - Object value = resolve(segments(path)).rawScalar(); - return value instanceof Boolean bool ? bool : fallback; - } - @Override public int getInt(String path, int fallback) { - Object value = resolve(segments(path)).rawScalar(); - return value instanceof Number number ? number.intValue() : fallback; - } - @Override public long getLong(String path, long fallback) { - Object value = resolve(segments(path)).rawScalar(); - return value instanceof Number number ? number.longValue() : fallback; + super(node, separator); + legacySeparator = separator; } - @Override public double getDouble(String path, double fallback) { - Object value = resolve(segments(path)).rawScalar(); - return value instanceof Number number ? number.doubleValue() : fallback; + private ConfigurateConfigView(com.bencodez.simpleapi.core.config.ConfigurateConfigView source) { + super(source); + // Document snapshots always use the default separator. + legacySeparator = '.'; } - @Override public List getStringList(String path) { - List result = new ArrayList<>(); - for (ConfigurationNode child : resolve(segments(path)).childrenList()) { - Object value = child.rawScalar(); - if (value instanceof String || value instanceof Number || value instanceof Boolean || value instanceof Character) - result.add(value.toString()); - } - return result; + static ConfigurateConfigView documentView(com.bencodez.simpleapi.core.config.ConfigurateConfigView source) { + return new ConfigurateConfigView(source); } - @Override public List getIntegerList(String path) { - List result = new ArrayList<>(); - for (ConfigurationNode child : resolve(segments(path)).childrenList()) { - Object value = child.rawScalar(); - if (value instanceof Number number) result.add(number.intValue()); - else if (value instanceof Character character) result.add((int) character); - else if (value instanceof String string) { - try { result.add(Integer.parseInt(string)); } catch (NumberFormatException ignored) { } - } - } - return result; + @Override protected ConfigurateConfigView sectionView(ConfigurationNode child) { + return new ConfigurateConfigView(child, legacySeparator); } - @Override public boolean isConfigurationSection(String path) { return at(segments(path)) != null; } - @Override public ConfigurateConfigView getConfigurationSection(String path) { return at(segments(path)); } - @Override public Set getKeys(boolean deep) { - LinkedHashSet result = new LinkedHashSet<>(); - collectKeys(node, "", deep, result, 0); - return result; + @Override public ConfigurateConfigView at(String... keys) { + return (ConfigurateConfigView) super.at(keys); } - private void collectKeys(ConfigurationNode parent, String prefix, boolean deep, Set keys, int depth) { - if (depth > 64) throw new IllegalArgumentException("Configuration nesting is too deep"); - Set siblings = new LinkedHashSet<>(); - for (Map.Entry entry : parent.childrenMap().entrySet()) { - String key = String.valueOf(entry.getKey()); - if (!siblings.add(key)) throw new IllegalArgumentException("Ambiguous configuration key"); - String path = prefix + key; - keys.add(path); - if (deep && entry.getValue().isMap()) collectKeys(entry.getValue(), path + separator, true, keys, depth + 1); - } + @Override public ConfigurateConfigView getConfigurationSection(String path) { + return (ConfigurateConfigView) super.getConfigurationSection(path); } } diff --git a/SimpleAPI/src/main/java/com/bencodez/simpleapi/file/config/configurate/YamlConfigDocument.java b/SimpleAPI/src/main/java/com/bencodez/simpleapi/file/config/configurate/YamlConfigDocument.java index fb79ca14..51793ede 100644 --- a/SimpleAPI/src/main/java/com/bencodez/simpleapi/file/config/configurate/YamlConfigDocument.java +++ b/SimpleAPI/src/main/java/com/bencodez/simpleapi/file/config/configurate/YamlConfigDocument.java @@ -1,270 +1,34 @@ package com.bencodez.simpleapi.file.config.configurate; -import java.io.BufferedWriter; -import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.nio.ByteBuffer; -import java.nio.channels.FileChannel; -import java.nio.charset.CodingErrorAction; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.LinkOption; -import java.nio.file.NoSuchFileException; import java.nio.file.Path; -import java.nio.file.StandardCopyOption; -import java.nio.file.StandardOpenOption; -import java.nio.file.attribute.BasicFileAttributes; -import java.nio.file.attribute.PosixFilePermission; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.ArrayList; -import java.util.HexFormat; -import java.util.IdentityHashMap; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; import java.util.function.Consumer; - -import org.spongepowered.configurate.ConfigurationNode; -import org.spongepowered.configurate.yaml.NodeStyle; -import org.spongepowered.configurate.yaml.YamlConfigurationLoader; - import com.bencodez.simpleapi.file.config.ConfigDocument; import com.bencodez.simpleapi.file.config.ConfigEditor; import com.bencodez.simpleapi.file.config.ConfigSnapshot; -/** - * YAML document for administrator-owned local configuration. No file is created - * by open/reload. Updates use private copies, content revisions, a bounded staged - * write and an atomic replacement; there is no truncate-in-place fallback. - * - *

The parent directory must exist and be trusted. Target symlinks/non-regular - * files are rejected. The caller owns exclusive cross-process writes: revision - * checks detect observed edits but cannot make an atomic filesystem compare/swap. - * File bytes are forced before rename; power-loss durability of directory entries - * and preservation of non-POSIX ACLs are not promised.

- * - *

Parsing uses Configurate's YAML loader and its parser policies. This is not - * a hostile YAML upload endpoint. Formatting/inline comments and native Bukkit - * serialized objects are not a lossless migration contract. Existing Bukkit and - * Velocity file implementations are not changed by this class.

- */ +/** Compatibility facade. Persistence and validation live only in the core implementation. */ public final class YamlConfigDocument implements ConfigDocument { - public static final int DEFAULT_MAX_BYTES = 1024 * 1024; - private static final int MAX_NODES = 100_000; - private static final int MAX_DEPTH = 64; - private final Path path; - private final int maxBytes; - private ConfigurationNode root; - private String revision; - private boolean editing; - - private YamlConfigDocument(Path path, int maxBytes) { - this.path = path; - this.maxBytes = maxBytes; - } + public static final int DEFAULT_MAX_BYTES = com.bencodez.simpleapi.core.config.YamlConfigDocument.DEFAULT_MAX_BYTES; + private final com.bencodez.simpleapi.core.config.YamlConfigDocument delegate; - public static YamlConfigDocument open(Path path) throws IOException { return open(path, DEFAULT_MAX_BYTES); } - - public static YamlConfigDocument open(Path path, int maxBytes) throws IOException { - Objects.requireNonNull(path, "path"); - if (maxBytes < 1 || maxBytes > 16 * DEFAULT_MAX_BYTES) - throw new IllegalArgumentException("maxBytes must be between 1 byte and 16 MiB"); - Path absolute = path.toAbsolutePath().normalize(); - if (absolute.getFileName() == null) throw new IOException("A configuration filename is required"); - // Resolve the trusted parent once. Never create parent directories as a side effect of reading. - Path canonical = absolute.getParent().toRealPath().resolve(absolute.getFileName()); - YamlConfigDocument document = new YamlConfigDocument(canonical, maxBytes); - document.reload(); - return document; + private YamlConfigDocument(com.bencodez.simpleapi.core.config.YamlConfigDocument delegate) { + this.delegate = delegate; } - - @Override public Path path() { return path; } - - @Override public synchronized ConfigSnapshot snapshot() { - return new ConfigSnapshot(revision, new ConfigurateConfigView(root.copy())); + public static YamlConfigDocument open(Path path) throws IOException { + return new YamlConfigDocument(com.bencodez.simpleapi.core.config.YamlConfigDocument.open(path)); } - - @Override public synchronized ConfigSnapshot reload() throws IOException { - if (editing) throw new IllegalStateException("Cannot reload inside an edit callback"); - byte[] bytes = readCurrent(); - ConfigurationNode candidate = parse(bytes == null ? new byte[0] : bytes); - ConfigSnapshot result = new ConfigSnapshot(revisionOf(bytes), new ConfigurateConfigView(candidate.copy())); - root = candidate; - revision = result.revision(); - return result; + public static YamlConfigDocument open(Path path, int maxBytes) throws IOException { + return new YamlConfigDocument(com.bencodez.simpleapi.core.config.YamlConfigDocument.open(path, maxBytes)); } - + @Override public Path path() { return delegate.path(); } + @Override public synchronized ConfigSnapshot snapshot() { return legacySnapshot(delegate.snapshot()); } + @Override public synchronized ConfigSnapshot reload() throws IOException { return legacySnapshot(delegate.reload()); } @Override public synchronized ConfigSnapshot update(String expectedRevision, Consumer edit) throws IOException { - Objects.requireNonNull(expectedRevision, "expectedRevision"); - Objects.requireNonNull(edit, "edit"); - if (editing) throw new IllegalStateException("Cannot nest document updates"); - if (!revision.equals(expectedRevision)) throw new IOException("Stale in-memory configuration revision"); - requireUnchanged(); - editing = true; - Editor editor = new Editor(root.copy()); - try { - try { edit.accept(editor); } finally { editor.active = false; } - validate(editor.node, 0, new int[1]); - ConfigurationNode candidate = editor.node.copy(); - byte[] encoded = serialize(candidate); - String nextRevision = revisionOf(encoded); - ConfigSnapshot result = new ConfigSnapshot(nextRevision, new ConfigurateConfigView(candidate.copy())); - replace(encoded); - root = candidate; - revision = nextRevision; - return result; - } finally { - editing = false; - } - } - - private void requireUnchanged() throws IOException { - if (!revision.equals(revisionOf(readCurrent()))) - throw new IOException("Configuration changed on disk; reload before applying an edit"); - } - - private byte[] readCurrent() throws IOException { - BasicFileAttributes attributes; - try { attributes = Files.readAttributes(path, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS); } - catch (NoSuchFileException missing) { return null; } - if (!attributes.isRegularFile() || attributes.isSymbolicLink()) - throw new IOException("Configuration target must be a regular file, not a symbolic link"); - if (attributes.size() > maxBytes) throw new IOException("Configuration exceeds byte limit"); - try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ, LinkOption.NOFOLLOW_LINKS)) { - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - ByteBuffer buffer = ByteBuffer.allocate(8192); - while (channel.read(buffer) != -1) { - buffer.flip(); - int count = buffer.remaining(); - if (count > maxBytes - bytes.size()) throw new IOException("Configuration exceeds byte limit"); - bytes.write(buffer.array(), 0, count); - buffer.clear(); - } - return bytes.toByteArray(); - } + return legacySnapshot(delegate.update(expectedRevision, edit)); } - - private ConfigurationNode parse(byte[] bytes) throws IOException { - String text = StandardCharsets.UTF_8.newDecoder().onMalformedInput(CodingErrorAction.REPORT) - .onUnmappableCharacter(CodingErrorAction.REPORT).decode(ByteBuffer.wrap(bytes)).toString(); - ConfigurationNode candidate = YamlConfigurationLoader.builder().buildAndLoadString(text); - if (candidate.isNull()) candidate.raw(new LinkedHashMap()); - if (!candidate.isMap()) throw new IOException("Configuration root must be a mapping"); - try { validate(candidate, 0, new int[1]); } - catch (IllegalArgumentException invalid) { throw new IOException("Unsupported configuration structure", invalid); } - return candidate; - } - - private byte[] serialize(ConfigurationNode candidate) throws IOException { - LimitedOutput output = new LimitedOutput(maxBytes); - try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(output, StandardCharsets.UTF_8))) { - YamlConfigurationLoader.builder().nodeStyle(NodeStyle.BLOCK).sink(() -> writer).build().save(candidate); - } - return output.bytes.toByteArray(); - } - - private void replace(byte[] encoded) throws IOException { - requireUnchanged(); - Set permissions = null; - if (Files.getFileAttributeView(path.getParent(), java.nio.file.attribute.PosixFileAttributeView.class) != null) { - permissions = revision.equals("missing") - ? Set.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE) - : Files.getPosixFilePermissions(path, LinkOption.NOFOLLOW_LINKS); - } - Path temporary = Files.createTempFile(path.getParent(), ".simpleapi-config-", ".tmp"); - try { - if (permissions != null) Files.setPosixFilePermissions(temporary, permissions); - try (FileChannel channel = FileChannel.open(temporary, StandardOpenOption.WRITE, LinkOption.NOFOLLOW_LINKS)) { - ByteBuffer buffer = ByteBuffer.wrap(encoded); - while (buffer.hasRemaining()) channel.write(buffer); - channel.force(true); - } - requireUnchanged(); - Files.move(temporary, path, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); - } finally { - Files.deleteIfExists(temporary); - } - } - - private static String revisionOf(byte[] bytes) { - if (bytes == null) return "missing"; - try { return "sha256:" + HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(bytes)); } - catch (NoSuchAlgorithmException impossible) { throw new IllegalStateException("SHA-256 unavailable", impossible); } - } - - private static void validate(ConfigurationNode node, int depth, int[] count) { - if (depth > MAX_DEPTH || ++count[0] > MAX_NODES) throw new IllegalArgumentException("Configuration structure exceeds limits"); - if (node.isMap()) { - Set keys = new LinkedHashSet<>(); - for (Map.Entry entry : node.childrenMap().entrySet()) { - Object key = entry.getKey(); - if (!(key instanceof String || key instanceof Number || key instanceof Boolean || key instanceof Character) - || !keys.add(key.toString())) throw new IllegalArgumentException("Unsupported or ambiguous configuration key"); - validate(entry.getValue(), depth + 1, count); - } - } else if (node.isList()) { - for (ConfigurationNode child : node.childrenList()) validate(child, depth + 1, count); - } - } - - private static Object copyValue(Object value, int depth, int[] count, IdentityHashMap ancestors) { - if (depth > MAX_DEPTH || ++count[0] > MAX_NODES) throw new IllegalArgumentException("Configuration value exceeds structure limits"); - if (value == null || value instanceof String || value instanceof Boolean - || value instanceof Byte || value instanceof Short || value instanceof Integer || value instanceof Long - || value instanceof Float || value instanceof Double || value instanceof java.math.BigInteger - || value instanceof java.math.BigDecimal) return value; - if (value instanceof Character character) return character.toString(); - if (ancestors.put(value, Boolean.TRUE) != null) throw new IllegalArgumentException("Cyclic configuration values are not supported"); - try { - if (value instanceof Map map) { - Map copy = new LinkedHashMap<>(); - for (Map.Entry entry : map.entrySet()) { - if (!(entry.getKey() instanceof String key)) throw new IllegalArgumentException("Edited map keys must be strings"); - copy.put(key, copyValue(entry.getValue(), depth + 1, count, ancestors)); - } - return copy; - } - if (value instanceof List list) { - List copy = new ArrayList<>(); - for (Object child : list) copy.add(copyValue(child, depth + 1, count, ancestors)); - return copy; - } - throw new IllegalArgumentException("Only scalar, list and string-keyed map values are supported"); - } finally { ancestors.remove(value); } - } - - private static final class Editor extends ConfigurateConfigView implements ConfigEditor { - private final Thread owner = Thread.currentThread(); - private boolean active = true; - private Editor(ConfigurationNode node) { super(node); } - @Override public void set(String path, Object value) { setAt(value, segments(path)); } - @Override public void setAt(Object value, String... keys) { - if (!active || Thread.currentThread() != owner) throw new IllegalStateException("Editor is no longer active on this thread"); - Objects.requireNonNull(keys, "keys"); - if (keys.length == 0 || keys.length > MAX_DEPTH) throw new IllegalArgumentException("A nonempty bounded key path is required"); - for (String key : keys) if (key == null || key.isEmpty()) throw new IllegalArgumentException("Empty/null key segment"); - Object copy = copyValue(value, 0, new int[1], new IdentityHashMap<>()); - resolve(keys).raw(copy); - } - } - - private static final class LimitedOutput extends OutputStream { - private final int limit; - private final ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - private LimitedOutput(int limit) { this.limit = limit; } - @Override public void write(int value) throws IOException { - if (bytes.size() >= limit) throw new IOException("Serialized configuration exceeds byte limit"); - bytes.write(value); - } - @Override public void write(byte[] value, int offset, int length) throws IOException { - if (length > limit - bytes.size()) throw new IOException("Serialized configuration exceeds byte limit"); - bytes.write(value, offset, length); - } + private static ConfigSnapshot legacySnapshot(ConfigSnapshot snapshot) { + return new ConfigSnapshot(snapshot.revision(), ConfigurateConfigView.documentView( + (com.bencodez.simpleapi.core.config.ConfigurateConfigView) snapshot.view())); } } diff --git a/SimpleAPI/src/main/java/com/bencodez/simpleapi/sql/mysql/config/MysqlConfigView.java b/SimpleAPI/src/main/java/com/bencodez/simpleapi/sql/mysql/config/MysqlConfigView.java index 42e958f4..dd67dffb 100644 --- a/SimpleAPI/src/main/java/com/bencodez/simpleapi/sql/mysql/config/MysqlConfigView.java +++ b/SimpleAPI/src/main/java/com/bencodez/simpleapi/sql/mysql/config/MysqlConfigView.java @@ -1,38 +1,8 @@ package com.bencodez.simpleapi.sql.mysql.config; -import java.util.Objects; import com.bencodez.simpleapi.file.config.ConfigView; -import com.bencodez.simpleapi.sql.mysql.DbType; -/** Platform-neutral snapshot of the existing MySQL section keys and defaults. */ -public final class MysqlConfigView extends MysqlConfig { - public MysqlConfigView(ConfigView section) { - Objects.requireNonNull(section, "section"); - setTablePrefix(section.getString("Prefix", null)); - String tableName = section.getString("Name", ""); - if (tableName != null && !tableName.isEmpty()) setTableName(tableName); - setHostName(section.getString("Host", null)); - setPort(section.getInt("Port", 0)); - setUser(section.getString("Username", null)); - setPass(section.getString("Password", null)); - setDatabase(section.getString("Database", null)); - setLifeTime(section.getLong("MaxLifeTime", -1)); - setMaxThreads(Math.max(1, section.getInt("MaxConnections", 1))); - setMinimumIdle(section.getInt("MinimumIdle", 2)); - setIdleTimeoutMs(section.getLong("IdleTimeoutMs", 10 * 60_000L)); - setKeepaliveMs(section.getLong("KeepaliveMs", 5 * 60_000L)); - setValidationMs(section.getLong("ValidationMs", 5_000L)); - setLeakDetectMs(section.getLong("LeakDetectMs", 20_000L)); - setConnectionTimeout(section.getInt("ConnectionTimeout", 50_000)); - String type = section.getString("DbType", ""); - setDbType(type != null && !type.isEmpty() ? DbType.fromString(type) - : section.getBoolean("UseMariaDB", false) ? DbType.MARIADB : DbType.MYSQL); - setDriver(section.getString("Driver", "")); - setUseSSL(section.getBoolean("UseSSL", false)); - setPublicKeyRetrieval(section.getBoolean("PublicKeyRetrieval", false)); - setUseMariaDB(section.getBoolean("UseMariaDB", false)); - setLine(section.getString("Line", "")); - setDebug(section.getBoolean("Debug", false)); - setPoolName(section.getString("PoolName", "")); - } +/** Compatibility name for the shared MySQL configuration adapter. */ +public final class MysqlConfigView extends com.bencodez.simpleapi.core.sql.MysqlConfigView { + public MysqlConfigView(ConfigView section) { super(section); } } diff --git a/SimpleAPI/src/test/java/com/bencodez/simpleapi/core/CoreClasspathTest.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/core/CoreClasspathTest.java new file mode 100644 index 00000000..c3769121 --- /dev/null +++ b/SimpleAPI/src/test/java/com/bencodez/simpleapi/core/CoreClasspathTest.java @@ -0,0 +1,23 @@ +package com.bencodez.simpleapi.core; + +import static org.junit.jupiter.api.Assertions.*; +import java.net.URLClassLoader; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Test; +import com.bencodez.simpleapi.sql.data.DataValueInt; +import com.bencodez.simpleapi.time.ParsedDuration; + +class CoreClasspathTest { + @Test void existingValueAndDurationClassesRemainUsableWithoutPlatforms() throws Exception { + assertEquals(7, new DataValueInt(7).getInt()); + assertEquals(5000L, ParsedDuration.parse("5s", TimeUnit.SECONDS).getMillis()); + var project = DataValueInt.class.getProtectionDomain().getCodeSource().getLocation(); + try (var loader = new URLClassLoader(new java.net.URL[] {project}, ClassLoader.getPlatformClassLoader())) { + assertThrows(ClassNotFoundException.class, () -> loader.loadClass("org.bukkit.Bukkit")); + assertThrows(ClassNotFoundException.class, () -> loader.loadClass("net.md_5.bungee.api.ProxyServer")); + assertThrows(ClassNotFoundException.class, () -> loader.loadClass("com.velocitypowered.api.proxy.ProxyServer")); + var value = loader.loadClass(DataValueInt.class.getName()).getConstructor(int.class).newInstance(7); + assertEquals(7, value.getClass().getMethod("getInt").invoke(value)); + } + } +} diff --git a/simpleapi-configurate/src/test/java/com/bencodez/simpleapi/file/config/configurate/ConfigurateConfigViewTest.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/file/config/configurate/ConfigurateConfigViewTest.java similarity index 97% rename from simpleapi-configurate/src/test/java/com/bencodez/simpleapi/file/config/configurate/ConfigurateConfigViewTest.java rename to SimpleAPI/src/test/java/com/bencodez/simpleapi/file/config/configurate/ConfigurateConfigViewTest.java index c7d95063..9e2cd9fd 100644 --- a/simpleapi-configurate/src/test/java/com/bencodez/simpleapi/file/config/configurate/ConfigurateConfigViewTest.java +++ b/SimpleAPI/src/test/java/com/bencodez/simpleapi/file/config/configurate/ConfigurateConfigViewTest.java @@ -3,7 +3,6 @@ import static org.junit.jupiter.api.Assertions.*; import java.util.ArrayList; import java.util.Arrays; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -14,6 +13,7 @@ import com.bencodez.simpleapi.file.annotation.*; import com.bencodez.simpleapi.file.config.ConfigView; import com.bencodez.simpleapi.time.ParsedDuration; +import com.bencodez.simpleapi.tests.shared.SharedRuntimeClasspath; class ConfigurateConfigViewTest { @Test void scalarReadsPreserveTypesAndExplicitDefaults() { @@ -38,7 +38,6 @@ class ConfigurateConfigViewTest { assertEquals(2.9, view.getDouble("decimal", -1)); assertEquals(99L, view.getLong("missing", 99)); } - @Test void missingReadsDoNotCreateNodes() { ConfigurationNode root = BasicConfigurationNode.root(); ConfigView view = new ConfigurateConfigView(root); @@ -51,7 +50,6 @@ class ConfigurateConfigViewTest { assertTrue(view.contains("")); assertNotNull(view.getConfigurationSection("")); } - @Test void listsUseBukkitCompatibleElementConversions() { ConfigurationNode root = BasicConfigurationNode.root(); root.node("list").raw(Arrays.asList("2", 3, 4.75, true, Map.of("nested", 1), List.of("nested"), "bad", " 5")); @@ -62,7 +60,6 @@ class ConfigurateConfigViewTest { copy.clear(); assertEquals(6, view.getStringList("list").size()); } - @Test void sectionsKeysAndLiteralSegmentsAreDistinct() { ConfigurationNode root = BasicConfigurationNode.root(); root.node("sites", "some.site", "reward").raw("say voted"); @@ -75,14 +72,12 @@ class ConfigurateConfigViewTest { assertNull(view.getConfigurationSection("sites.other")); assertEquals("say voted", new ConfigurateConfigView(root, '/').getString("sites/some.site/reward", "")); } - @Test void numericYamlKeysCanBeReadUsingStringPaths() throws Exception { ConfigurationNode root = YamlConfigurationLoader.builder().buildAndLoadString("rewards:\n 10:\n command: hello\n"); ConfigurateConfigView view = new ConfigurateConfigView(root); assertEquals("hello", view.getString("rewards.10.command", "")); assertEquals(Set.of("10"), view.getConfigurationSection("rewards").getKeys(false)); } - static class Values { @ConfigDataString(path="message", secondPath="old.message") String message="initial"; @ConfigDataInt(path="votes") int votes=3; @@ -92,7 +87,6 @@ static class Values { @ConfigDataKeys(path="rewards") Set keys; @ConfigDataParsedDuration(path="delay") ParsedDuration delay; } - @Test void realYamlFeedsTheSharedBinderWithoutBukkit() throws Exception { ConfigurationNode root = YamlConfigurationLoader.builder().buildAndLoadString(""" old: @@ -114,9 +108,8 @@ static class Values { assertEquals("command", target.rewards.getString("first", "")); assertEquals(Set.of("first"), target.keys); assertEquals(3000L, target.delay.getMillis()); - assertThrows(ClassNotFoundException.class, () -> Class.forName("org.bukkit.configuration.ConfigurationSection")); + SharedRuntimeClasspath.assertPlatformsAbsent(); } - @Test void adapterRejectsNullArguments() { assertThrows(NullPointerException.class, () -> new ConfigurateConfigView(null)); ConfigurateConfigView view=new ConfigurateConfigView(BasicConfigurationNode.root()); diff --git a/simpleapi-configurate/src/test/java/com/bencodez/simpleapi/file/config/configurate/YamlConfigDocumentTest.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/file/config/configurate/YamlConfigDocumentTest.java similarity index 100% rename from simpleapi-configurate/src/test/java/com/bencodez/simpleapi/file/config/configurate/YamlConfigDocumentTest.java rename to SimpleAPI/src/test/java/com/bencodez/simpleapi/file/config/configurate/YamlConfigDocumentTest.java diff --git a/simpleapi-sql/src/test/java/com/bencodez/simpleapi/sql/mysql/SharedSqlTest.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/sql/mysql/SharedSqlTest.java similarity index 72% rename from simpleapi-sql/src/test/java/com/bencodez/simpleapi/sql/mysql/SharedSqlTest.java rename to SimpleAPI/src/test/java/com/bencodez/simpleapi/sql/mysql/SharedSqlTest.java index 4bba72a2..1d194402 100644 --- a/simpleapi-sql/src/test/java/com/bencodez/simpleapi/sql/mysql/SharedSqlTest.java +++ b/SimpleAPI/src/test/java/com/bencodez/simpleapi/sql/mysql/SharedSqlTest.java @@ -6,9 +6,10 @@ import com.bencodez.simpleapi.file.config.configurate.ConfigurateConfigView; import com.bencodez.simpleapi.sql.mysql.config.MysqlConfigView; import com.bencodez.simpleapi.sql.mysql.queries.Query; +import com.bencodez.simpleapi.tests.shared.SharedRuntimeClasspath; class SharedSqlTest { - @Test void readsExistingDefaultsWithoutAPlatformOrOpeningConnections() { + @Test void readsExistingDefaultsWithoutAPlatformOrOpeningConnections() throws Exception { MysqlConfigView config=new MysqlConfigView(new ConfigurateConfigView(BasicConfigurationNode.root())); assertEquals(1, config.getMaxThreads()); assertEquals(-1, config.getLifeTime()); @@ -17,7 +18,7 @@ class SharedSqlTest { assertEquals(DbType.MYSQL, config.getDbType()); assertFalse(config.isUseSSL()); assertFalse(config.hasTableNameSet()); - assertThrows(ClassNotFoundException.class, () -> Class.forName("org.bukkit.Bukkit")); + SharedRuntimeClasspath.assertPlatformsAbsent(); } @Test void preservesMariaDbFallbackAndExplicitDbSelection() { var node=BasicConfigurationNode.root(); @@ -31,9 +32,12 @@ class SharedSqlTest { node.node("DbType").raw("POSTGRESQL"); assertEquals(DbType.fromString("POSTGRESQL"), new MysqlConfigView(new ConfigurateConfigView(node)).getDbType()); } - @Test void loadsTheExistingQueryAndConnectionApiWithoutBukkit() { - assertDoesNotThrow(() -> ConnectionManager.class.getDeclaredMethods()); - assertDoesNotThrow(() -> AbstractSqlTable.class.getDeclaredMethods()); - assertDoesNotThrow(() -> Query.class.getDeclaredMethods()); + @Test void loadsTheExistingQueryAndConnectionApiWithoutBukkit() throws Exception { + var project = Query.class.getProtectionDomain().getCodeSource().getLocation(); + try (var loader = SharedRuntimeClasspath.open(project)) { + SharedRuntimeClasspath.requirePlatformsAbsent(loader); + for (String name : new String[] {ConnectionManager.class.getName(), AbstractSqlTable.class.getName(), Query.class.getName()}) + assertDoesNotThrow(() -> loader.loadClass(name).getDeclaredMethods()); + } } } diff --git a/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/shared/ConfigParityTest.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/shared/ConfigParityTest.java new file mode 100644 index 00000000..a166a2d0 --- /dev/null +++ b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/shared/ConfigParityTest.java @@ -0,0 +1,62 @@ +package com.bencodez.simpleapi.tests.shared; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import java.lang.reflect.Method; +import java.util.LinkedHashMap; +import java.util.List; +import org.bukkit.configuration.MemoryConfiguration; +import org.junit.jupiter.api.Test; +import org.spongepowered.configurate.BasicConfigurationNode; +import com.bencodez.simpleapi.core.config.ConfigurateConfigView; +import com.bencodez.simpleapi.core.sql.MysqlConfigView; +import com.bencodez.simpleapi.sql.mysql.config.MysqlConfig; +import com.bencodez.simpleapi.sql.mysql.config.MysqlConfigSpigot; + +/** Compares against the real Bukkit getters, while packaged isolation is tested separately. */ +class ConfigParityTest { + @Test void preservesScalarListAndSqlConfigurationBehavior() throws Exception { + int comparisons = 0; + Object[] values = {null, "", "text", "true", "12", false, true, 0, -2, 5L, 4_000_000_000L, + 1.25, List.of(), List.of("1", 2, 3.75, true, "bad")}; + for (Object value : values) { + var bukkit = new MemoryConfiguration(); + var node = BasicConfigurationNode.root(); + bukkit.set("value", value); node.node("value").raw(value); + var view = new ConfigurateConfigView(node); + assertEquals(bukkit.contains("value"), view.contains("value")); + assertEquals(bukkit.getString("value", "fallback"), view.getString("value", "fallback")); + assertEquals(bukkit.getBoolean("value", false), view.getBoolean("value", false)); + assertEquals(bukkit.getBoolean("value", true), view.getBoolean("value", true)); + assertEquals(bukkit.getInt("value", 7), view.getInt("value", 7)); + assertEquals(bukkit.getLong("value", 9L), view.getLong("value", 9L)); + assertEquals(bukkit.getDouble("value", 2.5), view.getDouble("value", 2.5)); + assertEquals(bukkit.getStringList("value"), view.getStringList("value")); + assertEquals(bukkit.getIntegerList("value"), view.getIntegerList("value")); + comparisons += 9; + } + for (boolean populated : new boolean[] {false, true}) { + var bukkit = new MemoryConfiguration(); var node = BasicConfigurationNode.root(); + if (populated) { + var data = new LinkedHashMap(); + data.put("Host", "localhost"); data.put("Port", 3306); data.put("Username", "fixture"); + data.put("Password", "synthetic-fixture"); data.put("Database", "votes"); + data.put("Prefix", "test_"); data.put("Name", "users"); data.put("MaxConnections", 0); + data.put("UseMariaDB", true); data.put("UseSSL", true); data.put("PoolName", "fixture"); + data.put("ConnectionTimeout", 1000); + for (var entry : data.entrySet()) { + bukkit.set(entry.getKey(), entry.getValue()); node.node(entry.getKey()).raw(entry.getValue()); + } + } + var oldConfig = new MysqlConfigSpigot(bukkit); + var sharedConfig = new MysqlConfigView(new ConfigurateConfigView(node)); + for (Method method : MysqlConfig.class.getDeclaredMethods()) { + if (method.getParameterCount() == 0 && (method.getName().startsWith("get") + || method.getName().startsWith("is") || method.getName().startsWith("has"))) { + assertEquals(method.invoke(oldConfig), method.invoke(sharedConfig), method.getName()); + comparisons++; + } + } + } + System.out.println("Bukkit/core configuration parity: " + comparisons + " comparisons passed"); + } +} diff --git a/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/shared/NativeConfigFixture.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/shared/NativeConfigFixture.java new file mode 100644 index 00000000..aaefa432 --- /dev/null +++ b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/shared/NativeConfigFixture.java @@ -0,0 +1,47 @@ +package com.bencodez.simpleapi.tests.shared; + +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.List; +import com.bencodez.simpleapi.core.config.AnnotationBinder; +import com.bencodez.simpleapi.core.config.YamlConfigDocument; +import com.bencodez.simpleapi.core.sql.MysqlConfigView; +import com.bencodez.simpleapi.file.annotation.ConfigDataInt; +import com.bencodez.simpleapi.file.annotation.ConfigDataListString; + +/** No JUnit/Bukkit dependencies: executed with the shared JAR and its explicit runtime libraries only. */ +public final class NativeConfigFixture { + public static final class Options { + @ConfigDataInt(path = "points") public int points; + @ConfigDataListString(path = "commands") public ArrayList commands; + } + private NativeConfigFixture() { } + public static void run() throws Exception { + var folder = Files.createTempDirectory("simpleapi-single-project-"); + var path = folder.resolve("votes.yml"); + try { + var document = YamlConfigDocument.open(path); + document.update("missing", editor -> { + editor.set("points", 3); + editor.set("commands", List.of("say voted")); + editor.set("MySQL.Host", "localhost"); + }); + var view = YamlConfigDocument.open(path).snapshot().view(); + var options = new Options(); + new AnnotationBinder().load(view, options); + if (options.points != 3 || !List.of("say voted").equals(options.commands)) + throw new AssertionError("Core binding/persistence failed"); + if (!"localhost".equals(new MysqlConfigView(view.getConfigurationSection("MySQL")).getHostName())) + throw new AssertionError("Core SQL configuration failed"); + var legacy = com.bencodez.simpleapi.file.config.configurate.YamlConfigDocument.open(path); + if (!(legacy.snapshot().view() instanceof com.bencodez.simpleapi.file.config.configurate.ConfigurateConfigView)) + throw new AssertionError("Legacy snapshot type changed"); + var legacyOptions = new Options(); + new com.bencodez.simpleapi.file.annotation.AnnotationBinder().load(legacy.snapshot().view(), legacyOptions); + if (legacyOptions.points != 3) throw new AssertionError("Legacy binder failed"); + } finally { + Files.deleteIfExists(path); + Files.deleteIfExists(folder); + } + } +} diff --git a/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/shared/PackageCompatibilityTest.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/shared/PackageCompatibilityTest.java new file mode 100644 index 00000000..df9e6469 --- /dev/null +++ b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/shared/PackageCompatibilityTest.java @@ -0,0 +1,58 @@ +package com.bencodez.simpleapi.tests.shared; + +import static org.junit.jupiter.api.Assertions.*; +import java.nio.file.Path; +import java.util.Arrays; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.MemoryConfiguration; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.spongepowered.configurate.BasicConfigurationNode; +import com.bencodez.simpleapi.file.annotation.ConfigDataConfigurationSection; +import com.bencodez.simpleapi.file.annotation.ConfigDataInt; + +class PackageCompatibilityTest { + @TempDir Path directory; + static class Values { + @ConfigDataInt(path="value") int value; + @ConfigDataConfigurationSection(path="section") ConfigurationSection section; + } + @Test void bothBukkitEntryPointsPreserveNativeSectionIdentityAndNullOverload() { + var configuration = new MemoryConfiguration(); + configuration.set("value", 7); + var section = configuration.createSection("section"); + var modern = new Values(); var legacy = new Values(); + new com.bencodez.simpleapi.bukkit.config.AnnotationHandler().load(configuration, modern); + new com.bencodez.simpleapi.file.annotation.AnnotationHandler().load(configuration, legacy); + assertEquals(modern.value, legacy.value); + assertSame(section, modern.section); assertSame(section, legacy.section); + var oldView = new com.bencodez.simpleapi.file.config.bukkit.BukkitConfigView(configuration); + com.bencodez.simpleapi.file.config.bukkit.BukkitConfigView child = oldView.getConfigurationSection("section"); + assertSame(section, child.getSection()); + assertDoesNotThrow(() -> new com.bencodez.simpleapi.file.annotation.AnnotationHandler().load(null, new Object())); + assertEquals(1L, Arrays.stream(com.bencodez.simpleapi.file.annotation.AnnotationHandler.class.getMethods()) + .filter(method -> method.getName().equals("load")).count()); + } + @Test void legacyCovariantSectionsRetainCustomSeparator() { + var root = BasicConfigurationNode.root(); + root.node("a", "b", "c").raw(9); + var old = new com.bencodez.simpleapi.file.config.configurate.ConfigurateConfigView(root, '/'); + com.bencodez.simpleapi.file.config.configurate.ConfigurateConfigView child = old.getConfigurationSection("a"); + assertEquals(9, child.getInt("b/c", -1)); + assertInstanceOf(com.bencodez.simpleapi.file.config.configurate.ConfigurateConfigView.class, child.at("b")); + var core = new com.bencodez.simpleapi.core.config.ConfigurateConfigView(root, '/'); + assertEquals(9, core.getConfigurationSection("a").getInt("b/c", -1)); + } + @Test void bothDocumentNamesPreserveStateAndLegacySnapshotType() throws Exception { + var path = directory.resolve("config.yml"); + var core = com.bencodez.simpleapi.core.config.YamlConfigDocument.open(path); + var saved = core.update("missing", edit -> edit.set("value", 12)); + var legacy = com.bencodez.simpleapi.file.config.configurate.YamlConfigDocument.open(path); + assertEquals(saved.revision(), legacy.snapshot().revision()); + assertInstanceOf(com.bencodez.simpleapi.file.config.configurate.ConfigurateConfigView.class, legacy.snapshot().view()); + var changed = legacy.update(saved.revision(), edit -> edit.set("value", 13)); + assertInstanceOf(com.bencodez.simpleapi.file.config.configurate.ConfigurateConfigView.class, changed.view()); + assertEquals(13, core.reload().view().getInt("value", -1)); + assertEquals(12, saved.view().getInt("value", -1)); + } +} diff --git a/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/shared/SharedArtifactTest.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/shared/SharedArtifactTest.java new file mode 100644 index 00000000..87442679 --- /dev/null +++ b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/shared/SharedArtifactTest.java @@ -0,0 +1,62 @@ +package com.bencodez.simpleapi.tests.shared; + +import static org.junit.jupiter.api.Assertions.*; +import java.net.URLClassLoader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.jar.JarFile; +import org.junit.jupiter.api.Test; + +/** Runs in the package phase after both JARs exist, not against unfiltered target/classes. */ +public class SharedArtifactTest { + private static final List FORBIDDEN = List.of("org/bukkit/", "org/spigotmc/", "io/papermc/paper/", + "net/minecraft/", "net/fabricmc/", "net/minecraftforge/", "net/neoforged/", + "net/md_5/bungee/", "com/velocitypowered/api/", "com/bencodez/simpleapi/bukkit/"); + + @Test void packagedSharedLibraryLinksAndRunsWithoutPlatformApis() throws Exception { + Path shared = Path.of(System.getProperty("simpleapi.sharedJar")); + Path full = Path.of(System.getProperty("simpleapi.fullJar")); + assertTrue(Files.isRegularFile(shared), "Missing shared classifier"); + assertTrue(Files.isRegularFile(full), "Missing legacy distribution"); + Path sources = shared.resolveSibling(shared.getFileName().toString().replace(".jar", "-sources.jar")); + assertTrue(Files.isRegularFile(sources), "Missing shared sources"); + List classes = new ArrayList<>(); + try (JarFile jar = new JarFile(shared.toFile()); JarFile all = new JarFile(full.toFile()); + JarFile sourceJar = new JarFile(sources.toFile())) { + assertNotNull(all.getEntry("com/bencodez/simpleapi/file/annotation/AnnotationHandler.class")); + assertNotNull(all.getEntry("com/bencodez/simpleapi/bukkit/config/AnnotationHandler.class")); + assertNotNull(jar.getEntry("com/bencodez/simpleapi/core/config/YamlConfigDocument.class")); + for (var entry : jar.stream().toList()) { + if (!entry.getName().endsWith(".class")) continue; + assertTrue(entry.getName().startsWith("com/bencodez/simpleapi/"), "Thin artifact contains external classes"); + String constants = new String(jar.getInputStream(entry).readAllBytes(), StandardCharsets.ISO_8859_1); + for (String prefix : FORBIDDEN) { + assertFalse(entry.getName().contains(prefix) || constants.contains(prefix), + "Platform reference in " + entry.getName() + ": " + prefix); + } + assertNotNull(all.getEntry(entry.getName()), "Full distribution lost " + entry.getName()); + String outer = entry.getName().replaceFirst("\\$.*\\.class$", ".class").replace(".class", ".java"); + assertNotNull(sourceJar.getEntry(outer), "Missing source for " + entry.getName()); + classes.add(entry.getName().replace('/', '.').replace(".class", "")); + } + } + assertFalse(classes.isEmpty()); + try (URLClassLoader loader = SharedRuntimeClasspath.open(shared.toUri().toURL(), + NativeConfigFixture.class.getProtectionDomain().getCodeSource().getLocation())) { + SharedRuntimeClasspath.requirePlatformsAbsent(loader); + for (String name : classes) { + Class type = Class.forName(name, false, loader); + assertSame(loader, type.getClassLoader()); + type.getDeclaredConstructors(); type.getDeclaredMethods(); type.getDeclaredFields(); + type.getGenericSuperclass(); type.getGenericInterfaces(); + } + Class fixture = loader.loadClass(NativeConfigFixture.class.getName()); + assertSame(loader, fixture.getClassLoader()); + fixture.getMethod("run").invoke(null); + } + System.out.println("Shared classifier: " + classes.size() + " classes linked; native configuration smoke passed"); + } +} diff --git a/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/shared/SharedRuntimeClasspath.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/shared/SharedRuntimeClasspath.java new file mode 100644 index 00000000..e82e46c9 --- /dev/null +++ b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/shared/SharedRuntimeClasspath.java @@ -0,0 +1,47 @@ +package com.bencodez.simpleapi.tests.shared; + +import java.net.URL; +import java.net.URLClassLoader; +import java.util.LinkedHashSet; +import java.util.Set; + +/** Builds the native classpath from resolved dependency locations, never versioned Maven-cache paths. */ +public final class SharedRuntimeClasspath { + private SharedRuntimeClasspath() { } + + public static URLClassLoader open(URL project, URL... fixtures) throws Exception { + Set urls = new LinkedHashSet<>(); + urls.add(project); + for (URL fixture : fixtures) urls.add(fixture); + for (String type : new String[] { + "org.spongepowered.configurate.ConfigurationNode", + "org.spongepowered.configurate.yaml.YamlConfigurationLoader", + "io.leangen.geantyref.GenericTypeReflector", + "net.kyori.option.Option", + "com.zaxxer.hikari.HikariConfig", + "org.slf4j.Logger" }) { + Class resolved = Class.forName(type, false, SharedRuntimeClasspath.class.getClassLoader()); + urls.add(resolved.getProtectionDomain().getCodeSource().getLocation()); + } + return new URLClassLoader(urls.toArray(URL[]::new), ClassLoader.getPlatformClassLoader()); + } + + public static void assertPlatformsAbsent() throws Exception { + URL project = com.bencodez.simpleapi.core.config.AnnotationBinder.class + .getProtectionDomain().getCodeSource().getLocation(); + try (URLClassLoader loader = open(project)) { + requirePlatformsAbsent(loader); + } + } + + public static void requirePlatformsAbsent(ClassLoader loader) throws Exception { + for (String type : new String[] { "org.bukkit.Bukkit", "net.minecraft.server.MinecraftServer", + "net.md_5.bungee.api.ProxyServer", "com.velocitypowered.api.proxy.ProxyServer", + "net.fabricmc.api.ModInitializer", "net.minecraftforge.fml.ModList" }) { + try { + loader.loadClass(type); + throw new AssertionError("Platform leaked into shared classpath: " + type); + } catch (ClassNotFoundException expected) { } + } + } +} diff --git a/docs/platform-neutral-configuration.md b/docs/platform-neutral-configuration.md index f6800d26..d6587f65 100644 --- a/docs/platform-neutral-configuration.md +++ b/docs/platform-neutral-configuration.md @@ -1,81 +1,24 @@ -# Platform-neutral configuration foundation +# Platform-neutral configuration binding -This is the first compatibility-preserving step toward reusing SimpleAPI's -configuration annotations outside Bukkit. It is not a Forge/Fabric implementation -or a separately published `simpleapi-core` artifact yet. The existing Maven -coordinates, shaded JAR, build command, configuration files and dependencies are -unchanged. Do not put the entire current SimpleAPI JAR on a mod server and assume -that its other classes are platform-neutral. +The shared binder implementation lives in `com.bencodez.simpleapi.core.config`. +Bukkit adapters live in `com.bencodez.simpleapi.bukkit.config`. Existing callers can +continue using `com.bencodez.simpleapi.file.annotation.AnnotationHandler` and +`AnnotationBinder`; those classes delegate without changing their public signatures. +There is still no ambiguous ConfigView overload on the Bukkit handler. -## Existing Bukkit callers +The existing annotation types and neutral configuration contracts keep their +original package names. Bukkit section fields receive their original native +`ConfigurationSection` objects, including when typed as Object. Shared callers +use `ConfigView` fields. Defaults, alternate paths, list fallbacks, declared-field +traversal, annotation ordering and per-field exception isolation are unchanged. +The historical zero-default long reflection behavior is deliberately preserved. -Keep using the existing API without changes: - -```java -new AnnotationHandler().load(configurationSection, target); -``` - -`AnnotationHandler` delegates to `AnnotationBinder` through `BukkitConfigView`. -The adapter forwards reads to the original section rather than serializing, -copying or reparsing it. `@ConfigDataConfigurationSection` continues to assign the -original Bukkit section, including when the annotated field is typed as `Object`. -Section identity, edits through that section, Bukkit defaults and path options -are preserved. There is no new `load` overload on `AnnotationHandler`, so existing -`load(null, target)` call sites are still unambiguous. - -## New shared callers - -A platform supplies the small `ConfigView` read contract and uses: - -```java -new AnnotationBinder().load(view, target); -``` - -The no-argument binder assigns `ConfigView` instances to section-annotated fields; -shared models should declare those fields as `ConfigView`. The optional section -projection constructor exists for compatibility adapters, not for leaking native -server objects into new shared models. It is called only for present sections. - -The binder uses the existing annotation types and `ParsedDuration`. Its execution -path has no Bukkit, proxy, mod-loader or Minecraft dependency. The headless test -loads it with only project classes and the JDK and verifies that Bukkit cannot be -loaded. An in-memory test view is included; production Configurate/YAML adapters -and physical module extraction are separate follow-ups. - -## Behavior deliberately preserved - -This change moves the existing binding body rather than redesigning its rules: - -- Only declared fields are processed, in the existing annotation-check order. -- Existing field/annotation defaults, alternate paths and boolean inversion apply. -- Empty lists fall back to alternate paths and then the initialized list object. -- Missing sections become null; missing key sets become empty. -- Per-field exceptions are printed and isolated as before. -- The historical zero-default `ConfigDataLong` reflection behavior is retained: - a long field initializer is not recovered through `Field.getInt`. Changing it - would alter existing configurations and is intentionally outside this PR. - -`ConfigView` is read-only access, not a promise that its backing data is immutable -or safe to read from arbitrary threads. Each platform must preserve its existing -configuration ownership and thread rules. - -## Verification - -Run the existing full build: - -```sh -mvn -B -f SimpleAPI/pom.xml package -``` - -Focused regression tests: +Build and test the one project with: ```sh -mvn -B -f SimpleAPI/pom.xml \ - -Dtest=AnnotationHandlerCompatibilityTest,AnnotationBinderHeadlessTest test +mvn -B -f SimpleAPI/pom.xml clean package ``` -The compatibility tests exercise the real Bukkit configuration API. The headless -fixture deliberately does not depend on JUnit or Bukkit within its isolated -class loader. Before merging, also build AdvancedCore and VotingPlugin against -the locally installed candidate SimpleAPI artifact; no consumer changes should -be required. +The same POM attaches a platform-neutral `shared` JAR; there are no child modules. +See `shared-libraries.md` for dependency exclusions, package compatibility, +configuration persistence guarantees, and packaged headless verification. diff --git a/docs/shared-libraries.md b/docs/shared-libraries.md index 8751adb6..653a8105 100644 --- a/docs/shared-libraries.md +++ b/docs/shared-libraries.md @@ -1,190 +1,174 @@ -# Shared SimpleAPI libraries - -This follows the configuration binding foundation in PR #75. It adds buildable, -separately publishable, Bukkit-free artifacts and production configuration I/O. -It does not port VotingPlugin or AdvancedCore, and does not change their releases. - -## Artifacts - -| Artifact | Contents | Runtime dependencies | -| --- | --- | --- | -| `com.bencodez:simpleapi-core` | Existing annotation binder and annotation types, `ConfigView`, duration support, debug enum, SQL data values/columns; new document/editor/snapshot contracts | JDK 21 only | -| `com.bencodez:simpleapi-configurate` | `ConfigurateConfigView` and `YamlConfigDocument` | Core and Configurate YAML 4.2.0 | -| `com.bencodez:simpleapi-sql` | Existing JDBC/Hikari connection management, MySQL wrapper, abstract tables, queries and configuration model; new `MysqlConfigView` | Core and HikariCP 7.0.2 | -| `com.bencodez:simpleapi` | Existing full Bukkit/proxy distribution, including the shared classes | Existing dependency set, unchanged | - -The new modules currently use version `1.0.2-SNAPSHOT`, aligned with SimpleAPI. -They are libraries, not independently installed server mods. SQL drivers are still -selected/provided by the consuming application; publishing the SQL module does not -silently add a JDBC driver or change any existing database behavior/schema. - -### One maintained implementation, two packaging targets - -All implementation source stays under `SimpleAPI/src/main/java`, including the -new APIs. The shared module POMs stage explicit allow-lists into their own -`target/generated-sources/shared` directories. This is generated build input, -not a maintained source fork. Their javac source paths never include the rest of -the Bukkit tree. Source JARs contain the staged implementation as well. - -This layout deliberately preserves the old standalone build, source packages, -shading, and artifact coordinates while making the same implementation available -to native loaders. No existing class body, public signature, or legacy POM is -changed. Full distribution users can use the new APIs without adding new Maven -coordinates. - -**Do not package both representations of the same classes.** A native application -uses the shared artifacts and never the full `simpleapi` artifact. A Bukkit -application retains the full distribution. Future AdvancedCore common-module -integration must choose one representation of each SimpleAPI class in the final -Bukkit JAR, keep versions aligned, and verify the final shaded artifact's linkage. -Existing AdvancedCore/VotingPlugin builds do not consume the new thin modules and -need no dependency changes in this PR. - -The thin SQL artifact intentionally uses unrelocated HikariCP, whereas the full -SimpleAPI distribution already relocates HikariCP. Some existing SQL public -signatures expose Hikari types. Therefore excluding the thin dependencies in favor -of the full JAR is not sufficient by itself for common code compiled against those -signatures: the final Bukkit packaging must relocate the common callers and the -provided SQL implementations consistently. Prefer JDBC/JDK types at new shared -boundaries and add a final packaged Bukkit linkage test when making that consumer -change. This PR does not claim that future mixed packaging is already validated. - -A native mod packager must include its actual runtime dependency graph using the -target loader's supported mechanism. No changes to existing full-JAR shading are -made here. - -## Native configuration example - -```java -Path directory = Path.of("config", "votingplugin"); -Files.createDirectories(directory); // explicit application-owned setup -ConfigDocument document = YamlConfigDocument.open(directory.resolve("VoteSites.yml")); -ConfigSnapshot before = document.snapshot(); -ConfigSnapshot after = document.update(before.revision(), edit -> { - edit.set("PointsOnVote", 1); - edit.set("Rewards.Commands", List.of("give player minecraft:diamond")); - edit.setAt(Map.of("ServiceSite", "example.site"), "VoteSites", "example.site"); -}); -new AnnotationBinder().load(after.view(), options); +# One SimpleAPI project, shared and platform packages + +SimpleAPI has one Maven project: `SimpleAPI/pom.xml`. There is no root aggregator, +parent project, child module or generated-source staging. Use the existing project +in your IDE and the existing build command. No additional workflow is required. + +## Source layout + +```text +SimpleAPI/ + pom.xml + src/main/java/com/bencodez/simpleapi/ + core/config/ # Annotation binding, Configurate reads, YAML documents + core/sql/ # Shared database configuration + bukkit/config/ # Native Bukkit configuration/annotation adapters + file/... # Compatibility APIs and legacy facades + sql/... # Existing public data/JDBC APIs + ... # Existing APIs remain in their published packages + src/test/java/ # All former module tests live in the ordinary test tree ``` -Shared section-annotated fields use `ConfigView`. Existing Bukkit callers keep -using `AnnotationHandler` and receive native `ConfigurationSection` fields as in -PR #75. The old API has no new overloads or altered coercion rules. - -`ConfigurateConfigView` reads numeric and boolean scalars strictly, matching the -Bukkit getter rules rather than converting arbitrary strings to numbers/bools. -String/integer lists preserve the legacy element conversions and empty-list -annotation fallbacks. It supports a selectable path separator and literal key -segments through `at(String...)`. Numeric YAML keys remain addressable as strings; -ambiguous string representations are rejected by document validation. - -Getter and annotation defaults work. Bukkit's mutable configuration-default tree, -section `toString()` output, and Bukkit-serialized item/player objects are not -emulated. Parse-format-specific behavior is not claimed to be a lossless Bukkit -YAML migration. Port native object representations in the owning platform adapter. - -## Document safety and ownership - -All methods are synchronous: call file operations on an I/O worker, not a game or -entity thread. `open` and `reload` never create a file or parent directory. Missing -files produce an empty snapshot with revision `missing`; invalid/unreadable files -throw. A failed reload retains the last-good in-memory configuration. - -An update takes an expected revision and edits a private copy. Callback/validation -failure discards that copy. The editor is limited to its callback thread and cannot -write after the callback. Input collections and returned snapshots are detached -from future document state. Edits accept ordinary scalar/list/string-keyed-map -values, not native server objects or cyclic graphs. - -The writer checks the expected revision and current disk content, bounds serialized -bytes, writes a temporary sibling file, forces its contents, and atomically replaces -the target. There is no truncate-in-place or non-atomic replacement fallback. Target -symlinks and non-regular files are rejected. Existing POSIX permission bits are -retained; new POSIX files are owner-read/write only. Temporary files are cleaned up. - -Default size limit: 1 MiB (explicitly configurable from 1 byte to 16 MiB). Parsed/ -edited trees are limited to 64 levels and 100,000 nodes. UTF-8 decoding reports -malformed input instead of silently replacing bytes. - -This is for administrator-owned local files. Parent directories must be trusted. -The caller must own cross-process writes: revision checks detect observed external -edits, but an unrelated process can race the final check/rename. This is not a -filesystem compare-and-swap, database transaction, or hostile YAML upload validator. -Parsing uses Configurate's YAML parser policies. Inline comments/formatting, file -ownership/non-POSIX ACLs and directory-entry power-loss durability are not promised. -Do not wire this directly to an untrusted network editor without the application's -path, authentication, YAML-complexity, revision and secret-masking controls. - -## Building and publishing - -Existing command and distribution remain available: +New platform-neutral implementations belong in `core`; Bukkit integration belongs +in `bukkit`. Future implemented Forge/Fabric/proxy adapters can use sibling +packages. Empty loader projects or pretend loader implementations are not added. -```sh -mvn -B -f SimpleAPI/pom.xml package -``` +This is an incremental package migration, not a breaking rename of the public API. +Existing annotation types, `ConfigView`, document/editor/snapshot contracts, +`ParsedDuration`, SQL/value types and unrelated utilities retain their original +fully-qualified names. The legacy annotation binder, YAML/Configurate adapters, +MySQL configuration adapter and Bukkit configuration entry points delegate to the +new implementation packages. Section identity and covariant legacy return types +are preserved. Each migrated algorithm has one maintained implementation. -Build/test/install all artifacts, including the full legacy distribution: +## Outputs from one POM ```sh -mvn -B clean install +# From the repository root; unchanged for existing consumers and CI +mvn -B -f SimpleAPI/pom.xml clean package ``` -Build only shared modules and the parent (no server APIs needed): +The build produces: -```sh -mvn -B -pl simpleapi-core,simpleapi-configurate,simpleapi-sql -am clean install +| Local file | Maven artifact | Contents | +| --- | --- | --- | +| `SimpleAPI/target/SimpleAPI.jar` | `com.bencodez:simpleapi:` | Existing full Bukkit/proxy distribution, with unchanged dependency scopes and relocations | +| `SimpleAPI/target/SimpleAPI-shared.jar` | `com.bencodez:simpleapi::shared` | Platform-neutral classes and required legacy neutral APIs, unshaded | +| `SimpleAPI/target/SimpleAPI-shared-sources.jar` | Classifier `shared-sources` | Matching maintained source files | + +The attached JAR executions select compiled classes directly. They do not copy +sources to a second project or reimplement the library. Core code must not reference +Bukkit, proxy, Minecraft or mod-loader APIs. The shared class allow-list intentionally +excludes the legacy Bukkit-facing `AnnotationHandler`, `BukkitConfigView`, SQLite +plugin wrappers, player/item/GUI APIs and full platform entry points. + +The former `simpleapi-parent`, `simpleapi-core`, `simpleapi-configurate` and +`simpleapi-sql` coordinates are no longer built. A consumer using those experimental +module coordinates must switch to the shared classifier and its explicit dependencies. +Existing full `simpleapi` consumers do not need to change their coordinates/imports. +Previously published artifacts, if any, are not deleted from a repository by this change. + +## Native dependency configuration + +A classifier shares its project's POM; it does **not** have a smaller independent +transitive dependency graph. Native consumers must exclude the full distribution's +transitives, then explicitly declare the shared dependencies they use. For Maven: + +```xml + + com.bencodez + simpleapi + ${simpleapi.version} + shared + + ** + + + + org.spongepowered + configurate-yaml + ${configurate.version} + + + com.zaxxer + HikariCP + ${hikari.version} + ``` -A release operator can publish the new artifacts and their parent using existing -Nexus credentials. CI does not run this command: +Set these version properties to the versions selected by the SimpleAPI POM being +consumed. Configurate/Hikari supply their own runtime dependencies. JDBC drivers +remain the application's responsibility. Consumers using only the binder/value +APIs need neither Configurate nor Hikari; the full shared surface/linkage check uses +both. A Gradle consumer can disable transitives on the classified dependency and +add the same required runtime libraries explicitly. -```sh -mvn -B -Pdeploy-shared -pl simpleapi-core,simpleapi-configurate,simpleapi-sql -am deploy -``` +Do not include both the full and shared SimpleAPI representations in the same +native runtime. The existing full JAR still relocates HikariCP; the thin shared JAR +does not. Future mixed AdvancedCore common/Bukkit packaging must align relocations +of callers and implementations and test the final JAR. Exclusions alone do not +prove the mixed SQL signatures compatible. Prefer JDBC/JDK types at new boundaries. -Existing Jenkins/legacy publication remains untouched; it will not automatically -publish the new coordinates until its operator adds the shared publication step. -Keep all four artifacts' versions aligned when releasing. +## Configuration use and guarantees -## Verification +New code may import `com.bencodez.simpleapi.core.config.AnnotationBinder`, +`ConfigurateConfigView`, and `YamlConfigDocument`. Their existing public contract +and annotation types remain in the legacy neutral packages. Existing Bukkit code +can continue using `com.bencodez.simpleapi.file.annotation.AnnotationHandler` or +use the new `com.bencodez.simpleapi.bukkit.config.AnnotationHandler` entry point. -No additional shared-library GitHub Actions workflow is included. The existing -`maven.yml` remains unchanged and runs `mvn -B -f SimpleAPI/pom.xml package`; -it does not build or test the sibling shared modules. Run their tests explicitly -using the root or shared-module build commands above. +```java +Path directory = Path.of("config", "votingplugin"); +Files.createDirectories(directory); // Explicit application-owned setup +ConfigDocument document = YamlConfigDocument.open(directory.resolve("VoteSites.yml")); +ConfigSnapshot before = document.snapshot(); +ConfigSnapshot after = document.update(before.revision(), edit -> { + edit.set("PointsOnVote", 1); + edit.set("Rewards.Commands", List.of("give player minecraft:diamond")); +}); +new AnnotationBinder().load(after.view(), options); +``` -The packaged-artifact probes remain available for manual validation. From the -repository root with JDK 21, Maven and Python 3 available, run: +All file methods are synchronous; run I/O away from game/entity threads. Open and +reload do not create files/parents. Missing files produce an empty snapshot; +invalid files throw and failed reloads retain the last good in-memory state. +Updates edit a detached copy, check revisions, bound serialized bytes, write and +force a temporary sibling, then use atomic replacement with no truncate fallback. +Target symlinks are rejected; POSIX mode bits are retained and new files default to +owner read/write. Editors are callback/thread-scoped and snapshots are detached. + +Default byte limit is 1 MiB, configurable up to 16 MiB. Tree limits are 64 levels +and 100,000 nodes. These existing semantics are unchanged by package consolidation. +This API is for trusted administrator-owned local files, not hostile YAML uploads. +The parent directory and cross-process writer ownership belong to the application. +Observed external edits are rejected, but the final revision check/rename is not a +cross-process compare-and-swap. Inline formatting/comments, non-POSIX ACLs, file +ownership and directory-entry power-loss durability are not guaranteed. + +Getter/annotation defaults, strict scalar reads and legacy list conversions are +supported. Bukkit default-tree overlays and native serialized Bukkit items are not +emulated. Literal-key access remains available on Configurate views. This packaging +change does not add the broader reward configuration or SQLite extraction work. + +## Validation + +All previous module tests have moved into `SimpleAPI/src/test/java`; existing +headless/duration tests are reused instead of duplicated. Tests that need an absent +Bukkit classpath explicitly create an isolated classloader rather than assuming +that Bukkit is absent from the ordinary single-project test runtime. + +The normal package command runs the full unit suite, including real Bukkit/core +configuration parity and legacy package compatibility. After packaging/shading, +`SharedArtifactTest` checks the actual shared/source/full JARs, rejects platform +references in shared class files, links selected classes using only the shared JAR +and native dependencies, and runs the configuration/binder/SQL smoke fixture there. +Runtime dependency locations are taken from Maven's resolved classpath, not pinned +version paths. Reports are under `target/surefire-reports` and +`target/shared-artifact-reports`. Normal `-DskipTests` behavior is unchanged. + +There is no extra GitHub workflow, cross-repository checkout, pinned downstream +commit, production access or remote publication in these tests. AdvancedCore and +VotingPlugin compatibility builds remain deliberate checks for API/release work. +No live-server or live-database coverage is implied by the headless tests. + +## Install and publication ```sh -mvn -B -ntp clean install -mvn -B -ntp -nsu -pl simpleapi-core,simpleapi-configurate,simpleapi-sql -am org.apache.maven.plugins:maven-dependency-plugin:3.8.1:copy-dependencies -DincludeScope=runtime -DoutputDirectory=target/runtime-deps -mvn -B -ntp -nsu -f SimpleAPI/pom.xml org.apache.maven.plugins:maven-dependency-plugin:3.8.1:build-classpath -Dmdep.outputFile=target/compatibility-classpath.txt -python3 tools/verify-shared-artifacts.py +mvn -B -f SimpleAPI/pom.xml clean install ``` -These commands install artifacts only into the local Maven repository, not a -remote repository. The probes check packaged shared-class boundaries, compile -and run a native consumer against the JARs, compare Bukkit/shared configuration -behavior, and check source staging against the maintained implementation. - -Cross-repository builds remain an explicit check for significant API changes or -release preparation, not an automatic dependency of every SimpleAPI PR. The -initial downstream validation results are recorded in PR #76 as historical -verification. SQL tests here cover configuration/linkage, not a live database -matrix. Live Minecraft/Folia/Forge/Fabric smoke tests remain application-level work. - -## What remains for the platform port - -SimpleAPI's reusable configuration/duration/data/SQL layer is now packaged for -consumption without Bukkit. AdvancedCore still needs its own shared runtime and -platform-specific execution boundaries. The existing Bukkit/Folia scheduler, -player, messaging, item, inventory and mixed `ArrayUtils` APIs remain untouched; -their game operations belong behind adapters during that extraction. Do not replace -entity-aware scheduling with a generic global-thread executor. - -The separate HTTP transport work from PR #73 is not copied or reworked here. It -merged into main while this change was being validated; the initial PR integration -build included it. Packaging that transport as a thin native dependency is a -separate follow-up, not part of these configuration/data/SQL artifacts. +Installs the full, shared and shared-source artifacts under the same project +version. Existing deployment profiles/configuration remain unchanged; attached +artifacts are available to the existing publishing lifecycle. This change does not +invoke deployment or claim the new classifier is already on Nexus. There is no +separate parent or module publication step to maintain. diff --git a/pom.xml b/pom.xml deleted file mode 100644 index 272edff6..00000000 --- a/pom.xml +++ /dev/null @@ -1,47 +0,0 @@ - - - 4.0.0 - com.bencodez - simpleapi-parent - 1.0.2-SNAPSHOT - pom - SimpleAPI shared libraries - - - simpleapi-core - simpleapi-configurate - simpleapi-sql - SimpleAPI - - - UTF-8 - 21 - 1.18.44 - 6.0.3 - 4.2.0 - - - org.projectlomboklombok${lombok.version}provided - org.junit.jupiterjunit-jupiter${junit.version}test - - - org.apache.maven.pluginsmaven-compiler-plugin3.15.0 - 21none - org.projectlomboklombok${lombok.version} - - - org.apache.maven.pluginsmaven-resources-plugin3.4.0 - org.apache.maven.pluginsmaven-surefire-plugin3.5.4 - org.apache.maven.pluginsmaven-jar-plugin3.5.0 - org.apache.maven.pluginsmaven-source-plugin3.3.1 - org.apache.maven.pluginsmaven-install-plugin3.1.4 - org.apache.maven.pluginsmaven-deploy-plugin3.1.4 - org.codehaus.mojobuild-helper-maven-plugin3.6.0 - - - deploy-shared - nexushttps://nexus.bencodez.com/repository/maven-snapshots/ - nexushttps://nexus.bencodez.com/repository/maven-releases/ - - diff --git a/simpleapi-configurate/pom.xml b/simpleapi-configurate/pom.xml deleted file mode 100644 index afd68651..00000000 --- a/simpleapi-configurate/pom.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - 4.0.0 - com.bencodezsimpleapi-parent1.0.2-SNAPSHOT - simpleapi-configurate - SimpleAPI Configurate adapter - - com.bencodezsimpleapi-core${project.version} - org.spongepoweredconfigurate-yaml${configurate.version} - org.junit.jupiterjunit-jupiter - - - org.apache.maven.pluginsmaven-resources-plugin - stage-existing-shared-sourcesgenerate-sourcescopy-resources - ${project.build.directory}/generated-sources/shared - ${project.basedir}/../SimpleAPI/src/main/javafalse - com/bencodez/simpleapi/file/config/configurate/*.java - - - - org.codehaus.mojobuild-helper-maven-plugin - existing-shared-sourcesgenerate-sourcesadd-source - ${project.build.directory}/generated-sources/shared - - - org.apache.maven.pluginsmaven-source-plugin - sourcespackagejar-no-fork - - - diff --git a/simpleapi-core/pom.xml b/simpleapi-core/pom.xml deleted file mode 100644 index d0f6e73b..00000000 --- a/simpleapi-core/pom.xml +++ /dev/null @@ -1,50 +0,0 @@ - - - 4.0.0 - com.bencodezsimpleapi-parent1.0.2-SNAPSHOT - simpleapi-core - simpleapi-core - - org.projectlomboklomboktrue - org.junit.jupiterjunit-jupiter - - - - - org.apache.maven.pluginsmaven-resources-plugin - stage-existing-shared-sourcesgenerate-sourcescopy-resources - ${project.build.directory}/generated-sources/shared - ${project.basedir}/../SimpleAPI/src/main/javafalse - com/bencodez/simpleapi/file/config/*.java - com/bencodez/simpleapi/file/annotation/AnnotationBinder.java - com/bencodez/simpleapi/file/annotation/ConfigData*.java - com/bencodez/simpleapi/time/ParsedDuration.java - com/bencodez/simpleapi/debug/DebugLevel.java - com/bencodez/simpleapi/sql/Column.java - com/bencodez/simpleapi/sql/DataType.java - com/bencodez/simpleapi/sql/data/*.java - - - stage-existing-headless-testsgenerate-test-sourcescopy-resources - ${project.build.directory}/generated-test-sources/shared - ${project.basedir}/../SimpleAPI/src/test/javafalse - com/bencodez/simpleapi/tests/ParsedDurationTest.java - com/bencodez/simpleapi/tests/file/config/AnnotationBinderHeadlessTest.java - com/bencodez/simpleapi/tests/file/config/HeadlessBindingFixture.java - - - - org.codehaus.mojobuild-helper-maven-plugin - existing-shared-sourcesgenerate-sourcesadd-source - ${project.build.directory}/generated-sources/shared - - existing-headless-testsgenerate-test-sourcesadd-test-source - ${project.build.directory}/generated-test-sources/shared - - - org.apache.maven.pluginsmaven-source-plugin - sourcespackagejar-no-fork - - - diff --git a/simpleapi-core/src/test/java/com/bencodez/simpleapi/core/CoreClasspathTest.java b/simpleapi-core/src/test/java/com/bencodez/simpleapi/core/CoreClasspathTest.java deleted file mode 100644 index b2526597..00000000 --- a/simpleapi-core/src/test/java/com/bencodez/simpleapi/core/CoreClasspathTest.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.bencodez.simpleapi.core; - -import static org.junit.jupiter.api.Assertions.*; -import org.junit.jupiter.api.Test; -import com.bencodez.simpleapi.sql.data.DataValueInt; -import com.bencodez.simpleapi.time.ParsedDuration; -import java.util.concurrent.TimeUnit; - -class CoreClasspathTest { - @Test void existingValueAndDurationClassesRemainUsableWithoutPlatforms() { - assertEquals(7, new DataValueInt(7).getInt()); - assertEquals(5000L, ParsedDuration.parse("5s", TimeUnit.SECONDS).getMillis()); - assertThrows(ClassNotFoundException.class, () -> Class.forName("org.bukkit.Bukkit")); - assertThrows(ClassNotFoundException.class, () -> Class.forName("net.md_5.bungee.api.ProxyServer")); - assertThrows(ClassNotFoundException.class, () -> Class.forName("com.velocitypowered.api.proxy.ProxyServer")); - } -} diff --git a/simpleapi-sql/pom.xml b/simpleapi-sql/pom.xml deleted file mode 100644 index ce91646c..00000000 --- a/simpleapi-sql/pom.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - 4.0.0 - com.bencodezsimpleapi-parent1.0.2-SNAPSHOT - simpleapi-sql - simpleapi-sql - - org.projectlomboklomboktrue - org.junit.jupiterjunit-jupiter - com.bencodezsimpleapi-core${project.version} - com.zaxxerHikariCP7.0.2 - com.bencodezsimpleapi-configurate${project.version}test - - - - org.apache.maven.pluginsmaven-resources-plugin - stage-existing-shared-sourcesgenerate-sourcescopy-resources - ${project.build.directory}/generated-sources/shared - ${project.basedir}/../SimpleAPI/src/main/javafalse - com/bencodez/simpleapi/sql/mysql/AbstractSqlTable.java - com/bencodez/simpleapi/sql/mysql/ConnectionManager.java - com/bencodez/simpleapi/sql/mysql/DbType.java - com/bencodez/simpleapi/sql/mysql/MySQL.java - com/bencodez/simpleapi/sql/mysql/config/MysqlConfig.java - com/bencodez/simpleapi/sql/mysql/config/MysqlConfigView.java - com/bencodez/simpleapi/sql/mysql/queries/*.java - - - - - org.codehaus.mojobuild-helper-maven-plugin - existing-shared-sourcesgenerate-sourcesadd-source - ${project.build.directory}/generated-sources/shared - - - - org.apache.maven.pluginsmaven-source-plugin - sourcespackagejar-no-fork - - - diff --git a/tools/ConfigParityProbe.java b/tools/ConfigParityProbe.java deleted file mode 100644 index 4f5d70b4..00000000 --- a/tools/ConfigParityProbe.java +++ /dev/null @@ -1,54 +0,0 @@ -import java.lang.reflect.Method; -import java.util.Arrays; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Objects; -import org.bukkit.configuration.MemoryConfiguration; -import org.spongepowered.configurate.BasicConfigurationNode; -import com.bencodez.simpleapi.file.config.configurate.ConfigurateConfigView; -import com.bencodez.simpleapi.sql.mysql.config.MysqlConfig; -import com.bencodez.simpleapi.sql.mysql.config.MysqlConfigSpigot; -import com.bencodez.simpleapi.sql.mysql.config.MysqlConfigView; - -/** Deliberately has both platforms' test inputs; never a native runtime dependency. */ -public final class ConfigParityProbe { - private static int checks; - private static void equal(Object expected,Object actual) { - checks++; - if (!Objects.equals(expected,actual)) throw new AssertionError("Configuration parity failed: expected="+expected+", actual="+actual); - } - public static void main(String[] arguments) throws Exception { - Object[] values={null,"", "text", "true", "12", false, true, 0, -2, 5L, 4_000_000_000L, 1.25, List.of(), List.of("1", 2, 3.75, true, "bad")}; - for (Object value:values) { - var bukkit=new MemoryConfiguration(); - var node=BasicConfigurationNode.root(); - bukkit.set("value",value); node.node("value").raw(value); - var view=new ConfigurateConfigView(node); - equal(bukkit.contains("value"),view.contains("value")); - equal(bukkit.getString("value","fallback"),view.getString("value","fallback")); - equal(bukkit.getBoolean("value",false),view.getBoolean("value",false)); - equal(bukkit.getBoolean("value",true),view.getBoolean("value",true)); - equal(bukkit.getInt("value",7),view.getInt("value",7)); - equal(bukkit.getLong("value",9L),view.getLong("value",9L)); - equal(bukkit.getDouble("value",2.5),view.getDouble("value",2.5)); - equal(bukkit.getStringList("value"),view.getStringList("value")); - equal(bukkit.getIntegerList("value"),view.getIntegerList("value")); - } - for (boolean populated:new boolean[]{false,true}) { - var bukkit=new MemoryConfiguration();var node=BasicConfigurationNode.root(); - if (populated) { - var data=new LinkedHashMap(); - data.put("Host","localhost");data.put("Port",3306);data.put("Username","fixture");data.put("Password","synthetic-fixture"); - data.put("Database","votes");data.put("Prefix","test_");data.put("Name","users");data.put("MaxConnections",0); - data.put("UseMariaDB",true);data.put("UseSSL",true);data.put("PoolName","fixture");data.put("ConnectionTimeout",1000); - for (var entry:data.entrySet()) { bukkit.set(entry.getKey(),entry.getValue()); node.node(entry.getKey()).raw(entry.getValue()); } - } - var oldConfig=new MysqlConfigSpigot(bukkit); - var sharedConfig=new MysqlConfigView(new ConfigurateConfigView(node)); - for (Method method:MysqlConfig.class.getDeclaredMethods()) - if (method.getParameterCount()==0 && (method.getName().startsWith("get") || method.getName().startsWith("is") || method.getName().startsWith("has"))) - equal(method.invoke(oldConfig),method.invoke(sharedConfig)); - } - System.out.println("Bukkit/shared configuration parity: "+checks+" comparisons passed"); - } -} diff --git a/tools/NativeConfigSmoke.java b/tools/NativeConfigSmoke.java deleted file mode 100644 index 6c035fb4..00000000 --- a/tools/NativeConfigSmoke.java +++ /dev/null @@ -1,36 +0,0 @@ -import java.nio.file.Files; -import java.util.List; -import com.bencodez.simpleapi.file.annotation.AnnotationBinder; -import com.bencodez.simpleapi.file.annotation.ConfigDataInt; -import com.bencodez.simpleapi.file.annotation.ConfigDataListString; -import com.bencodez.simpleapi.file.config.configurate.YamlConfigDocument; -import com.bencodez.simpleapi.sql.mysql.config.MysqlConfigView; - -/** Consumer compiled against packaged artifacts, not reactor target/classes. */ -public final class NativeConfigSmoke { - static final class Options { - @ConfigDataInt(path="PointsOnVote") int points; - @ConfigDataListString(path="Rewards.Commands") java.util.ArrayList commands; - } - public static void main(String[] arguments) throws Exception { - var directory=Files.createTempDirectory("simpleapi-native-smoke-"); - var file=directory.resolve("VoteSites.yml"); - try { - var document=YamlConfigDocument.open(file); - document.update("missing", edit -> { - edit.set("PointsOnVote", 3); - edit.set("Rewards.Commands", List.of("give player minecraft:diamond")); - edit.set("MySQL.Host", "localhost"); - }); - var snapshot=YamlConfigDocument.open(file).snapshot(); - var options=new Options(); - new AnnotationBinder().load(snapshot.view(), options); - if (options.points != 3 || options.commands.size() != 1) throw new AssertionError("Native binding failed"); - var sql=new MysqlConfigView(snapshot.view().getConfigurationSection("MySQL")); - if (!"localhost".equals(sql.getHostName()) || sql.getMaxThreads() != 1) throw new AssertionError("Native SQL configuration failed"); - try { Class.forName("org.bukkit.Bukkit"); throw new AssertionError("Bukkit was available"); } - catch (ClassNotFoundException expected) { } - System.out.println("Packaged native consumer: YAML, binder, persistence and SQL configuration passed"); - } finally { Files.deleteIfExists(file); Files.deleteIfExists(directory); } - } -} diff --git a/tools/SharedArtifactProbe.java b/tools/SharedArtifactProbe.java deleted file mode 100644 index 3b99cec6..00000000 --- a/tools/SharedArtifactProbe.java +++ /dev/null @@ -1,41 +0,0 @@ -import java.net.URLClassLoader; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.LinkedHashSet; -import java.util.jar.JarFile; - -/** Link every published SimpleAPI class using only the selected runtime JARs. */ -public final class SharedArtifactProbe { - public static void main(String[] arguments) throws Exception { - var urls=new ArrayList(); - for (String argument:arguments) urls.add(Path.of(argument).toUri().toURL()); - int checked=0; - try (var loader=new URLClassLoader(urls.toArray(java.net.URL[]::new), ClassLoader.getPlatformClassLoader())) { - for (String forbidden:new String[]{"org.bukkit.Bukkit", "net.minecraft.server.MinecraftServer", "net.md_5.bungee.api.ProxyServer", "com.velocitypowered.api.proxy.ProxyServer"}) { - try { loader.loadClass(forbidden); throw new AssertionError("Platform leaked into native runtime: " + forbidden); } - catch (ClassNotFoundException expected) { } - } - var names=new LinkedHashSet(); - for (String argument:arguments) { - try (var jar=new JarFile(argument)) { - for (var entry:jar.stream().toList()) { - String name=entry.getName(); - if (name.startsWith("com/bencodez/simpleapi/") && name.endsWith(".class")) - names.add(name.substring(0,name.length()-6).replace('/', '.')); - } - } - } - if (names.isEmpty()) throw new AssertionError("No shared classes found"); - for (String name:names) { - Class type=Class.forName(name, false, loader); - type.getDeclaredConstructors(); type.getDeclaredFields(); type.getDeclaredMethods(); - type.getGenericSuperclass(); type.getGenericInterfaces(); - for (var method:type.getDeclaredMethods()) { - method.getGenericReturnType(); method.getGenericParameterTypes(); method.getGenericExceptionTypes(); - } - checked++; - } - } - System.out.println("Packaged headless linkage: " + checked + " classes passed"); - } -} diff --git a/tools/verify-shared-artifacts.py b/tools/verify-shared-artifacts.py deleted file mode 100644 index 0773d904..00000000 --- a/tools/verify-shared-artifacts.py +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env python3 -"""Check packaged class boundaries and compile/run a downstream native consumer.""" -from pathlib import Path -import os -import subprocess -import tempfile -import zipfile - -ROOT=Path(__file__).resolve().parents[1] -MODULES=("simpleapi-core", "simpleapi-configurate", "simpleapi-sql") -FORBIDDEN=(b"org/bukkit/", b"org/spigotmc/", b"io/papermc/paper/", b"net/minecraft/", - b"net/fabricmc/", b"net/minecraftforge/", b"net/neoforged/", b"net/md_5/bungee/", - b"com/velocitypowered/api/") - -def main(): - jars=[] - for module in MODULES: - target=ROOT/module/"target" - candidates=[p for p in target.glob(f"{module}-*.jar") if not p.name.endswith(("-sources.jar","-javadoc.jar"))] - if len(candidates)!=1: raise RuntimeError(f"Expected one freshly built artifact for {module}") - jar=candidates[0] - with zipfile.ZipFile(jar) as archive: - classes=[n for n in archive.namelist() if n.endswith(".class")] - if not classes: raise RuntimeError(f"Empty artifact: {jar}") - for name in classes: - data=archive.read(name) - if any(prefix in data or prefix.decode() in name for prefix in FORBIDDEN): - raise RuntimeError(f"Platform dependency in {module}: {name}") - sources=jar.with_name(jar.stem+"-sources.jar") - if not sources.is_file(): raise RuntimeError(f"Missing source artifact: {sources}") - # Copied at build time from the existing implementation, never maintained twice. - for source in (target/"generated-sources/shared").rglob("*.java"): - relative=source.relative_to(target/"generated-sources/shared") - if source.read_bytes()!=(ROOT/"SimpleAPI/src/main/java"/relative).read_bytes(): - raise RuntimeError(f"Shared source drift: {relative}") - jars.append(jar) - jars.extend(sorted((target/"runtime-deps").glob("*.jar"))) - jars=list(dict.fromkeys(p.resolve() for p in jars)) - published_classes={} - for jar in jars: - with zipfile.ZipFile(jar) as archive: - for name in archive.namelist(): - if name.startswith("com/bencodez/simpleapi/") and name.endswith(".class"): - data=archive.read(name) - if name in published_classes and published_classes[name]!=data: - raise RuntimeError(f"Conflicting shared class bytes: {name} in {jar}") - published_classes[name]=data - if any(name.startswith(prefix.decode()) for prefix in FORBIDDEN): - raise RuntimeError(f"Platform classes in native runtime dependency: {jar.name}: {name}") - subprocess.run(["java",str(ROOT/"tools/SharedArtifactProbe.java"),*[str(p) for p in jars]],check=True) - classpath=os.pathsep.join(str(p) for p in jars) - with tempfile.TemporaryDirectory(prefix="simpleapi-consumer-") as output: - subprocess.run(["javac","--release","21","-proc:none","-cp",classpath,"-d",output,str(ROOT/"tools/NativeConfigSmoke.java")],check=True) - subprocess.run(["java","-cp",output+os.pathsep+classpath,"NativeConfigSmoke"],check=True) - legacy_cp_file=ROOT/"SimpleAPI/target/compatibility-classpath.txt" - legacy_cp=legacy_cp_file.read_text().strip() - parity_cp=classpath+os.pathsep+str(ROOT/"SimpleAPI/target/SimpleAPI.jar")+os.pathsep+legacy_cp - subprocess.run(["javac","--release","21","-proc:none","-cp",parity_cp,"-d",output,str(ROOT/"tools/ConfigParityProbe.java")],check=True) - subprocess.run(["java","-cp",output+os.pathsep+parity_cp,"ConfigParityProbe"],check=True) - print("Shared binary/source artifacts and runtime boundaries passed") - -if __name__=="__main__": main()