Skip to content

Commit 118d50d

Browse files
committed
Improved null-safety and added some exception handling
1 parent 31bad17 commit 118d50d

6 files changed

Lines changed: 153 additions & 69 deletions

File tree

build.gradle

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ plugins {
44
}
55

66
group = 'me.andreasmelone'
7-
version = '1.0-SNAPSHOT'
7+
version = '1.0.1'
88

99
repositories {
1010
mavenCentral()
@@ -26,10 +26,6 @@ jar {
2626
}
2727
}
2828

29-
test {
30-
useJUnitPlatform()
31-
}
32-
3329
publishing {
3430
publications {
3531
mavenJava(MavenPublication) {

src/main/java/me/andreasmelone/basicmodinfoparser/BasicModInfo.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import java.util.Objects;
44

55
public class BasicModInfo {
6+
private static final BasicModInfo EMPTY = new BasicModInfo(null, null, null, null);
7+
68
private final String id;
79
private final String name;
810
private final String version;
@@ -53,4 +55,8 @@ public String toString() {
5355
", description='" + description + '\'' +
5456
'}';
5557
}
58+
59+
public static BasicModInfo empty() {
60+
return EMPTY;
61+
}
5662
}
Lines changed: 32 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,14 @@
11
package me.andreasmelone.basicmodinfoparser;
22

3-
import com.google.gson.Gson;
4-
import com.google.gson.JsonArray;
5-
import com.google.gson.JsonObject;
6-
import com.google.gson.JsonSyntaxException;
7-
import org.tomlj.Toml;
8-
import org.tomlj.TomlParseResult;
9-
import org.tomlj.TomlTable;
3+
import com.google.gson.*;
4+
import me.andreasmelone.basicmodinfoparser.util.ModInfoParseException;
5+
import me.andreasmelone.basicmodinfoparser.util.ParserUtils;
6+
import org.tomlj.*;
107

118
import java.io.File;
129
import java.io.FileInputStream;
1310
import java.io.IOException;
1411
import java.io.InputStream;
15-
import java.nio.file.Path;
16-
import java.nio.file.Paths;
1712
import java.util.ArrayList;
1813
import java.util.List;
1914
import java.util.jar.JarFile;
@@ -22,28 +17,31 @@
2217
import java.util.zip.ZipFile;
2318
import java.util.zip.ZipInputStream;
2419

20+
import static me.andreasmelone.basicmodinfoparser.util.ParserUtils.*;
21+
2522
public enum Platform {
2623
/**
2724
* Legacy Forge platform, which uses the {@code mcmod.info} file containing JSON data.
2825
*/
2926
FORGE_LEGACY((str) -> {
3027
Gson gson = new Gson();
31-
JsonObject jsonObj = gson.fromJson(str, JsonArray.class).get(0).getAsJsonObject();
32-
String modId = jsonObj.get("modid").getAsString();
33-
String name = jsonObj.get("name").getAsString();
34-
String description = jsonObj.get("description").getAsString();
35-
String version = jsonObj.get("version").getAsString();
36-
37-
return new BasicModInfo(modId, name, version, description);
28+
JsonArray topArray = gson.fromJson(str, JsonArray.class);
29+
if (topArray == null || topArray.size() == 0 || !topArray.get(0).isJsonObject()) {
30+
return BasicModInfo.empty();
31+
}
32+
return ParserUtils.createModInfoFromJsonObject(topArray.get(0).getAsJsonObject(),
33+
"modid", "name", "version", "description");
3834
}, "mcmod.info"),
3935
/**
4036
* Forge platform, which uses the {@code mods.toml} file with TOML data.
4137
* It can also be used to parse NeoForge mods.
4238
*/
43-
4439
FORGE((str) -> {
4540
TomlParseResult result = Toml.parse(str);
46-
TomlTable modInfo = result.getArray("mods").getTable(0);
41+
TomlArray modsArray = result.getArray("mods");
42+
if(modsArray == null || modsArray.isEmpty()) return BasicModInfo.empty();
43+
TomlTable modInfo = modsArray.getTable(0);
44+
if(modInfo.isEmpty()) return BasicModInfo.empty();
4745

4846
String modId = modInfo.getString("modId");
4947
String name = modInfo.getString("displayName");
@@ -58,12 +56,9 @@ public enum Platform {
5856
FABRIC((str) -> {
5957
Gson gson = new Gson();
6058
JsonObject jsonObj = gson.fromJson(str, JsonObject.class);
61-
String modId = jsonObj.get("id").getAsString();
62-
String name = jsonObj.get("name").getAsString();
63-
String description = jsonObj.get("description").getAsString();
64-
String version = jsonObj.get("version").getAsString();
65-
66-
return new BasicModInfo(modId, name, version, description);
59+
if(jsonObj == null) return BasicModInfo.empty();
60+
return ParserUtils.createModInfoFromJsonObject(jsonObj,
61+
"modId", "name", "version", "description");
6762
}, "fabric.mod.json");
6863

6964
private final String[] infoFilePaths;
@@ -80,15 +75,24 @@ public enum Platform {
8075
* @param toParse The string to parse, expected to be in the format of the specific platform (JSON, TOML, etc.).
8176
* This string must match the format required by the platform; otherwise, parsing will fail.
8277
* @return The {@link BasicModInfo} object containing mod information.
83-
* @throws JsonSyntaxException If the string is invalid JSON (for platforms like FABRIC and FORGE_LEGACY).
84-
* @throws IllegalStateException If the string does not match the expected TOML format (for FORGE).
78+
* @throws IllegalArgumentException If the input string is null.
79+
* @throws ModInfoParseException If an error occurs while parsing the TOML (for Forge) or JSON (for Legacy Forge and Fabric).
8580
*/
8681
public BasicModInfo parse(String toParse) {
87-
return parserFunction.apply(toParse);
82+
if (toParse == null) {
83+
throw new IllegalArgumentException("Input string cannot be null");
84+
}
85+
86+
try {
87+
return parserFunction.apply(toParse);
88+
} catch (TomlParseError | TomlInvalidTypeException | JsonParseException e) {
89+
throw new ModInfoParseException("Error parsing the mod info from the given string.", e);
90+
}
8891
}
8992

93+
9094
/**
91-
* Reads and returns the content of the platform-specific info file (e.g. `mcmod.info`, `mods.toml`, etc.) from a zip archive.
95+
* Reads and returns the content of the platform-specific info file (e.g. {@code mcmod.info}, {@code mods.toml}, etc.) from a zip archive.
9296
*
9397
* @param zip The {@link ZipFile} to search for the platform-specific info file. It is allowed to be a derivative of such, e.g. {@link JarFile}
9498
* @return The content of the info file as a string, or {@code null} if the info file was not found.
@@ -134,39 +138,4 @@ public static Platform[] findModPlatform(File file) throws IOException {
134138

135139
return platforms.toArray(new Platform[0]);
136140
}
137-
138-
/**
139-
* Compares an array of paths to a single path to check if any match after normalisation.<p>
140-
* This ensures that two paths are considered equal, even if their string representations differ
141-
* (e.g., {@code run/embeddium.jar} and {@code .\run\embeddium.jar}).
142-
*
143-
* @param paths An array of paths to compare.
144-
* @param path2 The path to compare against.
145-
* @return {@code true} if any of the provided paths are equal to {@code path2} after normalisation, otherwise {@code false}.
146-
*/
147-
private static boolean comparePaths(String[] paths, String path2) {
148-
for(String path : paths) {
149-
Path normalizedPath1 = Paths.get(path).normalize();
150-
Path normalizedPath2 = Paths.get(path2).normalize();
151-
if(normalizedPath1.equals(normalizedPath2)) return true;
152-
}
153-
return false;
154-
}
155-
156-
/**
157-
* Reads the entire content of an {@link InputStream} and returns it as a string.
158-
*
159-
* @param in The {@link InputStream} to read from.
160-
* @return The content of the InputStream as a string.
161-
* @throws IOException If an I/O error occurs during reading.
162-
*/
163-
private static String readEverythingAsString(InputStream in) throws IOException {
164-
StringBuilder sb = new StringBuilder(in.available());
165-
int readBytes;
166-
byte[] buffer = new byte[1024];
167-
while((readBytes = in.read(buffer)) > 0) {
168-
sb.append(new String(buffer, 0, readBytes));
169-
}
170-
return sb.toString();
171-
}
172141
}

src/main/java/me/andreasmelone/basicmodinfoparser/Main.java renamed to src/main/java/me/andreasmelone/basicmodinfoparser/standalone/Main.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
package me.andreasmelone.basicmodinfoparser;
1+
package me.andreasmelone.basicmodinfoparser.standalone;
2+
3+
import me.andreasmelone.basicmodinfoparser.Platform;
24

35
import java.io.File;
46
import java.io.IOException;
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package me.andreasmelone.basicmodinfoparser.util;
2+
3+
/**
4+
* This exception indicates that parsing the mod info has failed
5+
*/
6+
public class ModInfoParseException extends RuntimeException {
7+
public ModInfoParseException(String message, Exception parentException) {
8+
super(message, parentException);
9+
}
10+
11+
public ModInfoParseException(Exception parentException) {
12+
super(parentException);
13+
}
14+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package me.andreasmelone.basicmodinfoparser.util;
2+
3+
import com.google.gson.JsonElement;
4+
import com.google.gson.JsonObject;
5+
import me.andreasmelone.basicmodinfoparser.BasicModInfo;
6+
7+
import java.io.IOException;
8+
import java.io.InputStream;
9+
import java.nio.file.Path;
10+
import java.nio.file.Paths;
11+
import java.util.Optional;
12+
import java.util.function.Predicate;
13+
14+
public class ParserUtils {
15+
/**
16+
* Compares an array of paths to a single path to check if any match after normalisation.<p>
17+
* This ensures that two paths are considered equal, even if their string representations differ
18+
* (e.g., {@code run/embeddium.jar} and {@code .\run\embeddium.jar}).
19+
*
20+
* @param paths An array of paths to compare.
21+
* @param path2 The path to compare against.
22+
* @return {@code true} if any of the provided paths are equal to {@code path2} after normalisation, otherwise {@code false}.
23+
*/
24+
public static boolean comparePaths(String[] paths, String path2) {
25+
for(String path : paths) {
26+
Path normalizedPath1 = Paths.get(path).normalize();
27+
Path normalizedPath2 = Paths.get(path2).normalize();
28+
if(normalizedPath1.equals(normalizedPath2)) return true;
29+
}
30+
return false;
31+
}
32+
33+
/**
34+
* Reads the entire content of an {@link InputStream} and returns it as a string.
35+
*
36+
* @param in The {@link InputStream} to read from.
37+
* @return The content of the InputStream as a string.
38+
* @throws IOException If an I/O error occurs during reading.
39+
*/
40+
public static String readEverythingAsString(InputStream in) throws IOException {
41+
StringBuilder sb = new StringBuilder(in.available());
42+
int readBytes;
43+
byte[] buffer = new byte[1024];
44+
while((readBytes = in.read(buffer)) > 0) {
45+
sb.append(new String(buffer, 0, readBytes));
46+
}
47+
return sb.toString();
48+
}
49+
50+
/**
51+
* Finds a value by key in a {@link JsonObject} and checks it against a predicate.
52+
*
53+
* @param jsonObject The {@link JsonObject} in which to search for the value.
54+
* @param key The key for which the value needs to be found.
55+
* @param predicate The predicate that the value of the key must satisfy.
56+
* @return An {@link Optional} containing the {@link JsonElement} if it exists and matches the predicate,
57+
* or an empty {@link Optional} if the value was not found or did not match.
58+
*/
59+
public static Optional<JsonElement> findValidValue(JsonObject jsonObject, String key, Predicate<JsonElement> predicate) {
60+
if (!jsonObject.has(key)) {
61+
return Optional.empty();
62+
}
63+
64+
JsonElement element = jsonObject.get(key);
65+
return predicate.test(element) ? Optional.of(element) : Optional.empty();
66+
}
67+
68+
/**
69+
* Helper method to fetch a valid string value from a {@link JsonObject}
70+
* by key, ensuring it matches the specified predicate.
71+
*
72+
* @param obj The {@link JsonObject} from which to retrieve the value.
73+
* @param key The key for the value to be retrieved.
74+
* @param predicate The predicate that the value must satisfy.
75+
* @return The string value if found and valid, or {@code null} if not found
76+
* or invalid.
77+
*/
78+
public static String getValidString(JsonObject obj, String key, Predicate<JsonElement> predicate) {
79+
return findValidValue(obj, key, predicate)
80+
.map(JsonElement::getAsString)
81+
.orElse(null);
82+
}
83+
84+
public static BasicModInfo createModInfoFromJsonObject(JsonObject jsonObject, String modIdKey,
85+
String displayNameKey, String versionKey,
86+
String descriptionKey) {
87+
Predicate<JsonElement> isStringPredicate = element ->
88+
element.isJsonPrimitive() && element.getAsJsonPrimitive().isString();
89+
90+
String modId = getValidString(jsonObject, modIdKey, isStringPredicate);
91+
String name = getValidString(jsonObject, displayNameKey, isStringPredicate);
92+
String description = getValidString(jsonObject, descriptionKey, isStringPredicate);
93+
String version = getValidString(jsonObject, versionKey, isStringPredicate);
94+
95+
return new BasicModInfo(modId, name, version, description);
96+
}
97+
}

0 commit comments

Comments
 (0)