diff --git a/src/main/java/me/andreasmelone/basicmodinfoparser/jarinjar/JarInJarPlatform.java b/src/main/java/me/andreasmelone/basicmodinfoparser/jarinjar/JarInJarPlatform.java index c51c2c7..e00f22b 100644 --- a/src/main/java/me/andreasmelone/basicmodinfoparser/jarinjar/JarInJarPlatform.java +++ b/src/main/java/me/andreasmelone/basicmodinfoparser/jarinjar/JarInJarPlatform.java @@ -123,7 +123,7 @@ public enum JarInJarPlatform { private final String[] metadataFiles; - private JarInJarPlatform(String... metadataFiles) { + JarInJarPlatform(String... metadataFiles) { this.metadataFiles = metadataFiles; } diff --git a/src/main/java/me/andreasmelone/basicmodinfoparser/modfile/DependencyChecker.java b/src/main/java/me/andreasmelone/basicmodinfoparser/modfile/DependencyChecker.java index 9433b9f..65de783 100644 --- a/src/main/java/me/andreasmelone/basicmodinfoparser/modfile/DependencyChecker.java +++ b/src/main/java/me/andreasmelone/basicmodinfoparser/modfile/DependencyChecker.java @@ -29,6 +29,8 @@ import me.andreasmelone.basicmodinfoparser.platform.dependency.PresenceStatus; import me.andreasmelone.basicmodinfoparser.platform.dependency.fabric.LooseSemanticVersion; import me.andreasmelone.basicmodinfoparser.platform.dependency.forge.MavenVersion; +import me.andreasmelone.basicmodinfoparser.platform.dependency.version.LooseSemanticVersionFactory; +import me.andreasmelone.basicmodinfoparser.platform.dependency.version.MavenVersionFactory; import me.andreasmelone.basicmodinfoparser.platform.modinfo.StandardBasicModInfo; import me.andreasmelone.basicmodinfoparser.util.Pair; @@ -54,15 +56,28 @@ public static Pair> checkDependencies(S boolean isFabricBased = loaderInfo.getPlatform() == Platform.FABRIC || loaderInfo.getPlatform() == Platform.QUILT; if (isFabricBased) { javaInfo = new StandardBasicModInfo( - "java", "Java", - LooseSemanticVersion.parse(javaVersion).orElse(null), - "Java", new ArrayList<>(), null, Platform.FABRIC + "java", + "Java", + new LooseSemanticVersionFactory().parseVersion(javaVersion).orElse(null), + "Java", + new ArrayList<>(), + null, + Platform.FABRIC, + null ); } BasicModInfo gameInfo = new StandardBasicModInfo( - "minecraft", "Minecraft", - (isFabricBased ? LooseSemanticVersion.parse(gameVersion) : MavenVersion.parse(gameVersion)).orElse(null), - "Minecraft", new ArrayList<>(), null, loaderInfo.getPlatform() + "minecraft", + "Minecraft", + (isFabricBased + ? new LooseSemanticVersionFactory().parseVersion(gameVersion) + : new MavenVersionFactory().parseVersion(gameVersion)) + .orElse(null), + "Minecraft", + new ArrayList<>(), + null, + loaderInfo.getPlatform(), + null ); Map dependencyMap = new HashMap<>(); diff --git a/src/main/java/me/andreasmelone/basicmodinfoparser/platform/BasicModInfo.java b/src/main/java/me/andreasmelone/basicmodinfoparser/platform/BasicModInfo.java index af86f85..589f190 100644 --- a/src/main/java/me/andreasmelone/basicmodinfoparser/platform/BasicModInfo.java +++ b/src/main/java/me/andreasmelone/basicmodinfoparser/platform/BasicModInfo.java @@ -50,7 +50,7 @@ public interface BasicModInfo { * * @return the version of the mod */ - @Nullable Version getVersion(); + @Nullable Version getVersion(); /** * The mods description @@ -76,4 +76,9 @@ public interface BasicModInfo { * @return the platform that this mod is for */ @NotNull Platform getPlatform(); + + /** + * @return the authors of the mod + */ + @Nullable List getAuthors(); } diff --git a/src/main/java/me/andreasmelone/basicmodinfoparser/platform/Platform.java b/src/main/java/me/andreasmelone/basicmodinfoparser/platform/Platform.java index a82f4a6..cb7961f 100644 --- a/src/main/java/me/andreasmelone/basicmodinfoparser/platform/Platform.java +++ b/src/main/java/me/andreasmelone/basicmodinfoparser/platform/Platform.java @@ -29,18 +29,28 @@ import me.andreasmelone.abstractzip.IZipEntry; import me.andreasmelone.abstractzip.IZipFile; import me.andreasmelone.abstractzip.IZipFileFactory; -import me.andreasmelone.basicmodinfoparser.platform.dependency.Dependency; import me.andreasmelone.basicmodinfoparser.platform.dependency.ProvidedMod; -import me.andreasmelone.basicmodinfoparser.platform.dependency.StandardDependency; -import me.andreasmelone.basicmodinfoparser.platform.dependency.fabric.FabricVersionRange; import me.andreasmelone.basicmodinfoparser.platform.dependency.fabric.LooseSemanticVersion; import me.andreasmelone.basicmodinfoparser.platform.dependency.forge.MavenVersion; +import me.andreasmelone.basicmodinfoparser.platform.dependency.parser.FabricDependencyParser; +import me.andreasmelone.basicmodinfoparser.platform.dependency.parser.ForgeDependencyParser; +import me.andreasmelone.basicmodinfoparser.platform.dependency.parser.LegacyForgeDependencyParser; +import me.andreasmelone.basicmodinfoparser.platform.dependency.parser.QuiltDependencyParser; +import me.andreasmelone.basicmodinfoparser.platform.dependency.version.LooseSemanticVersionFactory; +import me.andreasmelone.basicmodinfoparser.platform.dependency.version.MavenVersionFactory; import me.andreasmelone.basicmodinfoparser.platform.modinfo.FabricModInfo; import me.andreasmelone.basicmodinfoparser.platform.modinfo.StandardBasicModInfo; +import me.andreasmelone.basicmodinfoparser.platform.modinfo.model.ModInfoKeys; import me.andreasmelone.basicmodinfoparser.util.ModInfoParseException; import me.andreasmelone.basicmodinfoparser.util.ParserUtils; +import me.andreasmelone.basicmodinfoparser.util.adapter.JsonAdapter; +import me.andreasmelone.basicmodinfoparser.util.adapter.TomlAdapter; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.tomlj.Toml; +import org.tomlj.TomlArray; +import org.tomlj.TomlParseResult; +import org.tomlj.TomlTable; import java.io.File; import java.io.IOException; @@ -49,39 +59,47 @@ import java.util.Arrays; import java.util.List; import java.util.Optional; -import java.util.stream.StreamSupport; -import static me.andreasmelone.basicmodinfoparser.util.ParserUtils.*; +import static me.andreasmelone.basicmodinfoparser.platform.modinfo.model.ModInfoKeys.forgeKeys; +import static me.andreasmelone.basicmodinfoparser.util.ParserUtils.GSON; +import static me.andreasmelone.basicmodinfoparser.util.ParserUtils.readEverythingAsString; public enum Platform { /** * Legacy Forge platform, which uses the {@code mcmod.info} file containing JSON data. */ - FORGE_LEGACY("mcmod.info") { + FORGE_LEGACY( + new ModInfoKeys( + "modid", + "name", + "version", + "description", + "logoFile", + "authorList", + new String[]{"dependencies"} + ), + "mcmod.info" + ) { @Override protected BasicModInfo[] parseFileData(String fileData) { + // In Legacy Forge, you can specify multiple mods per file (the root element is an array) JsonArray topArray = GSON.fromJson(fileData, JsonArray.class); + if (topArray == null || topArray.size() == 0) { return StandardBasicModInfo.emptyArray(); } + // Since the mod list is not empty, we iterate over all mods and parse each one List parsedInfos = new ArrayList<>(); for (JsonElement topArrayElement : topArray) { if (!topArrayElement.isJsonObject()) continue; + JsonObject modObject = topArrayElement.getAsJsonObject(); - List dependencyList = new ArrayList<>(); - if (modObject.has("dependencies") && modObject.get("dependencies").isJsonArray()) { - JsonArray dependenciesArray = modObject.getAsJsonArray("dependencies"); - for (JsonElement element : dependenciesArray) { - if (!element.isJsonPrimitive() || !element.getAsJsonPrimitive().isString()) { - continue; - } - parseLegacyForgeDependency(element.getAsString()).ifPresent(dependencyList::add); - } - } - parsedInfos.add(ParserUtils.createForgeModInfoFromJsonObject(modObject, - "modid", "name", "version", "description", "logoFile", - dependencyList, this + parsedInfos.add(ParserUtils.createModInfoFrom( + new JsonAdapter(modObject), + this, + new LegacyForgeDependencyParser(), + new MavenVersionFactory() )); } @@ -96,15 +114,37 @@ protected BasicModInfo[] parseFileData(String fileData) { /** * Forge platform, which uses the {@code mods.toml} file with TOML data. */ - FORGE("META-INF/mods.toml") { + FORGE(forgeKeys(), "META-INF/mods.toml") { @Override protected BasicModInfo[] parseFileData(String fileData) { - return ParserUtils.parseForgelikeInfo(fileData, this); + final TomlParseResult parseResult = Toml.parse(fileData); + + final TomlArray mods = parseResult.getArray("mods"); + if (mods == null) return StandardBasicModInfo.emptyArray(); + + final List modInfos = new ArrayList<>(); + // I transferred Forge-specific logic to the Platform Enumeration + // (the capacity to provide multiple mods per file) + for (int i = 0; i < mods.size(); i++) { + final TomlTable modInfo = mods.getTable(i); + if (modInfo.isEmpty()) continue; + + final BasicModInfo info = ParserUtils.createModInfoFrom( + new TomlAdapter(modInfo), + this, + new ForgeDependencyParser(), + new MavenVersionFactory() + ); + + modInfos.add(info); + } + + return modInfos.toArray(new BasicModInfo[0]); } @Override protected BasicModInfo createNullableLoaderInfo(String loaderVersion) { - Optional version = MavenVersion.parse(loaderVersion); + Optional version = new MavenVersionFactory().parseVersion(loaderVersion); return new StandardBasicModInfo( "forge", "Forge", @@ -112,22 +152,44 @@ protected BasicModInfo createNullableLoaderInfo(String loaderVersion) { "Modifications to the Minecraft base files to assist in compatibility between mods.", new ArrayList<>(), null, - this + this, + new ArrayList<>() ); } }, /** * NeoForge platform, which uses the {@code neoforge.mods.toml} file with TOML data, similarly to {@link Platform#FORGE}. */ - NEOFORGE("META-INF/neoforge.mods.toml") { + NEOFORGE(forgeKeys(), "META-INF/neoforge.mods.toml") { @Override protected @NotNull BasicModInfo[] parseFileData(String fileData) { - return ParserUtils.parseForgelikeInfo(fileData, this); + final TomlParseResult parsedFileData = Toml.parse(fileData); + + final TomlArray mods = parsedFileData.getArray("mods"); + if (mods == null) return StandardBasicModInfo.emptyArray(); + + final BasicModInfo[] result = new BasicModInfo[mods.size()]; + + for (int i = 0; i < mods.size(); i++) { + final TomlTable currentTable = mods.getTable(i); + if (currentTable.isEmpty()) continue; + + final BasicModInfo info = ParserUtils.createModInfoFrom( + new TomlAdapter(currentTable), + this, + new ForgeDependencyParser(), + new MavenVersionFactory() + ); + + result[i] = info; + } + + return result; } @Override protected @NotNull BasicModInfo createNullableLoaderInfo(String loaderVersion) { - Optional version = MavenVersion.parse(loaderVersion); + Optional version = new MavenVersionFactory().parseVersion(loaderVersion); return new StandardBasicModInfo( "neoforge", "NeoForge", @@ -135,48 +197,84 @@ protected BasicModInfo createNullableLoaderInfo(String loaderVersion) { "NeoForge is a free, open-source, community-oriented modding API for Minecraft.", new ArrayList<>(), null, - this + this, + new ArrayList<>() ); } }, + /** * Fabric platform, which uses the {@code fabric.mod.json} file. As the extension suggests, it stores data in JSON format. */ - FABRIC("fabric.mod.json") { + FABRIC( + new ModInfoKeys( + "id", + "name", + "version", + "description", + "icon", + "authors", + new String[]{ + "depends", + "recommends", + "suggests", + "breaks", + "conflicts" + } + ), + "fabric.mod.json" + ) { @Override protected BasicModInfo[] parseFileData(String fileData) { JsonElement root = GSON.fromJson(fileData, JsonElement.class); if (root == null || (!root.isJsonArray() && !root.isJsonObject())) { return StandardBasicModInfo.emptyArray(); } + + // Fabric 0.4.0 or older allowed the root object to be an array + // (https://wiki.fabricmc.net/documentation:fabric_mod_json_spec#fabricmodjson_specification) JsonArray jsonArray = root.isJsonArray() ? root.getAsJsonArray() : new JsonArray(); + // Treat everything as a Json Array so we can iterate over it (Even if there's only one element) if (root.isJsonObject()) { jsonArray.add(root.getAsJsonObject()); } + // Now that we have the mod list, we iterate over all mods and parse each one List parsedInfos = new ArrayList<>(); for (JsonElement jsonArrayElement : jsonArray) { if (!jsonArrayElement.isJsonObject()) continue; - JsonObject jsonObject = jsonArrayElement.getAsJsonObject(); - - Optional version = LooseSemanticVersion.parse(getValidString(jsonObject, "version")); - - List dependencyList = new ArrayList<>(); - ParserUtils.parseFabricDependencies(dependencyList, jsonObject, "depends", true); - ParserUtils.parseFabricDependencies(dependencyList, jsonObject, "recommends", false); - List breaksList = new ArrayList<>(); - ParserUtils.parseFabricDependencies(breaksList, jsonObject, "breaks", true); - List> provided = new ArrayList<>(); - if (jsonObject.has("provides") && jsonObject.get("provides").isJsonArray()) { - for (JsonElement dependency : jsonObject.getAsJsonArray("provides")) { + final JsonObject modObject = jsonArrayElement.getAsJsonObject(); + + final BasicModInfo current = ParserUtils.createModInfoFrom( + new JsonAdapter(modObject), + this, + new FabricDependencyParser(), + new LooseSemanticVersionFactory() + ); + + List providedMods = new ArrayList<>(); + if (modObject.has("provides") && modObject.get("provides").isJsonArray()) { + for (JsonElement dependency : modObject.getAsJsonArray("provides")) { if (!dependency.isJsonPrimitive() || !dependency.getAsJsonPrimitive().isString()) continue; - provided.add(new ProvidedMod<>(dependency.getAsString(), version.orElse(null))); + providedMods.add(new ProvidedMod(dependency.getAsString(), current.getVersion())); } } - parsedInfos.add(ParserUtils.createFabricModInfoFromJsonObject(jsonObject, - "id", "name", version.orElse(null), "description", "icon", - dependencyList, breaksList, provided, this)); + // Create a FabricModInfo with the above parsed information + // and add it to the list of parsed infos + parsedInfos.add( + new FabricModInfo( + current.getId(), + current.getName(), + current.getVersion(), + current.getDescription(), + current.getDependencies(), + current.getIconPath(), + current.getPlatform(), + current.getAuthors(), + providedMods + ) + ); } return parsedInfos.toArray(new BasicModInfo[0]); @@ -184,7 +282,7 @@ protected BasicModInfo[] parseFileData(String fileData) { @Override protected @NotNull BasicModInfo createNullableLoaderInfo(String loaderVersion) { - Optional version = LooseSemanticVersion.parse(loaderVersion); + Optional version = new LooseSemanticVersionFactory().parseVersion(loaderVersion); return new StandardBasicModInfo( "fabricloader", "Fabric Loader", @@ -192,14 +290,29 @@ protected BasicModInfo[] parseFileData(String fileData) { "A flexible platform-independent mod loader designed for Minecraft and other games and applications.", new ArrayList<>(), null, - this + this, + new ArrayList<>() ); } }, + /** * Quilt platform, which uses the {@code quilt.mod.json} file. As the extensions suggests, it stores data in the JSON format. */ - QUILT("quilt.mod.json") { + QUILT( + new ModInfoKeys( + "id", + "metadata.name", + "version", + "metadata.description", + "metadata.icon", + "metadata.contributors", + new String[]{ + "depends", "breaks" + } + ), + "quilt.mod.json" + ) { @Override protected BasicModInfo[] parseFileData(String fileData) { JsonObject jsonObj = GSON.fromJson(fileData, JsonObject.class); @@ -207,110 +320,48 @@ protected BasicModInfo[] parseFileData(String fileData) { if (!jsonObj.has("quilt_loader") || !jsonObj.get("quilt_loader").isJsonObject()) return StandardBasicModInfo.emptyArray(); - JsonObject quiltLoader = jsonObj.getAsJsonObject("quilt_loader"); - String modId = getValidString(quiltLoader, "id"); - String version = getValidString(quiltLoader, "version"); - - String name = null; - String description = null; - String iconPath = null; - if (quiltLoader.has("metadata") && quiltLoader.get("metadata").isJsonObject()) { - JsonObject metadata = quiltLoader.getAsJsonObject("metadata"); - name = getValidString(metadata, "name"); - description = getValidString(metadata, "description"); - iconPath = getValidString(metadata, "icon"); - } - List dependencies = new ArrayList<>(); - if (quiltLoader.has("depends") && quiltLoader.get("depends").isJsonArray()) { - for (JsonElement dependency : quiltLoader.getAsJsonArray("depends")) { - if (!dependency.isJsonObject()) continue; - JsonObject dependencyObject = dependency.getAsJsonObject(); - String dependencyId = getValidString(dependencyObject, "id"); - boolean isMandatory = true; - if (dependencyObject.has("optional") - && dependencyObject.get("optional").isJsonPrimitive() - && dependencyObject.get("optional").getAsJsonPrimitive().isString()) { - isMandatory = !dependencyObject.get("optional").getAsBoolean(); - } - String[] versions = new String[]{"*"}; - - if (dependencyObject.has("versions")) { - JsonElement dependencyVersion = dependencyObject.get("versions"); - if (dependencyVersion.isJsonPrimitive() && dependencyVersion.getAsJsonPrimitive().isString()) { - versions[0] = dependencyVersion.getAsString(); - } else if (dependencyVersion.isJsonArray()) { - versions = StreamSupport.stream(dependencyVersion.getAsJsonArray().spliterator(), false) - .filter((el) -> el.isJsonPrimitive() && el.getAsJsonPrimitive().isString()) - .map(JsonElement::getAsString) - .toArray(String[]::new); - } - } - - Optional fabricVersionRange = FabricVersionRange.parse(versions); - dependencies.add(new StandardDependency<>(dependencyId, isMandatory, fabricVersionRange.orElse(null))); - } - } - - List breaks = new ArrayList<>(); - if (quiltLoader.has("breaks") && quiltLoader.get("breaks").isJsonArray()) { - for (JsonElement dependency : quiltLoader.getAsJsonArray("breaks")) { - if (!dependency.isJsonObject()) continue; - JsonObject dependencyObject = dependency.getAsJsonObject(); - String dependencyId = getValidString(dependencyObject, "id"); - boolean isMandatory = true; - String[] versions = new String[]{"*"}; - - if (dependencyObject.has("versions")) { - JsonElement dependencyVersion = dependencyObject.get("versions"); - if (dependencyVersion.isJsonPrimitive() && dependencyVersion.getAsJsonPrimitive().isString()) { - versions[0] = dependencyVersion.getAsString(); - } else if (dependencyVersion.isJsonArray()) { - versions = StreamSupport.stream(dependencyVersion.getAsJsonArray().spliterator(), false) - .filter((el) -> el.isJsonPrimitive() && el.getAsJsonPrimitive().isString()) - .map(JsonElement::getAsString) - .toArray(String[]::new); - } - } - - Optional fabricVersionRange = FabricVersionRange.parse(versions); - breaks.add(new StandardDependency<>(dependencyId, isMandatory, fabricVersionRange.orElse(null))); - } - } + // Get the main quilt loader object + JsonAdapter quiltLoader = new JsonAdapter( + jsonObj.getAsJsonObject("quilt_loader") + ); - List> provides = new ArrayList<>(); - if (quiltLoader.has("provides") && quiltLoader.get("provides").isJsonArray()) { - for (JsonElement dependency : quiltLoader.getAsJsonArray("provides")) { - if (!dependency.isJsonObject()) continue; - JsonObject dependencyObject = dependency.getAsJsonObject(); - String dependencyId = getValidString(dependencyObject, "id"); - String providedVersion = version; - - if (dependencyObject.has("versions")) { - JsonElement dependencyVersion = dependencyObject.get("versions"); - if (dependencyVersion.isJsonPrimitive() && dependencyVersion.getAsJsonPrimitive().isString()) { - providedVersion = dependencyVersion.getAsString(); - } - } + // NOTE: The "provides" logic had no effect ultimately. The provides list was being updated, but was not + // being used anywhere else. Should we make it a property inside BasicModInfo so it's usable? + + // List> provides = new ArrayList<>(); + // if (quiltLoader.has("provides") && quiltLoader.get("provides").isJsonArray()) { + // for (JsonElement dependency : quiltLoader.getAsJsonArray("provides")) { + // if (!dependency.isJsonObject()) continue; + // JsonObject dependencyObject = dependency.getAsJsonObject(); + // String dependencyId = getValidString(dependencyObject, "id"); + // String providedVersion = version; + // + // if (dependencyObject.has("versions")) { + // JsonElement dependencyVersion = dependencyObject.get("versions"); + // if (dependencyVersion.isJsonPrimitive() && dependencyVersion.getAsJsonPrimitive().isString()) { + // providedVersion = dependencyVersion.getAsString(); + // } + // } + // + // Optional semVer = LooseSemanticVersion.parse(providedVersion); + // provides.add(new ProvidedMod<>(dependencyId, semVer.orElse(null))); + // } + // } - Optional semVer = LooseSemanticVersion.parse(providedVersion); - provides.add(new ProvidedMod<>(dependencyId, semVer.orElse(null))); - } - } - - Optional semanticVersion = LooseSemanticVersion.parse(version, false); return new BasicModInfo[]{ - new FabricModInfo(modId, name, - semanticVersion.orElse(null), - description, dependencies, iconPath, this, - breaks, provides + ParserUtils.createModInfoFrom( + quiltLoader, + this, + new QuiltDependencyParser(), + new LooseSemanticVersionFactory() ) }; } @Override protected @NotNull BasicModInfo createNullableLoaderInfo(String loaderVersion) { - Optional version = LooseSemanticVersion.parse(loaderVersion); + Optional version = new LooseSemanticVersionFactory().parseVersion(loaderVersion); return new StandardBasicModInfo( "quilt_loader", "Quilt Loader", @@ -318,14 +369,18 @@ protected BasicModInfo[] parseFileData(String fileData) { "The loader for mods under Quilt. It provides mod loading facilities and useful abstractions for other mods to use.", new ArrayList<>(), null, - this + this, + new ArrayList<>() ); } }; + protected final ModInfoKeys modInfoKeys; + private final String[] infoFilePaths; - private Platform(String... infoFilePaths) { + Platform(ModInfoKeys modInfoKeys, String... infoFilePaths) { + this.modInfoKeys = modInfoKeys; this.infoFilePaths = infoFilePaths; } @@ -370,7 +425,7 @@ public Optional getInfoFileContent(IZipFile zip) throws IOException { IZipEntry infoFileEntry = zip.findEntry(infoFilePath); if (infoFileEntry == null) continue; try (InputStream entry = zip.openEntry(infoFileEntry)) { - if(entry == null) return Optional.empty(); + if (entry == null) return Optional.empty(); return Optional.of(readEverythingAsString(entry)); } } @@ -446,4 +501,8 @@ public static Platform[] findModPlatform(IZipFile zip) throws IOException { } return platforms.toArray(new Platform[0]); } + + public ModInfoKeys getModInfoKeys() { + return modInfoKeys; + } } diff --git a/src/main/java/me/andreasmelone/basicmodinfoparser/platform/dependency/ProvidedMod.java b/src/main/java/me/andreasmelone/basicmodinfoparser/platform/dependency/ProvidedMod.java index a36a7bc..2ed6ec8 100644 --- a/src/main/java/me/andreasmelone/basicmodinfoparser/platform/dependency/ProvidedMod.java +++ b/src/main/java/me/andreasmelone/basicmodinfoparser/platform/dependency/ProvidedMod.java @@ -27,11 +27,11 @@ import java.util.Objects; -public class ProvidedMod> { +public class ProvidedMod { private final String id; - private final Version version; + private final Version version; - public ProvidedMod(String id, Version version) { + public ProvidedMod(String id, Version version) { this.id = id; this.version = version; } @@ -40,7 +40,7 @@ public String getId() { return id; } - public Version getVersion() { + public Version getVersion() { return version; } @@ -55,7 +55,7 @@ public String toString() { @Override public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) return false; - ProvidedMod that = (ProvidedMod) o; + ProvidedMod that = (ProvidedMod) o; return Objects.equals(id, that.id) && Objects.equals(version, that.version); } diff --git a/src/main/java/me/andreasmelone/basicmodinfoparser/platform/dependency/StandardDependency.java b/src/main/java/me/andreasmelone/basicmodinfoparser/platform/dependency/StandardDependency.java index 0cf2422..4c3fb75 100644 --- a/src/main/java/me/andreasmelone/basicmodinfoparser/platform/dependency/StandardDependency.java +++ b/src/main/java/me/andreasmelone/basicmodinfoparser/platform/dependency/StandardDependency.java @@ -31,7 +31,7 @@ import java.util.*; -public class StandardDependency> implements Dependency { +public class StandardDependency implements Dependency { protected final String id; protected final boolean mandatory; protected final VersionRange range; @@ -64,10 +64,10 @@ public boolean isMandatory() { if (mod instanceof ProvidesList) { ProvidesList providesList = (ProvidesList) mod; if (this.range == null || providesList.getType().isAssignableFrom(range.getType())) { - List> innerMods = new ArrayList<>(Optional.ofNullable(((ProvidesList) providesList).getProvidedIds()) + List innerMods = new ArrayList<>(Optional.ofNullable(((ProvidesList) providesList).getProvidedIds()) .orElse(Collections.emptyList())); - for (ProvidedMod innerMod : innerMods) { + for (ProvidedMod innerMod : innerMods) { if (innerMod.getId() == null || !innerMod.getId().equalsIgnoreCase(this.getModId())) continue; if (innerMod.getVersion() == null || this.range == null || !range.getType().isInstance(mod.getVersion())) { diff --git a/src/main/java/me/andreasmelone/basicmodinfoparser/platform/dependency/fabric/FabricVersionRange.java b/src/main/java/me/andreasmelone/basicmodinfoparser/platform/dependency/fabric/FabricVersionRange.java index 0d743be..367a8ac 100644 --- a/src/main/java/me/andreasmelone/basicmodinfoparser/platform/dependency/fabric/FabricVersionRange.java +++ b/src/main/java/me/andreasmelone/basicmodinfoparser/platform/dependency/fabric/FabricVersionRange.java @@ -23,6 +23,7 @@ */ package me.andreasmelone.basicmodinfoparser.platform.dependency.fabric; +import me.andreasmelone.basicmodinfoparser.platform.dependency.version.LooseSemanticVersionFactory; import me.andreasmelone.basicmodinfoparser.platform.dependency.version.VersionRange; import java.util.*; @@ -106,7 +107,7 @@ public static Optional parse(String... version) { if (operators == null || versionString == null || versionString.isEmpty()) continue; } - Optional parsedVersion = LooseSemanticVersion.parse(versionString, true); + Optional parsedVersion = new LooseSemanticVersionFactory().parseVersion(versionString, true); if (!parsedVersion.isPresent()) continue; if (operators.isEmpty()) operators.add(Operator.EQUALS); @@ -167,7 +168,8 @@ public boolean matches(LooseSemanticVersion version) { return true; } - if (operator == Operator.CARET || (operator == Operator.TILDE && this.version.getWildcardPositions().contains(1))) { + if (operator == Operator.CARET || (operator == Operator.TILDE && this.version.getWildcardPositions() + .contains(1))) { boolean isAboveLower = this.version.compareTo(version) <= 0; LooseSemanticVersion upperBound = this.version.increaseMajor(1); diff --git a/src/main/java/me/andreasmelone/basicmodinfoparser/platform/dependency/fabric/LooseSemanticVersion.java b/src/main/java/me/andreasmelone/basicmodinfoparser/platform/dependency/fabric/LooseSemanticVersion.java index c4a098f..2173388 100644 --- a/src/main/java/me/andreasmelone/basicmodinfoparser/platform/dependency/fabric/LooseSemanticVersion.java +++ b/src/main/java/me/andreasmelone/basicmodinfoparser/platform/dependency/fabric/LooseSemanticVersion.java @@ -26,18 +26,15 @@ import me.andreasmelone.basicmodinfoparser.platform.dependency.version.Version; import org.jetbrains.annotations.NotNull; -import java.util.*; -import java.util.regex.Matcher; -import java.util.regex.Pattern; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; /** * Represents a looser version of the SemVer 2.0, which is accepted by fabric */ -public class LooseSemanticVersion implements Version { - private static final Pattern ALPHANUMERIC = Pattern.compile("[a-zA-Z0-9_\\-.+*]+"); - private static final Pattern REGEX = Pattern.compile("^([0-9xX*]+(?:\\.[0-9xX*]+)*)(-.*?)?(\\+.+)?$", Pattern.MULTILINE); - - private final String stringRepresentation; +public class LooseSemanticVersion extends Version { private final int[] versionParts; private final List wildcardPositions; private final String preReleaseSuffix; @@ -45,8 +42,16 @@ public class LooseSemanticVersion implements Version { private final String buildMetadata; private final boolean usesWildcards; - public LooseSemanticVersion(String stringRepresentation, int[] versionParts, List wildcardPositions, String preReleaseSuffix, Integer preReleaseNumber, String buildMetadata, boolean usesWildcards) { - this.stringRepresentation = stringRepresentation; + public LooseSemanticVersion( + String stringRepresentation, + int[] versionParts, + List wildcardPositions, + String preReleaseSuffix, + Integer preReleaseNumber, + String buildMetadata, + boolean usesWildcards + ) { + super(stringRepresentation); this.versionParts = versionParts; this.wildcardPositions = wildcardPositions; this.preReleaseSuffix = preReleaseSuffix; @@ -109,21 +114,26 @@ public LooseSemanticVersion increasePatch(int amount) { } @Override - public int compareTo(@NotNull LooseSemanticVersion other) { - int suffixless = partComparison(other); + public int compareTo(@NotNull Version other) { + if (!(other instanceof LooseSemanticVersion)) { + return -1; + } - if (isNull(preReleaseSuffix) && isNull(other.preReleaseSuffix)) { + LooseSemanticVersion castOther = (LooseSemanticVersion) other; + int suffixless = partComparison(castOther); + + if (isNull(preReleaseSuffix) && isNull(castOther.preReleaseSuffix)) { return suffixless; } else if (suffixless == 0) { if (isNull(preReleaseSuffix)) return 1; - if (isNull(other.preReleaseSuffix)) return -1; + if (isNull(castOther.preReleaseSuffix)) return -1; - int suffix = suffixComparison(other); + int suffix = suffixComparison(castOther); - if (this.preReleaseNumber == null && other.preReleaseNumber != null) return -1; - if (this.preReleaseNumber != null && other.preReleaseNumber == null) return 1; - if (this.preReleaseNumber != null && other.preReleaseNumber != null && suffix == 0) - return Integer.compare(this.preReleaseNumber, other.preReleaseNumber); + if (this.preReleaseNumber == null && castOther.preReleaseNumber != null) return -1; + if (this.preReleaseNumber != null && castOther.preReleaseNumber == null) return 1; + if (this.preReleaseNumber != null && castOther.preReleaseNumber != null && suffix == 0) + return Integer.compare(this.preReleaseNumber, castOther.preReleaseNumber); return suffix; } @@ -197,73 +207,8 @@ public int hashCode() { return Objects.hash(Arrays.hashCode(versionParts), wildcardPositions, preReleaseSuffix, preReleaseNumber, buildMetadata, usesWildcards); } - public static Optional parse(String ver) { - return parse(ver, false); - } - - public static Optional parse(String ver, boolean wildcards) { - if (ver == null || ver.isEmpty() || !ALPHANUMERIC.matcher(ver).matches()) return Optional.empty(); - - Matcher matcher = REGEX.matcher(ver); - if (!matcher.matches()) return Optional.empty(); - String numbers = matcher.group(1); - String prerelease = matcher.group(2); - String metadata = matcher.group(3); - - if (prerelease != null && !prerelease.isEmpty()) { - prerelease = prerelease.substring(1); - } - - if (metadata != null && !metadata.isEmpty()) { - metadata = metadata.substring(1); - } - - String[] splitNumbers = numbers.split("\\."); - int[] versionInts = new int[splitNumbers.length]; - List wildcardPositions = new ArrayList<>(); - - for (int i = 0; i < splitNumbers.length; i++) { - String num = splitNumbers[i]; - if (num.equalsIgnoreCase("x") || num.equals("*")) { - if (!wildcards) return Optional.empty(); - versionInts[i] = 0; - wildcardPositions.add(i); - continue; - } - try { - versionInts[i] = Integer.parseUnsignedInt(num); - } catch (NumberFormatException ignored) { - return Optional.empty(); - } - } - - Integer prereleaseNumber = null; - if (prerelease != null) { - String[] prereleaseSplit = prerelease.split("\\.", 2); - if (prereleaseSplit.length > 1) { - try { - prereleaseNumber = Integer.parseInt(prereleaseSplit[1]); - prerelease = prereleaseSplit[0]; - } catch (NumberFormatException ignored) { - } - } - } - - return new LooseSemanticVersion( - ver, - versionInts, wildcardPositions, - prerelease, - prereleaseNumber, - metadata, wildcards).optional(); - } - @Override public String getStringRepresentation() { return this.stringRepresentation; } - - @Override - public Class getType() { - return LooseSemanticVersion.class; - } } diff --git a/src/main/java/me/andreasmelone/basicmodinfoparser/platform/dependency/forge/MavenVersion.java b/src/main/java/me/andreasmelone/basicmodinfoparser/platform/dependency/forge/MavenVersion.java index 6d0e572..fafd68c 100644 --- a/src/main/java/me/andreasmelone/basicmodinfoparser/platform/dependency/forge/MavenVersion.java +++ b/src/main/java/me/andreasmelone/basicmodinfoparser/platform/dependency/forge/MavenVersion.java @@ -27,32 +27,36 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.*; -import java.util.regex.Matcher; -import java.util.regex.Pattern; +import java.util.Arrays; +import java.util.Objects; /** * This class partially implements mavens version format: ComparableVersion *

* The format is quite complex, so this is not a fully compliant implementation. */ -public class MavenVersion implements Version { - private static final Pattern ALPHANUMERIC = Pattern.compile("[a-zA-Z0-9_\\-.]+"); - private static final Pattern STRING_PARSER = Pattern.compile("^(\\d*)?(\\D+)(\\d*)?$"); - - private final String stringRepresentation; +public class MavenVersion extends Version { private final VersionSegment[] versionSegments; - public MavenVersion(String stringRepresentation, VersionSegment[] versionSegments) { - this.stringRepresentation = stringRepresentation; + public MavenVersion( + String versionString, + VersionSegment[] versionSegments + ) { + super(versionString); this.versionSegments = versionSegments; } @Override - public int compareTo(@NotNull MavenVersion other) { - int lowestAmount = Math.min(this.versionSegments.length, other.versionSegments.length); + public int compareTo(@NotNull Version other) { + if (!(other instanceof MavenVersion)) { + return -1; + } + + MavenVersion castOther = (MavenVersion) other; + + int lowestAmount = Math.min(this.versionSegments.length, castOther.versionSegments.length); for (int i = 0; i < lowestAmount; i++) { - int cmp = this.versionSegments[i].compareTo(other.versionSegments[i]); + int cmp = this.versionSegments[i].compareTo(castOther.versionSegments[i]); if (cmp != 0) { return cmp; } @@ -62,60 +66,15 @@ public int compareTo(@NotNull MavenVersion other) { for (int i = lowestAmount; i < this.versionSegments.length; i++) { if (this.versionSegments[i].isGreater(VersionSegment.NumberVersionSegment.ZERO)) return 1; } - } else if (other.versionSegments.length > lowestAmount) { - for (int i = lowestAmount; i < other.versionSegments.length; i++) { - if (other.versionSegments[i].isGreater(VersionSegment.NumberVersionSegment.ZERO)) return -1; + } else if (castOther.versionSegments.length > lowestAmount) { + for (int i = lowestAmount; i < castOther.versionSegments.length; i++) { + if (castOther.versionSegments[i].isGreater(VersionSegment.NumberVersionSegment.ZERO)) return -1; } } return 0; } - public static Optional parse(String version) { - if (version == null || version.isEmpty() || !ALPHANUMERIC.matcher(version).matches()) return Optional.empty(); - - List segments = new ArrayList<>(); - - String noHyphens = version.replace("-", "."); - String[] splitByDot = noHyphens.split("\\."); - for (String segment : splitByDot) { - Matcher matcher = STRING_PARSER.matcher(segment); - if (matcher.matches()) { - String firstNumber = matcher.group(1); - String string = matcher.group(2); - String secondNumber = matcher.group(3); - - if (firstNumber != null && !firstNumber.isEmpty()) { - try { - segments.add(new VersionSegment.NumberVersionSegment(Integer.parseUnsignedInt(firstNumber))); - } catch (NumberFormatException ignored) { - } - } - if (string != null && !string.isEmpty()) { - VersionSegment.QualifierVersionSegment.Qualifier qualifier = VersionSegment.QualifierVersionSegment.Qualifier.getByName(string); - if (qualifier == null) { - segments.add(new VersionSegment.StringVersionSegment(string)); - } else { - segments.add(new VersionSegment.QualifierVersionSegment(qualifier)); - } - } - if (secondNumber != null && !secondNumber.isEmpty()) { - try { - segments.add(new VersionSegment.NumberVersionSegment(Integer.parseUnsignedInt(secondNumber))); - } catch (NumberFormatException ignored) { - } - } - } else { - try { - segments.add(new VersionSegment.NumberVersionSegment(Integer.parseUnsignedInt(segment))); - } catch (NumberFormatException ignored) { - } - } - } - - return Optional.of(new MavenVersion(version, segments.toArray(new VersionSegment[0]))); - } - @Override public String toString() { return getStringRepresentation(); @@ -138,11 +97,6 @@ public String getStringRepresentation() { return this.stringRepresentation; } - @Override - public Class getType() { - return MavenVersion.class; - } - public interface VersionSegment extends Comparable { default boolean isEqual(@NotNull VersionSegment other) { return this.compareTo(other) == 0; diff --git a/src/main/java/me/andreasmelone/basicmodinfoparser/platform/dependency/forge/MavenVersionRange.java b/src/main/java/me/andreasmelone/basicmodinfoparser/platform/dependency/forge/MavenVersionRange.java index da68ce7..4d48979 100644 --- a/src/main/java/me/andreasmelone/basicmodinfoparser/platform/dependency/forge/MavenVersionRange.java +++ b/src/main/java/me/andreasmelone/basicmodinfoparser/platform/dependency/forge/MavenVersionRange.java @@ -23,6 +23,7 @@ */ package me.andreasmelone.basicmodinfoparser.platform.dependency.forge; +import me.andreasmelone.basicmodinfoparser.platform.dependency.version.MavenVersionFactory; import me.andreasmelone.basicmodinfoparser.platform.dependency.version.VersionRange; import java.util.*; @@ -93,13 +94,22 @@ public static Optional parse(String range) { String lowerStr = matcher.group(2).trim(); String upperStr = matcher.group(3).trim(); - MavenVersion lower = lowerStr.isEmpty() ? null : MavenVersion.parse(lowerStr).orElse(null); - MavenVersion upper = upperStr.isEmpty() ? null : MavenVersion.parse(upperStr).orElse(null); + MavenVersion lower = lowerStr.isEmpty() + ? null + : new MavenVersionFactory().parseVersion(lowerStr).orElse(null); + MavenVersion upper = upperStr.isEmpty() + ? null + : new MavenVersionFactory().parseVersion(upperStr).orElse(null); ranges.add(new Range(lower, lowerExclusive, upper, upperExclusive)); } else { - Optional exact = MavenVersion.parse(part.trim()); - exact.ifPresent(v -> ranges.add(new Range(v, false, v, false))); + Optional exact = new MavenVersionFactory().parseVersion(part.trim()); + exact.ifPresent(v -> ranges.add(new Range( + v, + false, + v, + false + ))); } } diff --git a/src/main/java/me/andreasmelone/basicmodinfoparser/platform/dependency/parser/FabricDependencyParser.java b/src/main/java/me/andreasmelone/basicmodinfoparser/platform/dependency/parser/FabricDependencyParser.java new file mode 100644 index 0000000..c6d777d --- /dev/null +++ b/src/main/java/me/andreasmelone/basicmodinfoparser/platform/dependency/parser/FabricDependencyParser.java @@ -0,0 +1,95 @@ +package me.andreasmelone.basicmodinfoparser.platform.dependency.parser; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import me.andreasmelone.basicmodinfoparser.platform.dependency.Dependency; +import me.andreasmelone.basicmodinfoparser.platform.dependency.StandardDependency; +import me.andreasmelone.basicmodinfoparser.platform.dependency.fabric.FabricVersionRange; +import me.andreasmelone.basicmodinfoparser.util.adapter.JsonAdapter; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.StreamSupport; + +public class FabricDependencyParser implements IDependencyParser { + @Override + public List parse(String[] keys, JsonAdapter modAdapter) { + final List result = new ArrayList<>(); + + for (String key : keys) { + if (!modAdapter.hasKey(key) || !modAdapter.getBackingObject().get(key).isJsonObject()) { + continue; + } + + parseSingleDependency(key, modAdapter).ifPresent(result::add); + } + + return result; + } + + protected Optional parseSingleDependency(String key, JsonAdapter modAdapter) { + // As the fabric documentation specifies, the dependency keys are specified in the format + // of a string -> VersionRange dictionary, where the string key matches the desired ID. + // (https://wiki.fabricmc.net/documentation:fabric_mod_json_spec#optional_fields_dependency_resolution) + + if (!modAdapter.hasKey(key) || !modAdapter.getBackingObject().get(key).isJsonObject()) return Optional.empty(); + + final JsonObject dependencyObject = modAdapter.getBackingObject().getAsJsonObject(key); + final boolean required = isRequired(key); + + // Extract all the keys from the found object + for (Map.Entry entry : dependencyObject.entrySet()) { + // A versionRange can be a string or an array of string + final String dependencyId = entry.getKey(); + final JsonElement versionRange = entry.getValue(); + + // Case 1: String + if (versionRange.isJsonPrimitive() && versionRange.getAsJsonPrimitive().isString()) { + Optional range = FabricVersionRange.parse(versionRange.getAsString()); + + return range.map(fabricVersionRange -> + new StandardDependency<>( + dependencyId, + required, + fabricVersionRange + ) + ); + } + + // Case 2: array of Strings + if (versionRange.isJsonArray()) { + final String[] onlyStrings = StreamSupport.stream( + versionRange.getAsJsonArray().spliterator(), + false + ).filter(element -> + element.isJsonPrimitive() && element.getAsJsonPrimitive().isString() + ).map(JsonElement::getAsString) + .toArray(String[]::new); + + Optional fabricVersionRange = FabricVersionRange.parse(onlyStrings); + return fabricVersionRange.map( + range -> new StandardDependency<>( + dependencyId, + required, + range + ) + ); + } + } + + // No parsing was successful, return an empty optional + return Optional.empty(); + } + + protected boolean isRequired(String keyName) { + switch (keyName) { + case "depends": + case "breaks": + return true; + default: + return false; + } + } +} diff --git a/src/main/java/me/andreasmelone/basicmodinfoparser/platform/dependency/parser/ForgeDependencyParser.java b/src/main/java/me/andreasmelone/basicmodinfoparser/platform/dependency/parser/ForgeDependencyParser.java new file mode 100644 index 0000000..18f79c4 --- /dev/null +++ b/src/main/java/me/andreasmelone/basicmodinfoparser/platform/dependency/parser/ForgeDependencyParser.java @@ -0,0 +1,58 @@ +package me.andreasmelone.basicmodinfoparser.platform.dependency.parser; + +import me.andreasmelone.basicmodinfoparser.platform.dependency.forge.DependencySide; +import me.andreasmelone.basicmodinfoparser.platform.dependency.forge.ForgeDependency; +import me.andreasmelone.basicmodinfoparser.platform.dependency.forge.MavenVersionRange; +import me.andreasmelone.basicmodinfoparser.platform.dependency.forge.Ordering; +import me.andreasmelone.basicmodinfoparser.util.adapter.DataAdapter; +import me.andreasmelone.basicmodinfoparser.util.adapter.TomlAdapter; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +public class ForgeDependencyParser implements IDependencyParser { + @Override + public List parse(String[] keys, TomlAdapter modInfo) { + if (modInfo == null || keys.length == 0) return Collections.emptyList(); + + List dependencies = new ArrayList<>(); + + for (String dependencyKey : keys) { + modInfo.getArray(dependencyKey).ifPresent(dependenciesArray -> { + for (int i = 0; i < dependenciesArray.size(); i++) { + TomlAdapter dependencyAdapter = new TomlAdapter(dependenciesArray.getTable(i)); + dependencies.add(parseForgeDependency(dependencyAdapter)); + } + }); + } + return dependencies; + } + + /** + * Parses a {@link DataAdapter} into a {@link ForgeDependency} object. + *

+ * This method extracts the necessary fields from a DataAdapter and + * returns a corresponding {@link ForgeDependency} object. + *

+ * + * @param dependencyAdapter The DataAdapter containing the dependency's data. + * @return A {@link ForgeDependency} object constructed from the values in the given DataAdapter. + */ + @NotNull + private static ForgeDependency parseForgeDependency(TomlAdapter dependencyAdapter) { + String depModId = dependencyAdapter.getString("modId").orElse(""); + boolean mandatory = dependencyAdapter.getBoolean("mandatory").orElse(true); + String versionRange = dependencyAdapter.getString("versionRange").orElse(null); + String ordering = dependencyAdapter.getString("ordering").orElse("NONE"); + String side = dependencyAdapter.getString("side").orElse("BOTH"); + + Optional range = MavenVersionRange.parse(versionRange); + return new ForgeDependency( + depModId, range.orElse(null), mandatory, + Ordering.getFromString(ordering), DependencySide.getFromString(side) + ); + } +} diff --git a/src/main/java/me/andreasmelone/basicmodinfoparser/platform/dependency/parser/IDependencyParser.java b/src/main/java/me/andreasmelone/basicmodinfoparser/platform/dependency/parser/IDependencyParser.java new file mode 100644 index 0000000..99a3f69 --- /dev/null +++ b/src/main/java/me/andreasmelone/basicmodinfoparser/platform/dependency/parser/IDependencyParser.java @@ -0,0 +1,18 @@ +package me.andreasmelone.basicmodinfoparser.platform.dependency.parser; + +import me.andreasmelone.basicmodinfoparser.platform.dependency.Dependency; +import me.andreasmelone.basicmodinfoparser.util.adapter.DataAdapter; + +import java.util.List; + +/** + * An interface for parsing dependencies from a mod adapter. + * This is a functional interface whose only method is {@link #parse(String[], DataAdapter)}.+ + * + * @param The type of the mod adapter + * @param The type of the dependency + */ +@FunctionalInterface +public interface IDependencyParser, D extends Dependency> { + List parse(String[] keys, T modAdapter); +} \ No newline at end of file diff --git a/src/main/java/me/andreasmelone/basicmodinfoparser/platform/dependency/parser/LegacyForgeDependencyParser.java b/src/main/java/me/andreasmelone/basicmodinfoparser/platform/dependency/parser/LegacyForgeDependencyParser.java new file mode 100644 index 0000000..6720c3a --- /dev/null +++ b/src/main/java/me/andreasmelone/basicmodinfoparser/platform/dependency/parser/LegacyForgeDependencyParser.java @@ -0,0 +1,62 @@ +package me.andreasmelone.basicmodinfoparser.platform.dependency.parser; + +import com.google.gson.JsonElement; +import me.andreasmelone.basicmodinfoparser.platform.dependency.forge.DependencySide; +import me.andreasmelone.basicmodinfoparser.platform.dependency.forge.ForgeDependency; +import me.andreasmelone.basicmodinfoparser.platform.dependency.forge.MavenVersionRange; +import me.andreasmelone.basicmodinfoparser.platform.dependency.forge.Ordering; +import me.andreasmelone.basicmodinfoparser.util.adapter.JsonAdapter; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +public class LegacyForgeDependencyParser implements IDependencyParser { + @Override + public List parse(String[] keys, JsonAdapter modAdapter) { + if (modAdapter == null || keys.length == 0) return Collections.emptyList(); + + List dependencies = new ArrayList<>(); + + for (String dependencyKey : keys) { + modAdapter.getArray(dependencyKey).ifPresent(dependenciesArray -> { + for (JsonElement element : dependenciesArray) { + if (element.isJsonPrimitive() && element.getAsJsonPrimitive().isString()) { + parseLegacyForgeDependency(element.getAsString()).ifPresent(dependencies::add); + } + } + }); + } + + return dependencies; + } + + private Optional parseLegacyForgeDependency(String dependencyString) { + String modId; + String version = null; + Ordering ordering = Ordering.NONE; + + String[] splitPrefix = dependencyString.split(":", 2); + if (splitPrefix.length > 1) { + ordering = Ordering.getFromString(splitPrefix[0]); + dependencyString = splitPrefix[1]; + } + + String[] splitVersion = dependencyString.split("@"); + if (splitVersion.length > 1) { + version = splitVersion[1]; + dependencyString = splitVersion[0]; + } + + modId = dependencyString; + + if (modId == null || modId.isEmpty()) { + return Optional.empty(); + } + + Optional range = MavenVersionRange.parse(version); + return Optional.of(new ForgeDependency(modId, range.orElse(null), true, ordering, DependencySide.BOTH)); + + } +} diff --git a/src/main/java/me/andreasmelone/basicmodinfoparser/platform/dependency/parser/QuiltDependencyParser.java b/src/main/java/me/andreasmelone/basicmodinfoparser/platform/dependency/parser/QuiltDependencyParser.java new file mode 100644 index 0000000..47f85fd --- /dev/null +++ b/src/main/java/me/andreasmelone/basicmodinfoparser/platform/dependency/parser/QuiltDependencyParser.java @@ -0,0 +1,196 @@ +package me.andreasmelone.basicmodinfoparser.platform.dependency.parser; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import me.andreasmelone.basicmodinfoparser.platform.dependency.Dependency; +import me.andreasmelone.basicmodinfoparser.platform.dependency.StandardDependency; +import me.andreasmelone.basicmodinfoparser.platform.dependency.fabric.FabricVersionRange; +import me.andreasmelone.basicmodinfoparser.util.adapter.JsonAdapter; + +import java.util.*; + +public class QuiltDependencyParser implements IDependencyParser { + + public List parse(final String[] keys, final JsonAdapter modAdapter) { + final List result = new ArrayList<>(); + + for (final String key : keys) { + if (!modAdapter.hasKey(key)) continue; + + final JsonElement element = modAdapter.getBackingObject().get(key); + + // In Quilt, all the dependency properties MUST be an array + // (https://github.com/QuiltMC/rfcs/blob/main/specification/0002-quilt.mod.json.md#the-depends-field) + // (https://github.com/QuiltMC/rfcs/blob/main/specification/0002-quilt.mod.json.md#the-breaks-field) + if (!element.isJsonArray()) continue; + + final JsonArray asArray = element.getAsJsonArray(); + if (asArray.size() == 0) continue; + + // Get the dependencies present in the current key + for (final JsonElement dependency : asArray) { + result.addAll(parseSingleDependency(dependency)); + } + } + + return result; + } + + /** + * Parses a single dependency json element. + * According to + * + * Quilt's official specification for dependency objects, a single Dependency can be of one of three types: + *
    + *
  • An object containing at least the {@code id} field
  • + *
  • A string mod identifier in the form of either {@code mavenGroup:modId} or {@code modId}
  • + *
  • An array of dependency objects
  • + *
+ * + * @param dependency The dependency JSON element. The type can be any of the aforementioned types. + * @return A list of dependencies, or an empty optional if the element is not a valid dependency + */ + private List parseSingleDependency( + final JsonElement dependency + ) { + final List result = new ArrayList<>(); + + // If the passed dependency's type is not an array, + // add it to an array so the parsing logic is the same. + final JsonArray jsonElements = dependency.isJsonArray() + ? dependency.getAsJsonArray() + : new JsonArray(); + + if (jsonElements.size() == 0) jsonElements.add(dependency); + + + for (final JsonElement dependencyElement : jsonElements) { + // The supported versions for the current dependency + List versions = new ArrayList<>(Collections.singletonList("*")); + + // First case: string + // A string mod identifier in the form of either "mavenGroup:modId" or "modId" + if (dependencyElement.isJsonPrimitive() && dependencyElement.getAsJsonPrimitive().isString()) { + final String asString = dependencyElement.getAsString(); + final String dependencyId = asString.contains(":") ? asString.split(":")[1] : asString; + + result.add(new StandardDependency<>( + dependencyId, + true, + FabricVersionRange.parse(versions.toArray(new String[0])).orElse(null) + )); + continue; + } + + // Second case: object + // An object containing at least the id field + if (dependencyElement.isJsonObject()) { + final JsonObject asObject = dependencyElement.getAsJsonObject(); + + // The `id` field is required + if (!asObject.has("id")) continue; + + final String dependencyId = asObject.get("id").getAsString(); + + // The `Optional` field + boolean mandatory = true; + if (asObject.has("optional")) + mandatory = !asObject.getAsJsonPrimitive("optional").getAsBoolean(); + + // The versions + versions = Arrays.asList(getVersionsFor(asObject)); + + result.add(new StandardDependency<>( + dependencyId, + mandatory, + FabricVersionRange.parse( + versions.toArray(new String[0]) + ).orElse(null) + )); + continue; + } + + // Third case: array + // An array of dependency objects + // Since it is a nested array with the same specifications as the previous + // cases, we can use recursion to parse it. + if (dependencyElement.isJsonArray()) result.addAll(parseSingleDependency(dependencyElement)); + } + + return result; + } + + /** + * Helper method to retrieve the {@code versions} array from a dependency. + * This method takes into account the + * + * Quilt official specification for the versions field. + * on GitHub. + * + * @param dependencyElement the {@link JsonAdapter} containing the dependency data + * @return an array of versions supported by the dependency + */ + private String[] getVersionsFor(JsonElement dependencyElement) { + List result = new ArrayList<>(); + + // The "versions" field can be a string, an array of strings (deprecated) or an object + + // First case: object + // In this case, the object must contain a single field, which must either be `any` or `all` + // The field value must be an array, with more constraints. Each element of the array must + // either be a string version specifier, or an object which is interpreted in the same way as + // the versions field itself. + if (dependencyElement.isJsonObject()) { + final JsonObject asObject = dependencyElement.getAsJsonObject(); + + if (asObject.has("any") || asObject.has("all")) { + final JsonElement any = asObject.get("any"); + final JsonElement all = asObject.get("all"); + final JsonObject notNull; + + // Find which key type the object has (must be either `any` or `all`) + // and use the non-null one to make a recursive call to getVersionsFor + if (any != null) notNull = any.getAsJsonObject(); + else if (all != null) notNull = all.getAsJsonObject(); + else throw new RuntimeException("Dependency element must have 'any' or 'all': " + dependencyElement); + + List embeddedVersions = Arrays.asList(getVersionsFor(notNull)); + // Embedded versions found, add them to the result list and return + result.addAll(embeddedVersions); + return result.toArray(new String[0]); + } + + // The required `any` or `all` fields were not found, return an empty list + return new String[0]; + } + + // Second and third cases: string, array of strings + + // As a string, the content should be a single version specifier defining the versions + // this dependency applies to. + + // As an array of strings, regardless of being deprecated, it should be + // an array of version specifiers defining the versions this dependency applies to. + + // Since there's no fundamental difference in the way a single version without an array + // and a version element inside an array are written, we can treat a single version string + // as a version array with only one element. + + JsonArray versionsArray = dependencyElement.isJsonArray() + ? dependencyElement.getAsJsonArray() + : new JsonArray(); + + if (versionsArray.size() == 0) + versionsArray.add(dependencyElement); // In this case, the version element turned out to be a single string + + // In the array, the only allowed type is string, so we don't have to worry about recursion here. + for (JsonElement versionElement : versionsArray) { + if (!versionElement.isJsonPrimitive() || !versionElement.getAsJsonPrimitive().isString()) continue; + + result.add(versionElement.getAsString()); + } + + return result.toArray(new String[0]); + } +} diff --git a/src/main/java/me/andreasmelone/basicmodinfoparser/platform/dependency/version/LooseSemanticVersionFactory.java b/src/main/java/me/andreasmelone/basicmodinfoparser/platform/dependency/version/LooseSemanticVersionFactory.java new file mode 100644 index 0000000..361b78f --- /dev/null +++ b/src/main/java/me/andreasmelone/basicmodinfoparser/platform/dependency/version/LooseSemanticVersionFactory.java @@ -0,0 +1,82 @@ +package me.andreasmelone.basicmodinfoparser.platform.dependency.version; + +import me.andreasmelone.basicmodinfoparser.platform.dependency.fabric.LooseSemanticVersion; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class LooseSemanticVersionFactory implements VersionFactory { + private static final Pattern ALPHANUMERIC = Pattern.compile("[a-zA-Z0-9_\\-.+*]+"); + private static final Pattern VERSION_PATTERN = Pattern.compile("^([0-9xX*]+(?:\\.[0-9xX*]+)*)(-.*?)?(\\+.+)?$", Pattern.MULTILINE); + + @Override + public Optional parseVersion(String versionString) { + return parseVersion(versionString, false); + } + + public Optional parseVersion(String versionString, boolean wildcards) { + if (versionString == null || versionString.isEmpty() || !ALPHANUMERIC.matcher(versionString).matches()) + return Optional.empty(); + + Matcher matcher = VERSION_PATTERN.matcher(versionString); + if (!matcher.matches()) return Optional.empty(); + + String prerelease = matcher.group(2); + if (prerelease != null && !prerelease.isEmpty()) { + prerelease = prerelease.substring(1); + } + + String metadata = matcher.group(3); + if (metadata != null && !metadata.isEmpty()) { + metadata = metadata.substring(1); + } + + String numbers = matcher.group(1); + String[] splitNumbers = numbers.split("\\."); + int[] versionInts = new int[splitNumbers.length]; + List wildcardPositions = new ArrayList<>(); + + for (int i = 0; i < splitNumbers.length; i++) { + String num = splitNumbers[i]; + if (num.equalsIgnoreCase("x") || num.equals("*")) { + if (!wildcards) return Optional.empty(); + versionInts[i] = 0; + wildcardPositions.add(i); + continue; + } + try { + versionInts[i] = Integer.parseUnsignedInt(num); + } catch (NumberFormatException ignored) { + return Optional.empty(); + } + } + + Integer prereleaseNumber = null; + String[] prereleaseSplit = null; + if (prerelease != null) { + prereleaseSplit = prerelease.split("\\.", 2); + if (prereleaseSplit.length > 1) { + try { + prereleaseNumber = Integer.parseInt(prereleaseSplit[1]); + prerelease = prereleaseSplit[0]; + } catch (NumberFormatException ignored) { + } + } + } + + return Optional.of( + new LooseSemanticVersion( + versionString, + versionInts, + wildcardPositions, + prerelease, + prereleaseNumber, + metadata, + wildcards + ) + ); + } +} diff --git a/src/main/java/me/andreasmelone/basicmodinfoparser/platform/dependency/version/MavenVersionFactory.java b/src/main/java/me/andreasmelone/basicmodinfoparser/platform/dependency/version/MavenVersionFactory.java new file mode 100644 index 0000000..d430703 --- /dev/null +++ b/src/main/java/me/andreasmelone/basicmodinfoparser/platform/dependency/version/MavenVersionFactory.java @@ -0,0 +1,70 @@ +package me.andreasmelone.basicmodinfoparser.platform.dependency.version; + +import me.andreasmelone.basicmodinfoparser.platform.dependency.forge.MavenVersion; +import me.andreasmelone.basicmodinfoparser.platform.dependency.forge.MavenVersion.VersionSegment; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class MavenVersionFactory implements VersionFactory { + private static final Pattern ALPHANUMERIC = Pattern.compile("[a-zA-Z0-9_\\-.]+"); + private static final Pattern VERSION_PATTERN = Pattern.compile("^(\\d*)?(\\D+)(\\d*)?$"); + + @Override + public Optional parseVersion(String versionString) { + if (versionString == null || versionString.isEmpty() || !ALPHANUMERIC.matcher(versionString).matches()) + return Optional.empty(); + + List segments = new ArrayList<>(); + + String noHyphens = versionString.replace("-", "."); + String[] splitByDot = noHyphens.split("\\."); + for (String segment : splitByDot) { + Matcher matcher = VERSION_PATTERN.matcher(segment); + if (!matcher.matches()) { + try { + segments.add(new VersionSegment.NumberVersionSegment(Integer.parseUnsignedInt(segment))); + } catch (NumberFormatException ignored) { + // + } + + continue; + } + + String firstNumber = matcher.group(1); + String string = matcher.group(2); + String secondNumber = matcher.group(3); + + if (firstNumber != null && !firstNumber.isEmpty()) { + try { + segments.add(new VersionSegment.NumberVersionSegment(Integer.parseUnsignedInt(firstNumber))); + } catch (NumberFormatException ignored) { + } + } + if (string != null && !string.isEmpty()) { + VersionSegment.QualifierVersionSegment.Qualifier qualifier = VersionSegment.QualifierVersionSegment.Qualifier.getByName(string); + if (qualifier == null) { + segments.add(new VersionSegment.StringVersionSegment(string)); + } else { + segments.add(new VersionSegment.QualifierVersionSegment(qualifier)); + } + } + if (secondNumber != null && !secondNumber.isEmpty()) { + try { + segments.add(new VersionSegment.NumberVersionSegment(Integer.parseUnsignedInt(secondNumber))); + } catch (NumberFormatException ignored) { + } + } + } + + return Optional.of( + new MavenVersion( + versionString, + segments.toArray(new VersionSegment[0]) + ) + ); + } +} diff --git a/src/main/java/me/andreasmelone/basicmodinfoparser/platform/dependency/version/Version.java b/src/main/java/me/andreasmelone/basicmodinfoparser/platform/dependency/version/Version.java index 681e781..1dbe8c8 100644 --- a/src/main/java/me/andreasmelone/basicmodinfoparser/platform/dependency/version/Version.java +++ b/src/main/java/me/andreasmelone/basicmodinfoparser/platform/dependency/version/Version.java @@ -25,19 +25,19 @@ import java.util.Optional; -public interface Version> extends Comparable { - @SuppressWarnings("unchecked") - default Optional optional() { - return Optional.of((T) this); +public abstract class Version implements Comparable { + protected final String stringRepresentation; + + protected Version(String stringRepresentation) { + this.stringRepresentation = stringRepresentation; } - /** - * @return this version as a human-readable string - */ - String getStringRepresentation(); + public Optional optional() { + return Optional.of(this); + } /** - * @return the type of {@link Version} + * @return this version as a human-readable string */ - Class getType(); + public abstract String getStringRepresentation(); } diff --git a/src/main/java/me/andreasmelone/basicmodinfoparser/platform/dependency/version/VersionFactory.java b/src/main/java/me/andreasmelone/basicmodinfoparser/platform/dependency/version/VersionFactory.java new file mode 100644 index 0000000..71b617f --- /dev/null +++ b/src/main/java/me/andreasmelone/basicmodinfoparser/platform/dependency/version/VersionFactory.java @@ -0,0 +1,8 @@ +package me.andreasmelone.basicmodinfoparser.platform.dependency.version; + +import java.util.Optional; + +@FunctionalInterface +public interface VersionFactory { + Optional parseVersion(final String versionString); +} diff --git a/src/main/java/me/andreasmelone/basicmodinfoparser/platform/dependency/version/VersionRange.java b/src/main/java/me/andreasmelone/basicmodinfoparser/platform/dependency/version/VersionRange.java index a6f9dd2..e9e886f 100644 --- a/src/main/java/me/andreasmelone/basicmodinfoparser/platform/dependency/version/VersionRange.java +++ b/src/main/java/me/andreasmelone/basicmodinfoparser/platform/dependency/version/VersionRange.java @@ -23,7 +23,7 @@ */ package me.andreasmelone.basicmodinfoparser.platform.dependency.version; -public interface VersionRange> { +public interface VersionRange { /** * @return the string representation of the version range */ diff --git a/src/main/java/me/andreasmelone/basicmodinfoparser/platform/modinfo/BreaksList.java b/src/main/java/me/andreasmelone/basicmodinfoparser/platform/modinfo/BreaksList.java deleted file mode 100644 index a533a32..0000000 --- a/src/main/java/me/andreasmelone/basicmodinfoparser/platform/modinfo/BreaksList.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2024-2025 RaydanOMGr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -package me.andreasmelone.basicmodinfoparser.platform.modinfo; - -import me.andreasmelone.basicmodinfoparser.platform.dependency.Dependency; - -import java.util.List; - -public interface BreaksList { - /** - * @return a list of {@link Dependency} that this mod is incompatible with - */ - List getBreaks(); -} diff --git a/src/main/java/me/andreasmelone/basicmodinfoparser/platform/modinfo/FabricModInfo.java b/src/main/java/me/andreasmelone/basicmodinfoparser/platform/modinfo/FabricModInfo.java index 8564344..d76cd0d 100644 --- a/src/main/java/me/andreasmelone/basicmodinfoparser/platform/modinfo/FabricModInfo.java +++ b/src/main/java/me/andreasmelone/basicmodinfoparser/platform/modinfo/FabricModInfo.java @@ -35,24 +35,26 @@ import java.util.List; import java.util.Objects; -public class FabricModInfo extends StandardBasicModInfo implements BreaksList, ProvidesList { - private final List breaks; - private final List> provides; +public class FabricModInfo extends StandardBasicModInfo implements ProvidesList { + private final List provides; - public FabricModInfo(@Nullable String id, @Nullable String name, @Nullable Version version, @Nullable String description, @Nullable List dependencies, @Nullable String iconPath, @NotNull Platform platform, @Nullable List breaks, @Nullable List> provides) { - super(id, name, version, description, dependencies, iconPath, platform); - this.breaks = breaks != null ? new ArrayList<>(breaks) : null; + public FabricModInfo( + @Nullable String id, + @Nullable String name, + @Nullable Version version, + @Nullable String description, + @Nullable List dependencies, + @Nullable String iconPath, + @NotNull Platform platform, + @Nullable List authors, + @Nullable List provides + ) { + super(id, name, version, description, dependencies, iconPath, platform, authors); this.provides = provides != null ? new ArrayList<>(provides) : null; } @Override - public List getBreaks() { - if (breaks == null) return null; - return new ArrayList<>(breaks); - } - - @Override - public List> getProvidedIds() { + public List getProvidedIds() { if (provides == null) return null; return new ArrayList<>(provides); } @@ -65,8 +67,7 @@ public Class getType() { @Override public String toString() { return "FabricModInfo{" + - "breaks=" + breaks + - ", provides=" + provides + + "provides=" + provides + '}'; } @@ -75,11 +76,11 @@ public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) return false; if (!super.equals(o)) return false; FabricModInfo that = (FabricModInfo) o; - return Objects.equals(breaks, that.breaks) && Objects.equals(provides, that.provides); + return Objects.equals(provides, that.provides); } @Override public int hashCode() { - return Objects.hash(super.hashCode(), breaks, provides); + return Objects.hash(super.hashCode(), provides); } } diff --git a/src/main/java/me/andreasmelone/basicmodinfoparser/platform/modinfo/ProvidesList.java b/src/main/java/me/andreasmelone/basicmodinfoparser/platform/modinfo/ProvidesList.java index 43bc259..b528d98 100644 --- a/src/main/java/me/andreasmelone/basicmodinfoparser/platform/modinfo/ProvidesList.java +++ b/src/main/java/me/andreasmelone/basicmodinfoparser/platform/modinfo/ProvidesList.java @@ -28,12 +28,12 @@ import java.util.List; -public interface ProvidesList> { +public interface ProvidesList { /** * @return the provided mod IDs * @see ProvidedMod */ - List> getProvidedIds(); + List getProvidedIds(); /** * @return the type of the provided {@link Version} objects diff --git a/src/main/java/me/andreasmelone/basicmodinfoparser/platform/modinfo/StandardBasicModInfo.java b/src/main/java/me/andreasmelone/basicmodinfoparser/platform/modinfo/StandardBasicModInfo.java index 18f8f03..62321df 100644 --- a/src/main/java/me/andreasmelone/basicmodinfoparser/platform/modinfo/StandardBasicModInfo.java +++ b/src/main/java/me/andreasmelone/basicmodinfoparser/platform/modinfo/StandardBasicModInfo.java @@ -37,14 +37,23 @@ public class StandardBasicModInfo implements BasicModInfo { private final String id; private final String name; - private final Version version; + private final Version version; private final String description; private final List dependencies; private final String iconPath; private final Platform platform; + private final List authors; - public StandardBasicModInfo(@Nullable String id, @Nullable String name, @Nullable Version version, @Nullable String description, - @Nullable List dependencies, @Nullable String iconPath, @NotNull Platform platform) { + public StandardBasicModInfo( + @Nullable String id, + @Nullable String name, + @Nullable Version version, + @Nullable String description, + @Nullable List dependencies, + @Nullable String iconPath, + @NotNull Platform platform, + @Nullable List authors + ) { this.id = id; this.name = name; this.version = version; @@ -52,6 +61,7 @@ public StandardBasicModInfo(@Nullable String id, @Nullable String name, @Nullabl this.dependencies = dependencies != null ? new ArrayList<>(dependencies) : null; this.iconPath = iconPath; this.platform = platform; + this.authors = authors; } /** @@ -83,7 +93,7 @@ public String getName() { */ @Override @Nullable - public Version getVersion() { + public Version getVersion() { return version; } @@ -120,6 +130,10 @@ public List getDependencies() { return platform; } + public @Nullable List getAuthors() { + return authors; + } + @Override public String toString() { return "StandardBasicModInfo{" + @@ -149,4 +163,5 @@ public int hashCode() { public static StandardBasicModInfo[] emptyArray() { return new StandardBasicModInfo[0]; } + } diff --git a/src/main/java/me/andreasmelone/basicmodinfoparser/platform/modinfo/model/ModInfoKeys.java b/src/main/java/me/andreasmelone/basicmodinfoparser/platform/modinfo/model/ModInfoKeys.java new file mode 100644 index 0000000..9712ce7 --- /dev/null +++ b/src/main/java/me/andreasmelone/basicmodinfoparser/platform/modinfo/model/ModInfoKeys.java @@ -0,0 +1,113 @@ +package me.andreasmelone.basicmodinfoparser.platform.modinfo.model; + +import java.util.Objects; + +/** + * Holds information about the available keys inside a mod's information file. + * + * @see me.andreasmelone.basicmodinfoparser.platform.Platform + */ +public class ModInfoKeys { + /** + * The key name to access a mod's ID. + */ + public final String modIdKey; + + /** + * The key name to access a mod's display name. + */ + public final String displayNameKey; + + /** + * The key name to access a mod's version. + */ + public final String versionKey; + + /** + * The key name to access a mod's description. + */ + public final String descriptionKey; + + /** + * The key name to access a mod's logo. + */ + public final String logoFileKey; + + public final String authorsKey; + + /** + * The key names to access a mod's dependencies. Loaders such as Fabric may provide multiple + * keys to declare dependencies and compatibility, so an array is necessary. + * + * @see + * Fabric's documentation on dependency management + * + */ + public final String[] dependencyKeys; + + public ModInfoKeys( + String modIdKey, + String displayNameKey, + String versionKey, + String descriptionKey, + String logoFileKey, + String authorsKey, + String[] dependencyKeys + ) { + this.modIdKey = modIdKey; + this.displayNameKey = displayNameKey; + this.versionKey = versionKey; + this.descriptionKey = descriptionKey; + this.logoFileKey = logoFileKey; + this.authorsKey = authorsKey; + this.dependencyKeys = dependencyKeys; + } + + /** + * Helper method to create a forge-and-neoforge-compliant {@link ModInfoKeys} + * + * @return a forge-and-neoforge-compliant {@link ModInfoKeys} + */ + public static ModInfoKeys forgeKeys() { + return new ModInfoKeys( + "modId", + "displayName", + "version", + "description", + "logoFile", + "authors", + new String[]{"dependencies"} + ); + } + + @Override + public boolean equals(Object other) { + if (other == null) return false; + if (this == other) return true; + if (this.getClass() != other.getClass()) return false; + + ModInfoKeys castOther = (ModInfoKeys) other; + + return castOther.modIdKey.equals(modIdKey) + && castOther.displayNameKey.equals(displayNameKey) + && castOther.versionKey.equals(versionKey) + && castOther.descriptionKey.equals(descriptionKey) + && castOther.logoFileKey.equals(logoFileKey); + } + + @Override + public int hashCode() { + return Objects.hash(modIdKey, displayNameKey, versionKey, descriptionKey, logoFileKey); + } + + @Override + public String toString() { + return "ModInfoKeys{" + + "modIdKey=" + modIdKey + + ", displayNameKey=" + displayNameKey + + ", versionKey=" + versionKey + + ", descriptionKey=" + descriptionKey + + ", logoFileKey=" + logoFileKey + + "}"; + } +} diff --git a/src/main/java/me/andreasmelone/basicmodinfoparser/util/ParserUtils.java b/src/main/java/me/andreasmelone/basicmodinfoparser/util/ParserUtils.java index 999e2f7..e01cbab 100644 --- a/src/main/java/me/andreasmelone/basicmodinfoparser/util/ParserUtils.java +++ b/src/main/java/me/andreasmelone/basicmodinfoparser/util/ParserUtils.java @@ -24,57 +24,27 @@ package me.andreasmelone.basicmodinfoparser.util; import com.google.gson.Gson; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; import me.andreasmelone.basicmodinfoparser.platform.BasicModInfo; import me.andreasmelone.basicmodinfoparser.platform.Platform; import me.andreasmelone.basicmodinfoparser.platform.dependency.Dependency; -import me.andreasmelone.basicmodinfoparser.platform.dependency.ProvidedMod; -import me.andreasmelone.basicmodinfoparser.platform.dependency.StandardDependency; -import me.andreasmelone.basicmodinfoparser.platform.dependency.fabric.FabricVersionRange; -import me.andreasmelone.basicmodinfoparser.platform.dependency.fabric.LooseSemanticVersion; -import me.andreasmelone.basicmodinfoparser.platform.dependency.forge.*; +import me.andreasmelone.basicmodinfoparser.platform.dependency.parser.IDependencyParser; import me.andreasmelone.basicmodinfoparser.platform.dependency.version.Version; -import me.andreasmelone.basicmodinfoparser.platform.modinfo.FabricModInfo; +import me.andreasmelone.basicmodinfoparser.platform.dependency.version.VersionFactory; import me.andreasmelone.basicmodinfoparser.platform.modinfo.StandardBasicModInfo; +import me.andreasmelone.basicmodinfoparser.platform.modinfo.model.ModInfoKeys; +import me.andreasmelone.basicmodinfoparser.util.adapter.DataAdapter; import org.jetbrains.annotations.NotNull; -import org.tomlj.Toml; -import org.tomlj.TomlArray; -import org.tomlj.TomlParseResult; -import org.tomlj.TomlTable; import java.io.File; import java.io.IOException; import java.io.InputStream; -import java.nio.file.Path; -import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.Optional; -import java.util.function.Predicate; -import java.util.stream.StreamSupport; public class ParserUtils { public static final Gson GSON = new Gson(); - /** - * Compares an array of paths to a single path to check if any match after normalisation.

- * This ensures that two paths are considered equal, even if their string representations differ - * (e.g., {@code run/embeddium.jar} and {@code .\run\embeddium.jar}). - * - * @param paths An array of paths to compare. - * @param path2 The path to compare against. - * @return {@code true} if any of the provided paths are equal to {@code path2} after normalisation, otherwise {@code false}. - */ - public static boolean comparePaths(String[] paths, String path2) { - for (String path : paths) { - Path normalizedPath1 = Paths.get(path).normalize(); - Path normalizedPath2 = Paths.get(path2).normalize(); - if (normalizedPath1.equals(normalizedPath2)) return true; - } - return false; - } - /** * Reads the entire content of an {@link InputStream} and returns it as a string. * @@ -93,267 +63,55 @@ public static String readEverythingAsString(InputStream in) throws IOException { } /** - * Finds a value by key in a {@link JsonObject} and checks it against a predicate. - * - * @param jsonObject The {@link JsonObject} in which to search for the value. - * @param key The key for which the value needs to be found. - * @param predicate The predicate that the value of the key must satisfy. - * @return An {@link Optional} containing the {@link JsonElement} if it exists and matches the predicate, - * or a {@link Optional#empty()} if the value was not found or did not match. - */ - public static Optional findValidValue(JsonObject jsonObject, String key, Predicate predicate) { - if (!jsonObject.has(key)) { - return Optional.empty(); - } - - JsonElement element = jsonObject.get(key); - return predicate.test(element) ? Optional.of(element) : Optional.empty(); - } - - /** - * Helper method to fetch a valid string value from a {@link JsonObject} - * by key, ensuring it matches the specified predicate. - * - * @param obj The {@link JsonObject} from which to retrieve the value. - * @param key The key for the value to be retrieved. - * @param predicate The predicate that the value must satisfy. - * @return The string value if found and valid, or {@code null} if not found - * or invalid. - */ - public static String getValidString(JsonObject obj, String key, Predicate predicate) { - return findValidValue(obj, key, predicate) - .map(JsonElement::getAsString) - .orElse(null); - } - - /** - * Helper method to fetch a valid string value from a {@link JsonObject} - * by key. - * - * @param obj The {@link JsonObject} from which to retrieve the value. - * @param key The key for the value to be retrieved. - * @return The string value if found and valid, or {@code null} if not found - * or invalid. - */ - public static String getValidString(JsonObject obj, String key) { - return findValidValue(obj, key, (element) -> true) - .map(JsonElement::getAsString) - .orElse(null); - } - - /** - * Parses a dependency string into a {@link ForgeDependency} object. - *

- * This method parses a dependency string following the legacy Forge dependency format and returns - * a {@link ForgeDependency} object. The dependency string may include an ordering prefix and a - * version suffix, and these are parsed accordingly. - *

- * - * @param dependencyString The string representation of the dependency. The format is usually - * "ordering:modId@version". - * @return The parsed {@link ForgeDependency} object wrapped inside a {@link Optional}, can be {@link Optional#empty()} if invalid. Returns an {@link Optional#empty()} if the modId - * is empty or invalid. - */ - @NotNull - public static Optional parseLegacyForgeDependency(String dependencyString) { - String modId; - String version = null; - Ordering ordering = Ordering.NONE; - - String[] splitPrefix = dependencyString.split(":", 2); - if (splitPrefix.length > 1) { - ordering = Ordering.getFromString(splitPrefix[0]); - dependencyString = splitPrefix[1]; - } - - String[] splitVersion = dependencyString.split("@"); - if (splitVersion.length > 1) { - version = splitVersion[1]; - dependencyString = splitVersion[0]; - } - - modId = dependencyString; - - if (modId == null || modId.isEmpty()) { - return Optional.empty(); - } - - Optional range = MavenVersionRange.parse(version); - return Optional.of(new ForgeDependency(modId, range.orElse(null), true, ordering, DependencySide.BOTH)); - } - - /** - * Parses a {@link TomlTable} into a {@link ForgeDependency} object. - *

- * This method extracts the necessary fields from a TomlTable (a parsed TOML configuration) and - * returns a corresponding {@link ForgeDependency} object. - *

- * - * @param dependencyTable The TomlTable containing the dependency's data. - * @return A {@link ForgeDependency} object constructed from the values in the given TomlTable. - */ - @NotNull - public static Dependency parseForgeDependency(TomlTable dependencyTable) { - String depModId = dependencyTable.getString("modId"); - boolean mandatory = dependencyTable.getBoolean("mandatory", () -> true); - String versionRange = dependencyTable.getString("versionRange"); - String ordering = dependencyTable.getString("ordering"); - String side = dependencyTable.getString("side"); - - if (ordering == null) ordering = "NONE"; - if (side == null) side = "BOTH"; - - Optional range = MavenVersionRange.parse(versionRange); - return new ForgeDependency( - depModId, range.orElse(null), mandatory, - Ordering.getFromString(ordering), DependencySide.getFromString(side) - ); - } - - - /** - * Parses Fabric dependencies from a {@link JsonObject} and adds them to a given list of dependencies. + * Creates a {@link BasicModInfo} object from a {@link DataAdapter}. *

- * This method processes Fabric dependency entries within the given JSON object and adds them to the - * provided list. The dependencies may be either a single string or an array of strings. + * This method parses a data adapter, extracts the required fields, and creates a new {@link BasicModInfo} object along with any given dependencies. *

* - * @param dependencyList The list where parsed dependencies will be added. - * @param jsonObject The {@link JsonObject} containing the dependency data. - * @param key The key within the JSON object to retrieve the dependencies. - * @param mandatory Whether the dependency is mandatory or optional. - */ - public static void parseFabricDependencies(List dependencyList, JsonObject jsonObject, String key, boolean mandatory) { - if (jsonObject.has(key) && jsonObject.get(key).isJsonObject()) { - JsonObject depends = jsonObject.getAsJsonObject(key); - - depends.entrySet().forEach(entry -> { - String dependencyKey = entry.getKey(); - JsonElement dependency = entry.getValue(); - - if (dependency.isJsonPrimitive() && dependency.getAsJsonPrimitive().isString()) { - Optional range = FabricVersionRange.parse(dependency.getAsString()); - if (!range.isPresent()) return; - dependencyList.add(new StandardDependency<>(dependencyKey, mandatory, range.get())); - } else if (dependency.isJsonArray()) { - String[] resultingVersion = StreamSupport.stream(dependency.getAsJsonArray().spliterator(), false) - .filter((el) -> el.isJsonPrimitive() && el.getAsJsonPrimitive().isString()) - .map(JsonElement::getAsString) - .toArray(String[]::new); - Optional range = FabricVersionRange.parse(resultingVersion); - if (!range.isPresent()) return; - dependencyList.add(new StandardDependency<>(dependencyKey, mandatory, range.get())); - } - }); - } - } - - /** - * Creates a {@link BasicModInfo} object from a {@link JsonObject}. - *

- * This method parses a JSON object, extracts the required fields (modId, displayName, version, - * and description), and creates a new {@link BasicModInfo} object along with any given dependencies. - *

- * - * @param jsonObject The {@link JsonObject} containing the mod information. - * @param modIdKey The key used to retrieve the mod ID from the JSON object. - * @param displayNameKey The key used to retrieve the mod display name from the JSON object. - * @param versionKey The key used to retrieve the mod version from the JSON object. - * @param descriptionKey The key used to retrieve the mod description from the JSON object. - * @param logoFileKey The key used to retrieve the mod icon from the JSON object. - * @param dependencies The list of {@link Dependency} objects that the current mod depends on. - * @param platform The platform the mod is on. + * @param modAdapter The {@link DataAdapter} containing the mod information. + * @param platform The {@link Platform} this mod info belongs to. + * @param dependenciesParser A {@link IDependencyParser} that takes an array of dependency keys and the + * @param versionFactory A {@link VersionFactory} to parse the mod's version string into a {@link Version} object. + * {@link DataAdapter} to parse dependencies from, returning a list of {@link Dependency}. * @return A {@link BasicModInfo} object containing the mod information and its dependencies. */ - public static BasicModInfo createForgeModInfoFromJsonObject(JsonObject jsonObject, String modIdKey, - String displayNameKey, String versionKey, - String descriptionKey, String logoFileKey, List dependencies, Platform platform) { - Predicate isStringPredicate = element -> - element.isJsonPrimitive() && element.getAsJsonPrimitive().isString(); - - String modId = getValidString(jsonObject, modIdKey, isStringPredicate); - String name = getValidString(jsonObject, displayNameKey, isStringPredicate); - String description = getValidString(jsonObject, descriptionKey, isStringPredicate); - String version = getValidString(jsonObject, versionKey, isStringPredicate); - String logo = getValidString(jsonObject, logoFileKey, isStringPredicate); - - Optional mavenVersion = MavenVersion.parse(version); - return new StandardBasicModInfo(modId, name, mavenVersion.orElse(null), description, dependencies, logo, platform); - } - - /** - * Creates a {@link BasicModInfo} object from a {@link JsonObject}. - *

- * This method parses a JSON object, extracts the required fields (modId, displayName, version, - * and description), and creates a new {@link BasicModInfo} object along with any given dependencies. - * - * @param jsonObject The {@link JsonObject} containing the mod information. - * @param modIdKey The key used to retrieve the mod ID from the JSON object. - * @param displayNameKey The key used to retrieve the mod display name from the JSON object. - * @param version A parsed {@link Version} object - * @param descriptionKey The key used to retrieve the mod description from the JSON object. - * @param logoFileKey The key used to retrieve the mod icon from the JSON object. - * @param dependencies The list of {@link Dependency} objects that the current mod depends on. - * @param breaks The list of {@link Dependency} objects that the current mod is incompatible with. - * @return A {@link BasicModInfo} object containing the mod information and its dependencies. - */ - public static > BasicModInfo createFabricModInfoFromJsonObject(JsonObject jsonObject, String modIdKey, - String displayNameKey, Version version, - String descriptionKey, String logoFileKey, List dependencies, - List breaks, List> providedMods, Platform platform) { - Predicate isStringPredicate = element -> - element.isJsonPrimitive() && element.getAsJsonPrimitive().isString(); - - String modId = getValidString(jsonObject, modIdKey, isStringPredicate); - String name = getValidString(jsonObject, displayNameKey, isStringPredicate); - String description = getValidString(jsonObject, descriptionKey, isStringPredicate); - String logo = getValidString(jsonObject, logoFileKey, isStringPredicate); - - return new FabricModInfo(modId, name, version, description, dependencies, logo, platform, breaks, providedMods); - } - - /** - * Parses info in a forge-like way - * - * @param fileData the toml file contents - * @param platform the platform under which to parse (usually {@link Platform#FORGE} or {@link Platform#NEOFORGE}) - * @return the parsed info - */ - public static BasicModInfo[] parseForgelikeInfo(String fileData, Platform platform) { - TomlParseResult result = Toml.parse(fileData); - TomlArray modsArray = result.getArray("mods"); - if (modsArray == null || modsArray.isEmpty()) return StandardBasicModInfo.emptyArray(); - - List parsedInfos = new ArrayList<>(); - for (int index = 0; index < modsArray.size(); index++) { - TomlTable modInfo = modsArray.getTable(index); - if (modInfo.isEmpty()) continue; - - String modId = modInfo.getString("modId"); - String name = modInfo.getString("displayName"); - String description = modInfo.getString("description"); - String version = modInfo.getString("version"); - String logoFile = modInfo.getString("logoFile"); - - List dependencies = new ArrayList<>(); - TomlArray dependenciesArray = result.getArray("dependencies." + modId); - - if (dependenciesArray != null && !dependenciesArray.isEmpty()) { - for (int i = 0; i < dependenciesArray.size(); i++) { - TomlTable dependencyTable = dependenciesArray.getTable(i); - if (dependencyTable != null && !dependencyTable.isEmpty()) { - dependencies.add(ParserUtils.parseForgeDependency(dependencyTable)); - } - } - } - - Optional mavenVersion = MavenVersion.parse(version); - parsedInfos.add(new StandardBasicModInfo( - modId, name, mavenVersion.orElse(null), description, - dependencies, logoFile, platform - )); - } - return parsedInfos.toArray(new BasicModInfo[0]); + public static , D extends Dependency> + @NotNull BasicModInfo createModInfoFrom( + @NotNull T modAdapter, + @NotNull Platform platform, + @NotNull IDependencyParser dependenciesParser, + @NotNull VersionFactory versionFactory + ) { + // Get miscellaneous information + final ModInfoKeys modInfoKeys = platform.getModInfoKeys(); + String modId = modAdapter.getString(modInfoKeys.modIdKey).orElse(null); + String name = modAdapter.getString(modInfoKeys.displayNameKey).orElse(null); + String description = modAdapter.getString(modInfoKeys.descriptionKey).orElse(null); + String version = modAdapter.getString(modInfoKeys.versionKey).orElse(null); + String logo = modAdapter.getString(modInfoKeys.logoFileKey).orElse(null); + + // Parse Version + Optional parsedVersion = versionFactory.parseVersion(version); + + // Get authors + List authorsList = modAdapter.getListOrString(modInfoKeys.authorsKey); + + // Get dependencies + List dependencyList = new ArrayList<>(dependenciesParser.parse( + modInfoKeys.dependencyKeys, + modAdapter + )); + + return new StandardBasicModInfo( + modId, + name, + parsedVersion.orElse(null), + description, + dependencyList, + logo, + platform, + authorsList + ); } public static String getTempDir() { diff --git a/src/main/java/me/andreasmelone/basicmodinfoparser/util/adapter/DataAdapter.java b/src/main/java/me/andreasmelone/basicmodinfoparser/util/adapter/DataAdapter.java new file mode 100644 index 0000000..b6d36a8 --- /dev/null +++ b/src/main/java/me/andreasmelone/basicmodinfoparser/util/adapter/DataAdapter.java @@ -0,0 +1,101 @@ +/* + * MIT License + * + * Copyright (c) 2024-2025 RaydanOMGr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package me.andreasmelone.basicmodinfoparser.util.adapter; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +/** + * An abstract class for standardizing access to data from different sources, + * such as JSON or TOML. This allows for a unified way of retrieving values, + * regardless of the underlying data format. + * + * @param The type of the underlying data source (e.g., JsonObject, TomlTable). + * @param The type of the array-like data structure in the source (e.g., JsonArray, TomlArray). + */ +public abstract class DataAdapter { + /** + * The underlying data source object. + */ + protected final T backingObject; + + /** + * Constructs a new {@code DataAdapter} with the given backing object. + */ + public DataAdapter(T backingObject) { + this.backingObject = backingObject; + } + + /** + * Retrieves a string value for a given key. + * + * @param key The key to look up. + * @return An {@link Optional} containing the string value, or empty if not found. + */ + public abstract Optional getString(String key); + + /** + * Retrieves a boolean value for a given key. + * + * @param key The key to look up. + * @return An {@link Optional} containing the boolean value, or empty if not found. + */ + public abstract Optional getBoolean(String key); + + /** + * Retrieves an array-like data structure for a given key. + * + * @param key The key to look up. + * @return An {@link Optional} containing the array-like data, or empty if not found. + */ + public abstract Optional getArray(String key); + + /** + * Checks if a value for the given key exists. + * + * @param key The key to check. + * @return {@code true} if the key exists, otherwise {@code false}. + */ + public abstract boolean hasKey(String key); + + /** + * Returns the underlying data source object. + * + * @return The raw data source object. + */ + public T getBackingObject() { + return backingObject; + } + + /** + * Retrieves a list of strings from a key that can be either a single string or an array of strings. + * + * @param key The key to look up. + * @return A {@link List} of strings, or an empty list if not found. + */ + public List getListOrString(String key) { + return Collections.emptyList(); + } +} diff --git a/src/main/java/me/andreasmelone/basicmodinfoparser/util/adapter/JsonAdapter.java b/src/main/java/me/andreasmelone/basicmodinfoparser/util/adapter/JsonAdapter.java new file mode 100644 index 0000000..f9af9dc --- /dev/null +++ b/src/main/java/me/andreasmelone/basicmodinfoparser/util/adapter/JsonAdapter.java @@ -0,0 +1,156 @@ +/* + * MIT License + * + * Copyright (c) 2024-2025 RaydanOMGr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package me.andreasmelone.basicmodinfoparser.util.adapter; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +/** + * A {@link DataAdapter} implementation for {@link JsonObject}. This class + * provides a standardized way to access data from a JSON structure, allowing + * for consistent data retrieval regardless of the underlying format. + */ +public class JsonAdapter extends DataAdapter { + /** + * Constructs a {@link JsonAdapter} with the given {@link JsonObject}. + * + * @param jsonObject The JSON object to adapt. + */ + public JsonAdapter(JsonObject jsonObject) { + super(jsonObject); + } + + @Override + public Optional getString(String key) { + return getElement(key) + .filter(element -> element.isJsonPrimitive() + && element.getAsJsonPrimitive().isString()) + .map(JsonElement::getAsString); + } + + @Override + public Optional getBoolean(String key) { + return getElement(key) + .filter(element -> element.isJsonPrimitive() + && element.getAsJsonPrimitive().isBoolean()) + .map(JsonElement::getAsBoolean); + } + + @Override + public Optional getArray(String key) { + return getElement(key) + .filter(JsonElement::isJsonArray) + .map(JsonElement::getAsJsonArray); + } + + @Override + public boolean hasKey(String key) { + return getElement(key).isPresent(); + } + + @Override + public List getListOrString(String key) { + // When searching for author names, these are the situations we + // need to take into account: + // + // ["example", "example2", ...] + // (From Legacy Forge, Fabric and Quilt) + // + // [{ "name": "example" }, { "name": "example2" }, ...] + // (Also from Fabric and Quilt) + // + // { "Name": "Role", "Name2": "Role2", ... } (Only in Quilt) + // + // The first two ways of representing author names are interchangeable in Fabric, + // so an array could have both types at the same time! + + if (!hasKey(key)) return Collections.emptyList(); + + JsonElement element = backingObject.get(key); + List result = new ArrayList<>(); + + if (element.isJsonNull() || !element.isJsonArray() && !element.isJsonObject()) return Collections.emptyList(); + + // Since both on Forge Legacy (https://docs.minecraftforge.net/en/1.13.x/gettingstarted/structuring/) + // and Fabric (https://wiki.fabricmc.net/documentation:fabric_mod_json#metadata) the + // author names are inside a json array, we can assume that, if the element is an object, + // we are dealing with Quilt syntax + if (element.isJsonObject()) { + JsonObject asObject = element.getAsJsonObject(); + result.addAll(asObject.keySet()); + return result; + } + + JsonArray asJsonArray = element.getAsJsonArray(); + for (JsonElement jsonElement : asJsonArray) { + // Case number 1: "authors": ["example"] + if (jsonElement.isJsonPrimitive()) { + // Since we found an author name, we can add it to the list + result.add(jsonElement.getAsString()); + continue; + } + + // Case number 2: "authors": [{ "name": "example" }, {"..."}] + if (jsonElement.isJsonObject()) { + JsonObject asObject = asJsonArray.get(0).getAsJsonObject(); + if (asObject.has("name")) { + // Since we found an author name, we can add it to the list + result.add(asObject.get("name").getAsString()); + } + } + } + + return result; + } + + /** + * Retrieves a {@link JsonElement} from the backing {@link JsonObject} based on the given key. + * The key can be a dot-separated path to access nested elements. + * + * @param key The key or path to the element. + * @return An {@link Optional} containing the {@link JsonElement} if found, otherwise empty. + */ + private Optional getElement(String key) { + String[] parts = key.split("\\."); + JsonElement current = backingObject; + + for (String part : parts) { + if (current == null || !current.isJsonObject()) { + return Optional.empty(); + } + JsonObject obj = current.getAsJsonObject(); + if (!obj.has(part)) { + return Optional.empty(); + } + current = obj.get(part); + } + return Optional.ofNullable(current); + } +} \ No newline at end of file diff --git a/src/main/java/me/andreasmelone/basicmodinfoparser/util/adapter/TomlAdapter.java b/src/main/java/me/andreasmelone/basicmodinfoparser/util/adapter/TomlAdapter.java new file mode 100644 index 0000000..b86fa81 --- /dev/null +++ b/src/main/java/me/andreasmelone/basicmodinfoparser/util/adapter/TomlAdapter.java @@ -0,0 +1,83 @@ +/* + * MIT License + * + * Copyright (c) 2024-2025 RaydanOMGr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package me.andreasmelone.basicmodinfoparser.util.adapter; + +import org.tomlj.TomlArray; +import org.tomlj.TomlTable; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * A {@link DataAdapter} implementation for {@link TomlTable}. This class + * provides a standardized way to access data from a TOML structure, allowing + * for consistent data retrieval regardless of the underlying format. + */ +public class TomlAdapter extends DataAdapter { + public TomlAdapter(TomlTable backingObject) { + super(backingObject); + } + + @Override + public Optional getString(String key) { + return Optional.ofNullable(backingObject.getString(key)); + } + + @Override + public Optional getBoolean(String key) { + return Optional.ofNullable(backingObject.getBoolean(key)); + } + + @Override + public Optional getArray(String key) { + return Optional.ofNullable(backingObject.getArray(key)); + } + + @Override + public boolean hasKey(String key) { + return backingObject.contains(key); + } + + @Override + public List getListOrString(String key) { + if (!hasKey(key)) { + return Collections.emptyList(); + } + + if (backingObject.isString(key)) { + return Collections.singletonList(backingObject.getString(key)); + } + + Optional arrayOptional = getArray(key); + return arrayOptional.map(tomlArray -> tomlArray.toList().stream() + .map(Object::toString) + .collect(Collectors.toList())) + .orElseGet(() -> getString(key) + .map(Collections::singletonList) + .orElse(Collections.emptyList())); + + } +} diff --git a/src/test/java/me/andreasmelone/basicmodinfoparser/test/LooseSemanticVersionParserTests.java b/src/test/java/me/andreasmelone/basicmodinfoparser/test/LooseSemanticVersionParserTests.java index 6cc9725..b726b8e 100644 --- a/src/test/java/me/andreasmelone/basicmodinfoparser/test/LooseSemanticVersionParserTests.java +++ b/src/test/java/me/andreasmelone/basicmodinfoparser/test/LooseSemanticVersionParserTests.java @@ -1,6 +1,7 @@ package me.andreasmelone.basicmodinfoparser.test; import me.andreasmelone.basicmodinfoparser.platform.dependency.fabric.LooseSemanticVersion; +import me.andreasmelone.basicmodinfoparser.platform.dependency.version.LooseSemanticVersionFactory; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -13,7 +14,7 @@ public class LooseSemanticVersionParserTests { class Basic { @Test void parsesSimpleVersion() { - Optional ver = LooseSemanticVersion.parse("1.0.0"); + Optional ver = new LooseSemanticVersionFactory().parseVersion("1.0.0"); assertTrue(ver.isPresent()); assertArrayEquals(new int[] { 1, 0, 0 }, ver.get().getVersionParts()); assertNull(ver.get().getPreReleaseSuffix()); @@ -23,7 +24,7 @@ void parsesSimpleVersion() { @Test void parsesWithMetadata() { - Optional ver = LooseSemanticVersion.parse("1.0.0+metadata"); + Optional ver = new LooseSemanticVersionFactory().parseVersion("1.0.0+metadata"); assertTrue(ver.isPresent()); assertArrayEquals(new int[] { 1, 0, 0 }, ver.get().getVersionParts()); assertNull(ver.get().getPreReleaseSuffix()); @@ -36,7 +37,7 @@ void parsesWithMetadata() { class PreRelease { @Test void parsesWithoutNumber() { - Optional ver = LooseSemanticVersion.parse("1.0.0-alpha"); + Optional ver = new LooseSemanticVersionFactory().parseVersion("1.0.0-alpha"); assertTrue(ver.isPresent()); assertArrayEquals(new int[] { 1, 0, 0 }, ver.get().getVersionParts()); assertEquals("alpha", ver.get().getPreReleaseSuffix()); @@ -46,7 +47,7 @@ void parsesWithoutNumber() { @Test void parsesWithNumber() { - Optional ver = LooseSemanticVersion.parse("1.0.0-alpha.1"); + Optional ver = new LooseSemanticVersionFactory().parseVersion("1.0.0-alpha.1"); assertTrue(ver.isPresent()); assertArrayEquals(new int[] { 1, 0, 0 }, ver.get().getVersionParts()); assertEquals("alpha", ver.get().getPreReleaseSuffix()); @@ -56,7 +57,7 @@ void parsesWithNumber() { @Test void parsesWithEmptyPreRelease() { - Optional ver = LooseSemanticVersion.parse("1.2-"); + Optional ver = new LooseSemanticVersionFactory().parseVersion("1.2-"); assertTrue(ver.isPresent()); assertArrayEquals(new int[] { 1, 2 }, ver.get().getVersionParts()); assertEquals("", ver.get().getPreReleaseSuffix()); @@ -69,7 +70,7 @@ void parsesWithEmptyPreRelease() { class Wildcard { @Test void parsesLowercaseXWildcard() { - Optional ver = LooseSemanticVersion.parse("1.2.x", true); + Optional ver = new LooseSemanticVersionFactory().parseVersion("1.2.x", true); assertTrue(ver.isPresent()); assertArrayEquals(new int[] { 1, 2, 0 }, ver.get().getVersionParts()); assertNull(ver.get().getPreReleaseSuffix()); @@ -80,7 +81,7 @@ void parsesLowercaseXWildcard() { @Test void parsesUppercaseXWildcard() { - Optional ver = LooseSemanticVersion.parse("1.2.X", true); + Optional ver = new LooseSemanticVersionFactory().parseVersion("1.2.X", true); assertTrue(ver.isPresent()); assertArrayEquals(new int[] { 1, 2, 0 }, ver.get().getVersionParts()); assertNull(ver.get().getPreReleaseSuffix()); @@ -91,7 +92,7 @@ void parsesUppercaseXWildcard() { @Test void parsesAsterixWildcard() { - Optional ver = LooseSemanticVersion.parse("1.2.*", true); + Optional ver = new LooseSemanticVersionFactory().parseVersion("1.2.*", true); assertTrue(ver.isPresent()); assertArrayEquals(new int[] { 1, 2, 0 }, ver.get().getVersionParts()); assertNull(ver.get().getPreReleaseSuffix()); @@ -102,7 +103,7 @@ void parsesAsterixWildcard() { @Test void parsesWithMiddleWildcard() { - Optional ver = LooseSemanticVersion.parse("1.x.4", true); + Optional ver = new LooseSemanticVersionFactory().parseVersion("1.x.4", true); assertTrue(ver.isPresent()); assertArrayEquals(new int[] { 1, 0, 4 }, ver.get().getVersionParts()); assertNull(ver.get().getPreReleaseSuffix()); @@ -116,7 +117,7 @@ void parsesWithMiddleWildcard() { class General { @Test void parsesOrdinarySemVer() { - Optional ver = LooseSemanticVersion.parse("1.0.0-alpha.1+metadata"); + Optional ver = new LooseSemanticVersionFactory().parseVersion("1.0.0-alpha.1+metadata"); assertTrue(ver.isPresent()); assertArrayEquals(new int[] { 1, 0, 0 }, ver.get().getVersionParts()); assertEquals("alpha", ver.get().getPreReleaseSuffix()); @@ -126,7 +127,7 @@ void parsesOrdinarySemVer() { @Test void parsesWithLessComponents() { - Optional ver = LooseSemanticVersion.parse("1.0-alpha.1+metadata"); + Optional ver = new LooseSemanticVersionFactory().parseVersion("1.0-alpha.1+metadata"); assertTrue(ver.isPresent()); assertArrayEquals(new int[] { 1, 0 }, ver.get().getVersionParts()); assertEquals("alpha", ver.get().getPreReleaseSuffix()); @@ -136,7 +137,7 @@ void parsesWithLessComponents() { @Test void parsesWithMoreComponents() { - Optional ver = LooseSemanticVersion.parse("1.0.0.0-alpha.1+metadata"); + Optional ver = new LooseSemanticVersionFactory().parseVersion("1.0.0.0-alpha.1+metadata"); assertTrue(ver.isPresent()); assertArrayEquals(new int[] { 1, 0, 0, 0 }, ver.get().getVersionParts()); assertEquals("alpha", ver.get().getPreReleaseSuffix()); @@ -146,7 +147,7 @@ void parsesWithMoreComponents() { @Test void parsesRealVersion() { - Optional ver = LooseSemanticVersion.parse("11.0.0-alpha.3+0.102.0-1.21"); + Optional ver = new LooseSemanticVersionFactory().parseVersion("11.0.0-alpha.3+0.102.0-1.21"); assertTrue(ver.isPresent()); assertArrayEquals(new int[] { 11, 0, 0 }, ver.get().getVersionParts()); assertEquals("alpha", ver.get().getPreReleaseSuffix()); @@ -159,19 +160,19 @@ void parsesRealVersion() { class Invalid { @Test void rejectsGarbage() { - Optional ver = LooseSemanticVersion.parse("potato"); + Optional ver = new LooseSemanticVersionFactory().parseVersion("potato"); assertFalse(ver.isPresent()); } @Test void rejectsEmptyString() { - Optional ver = LooseSemanticVersion.parse(""); + Optional ver = new LooseSemanticVersionFactory().parseVersion(""); assertFalse(ver.isPresent()); } @Test void rejectsNonAlphanumeric() { - Optional ver = LooseSemanticVersion.parse("1.0.0-абрикос.2+不甜瓜"); + Optional ver = new LooseSemanticVersionFactory().parseVersion("1.0.0-абрикос.2+不甜瓜"); assertFalse(ver.isPresent()); } }