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(); - } - -}