From 0da90f3963bbe1a71cb3bb46b5be0ef35878ac4e Mon Sep 17 00:00:00 2001 From: Andreas Troelsen Date: Thu, 25 Mar 2021 10:20:49 +0100 Subject: [PATCH] Rework arena sign data store. This commit constitutes a major rewrite of how arena signs are stored and loaded. It fixes an issue where an unloaded or missing world would cause MobArena to throw errors if there were any arena signs recorded in said world. The solution is to load signs of a given world when it is available, rather than loading all signs at once indiscriminately. At startup, signs for all _currently available_ worlds are loaded. This fixes the errors. When a world loads and a WorldLoadEvent is fired, signs in that world are loaded. This ensures that all valid signs in existing worlds will _eventually_ load. To keep things tidy, a WorldUnloadEvent will unload all signs for the unloaded world. Bukkit's own YAML deserialization implementation doesn't re-throw all deserialization errors, which means we can't actually catch the problem of missing worlds without doing an awkward "scan" of the deserialized objects. This prompted a rewrite of the serialization and data storage into a custom CSV-like format which is both simpler and also provides a lot more control over the process. Instead of relying on world _names_, the new format uses world _UUIDs_. While the rest of the plugin won't necessarily adapt well to a world being renamed, the signs data store should be resilient enough to handle it. Most of the actual sign rendering code and almost all of the template code is intact, but quite a lot of other classes have been rewritten or replaced. Some of the rewrites weren't strictly necessary, but because the components were already fairly small, rewrites were much faster and a lot less awkward than attempting to adapt existing code. Fixes #645 --- changelog.md | 2 + .../com/garbagemule/MobArena/MobArena.java | 8 - .../garbagemule/MobArena/signs/ArenaSign.java | 30 +- ...nUpdates.java => HandlesArenaUpdates.java} | 28 +- .../MobArena/signs/HandlesSignClicks.java | 12 +- .../MobArena/signs/HandlesSignCreation.java | 86 +++--- .../signs/HandlesSignDestruction.java | 52 +++- .../MobArena/signs/HandlesWorldLoad.java | 57 ++++ .../MobArena/signs/HandlesWorldUnload.java | 34 +++ .../MobArena/signs/LoadsSignStore.java | 46 --- .../MobArena/signs/LoadsTemplateStore.java | 18 +- .../MobArena/signs/RedrawsArenaSigns.java | 61 ---- .../MobArena/signs/RemovesSignAtLocation.java | 26 -- .../MobArena/signs/RendersTemplate.java | 4 +- .../MobArena/signs/RendersTemplateById.java | 36 --- .../MobArena/signs/SavesSignStore.java | 32 --- .../garbagemule/MobArena/signs/SetsLines.java | 22 -- .../MobArena/signs/SignBootstrap.java | 216 +++++++++----- .../MobArena/signs/SignCreator.java | 85 ++++++ .../MobArena/signs/SignDataMigrator.java | 136 +++++++++ .../garbagemule/MobArena/signs/SignFile.java | 67 +++++ .../MobArena/signs/SignListeners.java | 42 ++- .../MobArena/signs/SignReader.java | 44 +++ .../MobArena/signs/SignRenderer.java | 80 ++++++ .../MobArena/signs/SignSerializer.java | 61 ++++ .../garbagemule/MobArena/signs/SignStore.java | 57 ++-- .../MobArena/signs/SignWriter.java | 50 ++++ .../MobArena/signs/StoresNewSign.java | 56 ---- .../garbagemule/MobArena/signs/Template.java | 2 +- .../MobArena/signs/ArenaSignTest.java | 57 ---- .../MobArena/signs/HandlesSignClicksTest.java | 16 +- .../signs/HandlesSignCreationTest.java | 220 +++++++------- .../signs/HandlesSignDestructionTest.java | 88 ++++-- .../MobArena/signs/HandlesWorldLoadTest.java | 122 ++++++++ .../signs/HandlesWorldUnloadTest.java | 65 +++++ .../MobArena/signs/InvokesSignActionTest.java | 6 +- .../MobArena/signs/RedrawsArenaSignsTest.java | 141 --------- .../signs/RemovesSignAtLocationTest.java | 56 ---- .../MobArena/signs/RendersTemplateTest.java | 8 +- .../MobArena/signs/SignCreatorTest.java | 211 ++++++++++++++ .../MobArena/signs/SignDataMigratorTest.java | 270 ++++++++++++++++++ .../MobArena/signs/SignFileTest.java | 98 +++++++ .../MobArena/signs/SignReaderTest.java | 154 ++++++++++ .../MobArena/signs/SignRendererTest.java | 161 +++++++++++ .../MobArena/signs/SignSerializerTest.java | 165 +++++++++++ .../MobArena/signs/SignStoreTest.java | 154 ++++++++++ .../MobArena/signs/SignWriterTest.java | 161 +++++++++++ .../MobArena/signs/StoresNewSignTest.java | 126 -------- 48 files changed, 2714 insertions(+), 1015 deletions(-) rename src/main/java/com/garbagemule/MobArena/signs/{RedrawsSignsOnUpdates.java => HandlesArenaUpdates.java} (64%) create mode 100644 src/main/java/com/garbagemule/MobArena/signs/HandlesWorldLoad.java create mode 100644 src/main/java/com/garbagemule/MobArena/signs/HandlesWorldUnload.java delete mode 100644 src/main/java/com/garbagemule/MobArena/signs/LoadsSignStore.java delete mode 100644 src/main/java/com/garbagemule/MobArena/signs/RedrawsArenaSigns.java delete mode 100644 src/main/java/com/garbagemule/MobArena/signs/RemovesSignAtLocation.java delete mode 100644 src/main/java/com/garbagemule/MobArena/signs/RendersTemplateById.java delete mode 100644 src/main/java/com/garbagemule/MobArena/signs/SavesSignStore.java delete mode 100644 src/main/java/com/garbagemule/MobArena/signs/SetsLines.java create mode 100644 src/main/java/com/garbagemule/MobArena/signs/SignCreator.java create mode 100644 src/main/java/com/garbagemule/MobArena/signs/SignDataMigrator.java create mode 100644 src/main/java/com/garbagemule/MobArena/signs/SignFile.java create mode 100644 src/main/java/com/garbagemule/MobArena/signs/SignReader.java create mode 100644 src/main/java/com/garbagemule/MobArena/signs/SignRenderer.java create mode 100644 src/main/java/com/garbagemule/MobArena/signs/SignSerializer.java create mode 100644 src/main/java/com/garbagemule/MobArena/signs/SignWriter.java delete mode 100644 src/main/java/com/garbagemule/MobArena/signs/StoresNewSign.java delete mode 100644 src/test/java/com/garbagemule/MobArena/signs/ArenaSignTest.java create mode 100644 src/test/java/com/garbagemule/MobArena/signs/HandlesWorldLoadTest.java create mode 100644 src/test/java/com/garbagemule/MobArena/signs/HandlesWorldUnloadTest.java delete mode 100644 src/test/java/com/garbagemule/MobArena/signs/RedrawsArenaSignsTest.java delete mode 100644 src/test/java/com/garbagemule/MobArena/signs/RemovesSignAtLocationTest.java create mode 100644 src/test/java/com/garbagemule/MobArena/signs/SignCreatorTest.java create mode 100644 src/test/java/com/garbagemule/MobArena/signs/SignDataMigratorTest.java create mode 100644 src/test/java/com/garbagemule/MobArena/signs/SignFileTest.java create mode 100644 src/test/java/com/garbagemule/MobArena/signs/SignReaderTest.java create mode 100644 src/test/java/com/garbagemule/MobArena/signs/SignRendererTest.java create mode 100644 src/test/java/com/garbagemule/MobArena/signs/SignSerializerTest.java create mode 100644 src/test/java/com/garbagemule/MobArena/signs/SignStoreTest.java create mode 100644 src/test/java/com/garbagemule/MobArena/signs/SignWriterTest.java delete mode 100644 src/test/java/com/garbagemule/MobArena/signs/StoresNewSignTest.java diff --git a/changelog.md b/changelog.md index 4e5df22..2809d48 100644 --- a/changelog.md +++ b/changelog.md @@ -11,6 +11,8 @@ These changes will (most likely) be included in the next version. ## [Unreleased] +### Fixed +- Arena signs in unloaded or missing worlds no longer break the startup procedure. Sign data is stored in a new format that MobArena will automatically migrate to on a per-world basis during startup. ## [0.105] - 2020-11-08 ### Minor breaking changes diff --git a/src/main/java/com/garbagemule/MobArena/MobArena.java b/src/main/java/com/garbagemule/MobArena/MobArena.java index ebb1ff6..9a3373f 100644 --- a/src/main/java/com/garbagemule/MobArena/MobArena.java +++ b/src/main/java/com/garbagemule/MobArena/MobArena.java @@ -13,7 +13,6 @@ import com.garbagemule.MobArena.metrics.IsolatedChatChart; import com.garbagemule.MobArena.metrics.MonsterInfightChart; import com.garbagemule.MobArena.metrics.PvpEnabledChart; import com.garbagemule.MobArena.metrics.VaultChart; -import com.garbagemule.MobArena.signs.ArenaSign; import com.garbagemule.MobArena.signs.SignBootstrap; import com.garbagemule.MobArena.signs.SignListeners; import com.garbagemule.MobArena.things.NothingPickerParser; @@ -29,7 +28,6 @@ import org.bukkit.ChatColor; import org.bukkit.configuration.InvalidConfigurationException; import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.configuration.file.YamlConfiguration; -import org.bukkit.configuration.serialization.ConfigurationSerialization; import org.bukkit.plugin.Plugin; import org.bukkit.plugin.PluginManager; import org.bukkit.plugin.RegisteredServiceProvider; @@ -94,7 +92,6 @@ public class MobArena extends JavaPlugin arenaMaster = null; } loadsConfigFile = null; - ConfigurationSerialization.unregisterClass(ArenaSign.class); } private void setup() { @@ -103,7 +100,6 @@ public class MobArena extends JavaPlugin setupArenaMaster(); setupCommandHandler(); - registerConfigurationSerializers(); setupVault(); setupBossAbilities(); setupListeners(); @@ -133,10 +129,6 @@ public class MobArena extends JavaPlugin getCommand("ma").setExecutor(new CommandHandler(this)); } - private void registerConfigurationSerializers() { - ConfigurationSerialization.registerClass(ArenaSign.class); - } - private void setupVault() { Plugin vaultPlugin = this.getServer().getPluginManager().getPlugin("Vault"); if (vaultPlugin == null) { diff --git a/src/main/java/com/garbagemule/MobArena/signs/ArenaSign.java b/src/main/java/com/garbagemule/MobArena/signs/ArenaSign.java index 783a189..9f723e1 100644 --- a/src/main/java/com/garbagemule/MobArena/signs/ArenaSign.java +++ b/src/main/java/com/garbagemule/MobArena/signs/ArenaSign.java @@ -1,12 +1,8 @@ package com.garbagemule.MobArena.signs; import org.bukkit.Location; -import org.bukkit.configuration.serialization.ConfigurationSerializable; -import java.util.HashMap; -import java.util.Map; - -public class ArenaSign implements ConfigurationSerializable { +class ArenaSign { final Location location; final String templateId; @@ -20,28 +16,4 @@ public class ArenaSign implements ConfigurationSerializable { this.type = type; } - @Override - public Map serialize() { - Map result = new HashMap<>(); - result.put("location", location); - result.put("templateId", templateId); - result.put("arenaId", arenaId); - result.put("type", type); - return result; - } - - @SuppressWarnings("WeakerAccess") - public static ArenaSign deserialize(Map map) { - try { - Location location = (Location) map.get("location"); - String templateId = (String) map.get("templateId"); - String arenaId = (String) map.get("arenaId"); - String type = (String) map.get("type"); - return new ArenaSign(location, templateId, arenaId, type); - } catch (ClassCastException e) { - String msg = "An arena sign in " + SignStore.FILENAME + " is invalid! You may have to delete the file."; - throw new IllegalStateException(msg); - } - } - } diff --git a/src/main/java/com/garbagemule/MobArena/signs/RedrawsSignsOnUpdates.java b/src/main/java/com/garbagemule/MobArena/signs/HandlesArenaUpdates.java similarity index 64% rename from src/main/java/com/garbagemule/MobArena/signs/RedrawsSignsOnUpdates.java rename to src/main/java/com/garbagemule/MobArena/signs/HandlesArenaUpdates.java index 054de17..966b6a4 100644 --- a/src/main/java/com/garbagemule/MobArena/signs/RedrawsSignsOnUpdates.java +++ b/src/main/java/com/garbagemule/MobArena/signs/HandlesArenaUpdates.java @@ -1,24 +1,35 @@ package com.garbagemule.MobArena.signs; import com.garbagemule.MobArena.MobArena; -import com.garbagemule.MobArena.events.*; +import com.garbagemule.MobArena.events.ArenaEndEvent; +import com.garbagemule.MobArena.events.ArenaPlayerDeathEvent; +import com.garbagemule.MobArena.events.ArenaPlayerJoinEvent; +import com.garbagemule.MobArena.events.ArenaPlayerLeaveEvent; +import com.garbagemule.MobArena.events.ArenaPlayerReadyEvent; +import com.garbagemule.MobArena.events.ArenaStartEvent; +import com.garbagemule.MobArena.events.NewWaveEvent; import com.garbagemule.MobArena.framework.Arena; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; import org.bukkit.scheduler.BukkitScheduler; -class RedrawsSignsOnUpdates implements Listener { +import java.util.List; - private final RedrawsArenaSigns redrawsArenaSigns; +class HandlesArenaUpdates implements Listener { + + private final SignStore signStore; + private final SignRenderer signRenderer; private final BukkitScheduler scheduler; private final MobArena plugin; - RedrawsSignsOnUpdates( - RedrawsArenaSigns redrawsArenaSigns, + HandlesArenaUpdates( + SignStore signStore, + SignRenderer signRenderer, MobArena plugin ) { - this.redrawsArenaSigns = redrawsArenaSigns; + this.signStore = signStore; + this.signRenderer = signRenderer; this.scheduler = plugin.getServer().getScheduler(); this.plugin = plugin; } @@ -59,7 +70,10 @@ class RedrawsSignsOnUpdates implements Listener { } private void handle(Arena arena) { - scheduler.runTask(plugin, () -> redrawsArenaSigns.redraw(arena)); + scheduler.runTask(plugin, () -> { + List signs = signStore.findByArenaId(arena.configName()); + signs.forEach(signRenderer::render); + }); } } diff --git a/src/main/java/com/garbagemule/MobArena/signs/HandlesSignClicks.java b/src/main/java/com/garbagemule/MobArena/signs/HandlesSignClicks.java index 07c1d3b..375719d 100644 --- a/src/main/java/com/garbagemule/MobArena/signs/HandlesSignClicks.java +++ b/src/main/java/com/garbagemule/MobArena/signs/HandlesSignClicks.java @@ -20,7 +20,10 @@ class HandlesSignClicks implements Listener { private final InvokesSignAction invokesSignAction; private final Map cooldowns; - HandlesSignClicks(SignStore signStore, InvokesSignAction invokesSignAction) { + HandlesSignClicks( + SignStore signStore, + InvokesSignAction invokesSignAction + ) { this.signStore = signStore; this.invokesSignAction = invokesSignAction; this.cooldowns = new HashMap<>(); @@ -35,8 +38,11 @@ class HandlesSignClicks implements Listener { if (!(block.getState() instanceof Sign)) { return; } - signStore.findByLocation(block.getLocation()) - .ifPresent(sign -> purgeAndInvoke(sign, event.getPlayer())); + + ArenaSign sign = signStore.findByLocation(block.getLocation()); + if (sign != null) { + purgeAndInvoke(sign, event.getPlayer()); + } } private void purgeAndInvoke(ArenaSign sign, Player player) { diff --git a/src/main/java/com/garbagemule/MobArena/signs/HandlesSignCreation.java b/src/main/java/com/garbagemule/MobArena/signs/HandlesSignCreation.java index a878afa..d9969d5 100644 --- a/src/main/java/com/garbagemule/MobArena/signs/HandlesSignCreation.java +++ b/src/main/java/com/garbagemule/MobArena/signs/HandlesSignCreation.java @@ -1,65 +1,79 @@ package com.garbagemule.MobArena.signs; import com.garbagemule.MobArena.Messenger; -import org.bukkit.Location; -import org.bukkit.entity.Player; +import org.bukkit.ChatColor; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; import org.bukkit.event.block.SignChangeEvent; -import java.util.stream.IntStream; +import java.util.logging.Level; +import java.util.logging.Logger; class HandlesSignCreation implements Listener { - private final StoresNewSign storesNewSign; - private final RendersTemplateById rendersTemplate; + private final SignCreator creator; + private final SignWriter writer; + private final SignStore store; + private final SignRenderer renderer; private final Messenger messenger; + private final Logger log; HandlesSignCreation( - StoresNewSign storesNewSign, - RendersTemplateById rendersTemplate, - Messenger messenger + SignCreator creator, + SignWriter writer, + SignStore store, + SignRenderer renderer, + Messenger messenger, + Logger log ) { - this.storesNewSign = storesNewSign; - this.rendersTemplate = rendersTemplate; + this.creator = creator; + this.writer = writer; + this.store = store; + this.renderer = renderer; this.messenger = messenger; + this.log = log; } @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) public void on(SignChangeEvent event) { - if (!trim(event, 0).equalsIgnoreCase("[MA]")) { + ArenaSign sign; + try { + sign = creator.create(event); + if (sign == null) { + return; + } + } catch (IllegalArgumentException e) { + messenger.tell(event.getPlayer(), e.getMessage()); return; } - Location location = event.getBlock().getLocation(); - String arenaId = trim(event, 1); - String signType = trim(event, 2).toLowerCase(); - String templateId = trim(event, 3).toLowerCase(); - - if (templateId.isEmpty()) { - templateId = signType; - } - - Player player = event.getPlayer(); try { - storesNewSign.store(location, arenaId, templateId, signType); - messenger.tell(player, "New " + signType + " sign created for arena " + arenaId); - - String[] lines = rendersTemplate.render(templateId, arenaId); - IntStream.range(0, 4) - .forEach(i -> event.setLine(i, lines[i])); - } catch (IllegalArgumentException e) { - messenger.tell(player, e.getMessage()); + writer.write(sign); + } catch (Exception e) { + messenger.tell(event.getPlayer(), "Sign creation failed:\n" + ChatColor.RED + e.getMessage()); + log.log(Level.SEVERE, "Failed to write arena sign to data file", e); + return; } - } - private String trim(SignChangeEvent event, int index) { - String line = event.getLine(index); - if (line == null) { - return ""; - } - return line.trim(); + store.add(sign); + renderer.render(sign, event); + + messenger.tell(event.getPlayer(), String.format( + "New %s sign created for arena %s.", + ChatColor.YELLOW + sign.type + ChatColor.RESET, + ChatColor.GREEN + sign.arenaId + ChatColor.RESET + )); + log.info(String.format( + "%s created %s sign for '%s' at (%d,%d,%d) in '%s'.", + event.getPlayer().getName(), + sign.type, + sign.arenaId, + sign.location.getBlockX(), + sign.location.getBlockY(), + sign.location.getBlockZ(), + sign.location.getWorld().getName() + )); } } diff --git a/src/main/java/com/garbagemule/MobArena/signs/HandlesSignDestruction.java b/src/main/java/com/garbagemule/MobArena/signs/HandlesSignDestruction.java index 348869b..d9b8f3d 100644 --- a/src/main/java/com/garbagemule/MobArena/signs/HandlesSignDestruction.java +++ b/src/main/java/com/garbagemule/MobArena/signs/HandlesSignDestruction.java @@ -1,33 +1,67 @@ package com.garbagemule.MobArena.signs; import com.garbagemule.MobArena.Messenger; +import org.bukkit.ChatColor; import org.bukkit.Location; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; import org.bukkit.event.block.BlockBreakEvent; +import java.util.logging.Level; +import java.util.logging.Logger; + class HandlesSignDestruction implements Listener { - private final RemovesSignAtLocation removesSignAtLocation; + private final SignStore store; + private final SignWriter writer; private final Messenger messenger; + private final Logger log; HandlesSignDestruction( - RemovesSignAtLocation removesSignAtLocation, - Messenger messenger + SignStore store, + SignWriter writer, + Messenger messenger, + Logger log ) { - this.removesSignAtLocation = removesSignAtLocation; + this.store = store; + this.writer = writer; this.messenger = messenger; + this.log = log; } @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void on(BlockBreakEvent event) { Location location = event.getBlock().getLocation(); - removesSignAtLocation.remove(location) - .ifPresent(sign -> messenger.tell( - event.getPlayer(), - "Removed " + sign.type + " sign for arena " + sign.arenaId - )); + + ArenaSign sign = store.removeByLocation(location); + if (sign == null) { + return; + } + + try { + writer.erase(sign); + } catch (Exception e) { + messenger.tell(event.getPlayer(), "Sign destruction failed:\n" + ChatColor.RED + e.getMessage()); + log.log(Level.SEVERE, "Failed to erase arena sign from data file", e); + return; + } + + messenger.tell(event.getPlayer(), String.format( + "Removed %s sign for arena %s.", + ChatColor.YELLOW + sign.type + ChatColor.RESET, + ChatColor.GREEN + sign.arenaId + ChatColor.RESET + )); + log.info(String.format( + "%s destroyed %s sign for '%s' at (%d,%d,%d) in '%s'.", + event.getPlayer().getName(), + sign.type, + sign.arenaId, + sign.location.getBlockX(), + sign.location.getBlockY(), + sign.location.getBlockZ(), + sign.location.getWorld().getName() + )); } } diff --git a/src/main/java/com/garbagemule/MobArena/signs/HandlesWorldLoad.java b/src/main/java/com/garbagemule/MobArena/signs/HandlesWorldLoad.java new file mode 100644 index 0000000..55aef2a --- /dev/null +++ b/src/main/java/com/garbagemule/MobArena/signs/HandlesWorldLoad.java @@ -0,0 +1,57 @@ +package com.garbagemule.MobArena.signs; + +import org.bukkit.World; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.world.WorldLoadEvent; + +import java.io.IOException; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +class HandlesWorldLoad implements Listener { + + private final SignDataMigrator migrator; + private final SignReader reader; + private final SignStore store; + private final Logger log; + + HandlesWorldLoad( + SignDataMigrator migrator, + SignReader reader, + SignStore store, + Logger log + ) { + this.migrator = migrator; + this.reader = reader; + this.store = store; + this.log = log; + } + + @EventHandler(priority = EventPriority.MONITOR) + public void on(WorldLoadEvent event) { + World world = event.getWorld(); + + try { + migrator.migrate(world); + } catch (IOException e) { + log.log(Level.SEVERE, "Failed to migrate sign data for world '" + world.getName() + "'", e); + } + + List loaded; + try { + loaded = reader.read(world); + } catch (IOException e) { + log.log(Level.SEVERE, "Failed to read from arena sign data file", e); + return; + } + + if (!loaded.isEmpty()) { + loaded.forEach(store::add); + log.info(loaded.size() + " arena sign(s) loaded due to loading of world '" + world.getName() + "' (" + world.getUID().toString() + ")."); + } + } + +} diff --git a/src/main/java/com/garbagemule/MobArena/signs/HandlesWorldUnload.java b/src/main/java/com/garbagemule/MobArena/signs/HandlesWorldUnload.java new file mode 100644 index 0000000..8da1229 --- /dev/null +++ b/src/main/java/com/garbagemule/MobArena/signs/HandlesWorldUnload.java @@ -0,0 +1,34 @@ +package com.garbagemule.MobArena.signs; + +import org.bukkit.World; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.world.WorldUnloadEvent; + +import java.util.List; +import java.util.logging.Logger; + +class HandlesWorldUnload implements Listener { + + private final SignStore store; + private final Logger log; + + HandlesWorldUnload( + SignStore store, + Logger log + ) { + this.store = store; + this.log = log; + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void on(WorldUnloadEvent event) { + World world = event.getWorld(); + List removed = store.removeByWorld(world); + if (!removed.isEmpty()) { + log.info(removed.size() + " arena sign(s) unloaded due to unloading of world '" + world.getName() + "' (" + world.getUID().toString() + ")."); + } + } + +} diff --git a/src/main/java/com/garbagemule/MobArena/signs/LoadsSignStore.java b/src/main/java/com/garbagemule/MobArena/signs/LoadsSignStore.java deleted file mode 100644 index 6074094..0000000 --- a/src/main/java/com/garbagemule/MobArena/signs/LoadsSignStore.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.garbagemule.MobArena.signs; - -import com.garbagemule.MobArena.MobArena; -import org.bukkit.configuration.InvalidConfigurationException; -import org.bukkit.configuration.file.YamlConfiguration; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.util.Collections; -import java.util.List; -import java.util.stream.Collectors; - -class LoadsSignStore { - - private final MobArena plugin; - - LoadsSignStore(MobArena plugin) { - this.plugin = plugin; - } - - SignStore load() { - YamlConfiguration yaml = new YamlConfiguration(); - try { - File data = new File(plugin.getDataFolder(), "data"); - yaml.load(new File(data, SignStore.FILENAME)); - } catch (FileNotFoundException e) { - return new SignStore(Collections.emptyList()); - } catch (InvalidConfigurationException e) { - String msg = SignStore.FILENAME + " is invalid! You may have to delete it."; - throw new IllegalStateException(msg, e); - } catch (IOException e) { - throw new IllegalStateException(e); - } - - List signs = yaml.getList("signs").stream() - .filter(raw -> raw instanceof ArenaSign) - .map(raw -> (ArenaSign) raw) - .collect(Collectors.toList()); - - plugin.getLogger().info("Loaded " + signs.size() + " arena signs."); - - return new SignStore(signs); - } - -} diff --git a/src/main/java/com/garbagemule/MobArena/signs/LoadsTemplateStore.java b/src/main/java/com/garbagemule/MobArena/signs/LoadsTemplateStore.java index bb7b13d..8520a47 100644 --- a/src/main/java/com/garbagemule/MobArena/signs/LoadsTemplateStore.java +++ b/src/main/java/com/garbagemule/MobArena/signs/LoadsTemplateStore.java @@ -13,13 +13,7 @@ import java.util.Map; class LoadsTemplateStore { - private final MobArena plugin; - - LoadsTemplateStore(MobArena plugin) { - this.plugin = plugin; - } - - TemplateStore read() { + static TemplateStore load(MobArena plugin) { YamlConfiguration yaml = new YamlConfiguration(); try { File file = new File(plugin.getDataFolder(), TemplateStore.FILENAME); @@ -48,7 +42,7 @@ class LoadsTemplateStore { return new TemplateStore(map); } - private void validateTemplateNode(String key, YamlConfiguration yaml) { + private static void validateTemplateNode(String key, YamlConfiguration yaml) { List list = yaml.getList(key); if (list == null) { String msg = "Template " + key + " in " + TemplateStore.FILENAME + " is not a list!"; @@ -62,21 +56,21 @@ class LoadsTemplateStore { }); } - private String stripStateSuffix(String id) { + private static String stripStateSuffix(String id) { if (hasStateSuffix(id)) { return id.split("-", -1)[0]; } return id; } - private boolean hasStateSuffix(String id) { + private static boolean hasStateSuffix(String id) { return id.endsWith("-idle") || id.endsWith("-joining") || id.endsWith("-ready") || id.endsWith("-running"); } - private Template loadTemplate(String id, YamlConfiguration yaml) { + private static Template loadTemplate(String id, YamlConfiguration yaml) { String[] base = getLines(yaml, id); String[] idle = getLines(yaml, id + "-idle"); String[] joining = getLines(yaml, id + "-joining"); @@ -92,7 +86,7 @@ class LoadsTemplateStore { .build(); } - private String[] getLines(YamlConfiguration config, String id) { + private static String[] getLines(YamlConfiguration config, String id) { List list = config.getStringList(id); if (list.isEmpty()) { return null; diff --git a/src/main/java/com/garbagemule/MobArena/signs/RedrawsArenaSigns.java b/src/main/java/com/garbagemule/MobArena/signs/RedrawsArenaSigns.java deleted file mode 100644 index 3c6856d..0000000 --- a/src/main/java/com/garbagemule/MobArena/signs/RedrawsArenaSigns.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.garbagemule.MobArena.signs; - -import com.garbagemule.MobArena.framework.Arena; -import org.bukkit.ChatColor; - -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -class RedrawsArenaSigns { - - private final SignStore signStore; - private final TemplateStore templateStore; - private final RendersTemplate rendersTemplate; - private final SetsLines setsSignLines; - - RedrawsArenaSigns( - SignStore signStore, - TemplateStore templateStore, - RendersTemplate rendersTemplate, - SetsLines setsSignLines - ) { - this.signStore = signStore; - this.templateStore = templateStore; - this.rendersTemplate = rendersTemplate; - this.setsSignLines = setsSignLines; - } - - void redraw(Arena arena) { - List signs = signStore.findByArenaId(arena.configName()); - - Map rendered = signs.stream() - .map(sign -> sign.templateId) - .distinct() - .collect(Collectors.toMap( - templateId -> templateId, - templateId -> render(templateId, arena) - )); - - signs.forEach(sign -> setsSignLines.set( - sign.location, - rendered.get(sign.templateId) - )); - } - - private String[] render(String templateId, Arena arena) { - return templateStore.findById(templateId) - .map(template -> rendersTemplate.render(template, arena)) - .orElseGet(() -> notFound(templateId)); - } - - private static String[] notFound(String templateId) { - return new String[]{ - String.join(ChatColor.MAGIC + "b" + ChatColor.RESET, "BROKEN".split("")), - "Template", - ChatColor.BOLD + templateId, - "not found! :(" - }; - } - -} diff --git a/src/main/java/com/garbagemule/MobArena/signs/RemovesSignAtLocation.java b/src/main/java/com/garbagemule/MobArena/signs/RemovesSignAtLocation.java deleted file mode 100644 index 4bf9c92..0000000 --- a/src/main/java/com/garbagemule/MobArena/signs/RemovesSignAtLocation.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.garbagemule.MobArena.signs; - -import org.bukkit.Location; - -import java.util.Optional; - -class RemovesSignAtLocation { - - private final SignStore signStore; - private final SavesSignStore savesSignStore; - - RemovesSignAtLocation( - SignStore signStore, - SavesSignStore savesSignStore - ) { - this.signStore = signStore; - this.savesSignStore = savesSignStore; - } - - Optional remove(Location location) { - Optional sign = signStore.remove(location); - sign.ifPresent(s -> savesSignStore.save(signStore)); - return sign; - } - -} diff --git a/src/main/java/com/garbagemule/MobArena/signs/RendersTemplate.java b/src/main/java/com/garbagemule/MobArena/signs/RendersTemplate.java index 50a51f8..a03fcf7 100644 --- a/src/main/java/com/garbagemule/MobArena/signs/RendersTemplate.java +++ b/src/main/java/com/garbagemule/MobArena/signs/RendersTemplate.java @@ -1,7 +1,5 @@ package com.garbagemule.MobArena.signs; -import static java.lang.String.valueOf; - import java.util.Collection; import java.util.Collections; import java.util.List; @@ -13,6 +11,8 @@ import com.garbagemule.MobArena.framework.Arena; import org.bukkit.ChatColor; import org.bukkit.entity.Player; +import static java.lang.String.valueOf; + class RendersTemplate { // Regex Pattern for player list variables. diff --git a/src/main/java/com/garbagemule/MobArena/signs/RendersTemplateById.java b/src/main/java/com/garbagemule/MobArena/signs/RendersTemplateById.java deleted file mode 100644 index f6a405e..0000000 --- a/src/main/java/com/garbagemule/MobArena/signs/RendersTemplateById.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.garbagemule.MobArena.signs; - -import com.garbagemule.MobArena.framework.Arena; -import com.garbagemule.MobArena.framework.ArenaMaster; - -class RendersTemplateById { - - private final ArenaMaster arenaMaster; - private final TemplateStore templateStore; - private final RendersTemplate rendersTemplate; - - RendersTemplateById( - ArenaMaster arenaMaster, - TemplateStore templateStore, - RendersTemplate rendersTemplate - ) { - this.arenaMaster = arenaMaster; - this.templateStore = templateStore; - this.rendersTemplate = rendersTemplate; - } - - String[] render(String templateId, String arenaId) { - Template template = templateStore.findById(templateId) - .orElseThrow(() -> new IllegalArgumentException( - "Template " + templateId + " not found" - )); - - Arena arena = arenaMaster.getArenaWithName(arenaId); - if (arena == null) { - throw new IllegalStateException("Arena " + arenaId + " not found"); - } - - return rendersTemplate.render(template, arena); - } - -} diff --git a/src/main/java/com/garbagemule/MobArena/signs/SavesSignStore.java b/src/main/java/com/garbagemule/MobArena/signs/SavesSignStore.java deleted file mode 100644 index 5aa2a0a..0000000 --- a/src/main/java/com/garbagemule/MobArena/signs/SavesSignStore.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.garbagemule.MobArena.signs; - -import com.garbagemule.MobArena.MobArena; -import org.bukkit.configuration.file.YamlConfiguration; - -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -class SavesSignStore { - - private final MobArena plugin; - - SavesSignStore(MobArena plugin) { - this.plugin = plugin; - } - - void save(SignStore signStore) { - YamlConfiguration yaml = new YamlConfiguration(); - List values = new ArrayList<>(signStore.findAll()); - yaml.set("signs", values); - try { - File data = new File(plugin.getDataFolder(), "data"); - yaml.options().header("MobArena Sign Store\n\nPlease DON'T edit this file by hand!\n"); - yaml.save(new File(data, SignStore.FILENAME)); - } catch (IOException e) { - throw new IllegalStateException(e); - } - } - -} diff --git a/src/main/java/com/garbagemule/MobArena/signs/SetsLines.java b/src/main/java/com/garbagemule/MobArena/signs/SetsLines.java deleted file mode 100644 index 24a8784..0000000 --- a/src/main/java/com/garbagemule/MobArena/signs/SetsLines.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.garbagemule.MobArena.signs; - -import org.bukkit.Location; -import org.bukkit.block.BlockState; -import org.bukkit.block.Sign; - -class SetsLines { - - void set(Location location, String[] lines) { - BlockState state = location.getBlock().getState(); - if (!(state instanceof Sign)) { - return; - } - - Sign sign = (Sign) state; - for (int i = 0; i < lines.length; i++) { - sign.setLine(i, lines[i]); - } - sign.update(); - } - -} diff --git a/src/main/java/com/garbagemule/MobArena/signs/SignBootstrap.java b/src/main/java/com/garbagemule/MobArena/signs/SignBootstrap.java index e05abb0..96fc0f2 100644 --- a/src/main/java/com/garbagemule/MobArena/signs/SignBootstrap.java +++ b/src/main/java/com/garbagemule/MobArena/signs/SignBootstrap.java @@ -1,42 +1,54 @@ package com.garbagemule.MobArena.signs; import com.garbagemule.MobArena.MobArena; +import org.bukkit.World; +import org.yaml.snakeyaml.Yaml; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; -@SuppressWarnings("WeakerAccess") public class SignBootstrap { private final MobArena plugin; - private final SignStore signStore; - private final TemplateStore templateStore; private InvokesSignAction invokesSignAction; - private RedrawsArenaSigns redrawsArenaSigns; - private RemovesSignAtLocation removesSignAtLocation; private RendersTemplate rendersTemplate; - private RendersTemplateById rendersTemplateById; - private SetsLines setsLines; - private StoresNewSign storesNewSign; - private SavesSignStore savesSignStore; + private SignCreator signCreator; + private SignDataMigrator signDataMigrator; + private SignFile signFile; + private SignReader signReader; + private SignRenderer signRenderer; + private SignSerializer signSerializer; + private SignStore signStore; + private SignWriter signWriter; + private TemplateStore templateStore; - private SignBootstrap( - MobArena plugin, - SignStore signStore, - TemplateStore templateStore - ) { + private SignBootstrap(MobArena plugin) { this.plugin = plugin; - this.signStore = signStore; - this.templateStore = templateStore; } MobArena getPlugin() { return plugin; } - SignStore getSignStore() { - return signStore; + SignFile getSignFile() { + if (signFile == null) { + Path root = plugin.getDataFolder().toPath(); + Path data = root.resolve("data"); + Path file = data.resolve("signs.csv"); + signFile = new SignFile(file); + } + return signFile; } TemplateStore getTemplateStore() { + if (templateStore == null) { + templateStore = LoadsTemplateStore.load(plugin); + } return templateStore; } @@ -50,28 +62,6 @@ public class SignBootstrap { return invokesSignAction; } - RedrawsArenaSigns getRedrawsArenaSigns() { - if (redrawsArenaSigns == null) { - redrawsArenaSigns = new RedrawsArenaSigns( - getSignStore(), - getTemplateStore(), - getRendersTemplate(), - getSetsLines() - ); - } - return redrawsArenaSigns; - } - - RemovesSignAtLocation getRemovesSignAtLocation() { - if (removesSignAtLocation == null) { - removesSignAtLocation = new RemovesSignAtLocation( - getSignStore(), - getSavesSignStore() - ); - } - return removesSignAtLocation; - } - RendersTemplate getRendersTemplate() { if (rendersTemplate == null) { rendersTemplate = new RendersTemplate(); @@ -79,52 +69,140 @@ public class SignBootstrap { return rendersTemplate; } - RendersTemplateById getRendersTemplateById() { - if (rendersTemplateById == null) { - rendersTemplateById = new RendersTemplateById( + SignCreator getSignCreator() { + if (signCreator == null) { + signCreator = new SignCreator( plugin.getArenaMaster(), + getTemplateStore() + ); + } + return signCreator; + } + + SignDataMigrator getSignDataMigrator() { + if (signDataMigrator == null) { + Path root = plugin.getDataFolder().toPath(); + Path data = root.resolve("data"); + Path legacyFile = data.resolve("signs.data"); + Path pendingFile = data.resolve("signs.tmp"); + signDataMigrator = new SignDataMigrator( + legacyFile, + pendingFile, + new Yaml(), + getSignFile(), + plugin.getLogger() + ); + } + return signDataMigrator; + } + + SignReader getSignReader() { + if (signReader == null) { + signReader = new SignReader( + getSignFile(), + getSignSerializer(), + plugin.getLogger() + ); + } + return signReader; + } + + SignRenderer getSignRenderer() { + if (signRenderer == null) { + signRenderer = new SignRenderer( getTemplateStore(), + plugin.getArenaMaster(), getRendersTemplate() ); } - return rendersTemplateById; + return signRenderer; } - SetsLines getSetsLines() { - if (setsLines == null) { - setsLines = new SetsLines(); + SignSerializer getSignSerializer() { + if (signSerializer == null) { + signSerializer = new SignSerializer(); } - return setsLines; + return signSerializer; } - StoresNewSign getStoresNewSign() { - if (storesNewSign == null) { - storesNewSign = new StoresNewSign( - plugin.getArenaMaster(), - getTemplateStore(), - getSignStore(), - getSavesSignStore() + SignStore getSignStore() { + if (signStore == null) { + signStore = new SignStore(); + } + return signStore; + } + + SignWriter getSignWriter() { + if (signWriter == null) { + signWriter = new SignWriter( + getSignFile(), + getSignSerializer(), + plugin.getLogger() ); } - return storesNewSign; - } - - SavesSignStore getSavesSignStore() { - if (savesSignStore == null) { - savesSignStore = new SavesSignStore(plugin); - } - return savesSignStore; + return signWriter; } public static SignBootstrap create(MobArena plugin) { - TemplateStore templateStore = new LoadsTemplateStore(plugin).read(); - SignStore signStore = new LoadsSignStore(plugin).load(); + SignBootstrap bootstrap = new SignBootstrap(plugin); - SignBootstrap bootstrap = new SignBootstrap(plugin, signStore, templateStore); - - plugin.getArenaMaster().getArenas() - .forEach(bootstrap.getRedrawsArenaSigns()::redraw); + migrateData(bootstrap); + loadSigns(bootstrap); + initialRender(bootstrap); return bootstrap; } + + private static void migrateData(SignBootstrap bootstrap) { + SignDataMigrator migrator = bootstrap.getSignDataMigrator(); + MobArena plugin = bootstrap.getPlugin(); + Logger log = plugin.getLogger(); + + try { + migrator.init(); + } catch (IOException e) { + log.log(Level.SEVERE, "Failed initial sign data migration step", e); + return; + } + + for (World world : plugin.getServer().getWorlds()) { + try { + migrator.migrate(world); + } catch (IOException e) { + log.log(Level.SEVERE, "Failed to migrate sign data for world '" + world.getName() + "'", e); + } + } + } + + private static void loadSigns(SignBootstrap bootstrap) { + SignReader reader = bootstrap.getSignReader(); + SignStore store = bootstrap.getSignStore(); + MobArena plugin = bootstrap.getPlugin(); + Logger log = plugin.getLogger(); + + List loaded = new ArrayList<>(); + try { + for (World world : plugin.getServer().getWorlds()) { + loaded.addAll(reader.read(world)); + } + } catch (IOException e) { + log.log(Level.SEVERE, "Failed to read from arena sign data file", e); + } + + if (!loaded.isEmpty()) { + loaded.forEach(store::add); + log.info(loaded.size() + " arena sign(s) loaded."); + } + } + + private static void initialRender(SignBootstrap bootstrap) { + SignStore store = bootstrap.getSignStore(); + SignRenderer renderer = bootstrap.getSignRenderer(); + + bootstrap.getPlugin().getArenaMaster().getArenas().forEach(arena -> { + List signs = store.findByArenaId(arena.configName()); + signs.forEach(renderer::render); + }); + } + } diff --git a/src/main/java/com/garbagemule/MobArena/signs/SignCreator.java b/src/main/java/com/garbagemule/MobArena/signs/SignCreator.java new file mode 100644 index 0000000..6977f2c --- /dev/null +++ b/src/main/java/com/garbagemule/MobArena/signs/SignCreator.java @@ -0,0 +1,85 @@ +package com.garbagemule.MobArena.signs; + +import com.garbagemule.MobArena.framework.Arena; +import com.garbagemule.MobArena.framework.ArenaMaster; +import org.bukkit.Location; +import org.bukkit.event.block.SignChangeEvent; + +class SignCreator { + + private final ArenaMaster arenaMaster; + private final TemplateStore templateStore; + + SignCreator( + ArenaMaster arenaMaster, + TemplateStore templateStore + ) { + this.arenaMaster = arenaMaster; + this.templateStore = templateStore; + } + + ArenaSign create(SignChangeEvent event) { + if (!trim(event, 0).equalsIgnoreCase("[MA]")) { + return null; + } + + Location location = event.getBlock().getLocation(); + String arenaId = getArenaId(event); + String signType = getSignType(event); + String templateId = getTemplateId(event, signType); + + return new ArenaSign(location, templateId, arenaId, signType); + } + + private String getArenaId(SignChangeEvent event) { + String arenaId = trim(event, 1); + if (arenaId.isEmpty()) { + throw new IllegalArgumentException("Missing arena name on line 2"); + } + + Arena arena = arenaMaster.getArenaWithName(arenaId); + if (arena == null) { + throw new IllegalArgumentException("Arena " + arenaId + " not found"); + } + + return arena.getSlug(); + } + + private String getSignType(SignChangeEvent event) { + String signType = trim(event, 2).toLowerCase(); + if (signType.isEmpty()) { + throw new IllegalArgumentException("Missing sign type on line 3"); + } + + switch (signType) { + case "info": + case "join": + case "leave": { + return signType; + } + default: { + throw new IllegalArgumentException("Invalid sign type: " + signType); + } + } + } + + private String getTemplateId(SignChangeEvent event, String signType) { + String line = trim(event, 3); + String templateId = !line.isEmpty() ? line : signType; + + templateStore + .findById(templateId) + .orElseThrow(() -> new IllegalArgumentException("Template " + templateId + " not found")); + + return templateId; + } + + private String trim(SignChangeEvent event, int index) { + String line = event.getLine(index); + if (line == null) { + return ""; + } + return line.trim(); + } + +} diff --git a/src/main/java/com/garbagemule/MobArena/signs/SignDataMigrator.java b/src/main/java/com/garbagemule/MobArena/signs/SignDataMigrator.java new file mode 100644 index 0000000..ef29ecf --- /dev/null +++ b/src/main/java/com/garbagemule/MobArena/signs/SignDataMigrator.java @@ -0,0 +1,136 @@ +package com.garbagemule.MobArena.signs; + +import com.garbagemule.MobArena.util.Slugs; +import org.bukkit.World; +import org.yaml.snakeyaml.Yaml; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.OpenOption; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.logging.Logger; + +class SignDataMigrator { + + private static final OpenOption[] WRITE_OPTIONS = { + StandardOpenOption.CREATE, + StandardOpenOption.WRITE, + StandardOpenOption.TRUNCATE_EXISTING, + StandardOpenOption.SYNC + }; + + private final Path legacyFile; + private final Path pendingFile; + private final Yaml yaml; + private final SignFile signFile; + private final Logger log; + + SignDataMigrator( + Path legacyFile, + Path pendingFile, + Yaml yaml, + SignFile signFile, + Logger log + ) { + this.legacyFile = legacyFile; + this.pendingFile = pendingFile; + this.yaml = yaml; + this.signFile = signFile; + this.log = log; + } + + void init() throws IOException { + if (!Files.exists(legacyFile)) { + return; + } + + log.info("Legacy sign data found, migrating..."); + List> signs = loadSignsInLegacyFile(); + List lines = new ArrayList<>(signs.size()); + for (Map sign : signs) { + String line = convert(sign); + lines.add(line); + } + Files.write(pendingFile, lines, WRITE_OPTIONS); + + Files.delete(legacyFile); + log.info("Legacy sign data migrated to temporary format."); + } + + @SuppressWarnings("unchecked") + private List> loadSignsInLegacyFile() throws IOException { + byte[] bytes = Files.readAllBytes(legacyFile); + Map map = yaml.load(new String(bytes)); + if (map != null && map.containsKey("signs")) { + List> signs = (List>) map.get("signs"); + if (signs != null) { + return signs; + } + } + return Collections.emptyList(); + } + + @SuppressWarnings("unchecked") + private String convert(Map sign) { + Map location = (Map) sign.get("location"); + String world = (String) location.get("world"); + String x = String.valueOf(((Number) location.get("x")).intValue()); + String y = String.valueOf(((Number) location.get("y")).intValue()); + String z = String.valueOf(((Number) location.get("z")).intValue()); + String arenaId = Slugs.create((String) sign.get("arenaId")); + String type = (String) sign.get("type"); + String templateId = (String) sign.get("templateId"); + return String.join(";", world, x, y, z, arenaId, type, templateId); + } + + void migrate(World world) throws IOException { + if (!Files.exists(pendingFile)) { + return; + } + + List lines = Files.readAllLines(pendingFile); + + // Partition the lines into two lists; the ones to take from + // the file for migration and the ones to skip. + String name = world.getName(); + String namePrefix = name + ";"; + List take = new ArrayList<>(lines.size()); + List skip = new ArrayList<>(lines.size()); + for (String line : lines) { + if (line.startsWith(namePrefix)) { + take.add(line); + } else { + skip.add(line); + } + } + if (take.isEmpty()) { + return; + } + + // Migrate all matching lines to the new sign data file. + log.info("Found temporary sign data for world '" + name + "', migrating..."); + String id = world.getUID().toString(); + String idPrefix = id + ";"; + for (String line : take) { + signFile.append(idPrefix + line); + } + signFile.save(); + log.info("Temporary data for " + take.size() + " sign(s) in world '" + name + "' migrated."); + + // Write the remaining lines back down into the pending file + // if there are any left. Otherwise, migration has completed + // and we are done. + if (!skip.isEmpty()) { + Files.write(pendingFile, skip, WRITE_OPTIONS); + } else { + Files.delete(pendingFile); + log.info("Sign data migration complete."); + } + } + +} diff --git a/src/main/java/com/garbagemule/MobArena/signs/SignFile.java b/src/main/java/com/garbagemule/MobArena/signs/SignFile.java new file mode 100644 index 0000000..945c16e --- /dev/null +++ b/src/main/java/com/garbagemule/MobArena/signs/SignFile.java @@ -0,0 +1,67 @@ +package com.garbagemule.MobArena.signs; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.OpenOption; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.List; + +class SignFile { + + private static final OpenOption[] WRITE_OPTIONS = { + StandardOpenOption.WRITE, + StandardOpenOption.TRUNCATE_EXISTING, + StandardOpenOption.SYNC + }; + + private final Path file; + private final List lines; + + private boolean stale; + + SignFile(Path file) { + this.file = file; + this.lines = new ArrayList<>(); + this.stale = true; + } + + List lines() throws IOException { + if (stale) { + load(); + } + return new ArrayList<>(lines); + } + + void append(String line) { + lines.add(line); + } + + void erase(String line) { + lines.remove(line); + } + + void load() throws IOException { + if (Files.notExists(file)) { + return; + } + + lines.clear(); + lines.addAll(Files.readAllLines(file)); + + stale = false; + } + + void save() throws IOException { + if (Files.notExists(file)) { + Files.createDirectories(file.getParent()); + Files.createFile(file); + } + + Files.write(file, lines, WRITE_OPTIONS); + + stale = true; + } + +} diff --git a/src/main/java/com/garbagemule/MobArena/signs/SignListeners.java b/src/main/java/com/garbagemule/MobArena/signs/SignListeners.java index b1276db..b9ba647 100644 --- a/src/main/java/com/garbagemule/MobArena/signs/SignListeners.java +++ b/src/main/java/com/garbagemule/MobArena/signs/SignListeners.java @@ -16,14 +16,32 @@ public class SignListeners { } public void register(SignBootstrap bootstrap) { + listeners.add(load(bootstrap)); + listeners.add(unload(bootstrap)); listeners.add(clicks(bootstrap)); listeners.add(creation(bootstrap)); listeners.add(destruction(bootstrap)); - listeners.add(redraw(bootstrap)); + listeners.add(updates(bootstrap)); listeners.forEach(listener -> register(listener, bootstrap)); } + private HandlesWorldLoad load(SignBootstrap bootstrap) { + return new HandlesWorldLoad( + bootstrap.getSignDataMigrator(), + bootstrap.getSignReader(), + bootstrap.getSignStore(), + bootstrap.getPlugin().getLogger() + ); + } + + private HandlesWorldUnload unload(SignBootstrap bootstrap) { + return new HandlesWorldUnload( + bootstrap.getSignStore(), + bootstrap.getPlugin().getLogger() + ); + } + private HandlesSignClicks clicks(SignBootstrap bootstrap) { return new HandlesSignClicks( bootstrap.getSignStore(), @@ -33,22 +51,28 @@ public class SignListeners { private HandlesSignCreation creation(SignBootstrap bootstrap) { return new HandlesSignCreation( - bootstrap.getStoresNewSign(), - bootstrap.getRendersTemplateById(), - bootstrap.getPlugin().getGlobalMessenger() + bootstrap.getSignCreator(), + bootstrap.getSignWriter(), + bootstrap.getSignStore(), + bootstrap.getSignRenderer(), + bootstrap.getPlugin().getGlobalMessenger(), + bootstrap.getPlugin().getLogger() ); } private HandlesSignDestruction destruction(SignBootstrap bootstrap) { return new HandlesSignDestruction( - bootstrap.getRemovesSignAtLocation(), - bootstrap.getPlugin().getGlobalMessenger() + bootstrap.getSignStore(), + bootstrap.getSignWriter(), + bootstrap.getPlugin().getGlobalMessenger(), + bootstrap.getPlugin().getLogger() ); } - private RedrawsSignsOnUpdates redraw(SignBootstrap bootstrap) { - return new RedrawsSignsOnUpdates( - bootstrap.getRedrawsArenaSigns(), + private HandlesArenaUpdates updates(SignBootstrap bootstrap) { + return new HandlesArenaUpdates( + bootstrap.getSignStore(), + bootstrap.getSignRenderer(), bootstrap.getPlugin() ); } diff --git a/src/main/java/com/garbagemule/MobArena/signs/SignReader.java b/src/main/java/com/garbagemule/MobArena/signs/SignReader.java new file mode 100644 index 0000000..e0eddaf --- /dev/null +++ b/src/main/java/com/garbagemule/MobArena/signs/SignReader.java @@ -0,0 +1,44 @@ +package com.garbagemule.MobArena.signs; + +import org.bukkit.World; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +class SignReader { + + private final SignFile file; + private final SignSerializer serializer; + private final Logger log; + + SignReader( + SignFile file, + SignSerializer serializer, + Logger log + ) { + this.file = file; + this.serializer = serializer; + this.log = log; + } + + List read(World world) throws IOException { + List result = new ArrayList<>(); + String id = world.getUID().toString(); + String prefix = id + ";"; + for (String line : file.lines()) { + if (line.startsWith(prefix)) { + try { + ArenaSign sign = serializer.deserialize(line, world); + result.add(sign); + } catch (Exception e) { + log.log(Level.SEVERE, "Failed to deserialize arena sign from line:\n" + line, e); + } + } + } + return result; + } + +} diff --git a/src/main/java/com/garbagemule/MobArena/signs/SignRenderer.java b/src/main/java/com/garbagemule/MobArena/signs/SignRenderer.java new file mode 100644 index 0000000..c9739bc --- /dev/null +++ b/src/main/java/com/garbagemule/MobArena/signs/SignRenderer.java @@ -0,0 +1,80 @@ +package com.garbagemule.MobArena.signs; + +import com.garbagemule.MobArena.framework.Arena; +import com.garbagemule.MobArena.framework.ArenaMaster; +import org.bukkit.ChatColor; +import org.bukkit.block.BlockState; +import org.bukkit.block.Sign; +import org.bukkit.event.block.SignChangeEvent; + +class SignRenderer { + + private final TemplateStore templateStore; + private final ArenaMaster arenaMaster; + private final RendersTemplate rendersTemplate; + + SignRenderer( + TemplateStore templateStore, + ArenaMaster arenaMaster, + RendersTemplate rendersTemplate + ) { + this.templateStore = templateStore; + this.arenaMaster = arenaMaster; + this.rendersTemplate = rendersTemplate; + } + + void render(ArenaSign sign) { + BlockState state = sign.location.getBlock().getState(); + if (state instanceof Sign) { + Sign target = (Sign) state; + String[] lines = renderLines(sign); + for (int i = 0; i < lines.length; i++) { + target.setLine(i, lines[i]); + } + target.update(); + } + } + + void render(ArenaSign sign, SignChangeEvent event) { + String[] lines = renderLines(sign); + for (int i = 0; i < lines.length; i++) { + event.setLine(i, lines[i]); + } + } + + private String[] renderLines(ArenaSign sign) { + String templateId = sign.templateId; + String arenaId = sign.arenaId; + + Template template = templateStore.findById(templateId).orElse(null); + if (template == null) { + return templateNotFound(templateId); + } + + Arena arena = arenaMaster.getArenaWithName(arenaId); + if (arena == null) { + return arenaNotFound(arenaId); + } + + return rendersTemplate.render(template, arena); + } + + private String[] templateNotFound(String templateId) { + return new String[]{ + ChatColor.RED + "[ERROR]", + "Template", + ChatColor.YELLOW + templateId, + "not found :(" + }; + } + + private String[] arenaNotFound(String arenaId) { + return new String[]{ + ChatColor.RED + "[ERROR]", + "Arena", + ChatColor.YELLOW + arenaId, + "not found :(" + }; + } + +} diff --git a/src/main/java/com/garbagemule/MobArena/signs/SignSerializer.java b/src/main/java/com/garbagemule/MobArena/signs/SignSerializer.java new file mode 100644 index 0000000..0d7eccc --- /dev/null +++ b/src/main/java/com/garbagemule/MobArena/signs/SignSerializer.java @@ -0,0 +1,61 @@ +package com.garbagemule.MobArena.signs; + +import org.bukkit.Location; +import org.bukkit.World; + +class SignSerializer { + + String serialize(ArenaSign sign) { + String id = sign.location.getWorld().getUID().toString(); + String name = sign.location.getWorld().getName(); + + String x = String.valueOf(sign.location.getBlockX()); + String y = String.valueOf(sign.location.getBlockY()); + String z = String.valueOf(sign.location.getBlockZ()); + + String arenaId = sign.arenaId; + String type = sign.type; + String templateId = sign.templateId; + + return String.join(";", id, name, x, y, z, arenaId, type, templateId); + } + + ArenaSign deserialize(String input, World world) { + String[] parts = input.split(";"); + if (parts.length != 8) { + throw new IllegalArgumentException("Invalid input; expected 8 parts, got " + parts.length); + } + + String id = parts[0]; + if (!id.equals(world.getUID().toString())) { + throw new IllegalArgumentException("World mismatch"); + } + + int x = Integer.parseInt(parts[2]); + int y = Integer.parseInt(parts[3]); + int z = Integer.parseInt(parts[4]); + + Location location = new Location(world, x, y, z); + String arenaId = parts[5]; + String type = parts[6]; + String templateId = parts[7]; + + return new ArenaSign(location, templateId, arenaId, type); + } + + boolean equal(String line1, String line2) { + if (line1.equals(line2)) { + return true; + } + + String[] parts1 = line1.split(";"); + String[] parts2 = line2.split(";"); + + // World ID and (x,y,z) are all that matter + return parts1[0].equals(parts2[0]) + && parts1[2].equals(parts2[2]) + && parts1[3].equals(parts2[3]) + && parts1[4].equals(parts2[4]); + } + +} diff --git a/src/main/java/com/garbagemule/MobArena/signs/SignStore.java b/src/main/java/com/garbagemule/MobArena/signs/SignStore.java index e244783..2b78064 100644 --- a/src/main/java/com/garbagemule/MobArena/signs/SignStore.java +++ b/src/main/java/com/garbagemule/MobArena/signs/SignStore.java @@ -1,43 +1,21 @@ package com.garbagemule.MobArena.signs; import org.bukkit.Location; +import org.bukkit.World; -import java.util.Collection; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.stream.Collectors; class SignStore { - static final String FILENAME = "signs.data"; + private final Map signs = new HashMap<>(); - private final Map signs; - - SignStore(List signs) { - this.signs = signs.stream() - .collect(Collectors.toMap( - sign -> sign.location, - sign -> sign - )); - } - - void store(ArenaSign sign) { - signs.put(sign.location, sign); - } - - Optional remove(Location location) { - ArenaSign sign = signs.remove(location); - return Optional.ofNullable(sign); - } - - Collection findAll() { - return signs.values(); - } - - Optional findByLocation(Location location) { - ArenaSign sign = signs.get(location); - return Optional.ofNullable(sign); + ArenaSign findByLocation(Location location) { + return signs.get(location); } List findByArenaId(String arenaId) { @@ -46,4 +24,25 @@ class SignStore { .collect(Collectors.toList()); } + void add(ArenaSign sign) { + signs.put(sign.location, sign); + } + + ArenaSign removeByLocation(Location location) { + return signs.remove(location); + } + + List removeByWorld(World world) { + List removed = new ArrayList<>(); + Iterator iterator = signs.values().iterator(); + while (iterator.hasNext()) { + ArenaSign sign = iterator.next(); + if (sign.location.getWorld().equals(world)) { + removed.add(sign); + iterator.remove(); + } + } + return removed; + } + } diff --git a/src/main/java/com/garbagemule/MobArena/signs/SignWriter.java b/src/main/java/com/garbagemule/MobArena/signs/SignWriter.java new file mode 100644 index 0000000..33384d9 --- /dev/null +++ b/src/main/java/com/garbagemule/MobArena/signs/SignWriter.java @@ -0,0 +1,50 @@ +package com.garbagemule.MobArena.signs; + +import java.io.IOException; +import java.util.logging.Logger; + +class SignWriter { + + private final SignFile file; + private final SignSerializer serializer; + private final Logger log; + + SignWriter( + SignFile file, + SignSerializer serializer, + Logger log + ) { + this.file = file; + this.serializer = serializer; + this.log = log; + } + + void write(ArenaSign sign) throws IOException { + String line = serializer.serialize(sign); + + for (String candidate : file.lines()) { + if (serializer.equal(candidate, line)) { + log.warning("Erasing conflicting sign entry:\n" + candidate); + file.erase(candidate); + } + } + + file.append(line); + file.save(); + } + + void erase(ArenaSign sign) throws IOException { + String line = serializer.serialize(sign); + + for (String candidate : file.lines()) { + if (serializer.equal(candidate, line)) { + file.erase(candidate); + file.save(); + return; + } + } + + log.warning("No match found in sign data file for sign:\n" + line); + } + +} diff --git a/src/main/java/com/garbagemule/MobArena/signs/StoresNewSign.java b/src/main/java/com/garbagemule/MobArena/signs/StoresNewSign.java deleted file mode 100644 index ebf9443..0000000 --- a/src/main/java/com/garbagemule/MobArena/signs/StoresNewSign.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.garbagemule.MobArena.signs; - -import com.garbagemule.MobArena.framework.Arena; -import com.garbagemule.MobArena.framework.ArenaMaster; -import org.bukkit.Location; - -class StoresNewSign { - - private final ArenaMaster arenaMaster; - private final TemplateStore templateStore; - private final SignStore signStore; - private final SavesSignStore savesSignStore; - - StoresNewSign( - ArenaMaster arenaMaster, - TemplateStore templateStore, - SignStore signStore, - SavesSignStore savesSignStore - ) { - this.arenaMaster = arenaMaster; - this.templateStore = templateStore; - this.signStore = signStore; - this.savesSignStore = savesSignStore; - } - - void store( - Location location, - String arenaId, - String templateId, - String signType - ) { - Arena arena = arenaMaster.getArenaWithName(arenaId); - if (arena == null) { - throw new IllegalArgumentException("Arena " + arenaId + " not found"); - } - - templateStore.findById(templateId) - .orElseThrow(() -> new IllegalArgumentException( - "Template " + templateId + " not found" - )); - - switch (signType) { - case "info": - case "join": - case "leave": - break; - default: - throw new IllegalArgumentException("Invalid sign type: " + signType); - } - - signStore.store(new ArenaSign(location, templateId, arenaId, signType)); - - savesSignStore.save(signStore); - } - -} diff --git a/src/main/java/com/garbagemule/MobArena/signs/Template.java b/src/main/java/com/garbagemule/MobArena/signs/Template.java index f540c46..f99f960 100644 --- a/src/main/java/com/garbagemule/MobArena/signs/Template.java +++ b/src/main/java/com/garbagemule/MobArena/signs/Template.java @@ -16,7 +16,7 @@ class Template { static class Builder { - private String id; + private final String id; private String[] base; private String[] idle; private String[] joining; diff --git a/src/test/java/com/garbagemule/MobArena/signs/ArenaSignTest.java b/src/test/java/com/garbagemule/MobArena/signs/ArenaSignTest.java deleted file mode 100644 index cf8aa88..0000000 --- a/src/test/java/com/garbagemule/MobArena/signs/ArenaSignTest.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.garbagemule.MobArena.signs; - -import static org.hamcrest.MatcherAssert.*; -import static org.hamcrest.Matchers.*; -import static org.mockito.Mockito.*; - -import org.bukkit.Location; -import org.bukkit.World; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.junit.MockitoJUnitRunner; - -import java.util.HashMap; -import java.util.Map; - -@RunWith(MockitoJUnitRunner.StrictStubs.class) -public class ArenaSignTest { - - @Test - public void serialize() { - World world = mock(World.class); - Location location = new Location(world, 1, 2, 3); - String templateId = "a good template"; - String arenaId = "cool arena"; - String type = "join"; - ArenaSign sign = new ArenaSign(location, templateId, arenaId, type); - - Map result = sign.serialize(); - - assertThat(result.get("location"), equalTo(location)); - assertThat(result.get("templateId"), equalTo(templateId)); - assertThat(result.get("arenaId"), equalTo(arenaId)); - assertThat(result.get("type"), equalTo(type)); - } - - @Test - public void deserialize() { - World world = mock(World.class); - Location location = new Location(world, 1, 2, 3); - String templateId = "a good template"; - String arenaId = "cool arena"; - String type = "join"; - Map map = new HashMap<>(); - map.put("location", location); - map.put("templateId", templateId); - map.put("arenaId", arenaId); - map.put("type", type); - - ArenaSign result = ArenaSign.deserialize(map); - - assertThat(result.location, is(equalTo(location))); - assertThat(result.templateId, is(equalTo(templateId))); - assertThat(result.arenaId, is(equalTo(arenaId))); - assertThat(result.type, is(equalTo(type))); - } - -} diff --git a/src/test/java/com/garbagemule/MobArena/signs/HandlesSignClicksTest.java b/src/test/java/com/garbagemule/MobArena/signs/HandlesSignClicksTest.java index a278f1a..9d97a7c 100644 --- a/src/test/java/com/garbagemule/MobArena/signs/HandlesSignClicksTest.java +++ b/src/test/java/com/garbagemule/MobArena/signs/HandlesSignClicksTest.java @@ -1,7 +1,5 @@ package com.garbagemule.MobArena.signs; -import static org.mockito.Mockito.*; - import org.bukkit.Location; import org.bukkit.block.Block; import org.bukkit.block.Chest; @@ -13,7 +11,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.junit.MockitoJUnitRunner; -import java.util.Optional; +import static org.mockito.Mockito.*; @SuppressWarnings("WeakerAccess") @RunWith(MockitoJUnitRunner.StrictStubs.class) @@ -27,8 +25,6 @@ public class HandlesSignClicksTest { @Before public void setup() { signStore = mock(SignStore.class); - when(signStore.findByLocation(any())) - .thenReturn(Optional.empty()); invokesSignAction = mock(InvokesSignAction.class); subject = new HandlesSignClicks(signStore, invokesSignAction); @@ -40,7 +36,7 @@ public class HandlesSignClicksTest { subject.on(event); - verifyZeroInteractions(signStore); + verifyNoInteractions(signStore, invokesSignAction); } @Test @@ -51,18 +47,19 @@ public class HandlesSignClicksTest { subject.on(event); - verifyZeroInteractions(signStore); + verifyNoInteractions(signStore, invokesSignAction); } @Test public void nonArenaSignNoFun() { Block block = mock(Block.class); when(block.getState()).thenReturn(mock(Sign.class)); + when(signStore.findByLocation(any())).thenReturn(null); PlayerInteractEvent event = event(null, block); subject.on(event); - verifyZeroInteractions(signStore); + verifyNoInteractions(invokesSignAction); } @Test @@ -72,8 +69,7 @@ public class HandlesSignClicksTest { when(block.getLocation()).thenReturn(location); when(block.getState()).thenReturn(mock(Sign.class)); ArenaSign sign = new ArenaSign(location, "", "", ""); - when(signStore.findByLocation(location)) - .thenReturn(Optional.of(sign)); + when(signStore.findByLocation(location)).thenReturn(sign); Player player = mock(Player.class); PlayerInteractEvent event = event(player, block); diff --git a/src/test/java/com/garbagemule/MobArena/signs/HandlesSignCreationTest.java b/src/test/java/com/garbagemule/MobArena/signs/HandlesSignCreationTest.java index 39d2051..cae4c12 100644 --- a/src/test/java/com/garbagemule/MobArena/signs/HandlesSignCreationTest.java +++ b/src/test/java/com/garbagemule/MobArena/signs/HandlesSignCreationTest.java @@ -1,10 +1,8 @@ package com.garbagemule.MobArena.signs; -import static org.mockito.Mockito.*; - import com.garbagemule.MobArena.Messenger; import org.bukkit.Location; -import org.bukkit.block.Block; +import org.bukkit.World; import org.bukkit.entity.Player; import org.bukkit.event.block.SignChangeEvent; import org.junit.Before; @@ -12,135 +10,139 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.junit.MockitoJUnitRunner; -@SuppressWarnings("WeakerAccess") +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + @RunWith(MockitoJUnitRunner.StrictStubs.class) public class HandlesSignCreationTest { - StoresNewSign storesNewSign; - RendersTemplateById rendersTemplate; + SignCreator creator; + SignWriter writer; + SignStore store; + SignRenderer renderer; Messenger messenger; + Logger log; HandlesSignCreation subject; @Before public void setup() { - storesNewSign = mock(StoresNewSign.class); - - rendersTemplate = mock(RendersTemplateById.class); - when(rendersTemplate.render(any(), any())) - .thenReturn(new String[]{"", "", "", ""}); - + creator = mock(SignCreator.class); + writer = mock(SignWriter.class); + store = mock(SignStore.class); + renderer = mock(SignRenderer.class); messenger = mock(Messenger.class); + log = mock(Logger.class); subject = new HandlesSignCreation( - storesNewSign, - rendersTemplate, - messenger + creator, + writer, + store, + renderer, + messenger, + log ); } @Test - public void noHeaderNoAction() { - String[] lines = {"why", "so", "serious", "?"}; - SignChangeEvent event = event(lines, null); + public void noSignCreationNoAction() { + SignChangeEvent event = new SignChangeEvent(null, null, null); + when(creator.create(event)).thenReturn(null); subject.on(event); - verifyZeroInteractions(storesNewSign, rendersTemplate, messenger); + verifyNoInteractions(writer, store, messenger, log); } @Test - public void nullLinesHandledGracefully() { - String[] lines = {"[MA]", null, null, null}; - SignChangeEvent event = event(lines, null); - - subject.on(event); - } - - @Test - public void useSignTypeIfTemplateNotAvailable() { - String arenaId = "castle"; - String type = "join"; - String[] lines = {"[MA]", arenaId, type, null}; - Location location = mock(Location.class); - SignChangeEvent event = event(lines, location); - - subject.on(event); - - verify(storesNewSign).store(location, arenaId, type, type); - } - - @Test - public void useTemplateIfAvailable() { - String arenaId = "castle"; - String type = "join"; - String templateId = "potato"; - String[] lines = {"[MA]", arenaId, type, templateId}; - Location location = mock(Location.class); - SignChangeEvent event = event(lines, location); - - subject.on(event); - - verify(storesNewSign).store(location, arenaId, templateId, type); - } - - @Test - public void typeIsLowercased() { - String type = "JOIN"; - String[] lines = {"[MA]", "", type, ""}; - SignChangeEvent event = event(lines, null); - - subject.on(event); - - String lower = type.toLowerCase(); - verify(storesNewSign).store(any(), any(), any(), eq(lower)); - } - - @Test - public void templateIdIsLowercased() { - String templateId = "BEST-TEMPLATE"; - String[] lines = {"[MA]", "", "", templateId}; - SignChangeEvent event = event(lines, null); - - subject.on(event); - - String lower = templateId.toLowerCase(); - verify(storesNewSign).store(any(), any(), eq(lower), any()); - } - - @Test - public void rendersTemplateAfterStoring() { - String arenaId = "castle"; - String type = "join"; - String templateId = "potato"; - String[] lines = {"[MA]", arenaId, type, templateId}; - Location location = mock(Location.class); - SignChangeEvent event = event(lines, location); - - subject.on(event); - - verify(rendersTemplate).render(templateId, arenaId); - } - - @Test - public void errorPassedToMessenger() { - String msg = "you messed up"; - doThrow(new IllegalArgumentException(msg)) - .when(storesNewSign).store(any(), any(), any(), any()); - String[] lines = {"[MA]", "", "", ""}; - SignChangeEvent event = event(lines, null); - - subject.on(event); - - verifyZeroInteractions(rendersTemplate); - verify(messenger).tell(event.getPlayer(), msg); - } - - private SignChangeEvent event(String[] lines, Location location) { - Block block = mock(Block.class); - when(block.getLocation()).thenReturn(location); + public void passesSignFromCreator() throws Exception { Player player = mock(Player.class); - return new SignChangeEvent(block, player, lines); + SignChangeEvent event = new SignChangeEvent(null, player, null); + ArenaSign sign = new ArenaSign(location(), null, null, null); + when(creator.create(event)).thenReturn(sign); + + subject.on(event); + + verify(writer).write(sign); + verify(store).add(sign); + verify(renderer).render(sign, event); + } + + @Test + public void successMessageOnCreation() { + Player player = mock(Player.class); + SignChangeEvent event = new SignChangeEvent(null, player, null); + ArenaSign sign = new ArenaSign(location(), null, "castle", "join"); + when(creator.create(event)).thenReturn(sign); + + subject.on(event); + + verify(messenger).tell(eq(player), anyString()); + verify(log).info(anyString()); + } + + @Test + public void noWriteIfCreatorThrows() { + SignChangeEvent event = new SignChangeEvent(null, null, null); + doThrow(IllegalArgumentException.class).when(creator).create(event); + + subject.on(event); + + verifyNoInteractions(writer, store, renderer, log); + } + + @Test + public void errorMessageIfCreatorThrows() { + Player player = mock(Player.class); + SignChangeEvent event = new SignChangeEvent(null, player, null); + String message = "it's bad"; + doThrow(new IllegalArgumentException(message)).when(creator).create(event); + + subject.on(event); + + verify(messenger).tell(player, message); + } + + @Test + public void noStorageIfWriterThrows() throws Exception { + SignChangeEvent event = new SignChangeEvent(null, null, null); + ArenaSign sign = new ArenaSign(null, null, null, null); + when(creator.create(event)).thenReturn(sign); + doThrow(IOException.class).when(writer).write(sign); + + subject.on(event); + + verifyNoInteractions(store, renderer); + } + + @Test + public void errorMessageIfWriterThrows() throws Exception { + Player player = mock(Player.class); + SignChangeEvent event = new SignChangeEvent(null, player, null); + ArenaSign sign = new ArenaSign(null, null, null, null); + when(creator.create(event)).thenReturn(sign); + IOException exception = new IOException("it's bad"); + doThrow(exception).when(writer).write(sign); + + subject.on(event); + + verify(messenger).tell(eq(player), anyString()); + verify(log).log(eq(Level.SEVERE), anyString(), eq(exception)); + } + + private Location location() { + World world = mock(World.class); + when(world.getName()).thenReturn("world"); + Location location = mock(Location.class); + when(location.getBlockX()).thenReturn(1); + when(location.getBlockY()).thenReturn(2); + when(location.getBlockZ()).thenReturn(3); + when(location.getWorld()).thenReturn(world); + return location; } } diff --git a/src/test/java/com/garbagemule/MobArena/signs/HandlesSignDestructionTest.java b/src/test/java/com/garbagemule/MobArena/signs/HandlesSignDestructionTest.java index e9e1118..6a65a89 100644 --- a/src/test/java/com/garbagemule/MobArena/signs/HandlesSignDestructionTest.java +++ b/src/test/java/com/garbagemule/MobArena/signs/HandlesSignDestructionTest.java @@ -1,8 +1,8 @@ package com.garbagemule.MobArena.signs; -import static org.mockito.Mockito.*; - import com.garbagemule.MobArena.Messenger; +import org.bukkit.Location; +import org.bukkit.World; import org.bukkit.block.Block; import org.bukkit.entity.Player; import org.bukkit.event.block.BlockBreakEvent; @@ -11,53 +11,109 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.junit.MockitoJUnitRunner; -import java.util.Optional; +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; -@SuppressWarnings("WeakerAccess") @RunWith(MockitoJUnitRunner.StrictStubs.class) public class HandlesSignDestructionTest { - RemovesSignAtLocation removesSignAtLocation; + SignStore store; + SignWriter writer; Messenger messenger; + Logger log; HandlesSignDestruction subject; @Before public void setup() { - removesSignAtLocation = mock(RemovesSignAtLocation.class); + store = mock(SignStore.class); + writer = mock(SignWriter.class); messenger = mock(Messenger.class); + log = mock(Logger.class); subject = new HandlesSignDestruction( - removesSignAtLocation, - messenger + store, + writer, + messenger, + log ); } @Test - public void doesNothingWithNonArenaSign() { + public void noSignNoAction() { + Location location = mock(Location.class); + Block block = mock(Block.class); + BlockBreakEvent event = new BlockBreakEvent(block, null); + when(block.getLocation()).thenReturn(location); + when(store.removeByLocation(location)).thenReturn(null); + + subject.on(event); + + verifyNoInteractions(writer, messenger, log); + } + + @Test + public void erasesFoundSign() throws IOException { + Location location = location(); Block block = mock(Block.class); Player player = mock(Player.class); - when(removesSignAtLocation.remove(any())) - .thenReturn(Optional.empty()); + ArenaSign sign = new ArenaSign(location, "cool-sign", "castle", "join"); + when(block.getLocation()).thenReturn(location); + when(store.removeByLocation(location)).thenReturn(sign); BlockBreakEvent event = new BlockBreakEvent(block, player); subject.on(event); - verifyZeroInteractions(messenger); + verify(writer).erase(sign); } @Test - public void reportsBreakageWithArenaSign() { + public void successMessageOnDestruction() { + Location location = location(); Block block = mock(Block.class); Player player = mock(Player.class); - ArenaSign sign = new ArenaSign(null, "", "", ""); - when(removesSignAtLocation.remove(any())) - .thenReturn(Optional.of(sign)); + ArenaSign sign = new ArenaSign(location, "cool-sign", "castle", "join"); + when(block.getLocation()).thenReturn(location); + when(store.removeByLocation(location)).thenReturn(sign); BlockBreakEvent event = new BlockBreakEvent(block, player); subject.on(event); verify(messenger).tell(eq(player), anyString()); + verify(log).info(anyString()); + } + + @Test + public void errorMessageIfWriterThrows() throws Exception { + Location location = mock(Location.class); + Block block = mock(Block.class); + Player player = mock(Player.class); + ArenaSign sign = new ArenaSign(location, "cool-sign", "castle", "join"); + IOException exception = new IOException("it's bad"); + when(block.getLocation()).thenReturn(location); + when(store.removeByLocation(location)).thenReturn(sign); + doThrow(exception).when(writer).erase(sign); + BlockBreakEvent event = new BlockBreakEvent(block, player); + + subject.on(event); + + verify(messenger).tell(eq(player), anyString()); + verify(log).log(eq(Level.SEVERE), anyString(), eq(exception)); + } + + private Location location() { + World world = mock(World.class); + when(world.getName()).thenReturn("world"); + Location location = mock(Location.class); + when(location.getBlockX()).thenReturn(1); + when(location.getBlockY()).thenReturn(2); + when(location.getBlockZ()).thenReturn(3); + when(location.getWorld()).thenReturn(world); + return location; } } diff --git a/src/test/java/com/garbagemule/MobArena/signs/HandlesWorldLoadTest.java b/src/test/java/com/garbagemule/MobArena/signs/HandlesWorldLoadTest.java new file mode 100644 index 0000000..d930488 --- /dev/null +++ b/src/test/java/com/garbagemule/MobArena/signs/HandlesWorldLoadTest.java @@ -0,0 +1,122 @@ +package com.garbagemule.MobArena.signs; + +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.event.world.WorldLoadEvent; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +import java.io.IOException; +import java.util.Collections; +import java.util.UUID; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.StrictStubs.class) +public class HandlesWorldLoadTest { + + SignDataMigrator migrator; + SignReader reader; + SignStore store; + Logger log; + + HandlesWorldLoad subject; + + @Before + public void setup() { + migrator = mock(SignDataMigrator.class); + reader = mock(SignReader.class); + store = mock(SignStore.class); + log = mock(Logger.class); + + subject = new HandlesWorldLoad( + migrator, + reader, + store, + log + ); + } + + @Test + public void errorMessageIfDataMigrationThrows() throws IOException { + World world = mock(World.class); + when(world.getName()).thenReturn("world"); + IOException exception = new IOException("it's bad"); + doThrow(exception).when(migrator).migrate(world); + WorldLoadEvent event = new WorldLoadEvent(world); + + subject.on(event); + + verify(log).log(eq(Level.SEVERE), anyString(), eq(exception)); + } + + @Test + public void addsLoadedSignsToStore() throws IOException { + World world = mock(World.class); + when(world.getName()).thenReturn("world"); + when(world.getUID()).thenReturn(UUID.fromString("cafebabe-ea75-dead-beef-deadcafebabe")); + Location location = mock(Location.class); + ArenaSign sign = new ArenaSign(location, "cool-sign", "castle", "join"); + when(reader.read(world)).thenReturn(Collections.singletonList(sign)); + WorldLoadEvent event = new WorldLoadEvent(world); + + subject.on(event); + + verify(store).add(sign); + } + + @Test + public void logsMessageOnSignAddition() throws IOException { + World world = mock(World.class); + when(world.getName()).thenReturn("world"); + when(world.getUID()).thenReturn(UUID.fromString("cafebabe-ea75-dead-beef-deadcafebabe")); + Location location = mock(Location.class); + ArenaSign sign = new ArenaSign(location, "cool-sign", "castle", "join"); + when(reader.read(world)).thenReturn(Collections.singletonList(sign)); + WorldLoadEvent event = new WorldLoadEvent(world); + + subject.on(event); + + verify(log).info(anyString()); + } + + @Test + public void addsNothingIfNoSignsLoaded() throws IOException { + World world = mock(World.class); + when(reader.read(world)).thenReturn(Collections.emptyList()); + WorldLoadEvent event = new WorldLoadEvent(world); + + subject.on(event); + + verifyNoInteractions(store); + } + + @Test + public void logsNothingIfNoSignsLoaded() throws IOException { + World world = mock(World.class); + when(reader.read(world)).thenReturn(Collections.emptyList()); + WorldLoadEvent event = new WorldLoadEvent(world); + + subject.on(event); + + verifyNoInteractions(log); + } + + @Test + public void errorMessageIfReaderThrows() throws IOException { + World world = mock(World.class); + IOException exception = new IOException("it's bad"); + when(reader.read(world)).thenThrow(exception); + WorldLoadEvent event = new WorldLoadEvent(world); + + subject.on(event); + + verify(log).log(eq(Level.SEVERE), anyString(), eq(exception)); + } + +} diff --git a/src/test/java/com/garbagemule/MobArena/signs/HandlesWorldUnloadTest.java b/src/test/java/com/garbagemule/MobArena/signs/HandlesWorldUnloadTest.java new file mode 100644 index 0000000..4219028 --- /dev/null +++ b/src/test/java/com/garbagemule/MobArena/signs/HandlesWorldUnloadTest.java @@ -0,0 +1,65 @@ +package com.garbagemule.MobArena.signs; + +import org.bukkit.World; +import org.bukkit.event.world.WorldUnloadEvent; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.UUID; +import java.util.logging.Logger; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.StrictStubs.class) +public class HandlesWorldUnloadTest { + + SignStore store; + Logger log; + + HandlesWorldUnload subject; + + @Before + public void setup() { + store = mock(SignStore.class); + log = mock(Logger.class); + + subject = new HandlesWorldUnload( + store, + log + ); + } + + @Test + public void logsMessagesOnSignRemoval() { + UUID id = UUID.fromString("cafebabe-ea75-dead-beef-deadcafebabe"); + String name = "world"; + World world = mock(World.class); + when(world.getUID()).thenReturn(id); + when(world.getName()).thenReturn(name); + List signs = Arrays.asList(null, null); + when(store.removeByWorld(world)).thenReturn(signs); + WorldUnloadEvent event = new WorldUnloadEvent(world); + + subject.on(event); + + verify(log).info(anyString()); + } + + @Test + public void logsNothingIfNoSignsRemoved() { + World world = mock(World.class); + when(store.removeByWorld(world)).thenReturn(Collections.emptyList()); + WorldUnloadEvent event = new WorldUnloadEvent(world); + + subject.on(event); + + verifyNoInteractions(log); + } + +} diff --git a/src/test/java/com/garbagemule/MobArena/signs/InvokesSignActionTest.java b/src/test/java/com/garbagemule/MobArena/signs/InvokesSignActionTest.java index dd18585..23e203b 100644 --- a/src/test/java/com/garbagemule/MobArena/signs/InvokesSignActionTest.java +++ b/src/test/java/com/garbagemule/MobArena/signs/InvokesSignActionTest.java @@ -1,7 +1,5 @@ package com.garbagemule.MobArena.signs; -import static org.mockito.Mockito.*; - import com.garbagemule.MobArena.Messenger; import com.garbagemule.MobArena.framework.Arena; import com.garbagemule.MobArena.framework.ArenaMaster; @@ -11,6 +9,8 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.junit.MockitoJUnitRunner; +import static org.mockito.Mockito.*; + @SuppressWarnings("WeakerAccess") @RunWith(MockitoJUnitRunner.StrictStubs.class) public class InvokesSignActionTest { @@ -36,7 +36,7 @@ public class InvokesSignActionTest { subject.invoke(sign, player); - verifyZeroInteractions(arenaMaster); + verifyNoInteractions(arenaMaster); } @Test diff --git a/src/test/java/com/garbagemule/MobArena/signs/RedrawsArenaSignsTest.java b/src/test/java/com/garbagemule/MobArena/signs/RedrawsArenaSignsTest.java deleted file mode 100644 index a53d908..0000000 --- a/src/test/java/com/garbagemule/MobArena/signs/RedrawsArenaSignsTest.java +++ /dev/null @@ -1,141 +0,0 @@ -package com.garbagemule.MobArena.signs; - -import static org.mockito.Mockito.*; - -import com.garbagemule.MobArena.framework.Arena; -import org.bukkit.Location; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.junit.MockitoJUnitRunner; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Optional; - -@SuppressWarnings("WeakerAccess") -@RunWith(MockitoJUnitRunner.StrictStubs.class) -public class RedrawsArenaSignsTest { - - SignStore signStore; - TemplateStore templateStore; - RendersTemplate rendersTemplate; - SetsLines setsSignLines; - - RedrawsArenaSigns subject; - - @Before - public void setup() { - signStore = mock(SignStore.class); - when(signStore.findByArenaId(any())) - .thenReturn(Collections.emptyList()); - - templateStore = mock(TemplateStore.class); - when(templateStore.findById(any())) - .thenReturn(Optional.empty()); - - rendersTemplate = mock(RendersTemplate.class); - when(rendersTemplate.render(any(), any())) - .thenReturn(new String[]{"a", "b", "c", "d"}); - - setsSignLines = mock(SetsLines.class); - - subject = new RedrawsArenaSigns( - signStore, - templateStore, - rendersTemplate, - setsSignLines - ); - } - - @Test - public void noSignsMeansNoRenderingOrLineSetting() { - Arena arena = arena("castle"); - - subject.redraw(arena); - - verifyZeroInteractions(rendersTemplate); - verifyZeroInteractions(setsSignLines); - } - - @Test - public void renderFoundTemplate() { - String arenaId = "castle"; - Arena arena = arena(arenaId); - ArenaSign sign = sign("join", arenaId); - when(signStore.findByArenaId(arenaId)) - .thenReturn(Collections.singletonList(sign)); - Template template = template("template", "some", "info", "about", "arena"); - when(templateStore.findById(sign.templateId)) - .thenReturn(Optional.of(template)); - - subject.redraw(arena); - - verify(rendersTemplate).render(template, arena); - } - - @Test - public void setRenderedTemplateOnSign() { - String arenaId = "castle"; - Arena arena = arena(arenaId); - ArenaSign sign = sign("join", arenaId); - when(signStore.findByArenaId(sign.arenaId)) - .thenReturn(Collections.singletonList(sign)); - Template template = template("template", "try", "with", "more", "fireballs"); - when(templateStore.findById(sign.templateId)) - .thenReturn(Optional.of(template)); - String[] lines = new String[]{"this", "is", "a", "sign"}; - when(rendersTemplate.render(template, arena)) - .thenReturn(lines); - - subject.redraw(arena); - - verify(setsSignLines).set(sign.location, lines); - } - - @Test - public void renderEachTemplateOnlyOnce() { - String arenaId = "castle"; - Arena arena = arena(arenaId); - String templateId1 = "join"; - String templateId2 = "info"; - List signs = new ArrayList<>(); - signs.add(sign(templateId1, arenaId)); - signs.add(sign(templateId1, arenaId)); - signs.add(sign(templateId1, arenaId)); - signs.add(sign(templateId2, arenaId)); - when(signStore.findByArenaId(arenaId)) - .thenReturn(signs); - Template template1 = template(templateId1, "join", "a", "MobArena", "today!"); - Template template2 = template(templateId2, "join", "another", "MobArena", "tomorrow!"); - when(templateStore.findById(templateId1)) - .thenReturn(Optional.of(template1)); - when(templateStore.findById(templateId2)) - .thenReturn(Optional.of(template2)); - - subject.redraw(arena); - - verify(rendersTemplate, times(1)).render(template1, arena); - verify(rendersTemplate, times(1)).render(template2, arena); - verify(setsSignLines, times(signs.size())).set(any(), any()); - } - - private Arena arena(String arenaId) { - Arena arena = mock(Arena.class); - when(arena.configName()).thenReturn(arenaId); - return arena; - } - - private ArenaSign sign(String templateId, String arenaId) { - Location location = mock(Location.class); - return new ArenaSign(location, templateId, arenaId, "join"); - } - - private Template template(String id, String l1, String l2, String l3, String l4) { - return new Template.Builder(id) - .withBase(new String[]{l1, l2, l3, l4}) - .build(); - } - -} diff --git a/src/test/java/com/garbagemule/MobArena/signs/RemovesSignAtLocationTest.java b/src/test/java/com/garbagemule/MobArena/signs/RemovesSignAtLocationTest.java deleted file mode 100644 index 0ab9f1e..0000000 --- a/src/test/java/com/garbagemule/MobArena/signs/RemovesSignAtLocationTest.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.garbagemule.MobArena.signs; - -import static org.mockito.Mockito.*; - -import org.bukkit.Location; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.junit.MockitoJUnitRunner; - -import java.util.Optional; - -@SuppressWarnings("WeakerAccess") -@RunWith(MockitoJUnitRunner.StrictStubs.class) -public class RemovesSignAtLocationTest { - - SignStore signStore; - SavesSignStore savesSignStore; - - RemovesSignAtLocation subject; - - @Before - public void setup() { - signStore = mock(SignStore.class); - savesSignStore = mock(SavesSignStore.class); - - subject = new RemovesSignAtLocation( - signStore, - savesSignStore - ); - } - - @Test - public void noSignMeansNoWrite() { - Location location = mock(Location.class); - when(signStore.remove(location)) - .thenReturn(Optional.empty()); - - subject.remove(location); - - verifyZeroInteractions(savesSignStore); - } - - @Test - public void signRemovedWritesStore() { - Location location = mock(Location.class); - ArenaSign sign = new ArenaSign(location, "", "", ""); - when(signStore.remove(location)) - .thenReturn(Optional.of(sign)); - - subject.remove(location); - - verify(savesSignStore).save(signStore); - } - -} diff --git a/src/test/java/com/garbagemule/MobArena/signs/RendersTemplateTest.java b/src/test/java/com/garbagemule/MobArena/signs/RendersTemplateTest.java index 788427b..607eb88 100644 --- a/src/test/java/com/garbagemule/MobArena/signs/RendersTemplateTest.java +++ b/src/test/java/com/garbagemule/MobArena/signs/RendersTemplateTest.java @@ -1,9 +1,5 @@ package com.garbagemule.MobArena.signs; -import static org.hamcrest.MatcherAssert.*; -import static org.hamcrest.Matchers.*; -import static org.mockito.Mockito.*; - import com.garbagemule.MobArena.framework.Arena; import com.garbagemule.MobArena.waves.WaveManager; import org.bukkit.entity.Player; @@ -14,6 +10,10 @@ import org.mockito.junit.MockitoJUnitRunner; import java.util.Collections; +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; +import static org.mockito.Mockito.*; + @SuppressWarnings("WeakerAccess") @RunWith(MockitoJUnitRunner.StrictStubs.class) public class RendersTemplateTest { diff --git a/src/test/java/com/garbagemule/MobArena/signs/SignCreatorTest.java b/src/test/java/com/garbagemule/MobArena/signs/SignCreatorTest.java new file mode 100644 index 0000000..4625f7c --- /dev/null +++ b/src/test/java/com/garbagemule/MobArena/signs/SignCreatorTest.java @@ -0,0 +1,211 @@ +package com.garbagemule.MobArena.signs; + +import com.garbagemule.MobArena.framework.Arena; +import com.garbagemule.MobArena.framework.ArenaMaster; +import org.bukkit.Location; +import org.bukkit.block.Block; +import org.bukkit.entity.Player; +import org.bukkit.event.block.SignChangeEvent; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.Optional; + +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.MatcherAssert.*; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.StrictStubs.class) +public class SignCreatorTest { + + ArenaMaster arenaMaster; + TemplateStore templateStore; + + SignCreator subject; + + @Before + public void setup() { + arenaMaster = mock(ArenaMaster.class); + templateStore = mock(TemplateStore.class); + + subject = new SignCreator( + arenaMaster, + templateStore + ); + } + + @Test + public void noHeaderNoAction() { + String[] lines = {"ma", "castle", "join", "cool-sign"}; + SignChangeEvent event = new SignChangeEvent(null, null, lines); + + ArenaSign result = subject.create(event); + + assertThat(result, nullValue()); + verifyNoInteractions(arenaMaster, templateStore); + } + + @Test + public void throwsOnMissingArena() { + String[] lines = {"[MA]", null, "join", "cool-sign"}; + SignChangeEvent event = event(lines, null); + + assertThrows( + IllegalArgumentException.class, + () -> subject.create(event) + ); + verifyNoInteractions(arenaMaster); + } + + @Test + public void throwsIfArenaNotFound() { + String arenaId = "castle"; + String[] lines = {"[MA]", arenaId, "join", "cool-sign"}; + when(arenaMaster.getArenaWithName(arenaId)).thenReturn(null); + SignChangeEvent event = event(lines, null); + + assertThrows( + IllegalArgumentException.class, + () -> subject.create(event) + ); + } + + @Test + public void throwsOnMissingType() { + String arenaId = "castle"; + String[] lines = {"[MA]", arenaId, null, "cool-sign"}; + Arena arena = mock(Arena.class); + when(arena.getSlug()).thenReturn(arenaId); + when(arenaMaster.getArenaWithName(arenaId)).thenReturn(arena); + SignChangeEvent event = event(lines, null); + + assertThrows( + IllegalArgumentException.class, + () -> subject.create(event) + ); + } + + @Test + public void throwsOnInvalidType() { + String arenaId = "castle"; + String[] lines = {"[MA]", arenaId, "bob", "cool-sign"}; + Arena arena = mock(Arena.class); + when(arena.getSlug()).thenReturn(arenaId); + when(arenaMaster.getArenaWithName(arenaId)).thenReturn(arena); + SignChangeEvent event = event(lines, null); + + assertThrows( + IllegalArgumentException.class, + () -> subject.create(event) + ); + } + + @Test + public void throwsIfTemplateNotFound() { + String arenaId = "castle"; + String templateId = "cool-sign"; + String[] lines = {"[MA]", arenaId, "join", templateId}; + Arena arena = mock(Arena.class); + when(arena.getSlug()).thenReturn(arenaId); + when(arenaMaster.getArenaWithName(arenaId)).thenReturn(arena); + when(templateStore.findById(templateId)).thenReturn(Optional.empty()); + SignChangeEvent event = event(lines, null); + + assertThrows( + IllegalArgumentException.class, + () -> subject.create(event) + ); + } + + @Test + public void completeSignDefinition() { + String arenaId = "castle"; + String signType = "join"; + String templateId = "cool-sign"; + String[] lines = {"[MA]", arenaId, signType, templateId}; + Location location = mock(Location.class); + Arena arena = mock(Arena.class); + when(arena.getSlug()).thenReturn(arenaId); + when(arenaMaster.getArenaWithName(arenaId)).thenReturn(arena); + when(templateStore.findById(templateId)).thenReturn(Optional.of(mock(Template.class))); + SignChangeEvent event = event(lines, location); + + ArenaSign result = subject.create(event); + + assertThat(result.location, equalTo(location)); + assertThat(result.arenaId, equalTo(arenaId)); + assertThat(result.type, equalTo(signType)); + assertThat(result.templateId, equalTo(templateId)); + } + + @Test + public void slugifiedArenaId() { + String arenaId = "Area 52"; + String arenaSlug = "area-52"; + String signType = "join"; + String templateId = "cool-sign"; + String[] lines = {"[MA]", arenaId, signType, templateId}; + Location location = mock(Location.class); + Arena arena = mock(Arena.class); + when(arena.getSlug()).thenReturn(arenaSlug); + when(arenaMaster.getArenaWithName(arenaId)).thenReturn(arena); + when(templateStore.findById(templateId)).thenReturn(Optional.of(mock(Template.class))); + SignChangeEvent event = event(lines, location); + + ArenaSign result = subject.create(event); + + assertThat(result.location, equalTo(location)); + assertThat(result.arenaId, equalTo(arenaSlug)); + assertThat(result.type, equalTo(signType.toLowerCase())); + assertThat(result.templateId, equalTo(templateId)); + } + + @Test + public void caseInsensitiveSignType() { + String arenaId = "castle"; + String signType = "jOiN"; + String templateId = "cool-sign"; + String[] lines = {"[MA]", arenaId, signType, templateId}; + Location location = mock(Location.class); + Arena arena = mock(Arena.class); + when(arena.getSlug()).thenReturn(arenaId); + when(arenaMaster.getArenaWithName(arenaId)).thenReturn(arena); + when(templateStore.findById(templateId)).thenReturn(Optional.of(mock(Template.class))); + SignChangeEvent event = event(lines, location); + + ArenaSign result = subject.create(event); + + assertThat(result.location, equalTo(location)); + assertThat(result.arenaId, equalTo(arenaId)); + assertThat(result.type, equalTo(signType.toLowerCase())); + assertThat(result.templateId, equalTo(templateId)); + } + + @Test + public void signWithoutTemplateUsesType() { + String arenaId = "castle"; + String signType = "join"; + String[] lines = {"[MA]", arenaId, signType, null}; + Location location = mock(Location.class); + Arena arena = mock(Arena.class); + when(arena.getSlug()).thenReturn(arenaId); + when(arenaMaster.getArenaWithName(arenaId)).thenReturn(arena); + when(templateStore.findById(signType)).thenReturn(Optional.of(mock(Template.class))); + SignChangeEvent event = event(lines, location); + + ArenaSign result = subject.create(event); + + assertThat(result.templateId, equalTo(signType)); + } + + private SignChangeEvent event(String[] lines, Location location) { + Block block = mock(Block.class); + when(block.getLocation()).thenReturn(location); + Player player = mock(Player.class); + return new SignChangeEvent(block, player, lines); + } + +} diff --git a/src/test/java/com/garbagemule/MobArena/signs/SignDataMigratorTest.java b/src/test/java/com/garbagemule/MobArena/signs/SignDataMigratorTest.java new file mode 100644 index 0000000..86a089e --- /dev/null +++ b/src/test/java/com/garbagemule/MobArena/signs/SignDataMigratorTest.java @@ -0,0 +1,270 @@ +package com.garbagemule.MobArena.signs; + +import org.bukkit.World; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; +import org.yaml.snakeyaml.Yaml; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.logging.Logger; + +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.MatcherAssert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.StrictStubs.class) +public class SignDataMigratorTest { + + Path legacyFile; + Path pendingFile; + Yaml yaml; + SignFile signFile; + Logger log; + + SignDataMigrator subject; + + @Before + public void setup() throws IOException { + legacyFile = Files.createTempFile("SignDataMigratorTest-", ".data.tmp"); + pendingFile = Files.createTempFile("SignDataMigratorTest-", ".tmp.tmp"); + yaml = mock(Yaml.class); + signFile = mock(SignFile.class); + log = mock(Logger.class); + + subject = new SignDataMigrator( + legacyFile, + pendingFile, + yaml, + signFile, + log + ); + } + + @After + public void teardown() throws IOException { + Files.deleteIfExists(legacyFile); + Files.deleteIfExists(pendingFile); + } + + @Test + public void initDoesNothingIfNoLegacyFile() throws IOException { + Files.delete(pendingFile); + Files.delete(legacyFile); + + subject.init(); + + verifyNoInteractions(yaml, log); + assertThat(Files.exists(legacyFile), equalTo(false)); + assertThat(Files.exists(pendingFile), equalTo(false)); + } + + @Test + public void initCreatesPendingFileAndDeletesLegacyFile() throws IOException { + Files.delete(pendingFile); + Map map = new LinkedHashMap<>(); + when(yaml.load(anyString())).thenReturn(map); + + subject.init(); + + assertThat(Files.exists(legacyFile), equalTo(false)); + assertThat(Files.exists(pendingFile), equalTo(true)); + } + + @Test + public void initLogsInfoMessagesOnConversion() throws IOException { + Files.delete(pendingFile); + Map map = new LinkedHashMap<>(); + when(yaml.load(anyString())).thenReturn(map); + + subject.init(); + + verify(log, times(2)).info(anyString()); + } + + @Test + public void initMigratesToPartialFormat() throws IOException { + Files.delete(pendingFile); + List> signs = new ArrayList<>(); + signs.add(sign("world", 46.0, 101.0, -191.0, "Area 52", "join", "cool-sign")); + signs.add(sign("lazy", 47.0, 102.0, -192.0, "Mr. Bob's Bus", "info", "status")); + signs.add(sign("eager", 48.0, 103.0, -194.0, "Mission: Impossible", "leave", "coward")); + signs.add(sign("world", 49.0, 104.0, -198.0, "castle", "leave", "bye-bye")); + Map root = new LinkedHashMap<>(); + root.put("signs", signs); + when(yaml.load(anyString())).thenReturn(root); + + subject.init(); + + List lines = Files.readAllLines(pendingFile); + assertThat(lines.size(), equalTo(signs.size())); + assertThat(lines, hasItems( + "world;46;101;-191;area-52;join;cool-sign", + "lazy;47;102;-192;mr-bobs-bus;info;status", + "eager;48;103;-194;mission-impossible;leave;coward", + "world;49;104;-198;castle;leave;bye-bye" + )); + } + + @Test + public void migrateDoesNothingIfNoPendingFile() throws IOException { + Files.delete(pendingFile); + World world = mock(World.class); + + subject.migrate(world); + + verifyNoInteractions(signFile, log); + } + + @Test + public void migrateDoesNothingIfNoMatchingLines() throws IOException { + List lines = Arrays.asList( + "world;46;101;-191;castle;join;cool-sign", + "lazy;47;102;-192;jungle;info;status", + "world;48;103;-194;island;leave;coward" + ); + Files.write(pendingFile, lines); + String name = "not-world"; + World world = mock(World.class); + when(world.getName()).thenReturn(name); + + subject.migrate(world); + + verifyNoInteractions(signFile, log); + } + + @Test + public void migratePassesMatchingLinesToSignFile() throws IOException { + List lines = Arrays.asList( + "world;46;101;-191;castle;join;cool-sign", + "lazy;47;102;-192;jungle;info;status", + "world;48;103;-194;island;leave;coward" + ); + Files.write(pendingFile, lines); + String name = "world"; + String id = "cafebabe-ea75-dead-beef-deadcafebabe"; + World world = mock(World.class); + when(world.getName()).thenReturn(name); + when(world.getUID()).thenReturn(UUID.fromString(id)); + + subject.migrate(world); + + verify(signFile).append(id + ";" + lines.get(0)); + verify(signFile).append(id + ";" + lines.get(2)); + verify(signFile).save(); + verifyNoMoreInteractions(signFile); + } + + @Test + public void migrateDeletesMatchingLinesFromPendingFile() throws IOException { + List lines = Arrays.asList( + "world;46;101;-191;castle;join;cool-sign", + "lazy;47;102;-192;jungle;info;status", + "world;48;103;-194;island;leave;coward" + ); + Files.write(pendingFile, lines); + String name = "world"; + String id = "cafebabe-ea75-dead-beef-deadcafebabe"; + World world = mock(World.class); + when(world.getName()).thenReturn(name); + when(world.getUID()).thenReturn(UUID.fromString(id)); + + subject.migrate(world); + + List remaining = Files.readAllLines(pendingFile); + assertThat(remaining, equalTo(lines.subList(1, 2))); + } + + @Test + public void migrateLogsInfoMessageOnMigration() throws IOException { + List lines = Arrays.asList( + "world;46;101;-191;castle;join;cool-sign", + "lazy;47;102;-192;jungle;info;status", + "world;48;103;-194;island;leave;coward" + ); + Files.write(pendingFile, lines); + String name = "world"; + String id = "cafebabe-ea75-dead-beef-deadcafebabe"; + World world = mock(World.class); + when(world.getName()).thenReturn(name); + when(world.getUID()).thenReturn(UUID.fromString(id)); + + subject.migrate(world); + + verify(log, times(2)).info(anyString()); + } + + @Test + public void migrateDeletesPendingFileOnCompletion() throws IOException { + List lines = Arrays.asList( + "world;46;101;-191;castle;join;cool-sign", + "world;48;103;-194;island;leave;coward" + ); + Files.write(pendingFile, lines); + String name = "world"; + String id = "cafebabe-ea75-dead-beef-deadcafebabe"; + World world = mock(World.class); + when(world.getName()).thenReturn(name); + when(world.getUID()).thenReturn(UUID.fromString(id)); + + subject.migrate(world); + + assertThat(Files.exists(pendingFile), equalTo(false)); + } + + @Test + public void migrateLogsAdditionallyOnCompletion() throws IOException { + List lines = Arrays.asList( + "world;46;101;-191;castle;join;cool-sign", + "world;48;103;-194;island;leave;coward" + ); + Files.write(pendingFile, lines); + String name = "world"; + String id = "cafebabe-ea75-dead-beef-deadcafebabe"; + World world = mock(World.class); + when(world.getName()).thenReturn(name); + when(world.getUID()).thenReturn(UUID.fromString(id)); + + subject.migrate(world); + + verify(log, times(3)).info(anyString()); + } + + private Map sign( + String world, + double x, + double y, + double z, + String arenaId, + String type, + String templateId + ) { + Map location = new LinkedHashMap<>(); + location.put("==", "org.bukkit.Location"); + location.put("world", world); + location.put("x", x); + location.put("y", y); + location.put("z", z); + location.put("pitch", 0.0); + location.put("yaw", 0.0); + Map result = new LinkedHashMap<>(); + result.put("==", "com.garbagemule.MobArena.signs.ArenaSign"); + result.put("arenaId", arenaId); + result.put("location", location); + result.put("templateId", templateId); + result.put("type", type); + return result; + } + +} diff --git a/src/test/java/com/garbagemule/MobArena/signs/SignFileTest.java b/src/test/java/com/garbagemule/MobArena/signs/SignFileTest.java new file mode 100644 index 0000000..af316fc --- /dev/null +++ b/src/test/java/com/garbagemule/MobArena/signs/SignFileTest.java @@ -0,0 +1,98 @@ +package com.garbagemule.MobArena.signs; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.MatcherAssert.*; + +@RunWith(MockitoJUnitRunner.StrictStubs.class) +public class SignFileTest { + + Path file; + + SignFile subject; + + @Before + public void setup() throws IOException { + file = Files.createTempFile("SignFileTest-", ".tmp"); + subject = new SignFile(file); + } + + @After + public void teardown() throws IOException { + Files.delete(file); + } + + @Test + public void emptyFileEmptyLines() throws IOException { + List result = subject.lines(); + + assertThat(result.isEmpty(), equalTo(true)); + } + + @Test + public void emptyLinesIfAppendWithoutSave() throws IOException { + String line1 = "We will"; + String line2 = "not be saved!"; + + subject.append(line1); + subject.append(line2); + List result = subject.lines(); + + assertThat(result.isEmpty(), equalTo(true)); + } + + @Test + public void linePersistedOnSave() throws IOException { + String line1 = "We will"; + String line2 = "be saved!"; + + subject.append(line1); + subject.append(line2); + subject.save(); + List result = subject.lines(); + + assertThat(result, equalTo(Arrays.asList(line1, line2))); + } + + @Test + public void lineLingersWithoutSave() throws IOException { + String line1 = "We will"; + String line2 = "be saved!"; + + subject.append(line1); + subject.append(line2); + subject.save(); + subject.erase(line1); + List result = subject.lines(); + + assertThat(result, equalTo(Arrays.asList(line1, line2))); + } + + @Test + public void lineRemovedOnSave() throws IOException { + String line1 = "We will"; + String line2 = "be saved!"; + + subject.append(line1); + subject.append(line2); + subject.save(); + subject.erase(line1); + subject.save(); + List result = subject.lines(); + + assertThat(result, equalTo(Collections.singletonList(line2))); + } + +} diff --git a/src/test/java/com/garbagemule/MobArena/signs/SignReaderTest.java b/src/test/java/com/garbagemule/MobArena/signs/SignReaderTest.java new file mode 100644 index 0000000..f172091 --- /dev/null +++ b/src/test/java/com/garbagemule/MobArena/signs/SignReaderTest.java @@ -0,0 +1,154 @@ +package com.garbagemule.MobArena.signs; + +import org.bukkit.World; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.UUID; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.MatcherAssert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.StrictStubs.class) +public class SignReaderTest { + + SignFile file; + SignSerializer serializer; + Logger log; + + SignReader subject; + + @Before + public void setup() { + file = mock(SignFile.class); + serializer = mock(SignSerializer.class); + log = mock(Logger.class); + + subject = new SignReader( + file, + serializer, + log + ); + } + + @Test + public void deserializesNothingIfEmptyFile() throws IOException { + String id = "cafebabe-ea75-dead-beef-deadcafebabe"; + World world = mock(World.class); + when(world.getUID()).thenReturn(UUID.fromString(id)); + when(file.lines()).thenReturn(Collections.emptyList()); + + subject.read(world); + + verifyNoInteractions(serializer); + } + + @Test + public void logsNothingIfEmptyFile() throws IOException { + String id = "cafebabe-ea75-dead-beef-deadcafebabe"; + World world = mock(World.class); + when(world.getUID()).thenReturn(UUID.fromString(id)); + when(file.lines()).thenReturn(Collections.emptyList()); + + subject.read(world); + + verifyNoInteractions(log); + } + + @Test + public void deserializesLinesWithMatchingWorldId() throws IOException { + String id = "cafebabe-ea75-dead-beef-deadcafebabe"; + World world = mock(World.class); + when(world.getUID()).thenReturn(UUID.fromString(id)); + String line1 = "cafebeef-ea75-dead-babe-deadcafebeef;world;1;2;3;jungle;info;status"; + String line2 = id + ";world;1;2;3;castle;join;cool-sign"; + when(file.lines()).thenReturn(Arrays.asList(line1, line2)); + ArenaSign sign = new ArenaSign(null, "cool-sign", "castle", "join"); + when(serializer.deserialize(line2, world)).thenReturn(sign); + + List result = subject.read(world); + + assertThat(result.size(), equalTo(1)); + assertThat(result, hasItem(sign)); + verifyNoMoreInteractions(serializer); + } + + @Test + public void logsNothingOnSuccess() throws IOException { + String id = "cafebabe-ea75-dead-beef-deadcafebabe"; + World world = mock(World.class); + when(world.getUID()).thenReturn(UUID.fromString(id)); + String line = id + ";world;1;2;3;castle;join;cool-sign"; + when(file.lines()).thenReturn(Collections.singletonList(line)); + ArenaSign sign = new ArenaSign(null, "cool-sign", "castle", "join"); + when(serializer.deserialize(line, world)).thenReturn(sign); + + subject.read(world); + + verifyNoInteractions(log); + } + + @Test + public void skipLineIfSerializerThrows() throws IOException { + String id = "cafebabe-ea75-dead-beef-deadcafebabe"; + World world = mock(World.class); + when(world.getUID()).thenReturn(UUID.fromString(id)); + String line1 = id + ";world;1;2;3;jungle;info;status"; + String line2 = id + ";world;4;5;6;castle;join;cool-sign"; + when(file.lines()).thenReturn(Arrays.asList(line1, line2)); + IllegalArgumentException exception = new IllegalArgumentException("it's bad"); + ArenaSign sign = new ArenaSign(null, "cool-sign", "castle", "join"); + when(serializer.deserialize(line1, world)).thenThrow(exception); + when(serializer.deserialize(line2, world)).thenReturn(sign); + + List result = subject.read(world); + + assertThat(result.size(), equalTo(1)); + assertThat(result, hasItem(sign)); + } + + @Test + public void errorMessageIfSerializerThrows() throws IOException { + String id = "cafebabe-ea75-dead-beef-deadcafebabe"; + World world = mock(World.class); + when(world.getUID()).thenReturn(UUID.fromString(id)); + String line1 = id + ";world;1;2;3;jungle;info;status"; + String line2 = id + ";world;4;5;6;castle;join;cool-sign"; + when(file.lines()).thenReturn(Arrays.asList(line1, line2)); + IllegalArgumentException exception = new IllegalArgumentException("it's bad"); + ArenaSign sign = new ArenaSign(null, "cool-sign", "castle", "join"); + when(serializer.deserialize(line1, world)).thenThrow(exception); + when(serializer.deserialize(line2, world)).thenReturn(sign); + + subject.read(world); + + verify(log).log(eq(Level.SEVERE), anyString(), eq(exception)); + verifyNoMoreInteractions(log); + } + + @Test + public void propagatesExceptionsFromFile() throws IOException { + String id = "cafebabe-ea75-dead-beef-deadcafebabe"; + World world = mock(World.class); + when(world.getUID()).thenReturn(UUID.fromString(id)); + IOException exception = new IOException("it's bad"); + when(file.lines()).thenThrow(exception); + + Assert.assertThrows( + IOException.class, + () -> subject.read(world) + ); + } + +} diff --git a/src/test/java/com/garbagemule/MobArena/signs/SignRendererTest.java b/src/test/java/com/garbagemule/MobArena/signs/SignRendererTest.java new file mode 100644 index 0000000..0ea47dc --- /dev/null +++ b/src/test/java/com/garbagemule/MobArena/signs/SignRendererTest.java @@ -0,0 +1,161 @@ +package com.garbagemule.MobArena.signs; + +import com.garbagemule.MobArena.framework.Arena; +import com.garbagemule.MobArena.framework.ArenaMaster; +import org.bukkit.ChatColor; +import org.bukkit.Location; +import org.bukkit.block.Block; +import org.bukkit.block.Sign; +import org.bukkit.event.block.SignChangeEvent; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.Optional; + +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.StrictStubs.class) +public class SignRendererTest { + + TemplateStore templateStore; + ArenaMaster arenaMaster; + RendersTemplate rendersTemplate; + + SignRenderer subject; + + @Before + public void setup() { + templateStore = mock(TemplateStore.class); + arenaMaster = mock(ArenaMaster.class); + rendersTemplate = mock(RendersTemplate.class); + + subject = new SignRenderer( + templateStore, + arenaMaster, + rendersTemplate + ); + } + + @Test + public void rendersErrorMessageOnSignIfTemplateNotFound() { + Location location = mock(Location.class); + Block block = mock(Block.class); + Sign target = mock(Sign.class); + when(location.getBlock()).thenReturn(block); + when(block.getState()).thenReturn(target); + String templateId = "cool-sign"; + when(templateStore.findById(templateId)).thenReturn(Optional.empty()); + ArenaSign sign = new ArenaSign(location, templateId, "castle", "join"); + + subject.render(sign); + + verify(target).setLine(0, ChatColor.RED + "[ERROR]"); + verify(target).setLine(1, "Template"); + verify(target).setLine(2, ChatColor.YELLOW + "cool-sign"); + verify(target).setLine(3, "not found :("); + } + + @Test + public void rendersErrorMessageOnSignIfArenaNotFound() { + Location location = mock(Location.class); + Block block = mock(Block.class); + Sign target = mock(Sign.class); + when(location.getBlock()).thenReturn(block); + when(block.getState()).thenReturn(target); + String templateId = "cool-sign"; + Template template = mock(Template.class); + String arenaId = "castle"; + when(templateStore.findById(templateId)).thenReturn(Optional.of(template)); + when(arenaMaster.getArenaWithName(arenaId)).thenReturn(null); + ArenaSign sign = new ArenaSign(location, templateId, arenaId, "join"); + + subject.render(sign); + + verify(target).setLine(0, ChatColor.RED + "[ERROR]"); + verify(target).setLine(1, "Arena"); + verify(target).setLine(2, ChatColor.YELLOW + arenaId); + verify(target).setLine(3, "not found :("); + } + + @Test + public void rendersResultsFromDelegateOnSign() { + Location location = mock(Location.class); + Block block = mock(Block.class); + Sign target = mock(Sign.class); + when(location.getBlock()).thenReturn(block); + when(block.getState()).thenReturn(target); + String templateId = "cool-sign"; + Template template = mock(Template.class); + String arenaId = "castle"; + Arena arena = mock(Arena.class); + String[] lines = new String[]{"this", "is", "a", "sign"}; + when(templateStore.findById(templateId)).thenReturn(Optional.of(template)); + when(arenaMaster.getArenaWithName(arenaId)).thenReturn(arena); + when(rendersTemplate.render(template, arena)).thenReturn(lines); + ArenaSign sign = new ArenaSign(location, templateId, arenaId, "join"); + + subject.render(sign); + + verify(target).setLine(0, lines[0]); + verify(target).setLine(1, lines[1]); + verify(target).setLine(2, lines[2]); + verify(target).setLine(3, lines[3]); + } + + @Test + public void rendersErrorMessageInEventIfTemplateNotFound() { + String templateId = "cool-sign"; + when(templateStore.findById(templateId)).thenReturn(Optional.empty()); + ArenaSign sign = new ArenaSign(null, templateId, "castle", "join"); + SignChangeEvent event = mock(SignChangeEvent.class); + + subject.render(sign, event); + + verify(event).setLine(0, ChatColor.RED + "[ERROR]"); + verify(event).setLine(1, "Template"); + verify(event).setLine(2, ChatColor.YELLOW + "cool-sign"); + verify(event).setLine(3, "not found :("); + } + + @Test + public void rendersErrorMessageInEventIfArenaNotFound() { + String templateId = "cool-sign"; + Template template = mock(Template.class); + String arenaId = "castle"; + when(templateStore.findById(templateId)).thenReturn(Optional.of(template)); + when(arenaMaster.getArenaWithName(arenaId)).thenReturn(null); + ArenaSign sign = new ArenaSign(null, templateId, arenaId, "join"); + SignChangeEvent event = mock(SignChangeEvent.class); + + subject.render(sign, event); + + verify(event).setLine(0, ChatColor.RED + "[ERROR]"); + verify(event).setLine(1, "Arena"); + verify(event).setLine(2, ChatColor.YELLOW + arenaId); + verify(event).setLine(3, "not found :("); + } + + @Test + public void rendersResultsFromDelegateInEvent() { + String templateId = "cool-sign"; + Template template = mock(Template.class); + String arenaId = "castle"; + Arena arena = mock(Arena.class); + String[] lines = new String[]{"this", "is", "a", "sign"}; + when(templateStore.findById(templateId)).thenReturn(Optional.of(template)); + when(arenaMaster.getArenaWithName(arenaId)).thenReturn(arena); + when(rendersTemplate.render(template, arena)).thenReturn(lines); + ArenaSign sign = new ArenaSign(null, templateId, arenaId, "join"); + SignChangeEvent event = mock(SignChangeEvent.class); + + subject.render(sign, event); + + verify(event).setLine(0, lines[0]); + verify(event).setLine(1, lines[1]); + verify(event).setLine(2, lines[2]); + verify(event).setLine(3, lines[3]); + } + +} diff --git a/src/test/java/com/garbagemule/MobArena/signs/SignSerializerTest.java b/src/test/java/com/garbagemule/MobArena/signs/SignSerializerTest.java new file mode 100644 index 0000000..d67b6e3 --- /dev/null +++ b/src/test/java/com/garbagemule/MobArena/signs/SignSerializerTest.java @@ -0,0 +1,165 @@ +package com.garbagemule.MobArena.signs; + +import org.bukkit.Location; +import org.bukkit.World; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.UUID; + +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.MatcherAssert.*; +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.StrictStubs.class) +public class SignSerializerTest { + + SignSerializer subject; + + @Before + public void setup() { + subject = new SignSerializer(); + } + + @Test + public void serializeReturnsSemicolonSeparatedRepresentation() { + World world = mock(World.class); + Location location = new Location(world, 1, 2, 3); + String arenaId = "castle"; + String type = "join"; + String templateId = "status"; + when(world.getUID()).thenReturn(UUID.fromString("cafebabe-ea75-dead-beef-deadcafebabe")); + when(world.getName()).thenReturn("world"); + ArenaSign sign = new ArenaSign(location, templateId, arenaId, type); + + String result = subject.serialize(sign); + + String expected = "cafebabe-ea75-dead-beef-deadcafebabe;world;1;2;3;castle;join;status"; + assertThat(result, equalTo(expected)); + } + + @Test + public void deserializeThrowsIfLengthLessThan8() { + World world = mock(World.class); + String input = "1;2;3;4;5;6;7"; + + Assert.assertThrows( + "Invalid input; expected 8 parts, got 7", + IllegalArgumentException.class, + () -> subject.deserialize(input, world) + ); + } + + @Test + public void deserializeThrowsIfLengthGreaterThan8() { + World world = mock(World.class); + String input = "1;2;3;4;5;6;7;8;9"; + + Assert.assertThrows( + "Invalid input; expected 8 parts, got 9", + IllegalArgumentException.class, + () -> subject.deserialize(input, world) + ); + } + + @Test + public void deserializeThrowsIfWorldDoesNotMatchId() { + World world = mock(World.class); + when(world.getUID()).thenReturn(UUID.fromString("deadbeef-ea75-cafe-babe-deadcafebeef")); + String input = "wrong-id;world;1;2;3;castle;join;status"; + + Assert.assertThrows( + "World mismatch", + IllegalArgumentException.class, + () -> subject.deserialize(input, world) + ); + } + + @Test + public void deserializeReturnsIfWorldMatchesId() { + World world = mock(World.class); + when(world.getUID()).thenReturn(UUID.fromString("cafebabe-ea75-dead-beef-deadcafebabe")); + String input = "cafebabe-ea75-dead-beef-deadcafebabe;wrong-world;1;2;3;castle;join;status"; + + ArenaSign result = subject.deserialize(input, world); + + Location location = new Location(world, 1, 2, 3); + assertThat(result.location, equalTo(location)); + assertThat(result.arenaId, equalTo("castle")); + assertThat(result.type, equalTo("join")); + assertThat(result.templateId, equalTo("status")); + } + + @Test + public void deserializeSerializeReflexivity() { + String id = "cafebabe-ea75-dead-beef-deadcafebabe"; + String name = "world"; + World world = mock(World.class); + when(world.getName()).thenReturn(name); + when(world.getUID()).thenReturn(UUID.fromString(id)); + String line = id + ";" + name + ";1;2;3;castle;join;cool-sign"; + + String result = subject.serialize(subject.deserialize(line, world)); + + assertThat(result, equalTo(line)); + } + + @Test + public void serializeDeserializeReflexivity() { + String id = "cafebabe-ea75-dead-beef-deadcafebabe"; + World world = mock(World.class); + when(world.getUID()).thenReturn(UUID.fromString(id)); + Location location = new Location(world, 1, 2, 3); + ArenaSign sign = new ArenaSign(location, "cool-sign", "castle", "join"); + + ArenaSign result = subject.deserialize(subject.serialize(sign), world); + + assertThat(result.location, equalTo(location)); + assertThat(result.arenaId, equalTo("castle")); + assertThat(result.type, equalTo("join")); + assertThat(result.templateId, equalTo("cool-sign")); + } + + @Test + public void equalReturnsTrueIfStringsAreEqual() { + String line = "cafebabe-ea75-dead-beef-deadcafebabe;world;1;2;3;castle;join;status"; + + boolean result = subject.equal(line, line); + + assertThat(result, equalTo(true)); + } + + @Test + public void equalReturnsTrueIfWorldIdAndLocationMatch() { + String line1 = "cafebabe-ea75-dead-beef-deadcafebabe;right-world;1;2;3;jungle;leave;out"; + String line2 = "cafebabe-ea75-dead-beef-deadcafebabe;wrong-world;1;2;3;castle;join;status"; + + boolean result = subject.equal(line1, line2); + + assertThat(result, equalTo(true)); + } + + @Test + public void equalReturnsFalseIfOnlyWorldIdIsDifferent() { + String line1 = "cafebabe-ea75-dead-beef-deadcafebabe;right-world;1;2;3;castle;join;status"; + String line2 = "deadbeef-feed-cafe-babe-a70ff1cecafe;right-world;1;2;3;castle;join;status"; + + boolean result = subject.equal(line1, line2); + + assertThat(result, equalTo(false)); + } + + @Test + public void equalReturnsFalseIfOnlyCoordsAreDifferent() { + String line1 = "cafebabe-ea75-dead-beef-deadcafebabe;right-world;1;2;3;castle;join;status"; + String line2 = "cafebabe-ea75-dead-beef-deadcafebabe;right-world;4;5;6;castle;join;status"; + + boolean result = subject.equal(line1, line2); + + assertThat(result, equalTo(false)); + } + +} diff --git a/src/test/java/com/garbagemule/MobArena/signs/SignStoreTest.java b/src/test/java/com/garbagemule/MobArena/signs/SignStoreTest.java new file mode 100644 index 0000000..89cb325 --- /dev/null +++ b/src/test/java/com/garbagemule/MobArena/signs/SignStoreTest.java @@ -0,0 +1,154 @@ +package com.garbagemule.MobArena.signs; + +import org.bukkit.Location; +import org.bukkit.World; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.List; + +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.MatcherAssert.*; +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.StrictStubs.class) +public class SignStoreTest { + + SignStore subject; + + @Before + public void setup() { + subject = new SignStore(); + } + + @Test + public void findByLocationReturnsNullIfSignDoesNotExist() { + Location location = mock(Location.class); + + ArenaSign result = subject.findByLocation(location); + + assertThat(result, nullValue()); + } + + @Test + public void findByLocationReturnsSignIfItExists() { + Location location = mock(Location.class); + ArenaSign sign = new ArenaSign(location, "cool-sign", "castle", "join"); + + subject.add(sign); + ArenaSign result = subject.findByLocation(location); + + assertThat(result, equalTo(sign)); + } + + @Test + public void findByArenaIdReturnsEmptyListIfNoSignsMatch() { + Location location1 = mock(Location.class); + Location location2 = mock(Location.class); + ArenaSign sign1 = new ArenaSign(location1, "cool-sign", "castle", "join"); + ArenaSign sign2 = new ArenaSign(location2, "lame-sign", "island", "leave"); + + subject.add(sign1); + subject.add(sign2); + List result = subject.findByArenaId("jungle"); + + assertThat(result.isEmpty(), equalTo(true)); + } + + @Test + public void findByArenaIdReturnsOnlyMatchingSigns() { + Location location1 = mock(Location.class); + Location location2 = mock(Location.class); + Location location3 = mock(Location.class); + ArenaSign sign1 = new ArenaSign(location1, "cool-sign", "castle", "join"); + ArenaSign sign2 = new ArenaSign(location2, "lame-sign", "island", "leave"); + ArenaSign sign3 = new ArenaSign(location3, "very-sign", "jungle", "info"); + + subject.add(sign1); + subject.add(sign2); + subject.add(sign3); + List result = subject.findByArenaId("island"); + + assertThat(result.size(), equalTo(1)); + assertThat(result, hasItem(sign2)); + } + + @Test + public void removeReturnsNullIfSignDoesNotExist() { + Location location = mock(Location.class); + ArenaSign sign = new ArenaSign(location, "cool-sign", "castle", "join"); + + ArenaSign result = subject.removeByLocation(location); + + assertThat(result, nullValue()); + + subject.add(sign); + assertThat(subject.removeByLocation(location), equalTo(sign)); + + assertThat(subject.removeByLocation(location), nullValue()); + } + + @Test + public void removeReturnsSignIfItExists() { + Location location = mock(Location.class); + ArenaSign sign = new ArenaSign(location, "cool-sign", "castle", "join"); + + subject.add(sign); + ArenaSign result = subject.removeByLocation(location); + + assertThat(result, equalTo(sign)); + } + + @Test + public void removeReturnsNullIfSignWasRemoved() { + Location location = mock(Location.class); + ArenaSign sign = new ArenaSign(location, "cool-sign", "castle", "join"); + + subject.add(sign); + subject.removeByLocation(location); + ArenaSign result = subject.removeByLocation(location); + + assertThat(result, nullValue()); + } + + @Test + public void removeByWorldReturnsOnlyMatchingSigns() { + World world1 = mock(World.class); + World world2 = mock(World.class); + Location location1 = mock(Location.class); + Location location2 = mock(Location.class); + when(location1.getWorld()).thenReturn(world1); + when(location2.getWorld()).thenReturn(world2); + ArenaSign sign1 = new ArenaSign(location1, "lame-sign", "jungle", "info"); + ArenaSign sign2 = new ArenaSign(location2, "cool-sign", "castle", "join"); + + subject.add(sign1); + subject.add(sign2); + List result = subject.removeByWorld(world2); + + assertThat(result, not(hasItem(sign1))); + assertThat(result, hasItem(sign2)); + } + + @Test + public void removeByWorldReturnsEmptyListIfSignsWereRemoved() { + World world1 = mock(World.class); + World world2 = mock(World.class); + Location location1 = mock(Location.class); + Location location2 = mock(Location.class); + when(location1.getWorld()).thenReturn(world1); + when(location2.getWorld()).thenReturn(world2); + ArenaSign sign1 = new ArenaSign(location1, "lame-sign", "jungle", "info"); + ArenaSign sign2 = new ArenaSign(location2, "cool-sign", "castle", "join"); + + subject.add(sign1); + subject.add(sign2); + subject.removeByWorld(world2); + List result = subject.removeByWorld(world2); + + assertThat(result.isEmpty(), equalTo(true)); + } + +} diff --git a/src/test/java/com/garbagemule/MobArena/signs/SignWriterTest.java b/src/test/java/com/garbagemule/MobArena/signs/SignWriterTest.java new file mode 100644 index 0000000..688d4e2 --- /dev/null +++ b/src/test/java/com/garbagemule/MobArena/signs/SignWriterTest.java @@ -0,0 +1,161 @@ +package com.garbagemule.MobArena.signs; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.logging.Logger; + +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.StrictStubs.class) +public class SignWriterTest { + + SignFile file; + SignSerializer serializer; + Logger log; + + SignWriter subject; + + @Before + public void setup() { + file = mock(SignFile.class); + serializer = mock(SignSerializer.class); + log = mock(Logger.class); + + subject = new SignWriter( + file, + serializer, + log + ); + } + + @Test + public void writeCallsAppendWithSerializedSign() throws IOException { + ArenaSign sign = new ArenaSign(null, "cool-sign", "castle", "join"); + String line = "some arbitrary serialization"; + when(serializer.serialize(sign)).thenReturn(line); + + subject.write(sign); + + verify(file).append(line); + verify(file).save(); + } + + @Test + public void writeCallsEraseOnConflicts() throws IOException { + ArenaSign sign = new ArenaSign(null, "cool-sign", "castle", "join"); + String line = "some arbitrary serialization"; + String c1 = "some conflicting line"; + String c2 = "some non-conflicting line"; + String c3 = "some other conflicting line"; + when(serializer.serialize(sign)).thenReturn(line); + when(serializer.equal(c1, line)).thenReturn(true); + when(serializer.equal(c2, line)).thenReturn(false); + when(serializer.equal(c3, line)).thenReturn(true); + when(file.lines()).thenReturn(Arrays.asList(c1, c2, c3)); + + subject.write(sign); + + verify(file).erase(c1); + verify(file).erase(c3); + } + + @Test + public void writeLogsWarningOnConflicts() throws IOException { + ArenaSign sign = new ArenaSign(null, "cool-sign", "castle", "join"); + String line = "some arbitrary serialization"; + String c1 = "some conflicting line"; + String c2 = "some non-conflicting line"; + String c3 = "some other conflicting line"; + when(serializer.serialize(sign)).thenReturn(line); + when(serializer.equal(c1, line)).thenReturn(true); + when(serializer.equal(c2, line)).thenReturn(false); + when(serializer.equal(c3, line)).thenReturn(true); + when(file.lines()).thenReturn(Arrays.asList(c1, c2, c3)); + + subject.write(sign); + + verify(log, times(2)).warning(anyString()); + } + + @Test + public void successfulWriteLogsNothing() throws IOException { + ArenaSign sign = new ArenaSign(null, "cool-sign", "castle", "join"); + String line = "some arbitrary serialization"; + when(serializer.serialize(sign)).thenReturn(line); + + subject.write(sign); + + verifyNoInteractions(log); + } + + @Test + public void eraseCallsEraseWithIdentifiedLineOnly() throws IOException { + ArenaSign sign = new ArenaSign(null, "cool-sign", "castle", "join"); + String line = "right-id;wrong-name;some other stuff"; + String c1 = "wrong-id;wrong-name;some stuff"; + String c2 = "right-id;right-name;some other stuff"; + String c3 = "wrong-id;wrong-name;some more stuff"; + when(serializer.serialize(sign)).thenReturn(line); + when(serializer.equal(anyString(), anyString())).thenReturn(false); + when(serializer.equal(c2, line)).thenReturn(true); + when(file.lines()).thenReturn(Arrays.asList(c1, c2, c3)); + + subject.erase(sign); + + verify(file).erase(c2); + verify(file).save(); + verifyNoMoreInteractions(file); + } + + @Test + public void successfulEraseLogsNothing() throws IOException { + ArenaSign sign = new ArenaSign(null, "cool-sign", "castle", "join"); + String line = "right-id;wrong-name;some other stuff"; + String candiate1 = "wrong-id;wrong-name;some stuff"; + String candiate2 = "right-id;right-name;some other stuff"; + String candiate3 = "wrong-id;wrong-name;some more stuff"; + when(serializer.serialize(sign)).thenReturn(line); + when(serializer.equal(anyString(), anyString())).thenReturn(false); + when(serializer.equal(candiate2, line)).thenReturn(true); + when(file.lines()).thenReturn(Arrays.asList(candiate1, candiate2, candiate3)); + + subject.erase(sign); + + verifyNoInteractions(log); + } + + @Test + public void eraseBailsOnNoMatch() throws IOException { + ArenaSign sign = new ArenaSign(null, "cool-sign", "castle", "join"); + String line = "right-id;right-name;some right stuff"; + List candidates = Arrays.asList("a", "b", "c"); + when(serializer.serialize(sign)).thenReturn(line); + when(serializer.equal(anyString(), anyString())).thenReturn(false); + when(file.lines()).thenReturn(candidates); + + subject.erase(sign); + + verify(file, never()).erase(anyString()); + } + + @Test + public void eraseLogsWarningOnNoMatch() throws IOException { + ArenaSign sign = new ArenaSign(null, "cool-sign", "castle", "join"); + String line = "right-id;right-name;some right stuff"; + List candidates = Arrays.asList("a", "b", "c"); + when(serializer.serialize(sign)).thenReturn(line); + when(serializer.equal(anyString(), anyString())).thenReturn(false); + when(file.lines()).thenReturn(candidates); + + subject.erase(sign); + + verify(log).warning(anyString()); + } + +} diff --git a/src/test/java/com/garbagemule/MobArena/signs/StoresNewSignTest.java b/src/test/java/com/garbagemule/MobArena/signs/StoresNewSignTest.java deleted file mode 100644 index a1c3dfb..0000000 --- a/src/test/java/com/garbagemule/MobArena/signs/StoresNewSignTest.java +++ /dev/null @@ -1,126 +0,0 @@ -package com.garbagemule.MobArena.signs; - -import static org.hamcrest.MatcherAssert.*; -import static org.hamcrest.Matchers.*; -import static org.mockito.Mockito.*; - -import com.garbagemule.MobArena.framework.Arena; -import com.garbagemule.MobArena.framework.ArenaMaster; -import org.bukkit.Location; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.junit.MockitoJUnitRunner; - -import java.util.Optional; - -@SuppressWarnings("WeakerAccess") -@RunWith(MockitoJUnitRunner.StrictStubs.class) -public class StoresNewSignTest { - - ArenaMaster arenaMaster; - TemplateStore templateStore; - SignStore signStore; - SavesSignStore savesSignStore; - - StoresNewSign subject; - - @Rule - public ExpectedException exception = ExpectedException.none(); - - @Before - public void setup() { - arenaMaster = mock(ArenaMaster.class); - when(arenaMaster.getArenaWithName(any())) - .thenReturn(null); - - templateStore = mock(TemplateStore.class); - when(templateStore.findById(any())) - .thenReturn(Optional.empty()); - - signStore = mock(SignStore.class); - - savesSignStore = mock(SavesSignStore.class); - - subject = new StoresNewSign( - arenaMaster, - templateStore, - signStore, - savesSignStore - ); - } - - @Test - public void throwOnNonExistentArena() { - Location location = mock(Location.class); - String arenaId = "castle"; - exception.expect(IllegalArgumentException.class); - - subject.store(location, arenaId, "", ""); - } - - @Test - public void throwOnNonExistentTemplate() { - Location location = mock(Location.class); - String arenaId = "castle"; - Arena arena = mock(Arena.class); - when(arenaMaster.getArenaWithName(arenaId)) - .thenReturn(arena); - String templateId = "template"; - exception.expect(IllegalArgumentException.class); - - subject.store(location, arenaId, templateId, ""); - } - - @Test - public void throwOnNonInvalidSignType() { - Location location = mock(Location.class); - String arenaId = "castle"; - Arena arena = mock(Arena.class); - when(arenaMaster.getArenaWithName(arenaId)) - .thenReturn(arena); - String templateId = "a very nice template"; - Template template = template(templateId); - when(templateStore.findById(templateId)) - .thenReturn(Optional.of(template)); - String signType = "not a real sign type"; - exception.expect(IllegalArgumentException.class); - - subject.store(location, arenaId, templateId, signType); - } - - @Test - public void storesSignAndWritesToDisk() { - Location location = mock(Location.class); - String arenaId = "castle"; - Arena arena = mock(Arena.class); - when(arenaMaster.getArenaWithName(arenaId)) - .thenReturn(arena); - String templateId = "a very nice template"; - Template template = template(templateId); - when(templateStore.findById(templateId)) - .thenReturn(Optional.of(template)); - String signType = "join"; - - subject.store(location, arenaId, templateId, signType); - - ArgumentCaptor captor = ArgumentCaptor.forClass(ArenaSign.class); - verify(signStore).store(captor.capture()); - verify(savesSignStore).save(signStore); - ArenaSign sign = captor.getValue(); - assertThat(sign.location, equalTo(location)); - assertThat(sign.arenaId, equalTo(arenaId)); - assertThat(sign.templateId, equalTo(templateId)); - assertThat(sign.type, equalTo(signType)); - } - - private Template template(String id) { - return new Template.Builder(id) - .withBase(new String[]{"", "", "", ""}) - .build(); - } - -}