package org.bukkit; import com.google.common.base.Preconditions; import java.util.Locale; import java.util.UUID; import org.bukkit.plugin.Plugin; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; /** * Represents a String based key which consists of two components - a namespace * and a key. * * Namespaces may only contain lowercase alphanumeric characters, periods, * underscores, and hyphens. *

* Keys may only contain lowercase alphanumeric characters, periods, * underscores, hyphens, and forward slashes. * */ public final class NamespacedKey { /** * The namespace representing all inbuilt keys. */ public static final String MINECRAFT = "minecraft"; /** * The namespace representing all keys generated by Bukkit for backwards * compatibility measures. */ public static final String BUKKIT = "bukkit"; // private final String namespace; private final String key; private static boolean isValidNamespaceChar(char c) { return (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '.' || c == '_' || c == '-'; } private static boolean isValidKeyChar(char c) { return isValidNamespaceChar(c) || c == '/'; } private static boolean isValidNamespace(String namespace) { int len = namespace.length(); if (len == 0) { return false; } for (int i = 0; i < len; i++) { if (!isValidNamespaceChar(namespace.charAt(i))) { return false; } } return true; } private static boolean isValidKey(String key) { int len = key.length(); if (len == 0) { return false; } for (int i = 0; i < len; i++) { if (!isValidKeyChar(key.charAt(i))) { return false; } } return true; } /** * Create a key in a specific namespace. * * @param namespace namespace * @param key key * @apiNote should never be used by plugins, for internal use only!! */ @ApiStatus.Internal public NamespacedKey(@NotNull String namespace, @NotNull String key) { Preconditions.checkArgument(namespace != null && isValidNamespace(namespace), "Invalid namespace. Must be [a-z0-9._-]: %s", namespace); Preconditions.checkArgument(key != null && isValidKey(key), "Invalid key. Must be [a-z0-9/._-]: %s", key); this.namespace = namespace; this.key = key; String string = toString(); Preconditions.checkArgument(string.length() < 256, "NamespacedKey must be less than 256 characters", string); } /** * Create a key in the plugin's namespace. *

* Namespaces may only contain lowercase alphanumeric characters, periods, * underscores, and hyphens. *

* Keys may only contain lowercase alphanumeric characters, periods, * underscores, hyphens, and forward slashes. * * @param plugin the plugin to use for the namespace * @param key the key to create */ public NamespacedKey(@NotNull Plugin plugin, @NotNull String key) { Preconditions.checkArgument(plugin != null, "Plugin cannot be null"); Preconditions.checkArgument(key != null, "Key cannot be null"); this.namespace = plugin.getName().toLowerCase(Locale.ROOT); this.key = key.toLowerCase(Locale.ROOT); // Check validity after normalization Preconditions.checkArgument(isValidNamespace(this.namespace), "Invalid namespace. Must be [a-z0-9._-]: %s", this.namespace); Preconditions.checkArgument(isValidKey(this.key), "Invalid key. Must be [a-z0-9/._-]: %s", this.key); String string = toString(); Preconditions.checkArgument(string.length() < 256, "NamespacedKey must be less than 256 characters (%s)", string); } @NotNull public String getNamespace() { return namespace; } @NotNull public String getKey() { return key; } @Override public int hashCode() { int hash = 5; hash = 47 * hash + this.namespace.hashCode(); hash = 47 * hash + this.key.hashCode(); return hash; } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } final NamespacedKey other = (NamespacedKey) obj; return this.namespace.equals(other.namespace) && this.key.equals(other.key); } @Override public String toString() { return this.namespace + ":" + this.key; } /** * Return a new random key in the {@link #BUKKIT} namespace. * * @return new key * @deprecated should never be used by plugins, for internal use only!! */ @ApiStatus.Internal @NotNull public static NamespacedKey randomKey() { return new NamespacedKey(BUKKIT, UUID.randomUUID().toString()); } /** * Get a key in the Minecraft namespace. * * @param key the key to use * @return new key in the Minecraft namespace */ @NotNull public static NamespacedKey minecraft(@NotNull String key) { return new NamespacedKey(MINECRAFT, key); } /** * Get a NamespacedKey from the supplied string with a default namespace if * a namespace is not defined. This is a utility method meant to fetch a * NamespacedKey from user input. Please note that casing does matter and * any instance of uppercase characters will be considered invalid. The * input contract is as follows: *

     * fromString("foo", plugin) -{@literal >} "plugin:foo"
     * fromString("foo:bar", plugin) -{@literal >} "foo:bar"
     * fromString(":foo", null) -{@literal >} "minecraft:foo"
     * fromString("foo", null) -{@literal >} "minecraft:foo"
     * fromString("Foo", plugin) -{@literal >} null
     * fromString(":Foo", plugin) -{@literal >} null
     * fromString("foo:bar:bazz", plugin) -{@literal >} null
     * fromString("", plugin) -{@literal >} null
     * 
* * @param string the string to convert to a NamespacedKey * @param defaultNamespace the default namespace to use if none was * supplied. If null, the {@code minecraft} namespace * ({@link #minecraft(String)}) will be used * @return the created NamespacedKey. null if invalid key * @see #fromString(String) */ @Nullable public static NamespacedKey fromString(@NotNull String string, @Nullable Plugin defaultNamespace) { Preconditions.checkArgument(string != null && !string.isEmpty(), "Input string must not be empty or null"); String[] components = string.split(":", 3); if (components.length > 2) { return null; } String key = (components.length == 2) ? components[1] : ""; if (components.length == 1) { String value = components[0]; if (value.isEmpty() || !isValidKey(value)) { return null; } return (defaultNamespace != null) ? new NamespacedKey(defaultNamespace, value) : minecraft(value); } else if (components.length == 2 && !isValidKey(key)) { return null; } String namespace = components[0]; if (namespace.isEmpty()) { return (defaultNamespace != null) ? new NamespacedKey(defaultNamespace, key) : minecraft(key); } if (!isValidNamespace(namespace)) { return null; } return new NamespacedKey(namespace, key); } /** * Get a NamespacedKey from the supplied string. * * The default namespace will be Minecraft's (i.e. * {@link #minecraft(String)}). * * @param key the key to convert to a NamespacedKey * @return the created NamespacedKey. null if invalid * @see #fromString(String, Plugin) */ @Nullable public static NamespacedKey fromString(@NotNull String key) { return fromString(key, null); } }