diff --git a/src/main/java/world/bentobox/bentobox/util/ItemParser.java b/src/main/java/world/bentobox/bentobox/util/ItemParser.java
index f483c3d2e..c315aa897 100644
--- a/src/main/java/world/bentobox/bentobox/util/ItemParser.java
+++ b/src/main/java/world/bentobox/bentobox/util/ItemParser.java
@@ -1,16 +1,14 @@
package world.bentobox.bentobox.util;
-import java.lang.reflect.Field;
-import java.util.Arrays;
-import java.util.MissingFormatArgumentException;
-import java.util.Optional;
-import java.util.UUID;
+import java.net.URL;
+import java.util.*;
import org.bukkit.Bukkit;
import org.bukkit.DyeColor;
import org.bukkit.Material;
import org.bukkit.block.banner.Pattern;
import org.bukkit.block.banner.PatternType;
+import org.bukkit.inventory.ItemFactory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.BannerMeta;
import org.bukkit.inventory.meta.Damageable;
@@ -19,10 +17,12 @@
import org.bukkit.inventory.meta.SkullMeta;
import org.bukkit.potion.PotionData;
import org.bukkit.potion.PotionType;
+import org.bukkit.profile.PlayerProfile;
import org.eclipse.jdt.annotation.Nullable;
-import com.mojang.authlib.GameProfile;
-import com.mojang.authlib.properties.Property;
+import com.google.common.base.Enums;
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
import world.bentobox.bentobox.BentoBox;
@@ -55,6 +55,33 @@ public static ItemStack parse(String text) {
*/
@Nullable
public static ItemStack parse(@Nullable String text, @Nullable ItemStack defaultItemStack) {
+ if (text == null || text.isBlank()) {
+ // Text does not exist or is empty.
+ return defaultItemStack;
+ }
+
+ ItemStack returnValue;
+
+ try {
+ // Check if item can be parsed using bukkit item factory.
+ returnValue = Bukkit.getItemFactory().createItemStack(text);
+ }
+ catch (IllegalArgumentException exception) {
+ returnValue = ItemParser.parseOld(text, defaultItemStack);
+ }
+
+ return returnValue;
+ }
+
+
+ /**
+ * Parse given string to ItemStack.
+ * @param text String value of item stack.
+ * @param defaultItemStack Material that should be returned if parsing failed.
+ * @return ItemStack of parsed item or defaultItemStack.
+ */
+ @Nullable
+ private static ItemStack parseOld(@Nullable String text, @Nullable ItemStack defaultItemStack) {
if (text == null || text.isBlank()) {
// Text does not exist or is empty.
@@ -113,22 +140,25 @@ else if (part.length == 2) {
returnValue = parseItemDurabilityAndQuantity(part);
}
- if (returnValue != null
- // If wrapper is just for code-style null-pointer checks.
- && customModelData != null) {
- return customValue(returnValue, customModelData);
+ // Update item meta with custom data model.
+ if (returnValue != null && customModelData != null) {
+ ItemParser.setCustomModelData(returnValue, customModelData);
}
-
} catch (Exception exception) {
BentoBox.getInstance().logError("Could not parse item " + text + " " + exception.getLocalizedMessage());
returnValue = defaultItemStack;
}
- return returnValue;
+ return returnValue;
}
- private static @Nullable ItemStack customValue(ItemStack returnValue, Integer customModelData) {
+ /**
+ * This method assigns custom model data to the item stack.
+ * @param returnValue Item stack that should be updated.
+ * @param customModelData Integer value of custom model data.
+ */
+ private static void setCustomModelData(ItemStack returnValue, Integer customModelData) {
// We have custom data model. Now assign it to the item-stack.
ItemMeta itemMeta = returnValue.getItemMeta();
@@ -138,8 +168,9 @@ else if (part.length == 2) {
// Update meta to the return item.
returnValue.setItemMeta(itemMeta);
}
- return null;
}
+
+
/**
* This method parses array of 2 items into an item stack.
* First array element is material, while second array element is integer, that represents item count.
@@ -197,8 +228,10 @@ private static ItemStack parseItemDurabilityAndQuantity(String[] part) {
* }
* @param part String array that contains 6 elements.
* @return Potion with given properties.
+ * @deprecated due to the spigot potion changes.
*/
- private static ItemStack parsePotion(String[] part) {
+ @Deprecated
+ private static ItemStack parsePotionOld(String[] part) {
if (part.length != 6) {
throw new MissingFormatArgumentException("Potion parsing requires 6 parts.");
}
@@ -235,6 +268,61 @@ private static ItemStack parsePotion(String[] part) {
}
+ /**
+ * This method parses array of 6 items into an item stack.
+ * Format:
+ *
{@code
+ * POTION::QTY
+ * }
+ * Example:
+ * {@code
+ * POTION:STRENGTH:1
+ * }
+ * @link Potion Type
+ * @param part String array that contains 3 elements.
+ * @return Potion with given properties.
+ */
+ private static ItemStack parsePotion(String[] part) {
+ if (part.length == 6) {
+ BentoBox.getInstance().logWarning("The old potion parsing detected for " + part[0] +
+ ". Please update your configs, as SPIGOT changed potion types.");
+ return parsePotionOld(part);
+ }
+
+ if (part.length != 3) {
+ throw new MissingFormatArgumentException("Potion parsing requires 3 parts.");
+ }
+
+ /*
+ # Format POTION::QTY
+ # Potion Type can be found out in: https://hub.spigotmc.org/javadocs/spigot/org/bukkit/potion/PotionType.html
+ # Examples:
+ # POTION:STRENGTH:1
+ # POTION:INSTANT_DAMAGE:2
+ # POTION:JUMP:1
+ # POTION:WEAKNESS:1 - any weakness potion
+ */
+
+ Material material = Material.matchMaterial(part[0]);
+
+ if (material == null) {
+ BentoBox.getInstance().logWarning("Could not parse potion item " + part[0] + " so using a regular potion.");
+ material = Material.POTION;
+ }
+
+ ItemStack result = new ItemStack(material, Integer.parseInt(part[2]));
+
+ if (result.getItemMeta() instanceof PotionMeta meta) {
+ PotionType potionType = Enums.getIfPresent(PotionType.class, part[1].toUpperCase(Locale.ENGLISH)).
+ or(PotionType.WATER);
+ meta.setBasePotionType(potionType);
+ result.setItemMeta(meta);
+ }
+
+ return result;
+ }
+
+
/**
* This method parses array of multiple elements for the Banner.
* @param part String array that contains at least 2 elements.
@@ -298,39 +386,62 @@ private static ItemStack parsePlayerHead(String[] part) {
// Set correct Skull texture
try {
- SkullMeta meta = (SkullMeta) playerHead.getItemMeta();
-
- if (part[1].length() < 17) {
- // Minecraft player names are in length between 3 and 16 chars.
- meta.setOwner(part[1]);
- } else if (part[1].length() == 32) {
- // trimmed UUID length are 32 chars.
- meta.setOwningPlayer(Bukkit.getOfflinePlayer(
- UUID.fromString(part[1].replaceAll("(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})", "$1-$2-$3-$4-$5"))));
- } else if (part[1].length() == 36) {
- // full UUID length are 36 chars.
- meta.setOwningPlayer(Bukkit.getOfflinePlayer(UUID.fromString(part[1])));
- } else {
- // If chars are more than 36, apparently it is base64 encoded texture.
- GameProfile profile = new GameProfile(UUID.randomUUID(), "");
- profile.getProperties().put("textures", new Property("textures", part[1]));
-
- // Null pointer will be caught and ignored.
- Field profileField = meta.getClass().getDeclaredField("profile");
- profileField.setAccessible(true);
- profileField.set(meta, profile);
- }
+ if (playerHead.getItemMeta() instanceof SkullMeta meta)
+ {
+ PlayerProfile profile;
+
+ if (part[1].length() < 17) {
+ // Minecraft player names are in length between 3 and 16 chars.
+ profile = Bukkit.createPlayerProfile(part[1]);
+ } else if (part[1].length() == 32) {
+ // trimmed UUID length are 32 chars.
+ profile = Bukkit.createPlayerProfile(UUID.fromString(part[1].replaceAll("(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})", "$1-$2-$3-$4-$5")));
+ } else if (part[1].length() == 36) {
+ // full UUID length are 36 chars.
+ profile = Bukkit.createPlayerProfile(UUID.fromString(part[1]));
+ } else {
+ // If chars are more than 36, apparently it is base64 encoded texture.
+ profile = Bukkit.createPlayerProfile(UUID.randomUUID(), "");
+ profile.getTextures().setSkin(ItemParser.getSkinURLFromBase64(part[1]));
+ }
- // Apply new meta to the item.
- playerHead.setItemMeta(meta);
+ // Apply item meta.
+ meta.setOwnerProfile(profile);
+ playerHead.setItemMeta(meta);
+ }
} catch (Exception ignored) {
- // Ignored
+ // Could not parse player head.
+ BentoBox.getInstance().logError("Could not parse player head item " + part[1] + " so using a Steve head.");
}
return playerHead;
}
+ /**
+ * This method parses base64 encoded string into URL.
+ * @param base64 Base64 encoded string.
+ * @return URL of the skin.
+ */
+ private static URL getSkinURLFromBase64(String base64) {
+ /*
+ * Base64 encoded string is in format: { "timestamp": 0, "profileId": "UUID",
+ * "profileName": "USERNAME", "textures": { "SKIN": { "url":
+ * "https://textures.minecraft.net/texture/TEXTURE_ID" }, "CAPE": { "url":
+ * "https://textures.minecraft.net/texture/TEXTURE_ID" } } }
+ */
+ try {
+ String decoded = new String(Base64.getDecoder().decode(base64));
+ JsonObject json = new Gson().fromJson(decoded, JsonObject.class);
+ String url = json.getAsJsonObject("textures").getAsJsonObject("SKIN").get("url").getAsString();
+ return new URL(url);
+ }
+ catch (Exception e) {
+ return null;
+ }
+ }
+
+
/**
* Check if given sting is an integer.
* @param string Value that must be checked.
diff --git a/src/test/java/world/bentobox/bentobox/util/ItemParserTest.java b/src/test/java/world/bentobox/bentobox/util/ItemParserTest.java
index 739587827..b14cc32ee 100644
--- a/src/test/java/world/bentobox/bentobox/util/ItemParserTest.java
+++ b/src/test/java/world/bentobox/bentobox/util/ItemParserTest.java
@@ -21,8 +21,10 @@
import org.bukkit.inventory.meta.BannerMeta;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.inventory.meta.PotionMeta;
+import org.bukkit.inventory.meta.SkullMeta;
import org.bukkit.potion.PotionData;
import org.bukkit.potion.PotionType;
+import org.bukkit.profile.PlayerProfile;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
@@ -50,6 +52,9 @@ public class ItemParserTest {
private ItemMeta itemMeta;
@Mock
private ItemFactory itemFactory;
+ @Mock
+ private SkullMeta skullMeta;
+
private ItemStack defaultItem;
@SuppressWarnings("deprecation")
@@ -61,6 +66,8 @@ public void setUp() throws Exception {
PowerMockito.mockStatic(Bukkit.class);
when(Bukkit.getItemFactory()).thenReturn(itemFactory);
+ // Do not test Bukkit createItemStack method output as I assume Bukkit has their tests covered.
+ when(itemFactory.createItemStack(any())).thenThrow(IllegalArgumentException.class);
/*
when(itemFactory.getItemMeta(Mockito.eq(Material.POTION))).thenReturn(potionMeta);
when(itemFactory.getItemMeta(Mockito.eq(Material.SPLASH_POTION))).thenReturn(potionMeta);
@@ -106,148 +113,66 @@ public void testParseNoColons() {
assertEquals(defaultItem, ItemParser.parse("NOCOLONS", defaultItem));
}
- /*
- * # Format POTION:NAME::::QTY
- # LEVEL, EXTENDED, SPLASH, LINGER are optional.
- # LEVEL is a number, 1 or 2
- # LINGER is for V1.9 servers and later
- # Examples:
- # POTION:STRENGTH:1:EXTENDED:SPLASH:1
- # POTION:INSTANT_DAMAGE:2::LINGER:2
- # POTION:JUMP:2:NOTEXTENDED:NOSPLASH:1
- # POTION:WEAKNESS::::1 - any weakness potion
- */
-
- @Ignore("Extended potions now have their own names and are not extended like this")
- @Test
- public void testParsePotionStrengthExtended() {
- when(itemFactory.getItemMeta(any())).thenReturn(potionMeta);
- ItemStack result = ItemParser.parse("POTION:STRENGTH:1:EXTENDED::5");
- assertNotNull(result);
- assertEquals(Material.POTION, result.getType());
- PotionType type = PotionType.STRENGTH;
- boolean isExtended = true;
- boolean isUpgraded = false;
- PotionData data = new PotionData(type, isExtended, isUpgraded);
- verify(potionMeta).setBasePotionData(Mockito.eq(data));
- assertEquals(5, result.getAmount());
- }
-
- @SuppressWarnings("deprecation")
@Test
- public void testParsePotionStrengthNotExtended() {
+ public void testParsePotion() {
when(itemFactory.getItemMeta(any())).thenReturn(potionMeta);
- ItemStack result = ItemParser.parse("POTION:STRENGTH:1:::4");
- assertNotNull(result);
- assertEquals(Material.POTION, result.getType());
- PotionType type = PotionType.STRENGTH;
- boolean isExtended = false;
- boolean isUpgraded = false;
- PotionData data = new PotionData(type, isExtended, isUpgraded);
- verify(potionMeta).setBasePotionData(Mockito.eq(data));
- assertEquals(4, result.getAmount());
+ for (PotionType type : PotionType.values()) {
+ ItemStack itemStack = ItemParser.parse("POTION:" + type.name() + ":1");
+ assertEquals(itemStack.getType(), Material.POTION);
+ // Not sure how this can be tested.
+ // assertEquals(type, ((PotionMeta) itemStack.getItemMeta()).getBasePotionType());
+ assertEquals(1, itemStack.getAmount());
+ }
}
- @SuppressWarnings("deprecation")
@Test
- public void testParsePotionStrengthNotExtendedSplash() {
+ public void testParseSplashPotion() {
when(itemFactory.getItemMeta(any())).thenReturn(potionMeta);
- ItemStack result = ItemParser.parse("POTION:STRENGTH:1::SPLASH:3");
- assertNotNull(result);
- assertEquals(Material.SPLASH_POTION, result.getType());
- PotionType type = PotionType.STRENGTH;
- boolean isExtended = false;
- boolean isUpgraded = false;
- PotionData data = new PotionData(type, isExtended, isUpgraded);
- verify(potionMeta).setBasePotionData(Mockito.eq(data));
- assertEquals(3, result.getAmount());
+ for (PotionType type : PotionType.values()) {
+ ItemStack itemStack = ItemParser.parse("SPLASH_POTION:" + type.name() + ":1");
+ assertEquals(itemStack.getType(), Material.SPLASH_POTION);
+ // Not sure how this can be tested.
+ // assertEquals(type, ((PotionMeta) itemStack.getItemMeta()).getBasePotionType());
+ assertEquals(1, itemStack.getAmount());
+ }
}
- @SuppressWarnings("deprecation")
- @Ignore("Potions are no longer upgraded like this")
@Test
- public void testParsePotionStrengthNotExtendedUpgradedSplash() {
+ public void testParseLingeringPotion() {
when(itemFactory.getItemMeta(any())).thenReturn(potionMeta);
- ItemStack result = ItemParser.parse("POTION:STRENGTH:2::SPLASH:3");
- assertNotNull(result);
- assertEquals(Material.SPLASH_POTION, result.getType());
- PotionType type = PotionType.STRENGTH;
- boolean isExtended = false;
- boolean isUpgraded = true;
- PotionData data = new PotionData(type, isExtended, isUpgraded);
- verify(potionMeta).setBasePotionData(Mockito.eq(data));
- assertEquals(3, result.getAmount());
- }
-
- enum extend {
- NOT_EXTENDED,
- EXTENDED
- }
-
- enum type {
- NO_SPLASH,
- SPLASH,
- LINGER
+ for (PotionType type : PotionType.values()) {
+ ItemStack itemStack = ItemParser.parse("LINGERING_POTION:" + type.name() + ":1");
+ assertEquals(itemStack.getType(), Material.LINGERING_POTION);
+ // Not sure how this can be tested.
+ // assertEquals(type, ((PotionMeta) itemStack.getItemMeta()).getBasePotionType());
+ assertEquals(1, itemStack.getAmount());
+ }
}
- List notExtendable = Arrays.asList(
- PotionType.UNCRAFTABLE,
- PotionType.WATER,
- PotionType.MUNDANE,
- PotionType.THICK,
- PotionType.AWKWARD,
- PotionType.INSTANT_HEAL,
- PotionType.INSTANT_DAMAGE,
- PotionType.LUCK,
- PotionType.NIGHT_VISION
- );
-
- @SuppressWarnings("deprecation")
@Test
- public void testParsePotion() {
+ public void testParseTippedArrow() {
when(itemFactory.getItemMeta(any())).thenReturn(potionMeta);
for (PotionType type : PotionType.values()) {
- if (type.name().contains("LONG") || type.name().contains("STRONG")) {
- continue;
- }
- for (ItemParserTest.type t: ItemParserTest.type.values()) {
- for (int up = 1; up < 2; up++) {
- boolean isUpgraded = up > 1;
- String req = "POTION:" + type.name() + ":" + up + "::"+ t.name() + ":3";
- ItemStack result = ItemParser.parse(req);
- assertNotNull(result);
- switch (t) {
- case LINGER:
- assertEquals(Material.LINGERING_POTION, result.getType());
- PotionData data = new PotionData(type, false, isUpgraded);
- verify(potionMeta, times(3)).setBasePotionData(Mockito.eq(data));
- break;
- case NO_SPLASH:
- assertEquals(Material.POTION, result.getType());
- data = new PotionData(type, false, isUpgraded);
- verify(potionMeta).setBasePotionData(Mockito.eq(data));
- break;
- case SPLASH:
- assertEquals(Material.SPLASH_POTION, result.getType());
- data = new PotionData(type, false, isUpgraded);
- verify(potionMeta, times(2)).setBasePotionData(Mockito.eq(data));
- break;
- default:
- break;
- }
-
- assertEquals(3, result.getAmount());
- }
- }
+ ItemStack itemStack = ItemParser.parse("TIPPED_ARROW:" + type.name() + ":1");
+ assertEquals(itemStack.getType(), Material.TIPPED_ARROW);
+ // Not sure how this can be tested.
+ // assertEquals(type, ((PotionMeta) itemStack.getItemMeta()).getBasePotionType());
+ assertEquals(1, itemStack.getAmount());
}
}
@Test
- public void testParseTippedArrow() {
+ public void testParseBadPotion()
+ {
when(itemFactory.getItemMeta(any())).thenReturn(potionMeta);
- ItemStack result = ItemParser.parse("TIPPED_ARROW:WEAKNESS::::1");
- assertNotNull(result);
- assertEquals(Material.TIPPED_ARROW, result.getType());
+ ItemStack itemStack = ItemParser.parse("POTION::5");
+ assertEquals(5, itemStack.getAmount());
+ // Not sure how this can be tested
+ // assertEquals(PotionType.WATER, ((PotionMeta) itemStack.getItemMeta()).getBasePotionType());
+ itemStack = ItemParser.parse("POTION:NO_POTION:1");
+ assertEquals(1, itemStack.getAmount());
+ // Not sure how this can be tested
+ // assertEquals(PotionType.WATER, ((PotionMeta) itemStack.getItemMeta()).getBasePotionType());
}
@@ -318,7 +243,6 @@ public void testParseBadThreeItem() {
assertEquals(defaultItem, ItemParser.parse("WOODEN_SWORD:4:AA", defaultItem));
}
- @Ignore("This doesn't work for some reason")
@Test
public void parseCustomModelData() {
ItemStack result = ItemParser.parse("WOODEN_SWORD:CMD-23151212:2");
@@ -327,4 +251,21 @@ public void parseCustomModelData() {
assertEquals(2, result.getAmount());
assertNull(ItemParser.parse("WOODEN_SWORD:CMD-23151212:2:CMD-23151212"));
}
+
+ @Test
+ public void parsePlayerHead() {
+ when(itemFactory.getItemMeta(any())).thenReturn(skullMeta);
+ ItemStack result = ItemParser.parse("PLAYER_HEAD:2");
+ assertNotNull(result);
+ assertEquals(Material.PLAYER_HEAD, result.getType());
+ assertEquals(2, result.getAmount());
+
+ result = ItemParser.parse("PLAYER_HEAD:BONNe1704");
+ assertNotNull(result);
+ assertEquals(Material.PLAYER_HEAD, result.getType());
+ assertEquals(1, result.getAmount());
+
+ // I do not know if it is possible to test metadata, as skull meta is not applied to player heads in testing.
+ //assertEquals("BONNe1704", ((SkullMeta) result.getItemMeta()).getOwnerProfile().getName());
+ }
}