From d7c7559546cc017896df84ff8379f04354ef8a23 Mon Sep 17 00:00:00 2001 From: tastybento Date: Mon, 28 Dec 2020 20:39:08 -0800 Subject: [PATCH] Adds a persistent metadata API to User and Island classes. This is modeled after the Bukkit metadata API with the difference that it is persistent, i.e., metadata is stored to the database. Metadata can be placed on Islands or Users. This API should be useful for addons that do not want or need to create their own database tables and instead just want to tag the user with some data, or tag the island with some data. It is intended for small amounts of data, like boolean tags or other values. --- .../bentobox/api/metadata/MetaDataAble.java | 48 ++++++++++ .../bentobox/api/metadata/MetaDataValue.java | 90 ++++++++++++++++++ .../bentobox/bentobox/api/user/User.java | 51 ++++++++++ .../bentobox/database/objects/Island.java | 76 +++++++++++++-- .../bentobox/database/objects/Players.java | 67 +++++++++++++- .../api/metadata/MetaDataValueTest.java | 92 +++++++++++++++++++ .../bentobox/bentobox/api/user/UserTest.java | 33 +++++++ 7 files changed, 448 insertions(+), 9 deletions(-) create mode 100644 src/main/java/world/bentobox/bentobox/api/metadata/MetaDataAble.java create mode 100644 src/main/java/world/bentobox/bentobox/api/metadata/MetaDataValue.java create mode 100644 src/test/java/world/bentobox/bentobox/api/metadata/MetaDataValueTest.java diff --git a/src/main/java/world/bentobox/bentobox/api/metadata/MetaDataAble.java b/src/main/java/world/bentobox/bentobox/api/metadata/MetaDataAble.java new file mode 100644 index 000000000..ce63fc552 --- /dev/null +++ b/src/main/java/world/bentobox/bentobox/api/metadata/MetaDataAble.java @@ -0,0 +1,48 @@ +package world.bentobox.bentobox.api.metadata; + +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * This interface is for all BentoBox objects that have meta data + * @author tastybento + * @since 1.15.4 + */ +public interface MetaDataAble { + /** + * @return the metaData + */ + public Map getMetaData(); + + /** + * Get meta data by key + * @param key - key + * @return the value to which the specified key is mapped, or null if there is no mapping for the key + * @since 1.15.4 + */ + public MetaDataValue getMetaData(@NonNull String key); + + /** + * @param metaData the metaData to set + * @since 1.15.4 + */ + public void setMetaData(Map metaData); + + /** + * Put a key, value string pair into the object's meta data + * @param key - key + * @param value - value + * @return the previous value associated with key, or null if there was no mapping for key. + * @since 1.15.4 + */ + public MetaDataValue putMetaData(@NonNull String key, @NonNull MetaDataValue value); + + /** + * Remove meta data + * @param key - key to remove + * @return the previous value associated with key, or null if there was no mapping for key. + * @since 1.15.4 + */ + public MetaDataValue removeMetaData(@NonNull String key); +} diff --git a/src/main/java/world/bentobox/bentobox/api/metadata/MetaDataValue.java b/src/main/java/world/bentobox/bentobox/api/metadata/MetaDataValue.java new file mode 100644 index 000000000..4ae5c6d14 --- /dev/null +++ b/src/main/java/world/bentobox/bentobox/api/metadata/MetaDataValue.java @@ -0,0 +1,90 @@ +package world.bentobox.bentobox.api.metadata; + +import org.eclipse.jdt.annotation.NonNull; + +import com.google.gson.annotations.Expose; + +/** + * Stores meta data value in a GSON friendly way so it can be serialized and deserialized. + * Values that are null are not stored in the database, so only the appropriate type is stored. + * @author tastybento + * @since 1.15.4 + * + */ +public class MetaDataValue { + + // Use classes so null value is supported + @Expose + private Integer intValue; + @Expose + private Float floatValue; + @Expose + private Double doubleValue; + @Expose + private Long longValue; + @Expose + private Short shortValue; + @Expose + private Byte byteValue; + @Expose + private Boolean booleanValue; + @Expose + private @NonNull String stringValue; + + /** + * Initialize this meta data value + * @param value the value assigned to this metadata value + */ + public MetaDataValue(@NonNull Object value) { + if (value instanceof Integer) { + intValue = (int)value; + } else if (value instanceof Float) { + floatValue = (float)value; + } else if (value instanceof Double) { + doubleValue = (double)value; + } else if (value instanceof Long) { + longValue = (long)value; + } else if (value instanceof Short) { + shortValue = (short)value; + } else if (value instanceof Byte) { + byteValue = (byte)value; + } else if (value instanceof Boolean) { + booleanValue = (boolean)value; + } else if (value instanceof String) { + stringValue = (String)value; + } + } + + public int asInt() { + return intValue; + } + + public float asFloat() { + return floatValue; + } + + public double asDouble() { + return doubleValue; + } + + public long asLong() { + return longValue; + } + + public short asShort() { + return shortValue; + } + + public byte asByte() { + return byteValue; + } + + public boolean asBoolean() { + return booleanValue; + } + + @NonNull + public String asString() { + return stringValue; + } +} diff --git a/src/main/java/world/bentobox/bentobox/api/user/User.java b/src/main/java/world/bentobox/bentobox/api/user/User.java index d2c1e57f8..cfe986526 100644 --- a/src/main/java/world/bentobox/bentobox/api/user/User.java +++ b/src/main/java/world/bentobox/bentobox/api/user/User.java @@ -4,6 +4,7 @@ import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; @@ -29,6 +30,7 @@ import org.eclipse.jdt.annotation.Nullable; import world.bentobox.bentobox.BentoBox; import world.bentobox.bentobox.api.addons.Addon; import world.bentobox.bentobox.api.events.OfflineMessageEvent; +import world.bentobox.bentobox.api.metadata.MetaDataValue; import world.bentobox.bentobox.util.Util; /** @@ -631,4 +633,53 @@ public class User { public void setAddon(Addon addon) { this.addon = addon; } + + /** + * Get all the meta data for this user + * @return the metaData + * @since 1.15.4 + */ + @NonNull + public Map getMetaData() { + return plugin.getPlayers().getPlayer(playerUUID).getMetaData(); + } + + /** + * Get meta data by key + * @param key - key + * @return optional value to which the specified key is mapped, or empty if there is no mapping for the key + * @since 1.15.4 + */ + public Optional getMetaData(String key) { + return Optional.ofNullable(plugin.getPlayers().getPlayer(playerUUID).getMetaData().get(key)); + } + + /** + * @param metaData the metaData to set + * @since 1.15.4 + */ + public void setMetaData(Map metaData) { + plugin.getPlayers().getPlayer(playerUUID).setMetaData(metaData); + } + + /** + * Put a key, value string pair into the user's meta data + * @param key - key + * @param value - value + * @return the previous value associated with key, or empty if there was no mapping for key. + * @since 1.15.4 + */ + public Optional putMetaData(String key, MetaDataValue value) { + return Optional.ofNullable(plugin.getPlayers().getPlayer(playerUUID).getMetaData().put(key, value)); + } + + /** + * Remove meta data + * @param key - key to remove + * @return the previous value associated with key, or empty if there was no mapping for key. + * @since 1.15.4 + */ + public Optional removeMetaData(String key) { + return Optional.ofNullable(plugin.getPlayers().getPlayer(playerUUID).getMetaData().remove(key)); + } } diff --git a/src/main/java/world/bentobox/bentobox/database/objects/Island.java b/src/main/java/world/bentobox/bentobox/database/objects/Island.java index c19b09be7..4e638b8cb 100644 --- a/src/main/java/world/bentobox/bentobox/database/objects/Island.java +++ b/src/main/java/world/bentobox/bentobox/database/objects/Island.java @@ -34,6 +34,8 @@ import world.bentobox.bentobox.api.events.island.IslandEvent; import world.bentobox.bentobox.api.flags.Flag; import world.bentobox.bentobox.api.localization.TextVariables; import world.bentobox.bentobox.api.logs.LogEntry; +import world.bentobox.bentobox.api.metadata.MetaDataAble; +import world.bentobox.bentobox.api.metadata.MetaDataValue; import world.bentobox.bentobox.api.user.User; import world.bentobox.bentobox.database.objects.adapters.Adapter; import world.bentobox.bentobox.database.objects.adapters.FlagSerializer; @@ -54,7 +56,7 @@ import world.bentobox.bentobox.util.Util; * @author Poslovitch */ @Table(name = "Islands") -public class Island implements DataObject { +public class Island implements DataObject, MetaDataAble { // True if this island is deleted and pending deletion from the database @Expose @@ -171,6 +173,13 @@ public class Island implements DataObject { @Nullable private Boolean reserved = null; + /** + * A place to store meta data for this island. + * @since 1.15.4 + */ + @Expose + private Map metaData; + /* * *************************** Constructors ****************************** */ @@ -956,14 +965,14 @@ public class Island implements DataObject { // Fixes #getLastPlayed() returning 0 when it is the owner's first connection. long lastPlayed = (Bukkit.getServer().getOfflinePlayer(owner).getLastPlayed() != 0) ? Bukkit.getServer().getOfflinePlayer(owner).getLastPlayed() : Bukkit.getServer().getOfflinePlayer(owner).getFirstPlayed(); - user.sendMessage("commands.admin.info.last-login","[date]", new Date(lastPlayed).toString()); + user.sendMessage("commands.admin.info.last-login","[date]", new Date(lastPlayed).toString()); - user.sendMessage("commands.admin.info.deaths", "[number]", String.valueOf(plugin.getPlayers().getDeaths(world, owner))); - String resets = String.valueOf(plugin.getPlayers().getResets(world, owner)); - String total = plugin.getIWM().getResetLimit(world) < 0 ? "Unlimited" : String.valueOf(plugin.getIWM().getResetLimit(world)); - user.sendMessage("commands.admin.info.resets-left", "[number]", resets, "[total]", total); - // Show team members - showMembers(user); + user.sendMessage("commands.admin.info.deaths", "[number]", String.valueOf(plugin.getPlayers().getDeaths(world, owner))); + String resets = String.valueOf(plugin.getPlayers().getResets(world, owner)); + String total = plugin.getIWM().getResetLimit(world) < 0 ? "Unlimited" : String.valueOf(plugin.getIWM().getResetLimit(world)); + user.sendMessage("commands.admin.info.resets-left", "[number]", resets, "[total]", total); + // Show team members + showMembers(user); } Vector location = center.toVector(); user.sendMessage("commands.admin.info.island-location", "[xyz]", Util.xyz(location)); @@ -1251,4 +1260,55 @@ public class Island implements DataObject { + ", levelHandicap=" + levelHandicap + ", spawnPoint=" + spawnPoint + ", doNotLoad=" + doNotLoad + "]"; } + /** + * @return the metaData + * @since 1.15.4 + */ + @Override + public Map getMetaData() { + return metaData; + } + + /** + * Get meta data by key + * @param key - key + * @return the value to which the specified key is mapped, or null if there is no mapping for the key + * @since 1.15.4 + */ + @Override + public MetaDataValue getMetaData(String key) { + return this.metaData.get(key); + } + + /** + * @param metaData the metaData to set + * @since 1.15.4 + */ + @Override + public void setMetaData(Map metaData) { + this.metaData = metaData; + } + + /** + * Put a key, value string pair into the island's meta data + * @param key - key + * @param value - value + * @return the previous value associated with key, or null if there was no mapping for key. + * @since 1.15.4 + */ + @Override + public MetaDataValue putMetaData(String key, MetaDataValue value) { + return this.metaData.put(key, value); + } + + /** + * Remove meta data + * @param key - key to remove + * @return the previous value associated with key, or null if there was no mapping for key. + * @since 1.15.4 + */ + @Override + public MetaDataValue removeMetaData(String key) { + return this.metaData.remove(key); + } } diff --git a/src/main/java/world/bentobox/bentobox/database/objects/Players.java b/src/main/java/world/bentobox/bentobox/database/objects/Players.java index c9dbf04c4..dc1ce9322 100644 --- a/src/main/java/world/bentobox/bentobox/database/objects/Players.java +++ b/src/main/java/world/bentobox/bentobox/database/objects/Players.java @@ -11,12 +11,15 @@ import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.World; import org.bukkit.entity.Player; +import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; import com.google.gson.annotations.Expose; import world.bentobox.bentobox.BentoBox; import world.bentobox.bentobox.api.flags.Flag; +import world.bentobox.bentobox.api.metadata.MetaDataAble; +import world.bentobox.bentobox.api.metadata.MetaDataValue; import world.bentobox.bentobox.util.Util; /** @@ -25,7 +28,7 @@ import world.bentobox.bentobox.util.Util; * @author tastybento */ @Table(name = "Players") -public class Players implements DataObject { +public class Players implements DataObject, MetaDataAble { @Expose private Map homeLocations = new HashMap<>(); @Expose @@ -53,6 +56,13 @@ public class Players implements DataObject { @Expose private Flag.Mode flagsDisplayMode = Flag.Mode.BASIC; + /** + * A place to store meta data for this player. + * @since 1.15.4 + */ + @Expose + private Map metaData; + /** * This is required for database storage */ @@ -338,4 +348,59 @@ public class Players implements DataObject { public void setFlagsDisplayMode(Flag.Mode flagsDisplayMode) { this.flagsDisplayMode = flagsDisplayMode; } + + /** + * @return the metaData + */ + @Override + public Map getMetaData() { + if (metaData == null) { + metaData = new HashMap<>(); + } + return metaData; + } + + /** + * Get meta data by key + * @param key - key + * @return the value to which the specified key is mapped, or null if there is no mapping for the key + * @since 1.15.4 + */ + @Override + public MetaDataValue getMetaData(@NonNull String key) { + return getMetaData().get(key); + } + + /** + * @param metaData the metaData to set + * @since 1.15.4 + */ + @Override + public void setMetaData(Map metaData) { + this.metaData = metaData; + } + + /** + * Put a key, value string pair into the player's meta data + * @param key - key + * @param value - value + * @return the previous value associated with key, or null if there was no mapping for key. + * @since 1.15.4 + */ + @Override + public MetaDataValue putMetaData(@NonNull String key, @NonNull MetaDataValue value) { + return getMetaData().put(key, value); + } + + /** + * Remove meta data + * @param key - key to remove + * @return the previous value associated with key, or null if there was no mapping for key. + * @since 1.15.4 + */ + @Override + public MetaDataValue removeMetaData(@NonNull String key) { + return getMetaData().remove(key); + } + } diff --git a/src/test/java/world/bentobox/bentobox/api/metadata/MetaDataValueTest.java b/src/test/java/world/bentobox/bentobox/api/metadata/MetaDataValueTest.java new file mode 100644 index 000000000..9f5f1aa5f --- /dev/null +++ b/src/test/java/world/bentobox/bentobox/api/metadata/MetaDataValueTest.java @@ -0,0 +1,92 @@ +package world.bentobox.bentobox.api.metadata; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.modules.junit4.PowerMockRunner; + +/** + * @author tastybento + * + */ +@RunWith(PowerMockRunner.class) +public class MetaDataValueTest { + + /** + * Test method for {@link world.bentobox.bentobox.api.metadata.MetaDataValue#asInt()}. + */ + @Test + public void testAsInt() { + MetaDataValue mdv = new MetaDataValue(123); + assertEquals(123, mdv.asInt()); + } + + /** + * Test method for {@link world.bentobox.bentobox.api.metadata.MetaDataValue#asFloat()}. + */ + @Test + public void testAsFloat() { + MetaDataValue mdv = new MetaDataValue(123.34F); + assertEquals(123.34F, mdv.asFloat(), 0F); + } + + /** + * Test method for {@link world.bentobox.bentobox.api.metadata.MetaDataValue#asDouble()}. + */ + @Test + public void testAsDouble() { + MetaDataValue mdv = new MetaDataValue(123.3444D); + assertEquals(123.3444D, mdv.asDouble(), 0D); + } + + /** + * Test method for {@link world.bentobox.bentobox.api.metadata.MetaDataValue#asLong()}. + */ + @Test + public void testAsLong() { + MetaDataValue mdv = new MetaDataValue(123456L); + assertEquals(123456L, mdv.asLong()); + } + + /** + * Test method for {@link world.bentobox.bentobox.api.metadata.MetaDataValue#asShort()}. + */ + @Test + public void testAsShort() { + MetaDataValue mdv = new MetaDataValue((short)12); + assertEquals((short)12, mdv.asShort()); + } + + /** + * Test method for {@link world.bentobox.bentobox.api.metadata.MetaDataValue#asByte()}. + */ + @Test + public void testAsByte() { + MetaDataValue mdv = new MetaDataValue((byte)12); + assertEquals((byte)12, mdv.asByte()); + } + + /** + * Test method for {@link world.bentobox.bentobox.api.metadata.MetaDataValue#asBoolean()}. + */ + @Test + public void testAsBoolean() { + MetaDataValue mdv = new MetaDataValue(false); + assertFalse(mdv.asBoolean()); + mdv = new MetaDataValue(true); + assertTrue(mdv.asBoolean()); + } + + /** + * Test method for {@link world.bentobox.bentobox.api.metadata.MetaDataValue#asString()}. + */ + @Test + public void testAsString() { + MetaDataValue mdv = new MetaDataValue("a string"); + assertEquals("a string", mdv.asString()); + } + +} diff --git a/src/test/java/world/bentobox/bentobox/api/user/UserTest.java b/src/test/java/world/bentobox/bentobox/api/user/UserTest.java index f3e8fe1ee..7ff1e1854 100644 --- a/src/test/java/world/bentobox/bentobox/api/user/UserTest.java +++ b/src/test/java/world/bentobox/bentobox/api/user/UserTest.java @@ -15,6 +15,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.Locale; import java.util.Optional; @@ -33,6 +34,7 @@ import org.bukkit.inventory.ItemFactory; import org.bukkit.inventory.PlayerInventory; import org.bukkit.permissions.PermissionAttachmentInfo; import org.bukkit.plugin.PluginManager; +import org.eclipse.jdt.annotation.Nullable; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -49,6 +51,8 @@ import world.bentobox.bentobox.BentoBox; import world.bentobox.bentobox.Settings; import world.bentobox.bentobox.api.addons.AddonDescription; import world.bentobox.bentobox.api.addons.GameModeAddon; +import world.bentobox.bentobox.api.metadata.MetaDataValue; +import world.bentobox.bentobox.database.objects.Players; import world.bentobox.bentobox.managers.IslandWorldManager; import world.bentobox.bentobox.managers.LocalesManager; import world.bentobox.bentobox.managers.PlaceholdersManager; @@ -80,6 +84,7 @@ public class UserTest { private Server server; @Mock private PlayersManager pm; + private @Nullable Players players; @Before public void setUp() throws Exception { @@ -120,6 +125,8 @@ public class UserTest { when(placeholdersManager.replacePlaceholders(any(), any())).thenAnswer((Answer) invocation -> invocation.getArgument(1, String.class)); when(plugin.getPlayers()).thenReturn(pm); + players = new Players(); + when(pm.getPlayer(any())).thenReturn(players); } @After @@ -592,4 +599,30 @@ public class UserTest { User u = User.getInstance(player); assertEquals(3, u.getPermissionValue("bskyblock.max", 22)); } + + @Test + public void testMetaData() { + User u = User.getInstance(player); + assertTrue(u.getMetaData().isEmpty()); + // Store a string in a new key + assertFalse(u.putMetaData("string", new MetaDataValue("a string")).isPresent()); + // Store an int in a new key + assertFalse(u.putMetaData("int", new MetaDataValue(1234)).isPresent()); + // Overwrite the string with the same key + assertEquals("a string", u.putMetaData("string", new MetaDataValue("a new string")).get().asString()); + // Get the new string with the same key + assertEquals("a new string", u.getMetaData("string").get().asString()); + // Try to get a non-existent key + assertFalse(u.getMetaData("boogie").isPresent()); + // Remove existing key + assertEquals(1234, u.removeMetaData("int").get().asInt()); + assertFalse(u.getMetaData("int").isPresent()); + // Try to remove non-existent key + assertFalse(u.removeMetaData("ggogg").isPresent()); + // Set the meta data as blank + assertFalse(u.getMetaData().isEmpty()); + u.setMetaData(new HashMap<>()); + assertTrue(u.getMetaData().isEmpty()); + } + }