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.projectlombok lombok ${lombok.version} provided
- org.junit.jupiter junit-jupiter ${junit.version} test
-
-
- org.apache.maven.plugins maven-compiler-plugin 3.15.0
- 21 none
- org.projectlombok lombok ${lombok.version}
-
-
- org.apache.maven.plugins maven-resources-plugin 3.4.0
- org.apache.maven.plugins maven-surefire-plugin 3.5.4
- org.apache.maven.plugins maven-jar-plugin 3.5.0
- org.apache.maven.plugins maven-source-plugin 3.3.1
- org.apache.maven.plugins maven-install-plugin 3.1.4
- org.apache.maven.plugins maven-deploy-plugin 3.1.4
- org.codehaus.mojo build-helper-maven-plugin 3.6.0
-
-
- deploy-shared
- nexus https://nexus.bencodez.com/repository/maven-snapshots/
- nexus https://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.bencodez simpleapi-parent 1.0.2-SNAPSHOT
- simpleapi-configurate
- SimpleAPI Configurate adapter
-
- com.bencodez simpleapi-core ${project.version}
- org.spongepowered configurate-yaml ${configurate.version}
- org.junit.jupiter junit-jupiter
-
-
- org.apache.maven.plugins maven-resources-plugin
- stage-existing-shared-sources generate-sources copy-resources
- ${project.build.directory}/generated-sources/shared
- ${project.basedir}/../SimpleAPI/src/main/java false
- com/bencodez/simpleapi/file/config/configurate/*.java
-
-
-
- org.codehaus.mojo build-helper-maven-plugin
- existing-shared-sources generate-sources add-source
- ${project.build.directory}/generated-sources/shared
-
-
- org.apache.maven.plugins maven-source-plugin
- sources package jar-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.bencodez simpleapi-parent 1.0.2-SNAPSHOT
- simpleapi-core
- simpleapi-core
-
- org.projectlombok lombok true
- org.junit.jupiter junit-jupiter
-
-
-
-
- org.apache.maven.plugins maven-resources-plugin
- stage-existing-shared-sources generate-sources copy-resources
- ${project.build.directory}/generated-sources/shared
- ${project.basedir}/../SimpleAPI/src/main/java false
- 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-tests generate-test-sources copy-resources
- ${project.build.directory}/generated-test-sources/shared
- ${project.basedir}/../SimpleAPI/src/test/java false
- com/bencodez/simpleapi/tests/ParsedDurationTest.java
- com/bencodez/simpleapi/tests/file/config/AnnotationBinderHeadlessTest.java
- com/bencodez/simpleapi/tests/file/config/HeadlessBindingFixture.java
-
-
-
- org.codehaus.mojo build-helper-maven-plugin
- existing-shared-sources generate-sources add-source
- ${project.build.directory}/generated-sources/shared
-
- existing-headless-tests generate-test-sources add-test-source
- ${project.build.directory}/generated-test-sources/shared
-
-
- org.apache.maven.plugins maven-source-plugin
- sources package jar-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.bencodez simpleapi-parent 1.0.2-SNAPSHOT
- simpleapi-sql
- simpleapi-sql
-
- org.projectlombok lombok true
- org.junit.jupiter junit-jupiter
- com.bencodez simpleapi-core ${project.version}
- com.zaxxer HikariCP 7.0.2
- com.bencodez simpleapi-configurate ${project.version} test
-
-
-
- org.apache.maven.plugins maven-resources-plugin
- stage-existing-shared-sources generate-sources copy-resources
- ${project.build.directory}/generated-sources/shared
- ${project.basedir}/../SimpleAPI/src/main/java false
- 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.mojo build-helper-maven-plugin
- existing-shared-sources generate-sources add-source
- ${project.build.directory}/generated-sources/shared
-
-
-
- org.apache.maven.plugins maven-source-plugin
- sources package jar-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()