diff --git a/paper-api/src/main/java/org/bukkit/World.java b/paper-api/src/main/java/org/bukkit/World.java index 5212f96141..1b326a558a 100644 --- a/paper-api/src/main/java/org/bukkit/World.java +++ b/paper-api/src/main/java/org/bukkit/World.java @@ -13,13 +13,14 @@ import org.bukkit.block.Block; import org.bukkit.entity.*; import org.bukkit.generator.BlockPopulator; import org.bukkit.inventory.ItemStack; +import org.bukkit.metadata.Metadatable; import org.bukkit.plugin.messaging.PluginMessageRecipient; import org.bukkit.util.Vector; /** * Represents a world, which may contain entities, chunks and blocks */ -public interface World extends PluginMessageRecipient { +public interface World extends PluginMessageRecipient, Metadatable { /** * Gets the {@link Block} at the given coordinates diff --git a/paper-api/src/main/java/org/bukkit/block/Block.java b/paper-api/src/main/java/org/bukkit/block/Block.java index 7ca3c01b22..99bc563051 100644 --- a/paper-api/src/main/java/org/bukkit/block/Block.java +++ b/paper-api/src/main/java/org/bukkit/block/Block.java @@ -7,6 +7,7 @@ import org.bukkit.Material; import org.bukkit.World; import org.bukkit.Location; import org.bukkit.inventory.ItemStack; +import org.bukkit.metadata.Metadatable; /** * Represents a block. This is a live object, and only one Block may exist for @@ -14,7 +15,7 @@ import org.bukkit.inventory.ItemStack; * to your own handling of it; use block.getState() to get a snapshot state of a * block which will not be modified. */ -public interface Block { +public interface Block extends Metadatable { /** * Gets the metadata for this block diff --git a/paper-api/src/main/java/org/bukkit/block/BlockState.java b/paper-api/src/main/java/org/bukkit/block/BlockState.java index 7e181017fe..cd5bcfbf3f 100644 --- a/paper-api/src/main/java/org/bukkit/block/BlockState.java +++ b/paper-api/src/main/java/org/bukkit/block/BlockState.java @@ -5,6 +5,7 @@ import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.World; import org.bukkit.material.MaterialData; +import org.bukkit.metadata.Metadatable; /** * Represents a captured state of a block, which will not change automatically. @@ -14,7 +15,7 @@ import org.bukkit.material.MaterialData; * the state of the block and you will not know, or they may change the block to * another type entirely, causing your BlockState to become invalid. */ -public interface BlockState { +public interface BlockState extends Metadatable { /** * Gets the block represented by this BlockState diff --git a/paper-api/src/main/java/org/bukkit/entity/Entity.java b/paper-api/src/main/java/org/bukkit/entity/Entity.java index c85547340c..d9d219b3ae 100644 --- a/paper-api/src/main/java/org/bukkit/entity/Entity.java +++ b/paper-api/src/main/java/org/bukkit/entity/Entity.java @@ -5,6 +5,7 @@ import org.bukkit.EntityEffect; import org.bukkit.Server; import org.bukkit.World; import org.bukkit.event.entity.EntityDamageEvent; +import org.bukkit.metadata.Metadatable; import org.bukkit.util.Vector; import java.util.List; @@ -14,7 +15,7 @@ import org.bukkit.event.player.PlayerTeleportEvent.TeleportCause; /** * Represents a base entity in the world */ -public interface Entity { +public interface Entity extends Metadatable { /** * Gets the entity's current position diff --git a/paper-api/src/main/java/org/bukkit/metadata/FixedMetadataValue.java b/paper-api/src/main/java/org/bukkit/metadata/FixedMetadataValue.java new file mode 100644 index 0000000000..41f89a0eff --- /dev/null +++ b/paper-api/src/main/java/org/bukkit/metadata/FixedMetadataValue.java @@ -0,0 +1,25 @@ +package org.bukkit.metadata; + +import org.bukkit.plugin.Plugin; + +import java.util.concurrent.Callable; + +/** + * A FixedMetadataValue is a special case metadata item that contains the same value forever after initialization. + * Invalidating a FixedMetadataValue has no affect. + */ +public class FixedMetadataValue extends LazyMetadataValue { + /** + * Initializes a FixedMetadataValue with an Object + * + * @param owningPlugin the {@link Plugin} that created this metadata value. + * @param value the value assigned to this metadata value. + */ + public FixedMetadataValue(Plugin owningPlugin, final Object value) { + super(owningPlugin, CacheStrategy.CACHE_ETERNALLY, new Callable() { + public Object call() throws Exception { + return value; + } + }); + } +} diff --git a/paper-api/src/main/java/org/bukkit/metadata/LazyMetadataValue.java b/paper-api/src/main/java/org/bukkit/metadata/LazyMetadataValue.java new file mode 100644 index 0000000000..cc0ba50603 --- /dev/null +++ b/paper-api/src/main/java/org/bukkit/metadata/LazyMetadataValue.java @@ -0,0 +1,158 @@ +package org.bukkit.metadata; + +import java.lang.ref.SoftReference; +import java.util.concurrent.Callable; + +import org.apache.commons.lang.Validate; +import org.bukkit.plugin.Plugin; +import org.bukkit.util.NumberConversions; + +/** + * The LazyMetadataValue class implements a type of metadata that is not computed until another plugin asks for it. + * By making metadata values lazy, no computation is done by the providing plugin until absolutely necessary (if ever). + * Additionally, LazyMetadataValue objects cache their values internally unless overridden by a {@link CacheStrategy} + * or invalidated at the individual or plugin level. Once invalidated, the LazyMetadataValue will recompute its value + * when asked. + */ +public class LazyMetadataValue implements MetadataValue { + private Callable lazyValue; + private CacheStrategy cacheStrategy; + private SoftReference internalValue = new SoftReference(null); + private Plugin owningPlugin; + private static final Object ACTUALLY_NULL = new Object(); + + /** + * Initialized a LazyMetadataValue object with the default CACHE_AFTER_FIRST_EVAL cache strategy. + * + * @param owningPlugin the {@link Plugin} that created this metadata value. + * @param lazyValue the lazy value assigned to this metadata value. + */ + public LazyMetadataValue(Plugin owningPlugin, Callable lazyValue) { + this(owningPlugin, CacheStrategy.CACHE_AFTER_FIRST_EVAL, lazyValue); + } + + /** + * Initializes a LazyMetadataValue object with a specific cache strategy. + * + * @param owningPlugin the {@link Plugin} that created this metadata value. + * @param cacheStrategy determines the rules for caching this metadata value. + * @param lazyValue the lazy value assigned to this metadata value. + */ + public LazyMetadataValue(Plugin owningPlugin, CacheStrategy cacheStrategy, Callable lazyValue) { + Validate.notNull(owningPlugin, "owningPlugin cannot be null"); + Validate.notNull(cacheStrategy, "cacheStrategy cannot be null"); + Validate.notNull(lazyValue, "lazyValue cannot be null"); + + this.lazyValue = lazyValue; + this.owningPlugin = owningPlugin; + this.cacheStrategy = cacheStrategy; + } + + public Plugin getOwningPlugin() { + return owningPlugin; + } + + public Object value() { + eval(); + Object value = internalValue.get(); + if (value == ACTUALLY_NULL) { + return null; + } + return value; + } + + public int asInt() { + return NumberConversions.toInt(value()); + } + + public float asFloat() { + return NumberConversions.toFloat(value()); + } + + public double asDouble() { + return NumberConversions.toDouble(value()); + } + + public long asLong() { + return NumberConversions.toLong(value()); + } + + public short asShort() { + return NumberConversions.toShort(value()); + } + + public byte asByte() { + return NumberConversions.toByte(value()); + } + + public boolean asBoolean() { + Object value = value(); + if (value instanceof Boolean) { + return (Boolean) value; + } + + if (value instanceof Number) { + return ((Number) value).intValue() != 0; + } + + if (value instanceof String) { + return Boolean.parseBoolean((String) value); + } + + return value != null; + } + + public String asString() { + Object value = value(); + + if (value == null) { + return ""; + } + return value.toString(); + } + + /** + * Lazily evaluates the value of this metadata item. + * + * @throws MetadataEvaluationException if computing the metadata value fails. + */ + private synchronized void eval() throws MetadataEvaluationException { + if (cacheStrategy == CacheStrategy.NEVER_CACHE || internalValue.get() == null) { + try { + Object value = lazyValue.call(); + if (value == null) { + value = ACTUALLY_NULL; + } + internalValue = new SoftReference(value); + } catch (Exception e) { + throw new MetadataEvaluationException(e); + } + } + } + + public synchronized void invalidate() { + if (cacheStrategy != CacheStrategy.CACHE_ETERNALLY) { + internalValue.clear(); + } + } + + /** + * Describes possible caching strategies for metadata. + */ + public enum CacheStrategy { + /** + * Once the metadata value has been evaluated, do not re-evaluate the value until it is manually invalidated. + */ + CACHE_AFTER_FIRST_EVAL, + + /** + * Re-evaluate the metadata item every time it is requested + */ + NEVER_CACHE, + + /** + * Once the metadata value has been evaluated, do not re-evaluate the value in spite of manual invalidation. + */ + CACHE_ETERNALLY + } +} diff --git a/paper-api/src/main/java/org/bukkit/metadata/MetadataConversionException.java b/paper-api/src/main/java/org/bukkit/metadata/MetadataConversionException.java new file mode 100644 index 0000000000..ff4d1205ea --- /dev/null +++ b/paper-api/src/main/java/org/bukkit/metadata/MetadataConversionException.java @@ -0,0 +1,13 @@ +package org.bukkit.metadata; + +/** + * A MetadataConversionException is thrown any time a {@link LazyMetadataValue} attempts to convert a metadata value + * to an inappropriate data type. + */ + +@SuppressWarnings("serial") +public class MetadataConversionException extends RuntimeException { + MetadataConversionException(String message) { + super(message); + } +} diff --git a/paper-api/src/main/java/org/bukkit/metadata/MetadataEvaluationException.java b/paper-api/src/main/java/org/bukkit/metadata/MetadataEvaluationException.java new file mode 100644 index 0000000000..6b5ac886ab --- /dev/null +++ b/paper-api/src/main/java/org/bukkit/metadata/MetadataEvaluationException.java @@ -0,0 +1,13 @@ +package org.bukkit.metadata; + +/** + * A MetadataEvaluationException is thrown any time a {@link LazyMetadataValue} fails to evaluate its value due to + * an exception. The originating exception will be included as this exception's cause. + */ + +@SuppressWarnings("serial") +public class MetadataEvaluationException extends RuntimeException { + MetadataEvaluationException(Throwable cause) { + super(cause); + } +} diff --git a/paper-api/src/main/java/org/bukkit/metadata/MetadataStore.java b/paper-api/src/main/java/org/bukkit/metadata/MetadataStore.java new file mode 100644 index 0000000000..0adfa7a006 --- /dev/null +++ b/paper-api/src/main/java/org/bukkit/metadata/MetadataStore.java @@ -0,0 +1,52 @@ +package org.bukkit.metadata; + +import org.bukkit.plugin.Plugin; + +import java.util.List; + +public interface MetadataStore { + /** + * Adds a metadata value to an object. + * + * @param subject The object receiving the metadata. + * @param metadataKey A unique key to identify this metadata. + * @param newMetadataValue The metadata value to apply. + */ + public void setMetadata(T subject, String metadataKey, MetadataValue newMetadataValue); + + /** + * Returns all metadata values attached to an object. If multiple plugins have attached metadata, each will value + * will be included. + * + * @param subject the object being interrogated. + * @param metadataKey the unique metadata key being sought. + * @return A list of values, one for each plugin that has set the requested value. + */ + public List getMetadata(T subject, String metadataKey); + + /** + * Tests to see if a metadata attribute has been set on an object. + * + * @param subject the object upon which the has-metadata test is performed. + * @param metadataKey the unique metadata key being queried. + * @return the existence of the metadataKey within subject. + */ + public boolean hasMetadata(T subject, String metadataKey); + + /** + * Removes a metadata item owned by a plugin from a subject. + * + * @param subject the object to remove the metadata from. + * @param metadataKey the unique metadata key identifying the metadata to remove. + * @param owningPlugin the plugin attempting to remove a metadata item. + */ + public void removeMetadata(T subject, String metadataKey, Plugin owningPlugin); + + /** + * Invalidates all metadata in the metadata store that originates from the given plugin. Doing this will force + * each invalidated metadata item to be recalculated the next time it is accessed. + * + * @param owningPlugin the plugin requesting the invalidation. + */ + public void invalidateAll(Plugin owningPlugin); +} diff --git a/paper-api/src/main/java/org/bukkit/metadata/MetadataStoreBase.java b/paper-api/src/main/java/org/bukkit/metadata/MetadataStoreBase.java new file mode 100644 index 0000000000..0c384abe89 --- /dev/null +++ b/paper-api/src/main/java/org/bukkit/metadata/MetadataStoreBase.java @@ -0,0 +1,143 @@ +package org.bukkit.metadata; + +import org.bukkit.plugin.Plugin; + +import java.util.*; + +public abstract class MetadataStoreBase { + private Map> metadataMap = new HashMap>(); + private WeakHashMap disambiguationCache = new WeakHashMap(); + + /** + * Adds a metadata value to an object. Each metadata value is owned by a specific{@link Plugin}. + * If a plugin has already added a metadata value to an object, that value + * will be replaced with the value of {@code newMetadataValue}. Multiple plugins can set independent values for + * the same {@code metadataKey} without conflict. + * + * Implementation note: I considered using a {@link java.util.concurrent.locks.ReadWriteLock} for controlling + * access to {@code metadataMap}, but decided that the added overhead wasn't worth the finer grained access control. + * Bukkit is almost entirely single threaded so locking overhead shouldn't pose a problem. + * + * @see MetadataStore#setMetadata(Object, String, MetadataValue) + * @param subject The object receiving the metadata. + * @param metadataKey A unique key to identify this metadata. + * @param newMetadataValue The metadata value to apply. + */ + public synchronized void setMetadata(T subject, String metadataKey, MetadataValue newMetadataValue) { + String key = cachedDisambiguate(subject, metadataKey); + if (!metadataMap.containsKey(key)) { + metadataMap.put(key, new ArrayList()); + } + // we now have a list of subject's metadata for the given metadata key. If newMetadataValue's owningPlugin + // is found in this list, replace the value rather than add a new one. + List metadataList = metadataMap.get(key); + for (int i = 0; i < metadataList.size(); i++) { + if (metadataList.get(i).getOwningPlugin().equals(newMetadataValue.getOwningPlugin())) { + metadataList.set(i, newMetadataValue); + return; + } + } + // we didn't find a duplicate...add the new metadata value + metadataList.add(newMetadataValue); + } + + /** + * Returns all metadata values attached to an object. If multiple plugins have attached metadata, each will value + * will be included. + * + * @see MetadataStore#getMetadata(Object, String) + * @param subject the object being interrogated. + * @param metadataKey the unique metadata key being sought. + * @return A list of values, one for each plugin that has set the requested value. + */ + public synchronized List getMetadata(T subject, String metadataKey) { + String key = cachedDisambiguate(subject, metadataKey); + if (metadataMap.containsKey(key)) { + return Collections.unmodifiableList(metadataMap.get(key)); + } else { + return Collections.emptyList(); + } + } + + /** + * Tests to see if a metadata attribute has been set on an object. + * + * @param subject the object upon which the has-metadata test is performed. + * @param metadataKey the unique metadata key being queried. + * @return the existence of the metadataKey within subject. + */ + public synchronized boolean hasMetadata(T subject, String metadataKey) { + String key = cachedDisambiguate(subject, metadataKey); + return metadataMap.containsKey(key); + } + + /** + * Removes a metadata item owned by a plugin from a subject. + * + * @see MetadataStore#removeMetadata(Object, String, org.bukkit.plugin.Plugin) + * @param subject the object to remove the metadata from. + * @param metadataKey the unique metadata key identifying the metadata to remove. + * @param owningPlugin the plugin attempting to remove a metadata item. + */ + public synchronized void removeMetadata(T subject, String metadataKey, Plugin owningPlugin) { + String key = cachedDisambiguate(subject, metadataKey); + List metadataList = metadataMap.get(key); + for (int i = 0; i < metadataList.size(); i++) { + if (metadataList.get(i).getOwningPlugin().equals(owningPlugin)) { + metadataList.remove(i); + } + } + } + + /** + * Invalidates all metadata in the metadata store that originates from the given plugin. Doing this will force + * each invalidated metadata item to be recalculated the next time it is accessed. + * + * @see MetadataStore#invalidateAll(org.bukkit.plugin.Plugin) + * @param owningPlugin the plugin requesting the invalidation. + */ + public synchronized void invalidateAll(Plugin owningPlugin) { + if(owningPlugin == null) { + throw new IllegalArgumentException("owningPlugin cannot be null"); + } + + for (List values : metadataMap.values()) { + for (MetadataValue value : values) { + if (value.getOwningPlugin().equals(owningPlugin)) { + value.invalidate(); + } + } + } + } + + /** + * Caches the results of calls to {@link MetadataStoreBase#disambiguate(Object, String)} in a {@link WeakHashMap}. Doing so maintains a + * canonical list + * of disambiguation strings for objects in memory. When those objects are garbage collected, the disambiguation string + * in the list is aggressively garbage collected as well. + * @param subject The object for which this key is being generated. + * @param metadataKey The name identifying the metadata value. + * @return a unique metadata key for the given subject. + */ + private String cachedDisambiguate(T subject, String metadataKey) { + if (disambiguationCache.containsKey(subject)) { + return disambiguationCache.get(subject); + } else { + String disambiguation = disambiguate(subject, metadataKey); + disambiguationCache.put(subject, disambiguation); + return disambiguation; + } + } + + /** + * Creates a unique name for the object receiving metadata by combining unique data from the subject with a metadataKey. + * The name created must be globally unique for the given object and any two equivalent objects must generate the + * same unique name. For example, two Player objects must generate the same string if they represent the same player, + * even if the objects would fail a reference equality test. + * + * @param subject The object for which this key is being generated. + * @param metadataKey The name identifying the metadata value. + * @return a unique metadata key for the given subject. + */ + protected abstract String disambiguate(T subject, String metadataKey); +} diff --git a/paper-api/src/main/java/org/bukkit/metadata/MetadataValue.java b/paper-api/src/main/java/org/bukkit/metadata/MetadataValue.java new file mode 100644 index 0000000000..761d1ac959 --- /dev/null +++ b/paper-api/src/main/java/org/bukkit/metadata/MetadataValue.java @@ -0,0 +1,73 @@ +package org.bukkit.metadata; + +import org.bukkit.plugin.Plugin; + +public interface MetadataValue { + + /** + * Fetches the value of this metadata item. + * + * @return the metadata value. + */ + public Object value(); + + /** + * Attempts to convert the value of this metadata item into an int. + * @return the value as an int. + */ + public int asInt(); + + /** + * Attempts to convert the value of this metadata item into a float. + * @return the value as a float. + */ + public float asFloat(); + + /** + * Attempts to convert the value of this metadata item into a double. + * @return the value as a double. + */ + public double asDouble(); + + /** + * Attempts to convert the value of this metadata item into a long. + * @return the value as a long. + */ + public long asLong(); + + /** + * Attempts to convert the value of this metadata item into a short. + * @return the value as a short. + */ + public short asShort(); + + /** + * Attempts to convert the value of this metadata item into a byte. + * @return the value as a byte. + */ + public byte asByte(); + + /** + * Attempts to convert the value of this metadata item into a boolean. + * @return the value as a boolean. + */ + public boolean asBoolean(); + + /** + * Attempts to convert the value of this metadata item into a string. + * @return the value as a string. + */ + public String asString(); + + /** + * Returns the {@link Plugin} that created this metadata item. + * + * @return the plugin that owns this metadata value. + */ + public Plugin getOwningPlugin(); + + /** + * Invalidates this metadata item, forcing it to recompute when next accessed. + */ + public void invalidate(); +} diff --git a/paper-api/src/main/java/org/bukkit/metadata/Metadatable.java b/paper-api/src/main/java/org/bukkit/metadata/Metadatable.java new file mode 100644 index 0000000000..956a1960fb --- /dev/null +++ b/paper-api/src/main/java/org/bukkit/metadata/Metadatable.java @@ -0,0 +1,42 @@ +package org.bukkit.metadata; + +import org.bukkit.plugin.Plugin; + +import java.util.List; + +/** + * This interface is implemented by all objects that can provide metadata about themselves. + */ +public interface Metadatable { + /** + * Sets a metadata value in the implementing object's metadata store. + * + * @param metadataKey A unique key to identify this metadata. + * @param newMetadataValue The metadata value to apply. + */ + public void setMetadata(String metadataKey, MetadataValue newMetadataValue); + + /** + * Returns a list of previously set metadata values from the implementing object's metadata store. + * + * @param metadataKey the unique metadata key being sought. + * @return A list of values, one for each plugin that has set the requested value. + */ + public List getMetadata(String metadataKey); + + /** + * Tests to see whether the implementing object contains the given metadata value in its metadata store. + * + * @param metadataKey the unique metadata key being queried. + * @return the existence of the metadataKey within subject. + */ + public boolean hasMetadata(String metadataKey); + + /** + * Removes the given metadata value from the implementing object's metadata store. + * + * @param metadataKey the unique metadata key identifying the metadata to remove. + * @param owningPlugin This plugin's metadata value will be removed. All other values will be left untouched. + */ + public void removeMetadata(String metadataKey, Plugin owningPlugin); +} diff --git a/paper-api/src/main/java/org/bukkit/plugin/Plugin.java b/paper-api/src/main/java/org/bukkit/plugin/Plugin.java index 450252d857..97c882aaaf 100644 --- a/paper-api/src/main/java/org/bukkit/plugin/Plugin.java +++ b/paper-api/src/main/java/org/bukkit/plugin/Plugin.java @@ -14,22 +14,21 @@ import com.avaje.ebean.EbeanServer; /** * Represents a Plugin */ -public interface Plugin extends CommandExecutor { - +public abstract class Plugin implements CommandExecutor { /** * Returns the folder that the plugin data's files are located in. The * folder may not yet exist. * * @return The folder */ - public File getDataFolder(); + public abstract File getDataFolder(); /** * Returns the plugin.yaml file containing the details for this plugin * * @return Contents of the plugin.yaml file */ - public PluginDescriptionFile getDescription(); + public abstract PluginDescriptionFile getDescription(); /** * Gets a {@link FileConfiguration} for this plugin, read through "config.yml" @@ -39,7 +38,7 @@ public interface Plugin extends CommandExecutor { * * @return Plugin configuration */ - public FileConfiguration getConfig(); + public abstract FileConfiguration getConfig(); /** * Gets an embedded resource in this plugin @@ -47,18 +46,18 @@ public interface Plugin extends CommandExecutor { * @param filename Filename of the resource * @return File if found, otherwise null */ - public InputStream getResource(String filename); + public abstract InputStream getResource(String filename); /** * Saves the {@link FileConfiguration} retrievable by {@link #getConfig()}. */ - public void saveConfig(); + public abstract void saveConfig(); /** * Saves the raw contents of the default config.yml file to the location retrievable by {@link #getConfig()}. * If there is no default config.yml embedded in the plugin, an empty config.yml file is saved. */ - public void saveDefaultConfig(); + public abstract void saveDefaultConfig(); /** * Saves the raw contents of any resource embedded with a plugin's .jar file assuming it can be found using @@ -69,70 +68,70 @@ public interface Plugin extends CommandExecutor { * @param replace if true, the embedded resource will overwrite the contents of an existing file. * @throws IllegalArgumentException if the resource path is null, empty, or points to a nonexistent resource. */ - public void saveResource(String resourcePath, boolean replace); + public abstract void saveResource(String resourcePath, boolean replace); /** * Discards any data in {@link #getConfig()} and reloads from disk. */ - public void reloadConfig(); + public abstract void reloadConfig(); /** * Gets the associated PluginLoader responsible for this plugin * * @return PluginLoader that controls this plugin */ - public PluginLoader getPluginLoader(); + public abstract PluginLoader getPluginLoader(); /** * Returns the Server instance currently running this plugin * * @return Server running this plugin */ - public Server getServer(); + public abstract Server getServer(); /** * Returns a value indicating whether or not this plugin is currently enabled * * @return true if this plugin is enabled, otherwise false */ - public boolean isEnabled(); + public abstract boolean isEnabled(); /** * Called when this plugin is disabled */ - public void onDisable(); + public abstract void onDisable(); /** * Called after a plugin is loaded but before it has been enabled. * When mulitple plugins are loaded, the onLoad() for all plugins is called before any onEnable() is called. */ - public void onLoad(); + public abstract void onLoad(); /** * Called when this plugin is enabled */ - public void onEnable(); + public abstract void onEnable(); /** * Simple boolean if we can still nag to the logs about things * * @return boolean whether we can nag */ - public boolean isNaggable(); + public abstract boolean isNaggable(); /** * Set naggable state * * @param canNag is this plugin still naggable? */ - public void setNaggable(boolean canNag); + public abstract void setNaggable(boolean canNag); /** * Gets the {@link EbeanServer} tied to this plugin * * @return Ebean server instance */ - public EbeanServer getDatabase(); + public abstract EbeanServer getDatabase(); /** * Gets a {@link ChunkGenerator} for use in a default world, as specified in the server configuration @@ -141,7 +140,7 @@ public interface Plugin extends CommandExecutor { * @param id Unique ID, if any, that was specified to indicate which generator was requested * @return ChunkGenerator for use in the default world generation */ - public ChunkGenerator getDefaultWorldGenerator(String worldName, String id); + public abstract ChunkGenerator getDefaultWorldGenerator(String worldName, String id); /** * Returns the primary logger associated with this server instance. The returned logger automatically @@ -149,5 +148,35 @@ public interface Plugin extends CommandExecutor { * * @return Logger associated with this server */ - public Logger getLogger(); + public abstract Logger getLogger(); + + /** + * Returns the name of the plugin. + * + * This should return the bare name of the plugin and should be used for comparison. + * + * @return name of the plugin + */ + public String getName() { + return getDescription().getName(); + } + + @Override + public int hashCode() { + return getName().hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof Plugin)) { + return false; + } + return getName().equals(((Plugin) obj).getName()); + } } diff --git a/paper-api/src/main/java/org/bukkit/plugin/java/JavaPlugin.java b/paper-api/src/main/java/org/bukkit/plugin/java/JavaPlugin.java index 10b19e915c..6cc7d9db42 100644 --- a/paper-api/src/main/java/org/bukkit/plugin/java/JavaPlugin.java +++ b/paper-api/src/main/java/org/bukkit/plugin/java/JavaPlugin.java @@ -34,7 +34,7 @@ import com.avaje.ebeaninternal.server.ddl.DdlGenerator; /** * Represents a Java plugin */ -public abstract class JavaPlugin implements Plugin { +public abstract class JavaPlugin extends Plugin { private boolean isEnabled = false; private boolean initialized = false; private PluginLoader loader = null; diff --git a/paper-api/src/main/java/org/bukkit/util/NumberConversions.java b/paper-api/src/main/java/org/bukkit/util/NumberConversions.java index f70cc3141b..9c0b132f56 100644 --- a/paper-api/src/main/java/org/bukkit/util/NumberConversions.java +++ b/paper-api/src/main/java/org/bukkit/util/NumberConversions.java @@ -9,84 +9,78 @@ public final class NumberConversions { public static int toInt(Object object) { if (object instanceof Number) { return ((Number) object).intValue(); - } else { - int result = 0; - - try { - result = Integer.parseInt(object.toString()); - } catch (NumberFormatException ex) {} - - return result; } + + try { + return Integer.valueOf(object.toString()); + } catch (NumberFormatException e) { + } catch (NullPointerException e) { + } + return 0; } public static float toFloat(Object object) { if (object instanceof Number) { return ((Number) object).floatValue(); - } else { - float result = 0; - - try { - result = Float.parseFloat(object.toString()); - } catch (NumberFormatException ex) {} - - return result; } + + try { + return Float.valueOf(object.toString()); + } catch (NumberFormatException e) { + } catch (NullPointerException e) { + } + return 0; } public static double toDouble(Object object) { if (object instanceof Number) { return ((Number) object).doubleValue(); - } else { - double result = 0; - - try { - result = Double.parseDouble(object.toString()); - } catch (NumberFormatException ex) {} - - return result; } + + try { + return Double.valueOf(object.toString()); + } catch (NumberFormatException e) { + } catch (NullPointerException e) { + } + return 0; } public static long toLong(Object object) { if (object instanceof Number) { return ((Number) object).longValue(); - } else { - long result = 0; - - try { - result = Long.parseLong(object.toString()); - } catch (NumberFormatException ex) {} - - return result; } + + try { + return Long.valueOf(object.toString()); + } catch (NumberFormatException e) { + } catch (NullPointerException e) { + } + return 0; } public static short toShort(Object object) { if (object instanceof Number) { return ((Number) object).shortValue(); - } else { - short result = 0; - - try { - result = Short.parseShort(object.toString()); - } catch (NumberFormatException ex) {} - - return result; } + + try { + return Short.valueOf(object.toString()); + } catch (NumberFormatException e) { + } catch (NullPointerException e) { + } + return 0; } public static byte toByte(Object object) { if (object instanceof Number) { return ((Number) object).byteValue(); - } else { - byte result = 0; - - try { - result = Byte.parseByte(object.toString()); - } catch (NumberFormatException ex) {} - - return result; } + + try { + return Byte.valueOf(object.toString()); + } catch (NumberFormatException e) { + } catch (NullPointerException e) { + } + return 0; } } diff --git a/paper-api/src/test/java/org/bukkit/metadata/FixedMetadataValueTest.java b/paper-api/src/test/java/org/bukkit/metadata/FixedMetadataValueTest.java new file mode 100644 index 0000000000..0844f97576 --- /dev/null +++ b/paper-api/src/test/java/org/bukkit/metadata/FixedMetadataValueTest.java @@ -0,0 +1,30 @@ +package org.bukkit.metadata; + +import static org.junit.Assert.assertEquals; + +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.messaging.TestPlugin; +import org.junit.Test; + +public class FixedMetadataValueTest { + private Plugin plugin = new TestPlugin("X"); + private FixedMetadataValue subject; + + private void valueEquals(Object value) { + subject = new FixedMetadataValue(plugin, value); + assertEquals(value, subject.value()); + } + + @Test + public void testTypes() { + valueEquals(10); + valueEquals(0.1); + valueEquals("TEN"); + valueEquals(true); + valueEquals(null); + valueEquals((float) 10.5); + valueEquals((long) 10); + valueEquals((short) 10); + valueEquals((byte) 10); + } +} diff --git a/paper-api/src/test/java/org/bukkit/metadata/LazyMetadataValueTest.java b/paper-api/src/test/java/org/bukkit/metadata/LazyMetadataValueTest.java new file mode 100644 index 0000000000..ece6a0f445 --- /dev/null +++ b/paper-api/src/test/java/org/bukkit/metadata/LazyMetadataValueTest.java @@ -0,0 +1,135 @@ +package org.bukkit.metadata; + +import org.bukkit.plugin.messaging.TestPlugin; +import org.junit.Test; + +import java.util.concurrent.Callable; + +import static org.junit.Assert.*; + +public class LazyMetadataValueTest { + private LazyMetadataValue subject; + private TestPlugin plugin = new TestPlugin("x"); + + @Test + public void testLazyInt() { + int value = 10; + subject = makeSimpleCallable(value); + + assertEquals(value, subject.value()); + } + + @Test + public void testLazyDouble() { + double value = 10.5; + subject = makeSimpleCallable(value); + + assertEquals(value, (Double)subject.value(), 0.01); + } + + @Test + public void testLazyString() { + String value = "TEN"; + subject = makeSimpleCallable(value); + + assertEquals(value, subject.value()); + } + + @Test + public void testLazyBoolean() { + boolean value = false; + subject = makeSimpleCallable(value); + + assertEquals(value, subject.value()); + } + + @Test(expected=MetadataEvaluationException.class) + public void testEvalException() { + subject = new LazyMetadataValue(plugin, LazyMetadataValue.CacheStrategy.CACHE_AFTER_FIRST_EVAL, new Callable() { + public Object call() throws Exception { + throw new RuntimeException("Gotcha!"); + } + }); + subject.value(); + } + + @Test + public void testCacheStrategyCacheAfterFirstEval() { + final Counter counter = new Counter(); + final int value = 10; + subject = new LazyMetadataValue(plugin, LazyMetadataValue.CacheStrategy.CACHE_AFTER_FIRST_EVAL, new Callable() { + public Object call() throws Exception { + counter.increment(); + return value; + } + }); + + subject.value(); + subject.value(); + assertEquals(value, subject.value()); + assertEquals(1, counter.value()); + + subject.invalidate(); + subject.value(); + assertEquals(2, counter.value()); + } + + @Test + public void testCacheStrategyNeverCache() { + final Counter counter = new Counter(); + final int value = 10; + subject = new LazyMetadataValue(plugin, LazyMetadataValue.CacheStrategy.NEVER_CACHE, new Callable() { + public Object call() throws Exception { + counter.increment(); + return value; + } + }); + + subject.value(); + subject.value(); + assertEquals(value, subject.value()); + assertEquals(3, counter.value()); + } + + @Test + public void testCacheStrategyEternally() { + final Counter counter = new Counter(); + final int value = 10; + subject = new LazyMetadataValue(plugin, LazyMetadataValue.CacheStrategy.CACHE_ETERNALLY, new Callable() { + public Object call() throws Exception { + counter.increment(); + return value; + } + }); + + subject.value(); + subject.value(); + assertEquals(value, subject.value()); + assertEquals(1, counter.value()); + + subject.invalidate(); + subject.value(); + assertEquals(value, subject.value()); + assertEquals(1, counter.value()); + } + + private LazyMetadataValue makeSimpleCallable(final Object value) { + return new LazyMetadataValue(plugin, new Callable() { + public Object call() throws Exception { + return value; + } + }); + } + + private class Counter { + private int c = 0; + + public void increment() { + c++; + } + + public int value() { + return c; + } + } +} diff --git a/paper-api/src/test/java/org/bukkit/metadata/MetadataConversionTest.java b/paper-api/src/test/java/org/bukkit/metadata/MetadataConversionTest.java new file mode 100644 index 0000000000..428768adda --- /dev/null +++ b/paper-api/src/test/java/org/bukkit/metadata/MetadataConversionTest.java @@ -0,0 +1,103 @@ +// Copyright (C) 2011 Ryan Michela +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package org.bukkit.metadata; + +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.messaging.TestPlugin; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + */ +public class MetadataConversionTest { + private Plugin plugin = new TestPlugin("x"); + private FixedMetadataValue subject; + + private void setSubject(Object value) { + subject = new FixedMetadataValue(plugin, value); + } + + @Test + public void testFromInt() { + setSubject(10); + + assertEquals(10, subject.asInt()); + assertEquals(10, subject.asFloat(), 0.000001); + assertEquals(10, subject.asDouble(), 0.000001); + assertEquals(10, subject.asLong()); + assertEquals(10, subject.asShort()); + assertEquals(10, subject.asByte()); + assertEquals(true, subject.asBoolean()); + assertEquals("10", subject.asString()); + } + + @Test + public void testFromFloat() { + setSubject(10.5); + + assertEquals(10, subject.asInt()); + assertEquals(10.5, subject.asFloat(), 0.000001); + assertEquals(10.5, subject.asDouble(), 0.000001); + assertEquals(10, subject.asLong()); + assertEquals(10, subject.asShort()); + assertEquals(10, subject.asByte()); + assertEquals(true, subject.asBoolean()); + assertEquals("10.5", subject.asString()); + } + + @Test + public void testFromNumericString() { + setSubject("10"); + + assertEquals(10, subject.asInt()); + assertEquals(10, subject.asFloat(), 0.000001); + assertEquals(10, subject.asDouble(), 0.000001); + assertEquals(10, subject.asLong()); + assertEquals(10, subject.asShort()); + assertEquals(10, subject.asByte()); + assertEquals(false, subject.asBoolean()); + assertEquals("10", subject.asString()); + } + + @Test + public void testFromNonNumericString() { + setSubject("true"); + + assertEquals(0, subject.asInt()); + assertEquals(0, subject.asFloat(), 0.000001); + assertEquals(0, subject.asDouble(), 0.000001); + assertEquals(0, subject.asLong()); + assertEquals(0, subject.asShort()); + assertEquals(0, subject.asByte()); + assertEquals(true, subject.asBoolean()); + assertEquals("true", subject.asString()); + } + + @Test + public void testFromNull() { + setSubject(null); + + assertEquals(0, subject.asInt()); + assertEquals(0, subject.asFloat(), 0.000001); + assertEquals(0, subject.asDouble(), 0.000001); + assertEquals(0, subject.asLong()); + assertEquals(0, subject.asShort()); + assertEquals(0, subject.asByte()); + assertEquals(false, subject.asBoolean()); + assertEquals("", subject.asString()); + } +} diff --git a/paper-api/src/test/java/org/bukkit/metadata/MetadataStoreTest.java b/paper-api/src/test/java/org/bukkit/metadata/MetadataStoreTest.java new file mode 100644 index 0000000000..98a9642321 --- /dev/null +++ b/paper-api/src/test/java/org/bukkit/metadata/MetadataStoreTest.java @@ -0,0 +1,127 @@ +package org.bukkit.metadata; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.List; +import java.util.concurrent.Callable; + +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.messaging.TestPlugin; +import org.junit.Test; + +public class MetadataStoreTest { + private Plugin pluginX = new TestPlugin("x"); + private Plugin pluginY = new TestPlugin("y"); + + StringMetadataStore subject = new StringMetadataStore(); + + @Test + public void testMetadataStore() { + subject.setMetadata("subject", "key", new FixedMetadataValue(pluginX, 10)); + + assertTrue(subject.hasMetadata("subject", "key")); + List values = subject.getMetadata("subject", "key"); + assertEquals(10, values.get(0).value()); + } + + @Test + public void testMetadataNotPresent() { + assertFalse(subject.hasMetadata("subject", "key")); + List values = subject.getMetadata("subject", "key"); + assertTrue(values.isEmpty()); + } + + @Test + public void testInvalidateAll() { + final Counter counter = new Counter(); + + subject.setMetadata("subject", "key", new LazyMetadataValue(pluginX, new Callable() { + public Object call() throws Exception { + counter.increment(); + return 10; + } + })); + + assertTrue(subject.hasMetadata("subject", "key")); + subject.getMetadata("subject", "key").get(0).value(); + subject.invalidateAll(pluginX); + subject.getMetadata("subject", "key").get(0).value(); + assertEquals(2, counter.value()); + } + + @Test + public void testInvalidateAllButActuallyNothing() { + final Counter counter = new Counter(); + + subject.setMetadata("subject", "key", new LazyMetadataValue(pluginX, new Callable() { + public Object call() throws Exception { + counter.increment(); + return 10; + } + })); + + assertTrue(subject.hasMetadata("subject", "key")); + subject.getMetadata("subject", "key").get(0).value(); + subject.invalidateAll(pluginY); + subject.getMetadata("subject", "key").get(0).value(); + assertEquals(1, counter.value()); + } + + @Test + public void testMetadataReplace() { + subject.setMetadata("subject", "key", new FixedMetadataValue(pluginX, 10)); + subject.setMetadata("subject", "key", new FixedMetadataValue(pluginY, 10)); + subject.setMetadata("subject", "key", new FixedMetadataValue(pluginX, 20)); + + for (MetadataValue mv : subject.getMetadata("subject", "key")) { + if (mv.getOwningPlugin().equals(pluginX)) { + assertEquals(20, mv.value()); + } + if (mv.getOwningPlugin().equals(pluginY)) { + assertEquals(10, mv.value()); + } + } + } + + @Test + public void testMetadataRemove() { + subject.setMetadata("subject", "key", new FixedMetadataValue(pluginX, 10)); + subject.setMetadata("subject", "key", new FixedMetadataValue(pluginY, 20)); + subject.removeMetadata("subject", "key", pluginX); + + assertTrue(subject.hasMetadata("subject", "key")); + assertEquals(1, subject.getMetadata("subject", "key").size()); + assertEquals(20, subject.getMetadata("subject", "key").get(0).value()); + } + + @Test + public void testMetadataRemoveForNonExistingPlugin() { + subject.setMetadata("subject", "key", new FixedMetadataValue(pluginX, 10)); + subject.removeMetadata("subject", "key", pluginY); + + assertTrue(subject.hasMetadata("subject", "key")); + assertEquals(1, subject.getMetadata("subject", "key").size()); + assertEquals(10, subject.getMetadata("subject", "key").get(0).value()); + } + + private class StringMetadataStore extends MetadataStoreBase implements MetadataStore { + @Override + protected String disambiguate(String subject, String metadataKey) { + return subject + ":" + metadataKey; + } + } + + private class Counter { + int c = 0; + + public void increment() { + c++; + } + + public int value() { + return c; + } + } +} diff --git a/paper-api/src/test/java/org/bukkit/plugin/messaging/StandardMessengerTest.java b/paper-api/src/test/java/org/bukkit/plugin/messaging/StandardMessengerTest.java index 52e4a0fb9d..ee3d8acf15 100644 --- a/paper-api/src/test/java/org/bukkit/plugin/messaging/StandardMessengerTest.java +++ b/paper-api/src/test/java/org/bukkit/plugin/messaging/StandardMessengerTest.java @@ -11,8 +11,9 @@ public class StandardMessengerTest { return new StandardMessenger(); } + private int count = 0; public TestPlugin getPlugin() { - return new TestPlugin(); + return new TestPlugin("" + count++); } @Test diff --git a/paper-api/src/test/java/org/bukkit/plugin/messaging/TestPlayer.java b/paper-api/src/test/java/org/bukkit/plugin/messaging/TestPlayer.java index 4ecd974581..da6b7d8138 100644 --- a/paper-api/src/test/java/org/bukkit/plugin/messaging/TestPlayer.java +++ b/paper-api/src/test/java/org/bukkit/plugin/messaging/TestPlayer.java @@ -22,6 +22,7 @@ import org.bukkit.event.player.PlayerTeleportEvent.TeleportCause; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.PlayerInventory; import org.bukkit.map.MapView; +import org.bukkit.metadata.MetadataValue; import org.bukkit.permissions.Permission; import org.bukkit.permissions.PermissionAttachment; import org.bukkit.permissions.PermissionAttachmentInfo; @@ -658,4 +659,20 @@ public class TestPlayer implements Player { public EntityType getType() { return EntityType.PLAYER; } + + public void setMetadata(String metadataKey, MetadataValue newMetadataValue) { + throw new UnsupportedOperationException("Not supported yet."); + } + + public List getMetadata(String metadataKey) { + throw new UnsupportedOperationException("Not supported yet."); + } + + public boolean hasMetadata(String metadataKey) { + throw new UnsupportedOperationException("Not supported yet."); + } + + public void removeMetadata(String metadataKey, Plugin owningPlugin) { + throw new UnsupportedOperationException("Not supported yet."); + } } diff --git a/paper-api/src/test/java/org/bukkit/plugin/messaging/TestPlugin.java b/paper-api/src/test/java/org/bukkit/plugin/messaging/TestPlugin.java index a03581c3c9..f14e2e7503 100644 --- a/paper-api/src/test/java/org/bukkit/plugin/messaging/TestPlugin.java +++ b/paper-api/src/test/java/org/bukkit/plugin/messaging/TestPlugin.java @@ -1,8 +1,8 @@ package org.bukkit.plugin.messaging; -import com.avaje.ebean.EbeanServer; import java.io.File; import java.io.InputStream; + import org.bukkit.Server; import org.bukkit.command.Command; import org.bukkit.command.CommandSender; @@ -13,19 +13,31 @@ import org.bukkit.plugin.PluginDescriptionFile; import org.bukkit.plugin.PluginLoader; import org.bukkit.plugin.PluginLogger; -public class TestPlugin implements Plugin { +import com.avaje.ebean.EbeanServer; + +public class TestPlugin extends Plugin { private boolean enabled = true; + final private String pluginName; + + public TestPlugin(String pluginName) { + this.pluginName = pluginName; + } + public void setEnabled(boolean enabled) { this.enabled = enabled; } + public String getName() { + return pluginName; + } + public File getDataFolder() { throw new UnsupportedOperationException("Not supported."); } public PluginDescriptionFile getDescription() { - throw new UnsupportedOperationException("Not supported."); + return new PluginDescriptionFile(pluginName, "1.0", "test.test"); } public FileConfiguration getConfig() { @@ -100,4 +112,19 @@ public class TestPlugin implements Plugin { throw new UnsupportedOperationException("Not supported."); } + @Override + public int hashCode() { + return getName().hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + return getName().equals(((TestPlugin) obj).getName()); + } }