Java tutorial
/* * This file is part of UltimateCore, licensed under the MIT License (MIT). * * Copyright (c) Bammerbom * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package bammerbom.ultimatecore.bukkit.resources.utils; import static bammerbom.ultimatecore.bukkit.resources.utils.TextualComponent.rawText; import com.google.common.base.Preconditions; import com.google.common.collect.BiMap; import com.google.common.collect.ImmutableBiMap; import com.google.common.collect.ImmutableMap; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.google.gson.stream.JsonWriter; import java.io.IOException; import java.io.StringWriter; import java.lang.reflect.*; import java.util.*; import java.util.logging.Level; import org.apache.commons.lang.Validate; import org.bukkit.*; import org.bukkit.command.CommandSender; import org.bukkit.configuration.serialization.ConfigurationSerializable; import org.bukkit.configuration.serialization.ConfigurationSerialization; import org.bukkit.entity.EntityType; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; /** * Represents an object that can be serialized to a JSON writer instance. */ interface JsonRepresentedObject { /** * Writes the JSON representation of this object to the specified writer. * * @param writer The JSON writer which will receive the object. * @throws IOException If an error occurs writing to the stream. */ public void writeJson(JsonWriter writer) throws IOException; } /** * Represents a formattable message. Such messages can use elements such as * colors, formatting codes, hover and click data, and other features provided * by the vanilla Minecraft * <a href="http://minecraft.gamepedia.com/Tellraw#Raw_JSON_Text">JSON message * formatter</a>. This class allows plugins to emulate the functionality of the * vanilla Minecraft * <a href="http://minecraft.gamepedia.com/Commands#tellraw">tellraw * command</a>. * <p> * This class follows the builder pattern, allowing for method chaining. It is * set up such that invocations of property-setting methods will affect the * current editing component, and a call to {@link #then()} or * {@link #then(Object)} will append a new editing component to the end of the * message, optionally initializing it with text. Further property-setting * method calls will affect that editing component. * </p> */ public class MessageUtil implements JsonRepresentedObject, Cloneable, Iterable<MessagePart>, ConfigurationSerializable { private static Constructor<?> nmsPacketPlayOutChatConstructor; // The ChatSerializer's instance of Gson private static Object nmsChatSerializerGsonInstance; private static Method fromJsonMethod; private static JsonParser _stringParser = new JsonParser(); static { ConfigurationSerialization.registerClass(MessageUtil.class); } /** * Deserializes a JSON-represented message from a mapping of key-value * pairs. This is called by the Bukkit serialization API. It is not intended * for direct public API consumption. * * @param serialized The key-value mapping which represents a fancy message. */ @SuppressWarnings(value = "unchecked") public static MessageUtil deserialize(Map<String, Object> serialized) { MessageUtil msg = new MessageUtil(); msg.messageParts = (List<MessagePart>) serialized.get("messageParts"); msg.jsonString = serialized.containsKey("JSON") ? serialized.get("JSON").toString() : null; msg.dirty = !serialized.containsKey("JSON"); return msg; } /** * Deserializes a fancy message from its JSON representation. This JSON * representation is of the format of that returned by * {@link #toJSONString()}, and is compatible with vanilla inputs. * * @param json The JSON string which represents a fancy message. * @return A {@code MessageUtil} representing the parameterized JSON * message. */ public static MessageUtil deserialize(String json) { JsonObject serialized = _stringParser.parse(json).getAsJsonObject(); JsonArray extra = serialized.getAsJsonArray("extra"); // Get the extra component MessageUtil returnVal = new MessageUtil(); returnVal.messageParts.clear(); for (JsonElement mPrt : extra) { MessagePart component = new MessagePart(); JsonObject messagePart = mPrt.getAsJsonObject(); for (Map.Entry<String, JsonElement> entry : messagePart.entrySet()) { // Deserialize text if (TextualComponent.isTextKey(entry.getKey())) { // The map mimics the YAML serialization, which has a "key" field and one or more "value" fields Map<String, Object> serializedMapForm = new HashMap<>(); // Must be object due to Bukkit serializer API compliance serializedMapForm.put("key", entry.getKey()); if (entry.getValue().isJsonPrimitive()) { // Assume string serializedMapForm.put("value", entry.getValue().getAsString()); } else { // Composite object, but we assume each element is a string for (Map.Entry<String, JsonElement> compositeNestedElement : entry.getValue() .getAsJsonObject().entrySet()) { serializedMapForm.put("value." + compositeNestedElement.getKey(), compositeNestedElement.getValue().getAsString()); } } component.text = TextualComponent.deserialize(serializedMapForm); } else if (MessagePart.stylesToNames.inverse().containsKey(entry.getKey())) { if (entry.getValue().getAsBoolean()) { component.styles.add(MessagePart.stylesToNames.inverse().get(entry.getKey())); } } else if (entry.getKey().equals("color")) { component.color = ChatColor.valueOf(entry.getValue().getAsString().toUpperCase()); } else if (entry.getKey().equals("clickEvent")) { JsonObject object = entry.getValue().getAsJsonObject(); component.clickActionName = object.get("action").getAsString(); component.clickActionData = object.get("value").getAsString(); } else if (entry.getKey().equals("hoverEvent")) { JsonObject object = entry.getValue().getAsJsonObject(); component.hoverActionName = object.get("action").getAsString(); if (object.get("value").isJsonPrimitive()) { // Assume string component.hoverActionData = new JsonString(object.get("value").getAsString()); } else { // Assume composite type // The only composite type we currently store is another MessageUtil // Therefore, recursion time! component.hoverActionData = deserialize(object.get("value") .toString() /* This should properly serialize the JSON object as a JSON string */); } } else if (entry.getKey().equals("insertion")) { component.insertionData = entry.getValue().getAsString(); } else if (entry.getKey().equals("with")) { for (JsonElement object : entry.getValue().getAsJsonArray()) { if (object.isJsonPrimitive()) { component.translationReplacements.add(new JsonString(object.getAsString())); } else { // Only composite type stored in this array is - again - MessageUtils // Recurse within this function to parse this as a translation replacement component.translationReplacements.add(deserialize(object.toString())); } } } } returnVal.messageParts.add(component); } return returnVal; } private List<MessagePart> messageParts; private String jsonString; private boolean dirty; /** * Creates a JSON message with text. * * @param firstPartText The existing text in the message. */ public MessageUtil(final String firstPartText) { this(rawText(firstPartText)); } public MessageUtil(final TextualComponent firstPartText) { messageParts = new ArrayList<>(); messageParts.add(new MessagePart(firstPartText)); jsonString = null; dirty = false; if (nmsPacketPlayOutChatConstructor == null) { try { nmsPacketPlayOutChatConstructor = Reflection.getNMSClass("PacketPlayOutChat") .getDeclaredConstructor(Reflection.getNMSClass("IChatBaseComponent")); nmsPacketPlayOutChatConstructor.setAccessible(true); } catch (NoSuchMethodException e) { Bukkit.getLogger().log(Level.SEVERE, "Could not find Minecraft method or constructor.", e); } catch (SecurityException e) { Bukkit.getLogger().log(Level.WARNING, "Could not access constructor.", e); } } } /** * Creates a JSON message without text. */ public MessageUtil() { this((TextualComponent) null); } @Override public MessageUtil clone() throws CloneNotSupportedException { MessageUtil instance = (MessageUtil) super.clone(); messageParts = new ArrayList<>(messageParts.size()); for (int i = 0; i < messageParts.size(); i++) { messageParts.add(i, messageParts.get(i).clone()); } instance.dirty = false; instance.jsonString = null; return instance; } /** * Sets the text of the current editing component to a value. * * @param text The new text of the current editing component. * @return This builder instance. */ public MessageUtil text(String text) { MessagePart latest = latest(); latest.text = rawText(text); dirty = true; return this; } /** * Sets the text of the current editing component to a value. * * @param text The new text of the current editing component. * @return This builder instance. */ public MessageUtil text(TextualComponent text) { MessagePart latest = latest(); latest.text = text; dirty = true; return this; } /** * Sets the color of the current editing component to a value. * * @param color The new color of the current editing component. * @return This builder instance. * @throws IllegalArgumentException If the specified {@code ChatColor} * enumeration value is not a color (but a format value). */ public MessageUtil color(final ChatColor color) { if (!color.isColor()) { throw new IllegalArgumentException(color.name() + " is not a color"); } latest().color = color; dirty = true; return this; } /** * Sets the stylization of the current editing component. * * @param styles The array of styles to apply to the editing component. * @return This builder instance. * @throws IllegalArgumentException If any of the enumeration values in the * array do not represent formatters. */ public MessageUtil style(ChatColor... styles) { for (final ChatColor style : styles) { if (!style.isFormat()) { throw new IllegalArgumentException(style.name() + " is not a style"); } } latest().styles.addAll(Arrays.asList(styles)); dirty = true; return this; } /** * Set the behavior of the current editing component to instruct the client * to open a file on the client side filesystem when the currently edited * part of the {@code MessageUtil} is clicked. * * @param path The path of the file on the client filesystem. * @return This builder instance. */ public MessageUtil file(final String path) { onClick("open_file", path); return this; } /** * Set the behavior of the current editing component to instruct the client * to open a webpage in the client's web browser when the currently edited * part of the {@code MessageUtil} is clicked. * * @param url The URL of the page to open when the link is clicked. * @return This builder instance. */ public MessageUtil link(final String url) { onClick("open_url", url); return this; } /** * Set the behavior of the current editing component to instruct the client * to replace the chat input box content with the specified string when the * currently edited part of the {@code MessageUtil} is clicked. The client * will not immediately send the command to the server to be executed unless * the client player submits the command/chat message, usually with the * enter key. * * @param command The text to display in the chat bar of the client. * @return This builder instance. */ public MessageUtil suggest(final String command) { onClick("suggest_command", command); return this; } /** * Set the behavior of the current editing component to instruct the client * to append the chat input box content with the specified string when the * currently edited part of the {@code MessageUtil} is SHIFT-CLICKED. The * client will not immediately send the command to the server to be executed * unless the client player submits the command/chat message, usually with * the enter key. * * @param command The text to append to the chat bar of the client. * @return This builder instance. */ public MessageUtil insert(final String command) { latest().insertionData = command; dirty = true; return this; } /** * Set the behavior of the current editing component to instruct the client * to send the specified string to the server as a chat message when the * currently edited part of the {@code MessageUtil} is clicked. The client * <b>will</b> immediately send the command to the server to be executed * when the editing component is clicked. * * @param command The text to display in the chat bar of the client. * @return This builder instance. */ public MessageUtil command(final String command) { onClick("run_command", command); return this; } /** * Set the behavior of the current editing component to display information * about an achievement when the client hovers over the text. * <p> * Tooltips do not inherit display characteristics, such as color and * styles, from the message component on which they are applied.</p> * * @param name The name of the achievement to display, excluding the * "achievement." prefix. * @return This builder instance. */ public MessageUtil achievementTooltip(final String name) { onHover("show_achievement", new JsonString("achievement." + (name.contains(".") ? name.split("\\.")[1] : name))); return this; } /** * Set the behavior of the current editing component to display information * about an achievement when the client hovers over the text. * <p> * Tooltips do not inherit display characteristics, such as color and * styles, from the message component on which they are applied.</p> * * @param which The achievement to display. * @return This builder instance. */ public MessageUtil achievementTooltip(final Achievement which) { try { Object achievement = Reflection .getMethod(Reflection.getOBCClass("CraftStatistic"), "getNMSAchievement", Achievement.class) .invoke(null, which); return achievementTooltip(((String) ReflectionUtil.execute("name", achievement).fetch())); } catch (IllegalAccessException e) { Bukkit.getLogger().log(Level.WARNING, "Could not access method.", e); return this; } catch (IllegalArgumentException e) { Bukkit.getLogger().log(Level.WARNING, "Argument could not be passed.", e); return this; } catch (InvocationTargetException e) { Bukkit.getLogger().log(Level.WARNING, "A error has occured durring invoking of method.", e); return this; } catch (Exception ex) { Bukkit.getLogger().log(Level.WARNING, "A error has occured durring invoking of method.", ex); return this; } } /** * Set the behavior of the current editing component to display information * about a parameterless statistic when the client hovers over the text. * <p> * Tooltips do not inherit display characteristics, such as color and * styles, from the message component on which they are applied.</p> * * @param which The statistic to display. * @return This builder instance. * @throws IllegalArgumentException If the statistic requires a parameter * which was not supplied. */ public MessageUtil statisticTooltip(final Statistic which) { Statistic.Type type = which.getType(); if (type != Statistic.Type.UNTYPED) { throw new IllegalArgumentException("That statistic requires an additional " + type + " parameter!"); } try { Object statistic = Reflection .getMethod(Reflection.getOBCClass("CraftStatistic"), "getNMSStatistic", Statistic.class) .invoke(null, which); return achievementTooltip(((String) ReflectionUtil.execute("name", statistic).fetch())); } catch (IllegalAccessException e) { Bukkit.getLogger().log(Level.WARNING, "Could not access method.", e); return this; } catch (IllegalArgumentException e) { Bukkit.getLogger().log(Level.WARNING, "Argument could not be passed.", e); return this; } catch (InvocationTargetException e) { Bukkit.getLogger().log(Level.WARNING, "A error has occured durring invoking of method.", e); return this; } catch (Exception ex) { Bukkit.getLogger().log(Level.WARNING, "A error has occured durring invoking of method.", ex); return this; } } /** * Set the behavior of the current editing component to display information * about a statistic parameter with a material when the client hovers over * the text. * <p> * Tooltips do not inherit display characteristics, such as color and * styles, from the message component on which they are applied.</p> * * @param which The statistic to display. * @param item The sole material parameter to the statistic. * @return This builder instance. * @throws IllegalArgumentException If the statistic requires a parameter * which was not supplied, or was supplied a parameter that was not * required. */ public MessageUtil statisticTooltip(final Statistic which, Material item) { Statistic.Type type = which.getType(); if (type == Statistic.Type.UNTYPED) { throw new IllegalArgumentException("That statistic needs no additional parameter!"); } if ((type == Statistic.Type.BLOCK && item.isBlock()) || type == Statistic.Type.ENTITY) { throw new IllegalArgumentException("Wrong parameter type for that statistic - needs " + type + "!"); } try { Object statistic = Reflection.getMethod(Reflection.getOBCClass("CraftStatistic"), "getMaterialStatistic", Statistic.class, Material.class).invoke(null, which, item); return achievementTooltip(((String) ReflectionUtil.execute("name", statistic).fetch())); } catch (IllegalAccessException e) { Bukkit.getLogger().log(Level.WARNING, "Could not access method.", e); return this; } catch (IllegalArgumentException e) { Bukkit.getLogger().log(Level.WARNING, "Argument could not be passed.", e); return this; } catch (InvocationTargetException e) { Bukkit.getLogger().log(Level.WARNING, "A error has occured durring invoking of method.", e); return this; } catch (Exception ex) { Bukkit.getLogger().log(Level.WARNING, "A error has occured durring invoking of method.", ex); return this; } } /** * Set the behavior of the current editing component to display information * about a statistic parameter with an entity type when the client hovers * over the text. * <p> * Tooltips do not inherit display characteristics, such as color and * styles, from the message component on which they are applied.</p> * * @param which The statistic to display. * @param entity The sole entity type parameter to the statistic. * @return This builder instance. * @throws IllegalArgumentException If the statistic requires a parameter * which was not supplied, or was supplied a parameter that was not * required. */ public MessageUtil statisticTooltip(final Statistic which, EntityType entity) { Statistic.Type type = which.getType(); if (type == Statistic.Type.UNTYPED) { throw new IllegalArgumentException("That statistic needs no additional parameter!"); } if (type != Statistic.Type.ENTITY) { throw new IllegalArgumentException("Wrong parameter type for that statistic - needs " + type + "!"); } try { Object statistic = Reflection.getMethod(Reflection.getOBCClass("CraftStatistic"), "getEntityStatistic", Statistic.class, EntityType.class).invoke(null, which, entity); return achievementTooltip(((String) ReflectionUtil.execute("name", statistic).fetch())); } catch (IllegalAccessException e) { Bukkit.getLogger().log(Level.WARNING, "Could not access method.", e); return this; } catch (IllegalArgumentException e) { Bukkit.getLogger().log(Level.WARNING, "Argument could not be passed.", e); return this; } catch (InvocationTargetException e) { Bukkit.getLogger().log(Level.WARNING, "A error has occured durring invoking of method.", e); return this; } catch (Exception ex) { Bukkit.getLogger().log(Level.WARNING, "A error has occured durring invoking of method.", ex); return this; } } /** * Set the behavior of the current editing component to display information * about an item when the client hovers over the text. * <p> * Tooltips do not inherit display characteristics, such as color and * styles, from the message component on which they are applied.</p> * * @param itemJSON A string representing the JSON-serialized NBT data tag of * an {@link ItemStack}. * @return This builder instance. */ public MessageUtil itemTooltip(final String itemJSON) { onHover("show_item", new JsonString(itemJSON)); // Seems a bit hacky, considering we have a JSON object as a parameter return this; } /** * Set the behavior of the current editing component to display information * about an item when the client hovers over the text. * <p> * Tooltips do not inherit display characteristics, such as color and * styles, from the message component on which they are applied.</p> * * @param itemStack The stack for which to display information. * @return This builder instance. */ public MessageUtil itemTooltip(final ItemStack itemStack) { try { Object nmsItem = Reflection .getMethod(Reflection.getOBCClass("inventory.CraftItemStack"), "asNMSCopy", ItemStack.class) .invoke(null, itemStack); return itemTooltip(Reflection .getMethod(Reflection.getNMSClass("ItemStack"), "save", Reflection.getNMSClass("NBTTagCompound")) .invoke(nmsItem, Reflection.getNMSClass("NBTTagCompound").newInstance()).toString()); } catch (Exception e) { e.printStackTrace(); return this; } } /** * Set the behavior of the current editing component to display raw text * when the client hovers over the text. * <p> * Tooltips do not inherit display characteristics, such as color and * styles, from the message component on which they are applied.</p> * * @param text The text, which supports newlines, which will be displayed to * the client upon hovering. * @return This builder instance. */ public MessageUtil tooltip(final String text) { onHover("show_text", new JsonString(text)); return this; } /** * Set the behavior of the current editing component to display raw text * when the client hovers over the text. * <p> * Tooltips do not inherit display characteristics, such as color and * styles, from the message component on which they are applied.</p> * * @param lines The lines of text which will be displayed to the client upon * hovering. The iteration order of this object will be the order in which * the lines of the tooltip are created. * @return This builder instance. */ public MessageUtil tooltip(final Iterable<String> lines) { tooltip(ArrayWrapper.toArray(lines, String.class)); return this; } /* /** * If the text is a translatable key, and it has replaceable values, this function can be used to set the replacements that will be used in the message. * @param replacements The replacements, in order, that will be used in the language-specific message. * @return This builder instance. */ /* ------------ public MessageUtil translationReplacements(final Iterable<? extends CharSequence> replacements){ for(CharSequence str : replacements){ latest().translationReplacements.add(new JsonString(str)); } return this; } */ /** * Set the behavior of the current editing component to display raw text * when the client hovers over the text. * <p> * Tooltips do not inherit display characteristics, such as color and * styles, from the message component on which they are applied.</p> * * @param lines The lines of text which will be displayed to the client upon * hovering. * @return This builder instance. */ public MessageUtil tooltip(final String... lines) { StringBuilder builder = new StringBuilder(); for (int i = 0; i < lines.length; i++) { builder.append(lines[i]); if (i != lines.length - 1) { builder.append('\n'); } } tooltip(builder.toString()); return this; } /** * Set the behavior of the current editing component to display formatted * text when the client hovers over the text. * <p> * Tooltips do not inherit display characteristics, such as color and * styles, from the message component on which they are applied.</p> * * @param text The formatted text which will be displayed to the client upon * hovering. * @return This builder instance. */ public MessageUtil formattedTooltip(MessageUtil text) { for (MessagePart component : text.messageParts) { if (component.clickActionData != null && component.clickActionName != null) { throw new IllegalArgumentException("The tooltip text cannot have click data."); } else if (component.hoverActionData != null && component.hoverActionName != null) { throw new IllegalArgumentException("The tooltip text cannot have a tooltip."); } } onHover("show_text", text); return this; } /** * Set the behavior of the current editing component to display the * specified lines of formatted text when the client hovers over the text. * <p> * Tooltips do not inherit display characteristics, such as color and * styles, from the message component on which they are applied.</p> * * @param lines The lines of formatted text which will be displayed to the * client upon hovering. * @return This builder instance. */ public MessageUtil formattedTooltip(MessageUtil... lines) { if (lines.length < 1) { onHover(null, null); // Clear tooltip return this; } MessageUtil result = new MessageUtil(); result.messageParts.clear(); // Remove the one existing text component that exists by default, which destabilizes the object for (int i = 0; i < lines.length; i++) { try { for (MessagePart component : lines[i]) { if (component.clickActionData != null && component.clickActionName != null) { throw new IllegalArgumentException("The tooltip text cannot have click data."); } else if (component.hoverActionData != null && component.hoverActionName != null) { throw new IllegalArgumentException("The tooltip text cannot have a tooltip."); } if (component.hasText()) { result.messageParts.add(component.clone()); } } if (i != lines.length - 1) { result.messageParts.add(new MessagePart(rawText("\n"))); } } catch (CloneNotSupportedException e) { Bukkit.getLogger().log(Level.WARNING, "Failed to clone object", e); return this; } } return formattedTooltip(result.messageParts.isEmpty() ? null : result); // Throws NPE if size is 0, intended } /** * Set the behavior of the current editing component to display the * specified lines of formatted text when the client hovers over the text. * <p> * Tooltips do not inherit display characteristics, such as color and * styles, from the message component on which they are applied.</p> * * @param lines The lines of text which will be displayed to the client upon * hovering. The iteration order of this object will be the order in which * the lines of the tooltip are created. * @return This builder instance. */ public MessageUtil formattedTooltip(final Iterable<MessageUtil> lines) { return formattedTooltip(ArrayWrapper.toArray(lines, MessageUtil.class)); } /** * If the text is a translatable key, and it has replaceable values, this * function can be used to set the replacements that will be used in the * message. * * @param replacements The replacements, in order, that will be used in the * language-specific message. * @return This builder instance. */ public MessageUtil translationReplacements(final String... replacements) { for (String str : replacements) { latest().translationReplacements.add(new JsonString(str)); } dirty = true; return this; } /** * If the text is a translatable key, and it has replaceable values, this * function can be used to set the replacements that will be used in the * message. * * @param replacements The replacements, in order, that will be used in the * language-specific message. * @return This builder instance. */ public MessageUtil translationReplacements(final MessageUtil... replacements) { for (MessageUtil str : replacements) { latest().translationReplacements.add(str); } dirty = true; return this; } /** * If the text is a translatable key, and it has replaceable values, this * function can be used to set the replacements that will be used in the * message. * * @param replacements The replacements, in order, that will be used in the * language-specific message. * @return This builder instance. */ public MessageUtil translationReplacements(final Iterable<MessageUtil> replacements) { return translationReplacements(ArrayWrapper.toArray(replacements, MessageUtil.class)); } /** * Terminate construction of the current editing component, and begin * construction of a new message component. After a successful call to this * method, all setter methods will refer to a new message component, created * as a result of the call to this method. * * @param text The text which will populate the new message component. * @return This builder instance. */ public MessageUtil then(final String text) { return then(rawText(text)); } /** * Terminate construction of the current editing component, and begin * construction of a new message component. After a successful call to this * method, all setter methods will refer to a new message component, created * as a result of the call to this method. * * @param text The text which will populate the new message component. * @return This builder instance. */ public MessageUtil then(final TextualComponent text) { if (!latest().hasText()) { throw new IllegalStateException("previous message part has no text"); } messageParts.add(new MessagePart(text)); dirty = true; return this; } /** * Terminate construction of the current editing component, and begin * construction of a new message component. After a successful call to this * method, all setter methods will refer to a new message component, created * as a result of the call to this method. * * @return This builder instance. */ public MessageUtil then() { if (!latest().hasText()) { throw new IllegalStateException("previous message part has no text"); } messageParts.add(new MessagePart()); dirty = true; return this; } @Override public void writeJson(JsonWriter writer) throws IOException { if (messageParts.size() == 1) { latest().writeJson(writer); } else { writer.beginObject().name("text").value("").name("extra").beginArray(); for (final MessagePart part : this) { part.writeJson(writer); } writer.endArray().endObject(); } } /** * Serialize this fancy message, converting it into syntactically-valid JSON * using a {@link JsonWriter}. This JSON should be compatible with vanilla * formatter commands such as {@code /tellraw}. * * @return The JSON string representing this object. */ public String toJSONString() { if (!dirty && jsonString != null) { return jsonString; } StringWriter string = new StringWriter(); JsonWriter json = new JsonWriter(string); try { writeJson(json); json.close(); } catch (IOException e) { throw new RuntimeException("invalid message"); } jsonString = string.toString(); dirty = false; return jsonString; } /** * Sends this message to a player. The player will receive the fully-fledged * formatted display of this message. * * @param player The player who will receive the message. */ public void send(Player player) { send(player, toJSONString()); } private void send(CommandSender sender, String jsonString) { if (!(sender instanceof Player)) { sender.sendMessage(toOldMessageFormat()); return; } Player player = (Player) sender; try { Object handle = Reflection.getHandle(player); Object connection = Reflection.getField(handle.getClass(), "playerConnection").get(handle); Reflection.getMethod(connection.getClass(), "sendPacket", Reflection.getNMSClass("Packet")) .invoke(connection, createChatPacket(jsonString)); } catch (IllegalArgumentException e) { Bukkit.getLogger().log(Level.WARNING, "Argument could not be passed.", e); } catch (IllegalAccessException e) { Bukkit.getLogger().log(Level.WARNING, "Could not access method.", e); } catch (InstantiationException e) { Bukkit.getLogger().log(Level.WARNING, "Underlying class is abstract.", e); } catch (InvocationTargetException e) { Bukkit.getLogger().log(Level.WARNING, "A error has occured durring invoking of method.", e); } catch (NoSuchMethodException e) { Bukkit.getLogger().log(Level.WARNING, "Could not find method.", e); } } private Object createChatPacket(String json) throws IllegalArgumentException, IllegalAccessException, InstantiationException, InvocationTargetException, NoSuchMethodException { if (nmsChatSerializerGsonInstance == null) { // Find the field and its value, completely bypassing obfuscation for (Field declaredField : Reflection.getNMSClass("ChatSerializer").getDeclaredFields()) { if (Modifier.isFinal(declaredField.getModifiers()) && Modifier.isStatic(declaredField.getModifiers()) && declaredField.getType().getName().endsWith("Gson")) { // We've found our field declaredField.setAccessible(true); nmsChatSerializerGsonInstance = declaredField.get(null); fromJsonMethod = nmsChatSerializerGsonInstance.getClass().getMethod("fromJson", String.class, Class.class); break; } } } // Since the method is so simple, and all the obfuscated methods have the same name, it's easier to reimplement 'IChatBaseComponent a(String)' than to reflectively call it // Of course, the implementation may change, but fuzzy matches might break with signature changes Object serializedChatComponent = fromJsonMethod.invoke(nmsChatSerializerGsonInstance, json, Reflection.getNMSClass("IChatBaseComponent")); return nmsPacketPlayOutChatConstructor.newInstance(serializedChatComponent); } /** * Sends this message to a command sender. If the sender is a player, they * will receive the fully-fledged formatted display of this message. * Otherwise, they will receive a version of this message with less * formatting. * * @param sender The command sender who will receive the message. * @see #toOldMessageFormat() */ public void send(CommandSender sender) { send(sender, toJSONString()); } /** * Sends this message to multiple command senders. * * @param senders The command senders who will receive the message. * @see #send(CommandSender) */ public void send(final Iterable<? extends CommandSender> senders) { String string = toJSONString(); for (final CommandSender sender : senders) { send(sender, string); } } /** * Convert this message to a human-readable string with limited formatting. * This method is used to send this message to clients without JSON * formatting support. * <p> * Serialization of this message by using this message will include (in this * order for each message part): * <ol> * <li>The color of each message part.</li> * <li>The applicable stylizations for each message part.</li> * <li>The core text of the message part.</li> * </ol> * The primary omissions are tooltips and clickable actions. Consequently, * this method should be used only as a last resort. * </p> * <p> * Color and formatting can be removed from the returned string by using * {@link ChatColor#stripColor(String)}.</p> * * @return A human-readable string representing limited formatting in * addition to the core text of this message. */ public String toOldMessageFormat() { StringBuilder result = new StringBuilder(); for (MessagePart part : this) { result.append(part.color == null ? "" : part.color); for (ChatColor formatSpecifier : part.styles) { result.append(formatSpecifier); } result.append(part.text); } return result.toString(); } private MessagePart latest() { return messageParts.get(messageParts.size() - 1); } private void onClick(final String name, final String data) { final MessagePart latest = latest(); latest.clickActionName = name; latest.clickActionData = data; dirty = true; } private void onHover(final String name, final JsonRepresentedObject data) { final MessagePart latest = latest(); latest.hoverActionName = name; latest.hoverActionData = data; dirty = true; } // Doc copied from interface public Map<String, Object> serialize() { HashMap<String, Object> map = new HashMap<>(); map.put("messageParts", messageParts); // map.put("JSON", toJSONString()); return map; } /** * <b>Internally called method. Not for API consumption.</b> */ public Iterator<MessagePart> iterator() { return messageParts.iterator(); } } /** * Represents a JSON string value. Writes by this object will not write name * values nor begin/end objects in the JSON stream. All writes merely write the * represented string value. */ final class JsonString implements JsonRepresentedObject, ConfigurationSerializable { public static JsonString deserialize(Map<String, Object> map) { return new JsonString(map.get("stringValue").toString()); } private String _value; public JsonString(CharSequence value) { _value = value == null ? null : value.toString(); } @Override public void writeJson(JsonWriter writer) throws IOException { writer.value(getValue()); } public String getValue() { return _value; } public Map<String, Object> serialize() { HashMap<String, Object> theSingleValue = new HashMap<>(); theSingleValue.put("stringValue", _value); return theSingleValue; } @Override public String toString() { return _value; } } /** * Internal class: Represents a component of a JSON-serializable * {@link MessageUtil}. */ final class MessagePart implements JsonRepresentedObject, ConfigurationSerializable, Cloneable { static final BiMap<ChatColor, String> stylesToNames; static { ImmutableBiMap.Builder<ChatColor, String> builder = ImmutableBiMap.builder(); for (final ChatColor style : ChatColor.values()) { if (!style.isFormat()) { continue; } String styleName; switch (style) { case MAGIC: styleName = "obfuscated"; break; case UNDERLINE: styleName = "underlined"; break; default: styleName = style.name().toLowerCase(); break; } builder.put(style, styleName); } stylesToNames = builder.build(); } static { ConfigurationSerialization.registerClass(MessagePart.class); } @SuppressWarnings(value = "unchecked") public static MessagePart deserialize(Map<String, Object> serialized) { MessagePart part = new MessagePart((TextualComponent) serialized.get("text")); part.styles = (ArrayList<ChatColor>) serialized.get("styles"); part.color = ChatColor.getByChar(serialized.get("color").toString()); part.hoverActionName = (String) serialized.get("hoverActionName"); part.hoverActionData = (JsonRepresentedObject) serialized.get("hoverActionData"); part.clickActionName = (String) serialized.get("clickActionName"); part.clickActionData = (String) serialized.get("clickActionData"); part.insertionData = (String) serialized.get("insertion"); part.translationReplacements = (ArrayList<JsonRepresentedObject>) serialized.get("translationReplacements"); return part; } ChatColor color = ChatColor.WHITE; ArrayList<ChatColor> styles = new ArrayList<>(); String clickActionName = null, clickActionData = null, hoverActionName = null; JsonRepresentedObject hoverActionData = null; TextualComponent text = null; String insertionData = null; ArrayList<JsonRepresentedObject> translationReplacements = new ArrayList<>(); MessagePart(final TextualComponent text) { this.text = text; } MessagePart() { this.text = null; } boolean hasText() { return text != null; } @Override @SuppressWarnings("unchecked") public MessagePart clone() throws CloneNotSupportedException { MessagePart obj = (MessagePart) super.clone(); obj.styles = (ArrayList<ChatColor>) styles.clone(); if (hoverActionData instanceof JsonString) { obj.hoverActionData = new JsonString(((JsonString) hoverActionData).getValue()); } else if (hoverActionData instanceof MessageUtil) { obj.hoverActionData = ((MessageUtil) hoverActionData).clone(); } obj.translationReplacements = (ArrayList<JsonRepresentedObject>) translationReplacements.clone(); return obj; } public void writeJson(JsonWriter json) { try { json.beginObject(); text.writeJson(json); json.name("color").value(color.name().toLowerCase()); for (final ChatColor style : styles) { json.name(stylesToNames.get(style)).value(true); } if (clickActionName != null && clickActionData != null) { json.name("clickEvent").beginObject().name("action").value(clickActionName).name("value") .value(clickActionData).endObject(); } if (hoverActionName != null && hoverActionData != null) { json.name("hoverEvent").beginObject().name("action").value(hoverActionName).name("value"); hoverActionData.writeJson(json); json.endObject(); } if (insertionData != null) { json.name("insertion").value(insertionData); } if (translationReplacements.size() > 0 && text != null && TextualComponent.isTranslatableText(text)) { json.name("with").beginArray(); for (JsonRepresentedObject obj : translationReplacements) { obj.writeJson(json); } json.endArray(); } json.endObject(); } catch (IOException e) { Bukkit.getLogger().log(Level.WARNING, "A problem occured during writing of JSON string", e); } } public Map<String, Object> serialize() { HashMap<String, Object> map = new HashMap<>(); map.put("text", text); map.put("styles", styles); map.put("color", color.getChar()); map.put("hoverActionName", hoverActionName); map.put("hoverActionData", hoverActionData); map.put("clickActionName", clickActionName); map.put("clickActionData", clickActionData); map.put("insertion", insertionData); map.put("translationReplacements", translationReplacements); return map; } } /** * Represents a textual component of a message part. This can be used to not * only represent string literals in a JSON message, but also to represent * localized strings and other text values. * <p> * Different instances of this class can be created with static constructor * methods.</p> */ abstract class TextualComponent implements Cloneable { static { ConfigurationSerialization.registerClass(TextualComponent.ArbitraryTextTypeComponent.class); ConfigurationSerialization.registerClass(TextualComponent.ComplexTextTypeComponent.class); } static TextualComponent deserialize(Map<String, Object> map) { if (map.containsKey("key") && map.size() == 2 && map.containsKey("value")) { // Arbitrary text component return ArbitraryTextTypeComponent.deserialize(map); } else if (map.size() >= 2 && map.containsKey("key") && !map.containsKey("value") /* It contains keys that START WITH value */) { // Complex JSON object return ComplexTextTypeComponent.deserialize(map); } return null; } static boolean isTextKey(String key) { return key.equals("translate") || key.equals("text") || key.equals("score") || key.equals("selector"); } static boolean isTranslatableText(TextualComponent component) { return component instanceof ComplexTextTypeComponent && component.getKey().equals("translate"); } /** * Create a textual component representing a string literal. This is the * default type of textual component when a single string literal is given * to a method. * * @param textValue The text which will be represented. * @return The text component representing the specified literal text. */ public static TextualComponent rawText(String textValue) { return new ArbitraryTextTypeComponent("text", textValue); } /** * Create a textual component representing a localized string. The client * will see this text component as their localized version of the specified * string <em>key</em>, which can be overridden by a resource pack. * <p> * If the specified translation key is not present on the client resource * pack, the translation key will be displayed as a string literal to the * client. * </p> * * @param translateKey The string key which maps to localized text. * @return The text component representing the specified localized text. */ public static TextualComponent localizedText(String translateKey) { return new ArbitraryTextTypeComponent("translate", translateKey); } private static void throwUnsupportedSnapshot() { throw new UnsupportedOperationException("This feature is only supported in snapshot releases."); } /** * Create a textual component representing a scoreboard value. The client * will see their own score for the specified objective as the text * represented by this component. * <p> * <b>This method is currently guaranteed to throw an * {@code UnsupportedOperationException} as it is only supported on snapshot * clients.</b> * </p> * * @param scoreboardObjective The name of the objective for which to display * the score. * @return The text component representing the specified scoreboard score * (for the viewing player), or {@code null} if an error occurs during JSON * serialization. */ public static TextualComponent objectiveScore(String scoreboardObjective) { return objectiveScore("*", scoreboardObjective); } /** * Create a textual component representing a scoreboard value. The client * will see the score of the specified player for the specified objective as * the text represented by this component. * <p> * <b>This method is currently guaranteed to throw an * {@code UnsupportedOperationException} as it is only supported on snapshot * clients.</b> * </p> * * @param playerName The name of the player whos score will be shown. If * this string represents the single-character sequence "*", the viewing * player's score will be displayed. Standard minecraft selectors (@a, @p, * etc) are <em>not</em> supported. * @param scoreboardObjective The name of the objective for which to display * the score. * @return The text component representing the specified scoreboard score * for the specified player, or {@code null} if an error occurs during JSON * serialization. */ public static TextualComponent objectiveScore(String playerName, String scoreboardObjective) { throwUnsupportedSnapshot(); // Remove this line when the feature is released to non-snapshot versions, in addition to updating ALL THE OVERLOADS documentation accordingly return new ComplexTextTypeComponent("score", ImmutableMap.<String, String>builder().put("name", playerName) .put("objective", scoreboardObjective).build()); } /** * Create a textual component representing a player name, retrievable by * using a standard minecraft selector. The client will see the players or * entities captured by the specified selector as the text represented by * this component. * <p> * <b>This method is currently guaranteed to throw an * {@code UnsupportedOperationException} as it is only supported on snapshot * clients.</b> * </p> * * @param selector The minecraft player or entity selector which will * capture the entities whose string representations will be displayed in * the place of this text component. * @return The text component representing the name of the entities captured * by the selector. */ public static TextualComponent selector(String selector) { throwUnsupportedSnapshot(); // Remove this line when the feature is released to non-snapshot versions, in addition to updating ALL THE OVERLOADS documentation accordingly return new ArbitraryTextTypeComponent("selector", selector); } @Override public String toString() { return getReadableString(); } /** * @return The JSON key used to represent text components of this type. */ public abstract String getKey(); /** * @return A readable String */ public abstract String getReadableString(); /** * Clones a textual component instance. The returned object should not * reference this textual component instance, but should maintain the same * key and value. */ @Override public abstract TextualComponent clone() throws CloneNotSupportedException; /** * Writes the text data represented by this textual component to the * specified JSON writer object. A new object within the writer is not * started. * * @param writer The object to which to write the JSON data. * @throws IOException If an error occurs while writing to the stream. */ public abstract void writeJson(JsonWriter writer) throws IOException; /** * Internal class used to represent all types of text components. Exception * validating done is on keys and values. */ private static final class ArbitraryTextTypeComponent extends TextualComponent implements ConfigurationSerializable { public static ArbitraryTextTypeComponent deserialize(Map<String, Object> map) { return new ArbitraryTextTypeComponent(map.get("key").toString(), map.get("value").toString()); } private String _key; private String _value; public ArbitraryTextTypeComponent(String key, String value) { setKey(key); setValue(value); } @Override public String getKey() { return _key; } public void setKey(String key) { Preconditions.checkArgument(key != null && !key.isEmpty(), "The key must be specified."); _key = key; } public String getValue() { return _value; } public void setValue(String value) { Preconditions.checkArgument(value != null, "The value must be specified."); _value = value; } @Override public TextualComponent clone() throws CloneNotSupportedException { // Since this is a private and final class, we can just reinstantiate this class instead of casting super.clone return new ArbitraryTextTypeComponent(getKey(), getValue()); } @Override public void writeJson(JsonWriter writer) throws IOException { writer.name(getKey()).value(getValue()); } @SuppressWarnings("serial") public Map<String, Object> serialize() { return new HashMap<String, Object>() { { put("key", getKey()); put("value", getValue()); } }; } @Override public String getReadableString() { return getValue(); } } /** * Internal class used to represent a text component with a nested JSON * value. Exception validating done is on keys and values. */ private static final class ComplexTextTypeComponent extends TextualComponent implements ConfigurationSerializable { public static ComplexTextTypeComponent deserialize(Map<String, Object> map) { String key = null; Map<String, String> value = new HashMap<>(); for (Map.Entry<String, Object> valEntry : map.entrySet()) { if (valEntry.getKey().equals("key")) { key = (String) valEntry.getValue(); } else if (valEntry.getKey().startsWith("value.")) { value.put(valEntry.getKey().substring(6) /* Strips out the value prefix */, valEntry.getValue().toString()); } } return new ComplexTextTypeComponent(key, value); } private String _key; private Map<String, String> _value; public ComplexTextTypeComponent(String key, Map<String, String> values) { setKey(key); setValue(values); } @Override public String getKey() { return _key; } public void setKey(String key) { Preconditions.checkArgument(key != null && !key.isEmpty(), "The key must be specified."); _key = key; } public Map<String, String> getValue() { return _value; } public void setValue(Map<String, String> value) { Preconditions.checkArgument(value != null, "The value must be specified."); _value = value; } @Override public TextualComponent clone() throws CloneNotSupportedException { // Since this is a private and final class, we can just reinstantiate this class instead of casting super.clone return new ComplexTextTypeComponent(getKey(), getValue()); } @Override public void writeJson(JsonWriter writer) throws IOException { writer.name(getKey()); writer.beginObject(); for (Map.Entry<String, String> jsonPair : _value.entrySet()) { writer.name(jsonPair.getKey()).value(jsonPair.getValue()); } writer.endObject(); } @SuppressWarnings("serial") public Map<String, Object> serialize() { return new java.util.HashMap<String, Object>() { { put("key", getKey()); for (Map.Entry<String, String> valEntry : getValue().entrySet()) { put("value." + valEntry.getKey(), valEntry.getValue()); } } }; } @Override public String getReadableString() { return getKey(); } } } /** * Represents a wrapper around an array class of an arbitrary reference type, * which properly implements "value" hash code and equality functions. * <p> * This class is intended for use as a key to a map. * </p> * * @param <E> The type of elements in the array. * @author Glen Husman * @see Arrays */ final class ArrayWrapper<E> { /** * Converts an iterable element collection to an array of elements. The * iteration order of the specified object will be used as the array element * order. * * @param list The iterable of objects which will be converted to an array. * @param c The type of the elements of the array. * @return An array of elements in the specified iterable. */ @SuppressWarnings(value = "unchecked") public static <T> T[] toArray(Iterable<? extends T> list, Class<T> c) { int size = -1; if (list instanceof Collection<?>) { @SuppressWarnings("rawtypes") Collection coll = (Collection) list; size = coll.size(); } if (size < 0) { size = 0; // Ugly hack: Count it ourselves for (@SuppressWarnings("unused") T element : list) { size++; } } T[] result = (T[]) Array.newInstance(c, size); int i = 0; for (T element : list) { // Assumes iteration order is consistent result[i++] = element; // Assign array element at index THEN increment counter } return result; } private E[] _array; /** * Creates an array wrapper with some elements. * * @param elements The elements of the array. */ public ArrayWrapper(E... elements) { setArray(elements); } /** * Retrieves a reference to the wrapped array instance. * * @return The array wrapped by this instance. */ public E[] getArray() { return _array; } /** * Set this wrapper to wrap a new array instance. * * @param array The new wrapped array. */ public void setArray(E[] array) { Validate.notNull(array, "The array must not be null."); _array = array; } /** * Determines if this object has a value equivalent to another object. * * @see Arrays#equals(Object[], Object[]) */ @SuppressWarnings("rawtypes") @Override public boolean equals(Object other) { if (!(other instanceof ArrayWrapper)) { return false; } return Arrays.equals(_array, ((ArrayWrapper) other)._array); } /** * Gets the hash code represented by this objects value. * * @return This object's hash code. * @see Arrays#hashCode(Object[]) */ @Override public int hashCode() { return Arrays.hashCode(_array); } } /** * A class containing static utility methods and caches which are intended as * reflective conveniences. Unless otherwise noted, upon failure methods will * return {@code null}. */ final class Reflection { /** * Stores loaded classes from the {@code net.minecraft.server} package. */ private static final Map<String, Class<?>> _loadedNMSClasses = new HashMap<>(); /** * Stores loaded classes from the {@code org.bukkit.craftbukkit} package * (and subpackages). */ private static final Map<String, Class<?>> _loadedOBCClasses = new HashMap<>(); private static final Map<Class<?>, Map<String, Field>> _loadedFields = new HashMap<>(); /** * Contains loaded methods in a cache. The map maps [types to maps of * [method names to maps of [parameter types to method instances]]]. */ private static final Map<Class<?>, Map<String, Map<ArrayWrapper<Class<?>>, Method>>> _loadedMethods = new HashMap<>(); private static String _versionString; /** * Gets the version string from the package name of the CraftBukkit server * implementation. This is needed to bypass the JAR package name changing on * each update. * * @return The version string of the OBC and NMS packages, <em>including the * trailing dot</em>. */ public static synchronized String getVersion() { if (_versionString == null) { if (Bukkit.getServer() == null) { // The server hasn't started, static initializer call? return null; } String name = Bukkit.getServer().getClass().getPackage().getName(); _versionString = name.substring(name.lastIndexOf('.') + 1) + "."; } return _versionString; } /** * Gets a {@link Class} object representing a type contained within the * {@code net.minecraft.server} versioned package. The class instances * returned by this method are cached, such that no lookup will be done * twice (unless multiple threads are accessing this method simultaneously). * * @param className The name of the class, excluding the package, within * NMS. * @return The class instance representing the specified NMS class, or * {@code null} if it could not be loaded. */ public synchronized static Class<?> getNMSClass(String className) { if (_loadedNMSClasses.containsKey(className)) { return _loadedNMSClasses.get(className); } String fullName = "net.minecraft.server." + getVersion() + className; Class<?> clazz; try { clazz = Class.forName(fullName); } catch (Exception e) { e.printStackTrace(); _loadedNMSClasses.put(className, null); return null; } _loadedNMSClasses.put(className, clazz); return clazz; } /** * Gets a {@link Class} object representing a type contained within the * {@code org.bukkit.craftbukkit} versioned package. The class instances * returned by this method are cached, such that no lookup will be done * twice (unless multiple threads are accessing this method simultaneously). * * @param className The name of the class, excluding the package, within * OBC. This name may contain a subpackage name, such as * {@code inventory.CraftItemStack}. * @return The class instance representing the specified OBC class, or * {@code null} if it could not be loaded. */ public synchronized static Class<?> getOBCClass(String className) { if (_loadedOBCClasses.containsKey(className)) { return _loadedOBCClasses.get(className); } String fullName = "org.bukkit.craftbukkit." + getVersion() + className; Class<?> clazz; try { clazz = Class.forName(fullName); } catch (Exception e) { e.printStackTrace(); _loadedOBCClasses.put(className, null); return null; } _loadedOBCClasses.put(className, clazz); return clazz; } /** * Attempts to get the NMS handle of a CraftBukkit object. * <p> * The only match currently attempted by this method is a retrieval by using * a parameterless {@code getHandle()} method implemented by the runtime * type of the specified object. * </p> * * @param obj The object for which to retrieve an NMS handle. * @return The NMS handle of the specified object, or {@code null} if it * could not be retrieved using {@code getHandle()}. */ public synchronized static Object getHandle(Object obj) { try { return getMethod(obj.getClass(), "getHandle").invoke(obj); } catch (Exception e) { e.printStackTrace(); return null; } } /** * Retrieves a {@link Field} instance declared by the specified class with * the specified name. Java access modifiers are ignored during this * retrieval. No guarantee is made as to whether the field returned will be * an instance or static field. * <p> * A global caching mechanism within this class is used to store fields. * Combined with synchronization, this guarantees that no field will be * reflectively looked up twice. * </p> * <p> * If a field is deemed suitable for return, * {@link Field#setAccessible(boolean) setAccessible} will be invoked with * an argument of {@code true} before it is returned. This ensures that * callers do not have to check or worry about Java access modifiers when * dealing with the returned instance. * </p> * * @param clazz The class which contains the field to retrieve. * @param name The declared name of the field in the class. * @return A field object with the specified name declared by the specified * class. * @see Class#getDeclaredField(String) */ public synchronized static Field getField(Class<?> clazz, String name) { Map<String, Field> loaded; if (!_loadedFields.containsKey(clazz)) { loaded = new HashMap<>(); _loadedFields.put(clazz, loaded); } else { loaded = _loadedFields.get(clazz); } if (loaded.containsKey(name)) { // If the field is loaded (or cached as not existing), return the relevant value, which might be null return loaded.get(name); } try { Field field = clazz.getDeclaredField(name); field.setAccessible(true); loaded.put(name, field); return field; } catch (Exception e) { // Error loading e.printStackTrace(); // Cache field as not existing loaded.put(name, null); return null; } } /** * Retrieves a {@link Method} instance declared by the specified class with * the specified name and argument types. Java access modifiers are ignored * during this retrieval. No guarantee is made as to whether the field * returned will be an instance or static field. * <p> * A global caching mechanism within this class is used to store method. * Combined with synchronization, this guarantees that no method will be * reflectively looked up twice. * </p> * <p> * If a method is deemed suitable for return, * {@link Method#setAccessible(boolean) setAccessible} will be invoked with * an argument of {@code true} before it is returned. This ensures that * callers do not have to check or worry about Java access modifiers when * dealing with the returned instance. * </p> * <p/> * This method does <em>not</em> search superclasses of the specified type * for methods with the specified signature. Callers wishing this behavior * should use {@link Class#getDeclaredMethod(String, Class...)}. * * @param clazz The class which contains the method to retrieve. * @param name The declared name of the method in the class. * @param args The formal argument types of the method. * @return A method object with the specified name declared by the specified * class. */ public synchronized static Method getMethod(Class<?> clazz, String name, Class<?>... args) { if (!_loadedMethods.containsKey(clazz)) { _loadedMethods.put(clazz, new HashMap<String, Map<ArrayWrapper<Class<?>>, Method>>()); } Map<String, Map<ArrayWrapper<Class<?>>, Method>> loadedMethodNames = _loadedMethods.get(clazz); if (!loadedMethodNames.containsKey(name)) { loadedMethodNames.put(name, new HashMap<ArrayWrapper<Class<?>>, Method>()); } Map<ArrayWrapper<Class<?>>, Method> loadedSignatures = loadedMethodNames.get(name); ArrayWrapper<Class<?>> wrappedArg = new ArrayWrapper<>(args); if (loadedSignatures.containsKey(wrappedArg)) { return loadedSignatures.get(wrappedArg); } for (Method m : clazz.getMethods()) { if (m.getName().equals(name) && Arrays.equals(args, m.getParameterTypes())) { m.setAccessible(true); loadedSignatures.put(wrappedArg, m); return m; } } loadedSignatures.put(wrappedArg, null); return null; } private Reflection() { } }