From 37de1e66e98e671d93b6237bdf212efa0478af94 Mon Sep 17 00:00:00 2001 From: Andreas Troelsen Date: Sun, 22 Apr 2018 13:18:38 +0200 Subject: [PATCH] Use Things API for class armor. Swaps out the hard dependecy on ItemStack for class armor, such that the ThingManager - and thus every ThingParser registered within - has a chance at providing "armor" for a class. To distinguish armor from items, ArenaMasterImpl prepends "armor:" or slot-specific prefixes to these values before passing them to the ThingManager. This makes it possible to distinguish between items and armor on the thing parser end if applicable - it doesn't make sense for money and command parsers, but they will just end up skipping these values. The ItemStackThingParser is given a bit of an overhaul. It now depends internally on ItemStack parsers, which can be registered via the ThingManager. This allows custom plugins to hook into the ItemStack-specific part of the ItemStackThingParser to avoid having to duplicate code. Plugins that just want to provide ItemStacks can use this parser approach, and plugins that want to provide more abstract Things can use the Thing parser approach. --- .../com/garbagemule/MobArena/ArenaClass.java | 66 ++++------ .../garbagemule/MobArena/ArenaMasterImpl.java | 73 +++++++---- .../MobArena/things/BootsThing.java | 27 ++++ .../MobArena/things/ChestplateThing.java | 27 ++++ .../MobArena/things/Equippable.java | 78 ++++++++++++ .../MobArena/things/HelmetThing.java | 27 ++++ .../MobArena/things/ItemStackParser.java | 19 +++ .../MobArena/things/ItemStackThing.java | 4 + .../MobArena/things/ItemStackThingParser.java | 65 +++++++++- .../MobArena/things/LeggingsThing.java | 27 ++++ .../MobArena/things/OffHandThing.java | 27 ++++ .../MobArena/things/ThingManager.java | 23 +++- .../things/ItemStackThingParserTest.java | 119 ++++++++++++++++++ 13 files changed, 507 insertions(+), 75 deletions(-) create mode 100644 src/main/java/com/garbagemule/MobArena/things/BootsThing.java create mode 100644 src/main/java/com/garbagemule/MobArena/things/ChestplateThing.java create mode 100644 src/main/java/com/garbagemule/MobArena/things/Equippable.java create mode 100644 src/main/java/com/garbagemule/MobArena/things/HelmetThing.java create mode 100644 src/main/java/com/garbagemule/MobArena/things/ItemStackParser.java create mode 100644 src/main/java/com/garbagemule/MobArena/things/LeggingsThing.java create mode 100644 src/main/java/com/garbagemule/MobArena/things/OffHandThing.java create mode 100644 src/test/java/com/garbagemule/MobArena/things/ItemStackThingParserTest.java diff --git a/src/main/java/com/garbagemule/MobArena/ArenaClass.java b/src/main/java/com/garbagemule/MobArena/ArenaClass.java index b9b195b..e508d25 100644 --- a/src/main/java/com/garbagemule/MobArena/ArenaClass.java +++ b/src/main/java/com/garbagemule/MobArena/ArenaClass.java @@ -25,8 +25,8 @@ import java.util.stream.IntStream; public class ArenaClass { private String configName, lowercaseName; - private ItemStack helmet, chestplate, leggings, boots, offhand; - private List armor; + private Thing helmet, chestplate, leggings, boots, offhand; + private List armor; private List items; private Map perms; private Map lobbyperms; @@ -71,42 +71,42 @@ public class ArenaClass /** * Set the helmet slot for the class. - * @param helmet an item + * @param helmet a Thing */ - public void setHelmet(ItemStack helmet) { + public void setHelmet(Thing helmet) { this.helmet = helmet; } /** * Set the chestplate slot for the class. - * @param chestplate an item + * @param chestplate a Thing */ - public void setChestplate(ItemStack chestplate) { + public void setChestplate(Thing chestplate) { this.chestplate = chestplate; } /** * Set the leggings slot for the class. - * @param leggings an item + * @param leggings a Thing */ - public void setLeggings(ItemStack leggings) { + public void setLeggings(Thing leggings) { this.leggings = leggings; } /** * Set the boots slot for the class. - * @param boots an item + * @param boots a Thing */ - public void setBoots(ItemStack boots) { + public void setBoots(Thing boots) { this.boots = boots; } /** * Set the off-hand slot for the class. - * @param offHand + * @param offhand a Thing */ - public void setOffHand(ItemStack offHand) { - this.offhand = offHand; + public void setOffHand(Thing offhand) { + this.offhand = offhand; } /** @@ -131,9 +131,9 @@ public class ArenaClass /** * Replace the current armor list with the given list. - * @param armor a list of items + * @param armor a list of Things */ - public void setArmor(List armor) { + public void setArmor(List armor) { this.armor = armor; } @@ -152,36 +152,14 @@ public class ArenaClass items.forEach(item -> item.giveTo(p)); // Check for legacy armor-node items - if (!armor.isEmpty()) { - for (ItemStack piece : armor) { - ArmorType type = ArmorType.getType(piece); - if (type == null) continue; - - switch (type) { - case HELMET: - inv.setHelmet(piece); - break; - case CHESTPLATE: - inv.setChestplate(piece); - break; - case LEGGINGS: - inv.setLeggings(piece); - break; - case BOOTS: - inv.setBoots(piece); - break; - default: - break; - } - } - } - + armor.forEach(thing -> thing.giveTo(p)); + // Check type specifics. - if (helmet != null) inv.setHelmet(helmet); - if (chestplate != null) inv.setChestplate(chestplate); - if (leggings != null) inv.setLeggings(leggings); - if (boots != null) inv.setBoots(boots); - if (offhand != null) inv.setItemInOffHand(offhand); + if (helmet != null) helmet.giveTo(p); + if (chestplate != null) chestplate.giveTo(p); + if (leggings != null) leggings.giveTo(p); + if (boots != null) boots.giveTo(p); + if (offhand != null) offhand.giveTo(p); } /** diff --git a/src/main/java/com/garbagemule/MobArena/ArenaMasterImpl.java b/src/main/java/com/garbagemule/MobArena/ArenaMasterImpl.java index 23c7f91..739f34f 100644 --- a/src/main/java/com/garbagemule/MobArena/ArenaMasterImpl.java +++ b/src/main/java/com/garbagemule/MobArena/ArenaMasterImpl.java @@ -32,6 +32,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.function.Consumer; import java.util.stream.Collectors; public class ArenaMasterImpl implements ArenaMaster @@ -330,33 +331,8 @@ public class ArenaMasterImpl implements ArenaMaster // Load items loadClassItems(section, arenaClass); - // And the legacy armor-node - String armor = section.getString("armor", ""); - if (!armor.equals("")) { - List stacks = ItemParser.parseItems(armor); - arenaClass.setArmor(stacks); - } - - // Get armor strings - String head = section.getString("helmet", null); - String chest = section.getString("chestplate", null); - String legs = section.getString("leggings", null); - String feet = section.getString("boots", null); - String off = section.getString("offhand", null); - - // Parse to ItemStacks - ItemStack helmet = ItemParser.parseItem(head); - ItemStack chestplate = ItemParser.parseItem(chest); - ItemStack leggings = ItemParser.parseItem(legs); - ItemStack boots = ItemParser.parseItem(feet); - ItemStack offhand = ItemParser.parseItem(off); - - // Set in ArenaClass - arenaClass.setHelmet(helmet); - arenaClass.setChestplate(chestplate); - arenaClass.setLeggings(leggings); - arenaClass.setBoots(boots); - arenaClass.setOffHand(offhand); + // Load armor + loadClassArmor(section, arenaClass); // Per-class permissions loadClassPermissions(arenaClass, section); @@ -390,6 +366,49 @@ public class ArenaMasterImpl implements ArenaMaster arenaClass.setItems(things); } + private void loadClassArmor(ConfigurationSection section, ArenaClass arenaClass) { + // Legacy armor node + loadClassArmorLegacyNode(section, arenaClass); + + // Specific armor pieces + loadClassArmorPiece(section, "helmet", arenaClass::setHelmet); + loadClassArmorPiece(section, "chestplate", arenaClass::setChestplate); + loadClassArmorPiece(section, "leggings", arenaClass::setLeggings); + loadClassArmorPiece(section, "boots", arenaClass::setBoots); + loadClassArmorPiece(section, "offhand", arenaClass::setOffHand); + } + + private void loadClassArmorLegacyNode(ConfigurationSection section, ArenaClass arenaClass) { + List armor = section.getStringList("armor"); + if (armor == null || armor.isEmpty()) { + String value = section.getString("armor", ""); + armor = Arrays.asList(value.split(",")); + } + + // Prepend "armor:" for the armor thing parser + List things = armor.stream() + .map(String::trim) + .map(s -> "armor:" + s) + .map(plugin.getThingManager()::parse) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + arenaClass.setArmor(things); + } + + private void loadClassArmorPiece(ConfigurationSection section, String slot, Consumer setter) { + String value = section.getString(slot, null); + if (value == null) { + return; + } + // Prepend the slot name for the item parser + Thing thing = plugin.getThingManager().parse(slot + ":" + value); + if (thing == null) { + return; + } + setter.accept(thing); + } + private void loadClassPermissions(ArenaClass arenaClass, ConfigurationSection section) { List perms = section.getStringList("permissions"); if (perms.isEmpty()) return; diff --git a/src/main/java/com/garbagemule/MobArena/things/BootsThing.java b/src/main/java/com/garbagemule/MobArena/things/BootsThing.java new file mode 100644 index 0000000..5c555a7 --- /dev/null +++ b/src/main/java/com/garbagemule/MobArena/things/BootsThing.java @@ -0,0 +1,27 @@ +package com.garbagemule.MobArena.things; + +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +public class BootsThing extends ItemStackThing { + public BootsThing(ItemStack stack) { + super(stack); + } + + @Override + public boolean giveTo(Player player) { + player.getInventory().setBoots(super.getItemStack()); + return true; + } + + @Override + public boolean takeFrom(Player player) { + player.getInventory().setBoots(null); + return true; + } + + @Override + public boolean heldBy(Player player) { + return super.getItemStack().equals(player.getInventory().getBoots()); + } +} diff --git a/src/main/java/com/garbagemule/MobArena/things/ChestplateThing.java b/src/main/java/com/garbagemule/MobArena/things/ChestplateThing.java new file mode 100644 index 0000000..ea2ef0e --- /dev/null +++ b/src/main/java/com/garbagemule/MobArena/things/ChestplateThing.java @@ -0,0 +1,27 @@ +package com.garbagemule.MobArena.things; + +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +public class ChestplateThing extends ItemStackThing { + public ChestplateThing(ItemStack stack) { + super(stack); + } + + @Override + public boolean giveTo(Player player) { + player.getInventory().setChestplate(super.getItemStack()); + return true; + } + + @Override + public boolean takeFrom(Player player) { + player.getInventory().setChestplate(null); + return true; + } + + @Override + public boolean heldBy(Player player) { + return super.getItemStack().equals(player.getInventory().getChestplate()); + } +} diff --git a/src/main/java/com/garbagemule/MobArena/things/Equippable.java b/src/main/java/com/garbagemule/MobArena/things/Equippable.java new file mode 100644 index 0000000..9293298 --- /dev/null +++ b/src/main/java/com/garbagemule/MobArena/things/Equippable.java @@ -0,0 +1,78 @@ +package com.garbagemule.MobArena.things; + +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; + +import java.util.EnumSet; + +class Equippable { + @FunctionalInterface + interface Wrapper { + ItemStackThing wrap(ItemStack stack); + } + + private static EnumSet helmets = EnumSet.of( + Material.LEATHER_HELMET, + Material.IRON_HELMET, + Material.CHAINMAIL_HELMET, + Material.GOLD_HELMET, + Material.DIAMOND_HELMET + ); + private static EnumSet chestplates = EnumSet.of( + Material.LEATHER_CHESTPLATE, + Material.IRON_CHESTPLATE, + Material.CHAINMAIL_CHESTPLATE, + Material.GOLD_CHESTPLATE, + Material.DIAMOND_CHESTPLATE + ); + private static EnumSet leggings = EnumSet.of( + Material.LEATHER_LEGGINGS, + Material.IRON_LEGGINGS, + Material.CHAINMAIL_LEGGINGS, + Material.GOLD_LEGGINGS, + Material.DIAMOND_LEGGINGS + ); + private static EnumSet boots = EnumSet.of( + Material.LEATHER_BOOTS, + Material.IRON_BOOTS, + Material.CHAINMAIL_BOOTS, + Material.GOLD_BOOTS, + Material.DIAMOND_BOOTS + ); + + static Wrapper getWrapperByPrefix(String prefix) { + if (prefix.equals("helmet")) { + return HelmetThing::new; + } + if (prefix.equals("chestplate")) { + return ChestplateThing::new; + } + if (prefix.equals("leggings")) { + return LeggingsThing::new; + } + if (prefix.equals("boots")) { + return BootsThing::new; + } + if (prefix.equals("offhand")) { + return OffHandThing::new; + } + return null; + } + + static Wrapper guessWrapperFromItemStack(ItemStack stack) { + Material type = stack.getType(); + if (helmets.contains(type)) { + return HelmetThing::new; + } + if (chestplates.contains(type)) { + return ChestplateThing::new; + } + if (leggings.contains(type)) { + return LeggingsThing::new; + } + if (boots.contains(type)) { + return BootsThing::new; + } + return null; + } +} diff --git a/src/main/java/com/garbagemule/MobArena/things/HelmetThing.java b/src/main/java/com/garbagemule/MobArena/things/HelmetThing.java new file mode 100644 index 0000000..2a8628a --- /dev/null +++ b/src/main/java/com/garbagemule/MobArena/things/HelmetThing.java @@ -0,0 +1,27 @@ +package com.garbagemule.MobArena.things; + +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +public class HelmetThing extends ItemStackThing { + public HelmetThing(ItemStack stack) { + super(stack); + } + + @Override + public boolean giveTo(Player player) { + player.getInventory().setHelmet(super.getItemStack()); + return true; + } + + @Override + public boolean takeFrom(Player player) { + player.getInventory().setHelmet(null); + return true; + } + + @Override + public boolean heldBy(Player player) { + return super.getItemStack().equals(player.getInventory().getHelmet()); + } +} diff --git a/src/main/java/com/garbagemule/MobArena/things/ItemStackParser.java b/src/main/java/com/garbagemule/MobArena/things/ItemStackParser.java new file mode 100644 index 0000000..3bcf22a --- /dev/null +++ b/src/main/java/com/garbagemule/MobArena/things/ItemStackParser.java @@ -0,0 +1,19 @@ +package com.garbagemule.MobArena.things; + +import org.bukkit.inventory.ItemStack; + +/** + * An ItemStack parser takes a string as input and returns either an instance + * of {@link ItemStack} or null. + */ +@FunctionalInterface +public interface ItemStackParser { + /** + * Parse the given string, returning an {@link ItemStack} instance on + * success, otherwise null. + * + * @param s a string to parse + * @return an instance of {@link ItemStack}, or null + */ + ItemStack parse(String s); +} diff --git a/src/main/java/com/garbagemule/MobArena/things/ItemStackThing.java b/src/main/java/com/garbagemule/MobArena/things/ItemStackThing.java index 28c37c0..2880816 100644 --- a/src/main/java/com/garbagemule/MobArena/things/ItemStackThing.java +++ b/src/main/java/com/garbagemule/MobArena/things/ItemStackThing.java @@ -26,6 +26,10 @@ public class ItemStackThing implements Thing { return player.getInventory().containsAtLeast(stack, stack.getAmount()); } + ItemStack getItemStack() { + return stack; + } + @Override public String toString() { String name = getName(); diff --git a/src/main/java/com/garbagemule/MobArena/things/ItemStackThingParser.java b/src/main/java/com/garbagemule/MobArena/things/ItemStackThingParser.java index 2a9fbd1..f63a85e 100644 --- a/src/main/java/com/garbagemule/MobArena/things/ItemStackThingParser.java +++ b/src/main/java/com/garbagemule/MobArena/things/ItemStackThingParser.java @@ -1,15 +1,78 @@ package com.garbagemule.MobArena.things; +import com.garbagemule.MobArena.things.Equippable.Wrapper; import com.garbagemule.MobArena.util.ItemParser; import org.bukkit.inventory.ItemStack; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + class ItemStackThingParser implements ThingParser { + private List parsers; + + ItemStackThingParser() { + this.parsers = new ArrayList<>(); + } + + public void register(ItemStackParser parser) { + parsers.add(parser); + } + @Override public ItemStackThing parse(String s) { - ItemStack stack = ItemParser.parseItem(s, false); + String[] parts = s.split(":", 2); + if (parts.length == 1) { + return genericItem(s); + } + + String prefix = parts[0]; + String rest = parts[1]; + + if (prefix.equals("armor")) { + return genericArmor(rest); + } + + return specificSlot(s, prefix, rest); + } + + private ItemStackThing genericItem(String s) { + ItemStack stack = parseItemStack(s); if (stack == null) { return null; } return new ItemStackThing(stack); } + + private ItemStackThing genericArmor(String s) { + ItemStack stack = parseItemStack(s); + if (stack == null) { + return null; + } + Wrapper wrapper = Equippable.guessWrapperFromItemStack(stack); + if (wrapper == null) { + return new ItemStackThing(stack); + } + return wrapper.wrap(stack); + } + + private ItemStackThing specificSlot(String s, String prefix, String rest) { + Wrapper wrapper = Equippable.getWrapperByPrefix(prefix); + if (wrapper == null) { + return genericItem(s); + } + ItemStack stack = parseItemStack(rest); + if (stack == null) { + return null; + } + return wrapper.wrap(stack); + } + + private ItemStack parseItemStack(String s) { + return parsers.stream() + .map(p -> p.parse(s)) + .filter(Objects::nonNull) + .findFirst() + .orElseGet(() -> ItemParser.parseItem(s, false)); + } } diff --git a/src/main/java/com/garbagemule/MobArena/things/LeggingsThing.java b/src/main/java/com/garbagemule/MobArena/things/LeggingsThing.java new file mode 100644 index 0000000..50e5b8b --- /dev/null +++ b/src/main/java/com/garbagemule/MobArena/things/LeggingsThing.java @@ -0,0 +1,27 @@ +package com.garbagemule.MobArena.things; + +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +public class LeggingsThing extends ItemStackThing { + public LeggingsThing(ItemStack stack) { + super(stack); + } + + @Override + public boolean giveTo(Player player) { + player.getInventory().setLeggings(super.getItemStack()); + return true; + } + + @Override + public boolean takeFrom(Player player) { + player.getInventory().setLeggings(null); + return true; + } + + @Override + public boolean heldBy(Player player) { + return super.getItemStack().equals(player.getInventory().getLeggings()); + } +} diff --git a/src/main/java/com/garbagemule/MobArena/things/OffHandThing.java b/src/main/java/com/garbagemule/MobArena/things/OffHandThing.java new file mode 100644 index 0000000..9a4f2b8 --- /dev/null +++ b/src/main/java/com/garbagemule/MobArena/things/OffHandThing.java @@ -0,0 +1,27 @@ +package com.garbagemule.MobArena.things; + +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +public class OffHandThing extends ItemStackThing { + public OffHandThing(ItemStack stack) { + super(stack); + } + + @Override + public boolean giveTo(Player player) { + player.getInventory().setItemInOffHand(super.getItemStack()); + return true; + } + + @Override + public boolean takeFrom(Player player) { + player.getInventory().setItemInOffHand(null); + return true; + } + + @Override + public boolean heldBy(Player player) { + return super.getItemStack().equals(player.getInventory().getItemInOffHand()); + } +} diff --git a/src/main/java/com/garbagemule/MobArena/things/ThingManager.java b/src/main/java/com/garbagemule/MobArena/things/ThingManager.java index 730721f..4dc72df 100644 --- a/src/main/java/com/garbagemule/MobArena/things/ThingManager.java +++ b/src/main/java/com/garbagemule/MobArena/things/ThingManager.java @@ -7,12 +7,17 @@ import java.util.List; public class ThingManager implements ThingParser { private List parsers; + private ItemStackThingParser items; - public ThingManager(MobArena plugin) { + public ThingManager(MobArena plugin, ItemStackThingParser parser) { parsers = new ArrayList<>(); parsers.add(new CommandThingParser()); parsers.add(new MoneyThingParser(plugin)); - parsers.add(new ItemStackThingParser()); + items = parser; + } + + public ThingManager(MobArena plugin) { + this(plugin, new ItemStackThingParser()); } /** @@ -43,6 +48,18 @@ public class ThingManager implements ThingParser { register(parser, false); } + /** + * Register a new ItemStack parser in the manager. + *

+ * The provided parser will be invoked if no other thing parsers return a + * non-null result, and if all other ItemStack parsers also return null. + * + * @param parser an ItemStack parser, non-null + */ + public void register(ItemStackParser parser) { + items.register(parser); + } + @Override public Thing parse(String s) { for (ThingParser parser : parsers) { @@ -51,6 +68,6 @@ public class ThingManager implements ThingParser { return thing; } } - return null; + return items.parse(s); } } diff --git a/src/test/java/com/garbagemule/MobArena/things/ItemStackThingParserTest.java b/src/test/java/com/garbagemule/MobArena/things/ItemStackThingParserTest.java new file mode 100644 index 0000000..4daefba --- /dev/null +++ b/src/test/java/com/garbagemule/MobArena/things/ItemStackThingParserTest.java @@ -0,0 +1,119 @@ +package com.garbagemule.MobArena.things; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.junit.Before; +import org.junit.Test; +import org.mockito.InOrder; + +public class ItemStackThingParserTest { + + private ItemStackThingParser subject; + + @Before + public void setup() { + subject = new ItemStackThingParser(); + } + + @Test + public void emptyStringReturnsNull() { + ItemStackThing result = subject.parse(""); + + assertThat(result, is(nullValue())); + } + + @Test + public void gibberishReturnsNull() { + ItemStackThing result = subject.parse("I can't believe you've done this"); + + assertThat(result, is(nullValue())); + } + + @Test + public void fiveDirt() { + ItemStackThing result = subject.parse("dirt:5"); + + // ItemStack's equals() method does naughty things, so verify manually + ItemStack stack = result.getItemStack(); + assertThat(stack.getType(), equalTo(Material.DIRT)); + assertThat(stack.getAmount(), equalTo(5)); + } + + @Test + public void bareLederhosenReturnsGenericItemStackThing() { + ItemStackThing result = subject.parse("leather_leggings"); + + assertThat(result.getClass(), equalTo(ItemStackThing.class)); + } + + @Test + public void prefixedIronChestplateReturnsLeggingsThing() { + ItemStackThing result = subject.parse("leggings:leather_leggings"); + + assertThat(result.getClass(), equalTo(LeggingsThing.class)); + } + + @Test + public void genericArmorPrefixGuessesChestplateThing() { + ItemStackThing result = subject.parse("armor:iron_chestplate"); + + assertThat(result.getClass(), equalTo(ChestplateThing.class)); + } + + @Test + public void returnsNonNullCustomParserResult() { + String input = "something:soft"; + ItemStack stack = new ItemStack(Material.SPONGE, 3); + ItemStackParser parser = mock(ItemStackParser.class); + when(parser.parse(input)).thenReturn(stack); + subject.register(parser); + + ItemStackThing result = subject.parse(input); + + assertThat(result.getItemStack(), equalTo(stack)); + } + + @Test + public void customParsersInvokedInOrder() { + String input = "unicorns and rainbows"; + ItemStackParser first = mock(ItemStackParser.class); + ItemStackParser second = mock(ItemStackParser.class); + subject.register(first); + subject.register(second); + + subject.parse(input); + + InOrder order = inOrder(first, second); + order.verify(first).parse(input); + order.verify(second).parse(input); + } + + @Test + public void customParsersInvokedUntilNonNullResult() { + String input = "unicorns and rainbows"; + ItemStackParser first = mock(ItemStackParser.class); + ItemStackParser second = mock(ItemStackParser.class); + ItemStackParser third = mock(ItemStackParser.class); + ItemStack stack = new ItemStack(Material.GLOWSTONE, 9); + when(second.parse(input)).thenReturn(stack); + subject.register(first); + subject.register(second); + subject.register(third); + + subject.parse(input); + + verify(first).parse(input); + verifyZeroInteractions(third); + } + +}