package org.bukkit; import com.google.common.base.Preconditions; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Locale; import java.util.OptionalInt; import java.util.UUID; import net.kyori.adventure.key.Key; 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 implements Key, com.destroystokyo.paper.Namespaced { /** * 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; /** * Create a key in a specific namespace. *
* For most plugin related code, you should prefer using the * {@link NamespacedKey#NamespacedKey(Plugin, String)} constructor. * * @param namespace namespace * @param key key * @see #NamespacedKey(Plugin, String) */ public NamespacedKey(@NotNull String namespace, @NotNull String key) { Preconditions.checkArgument(namespace != null, "Namespace cannot be null"); Preconditions.checkArgument(key != null, "Key cannot be null"); this.namespace = namespace; this.key = key; this.validate(); } /** * 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 this.validate(); } private void validate() { Preconditions.checkArgument(this.namespace.length() + 1 + this.key.length() <= Short.MAX_VALUE, "NamespacedKey must be less than 32768 characters"); checkError("[a-z0-9_-.]", "namespace", this.namespace, Key.checkNamespace(this.namespace)); checkError("[a-z0-9_-./]", "key", this.key, Key.checkValue(this.key)); } private static void checkError(String pattern, String name, String value, OptionalInt index) { index.ifPresent(indexValue -> { char character = value.charAt(indexValue); throw new IllegalArgumentException(String.format("Non %s character in %s '%s' at index %d ('%s', bytes: %s)", pattern, name, value, indexValue, character, Arrays.toString(String.valueOf(character).getBytes(StandardCharsets.UTF_8)))); }); } @NotNull @Override public String getNamespace() { return this.namespace; } @NotNull @Override public String getKey() { return this.key; } @Override public int hashCode() { int result = this.namespace.hashCode(); result = (31 * result) + this.key.hashCode(); return result; } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (!(obj instanceof Key key)) return false; return this.namespace.equals(key.namespace()) && this.key.equals(key.value()); } @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 @Deprecated(since = "1.20.5") 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, "Input string must not be null");
if (string.isEmpty() || string.length() > Short.MAX_VALUE) return 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() || !Key.parseableValue(value)) {
return null;
}
return (defaultNamespace != null) ? new NamespacedKey(defaultNamespace, value) : minecraft(value);
} else if (components.length == 2 && !Key.parseableValue(key)) {
return null;
}
String namespace = components[0];
if (namespace.isEmpty()) {
return (defaultNamespace != null) ? new NamespacedKey(defaultNamespace, key) : minecraft(key);
}
if (!Key.parseableNamespace(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); } @NotNull @Override public String namespace() { return this.getNamespace(); } @NotNull @Override public String value() { return this.getKey(); } @NotNull @Override public String asString() { return this.toString(); } }