diff --git a/pom.xml b/pom.xml index 8555e1e10..17163bc3e 100644 --- a/pom.xml +++ b/pom.xml @@ -309,7 +309,9 @@ wiki.json languages/translators.json + tags/*.json + biome-maps/*.json languages/**/*.yml diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/api/exceptions/BiomeMapException.java b/src/main/java/io/github/thebusybiscuit/slimefun4/api/exceptions/BiomeMapException.java new file mode 100644 index 000000000..9e691185a --- /dev/null +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/api/exceptions/BiomeMapException.java @@ -0,0 +1,48 @@ +package io.github.thebusybiscuit.slimefun4.api.exceptions; + +import javax.annotation.ParametersAreNonnullByDefault; + +import org.bukkit.NamespacedKey; + +import io.github.thebusybiscuit.slimefun4.utils.biomes.BiomeMap; + +/** + * An {@link BiomeMapException} is thrown whenever a {@link BiomeMap} + * contains illegal, invalid or unknown values. + * + * @author TheBusyBiscuit + * + */ +public class BiomeMapException extends Exception { + + private static final long serialVersionUID = -1894334121194788527L; + + /** + * This constructs a new {@link BiomeMapException} for the given + * {@link BiomeMap}'s {@link NamespacedKey} with the provided context. + * + * @param key + * The {@link NamespacedKey} of our {@link BiomeMap} + * @param message + * The message to display + */ + @ParametersAreNonnullByDefault + public BiomeMapException(NamespacedKey key, String message) { + super("Biome Map '" + key + "' has been misconfigured: " + message); + } + + /** + * This constructs a new {@link BiomeMapException} for the given + * {@link BiomeMap}'s {@link NamespacedKey} with the provided context. + * + * @param key + * The {@link NamespacedKey} of our {@link BiomeMap} + * @param cause + * The {@link Throwable} which has caused this to happen + */ + @ParametersAreNonnullByDefault + public BiomeMapException(NamespacedKey key, Throwable cause) { + super("Tag '" + key + "' has been misconfigured (" + cause.getMessage() + ')', cause); + } + +} diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/resources/SlimefunResource.java b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/resources/SlimefunResource.java index b8fb6c7e2..975e463df 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/resources/SlimefunResource.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/resources/SlimefunResource.java @@ -1,5 +1,7 @@ package io.github.thebusybiscuit.slimefun4.implementation.resources; +import java.util.logging.Level; + import javax.annotation.Nonnull; import javax.annotation.ParametersAreNonnullByDefault; @@ -7,8 +9,12 @@ import org.apache.commons.lang.Validate; import org.bukkit.NamespacedKey; import org.bukkit.inventory.ItemStack; +import com.google.gson.JsonElement; + +import io.github.thebusybiscuit.slimefun4.api.exceptions.BiomeMapException; import io.github.thebusybiscuit.slimefun4.api.geo.GEOResource; import io.github.thebusybiscuit.slimefun4.implementation.Slimefun; +import io.github.thebusybiscuit.slimefun4.utils.biomes.BiomeMap; /** * This is an abstract parent class for any {@link GEOResource} @@ -70,4 +76,34 @@ abstract class SlimefunResource implements GEOResource { return geoMiner; } + /** + * Internal helper method for reading a {@link BiomeMap} of {@link Integer} type values from + * a resource file. + * + * @param resource + * The {@link SlimefunResource} instance + * @param path + * The path to our biome map file + * + * @return A {@link BiomeMap} for this resource + */ + @ParametersAreNonnullByDefault + static final @Nonnull BiomeMap getBiomeMap(SlimefunResource resource, String path) { + Validate.notNull(resource, "Resource cannot be null"); + Validate.notNull(path, "Path cannot be null"); + + try { + return BiomeMap.fromResource(resource.getKey(), path, JsonElement::getAsInt); + } catch (BiomeMapException x) { + if (Slimefun.instance().isUnitTest()) { + // Unit Tests should always fail here, so we re-throw the exception + throw new IllegalStateException(x); + } else { + // In a server environment, we should just print a warning and carry on + Slimefun.logger().log(Level.WARNING, x, () -> "Failed to load BiomeMap for GEO-resource: " + resource.getKey()); + return new BiomeMap<>(resource.getKey()); + } + } + } + } diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/utils/PatternUtils.java b/src/main/java/io/github/thebusybiscuit/slimefun4/utils/PatternUtils.java index 354aa17f5..3063f55f1 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/utils/PatternUtils.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/utils/PatternUtils.java @@ -20,7 +20,8 @@ public final class PatternUtils { public static final Pattern SLASH_SEPARATOR = Pattern.compile(" / "); - public static final Pattern MINECRAFT_MATERIAL = Pattern.compile("minecraft:[a-z_]+"); + public static final Pattern MINECRAFT_NAMESPACEDKEY = Pattern.compile("minecraft:[a-z_]+"); + public static final Pattern MINECRAFT_TAG = Pattern.compile("#minecraft:[a-z_]+"); public static final Pattern SLIMEFUN_TAG = Pattern.compile("#slimefun:[a-z_]+"); diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/utils/biomes/BiomeMap.java b/src/main/java/io/github/thebusybiscuit/slimefun4/utils/biomes/BiomeMap.java new file mode 100644 index 000000000..60c8488a9 --- /dev/null +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/utils/biomes/BiomeMap.java @@ -0,0 +1,107 @@ +package io.github.thebusybiscuit.slimefun4.utils.biomes; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.EnumMap; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.annotation.ParametersAreNonnullByDefault; + +import org.apache.commons.lang.Validate; +import org.bukkit.Keyed; +import org.bukkit.NamespacedKey; +import org.bukkit.block.Biome; + +import com.google.gson.JsonElement; + +import io.github.thebusybiscuit.slimefun4.api.exceptions.BiomeMapException; +import io.github.thebusybiscuit.slimefun4.implementation.Slimefun; + +public class BiomeMap implements Keyed { + + private final Map dataMap = new EnumMap<>(Biome.class); + private final NamespacedKey namespacedKey; + + @ParametersAreNonnullByDefault + public BiomeMap(NamespacedKey namespacedKey) { + Validate.notNull(namespacedKey, "The key must not be null."); + + this.namespacedKey = namespacedKey; + } + + public @Nullable T get(@Nonnull Biome biome) { + Validate.notNull(biome, "The biome shall not be null."); + + return dataMap.get(biome); + } + + public @Nonnull T getOrDefault(@Nonnull Biome biome, T defaultValue) { + Validate.notNull(biome, "The biome should not be null."); + + return dataMap.getOrDefault(biome, defaultValue); + } + + public boolean contains(@Nonnull Biome biome) { + Validate.notNull(biome, "The biome must not be null."); + + return dataMap.containsKey(biome); + } + + public boolean put(@Nonnull Biome biome, @Nonnull T value) { + Validate.notNull(biome, "The biome should not be null."); + Validate.notNull(value, "Values cannot be null."); + + return dataMap.put(biome, value) == null; + } + + public void putAll(@Nonnull Map map) { + Validate.notNull(map, "The map should not be null."); + + dataMap.putAll(map); + } + + public void putAll(@Nonnull BiomeMap map) { + Validate.notNull(map, "The map should not be null."); + + dataMap.putAll(map.dataMap); + } + + /** + * {@inheritDoc} + */ + @Override + public @Nonnull NamespacedKey getKey() { + return namespacedKey; + } + + @Override + public String toString() { + return "BiomeMap " + dataMap.toString(); + } + + @ParametersAreNonnullByDefault + public static @Nonnull BiomeMap fromJson(NamespacedKey key, String json, Function valueConverter) throws BiomeMapException { + // All parameters are validated by the Parser. + BiomeMapParser parser = new BiomeMapParser<>(key, valueConverter); + parser.read(json); + return parser.buildBiomeMap(); + } + + @ParametersAreNonnullByDefault + public static @Nonnull BiomeMap fromResource(NamespacedKey key, String path, Function valueConverter) throws BiomeMapException { + Validate.notNull(key, "The key shall not be null."); + Validate.notNull(path, "The path should not be null!"); + + try (BufferedReader reader = new BufferedReader(new InputStreamReader(Slimefun.class.getResourceAsStream(path), StandardCharsets.UTF_8))) { + return fromJson(key, reader.lines().collect(Collectors.joining("")), valueConverter); + } catch (IOException x) { + throw new BiomeMapException(key, x); + } + } +} diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/utils/biomes/BiomeMapParser.java b/src/main/java/io/github/thebusybiscuit/slimefun4/utils/biomes/BiomeMapParser.java new file mode 100644 index 000000000..93a2ab87e --- /dev/null +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/utils/biomes/BiomeMapParser.java @@ -0,0 +1,135 @@ +package io.github.thebusybiscuit.slimefun4.utils.biomes; + +import java.util.EnumMap; +import java.util.EnumSet; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; + +import javax.annotation.Nonnull; +import javax.annotation.ParametersAreNonnullByDefault; + +import org.apache.commons.lang.Validate; +import org.bukkit.NamespacedKey; +import org.bukkit.block.Biome; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonParser; + +import io.github.bakedlibs.dough.common.CommonPatterns; +import io.github.thebusybiscuit.slimefun4.api.exceptions.BiomeMapException; +import io.github.thebusybiscuit.slimefun4.utils.PatternUtils; + +class BiomeMapParser { + + private static final String VALUE_KEY = "value"; + private static final String BIOMES_KEY = "biomes"; + + private final NamespacedKey key; + private final Function valueConverter; + private final Map map = new EnumMap<>(Biome.class); + + @ParametersAreNonnullByDefault + BiomeMapParser(NamespacedKey key, Function valueConverter) { + Validate.notNull(key, "The key shall not be null."); + Validate.notNull(valueConverter, "You must provide a Function to convert raw json values to your desired data type."); + + this.key = key; + this.valueConverter = valueConverter; + } + + void read(@Nonnull String json) throws BiomeMapException { + Validate.notNull(json, "The JSON string should not be null!"); + JsonArray root = null; + + try { + JsonParser parser = new JsonParser(); + root = parser.parse(json).getAsJsonArray(); + } catch (IllegalStateException | JsonParseException x) { + throw new BiomeMapException(key, x); + } + + /* + * We don't include this in our try/catch, as this type of exception + * is already specified in the throws-declaration. + */ + read(root); + } + + void read(@Nonnull JsonArray json) throws BiomeMapException { + Validate.notNull(json, "The JSON Array should not be null!"); + + for (JsonElement element : json) { + if (element instanceof JsonObject) { + readEntry(element.getAsJsonObject()); + } else { + throw new BiomeMapException(key, "Unexpected array element: " + element.getClass().getSimpleName() + " - " + element.toString()); + } + } + } + + private void readEntry(@Nonnull JsonObject entry) throws BiomeMapException { + Validate.notNull(entry, "The JSON entry should not be null!"); + + if (entry.has(VALUE_KEY)) { + T value = valueConverter.apply(entry.get(VALUE_KEY)); + + if (entry.has(BIOMES_KEY) && entry.get(BIOMES_KEY).isJsonArray()) { + Set biomes = readBiomes(entry.get(BIOMES_KEY).getAsJsonArray()); + + for (Biome biome : biomes) { + T prev = map.put(biome, value); + + if (prev != null) { + throw new BiomeMapException(key, "Biome '" + biome.getKey() + "' is registered twice"); + } + } + } else { + throw new BiomeMapException(key, "Entry is missing a 'biomes' child of type array."); + } + } else { + throw new BiomeMapException(key, "Entry is missing a 'value' child."); + } + } + + private @Nonnull Set readBiomes(@Nonnull JsonArray array) throws BiomeMapException { + Validate.notNull(array, "The JSON array should not be null!"); + Set biomes = EnumSet.noneOf(Biome.class); + + for (JsonElement element : array) { + if (element.isJsonPrimitive() && element.getAsJsonPrimitive().isString()) { + String value = element.getAsString(); + + if (PatternUtils.MINECRAFT_NAMESPACEDKEY.matcher(value).matches()) { + String formattedValue = CommonPatterns.COLON.split(value)[1].toUpperCase(Locale.ROOT); + + try { + Biome biome = Biome.valueOf(formattedValue); + biomes.add(biome); + } catch (IllegalArgumentException x) { + throw new BiomeMapException(key, "The Biome '" + value + "' does not exist!"); + } + } else { + // The regular expression did not match + throw new BiomeMapException(key, "Could not recognize value '" + value + "'"); + } + } else { + throw new BiomeMapException(key, "Unexpected array element: " + element.getClass().getSimpleName() + " - " + element.toString()); + } + } + + return biomes; + } + + @Nonnull + BiomeMap buildBiomeMap() { + BiomeMap biomeMap = new BiomeMap<>(key); + biomeMap.putAll(map); + return biomeMap; + } + +} diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/utils/biomes/package-info.java b/src/main/java/io/github/thebusybiscuit/slimefun4/utils/biomes/package-info.java new file mode 100644 index 000000000..c4f513a0b --- /dev/null +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/utils/biomes/package-info.java @@ -0,0 +1,5 @@ +/** + * This package contains classes centered around our {@link io.github.thebusybiscuit.slimefun4.utils.biomes.BiomeMap} + * utility. + */ +package io.github.thebusybiscuit.slimefun4.utils.biomes; \ No newline at end of file diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/utils/tags/TagParser.java b/src/main/java/io/github/thebusybiscuit/slimefun4/utils/tags/TagParser.java index 41d951452..dc181d2d0 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/utils/tags/TagParser.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/utils/tags/TagParser.java @@ -134,7 +134,7 @@ public class TagParser implements Keyed { @ParametersAreNonnullByDefault private void parsePrimitiveValue(String value, Set materials, Set> tags, boolean throwException) throws TagMisconfigurationException { - if (PatternUtils.MINECRAFT_MATERIAL.matcher(value).matches()) { + if (PatternUtils.MINECRAFT_NAMESPACEDKEY.matcher(value).matches()) { // Match the NamespacedKey against Materials Material material = Material.matchMaterial(value); diff --git a/src/test/java/io/github/thebusybiscuit/slimefun4/utils/biomes/TestBiomeMapParser.java b/src/test/java/io/github/thebusybiscuit/slimefun4/utils/biomes/TestBiomeMapParser.java new file mode 100644 index 000000000..aaa436432 --- /dev/null +++ b/src/test/java/io/github/thebusybiscuit/slimefun4/utils/biomes/TestBiomeMapParser.java @@ -0,0 +1,78 @@ +package io.github.thebusybiscuit.slimefun4.utils.biomes; + +import java.util.function.Function; + +import javax.annotation.ParametersAreNonnullByDefault; + +import org.bukkit.NamespacedKey; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import com.google.gson.JsonElement; + +import io.github.thebusybiscuit.slimefun4.api.exceptions.BiomeMapException; +import io.github.thebusybiscuit.slimefun4.implementation.Slimefun; + +import be.seeseemelk.mockbukkit.MockBukkit; + +class TestBiomeMapParser { + + private static final Function AS_STRING = JsonElement::getAsString; + private static final Function AS_INT = JsonElement::getAsInt; + + private static Slimefun plugin; + private static NamespacedKey key; + + @BeforeAll + public static void load() { + MockBukkit.mock(); + plugin = MockBukkit.load(Slimefun.class); + key = new NamespacedKey(plugin, "test"); + } + + @AfterAll + public static void unload() { + MockBukkit.unmock(); + } + + @Test + @DisplayName("Test JSON Parsing Error handling") + void testInvalidJson() { + assertMisconfiguration(AS_STRING, ""); + assertMisconfiguration(AS_STRING, "1234"); + assertMisconfiguration(AS_STRING, "hello world"); + assertMisconfiguration(AS_STRING, "{}"); + } + + @Test + @DisplayName("Test Array not having proper children") + void testInvalidArrayChildren() { + assertMisconfiguration(AS_STRING, "[1, 2, 3]"); + assertMisconfiguration(AS_STRING, "[[1], [2]]"); + assertMisconfiguration(AS_STRING, "[\"foo\", \"bar\"]"); + } + + @Test + @DisplayName("Test Array entries being misconfigured") + void testInvalidEntries() { + assertMisconfiguration(AS_STRING, "[{}]"); + assertMisconfiguration(AS_STRING, "[{\"id\": \"one\"}]"); + } + + @Test + @DisplayName("Test Array entries being incomplete") + void testIncompleteEntries() { + assertMisconfiguration(AS_STRING, "[{\"value\": \"cool\"}]"); + assertMisconfiguration(AS_STRING, "[{\"biomes\": []}]"); + } + + @ParametersAreNonnullByDefault + private void assertMisconfiguration(Function function, String json) { + BiomeMapParser parser = new BiomeMapParser<>(key, function); + Assertions.assertThrows(BiomeMapException.class, () -> parser.read(json)); + } + +}