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.
This commit is contained in:
tastybento 2020-12-28 20:39:08 -08:00
parent e84b1f1830
commit d7c7559546
7 changed files with 448 additions and 9 deletions

View File

@ -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<String, MetaDataValue> 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<String, MetaDataValue> 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);
}

View File

@ -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;
}
}

View File

@ -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<String, MetaDataValue> 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<MetaDataValue> 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<String, MetaDataValue> 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<MetaDataValue> 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<MetaDataValue> removeMetaData(String key) {
return Optional.ofNullable(plugin.getPlayers().getPlayer(playerUUID).getMetaData().remove(key));
}
}

View File

@ -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<String, MetaDataValue> 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<String, MetaDataValue> 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<String, MetaDataValue> 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);
}
}

View File

@ -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<Location, Integer> 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<String, MetaDataValue> 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<String, MetaDataValue> 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<String, MetaDataValue> 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);
}
}

View File

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

View File

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