1.20_1.0.38.D2
This commit is contained in:
1 parent
59df2dc97b
commit
4da43fcb5d
55 files changed
+16662
No files matched your search
+3479
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,155 @@
|
||||
package com.github.joelgodofwar.mmh.common;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.lang.Validate;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
|
||||
import com.github.joelgodofwar.mmh.common.error.BasicErrorReporter;
|
||||
import com.github.joelgodofwar.mmh.common.error.ErrorReporter;
|
||||
import com.github.joelgodofwar.mmh.common.error.ReportType;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
|
||||
public class PluginLibrary {
|
||||
|
||||
/**
|
||||
* The minimum version MoreMobHeads has been tested with.
|
||||
*/
|
||||
public static final String MINIMUM_MINECRAFT_VERSION = "1.20";
|
||||
|
||||
/**
|
||||
* The maximum version MoreMobHeads has been tested with.
|
||||
*/
|
||||
public static final String MAXIMUM_MINECRAFT_VERSION = "1.20.4";
|
||||
|
||||
/**
|
||||
* The date (with ISO 8601 or YYYY-MM-DD) when the most recent version (1.20.4) was released.
|
||||
*/
|
||||
public static final String MINECRAFT_LAST_RELEASE_DATE = "2023-12-07";
|
||||
|
||||
/**
|
||||
* Plugins that are currently incompatible with MoreMobHeads.
|
||||
*/
|
||||
public static final List<String> INCOMPATIBLE = ImmutableList.of("");
|
||||
|
||||
private static Plugin plugin;
|
||||
|
||||
private static boolean updatesDisabled;
|
||||
private static boolean initialized;
|
||||
private static ErrorReporter reporter = new BasicErrorReporter();
|
||||
|
||||
protected static void init(Plugin plugin, ErrorReporter reporter) {
|
||||
Validate.isTrue(!initialized, "MoreMobHeads has already been initialized.");
|
||||
PluginLibrary.plugin = plugin;
|
||||
PluginLibrary.reporter = reporter;
|
||||
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
// Every possible error or warning report type
|
||||
public static final ReportType REPORT_CANNOT_DELETE_CONFIG = new ReportType("Cannot delete old MoreMobHeads configuration.");
|
||||
public static final ReportType REPORT_CANNOT_COPY_FILE = new ReportType("Cannot copy file.");
|
||||
public static final ReportType REPORT_METRICS_LOAD_ERROR = new ReportType("Cannot load bStats Metrics.");
|
||||
|
||||
public static final ReportType REPORT_PLUGIN_LOAD_ERROR = new ReportType("Cannot load MoreMobHeads.");
|
||||
public static final ReportType REPORT_CANNOT_LOAD_CONFIG = new ReportType("Cannot load configuration");
|
||||
public static final ReportType REPORT_CANNOT_CHECK_CONFIG = new ReportType("Cannot check configuration");
|
||||
public static final ReportType REPORT_CANNOT_SAVE_CONFIG = new ReportType("Cannot save configuration");
|
||||
public static final ReportType REPORT_PLUGIN_ENABLE_ERROR = new ReportType("Cannot enable MoreMobHeads.");
|
||||
public static final ReportType REPORT_PLUGIN_UNKNOWN_ERROR = new ReportType("Unknown Error");
|
||||
|
||||
public static final ReportType REPORT_MESSAGES_LOAD_ERROR = new ReportType("Could not load messages.yml");
|
||||
public static final ReportType REPORT_MESSAGES_COPY_ERROR = new ReportType("Could not copy messages.yml to old_messages.yml");
|
||||
public static final ReportType REPORT_OLDMESSAGES_LOAD_ERROR = new ReportType("Could not load old_messages.yml");
|
||||
public static final ReportType REPORT_OLDMESSAGES_SAVE_ERROR = new ReportType("Could not save old messages to messages.yml");
|
||||
public static final ReportType REPORT_PLAYERHEAD_LOAD_ERROR = new ReportType("Could not load player_heads.yml");
|
||||
public static final ReportType REPORT_CUSTOM_LOAD_ERROR = new ReportType("Could not load custom_trades.yml");
|
||||
public static final ReportType REPORT_CHANCE_LOAD_ERROR = new ReportType("Could not load chance_config.yml");
|
||||
public static final ReportType REPORT_CHANCE_SAVE_ERROR = new ReportType("Could not save chance_config.yml");
|
||||
public static final ReportType REPORT_CHANCE_COPY_ERROR = new ReportType("Could not copy chance_config.yml to old_chance_config.yml");
|
||||
public static final ReportType REPORT_MOBNAMES_LOAD_ERROR = new ReportType("Could not load lang_mobnames file");
|
||||
|
||||
public static final ReportType REPORT_PIE_LOAD_ERROR = new ReportType("Error with naming mob.");
|
||||
public static final ReportType REPORT_HEAD_URL_ERROR = new ReportType("Malformed URL Error");
|
||||
public static final ReportType REPORT_BLOCKHEAD_LOAD_ERROR = new ReportType("Cannot load BlockHeads file");
|
||||
|
||||
public static final ReportType REPORT_METRICS_IO_ERROR = new ReportType("Unable to enable metrics due to network problems.");
|
||||
public static final ReportType REPORT_METRICS_GENERIC_ERROR = new ReportType("Unable to enable metrics due to network problems.");
|
||||
|
||||
public static final ReportType REPORT_CANNOT_PARSE_MINECRAFT_VERSION = new ReportType("Unable to retrieve current Minecraft version. Assuming %s");
|
||||
public static final ReportType REPORT_CANNOT_DETECT_CONFLICTING_PLUGINS = new ReportType("Unable to detect conflicting plugin versions.");
|
||||
public static final ReportType REPORT_CANNOT_REGISTER_COMMAND = new ReportType("Cannot register command %s: %s");
|
||||
|
||||
public static final ReportType REPORT_CANNOT_CREATE_TIMEOUT_TASK = new ReportType("Unable to create packet timeout task.");
|
||||
public static final ReportType REPORT_CANNOT_UPDATE_PLUGIN = new ReportType("Cannot perform automatic updates.");
|
||||
|
||||
public static final ReportType REPORT_CANNOT_GET_HEXNICK = new ReportType("Cannot get Nick from HexNicks.");
|
||||
|
||||
public static final ReportType REPORT_PLAYER_NAMED_MOB = new ReportType("Unable to parse named mob kill.");
|
||||
public static final ReportType REPORT_PLAYER_KILL_PLAYER = new ReportType("Unable to parse Player Death.");
|
||||
public static final ReportType REPORT_WHITE_BLACK_LIST = new ReportType("Unable to parse global whitelist/blacklist.");
|
||||
public static final ReportType REPORT_PLAYER_KILL_MOB = new ReportType("Unable to parse Mob Kill.");
|
||||
|
||||
public static final ReportType REPORT_EVENT_HANDLER_LOAD = new ReportType("Error while loading EventHandler.");
|
||||
public static final ReportType REPORT_CS_EVENT_ERROR = new ReportType("Error while entity spawning.");
|
||||
public static final ReportType REPORT_ANNOUNCE_ERROR = new ReportType("Error announcing beheading.");
|
||||
public static final ReportType REPORT_RESOLVE_DAMAGE_WEAPON = new ReportType("Error resolving the Damaging Weapon.");
|
||||
public static final ReportType REPORT_GET_WEAPON_ERROR = new ReportType("Error getting weapon.");
|
||||
|
||||
|
||||
public static final ReportType REPORT_COMMAND_MENU_ERROR = new ReportType("Error displaying Command Menu.");
|
||||
public static final ReportType REPORT_COMMAND_HEADNBT_ERROR = new ReportType("Error executing HeadNBT command.");
|
||||
public static final ReportType REPORT_COMMAND_DISPLAY_PERMS = new ReportType("Error executing Display Perms command.");
|
||||
public static final ReportType REPORT_COMMAND_DISPLAY_VARS = new ReportType("Error executing Display Vars command.");
|
||||
public static final ReportType REPORT_COMMAND_DISPLAY_CHANCE = new ReportType("Error executing Display Chance command.");
|
||||
public static final ReportType REPORT_COMMAND_RELOAD_ERROR = new ReportType("Error executing Reload command.");
|
||||
public static final ReportType REPORT_COMMAND_TOGGLE_DEBUG = new ReportType("Error executing ToggleDebug Command.");
|
||||
public static final ReportType REPORT_COMMAND_CUSTOM_TRADER = new ReportType("Error executing CustomTrader Command.");
|
||||
public static final ReportType REPORT_COMMAND_PLAYER_HEADS = new ReportType("Error executing PlayerHeads Command.");
|
||||
public static final ReportType REPORT_COMMAND_FIX_HEAD = new ReportType("Error executing Fixhead Command.");
|
||||
public static final ReportType REPORT_COMMAND_GIVE_MH = new ReportType("Error executing GiveMH Command.");
|
||||
public static final ReportType REPORT_COMMAND_GIVE_PH = new ReportType("Error executing GivePH Command.");
|
||||
public static final ReportType REPORT_COMMAND_GIVE_BH = new ReportType("Error executing GiveBH Command.");
|
||||
public static final ReportType REPORT_COMMAND_DEV_ERROR = new ReportType("Error exuting dev Command.");
|
||||
|
||||
public static final ReportType UNHANDLED_COMMAND_ERROR = new ReportType("Command had an Unhandled exception.");
|
||||
public static final ReportType REPORT_TAB_COMPLETE_ERROR = new ReportType("Error parsing Tab Complete.");
|
||||
|
||||
public static final ReportType UNHANDLED_ENTITYDEATHEVENT_ERROR = new ReportType("Unhandled EntityDeathEvent Exception.");
|
||||
public static final ReportType UNHANDLED_ENTITYDAMAGE_EVENT_ERROR = new ReportType("Unable to parse fire damage.");
|
||||
public static final ReportType UNHANDLED_ENTITYDAMAGEBYENTITY_EVENT_ERROR = new ReportType("Unable to get damager weapon.");
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Gets the MoreMobHeads plugin instance.
|
||||
* @return The plugin instance
|
||||
*/
|
||||
public static Plugin getPlugin() {
|
||||
return plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables the MoreMobHeads update checker.
|
||||
*/
|
||||
public static void disableUpdates() {
|
||||
updatesDisabled = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the current error reporter.
|
||||
* @return Current error reporter.
|
||||
*/
|
||||
public static ErrorReporter getErrorReporter() {
|
||||
return reporter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether updates are currently disabled.
|
||||
* @return True if it is, false if not
|
||||
*/
|
||||
public static boolean updatesDisabled() {
|
||||
return updatesDisabled;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package com.github.joelgodofwar.mmh.common;
|
||||
|
||||
import java.text.MessageFormat;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.ChatColor;
|
||||
|
||||
import com.github.joelgodofwar.mmh.MoreMobHeads;
|
||||
import com.github.joelgodofwar.mmh.util.MinecraftVersion;
|
||||
|
||||
/**
|
||||
* Custom logger for the MoreMobHeads plugin.
|
||||
*/
|
||||
public class PluginLogger {
|
||||
private Logger logger = Logger.getLogger("Minecraft");
|
||||
private MoreMobHeads mmh;
|
||||
|
||||
/**
|
||||
* Constructor for the PluginLogger class.
|
||||
* @param plugin The MoreMobHeads plugin instance.
|
||||
*/
|
||||
public PluginLogger(MoreMobHeads plugin) {
|
||||
logger = plugin.getLogger();
|
||||
mmh = plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message to console with the INFO level.
|
||||
* @param message Message to log.
|
||||
*/
|
||||
public void log(String message) {
|
||||
log(message, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message to console with a given level.
|
||||
* @param level Logging level
|
||||
* @param message Message to log
|
||||
* @param args Arguments to format in
|
||||
*/
|
||||
public void log(Level level, String message, Object... args) {
|
||||
String pluginName = ChatColor.RESET + "[" + ChatColor.YELLOW + mmh.getName() + ChatColor.RESET + "] ";
|
||||
if (!areAllArgsBlank(args)) {
|
||||
//logger.log(level, applyColorFormatting(MessageFormat.format(message, args)));
|
||||
Bukkit.getConsoleSender().sendMessage(pluginName + MessageFormat.format(pluginName + message, args));
|
||||
} else {
|
||||
//logger.log(level, applyColorFormatting(message));
|
||||
Bukkit.getConsoleSender().sendMessage(pluginName + message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a method to console with the INFO level.
|
||||
* @param message Message to log
|
||||
* @param args Arguments to format in
|
||||
*/
|
||||
public void log(String message, Object... args) {
|
||||
log(Level.INFO, message, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message to console with a given level and exception.
|
||||
* @param level Logging level
|
||||
* @param message Message to log
|
||||
* @param ex Exception to log
|
||||
*/
|
||||
public void log(Level level, String message, Throwable ex) {
|
||||
logger.log(level, message, ex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a debug message to console if debugging is enabled.
|
||||
* @param message Debug message to log.
|
||||
*/
|
||||
public void debug(String message) {
|
||||
debug(message, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a debug message to console with arguments if debugging is enabled.
|
||||
* @param message Debug message to log.
|
||||
* @param args Arguments to format in the debug message.
|
||||
*/
|
||||
public void debug(String message, Object... args) {
|
||||
if (mmh.debug) {
|
||||
log("[Debug] " + message, args);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a debug message to console with an exception if debugging is enabled.
|
||||
* @param message Debug message to log.
|
||||
* @param ex Exception to log.
|
||||
*/
|
||||
public void debug(String message, Throwable ex) {
|
||||
if (mmh.debug) {
|
||||
log(Level.WARNING, "[Debug] " + message, ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a warning message to console with the WARNING level.
|
||||
* @param message Warning message to log.
|
||||
*/
|
||||
public void warn(String message) {
|
||||
warn(message, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a warning message to console with the WARNING level and arguments.
|
||||
* @param message Warning message to log.
|
||||
* @param args Arguments to format in the warning message.
|
||||
*/
|
||||
public void warn(String message, Object... args) {
|
||||
log(Level.WARNING, "[Warning] " + message, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a warning message to console with the WARNING level if the Minecraft version is at or above a specified version.
|
||||
* @param version The Minecraft version to compare against.
|
||||
* @param message Warning message to log.
|
||||
* @param args Arguments to format in the warning message.
|
||||
*/
|
||||
public void warnAbove(MinecraftVersion version, String message, Object... args) {
|
||||
if (version.atOrAbove()) {
|
||||
log(Level.WARNING, message, args);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if all arguments are null or blank.
|
||||
* @param args Arguments to check
|
||||
* @return true if all arguments are null or blank, false otherwise
|
||||
*/
|
||||
private boolean areAllArgsBlank(Object... args) {
|
||||
if ((args == null) || (args.length == 0)) {
|
||||
return true; // Null or empty arguments are considered blank
|
||||
}
|
||||
for (Object arg : args) {
|
||||
if ((arg == null) || ((arg instanceof String) && ((String) arg).isEmpty())) {
|
||||
continue; // Skip null or empty string
|
||||
} else {
|
||||
return false; // Found a non-blank argument
|
||||
}
|
||||
}
|
||||
return true; // All arguments are null or blank
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
package com.github.joelgodofwar.mmh.common.collections;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.PriorityQueue;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import com.google.common.base.Objects;
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.base.Ticker;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.primitives.Longs;
|
||||
|
||||
/**
|
||||
* Represents a hash map where each association may expire after a given time has elapsed.
|
||||
* <p>
|
||||
* Note that replaced key-value associations are only collected once the original expiration time has elapsed.
|
||||
*
|
||||
* @author Kristian Stangeland
|
||||
*
|
||||
* @param <K> - type of the keys.
|
||||
* @param <V> - type of the values.
|
||||
*/
|
||||
public class ExpireHashMap<K, V> {
|
||||
private class ExpireEntry implements Comparable<ExpireEntry> {
|
||||
public final long expireTime;
|
||||
public final K expireKey;
|
||||
public final V expireValue;
|
||||
|
||||
public ExpireEntry(long expireTime, K expireKey, V expireValue) {
|
||||
this.expireTime = expireTime;
|
||||
this.expireKey = expireKey;
|
||||
this.expireValue = expireValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(ExpireEntry o) {
|
||||
return Longs.compare(expireTime, o.expireTime);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ExpireEntry [expireTime=" + expireTime + ", expireKey=" + expireKey
|
||||
+ ", expireValue=" + expireValue + "]";
|
||||
}
|
||||
}
|
||||
|
||||
private final Map<K, ExpireEntry> keyLookup = new HashMap<>();
|
||||
private final PriorityQueue<ExpireEntry> expireQueue = new PriorityQueue<>();
|
||||
|
||||
// View of keyLookup with direct values
|
||||
private final Map<K, V> valueView = Maps.transformValues(keyLookup, entry -> entry.expireValue);
|
||||
|
||||
// Supplied by the constructor
|
||||
private final Ticker ticker;
|
||||
|
||||
/**
|
||||
* Construct a new hash map where each entry may expire at a given time.
|
||||
*/
|
||||
public ExpireHashMap() {
|
||||
this(Ticker.systemTicker());
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a new hash map where each entry may expire at a given time.
|
||||
* @param ticker - supplier of the current time.
|
||||
*/
|
||||
public ExpireHashMap(Ticker ticker) {
|
||||
this.ticker = ticker;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the value associated with the given key, if it has not expired.
|
||||
* @param key - the key.
|
||||
* @return The value, or NULL if not found or it has expired.
|
||||
*/
|
||||
public V get(K key) {
|
||||
evictExpired();
|
||||
|
||||
ExpireEntry entry = keyLookup.get(key);
|
||||
return entry != null ? entry.expireValue : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Associate the given key with the given value, until the expire delay have elapsed.
|
||||
* @param key - the key.
|
||||
* @param value - the value.
|
||||
* @param expireDelay - the amount of time until this association expires. Must be greater than zero.
|
||||
* @param expireUnit - the unit of the expiration.
|
||||
* @return Any previously unexpired association with this key, or NULL.
|
||||
*/
|
||||
public V put(K key, V value, long expireDelay, TimeUnit expireUnit) {
|
||||
Preconditions.checkNotNull(expireUnit, "expireUnit cannot be NULL");
|
||||
Preconditions.checkState(expireDelay > 0, "expireDelay cannot be equal or less than zero.");
|
||||
evictExpired();
|
||||
|
||||
ExpireEntry entry = new ExpireEntry(
|
||||
ticker.read() + TimeUnit.NANOSECONDS.convert(expireDelay, expireUnit),
|
||||
key, value
|
||||
);
|
||||
ExpireEntry previous = keyLookup.put(key, entry);
|
||||
|
||||
// We enqueue its removal
|
||||
expireQueue.add(entry);
|
||||
return previous != null ? previous.expireValue : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the given key is referring to an unexpired association in the map.
|
||||
* @param key - the key.
|
||||
* @return TRUE if it is, FALSE otherwise.
|
||||
*/
|
||||
public boolean containsKey(K key) {
|
||||
evictExpired();
|
||||
return keyLookup.containsKey(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the given value is referring to an unexpired association in the map.
|
||||
* @param value - the value.
|
||||
* @return TRUE if it is, FALSE otherwise.
|
||||
*/
|
||||
public boolean containsValue(V value) {
|
||||
evictExpired();
|
||||
|
||||
// Linear scan is the best we've got
|
||||
for (ExpireEntry entry : keyLookup.values()) {
|
||||
if (Objects.equal(value, entry.expireValue)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a key and its associated value from the map.
|
||||
* @param key - the key to remove.
|
||||
* @return Value of the removed association, NULL otherwise.
|
||||
*/
|
||||
public V removeKey(K key) {
|
||||
evictExpired();
|
||||
|
||||
ExpireEntry entry = keyLookup.remove(key);
|
||||
return entry != null ? entry.expireValue : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the number of entries in the map.
|
||||
* @return The number of entries.
|
||||
*/
|
||||
public int size() {
|
||||
evictExpired();
|
||||
return keyLookup.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a view of the keys in the current map.
|
||||
* @return View of the keys.
|
||||
*/
|
||||
public Set<K> keySet() {
|
||||
evictExpired();
|
||||
return keyLookup.keySet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a view of all the values in the current map.
|
||||
* @return All the values.
|
||||
*/
|
||||
public Collection<V> values() {
|
||||
evictExpired();
|
||||
return valueView.values();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a view of all the entries in the set.
|
||||
* @return All the entries.
|
||||
*/
|
||||
public Set<Entry<K, V>> entrySet() {
|
||||
evictExpired();
|
||||
return valueView.entrySet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a view of this expire map as an ordinary map that does not support insertion.
|
||||
* @return The map.
|
||||
*/
|
||||
public Map<K, V> asMap() {
|
||||
evictExpired();
|
||||
return valueView;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all references to key-value pairs that have been removed or replaced before they were naturally evicted.
|
||||
* <p>
|
||||
* This operation requires a linear scan of the current entries in the map.
|
||||
*/
|
||||
public void collect() {
|
||||
// First evict what we can
|
||||
evictExpired();
|
||||
|
||||
// Recreate the eviction queue - this is faster than removing entries in the old queue
|
||||
expireQueue.clear();
|
||||
expireQueue.addAll(keyLookup.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all the entries in the current map.
|
||||
*/
|
||||
public void clear() {
|
||||
keyLookup.clear();
|
||||
expireQueue.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Evict any expired entries in the map.
|
||||
* <p>
|
||||
* This is called automatically by any of the read or write operations.
|
||||
*/
|
||||
protected void evictExpired() {
|
||||
long currentTime = ticker.read();
|
||||
|
||||
// Remove expired entries
|
||||
while ((expireQueue.size() > 0) && (expireQueue.peek().expireTime <= currentTime)) {
|
||||
ExpireEntry entry = expireQueue.poll();
|
||||
|
||||
if (entry == keyLookup.get(entry.expireKey)) {
|
||||
keyLookup.remove(entry.expireKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return valueView.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package com.github.joelgodofwar.mmh.common.error;
|
||||
|
||||
import java.io.PrintStream;
|
||||
|
||||
import org.bukkit.plugin.Plugin;
|
||||
|
||||
import com.github.joelgodofwar.mmh.common.error.Report.ReportBuilder;
|
||||
import com.github.joelgodofwar.mmh.common.reflect.PrettyPrinter;
|
||||
|
||||
/**
|
||||
* Represents a basic error reporter that prints error reports to the standard error stream.
|
||||
* <p>
|
||||
* Note that this implementation doesn't distinguish between {@link #reportWarning(Object, Report)}
|
||||
* and {@link #reportDetailed(Object, Report)} - they both have the exact same behavior.
|
||||
* @author Kristian
|
||||
*/
|
||||
public class BasicErrorReporter implements ErrorReporter {
|
||||
private final PrintStream output;
|
||||
|
||||
/**
|
||||
* Construct a new basic error reporter that prints directly the standard error stream.
|
||||
*/
|
||||
public BasicErrorReporter() {
|
||||
this(System.err);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a error reporter that prints to the given output stream.
|
||||
* @param output - the output stream.
|
||||
*/
|
||||
public BasicErrorReporter(PrintStream output) {
|
||||
this.output = output;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reportMinimal(Plugin sender, String methodName, Throwable error) {
|
||||
output.println("Unhandled exception occured in " + methodName + " for " + sender.getName());
|
||||
error.printStackTrace(output);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reportMinimal(Plugin sender, String methodName, Throwable error, Object... parameters) {
|
||||
reportMinimal(sender, methodName, error);
|
||||
|
||||
// Also print parameters
|
||||
printParameters(parameters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reportDebug(Object sender, Report report) {
|
||||
// We just have to swallow it
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reportDebug(Object sender, ReportBuilder builder) {
|
||||
// As above
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reportWarning(Object sender, Report report) {
|
||||
// Basic warning
|
||||
output.println("[" + sender.getClass().getSimpleName() + "] " + report.getReportMessage());
|
||||
|
||||
if (report.getException() != null) {
|
||||
report.getException().printStackTrace(output);
|
||||
}
|
||||
printParameters(report.getCallerParameters());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reportWarning(Object sender, ReportBuilder reportBuilder) {
|
||||
reportWarning(sender, reportBuilder.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reportDetailed(Object sender, Report report) {
|
||||
// No difference from warning
|
||||
reportWarning(sender, report);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reportDetailed(Object sender, ReportBuilder reportBuilder) {
|
||||
reportWarning(sender, reportBuilder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Print the given parameters to the standard error stream.
|
||||
* @param parameters - the output parameters.
|
||||
*/
|
||||
private void printParameters(Object[] parameters) {
|
||||
if ((parameters != null) && (parameters.length > 0)) {
|
||||
output.println("Parameters: ");
|
||||
|
||||
try {
|
||||
for (Object parameter : parameters) {
|
||||
if (parameter == null) {
|
||||
output.println("[NULL]");
|
||||
} else {
|
||||
output.println(PrettyPrinter.printObject(parameter));
|
||||
}
|
||||
}
|
||||
} catch (IllegalAccessException e) {
|
||||
// Damn it
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,663 @@
|
||||
package com.github.joelgodofwar.mmh.common.error;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import org.apache.commons.lang.builder.ToStringBuilder;
|
||||
import org.apache.commons.lang.builder.ToStringStyle;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.configuration.file.FileConfiguration;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
|
||||
import com.github.joelgodofwar.mmh.MoreMobHeads;
|
||||
import com.github.joelgodofwar.mmh.common.collections.ExpireHashMap;
|
||||
import com.github.joelgodofwar.mmh.common.error.Report.ReportBuilder;
|
||||
import com.github.joelgodofwar.mmh.common.reflect.PrettyPrinter;
|
||||
import com.github.joelgodofwar.mmh.util.Ansi;
|
||||
import com.github.joelgodofwar.mmh.util.StrUtils;
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.primitives.Primitives;
|
||||
|
||||
/**
|
||||
* Internal class used to handle exceptions.
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
public class DetailedErrorReporter implements ErrorReporter {
|
||||
/**
|
||||
* Report format for printing the current exception count.
|
||||
*/
|
||||
public static final ReportType REPORT_EXCEPTION_COUNT = new ReportType("Internal exception count: %s!");
|
||||
|
||||
public static final String SECOND_LEVEL_PREFIX = " ";
|
||||
public static final String DEFAULT_PREFIX = " ";
|
||||
public static final String DEFAULT_SUPPORT_URL = "https://discord.gg/Fq9HQ2dmtd";
|
||||
|
||||
// Users that are informed about errors in the chat
|
||||
public static final String ERROR_PERMISSION = "protocol.info";
|
||||
|
||||
// We don't want to spam the server
|
||||
public static final int DEFAULT_MAX_ERROR_COUNT = 20;
|
||||
|
||||
// Prevent spam per plugin too
|
||||
private ConcurrentMap<String, AtomicInteger> warningCount = new ConcurrentHashMap<String, AtomicInteger>();
|
||||
|
||||
protected String prefix;
|
||||
protected String supportURL;
|
||||
|
||||
protected AtomicInteger internalErrorCount = new AtomicInteger();
|
||||
|
||||
protected int maxErrorCount;
|
||||
protected Logger logger;
|
||||
|
||||
protected WeakReference<Plugin> pluginReference;
|
||||
protected String pluginName;
|
||||
|
||||
// Whether or not Apache Commons is not present
|
||||
protected static boolean apacheCommonsMissing;
|
||||
|
||||
// Whether or not detailed errror reporting is enabled
|
||||
protected boolean detailedReporting;
|
||||
|
||||
// Map of global objects
|
||||
protected Map<String, Object> globalParameters = new HashMap<String, Object>();
|
||||
|
||||
// Reports to ignore
|
||||
private ExpireHashMap<Report, Boolean> rateLimited = new ExpireHashMap<Report, Boolean>();
|
||||
private final Object rateLock = new Object();
|
||||
private MoreMobHeads mmh;
|
||||
|
||||
/**
|
||||
* Create a default error reporting system.
|
||||
* @param plugin - the plugin owner.
|
||||
*/
|
||||
public DetailedErrorReporter(Plugin plugin) {
|
||||
this(plugin, DEFAULT_PREFIX, DEFAULT_SUPPORT_URL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a central error reporting system.
|
||||
* @param plugin - the plugin owner.
|
||||
* @param prefix - default line prefix.
|
||||
* @param supportURL - URL to report the error.
|
||||
*/
|
||||
public DetailedErrorReporter(Plugin plugin, String prefix, String supportURL) {
|
||||
this(plugin, prefix, supportURL, DEFAULT_MAX_ERROR_COUNT, getBukkitLogger());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a central error reporting system.
|
||||
* @param plugin - the plugin owner.
|
||||
* @param prefix - default line prefix.
|
||||
* @param supportURL - URL to report the error.
|
||||
* @param maxErrorCount - number of errors to print before giving up.
|
||||
* @param LOGGER - current logger.
|
||||
*/
|
||||
public DetailedErrorReporter(Plugin plugin, String prefix, String supportURL, int maxErrorCount, Logger logger) {
|
||||
if (plugin == null) {
|
||||
throw new IllegalArgumentException("Plugin cannot be NULL.");
|
||||
}
|
||||
|
||||
this.pluginReference = new WeakReference<Plugin>(plugin);
|
||||
this.pluginName = getNameSafely(plugin);
|
||||
this.prefix = prefix;
|
||||
this.supportURL = supportURL;
|
||||
this.maxErrorCount = maxErrorCount;
|
||||
this.logger = logger;
|
||||
this.mmh = (MoreMobHeads) plugin;
|
||||
}
|
||||
|
||||
private String getNameSafely(Plugin plugin) {
|
||||
try {
|
||||
return plugin.getName();
|
||||
} catch (LinkageError e) {
|
||||
return "MoreMobHeads";
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt to get the logger.
|
||||
private static Logger getBukkitLogger() {
|
||||
try {
|
||||
return Bukkit.getLogger();
|
||||
} catch (LinkageError e) {
|
||||
return Logger.getLogger("Minecraft");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if we're using detailed error reporting.
|
||||
* @return TRUE if we are, FALSE otherwise.
|
||||
*/
|
||||
public boolean isDetailedReporting() {
|
||||
return detailedReporting;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether or not to use detailed error reporting.
|
||||
* @param detailedReporting - TRUE to enable it, FALSE otherwise.
|
||||
*/
|
||||
public void setDetailedReporting(boolean detailedReporting) {
|
||||
this.detailedReporting = detailedReporting;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reportMinimal(Plugin sender, String methodName, Throwable error, Object... parameters) {
|
||||
if (reportMinimalNoSpam(sender, methodName, error)) {
|
||||
// Print parameters, if they are given
|
||||
if ((parameters != null) && (parameters.length > 0)) {
|
||||
logger.log(Level.SEVERE, printParameters(parameters));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reportMinimal(Plugin sender, String methodName, Throwable error) {
|
||||
reportMinimalNoSpam(sender, methodName, error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Report a problem with a given method and plugin, ensuring that we don't exceed the maximum number of error reports.
|
||||
* @param sender - the component that observed this exception.
|
||||
* @param methodName - the method name.
|
||||
* @param error - the error itself.
|
||||
* @return TRUE if the error was printed, FALSE if it was suppressed.
|
||||
*/
|
||||
public boolean reportMinimalNoSpam(Plugin sender, String methodName, Throwable error) {
|
||||
String pluginName = sender.getName();
|
||||
AtomicInteger counter = warningCount.get(pluginName);
|
||||
|
||||
// Thread safe pattern
|
||||
if (counter == null) {
|
||||
AtomicInteger created = new AtomicInteger();
|
||||
counter = warningCount.putIfAbsent(pluginName, created);
|
||||
|
||||
if (counter == null) {
|
||||
counter = created;
|
||||
}
|
||||
}
|
||||
|
||||
final int errorCount = counter.incrementAndGet();
|
||||
|
||||
// See if we should print the full error
|
||||
if (errorCount < getMaxErrorCount()) {
|
||||
logger.log(Level.SEVERE, "[" + pluginName + "] Unhandled exception occurred in " +
|
||||
methodName + " for " + pluginName, error);
|
||||
return true;
|
||||
|
||||
} else {
|
||||
// Nope - only print the error count occasionally
|
||||
if (isPowerOfTwo(errorCount)) {
|
||||
logger.log(Level.SEVERE, "[" + pluginName + "] Unhandled exception number " + errorCount + " occurred in " +
|
||||
methodName + " for " + pluginName, error);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a given number is a power of two.
|
||||
* <p>
|
||||
* That is, if there exists an N such that 2^N = number.
|
||||
* @param number - the number to check.
|
||||
* @return TRUE if the given number is a power of two, FALSE otherwise.
|
||||
*/
|
||||
private boolean isPowerOfTwo(int number) {
|
||||
return (number & (number - 1)) == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reportDebug(Object sender, ReportBuilder builder) {
|
||||
reportDebug(sender, Preconditions.checkNotNull(builder, "builder cannot be NULL").build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reportDebug(Object sender, Report report) {
|
||||
if (logger.isLoggable(Level.FINE) && canReport(report)) {
|
||||
reportLevel(Level.FINE, sender, report);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reportWarning(Object sender, ReportBuilder reportBuilder) {
|
||||
if (reportBuilder == null) {
|
||||
throw new IllegalArgumentException("reportBuilder cannot be NULL.");
|
||||
}
|
||||
|
||||
reportWarning(sender, reportBuilder.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reportWarning(Object sender, Report report) {
|
||||
if (logger.isLoggable(Level.WARNING) && canReport(report)) {
|
||||
reportLevel(Level.WARNING, sender, report);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if we should print the given report.
|
||||
* <p>
|
||||
* The default implementation will check for rate limits.
|
||||
* @param report - the report to check.
|
||||
* @return TRUE if we should print it, FALSE otherwise.
|
||||
*/
|
||||
protected boolean canReport(Report report) {
|
||||
long rateLimit = report.getRateLimit();
|
||||
|
||||
// Check for rate limit
|
||||
if (rateLimit > 0) {
|
||||
synchronized (rateLock) {
|
||||
if (rateLimited.containsKey(report)) {
|
||||
return false;
|
||||
}
|
||||
rateLimited.put(report, true, rateLimit, TimeUnit.NANOSECONDS);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void reportLevel(Level level, Object sender, Report report) {
|
||||
String message = "[" + pluginName + "] [" + getSenderName(sender) + "] " + report.getReportMessage();
|
||||
|
||||
// Print the main warning
|
||||
if (report.getException() != null) {
|
||||
logger.log(level, message, report.getException());
|
||||
} else {
|
||||
logger.log(level, message);
|
||||
|
||||
// Remember the call stack
|
||||
if (detailedReporting) {
|
||||
printCallStack(level, logger);
|
||||
}
|
||||
}
|
||||
|
||||
// Parameters?
|
||||
if (report.hasCallerParameters()) {
|
||||
// Write it
|
||||
logger.log(level, printParameters(report.getCallerParameters()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the name of a sender class.
|
||||
* @param sender - sender object.
|
||||
* @return The name of the sender's class.
|
||||
*/
|
||||
private String getSenderName(Object sender) {
|
||||
if (sender != null) {
|
||||
return ReportType.getSenderClass(sender).getSimpleName();
|
||||
} else {
|
||||
return "NULL";
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reportDetailed(Object sender, ReportBuilder reportBuilder) {
|
||||
reportDetailed(sender, reportBuilder.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reportDetailed(Object sender, Report report) {
|
||||
final Plugin plugin = pluginReference.get();
|
||||
final int errorCount = internalErrorCount.incrementAndGet();
|
||||
|
||||
// Do not overtly spam the server!
|
||||
if (errorCount > getMaxErrorCount()) {
|
||||
// Only allow the error count at rare occations
|
||||
if (isPowerOfTwo(errorCount)) {
|
||||
// Permit it - but print the number of exceptions first
|
||||
reportWarning(this, Report.newBuilder(REPORT_EXCEPTION_COUNT).messageParam(errorCount).build());
|
||||
} else {
|
||||
// NEVER SPAM THE CONSOLE
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Secondary rate limit
|
||||
if (!canReport(report)) {
|
||||
return;
|
||||
}
|
||||
|
||||
StringWriter text = new StringWriter();
|
||||
PrintWriter writer = new PrintWriter(text);
|
||||
|
||||
// Helpful message
|
||||
writer.println("[" + pluginName + "] INTERNAL ERROR: " + report.getReportMessage());
|
||||
writer.println("Copy from above line down to ↓↓↓");
|
||||
writer.println("If this problem hasn't already been reported, please post");
|
||||
writer.println("at " + supportURL + " with the following data:");
|
||||
|
||||
// Data dump!
|
||||
writer.println("Data Dump:");
|
||||
|
||||
// Relevant parameters
|
||||
if (report.hasCallerParameters()) {
|
||||
printParameters(writer, report.getCallerParameters());
|
||||
}
|
||||
|
||||
// Global parameters
|
||||
for (String param : globalParameters()) {
|
||||
writer.println(SECOND_LEVEL_PREFIX + param + ":");
|
||||
writer.println(addPrefix(getStringDescription(getGlobalParameter(param)),
|
||||
SECOND_LEVEL_PREFIX + SECOND_LEVEL_PREFIX));
|
||||
}
|
||||
|
||||
// Now, for the sender itself
|
||||
writer.println("Sender:");
|
||||
writer.println(addPrefix(getStringDescription(sender), SECOND_LEVEL_PREFIX));
|
||||
|
||||
// And plugin
|
||||
if (plugin != null) {
|
||||
writer.println("Version: " + mmh.getName() + " v" + mmh.getDescription().getVersion());
|
||||
String jarfilename = mmh.getjarfilename();
|
||||
writer.println(addPrefix("jarfilename: " + StrUtils.Right(jarfilename, jarfilename.length() - jarfilename.lastIndexOf(File.separatorChar)), SECOND_LEVEL_PREFIX));
|
||||
//writer.println(addPrefix(plugin.toString(), SECOND_LEVEL_PREFIX));
|
||||
}
|
||||
|
||||
writer.println("config.yml:");
|
||||
FileConfiguration config = mmh.getConfig();
|
||||
for (String key : config.getKeys(true)) {
|
||||
Object value = config.get(key);
|
||||
if ((value != null) && value.getClass().isArray()) {
|
||||
value = Arrays.asList((Object[]) value);
|
||||
}
|
||||
if ((value != null) && value.toString().contains("MemorySection")) {
|
||||
value = "";
|
||||
}
|
||||
writer.println(addPrefix( key + " = '" + value + "'" , SECOND_LEVEL_PREFIX));
|
||||
}
|
||||
|
||||
// And java version
|
||||
writer.println("Java Version: " + System.getProperty("java.version"));
|
||||
//writer.println(addPrefix(System.getProperty("java.version"), SECOND_LEVEL_PREFIX));
|
||||
|
||||
// Add the server version too
|
||||
if (Bukkit.getServer() != null) {
|
||||
writer.println("Server: ");
|
||||
writer.println(addPrefix("This server is running " + Bukkit.getName() + " version " + Bukkit.getVersion() + " (Implementing API version " + Bukkit.getBukkitVersion() + ")", SECOND_LEVEL_PREFIX));
|
||||
/** add list of all plugins, and their version */
|
||||
writer.println(getPluginList());
|
||||
|
||||
|
||||
// Inform of this occurrence
|
||||
if (isChatWarnings()) {
|
||||
Bukkit.getServer().broadcast(
|
||||
String.format("Error %s (%s) occurred in %s.", report.getReportMessage(), report.getException(), sender),
|
||||
ERROR_PERMISSION
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Now, let us print important exception information
|
||||
writer.println("Stack Trace:");
|
||||
|
||||
if (report.getException() != null) {
|
||||
report.getException().printStackTrace(writer);
|
||||
} else if (detailedReporting) {
|
||||
printCallStack(writer);
|
||||
}
|
||||
writer.println("-> -> -> -> -> -> -> -> This line <- <- <- <- <- <- <- <-");
|
||||
|
||||
// Make sure it is reported
|
||||
logger.severe(addPrefix(text.toString(), prefix));
|
||||
}
|
||||
|
||||
private boolean isChatWarnings() {
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Print the call stack to the given logger.
|
||||
* @param logger - the logger.
|
||||
*/
|
||||
private void printCallStack(Level level, Logger logger) {
|
||||
StringWriter text = new StringWriter();
|
||||
printCallStack(new PrintWriter(text));
|
||||
|
||||
// Print the exception
|
||||
logger.log(level, text.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Print the current call stack.
|
||||
* @param writer - the writer.
|
||||
*/
|
||||
private void printCallStack(PrintWriter writer) {
|
||||
Exception current = new Exception("Not an error! This is the call stack.");
|
||||
current.printStackTrace(writer);
|
||||
}
|
||||
|
||||
private String printParameters(Object... parameters) {
|
||||
StringWriter writer = new StringWriter();
|
||||
|
||||
// Print and retrieve the string buffer
|
||||
printParameters(new PrintWriter(writer), parameters);
|
||||
return writer.toString();
|
||||
}
|
||||
|
||||
private void printParameters(PrintWriter writer, Object[] parameters) {
|
||||
writer.println("Parameters: ");
|
||||
|
||||
// We *really* want to get as much information as possible
|
||||
for (Object param : parameters) {
|
||||
writer.println(addPrefix(getStringDescription(param), SECOND_LEVEL_PREFIX));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the given prefix to every line in the text.
|
||||
* @param text - text to modify.
|
||||
* @param prefix - prefix added to every line in the text.
|
||||
* @return The modified text.
|
||||
*/
|
||||
protected String addPrefix(String text, String prefix) {
|
||||
return text.replaceAll("(?m)^", prefix);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a string representation of the given object.
|
||||
* @param value - object to convert.
|
||||
* @return String representation.
|
||||
*/
|
||||
public static String getStringDescription(Object value) {
|
||||
// We can't only rely on toString.
|
||||
if (value == null) {
|
||||
return "[NULL]";
|
||||
} if (isSimpleType(value) || (value instanceof Class<?>)) {
|
||||
return value.toString();
|
||||
} else {
|
||||
try {
|
||||
if (!apacheCommonsMissing) {
|
||||
return ToStringBuilder.reflectionToString(value, ToStringStyle.MULTI_LINE_STYLE, false, null);
|
||||
}
|
||||
} catch (LinkageError ex) {
|
||||
// Apache is probably missing
|
||||
apacheCommonsMissing = true;
|
||||
} catch (ThreadDeath | OutOfMemoryError e) {
|
||||
throw e;
|
||||
} catch (Throwable ex) {
|
||||
// Don't use the error logger to log errors in error logging (that could lead to infinite loops)
|
||||
getBukkitLogger().log(Level.WARNING, "Cannot convert to a String with Apache: " + ex.getMessage());
|
||||
}
|
||||
|
||||
// Use our custom object printer instead
|
||||
try {
|
||||
return PrettyPrinter.printObject(value, value.getClass(), Object.class);
|
||||
} catch (IllegalAccessException e) {
|
||||
return "[Error: " + e.getMessage() + "]";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the given object is a wrapper for a primitive/simple type or not.
|
||||
* @param test - the object to test.
|
||||
* @return TRUE if this object is simple enough to simply be printed, FALSE othewise.
|
||||
*/
|
||||
protected static boolean isSimpleType(Object test) {
|
||||
return (test instanceof String) || Primitives.isWrapperType(test.getClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the current number of errors printed through {@link #reportDetailed(Object, Report)}.
|
||||
* @return Number of errors printed.
|
||||
*/
|
||||
public int getErrorCount() {
|
||||
return internalErrorCount.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the number of errors printed.
|
||||
* @param errorCount - new number of errors printed.
|
||||
*/
|
||||
public void setErrorCount(int errorCount) {
|
||||
internalErrorCount.set(errorCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the maximum number of errors we can print before we begin suppressing errors.
|
||||
* @return Maximum number of errors.
|
||||
*/
|
||||
public int getMaxErrorCount() {
|
||||
return maxErrorCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the maximum number of errors we can print before we begin suppressing errors.
|
||||
* @param maxErrorCount - new max count.
|
||||
*/
|
||||
public void setMaxErrorCount(int maxErrorCount) {
|
||||
this.maxErrorCount = maxErrorCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the given global parameter. It will be included in every error report.
|
||||
* <p>
|
||||
* Both key and value must be non-null.
|
||||
* @param key - name of parameter.
|
||||
* @param value - the global parameter itself.
|
||||
*/
|
||||
public void addGlobalParameter(String key, Object value) {
|
||||
if (key == null) {
|
||||
throw new IllegalArgumentException("key cannot be NULL.");
|
||||
}
|
||||
if (value == null) {
|
||||
throw new IllegalArgumentException("value cannot be NULL.");
|
||||
}
|
||||
|
||||
globalParameters.put(key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a global parameter by its key.
|
||||
* @param key - key of the parameter to retrieve.
|
||||
* @return The value of the global parameter, or NULL if not found.
|
||||
*/
|
||||
public Object getGlobalParameter(String key) {
|
||||
if (key == null) {
|
||||
throw new IllegalArgumentException("key cannot be NULL.");
|
||||
}
|
||||
|
||||
return globalParameters.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all global parameters.
|
||||
*/
|
||||
public void clearGlobalParameters() {
|
||||
globalParameters.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a set of every registered global parameter.
|
||||
* @return Set of all registered global parameters.
|
||||
*/
|
||||
public Set<String> globalParameters() {
|
||||
return globalParameters.keySet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the support URL that will be added to all detailed reports.
|
||||
* @return Support URL.
|
||||
*/
|
||||
public String getSupportURL() {
|
||||
return supportURL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the support URL that will be added to all detailed reports.
|
||||
* @param supportURL - the new support URL.
|
||||
*/
|
||||
public void setSupportURL(String supportURL) {
|
||||
this.supportURL = supportURL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the prefix to apply to every line in the error reports.
|
||||
* @return Error report prefix.
|
||||
*/
|
||||
public String getPrefix() {
|
||||
return prefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the prefix to apply to every line in the error reports.
|
||||
* @param prefix - new prefix.
|
||||
*/
|
||||
public void setPrefix(String prefix) {
|
||||
this.prefix = prefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the current logger that is used to print all reports.
|
||||
* @return The current logger.
|
||||
*/
|
||||
public Logger getLogger() {
|
||||
return logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current logger that is used to print all reports.
|
||||
* @param logger - new logger.
|
||||
*/
|
||||
public void setLogger(Logger logger) {
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
private String getPluginList() {
|
||||
StringBuilder pluginList = new StringBuilder();
|
||||
Plugin[] plugins = Bukkit.getPluginManager().getPlugins();
|
||||
Plugin[] var6 = plugins;
|
||||
int var5 = plugins.length;
|
||||
|
||||
for (int var4 = 0; var4 < var5; ++var4) {
|
||||
Plugin plugin = var6[var4];
|
||||
if (pluginList.length() > 0) {
|
||||
pluginList.append(Ansi.WHITE);
|
||||
pluginList.append(", ");
|
||||
}
|
||||
|
||||
pluginList.append(plugin.isEnabled() ? Ansi.GREEN : Ansi.RED);
|
||||
pluginList.append(plugin.getDescription().getName());
|
||||
if (plugin.getDescription().getProvides().size() > 0) {
|
||||
pluginList.append(" (").append(String.join(", ", plugin.getDescription().getProvides())).append(")");
|
||||
}
|
||||
pluginList.append(" [" + plugin.getDescription().getVersion() + "]");
|
||||
}
|
||||
|
||||
return "Plugins (" + plugins.length + "): " + pluginList.toString() + Ansi.RESET;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package com.github.joelgodofwar.mmh.common.error;
|
||||
|
||||
import org.bukkit.plugin.Plugin;
|
||||
|
||||
import com.github.joelgodofwar.mmh.common.error.Report.ReportBuilder;
|
||||
|
||||
/**
|
||||
* Represents an object that can forward an error {@link Report} to the display and permanent storage.
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
public interface ErrorReporter {
|
||||
/**
|
||||
* Prints a small minimal error report regarding an exception from another plugin.
|
||||
* @param sender - the other plugin.
|
||||
* @param methodName - name of the caller method.
|
||||
* @param error - the exception itself.
|
||||
*/
|
||||
void reportMinimal(Plugin sender, String methodName, Throwable error);
|
||||
|
||||
/**
|
||||
* Prints a small minimal error report regarding an exception from another plugin.
|
||||
* @param sender - the other plugin.
|
||||
* @param methodName - name of the caller method.
|
||||
* @param error - the exception itself.
|
||||
* @param parameters - any relevant parameters to print.
|
||||
*/
|
||||
void reportMinimal(Plugin sender, String methodName, Throwable error, Object... parameters);
|
||||
|
||||
/**
|
||||
* Prints a debug message from the current sender.
|
||||
* <p>
|
||||
* Most users will not see this message.
|
||||
* @param sender - the sender.
|
||||
* @param report - the report.
|
||||
*/
|
||||
void reportDebug(Object sender, Report report);
|
||||
|
||||
/**
|
||||
* Prints a debug message from the current sender.
|
||||
* @param sender - the sender.
|
||||
* @param builder - the report builder.
|
||||
*/
|
||||
void reportDebug(Object sender, ReportBuilder builder);
|
||||
|
||||
/**
|
||||
* Prints a warning message from the current plugin.
|
||||
* @param sender - the object containing the caller method.
|
||||
* @param report - an error report to include.
|
||||
*/
|
||||
void reportWarning(Object sender, Report report);
|
||||
|
||||
/**
|
||||
* Prints a warning message from the current plugin.
|
||||
* @param sender - the object containing the caller method.
|
||||
* @param reportBuilder - an error report builder that will be used to get the report.
|
||||
*/
|
||||
void reportWarning(Object sender, ReportBuilder reportBuilder);
|
||||
|
||||
/**
|
||||
* Prints a detailed error report about an unhandled exception.
|
||||
* @param sender - the object containing the caller method.
|
||||
* @param report - an error report to include.
|
||||
*/
|
||||
void reportDetailed(Object sender, Report report);
|
||||
|
||||
/**
|
||||
* Prints a detailed error report about an unhandled exception.
|
||||
* @param sender - the object containing the caller method.
|
||||
* @param reportBuilder - an error report builder that will be used to get the report.
|
||||
*/
|
||||
void reportDetailed(Object sender, ReportBuilder reportBuilder);
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
package com.github.joelgodofwar.mmh.common.error;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Represents a error or warning report.
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
public class Report {
|
||||
private final ReportType type;
|
||||
private final Throwable exception;
|
||||
private final Object[] messageParameters;
|
||||
private final Object[] callerParameters;
|
||||
|
||||
// Used to rate limit reports that are similar
|
||||
private final long rateLimit;
|
||||
|
||||
/**
|
||||
* Must be constructed through the factory method in Report.
|
||||
*/
|
||||
public static class ReportBuilder {
|
||||
private ReportType type;
|
||||
private Throwable exception;
|
||||
private Object[] messageParameters;
|
||||
private Object[] callerParameters;
|
||||
private long rateLimit;
|
||||
|
||||
private ReportBuilder() {
|
||||
// Don't allow
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current report type. Cannot be NULL.
|
||||
* @param type - report type.
|
||||
* @return This builder, for chaining.
|
||||
*/
|
||||
public ReportBuilder type(ReportType type) {
|
||||
if (type == null) {
|
||||
throw new IllegalArgumentException("Report type cannot be set to NULL.");
|
||||
}
|
||||
this.type = type;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current exception that occurred.
|
||||
* @param exception - exception that occurred.
|
||||
* @return This builder, for chaining.
|
||||
*/
|
||||
public ReportBuilder error(@Nullable Throwable exception) {
|
||||
this.exception = exception;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the message parameters that are used to construct a message text.
|
||||
* @param messageParameters - parameters for the report type.
|
||||
* @return This builder, for chaining.
|
||||
*/
|
||||
public ReportBuilder messageParam(@Nullable Object... messageParameters) {
|
||||
this.messageParameters = messageParameters;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the parameters in the caller method. This is optional.
|
||||
* @param callerParameters - parameters of the caller method.
|
||||
* @return This builder, for chaining.
|
||||
*/
|
||||
public ReportBuilder callerParam(@Nullable Object... callerParameters) {
|
||||
this.callerParameters = callerParameters;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the minimum number of nanoseconds to wait until a report of equal type and parameters
|
||||
* is allowed to be printed again.
|
||||
* @param rateLimit - number of nanoseconds, or 0 to disable. Cannot be negative.
|
||||
* @return This builder, for chaining.
|
||||
*/
|
||||
public ReportBuilder rateLimit(long rateLimit) {
|
||||
if (rateLimit < 0) {
|
||||
throw new IllegalArgumentException("Rate limit cannot be less than zero.");
|
||||
}
|
||||
this.rateLimit = rateLimit;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the minimum time to wait until a report of equal type and parameters is allowed to be printed again.
|
||||
* @param rateLimit - the time, or 0 to disable. Cannot be negative.
|
||||
* @param rateUnit - the unit of the rate limit.
|
||||
* @return This builder, for chaining.
|
||||
*/
|
||||
public ReportBuilder rateLimit(long rateLimit, TimeUnit rateUnit) {
|
||||
return rateLimit(TimeUnit.NANOSECONDS.convert(rateLimit, rateUnit));
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a new report with the provided input.
|
||||
* @return A new report.
|
||||
*/
|
||||
public Report build() {
|
||||
return new Report(type, exception, messageParameters, callerParameters, rateLimit);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a new report builder.
|
||||
* @param type - the initial report type.
|
||||
* @return Report builder.
|
||||
*/
|
||||
public static ReportBuilder newBuilder(ReportType type) {
|
||||
return new ReportBuilder().type(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a new report with the given type and parameters.
|
||||
* @param exception - exception that occured in the caller method.
|
||||
* @param type - the report type that will be used to construct the message.
|
||||
* @param messageParameters - parameters used to construct the report message.
|
||||
* @param callerParameters - parameters from the caller method.
|
||||
*/
|
||||
protected Report(ReportType type, @Nullable Throwable exception,
|
||||
@Nullable Object[] messageParameters, @Nullable Object[] callerParameters) {
|
||||
this(type, exception, messageParameters, callerParameters, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a new report with the given type and parameters.
|
||||
* @param exception - exception that occurred in the caller method.
|
||||
* @param type - the report type that will be used to construct the message.
|
||||
* @param messageParameters - parameters used to construct the report message.
|
||||
* @param callerParameters - parameters from the caller method.
|
||||
* @param rateLimit - minimum number of nanoseconds to wait until a report of equal type and parameters is allowed to be printed again.
|
||||
*/
|
||||
protected Report(ReportType type, @Nullable Throwable exception,
|
||||
@Nullable Object[] messageParameters, @Nullable Object[] callerParameters, long rateLimit) {
|
||||
if (type == null) {
|
||||
throw new IllegalArgumentException("type cannot be NULL.");
|
||||
}
|
||||
this.type = type;
|
||||
this.exception = exception;
|
||||
this.messageParameters = messageParameters;
|
||||
this.callerParameters = callerParameters;
|
||||
this.rateLimit = rateLimit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the current report type with the provided message parameters.
|
||||
* @return The formated report message.
|
||||
*/
|
||||
public String getReportMessage() {
|
||||
return type.getMessage(messageParameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the message parameters that will be used to construc the report message.
|
||||
* <p>
|
||||
* This should not be confused with the method parameters of the caller method.
|
||||
* @return Message parameters.
|
||||
*/
|
||||
public Object[] getMessageParameters() {
|
||||
return messageParameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the parameters of the caller method. Optional - may be NULL.
|
||||
* @return Parameters or the caller method.
|
||||
*/
|
||||
public Object[] getCallerParameters() {
|
||||
return callerParameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the report type.
|
||||
* @return Report type.
|
||||
*/
|
||||
public ReportType getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the associated exception, or NULL if not found.
|
||||
* @return Associated exception, or NULL.
|
||||
*/
|
||||
public Throwable getException() {
|
||||
return exception;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if we have any message parameters.
|
||||
* @return TRUE if there are any message parameters, FALSE otherwise.
|
||||
*/
|
||||
public boolean hasMessageParameters() {
|
||||
return (messageParameters != null) && (messageParameters.length > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if we have any caller parameters.
|
||||
* @return TRUE if there are any caller parameters, FALSE otherwise.
|
||||
*/
|
||||
public boolean hasCallerParameters() {
|
||||
return (callerParameters != null) && (callerParameters.length > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve desired minimum number of nanoseconds until a report of the same type and parameters should be reprinted.
|
||||
* <p>
|
||||
* Note that this may be ignored or modified by the error reporter. Zero indicates no rate limit.
|
||||
* @return The number of nanoseconds. Never negative.
|
||||
*/
|
||||
public long getRateLimit() {
|
||||
return rateLimit;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = (prime * result) + Arrays.hashCode(callerParameters);
|
||||
result = (prime * result) + Arrays.hashCode(messageParameters);
|
||||
result = (prime * result) + ((type == null) ? 0 : type.hashCode());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj instanceof Report) {
|
||||
Report other = (Report) obj;
|
||||
return (type == other.type) &&
|
||||
Arrays.equals(callerParameters, other.callerParameters) &&
|
||||
Arrays.equals(messageParameters, other.messageParameters);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package com.github.joelgodofwar.mmh.common.error;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import com.github.joelgodofwar.mmh.common.reflect.FieldAccessException;
|
||||
import com.github.joelgodofwar.mmh.common.reflect.FuzzyReflection;
|
||||
import com.github.joelgodofwar.mmh.common.reflect.fuzzy.FuzzyFieldContract;
|
||||
|
||||
/**
|
||||
* Represents a strongly-typed report. Subclasses should be immutable.
|
||||
* <p>
|
||||
* By convention, a report must be declared as a static field publicly accessible from the sender class.
|
||||
* @author Kristian
|
||||
*/
|
||||
public class ReportType {
|
||||
private final String errorFormat;
|
||||
|
||||
// Used to store the report name
|
||||
protected String reportName;
|
||||
|
||||
/**
|
||||
* Construct a new report type.
|
||||
* @param errorFormat - string used to format the underlying report.
|
||||
*/
|
||||
public ReportType(String errorFormat) {
|
||||
this.errorFormat = errorFormat;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the given report to a string, using the provided parameters.
|
||||
* @param parameters - parameters to insert, or NULL to insert nothing.
|
||||
* @return The full report in string format.
|
||||
*/
|
||||
public String getMessage(Object[] parameters) {
|
||||
if ((parameters == null) || (parameters.length == 0)) {
|
||||
return toString();
|
||||
} else {
|
||||
return String.format(errorFormat, parameters);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return errorFormat;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the class of the given sender.
|
||||
* <p>
|
||||
* If the sender is already a Class, we return it.
|
||||
* @param sender - the sender to look up.
|
||||
* @return The class of the sender.
|
||||
*/
|
||||
public static Class<?> getSenderClass(Object sender) {
|
||||
if (sender == null) {
|
||||
throw new IllegalArgumentException("sender cannot be null.");
|
||||
} else if (sender instanceof Class<?>) {
|
||||
return (Class<?>) sender;
|
||||
} else {
|
||||
return sender.getClass();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the full canonical name of a given report type.
|
||||
* <p>
|
||||
* Note that the sender may be a class (for static callers), in which
|
||||
* case it will be used directly instead of its getClass() method.
|
||||
* <p>
|
||||
* It is thus not advisable for class classes to report reports.
|
||||
* @param sender - the sender, or its class.
|
||||
* @param type - the report type.
|
||||
* @return The full canonical name.
|
||||
*/
|
||||
public static String getReportName(Object sender, ReportType type) {
|
||||
if (sender == null) {
|
||||
throw new IllegalArgumentException("sender cannot be null.");
|
||||
}
|
||||
return getReportName(getSenderClass(sender), type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the full canonical name of a given report type.
|
||||
* <p>
|
||||
* This is in the format <i>canonical_name_of_class#report_type</i>
|
||||
* @param sender - the sender class.
|
||||
* @param type - the report instance.
|
||||
* @return The full canonical name.
|
||||
*/
|
||||
private static String getReportName(Class<?> sender, ReportType type) {
|
||||
if (sender == null) {
|
||||
throw new IllegalArgumentException("sender cannot be null.");
|
||||
}
|
||||
|
||||
// Whether or not we need to retrieve the report name again
|
||||
if (type.reportName == null) {
|
||||
for (Field field : getReportFields(sender)) {
|
||||
try {
|
||||
field.setAccessible(true);
|
||||
|
||||
if (field.get(null) == type) {
|
||||
// We got the right field!
|
||||
return type.reportName = field.getDeclaringClass().getCanonicalName() + "#" + field.getName();
|
||||
}
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new FieldAccessException("Unable to read field " + field, e);
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("Cannot find report name for " + type);
|
||||
}
|
||||
return type.reportName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all publicly associated reports.
|
||||
* @param sender - sender class.
|
||||
* @return All associated reports.
|
||||
*/
|
||||
public static ReportType[] getReports(Class<?> sender) {
|
||||
if (sender == null) {
|
||||
throw new IllegalArgumentException("sender cannot be NULL.");
|
||||
}
|
||||
List<ReportType> result = new ArrayList<ReportType>();
|
||||
|
||||
// Go through all the fields
|
||||
for (Field field : getReportFields(sender)) {
|
||||
try {
|
||||
field.setAccessible(true);
|
||||
result.add((ReportType) field.get(null));
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new FieldAccessException("Unable to read field " + field, e);
|
||||
}
|
||||
}
|
||||
return result.toArray(new ReportType[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all publicly associated report fields.
|
||||
* @param clazz - sender class.
|
||||
* @return All associated report fields.
|
||||
*/
|
||||
private static List<Field> getReportFields(Class<?> clazz) {
|
||||
return FuzzyReflection.fromClass(clazz, true)
|
||||
.getFieldList(FuzzyFieldContract.newBuilder()
|
||||
.requireModifier(Modifier.STATIC)
|
||||
.typeDerivedOf(ReportType.class)
|
||||
.build());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
package com.github.joelgodofwar.mmh.common.reflect;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.Arrays;
|
||||
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.base.Preconditions;
|
||||
|
||||
public class ExactReflection {
|
||||
|
||||
private static final Joiner COMMA_SEPERATED_JOINER = Joiner.on(", ");
|
||||
|
||||
// The class we're actually representing
|
||||
private final Class<?> source;
|
||||
private final boolean forceAccess;
|
||||
|
||||
private ExactReflection(Class<?> source, boolean forceAccess) {
|
||||
this.source = Preconditions.checkNotNull(source, "source class cannot be NULL");
|
||||
this.forceAccess = forceAccess;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves an exact reflection instance from a given class.
|
||||
*
|
||||
* @param source - the class we'll use.
|
||||
* @param forceAccess - whether to also search for members which are out of the allowed scope.
|
||||
* @return A fuzzy reflection instance.
|
||||
*/
|
||||
public static ExactReflection fromClass(Class<?> source, boolean forceAccess) {
|
||||
return new ExactReflection(source, forceAccess);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves an exact reflection instance from an object.
|
||||
*
|
||||
* @param reference - the object we'll use.
|
||||
* @param forceAccess - whether to also search for members which are out of the allowed scope.
|
||||
* @return A fuzzy reflection instance that uses the class of the given object.
|
||||
*/
|
||||
public static ExactReflection fromObject(Object reference, boolean forceAccess) {
|
||||
return new ExactReflection(reference.getClass(), forceAccess);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the first method in the class hierarchy with the given name and parameters.
|
||||
* <p>
|
||||
* If {@link #isForceAccess()} is TRUE, we will also search for methods which are out of the caller scope.
|
||||
*
|
||||
* @param methodName - the name of the method to find, NULL to only search by using the given parameters.
|
||||
* @param parameters - the parameters of the method to find.
|
||||
* @return the first matching method.
|
||||
* @throws IllegalArgumentException if there is no method with the given name and parameter types.
|
||||
*/
|
||||
public Method getMethod(String methodName, Class<?>... parameters) {
|
||||
Method method = this.lookupMethod(this.source, methodName, parameters);
|
||||
if (method == null) {
|
||||
throw new IllegalArgumentException(String.format(
|
||||
"Unable to find method %s(%s) in %s",
|
||||
methodName,
|
||||
COMMA_SEPERATED_JOINER.join(parameters),
|
||||
this.source.getName()));
|
||||
}
|
||||
|
||||
return method;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the first method in the class hierarchy with the given name and parameters.
|
||||
* <p>
|
||||
* If {@link #isForceAccess()} is TRUE, we will also search for methods which are out of the caller scope.
|
||||
*
|
||||
* @param methodName - the name of the method to find, NULL to only search by using the given parameters.
|
||||
* @param parameters - the parameters of the method to find.
|
||||
* @return the first matching method, NULL if no method matches.
|
||||
*/
|
||||
public Method findMethod(String methodName, Class<?>... parameters) {
|
||||
return this.lookupMethod(this.source, methodName, parameters);
|
||||
}
|
||||
|
||||
// For recursion
|
||||
private Method lookupMethod(Class<?> instanceClass, String methodName, Class<?>... parameters) {
|
||||
for (Method method : instanceClass.getDeclaredMethods()) {
|
||||
if ((this.forceAccess || Modifier.isPublic(method.getModifiers()))
|
||||
&& ((methodName == null) || method.getName().equals(methodName))
|
||||
&& Arrays.equals(method.getParameterTypes(), parameters)) {
|
||||
return method;
|
||||
}
|
||||
}
|
||||
|
||||
// Search in every superclass
|
||||
if (instanceClass.getSuperclass() != null) {
|
||||
return this.lookupMethod(instanceClass.getSuperclass(), methodName, parameters);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a field in the class hierarchy by the given name.
|
||||
* <p>
|
||||
* If {@link #isForceAccess()} is TRUE, we will also search for fields which are out of the caller scope.
|
||||
*
|
||||
* @param fieldName - the name of the field to find.
|
||||
* @return the first matching field.
|
||||
* @throws IllegalArgumentException if no field with the given name was found.
|
||||
*/
|
||||
public Field getField(String fieldName) {
|
||||
Field field = this.lookupField(this.source, fieldName);
|
||||
if (field == null) {
|
||||
throw new IllegalArgumentException(String.format(
|
||||
"Unable to find field with name %s in %s.",
|
||||
fieldName,
|
||||
this.source.getName()));
|
||||
}
|
||||
|
||||
return field;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a field in the class hierarchy by the given name.
|
||||
* <p>
|
||||
* If {@link #isForceAccess()} is TRUE, we will also search for fields which are out of the caller scope.
|
||||
*
|
||||
* @param fieldName - the name of the field to find.
|
||||
* @return the first matching field, null if no field matches.
|
||||
*/
|
||||
public Field findField(String fieldName) {
|
||||
return this.lookupField(this.source, fieldName);
|
||||
}
|
||||
|
||||
// For recursion
|
||||
private Field lookupField(Class<?> instanceClass, String fieldName) {
|
||||
for (Field field : instanceClass.getDeclaredFields()) {
|
||||
if ((this.forceAccess || Modifier.isPublic(field.getModifiers())) && field.getName().equals(fieldName)) {
|
||||
return field;
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively find the correct field
|
||||
if (instanceClass.getSuperclass() != null) {
|
||||
return this.lookupField(instanceClass.getSuperclass(), fieldName);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the first constructor in the class hierarchy with the given parameters.
|
||||
* <p>
|
||||
* If {@link #isForceAccess()} is TRUE, we will also search for constructors which are out of the caller scope.
|
||||
*
|
||||
* @param parameters - the parameters of the constructor to find.
|
||||
* @return the first matching constructor.
|
||||
* @throws IllegalArgumentException if no constructor with the given parameters was found.
|
||||
*/
|
||||
public Constructor<?> getConstructor(Class<?>... parameters) {
|
||||
Constructor<?> constructor = this.findConstructor(parameters);
|
||||
if (constructor == null) {
|
||||
throw new IllegalArgumentException(String.format(
|
||||
"Unable to find constructor (%s) in %s",
|
||||
COMMA_SEPERATED_JOINER.join(parameters),
|
||||
this.source.getName()));
|
||||
}
|
||||
|
||||
return constructor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the first constructor in the class hierarchy with the given parameters.
|
||||
* <p>
|
||||
* If {@link #isForceAccess()} is TRUE, we will also search for constructors which are out of the caller scope.
|
||||
*
|
||||
* @param parameters - the parameters of the constructor to find.
|
||||
* @return the first matching constructor, NULL if no constructor matches.
|
||||
*/
|
||||
public Constructor<?> findConstructor(Class<?>... parameters) {
|
||||
try {
|
||||
Constructor<?> constructor = this.source.getDeclaredConstructor(parameters);
|
||||
return this.forceAccess || Modifier.isPublic(constructor.getModifiers()) ? constructor : null;
|
||||
} catch (NoSuchMethodException exception) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve an {@link ExactReflection} object where scope restrictions are ignored.
|
||||
*
|
||||
* @return A copy of the current object.
|
||||
*/
|
||||
public ExactReflection forceAccess() {
|
||||
return new ExactReflection(this.source, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if we are overriding scope restrictions and will also find private, protected or package members.
|
||||
*
|
||||
* @return TRUE if we are, FALSE otherwise.
|
||||
*/
|
||||
public boolean isForceAccess() {
|
||||
return this.forceAccess;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the source class we are searching.
|
||||
*
|
||||
* @return The source.
|
||||
*/
|
||||
public Class<?> getSource() {
|
||||
return this.source;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
|
||||
* Copyright (C) 2012 Kristian S. Stangeland
|
||||
*
|
||||
* 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 2 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, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
|
||||
* 02111-1307 USA
|
||||
*/
|
||||
package com.github.joelgodofwar.mmh.common.reflect;
|
||||
|
||||
/**
|
||||
* Invoked when a field is inaccessible due to security limitations, or when it simply doesn't exist.
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
public class FieldAccessException extends RuntimeException {
|
||||
|
||||
/**
|
||||
* Generated by Eclipse.
|
||||
*/
|
||||
private static final long serialVersionUID = 1911011681494034617L;
|
||||
|
||||
public FieldAccessException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public FieldAccessException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public FieldAccessException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public FieldAccessException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public static FieldAccessException fromFormat(String message, Object... params) {
|
||||
return new FieldAccessException(String.format(message, params));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
String message = getMessage();
|
||||
return "FieldAccessException" + (message != null ? ": " + message : "");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,597 @@
|
||||
/*
|
||||
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
|
||||
* Copyright (C) 2012 Kristian S. Stangeland
|
||||
*
|
||||
* 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 2 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, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
|
||||
* 02111-1307 USA
|
||||
*/
|
||||
package com.github.joelgodofwar.mmh.common.reflect;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import com.github.joelgodofwar.mmh.common.reflect.accessors.Accessors;
|
||||
import com.github.joelgodofwar.mmh.common.reflect.fuzzy.AbstractFuzzyMatcher;
|
||||
import com.github.joelgodofwar.mmh.common.reflect.fuzzy.FuzzyFieldContract;
|
||||
import com.github.joelgodofwar.mmh.common.reflect.fuzzy.FuzzyMethodContract;
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.collect.Sets;
|
||||
|
||||
/**
|
||||
* Retrieves fields and methods by signature, not just name.
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
public class FuzzyReflection {
|
||||
|
||||
private static final Joiner COMMA_JOINER = Joiner.on(", ");
|
||||
|
||||
// The class we're actually representing
|
||||
private final Class<?> source;
|
||||
private final boolean forceAccess;
|
||||
|
||||
public FuzzyReflection(Class<?> source, boolean forceAccess) {
|
||||
this.source = source;
|
||||
this.forceAccess = forceAccess;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a fuzzy reflection instance from a given class.
|
||||
*
|
||||
* @param source - the class we'll use.
|
||||
* @return A fuzzy reflection instance.
|
||||
*/
|
||||
public static FuzzyReflection fromClass(Class<?> source) {
|
||||
return fromClass(source, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a fuzzy reflection instance from a given class.
|
||||
*
|
||||
* @param source - the class we'll use.
|
||||
* @param forceAccess - whether to override scope restrictions.
|
||||
* @return A fuzzy reflection instance.
|
||||
*/
|
||||
public static FuzzyReflection fromClass(Class<?> source, boolean forceAccess) {
|
||||
return new FuzzyReflection(source, forceAccess);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a fuzzy reflection instance from an object.
|
||||
*
|
||||
* @param reference - the object we'll use.
|
||||
* @return A fuzzy reflection instance that uses the class of the given object.
|
||||
*/
|
||||
public static FuzzyReflection fromObject(Object reference) {
|
||||
return new FuzzyReflection(reference.getClass(), false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a fuzzy reflection instance from an object.
|
||||
*
|
||||
* @param reference - the object we'll use.
|
||||
* @param forceAccess - whether to override scope restrictions.
|
||||
* @return A fuzzy reflection instance that uses the class of the given object.
|
||||
*/
|
||||
public static FuzzyReflection fromObject(Object reference, boolean forceAccess) {
|
||||
return new FuzzyReflection(reference.getClass(), forceAccess);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the value of the first field of the given type.
|
||||
*
|
||||
* @param <T> Type
|
||||
* @param instance - the instance to retrieve from.
|
||||
* @param fieldClass - type of the field to retrieve.
|
||||
* @param forceAccess - whether to look for private and protected fields.
|
||||
* @return The value of that field.
|
||||
* @throws IllegalArgumentException If the field cannot be found.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T> T getFieldValue(Object instance, Class<T> fieldClass, boolean forceAccess) {
|
||||
return (T) Accessors.getFieldAccessor(instance.getClass(), fieldClass, forceAccess).get(instance);
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
public static <T> Set<T> combineArrays(T[]... arrays) {
|
||||
Set<T> result = new LinkedHashSet<>();
|
||||
for (T[] elements : arrays) {
|
||||
if (elements != null) {
|
||||
Collections.addAll(result, elements);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the underlying class.
|
||||
*
|
||||
* @return The underlying class.
|
||||
*/
|
||||
public Class<?> getSource() {
|
||||
return this.source;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves whether or not not to override any scope restrictions.
|
||||
*
|
||||
* @return TRUE if we override scope, FALSE otherwise.
|
||||
*/
|
||||
public boolean isForceAccess() {
|
||||
return this.forceAccess;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the singleton instance of a class, from a method or field.
|
||||
*
|
||||
* @return The singleton instance.
|
||||
* @throws IllegalStateException If the class has no singleton.
|
||||
*/
|
||||
public Object getSingleton() {
|
||||
try {
|
||||
// try a no-args method which is static and returns the same type as the target class
|
||||
Method method = this.getMethod(FuzzyMethodContract.newBuilder()
|
||||
.parameterCount(0)
|
||||
.returnDerivedOf(this.source)
|
||||
.requireModifier(Modifier.STATIC)
|
||||
.build());
|
||||
return Accessors.getMethodAccessor(method).invoke(null);
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
// that method doesn't exist...
|
||||
}
|
||||
|
||||
try {
|
||||
// try a field which is static and of the same type as the target class
|
||||
Field field = this.getField(FuzzyFieldContract.newBuilder()
|
||||
.typeDerivedOf(this.source)
|
||||
.requireModifier(Modifier.STATIC)
|
||||
.build());
|
||||
return Accessors.getFieldAccessor(field).get(null);
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
// that field doesn't exist
|
||||
}
|
||||
|
||||
// we're unable to find the field
|
||||
throw new IllegalStateException("Unable to retrieve singleton instance of " + this.source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the first method that matches.
|
||||
* <p>
|
||||
* ForceAccess must be TRUE in order for this method to access private, protected and package level method.
|
||||
*
|
||||
* @param matcher - the matcher to use.
|
||||
* @return The first method that satisfies the given matcher.
|
||||
* @throws IllegalArgumentException If the method cannot be found.
|
||||
*/
|
||||
public Method getMethod(AbstractFuzzyMatcher<MethodInfo> matcher) {
|
||||
List<Method> result = this.getMethodList(matcher);
|
||||
if (result.size() > 0) {
|
||||
return result.get(0);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unable to find a method that matches " + matcher);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a method that matches. If there are multiple methods that match, the first one with the preferred name is
|
||||
* selected.
|
||||
* <p>
|
||||
* ForceAccess must be TRUE in order for this method to access private, protected and package level method.
|
||||
*
|
||||
* @param matcher - the matcher to use.
|
||||
* @param preferred - the preferred name, null for no preference.
|
||||
* @return The first method that satisfies the given matcher.
|
||||
* @throws IllegalArgumentException If the method cannot be found.
|
||||
*/
|
||||
public Method getMethod(AbstractFuzzyMatcher<MethodInfo> matcher, String preferred) {
|
||||
List<Method> result = this.getMethodList(matcher);
|
||||
|
||||
// if we got more than one result check for the preferred method name
|
||||
if ((result.size() > 1) && (preferred != null)) {
|
||||
for (Method method : result) {
|
||||
if (method.getName().equals(preferred)) {
|
||||
return method;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (result.size() > 0) {
|
||||
return result.get(0);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unable to find a method that matches " + matcher);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a method by looking at its name.
|
||||
*
|
||||
* @param nameRegex - regular expression that will match method names.
|
||||
* @return The first method that satisfies the regular expression.
|
||||
* @throws IllegalArgumentException If the method cannot be found.
|
||||
*/
|
||||
public Method getMethodByName(String nameRegex) {
|
||||
// compile the regex only once
|
||||
Pattern match = Pattern.compile(nameRegex);
|
||||
for (Method method : this.getMethods()) {
|
||||
if (match.matcher(method.getName()).matches()) {
|
||||
// Right - this is probably it.
|
||||
return method;
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException(String.format(
|
||||
"Unable to find a method in %s that matches \"%s\"",
|
||||
this.source,
|
||||
nameRegex));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a method by looking at the parameter types only.
|
||||
*
|
||||
* @param name - potential name of the method. Only used by the error mechanism.
|
||||
* @param args - parameter types of the method to find.
|
||||
* @return The first method that satisfies the parameter types.
|
||||
* @throws IllegalArgumentException If the method cannot be found.
|
||||
*/
|
||||
public Method getMethodByParameters(String name, Class<?>... args) {
|
||||
// Find the correct method to call
|
||||
for (Method method : this.getMethods()) {
|
||||
if (Arrays.equals(method.getParameterTypes(), args)) {
|
||||
return method;
|
||||
}
|
||||
}
|
||||
|
||||
// That sucks
|
||||
throw new IllegalArgumentException(String.format(
|
||||
"Unable to find %s(%s) in %s",
|
||||
name,
|
||||
COMMA_JOINER.join(args),
|
||||
this.source));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a method by looking at the parameter types and return type only.
|
||||
*
|
||||
* @param name - potential name of the method. Only used by the error mechanism.
|
||||
* @param returnType - return type of the method to find.
|
||||
* @param args - parameter types of the method to find.
|
||||
* @return The first method that satisfies the parameter types.
|
||||
* @throws IllegalArgumentException If the method cannot be found.
|
||||
*/
|
||||
public Method getMethodByReturnTypeAndParameters(String name, Class<?> returnType, Class<?>... args) {
|
||||
// Find the correct method to call
|
||||
List<Method> methods = this.getMethodListByParameters(returnType, args);
|
||||
if (methods.size() > 0) {
|
||||
return methods.get(0);
|
||||
} else {
|
||||
// That sucks
|
||||
throw new IllegalArgumentException(String.format(
|
||||
"Unable to find %s(%s): %s in %s",
|
||||
name,
|
||||
COMMA_JOINER.join(args),
|
||||
returnType,
|
||||
this.source));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a list of every method that matches the given matcher.
|
||||
* <p>
|
||||
* ForceAccess must be TRUE in order for this method to access private, protected and package level methods.
|
||||
*
|
||||
* @param matcher - the matcher to apply.
|
||||
* @return List of found methods.
|
||||
*/
|
||||
public List<Method> getMethodList(AbstractFuzzyMatcher<MethodInfo> matcher) {
|
||||
// finds and adds all matching methods
|
||||
List<Method> methods = new ArrayList<>();
|
||||
for (Method method : this.getMethods()) {
|
||||
if (matcher.isMatch(MethodInfo.fromMethod(method), this.source)) {
|
||||
methods.add(method);
|
||||
}
|
||||
}
|
||||
|
||||
return methods;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves every method that has the given parameter types and return type.
|
||||
*
|
||||
* @param returnType - return type of the method to find.
|
||||
* @param args - parameter types of the method to find.
|
||||
* @return Every method that satisfies the given constraints.
|
||||
*/
|
||||
public List<Method> getMethodListByParameters(Class<?> returnType, Class<?>... args) {
|
||||
List<Method> methods = new ArrayList<>();
|
||||
// Find the correct method to call
|
||||
for (Method method : this.getMethods()) {
|
||||
if (method.getReturnType().equals(returnType) && Arrays.equals(method.getParameterTypes(), args)) {
|
||||
methods.add(method);
|
||||
}
|
||||
}
|
||||
|
||||
return methods;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the first field that matches.
|
||||
* <p>
|
||||
* ForceAccess must be TRUE in order for this method to access private, protected and package level fields.
|
||||
*
|
||||
* @param matcher - the matcher to use.
|
||||
* @return The first method that satisfies the given matcher.
|
||||
* @throws IllegalArgumentException If the method cannot be found.
|
||||
*/
|
||||
public Field getField(AbstractFuzzyMatcher<Field> matcher) {
|
||||
List<Field> result = this.getFieldList(matcher);
|
||||
if (result.size() > 0) {
|
||||
return result.get(0);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unable to find a field that matches " + matcher);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a field by name.
|
||||
*
|
||||
* @param nameRegex - regular expression that will match a field name.
|
||||
* @return The first field to match the given expression.
|
||||
* @throws IllegalArgumentException If the field cannot be found.
|
||||
*/
|
||||
public Field getFieldByName(String nameRegex) {
|
||||
// compile the pattern only once
|
||||
Pattern match = Pattern.compile(nameRegex);
|
||||
for (Field field : this.getFields()) {
|
||||
if (match.matcher(field.getName()).matches()) {
|
||||
return field;
|
||||
}
|
||||
}
|
||||
|
||||
// Looks like we're outdated. Too bad.
|
||||
throw new IllegalArgumentException(String.format(
|
||||
"Unable to find a field with a name matching \"%s\" in %s",
|
||||
nameRegex,
|
||||
this.source));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the first field with a type equal to or more specific to the given type.
|
||||
*
|
||||
* @param name - name the field probably is given. This will only be used in the error message.
|
||||
* @param type - type of the field to find.
|
||||
* @return The first field with a type that is an instance of the given type.
|
||||
*/
|
||||
public Field getFieldByType(String name, Class<?> type) {
|
||||
List<Field> fields = this.getFieldListByType(type);
|
||||
if (fields.size() > 0) {
|
||||
return fields.get(0);
|
||||
} else {
|
||||
// Looks like we're outdated. Too bad.
|
||||
throw new IllegalArgumentException(String.format(
|
||||
"Unable to find a field \"%s\" with the type %s in %s",
|
||||
name,
|
||||
type,
|
||||
this.source));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves every field with a type equal to or more specific to the given type.
|
||||
*
|
||||
* @param type - type of the fields to find.
|
||||
* @return Every field with a type that is an instance of the given type.
|
||||
*/
|
||||
public List<Field> getFieldListByType(Class<?> type) {
|
||||
// Field with a compatible type
|
||||
List<Field> fields = new ArrayList<>();
|
||||
for (Field field : this.getFields()) {
|
||||
if (type.isAssignableFrom(field.getType())) {
|
||||
fields.add(field);
|
||||
}
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a field with a given type and parameters. This is most useful when dealing with Collections.
|
||||
*
|
||||
* @param fieldType Type of the field
|
||||
* @param params Variable length array of type parameters
|
||||
* @return The field
|
||||
* @throws IllegalArgumentException If the field cannot be found
|
||||
*/
|
||||
public Field getParameterizedField(Class<?> fieldType, Class<?>... params) {
|
||||
for (Field field : this.getFields()) {
|
||||
if (field.getType().equals(fieldType)) {
|
||||
Type type = field.getGenericType();
|
||||
if (type instanceof ParameterizedType) {
|
||||
if (Arrays.equals(((ParameterizedType) type).getActualTypeArguments(), params)) {
|
||||
return field;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException(String.format(
|
||||
"Unable to find a field of type %s<%s> in %s",
|
||||
fieldType,
|
||||
COMMA_JOINER.join(params),
|
||||
this.source));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a list of every field that matches the given matcher.
|
||||
* <p>
|
||||
* ForceAccess must be TRUE in order for this method to access private, protected and package level fields.
|
||||
*
|
||||
* @param matcher - the matcher to apply.
|
||||
* @return List of found fields.
|
||||
*/
|
||||
public List<Field> getFieldList(AbstractFuzzyMatcher<Field> matcher) {
|
||||
// Add all matching fields to the list
|
||||
List<Field> fields = new ArrayList<>();
|
||||
for (Field field : this.getFields()) {
|
||||
if (matcher.isMatch(field, this.source)) {
|
||||
fields.add(field);
|
||||
}
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a field by type.
|
||||
* <p>
|
||||
* Note that the type is matched using the full canonical representation, i.e.:
|
||||
* <ul>
|
||||
* <li>java.util.List</li>
|
||||
* <li>net.comphenix.xp.ExperienceMod</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param typeRegex - regular expression that will match the field type.
|
||||
* @return The first field with a type that matches the given regular expression.
|
||||
* @throws IllegalArgumentException If the field cannot be found.
|
||||
*/
|
||||
public Field getFieldByType(String typeRegex) {
|
||||
Pattern match = Pattern.compile(typeRegex);
|
||||
|
||||
// Like above, only here we test the field type
|
||||
for (Field field : this.getFields()) {
|
||||
String name = field.getType().getName();
|
||||
if (match.matcher(name).matches()) {
|
||||
return field;
|
||||
}
|
||||
}
|
||||
|
||||
// Looks like we're outdated. Too bad.
|
||||
throw new IllegalArgumentException(String.format(
|
||||
"Unable to find a field with a type that matches \"%s\" in %s",
|
||||
typeRegex,
|
||||
this.source));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the first constructor that matches.
|
||||
* <p>
|
||||
* ForceAccess must be TRUE in order for this method to access private, protected and package level constructors.
|
||||
*
|
||||
* @param matcher - the matcher to use.
|
||||
* @return The first constructor that satisfies the given matcher.
|
||||
* @throws IllegalArgumentException If the constructor cannot be found.
|
||||
*/
|
||||
public Constructor<?> getConstructor(AbstractFuzzyMatcher<MethodInfo> matcher) {
|
||||
List<Constructor<?>> result = this.getConstructorList(matcher);
|
||||
if (result.size() > 0) {
|
||||
return result.get(0);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unable to find a method that matches " + matcher);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a list of every constructor that matches the given matcher.
|
||||
* <p>
|
||||
* ForceAccess must be TRUE in order for this method to access private, protected and package level constructors.
|
||||
*
|
||||
* @param matcher - the matcher to apply.
|
||||
* @return List of found constructors.
|
||||
*/
|
||||
public List<Constructor<?>> getConstructorList(AbstractFuzzyMatcher<MethodInfo> matcher) {
|
||||
// Add all matching constructors to the list
|
||||
List<Constructor<?>> constructors = new ArrayList<>();
|
||||
for (Constructor<?> constructor : this.getConstructors()) {
|
||||
if (matcher.isMatch(MethodInfo.fromConstructor(constructor), this.source)) {
|
||||
constructors.add(constructor);
|
||||
}
|
||||
}
|
||||
|
||||
return constructors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all private and public fields in declared order.
|
||||
* <p>
|
||||
* Private, protected and package fields are ignored if forceAccess is FALSE.
|
||||
*
|
||||
* @return Every field.
|
||||
*/
|
||||
public Set<Field> getFields() {
|
||||
if (this.forceAccess) {
|
||||
return combineArrays(this.source.getDeclaredFields(), this.source.getFields());
|
||||
} else {
|
||||
return combineArrays(this.source.getFields());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all private and public fields, up until a certain superclass.
|
||||
*
|
||||
* @param excludeClass - the class (and its superclasses) to exclude from the search.
|
||||
* @return Every such declared field.
|
||||
*/
|
||||
public Set<Field> getDeclaredFields(Class<?> excludeClass) {
|
||||
// we only need to do this if we include inherited fields
|
||||
if (this.forceAccess) {
|
||||
Class<?> current = this.source;
|
||||
Set<Field> fields = Sets.newLinkedHashSet();
|
||||
|
||||
while ((current != null) && (current != excludeClass)) {
|
||||
fields.addAll(Arrays.asList(current.getDeclaredFields()));
|
||||
current = current.getSuperclass();
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
return this.getFields();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all private and public methods in declared order (after JDK 1.5).
|
||||
* <p>
|
||||
* Private, protected and package methods are ignored if forceAccess is FALSE.
|
||||
*
|
||||
* @return Every method.
|
||||
*/
|
||||
public Set<Method> getMethods() {
|
||||
if (this.forceAccess) {
|
||||
return combineArrays(this.source.getDeclaredMethods(), this.source.getMethods());
|
||||
} else {
|
||||
return combineArrays(this.source.getMethods());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all private and public constructors in declared order (after JDK 1.5).
|
||||
* <p>
|
||||
* Private, protected and package constructors are ignored if forceAccess is FALSE.
|
||||
*
|
||||
* @return Every constructor.
|
||||
*/
|
||||
public Set<Constructor<?>> getConstructors() {
|
||||
return combineArrays(this.forceAccess ? this.source.getDeclaredConstructors() : this.source.getConstructors());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
package com.github.joelgodofwar.mmh.common.reflect;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.GenericDeclaration;
|
||||
import java.lang.reflect.Member;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.TypeVariable;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
/**
|
||||
* Represents a method or a constructor.
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
public abstract class MethodInfo implements GenericDeclaration, Member {
|
||||
|
||||
/**
|
||||
* Wraps a method as a MethodInfo object.
|
||||
*
|
||||
* @param method - the method to wrap.
|
||||
* @return The wrapped method.
|
||||
*/
|
||||
public static MethodInfo fromMethod(final Method method) {
|
||||
return new MethodInfo() {
|
||||
@Override
|
||||
public <T extends Annotation> T getAnnotation(Class<T> annotationClass) {
|
||||
return method.getAnnotation(annotationClass);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Annotation[] getAnnotations() {
|
||||
return method.getAnnotations();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Annotation[] getDeclaredAnnotations() {
|
||||
return method.getDeclaredAnnotations();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return method.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?>[] getParameterTypes() {
|
||||
return method.getParameterTypes();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getDeclaringClass() {
|
||||
return method.getDeclaringClass();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getReturnType() {
|
||||
return method.getReturnType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getModifiers() {
|
||||
return method.getModifiers();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?>[] getExceptionTypes() {
|
||||
return method.getExceptionTypes();
|
||||
}
|
||||
|
||||
@Override
|
||||
public TypeVariable<?>[] getTypeParameters() {
|
||||
return method.getTypeParameters();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toGenericString() {
|
||||
return method.toGenericString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return method.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSynthetic() {
|
||||
return method.isSynthetic();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return method.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConstructor() {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a list of method infos from a given array of methods.
|
||||
*
|
||||
* @param methods - array of methods.
|
||||
* @return Method info list.
|
||||
*/
|
||||
public static Collection<MethodInfo> fromMethods(Method[] methods) {
|
||||
return fromMethods(Arrays.asList(methods));
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a list of method infos from a given collection of methods.
|
||||
*
|
||||
* @param methods - list of methods.
|
||||
* @return Method info list.
|
||||
*/
|
||||
public static List<MethodInfo> fromMethods(Collection<Method> methods) {
|
||||
List<MethodInfo> infos = Lists.newArrayList();
|
||||
for (Method method : methods) {
|
||||
infos.add(fromMethod(method));
|
||||
}
|
||||
|
||||
return infos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a constructor as a method information object.
|
||||
*
|
||||
* @param constructor - the constructor to wrap.
|
||||
* @return A wrapped constructor.
|
||||
*/
|
||||
public static MethodInfo fromConstructor(final Constructor<?> constructor) {
|
||||
return new MethodInfo() {
|
||||
@Override
|
||||
public <T extends Annotation> T getAnnotation(Class<T> annotationClass) {
|
||||
return constructor.getAnnotation(annotationClass);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Annotation[] getAnnotations() {
|
||||
return constructor.getAnnotations();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Annotation[] getDeclaredAnnotations() {
|
||||
return constructor.getDeclaredAnnotations();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return constructor.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?>[] getParameterTypes() {
|
||||
return constructor.getParameterTypes();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getDeclaringClass() {
|
||||
return constructor.getDeclaringClass();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getReturnType() {
|
||||
return Void.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getModifiers() {
|
||||
return constructor.getModifiers();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?>[] getExceptionTypes() {
|
||||
return constructor.getExceptionTypes();
|
||||
}
|
||||
|
||||
@Override
|
||||
public TypeVariable<?>[] getTypeParameters() {
|
||||
return constructor.getTypeParameters();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toGenericString() {
|
||||
return constructor.toGenericString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return constructor.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSynthetic() {
|
||||
return constructor.isSynthetic();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return constructor.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConstructor() {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a list of method infos from a given array of constructors.
|
||||
*
|
||||
* @param constructors - array of constructors.
|
||||
* @return Method info list.
|
||||
*/
|
||||
public static Collection<MethodInfo> fromConstructors(Constructor<?>[] constructors) {
|
||||
return fromConstructors(Arrays.asList(constructors));
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a list of method infos from a given collection of constructors.
|
||||
*
|
||||
* @param constructors - list of constructors.
|
||||
* @return Method info list.
|
||||
*/
|
||||
public static List<MethodInfo> fromConstructors(Collection<Constructor<?>> constructors) {
|
||||
List<MethodInfo> infos = Lists.newArrayList();
|
||||
|
||||
for (Constructor<?> constructor : constructors) {
|
||||
infos.add(fromConstructor(constructor));
|
||||
}
|
||||
return infos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string describing this method or constructor
|
||||
*
|
||||
* @return A string representation of the object.
|
||||
* @see Method#toString()
|
||||
* @see Constructor#toString()
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string describing this method or constructor, including type parameters.
|
||||
*
|
||||
* @return A string describing this Method, include type parameters
|
||||
* @see Method#toGenericString()
|
||||
* @see Constructor#toGenericString()
|
||||
*/
|
||||
public abstract String toGenericString();
|
||||
|
||||
/**
|
||||
* Returns an array of Class objects that represent the types of the exceptions declared to be thrown by the
|
||||
* underlying method or constructor represented by this MethodInfo object.
|
||||
*
|
||||
* @return The exception types declared as being thrown by the method or constructor this object represents.
|
||||
* @see Method#getExceptionTypes()
|
||||
* @see Constructor#getExceptionTypes()
|
||||
*/
|
||||
public abstract Class<?>[] getExceptionTypes();
|
||||
|
||||
/**
|
||||
* Returns a Class object that represents the formal return type of the method or constructor represented by this
|
||||
* MethodInfo object.
|
||||
* <p>
|
||||
* This is always {@link Void} for constructors.
|
||||
*
|
||||
* @return The return value, or Void if a constructor.
|
||||
* @see Method#getReturnType()
|
||||
*/
|
||||
public abstract Class<?> getReturnType();
|
||||
|
||||
/**
|
||||
* Returns an array of Class objects that represent the formal parameter types, in declaration order, of the method or
|
||||
* constructor represented by this MethodInfo object.
|
||||
*
|
||||
* @return The parameter types for the method or constructor this object represents.
|
||||
* @see Method#getParameterTypes()
|
||||
* @see Constructor#getParameterTypes()
|
||||
*/
|
||||
public abstract Class<?>[] getParameterTypes();
|
||||
|
||||
/**
|
||||
* Determine if this is a constructor or not.
|
||||
*
|
||||
* @return TRUE if this represents a constructor, FALSE otherwise.
|
||||
*/
|
||||
public abstract boolean isConstructor();
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
package com.github.joelgodofwar.mmh.common.reflect;
|
||||
|
||||
import java.lang.reflect.Array;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Set;
|
||||
|
||||
import com.github.joelgodofwar.mmh.common.reflect.accessors.Accessors;
|
||||
import com.google.common.primitives.Primitives;
|
||||
|
||||
/**
|
||||
* Used to print the content of an arbitrary class.
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
public class PrettyPrinter {
|
||||
|
||||
/**
|
||||
* How far we will recurse.
|
||||
*/
|
||||
public static final int RECURSE_DEPTH = 3;
|
||||
|
||||
/**
|
||||
* Print the contents of an object.
|
||||
*
|
||||
* @param object - the object to serialize.
|
||||
* @return String representation of the class.
|
||||
* @throws IllegalAccessException If the object is null
|
||||
*/
|
||||
public static String printObject(Object object) throws IllegalAccessException {
|
||||
if (object == null) {
|
||||
throw new IllegalArgumentException("object cannot be NULL.");
|
||||
}
|
||||
|
||||
return printObject(object, object.getClass(), Object.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Print the contents of an object.
|
||||
*
|
||||
* @param object - the object to serialize.
|
||||
* @param start - class to start at.
|
||||
* @param stop - superclass that will stop the process.
|
||||
* @return String representation of the class
|
||||
* @throws IllegalAccessException If the object is null
|
||||
*/
|
||||
public static String printObject(Object object, Class<?> start, Class<?> stop) throws IllegalAccessException {
|
||||
if (object == null) {
|
||||
throw new IllegalArgumentException("object cannot be NULL.");
|
||||
}
|
||||
|
||||
return printObject(object, start, stop, RECURSE_DEPTH);
|
||||
}
|
||||
|
||||
/**
|
||||
* Print the contents of an object.
|
||||
*
|
||||
* @param object - the object to serialize.
|
||||
* @param start - class to start at.
|
||||
* @param stop - superclass that will stop the process.
|
||||
* @param hierachyDepth - maximum recursion level.
|
||||
* @return String representation of the class.
|
||||
* @throws IllegalAccessException If the object is null
|
||||
*/
|
||||
public static String printObject(Object object, Class<?> start, Class<?> stop, int hierachyDepth)
|
||||
throws IllegalAccessException {
|
||||
return printObject(object, start, stop, hierachyDepth, ObjectPrinter.DEFAULT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Print the contents of an object.
|
||||
*
|
||||
* @param object - the object to serialize.
|
||||
* @param start - class to start at.
|
||||
* @param stop - superclass that will stop the process.
|
||||
* @param hierachyDepth - maximum recursion level.
|
||||
* @param printer - a generic object printer.
|
||||
* @return String representation of the class.
|
||||
* @throws IllegalAccessException If the object is null
|
||||
*/
|
||||
public static String printObject(Object object, Class<?> start, Class<?> stop, int hierachyDepth,
|
||||
ObjectPrinter printer) throws IllegalAccessException {
|
||||
if (object == null) {
|
||||
throw new IllegalArgumentException("object cannot be NULL.");
|
||||
}
|
||||
|
||||
StringBuilder output = new StringBuilder();
|
||||
Set<Object> previous = new HashSet<>();
|
||||
|
||||
// Start and stop
|
||||
output.append("{ ");
|
||||
printObject(output, object, start, stop, previous, hierachyDepth, true, printer);
|
||||
output.append(" }");
|
||||
|
||||
return output.toString();
|
||||
}
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
private static void printIterables(StringBuilder output, Iterable iterable, Class<?> stop,
|
||||
Set<Object> previous, int hierachyIndex, ObjectPrinter printer) throws IllegalAccessException {
|
||||
|
||||
boolean first = true;
|
||||
output.append("(");
|
||||
|
||||
for (Object value : iterable) {
|
||||
if (first) {
|
||||
first = false;
|
||||
} else {
|
||||
output.append(", ");
|
||||
}
|
||||
|
||||
// Print value
|
||||
printValue(output, value, stop, previous, hierachyIndex - 1, printer);
|
||||
}
|
||||
|
||||
output.append(")");
|
||||
}
|
||||
|
||||
/**
|
||||
* Print the content of a maps entries.
|
||||
*
|
||||
* @param output - the output string builder.
|
||||
* @param map - the map to print.
|
||||
* @param current - the type of this map.
|
||||
* @param stop - the class that indicates we should stop printing.
|
||||
* @param previous - previous objects printed.
|
||||
* @param hierachyIndex - hierachy index.
|
||||
* @throws IllegalAccessException If any reflection went wrong.
|
||||
*/
|
||||
private static void printMap(StringBuilder output, Map<Object, Object> map, Class<?> current, Class<?> stop,
|
||||
Set<Object> previous, int hierachyIndex, ObjectPrinter printer) throws IllegalAccessException {
|
||||
|
||||
boolean first = true;
|
||||
output.append("[");
|
||||
|
||||
for (Entry<Object, Object> entry : map.entrySet()) {
|
||||
if (first) {
|
||||
first = false;
|
||||
} else {
|
||||
output.append(", ");
|
||||
}
|
||||
|
||||
printValue(output, entry.getKey(), stop, previous, hierachyIndex - 1, printer);
|
||||
output.append(": ");
|
||||
printValue(output, entry.getValue(), stop, previous, hierachyIndex - 1, printer);
|
||||
}
|
||||
|
||||
output.append("]");
|
||||
}
|
||||
|
||||
private static void printArray(StringBuilder output, Object array, Class<?> current, Class<?> stop,
|
||||
Set<Object> previous, int hierachyIndex, ObjectPrinter printer) throws IllegalAccessException {
|
||||
|
||||
Class<?> component = current.getComponentType();
|
||||
boolean first = true;
|
||||
|
||||
if (!component.isArray()) {
|
||||
output.append(component.getName());
|
||||
}
|
||||
output.append("[");
|
||||
|
||||
for (int i = 0; i < Array.getLength(array); i++) {
|
||||
if (first) {
|
||||
first = false;
|
||||
} else {
|
||||
output.append(", ");
|
||||
}
|
||||
|
||||
// Handle exceptions
|
||||
try {
|
||||
printValue(output, Array.get(array, i), component, stop, previous, hierachyIndex - 1, printer);
|
||||
} catch (ArrayIndexOutOfBoundsException e) {
|
||||
e.printStackTrace();
|
||||
break;
|
||||
} catch (IllegalArgumentException e) {
|
||||
e.printStackTrace();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
output.append("]");
|
||||
}
|
||||
|
||||
// Internal recursion method
|
||||
private static void printObject(StringBuilder output, Object object, Class<?> current, Class<?> stop,
|
||||
Set<Object> previous, int hierachyIndex, boolean first,
|
||||
ObjectPrinter printer) throws IllegalAccessException {
|
||||
|
||||
// See if we're supposed to skip this class
|
||||
if ((current == null) || (current == Object.class) || ((stop != null) && current.equals(stop))) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't iterate twice
|
||||
previous.add(object);
|
||||
|
||||
// Hard coded limit
|
||||
if (hierachyIndex < 0) {
|
||||
output.append("...");
|
||||
return;
|
||||
}
|
||||
|
||||
for (Field field : current.getDeclaredFields()) {
|
||||
int mod = field.getModifiers();
|
||||
|
||||
// Skip a good number of the fields
|
||||
if (!Modifier.isTransient(mod) && !Modifier.isStatic(mod)) {
|
||||
Class<?> type = field.getType();
|
||||
Object value = Accessors.getFieldAccessor(field).get(object);
|
||||
|
||||
if (first) {
|
||||
first = false;
|
||||
} else {
|
||||
output.append(", ");
|
||||
}
|
||||
|
||||
output.append(field.getName());
|
||||
output.append(" = ");
|
||||
printValue(output, value, type, stop, previous, hierachyIndex - 1, printer);
|
||||
}
|
||||
}
|
||||
|
||||
// Recurse
|
||||
printObject(output, object, current.getSuperclass(), stop, previous, hierachyIndex, first, printer);
|
||||
}
|
||||
|
||||
private static void printValue(StringBuilder output, Object value, Class<?> stop,
|
||||
Set<Object> previous, int hierachyIndex, ObjectPrinter printer) throws IllegalAccessException {
|
||||
// Handle the NULL case
|
||||
printValue(output, value, value != null ? value.getClass() : null, stop, previous, hierachyIndex, printer);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked") private static void printValue(StringBuilder output, Object value, Class<?> type,
|
||||
Class<?> stop, Set<Object> previous, int hierachyIndex,
|
||||
ObjectPrinter printer) throws IllegalAccessException {
|
||||
|
||||
// Just print primitive types
|
||||
if (printer.print(output, value)) {
|
||||
return;
|
||||
} else if (value == null) {
|
||||
output.append("NULL");
|
||||
} else if (type.isPrimitive() || Primitives.isWrapperType(type)) {
|
||||
output.append(value);
|
||||
} else if ((type == String.class) || (hierachyIndex <= 0)) {
|
||||
output.append("\"").append(value).append("\"");
|
||||
} else if (type.isArray()) {
|
||||
printArray(output, value, type, stop, previous, hierachyIndex, printer);
|
||||
} else if (Iterable.class.isAssignableFrom(type)) {
|
||||
printIterables(output, (Iterable<?>) value, stop, previous, hierachyIndex, printer);
|
||||
} else if (Map.class.isAssignableFrom(type)) {
|
||||
printMap(output, (Map<Object, Object>) value, type, stop, previous, hierachyIndex, printer);
|
||||
} else if (ClassLoader.class.isAssignableFrom(type) || previous.contains(value)) {
|
||||
// Don't print previous objects
|
||||
output.append("\"").append(value).append("\"");
|
||||
} else {
|
||||
output.append("{ ");
|
||||
printObject(output, value, value.getClass(), stop, previous, hierachyIndex, true, printer);
|
||||
output.append(" }");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a generic object printer.
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
public interface ObjectPrinter {
|
||||
|
||||
ObjectPrinter DEFAULT = (output, value) -> false;
|
||||
|
||||
/**
|
||||
* Print the content of the given object.
|
||||
* <p>
|
||||
* Return FALSE in order for let the default printer take over.
|
||||
*
|
||||
* @param output - where to print the output.
|
||||
* @param value - the value to print, may be NULL.
|
||||
* @return TRUE if we processed the value and added to the output, FALSE otherwise.
|
||||
*/
|
||||
boolean print(StringBuilder output, Object value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
package com.github.joelgodofwar.mmh.common.reflect.accessors;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import com.github.joelgodofwar.mmh.common.reflect.ExactReflection;
|
||||
import com.github.joelgodofwar.mmh.common.reflect.FuzzyReflection;
|
||||
|
||||
// TODO: at some point we should make everything nullable to make updates easier
|
||||
|
||||
public final class Accessors {
|
||||
|
||||
// Seal this class
|
||||
private Accessors() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve an accessor (in declared order) for every field of the givne type.
|
||||
*
|
||||
* @param clazz - the type of the instance to retrieve.
|
||||
* @param fieldClass - type of the field(s) to retrieve.
|
||||
* @param forceAccess - whether to look for private and protected fields.
|
||||
* @return The accessors.
|
||||
*/
|
||||
public static FieldAccessor[] getFieldAccessorArray(Class<?> clazz, Class<?> fieldClass, boolean forceAccess) {
|
||||
return FuzzyReflection.fromClass(clazz, forceAccess).getFieldListByType(fieldClass).stream()
|
||||
.map(Accessors::getFieldAccessor)
|
||||
.toArray(FieldAccessor[]::new);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve an accessor for the first field of the given type.
|
||||
*
|
||||
* @param instanceClass - the type of the instance to retrieve.
|
||||
* @param fieldName - name of the field to retrieve.
|
||||
* @param forceAccess - whether to look for private and protected fields.
|
||||
* @return The value of that field.
|
||||
* @throws IllegalArgumentException If the field cannot be found.
|
||||
*/
|
||||
//public static FieldAccessor getFieldAccessor(Class<?> instanceClass, String fieldName, boolean forceAccess) {
|
||||
// return Accessors.getFieldAccessor(ExactReflection.fromClass(instanceClass, forceAccess).getField(fieldName));
|
||||
//}
|
||||
|
||||
/**
|
||||
* Retrieve an accessor for the first field of the given type.
|
||||
*
|
||||
* @param instanceClass - the type of the instance to retrieve.
|
||||
* @param fieldClass - type of the field to retrieve.
|
||||
* @param forceAccess - whether to look for private and protected fields.
|
||||
* @return The field accessor.
|
||||
* @throws IllegalArgumentException If the field cannot be found.
|
||||
*/
|
||||
public static FieldAccessor getFieldAccessor(Class<?> instanceClass, Class<?> fieldClass, boolean forceAccess) {
|
||||
// Get a field accessor
|
||||
Field field = FuzzyReflection.fromClass(instanceClass, forceAccess).getFieldByType(null, fieldClass);
|
||||
return Accessors.getFieldAccessor(field);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a field accessor for a field with the given name and equivalent type, or NULL.
|
||||
*
|
||||
* @param clazz - the declaration class.
|
||||
* @param fieldName - the field name.
|
||||
* @param fieldType - assignable field type.
|
||||
* @return The field accessor, or NULL if not found.
|
||||
*/
|
||||
public static FieldAccessor getFieldAccessorOrNull(Class<?> clazz, String fieldName, Class<?> fieldType) {
|
||||
Field field = ExactReflection.fromClass(clazz, true).findField(fieldName);
|
||||
if ((field != null) && ((fieldType == null) || fieldType.isAssignableFrom(field.getType()))) {
|
||||
return Accessors.getFieldAccessor(field);
|
||||
}
|
||||
|
||||
// no matching field found
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a field accessor from a given field that uses unchecked exceptions.
|
||||
*
|
||||
* @param field - the field.
|
||||
* @return The field accessor.
|
||||
*/
|
||||
public static FieldAccessor getFieldAccessor(Field field) {
|
||||
return MethodHandleHelper.getFieldAccessor(field);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a field accessor that will cache the content of the field.
|
||||
* <p>
|
||||
* Note that we don't check if the underlying field has changed after the value has been cached, so it's best to use
|
||||
* this on final fields.
|
||||
*
|
||||
* @param inner - the accessor.
|
||||
* @return A cached field accessor.
|
||||
*/
|
||||
public static FieldAccessor getMemorizing(FieldAccessor inner) {
|
||||
return new MemorizingFieldAccessor(inner);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a method accessor for a method with the given name and signature.
|
||||
*
|
||||
* @param instanceClass - the parent class.
|
||||
* @param methodName - the method name.
|
||||
* @param parameters - the parameters.
|
||||
* @return The method accessor.
|
||||
*/
|
||||
public static MethodAccessor getMethodAccessor(Class<?> instanceClass, String methodName, Class<?>... parameters) {
|
||||
Method method = ExactReflection.fromClass(instanceClass, true).getMethod(methodName, parameters);
|
||||
return Accessors.getMethodAccessor(method);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a method accessor for a field with the given name and equivalent type, or NULL.
|
||||
*
|
||||
* @param clazz - the declaration class.
|
||||
* @param methodName - the method name.
|
||||
* @return The method accessor, or NULL if not found.
|
||||
*/
|
||||
public static MethodAccessor getMethodAccessorOrNull(Class<?> clazz, String methodName, Class<?>... parameters) {
|
||||
Method method = ExactReflection.fromClass(clazz, true).findMethod(methodName, parameters);
|
||||
return method == null ? null : Accessors.getMethodAccessor(method);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a method accessor for a particular method, avoding checked exceptions.
|
||||
*
|
||||
* @param method - the method to access.
|
||||
* @return The method accessor.
|
||||
*/
|
||||
public static MethodAccessor getMethodAccessor(Method method) {
|
||||
return MethodHandleHelper.getMethodAccessor(method);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a constructor accessor for a constructor with the given signature.
|
||||
*
|
||||
* @param instanceClass - the parent class.
|
||||
* @param parameters - the parameters.
|
||||
* @return The constructor accessor.
|
||||
* @throws IllegalArgumentException If we cannot find this constructor.
|
||||
*/
|
||||
public static ConstructorAccessor getConstructorAccessor(Class<?> instanceClass, Class<?>... parameters) {
|
||||
Constructor<?> constructor = ExactReflection.fromClass(instanceClass, true).findConstructor(parameters);
|
||||
return Accessors.getConstructorAccessor(constructor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a specific constructor in a class.
|
||||
*
|
||||
* @param clazz - the class.
|
||||
* @param parameters - the signature of the constructor to find.
|
||||
* @return The constructor, or NULL if not found.
|
||||
*/
|
||||
public static ConstructorAccessor getConstructorAccessorOrNull(Class<?> clazz, Class<?>... parameters) {
|
||||
Constructor<?> constructor = ExactReflection.fromClass(clazz, true).findConstructor(parameters);
|
||||
return constructor == null ? null : Accessors.getConstructorAccessor(constructor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a constructor accessor for a particular constructor, avoding checked exceptions.
|
||||
*
|
||||
* @param constructor - the constructor to access.
|
||||
* @return The method accessor.
|
||||
*/
|
||||
public static ConstructorAccessor getConstructorAccessor(final Constructor<?> constructor) {
|
||||
return MethodHandleHelper.getConstructorAccessor(constructor);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.github.joelgodofwar.mmh.common.reflect.accessors;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
|
||||
public interface ConstructorAccessor {
|
||||
|
||||
/**
|
||||
* Invoke the underlying constructor.
|
||||
*
|
||||
* @param args - the arguments to pass to the method.
|
||||
* @return The return value, or NULL for void methods.
|
||||
*/
|
||||
Object invoke(Object... args);
|
||||
|
||||
/**
|
||||
* Retrieve the underlying constructor.
|
||||
*
|
||||
* @return The method.
|
||||
*/
|
||||
Constructor<?> getConstructor();
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.github.joelgodofwar.mmh.common.reflect.accessors;
|
||||
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.reflect.Constructor;
|
||||
|
||||
final class DefaultConstrutorAccessor implements ConstructorAccessor {
|
||||
|
||||
private final Constructor<?> constructor;
|
||||
private final MethodHandle constructorAccessor;
|
||||
|
||||
public DefaultConstrutorAccessor(Constructor<?> constructor, MethodHandle constructorAccessor) {
|
||||
this.constructor = constructor;
|
||||
this.constructorAccessor = constructorAccessor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object invoke(Object... args) {
|
||||
try {
|
||||
return this.constructorAccessor.invokeExact(args);
|
||||
} catch (Throwable throwable) {
|
||||
throw new IllegalStateException("Unable to construct new instance using " + this.constructor, throwable);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Constructor<?> getConstructor() {
|
||||
return this.constructor;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.github.joelgodofwar.mmh.common.reflect.accessors;
|
||||
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
|
||||
final class DefaultFieldAccessor implements FieldAccessor {
|
||||
|
||||
private final Field field;
|
||||
private final boolean staticField;
|
||||
|
||||
private final MethodHandle setter;
|
||||
private final MethodHandle getter;
|
||||
|
||||
public DefaultFieldAccessor(Field field, MethodHandle setter, MethodHandle getter, boolean staticField) {
|
||||
this.field = Preconditions.checkNotNull(field, "field");
|
||||
this.setter = Preconditions.checkNotNull(setter, "setter");
|
||||
this.getter = Preconditions.checkNotNull(getter, "getter");
|
||||
this.staticField = staticField;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object get(Object instance) {
|
||||
try {
|
||||
// we need this check to as the handle will treat "null" as an instance too
|
||||
return this.staticField ? this.getter.invokeExact() : this.getter.invokeExact(instance);
|
||||
} catch (Throwable throwable) {
|
||||
throw new IllegalStateException("Unable to read field value of " + this.field, throwable);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void set(Object instance, Object value) {
|
||||
try {
|
||||
// we need this check to as the handle will treat "null" as an instance too
|
||||
if (this.staticField) {
|
||||
this.setter.invokeExact(value);
|
||||
} else {
|
||||
this.setter.invokeExact(instance, value);
|
||||
}
|
||||
} catch (Throwable throwable) {
|
||||
throw new IllegalStateException("Unable to set value of field " + this.field, throwable);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Field getField() {
|
||||
return this.field;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.github.joelgodofwar.mmh.common.reflect.accessors;
|
||||
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
final class DefaultMethodAccessor implements MethodAccessor {
|
||||
|
||||
private final Method method;
|
||||
private final MethodHandle methodHandle;
|
||||
|
||||
public DefaultMethodAccessor(Method method, MethodHandle methodHandle, boolean staticMethod) {
|
||||
this.method = method;
|
||||
this.methodHandle = methodHandle;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object invoke(Object target, Object... args) {
|
||||
try {
|
||||
return this.methodHandle.invoke(target, args);
|
||||
} catch (Throwable throwable) {
|
||||
throw new IllegalStateException("Unable to invoke method " + this.method, throwable);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Method getMethod() {
|
||||
return this.method;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.github.joelgodofwar.mmh.common.reflect.accessors;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
/**
|
||||
* Represents an interface for accessing a field.
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
public interface FieldAccessor {
|
||||
|
||||
/**
|
||||
* Retrieve the value of a field for a particular instance.
|
||||
*
|
||||
* @param instance - the instance, or NULL for a static field.
|
||||
* @return The value of the field.
|
||||
* @throws IllegalStateException If the current security context prohibits reflection.
|
||||
*/
|
||||
Object get(Object instance);
|
||||
|
||||
/**
|
||||
* Set the value of a field for a particular instance.
|
||||
*
|
||||
* @param instance - the instance, or NULL for a static field.
|
||||
* @param value - the new value of the field.
|
||||
*/
|
||||
void set(Object instance, Object value);
|
||||
|
||||
/**
|
||||
* Retrieve the underlying field.
|
||||
*
|
||||
* @return The field.
|
||||
*/
|
||||
Field getField();
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.github.joelgodofwar.mmh.common.reflect.accessors;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
final class MemorizingFieldAccessor implements FieldAccessor {
|
||||
|
||||
// a marker object which indicates the value of the field wasn't yet read
|
||||
private static final Object NIL = new Object();
|
||||
|
||||
private final FieldAccessor inner;
|
||||
private volatile Object fieldValue = NIL;
|
||||
|
||||
public MemorizingFieldAccessor(FieldAccessor inner) {
|
||||
this.inner = inner;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object get(Object instance) {
|
||||
if (this.fieldValue == NIL) {
|
||||
this.fieldValue = this.inner.get(instance);
|
||||
}
|
||||
|
||||
return this.fieldValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void set(Object instance, Object value) {
|
||||
this.inner.set(instance, value);
|
||||
this.fieldValue = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Field getField() {
|
||||
return this.inner.getField();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.github.joelgodofwar.mmh.common.reflect.accessors;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
/**
|
||||
* Represents an interface for invoking a method.
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
public interface MethodAccessor {
|
||||
|
||||
/**
|
||||
* Invoke the underlying method.
|
||||
*
|
||||
* @param target - the target instance, or NULL for a static method.
|
||||
* @param args - the arguments to pass to the method.
|
||||
* @return The return value, or NULL for void methods.
|
||||
*/
|
||||
Object invoke(Object target, Object... args);
|
||||
|
||||
/**
|
||||
* Retrieve the underlying method.
|
||||
*
|
||||
* @return The method.
|
||||
*/
|
||||
Method getMethod();
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
package com.github.joelgodofwar.mmh.common.reflect.accessors;
|
||||
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.MethodHandles.Lookup;
|
||||
import java.lang.invoke.MethodType;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
|
||||
final class MethodHandleHelper {
|
||||
|
||||
private static final Lookup LOOKUP;
|
||||
|
||||
// static fields, converted as "public Object get()" and "public void set(Object value)"
|
||||
private static final MethodType STATIC_FIELD_GETTER = MethodType.methodType(Object.class);
|
||||
private static final MethodType STATIC_FIELD_SETTER = MethodType.methodType(void.class, Object.class);
|
||||
// instance fields, converted as "public Object get(Object instance)" and "public void set(Object instance, Object value)"
|
||||
private static final MethodType VIRTUAL_FIELD_GETTER = MethodType.methodType(Object.class, Object.class);
|
||||
private static final MethodType VIRTUAL_FIELD_SETTER = MethodType.methodType(void.class, Object.class, Object.class);
|
||||
|
||||
static {
|
||||
Lookup lookup;
|
||||
try {
|
||||
// get the unsafe class
|
||||
Class<?> unsafeClass = Class.forName("sun.misc.Unsafe");
|
||||
// get the unsafe instance
|
||||
Field theUnsafe = unsafeClass.getDeclaredField("theUnsafe");
|
||||
theUnsafe.setAccessible(true);
|
||||
sun.misc.Unsafe unsafe = (sun.misc.Unsafe) theUnsafe.get(null);
|
||||
// get the trusted lookup field
|
||||
Field trustedLookup = Lookup.class.getDeclaredField("IMPL_LOOKUP");
|
||||
// get access to the base and offset value of it
|
||||
long offset = unsafe.staticFieldOffset(trustedLookup);
|
||||
Object baseValue = unsafe.staticFieldBase(trustedLookup);
|
||||
// get the trusted lookup instance
|
||||
lookup = (Lookup) unsafe.getObject(baseValue, offset);
|
||||
} catch (Exception exception) {
|
||||
//PluginLogger.log(Level.SEVERE, "Unable to retrieve trusted lookup", exception);
|
||||
lookup = MethodHandles.lookup();
|
||||
}
|
||||
|
||||
LOOKUP = lookup;
|
||||
}
|
||||
|
||||
// sealed class
|
||||
private MethodHandleHelper() {
|
||||
}
|
||||
|
||||
public static MethodAccessor getMethodAccessor(Method method) {
|
||||
Preconditions.checkNotNull(method, "method");
|
||||
|
||||
try {
|
||||
MethodHandle unreflected = LOOKUP.unreflect(method);
|
||||
boolean staticMethod = Modifier.isStatic(method.getModifiers());
|
||||
|
||||
MethodHandle generified = convertToGeneric(unreflected, staticMethod, false);
|
||||
return new DefaultMethodAccessor(method, generified, staticMethod);
|
||||
} catch (IllegalAccessException ex) {
|
||||
throw new IllegalStateException("Unable to access method " + method, ex);
|
||||
}
|
||||
}
|
||||
|
||||
public static ConstructorAccessor getConstructorAccessor(Constructor<?> constructor) {
|
||||
Preconditions.checkNotNull(constructor, "constructor");
|
||||
|
||||
try {
|
||||
MethodHandle unreflected = LOOKUP.unreflectConstructor(constructor);
|
||||
MethodHandle generified = convertToGeneric(unreflected, false, true);
|
||||
|
||||
return new DefaultConstrutorAccessor(constructor, generified);
|
||||
} catch (IllegalAccessException ex) {
|
||||
throw new IllegalStateException("Unable to access constructor " + constructor, ex);
|
||||
}
|
||||
}
|
||||
|
||||
public static FieldAccessor getFieldAccessor(Field field) {
|
||||
Preconditions.checkNotNull(field, "field");
|
||||
|
||||
try {
|
||||
boolean staticField = Modifier.isStatic(field.getModifiers());
|
||||
|
||||
// java hates us - unreflecting a trusted field always results in an exception, finding them doesn't...
|
||||
MethodHandle getter;
|
||||
MethodHandle setter;
|
||||
if (staticField) {
|
||||
getter = LOOKUP.findStaticGetter(field.getDeclaringClass(), field.getName(), field.getType());
|
||||
setter = LOOKUP.findStaticSetter(field.getDeclaringClass(), field.getName(), field.getType());
|
||||
} else {
|
||||
getter = LOOKUP.findGetter(field.getDeclaringClass(), field.getName(), field.getType());
|
||||
setter = LOOKUP.findSetter(field.getDeclaringClass(), field.getName(), field.getType());
|
||||
}
|
||||
|
||||
// generify the method type so that we don't need to worry about it when using the handles
|
||||
if (staticField) {
|
||||
getter = getter.asType(STATIC_FIELD_GETTER);
|
||||
setter = setter.asType(STATIC_FIELD_SETTER);
|
||||
} else {
|
||||
getter = getter.asType(VIRTUAL_FIELD_GETTER);
|
||||
setter = setter.asType(VIRTUAL_FIELD_SETTER);
|
||||
}
|
||||
|
||||
if (getter == null) {
|
||||
throw new IllegalStateException("Unable to access field " + field + ". Could not find getter");
|
||||
}
|
||||
|
||||
if (setter == null) {
|
||||
throw new IllegalStateException("Unable to access field " + field + ". Could not find setter");
|
||||
}
|
||||
|
||||
return new DefaultFieldAccessor(field, setter, getter, staticField);
|
||||
} catch (IllegalAccessException | NoSuchFieldException ex) {
|
||||
// NoSuchFieldException can never happen, the field always exists
|
||||
throw new IllegalStateException("Unable to access field " + field, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static MethodHandle convertToGeneric(MethodHandle handle, boolean staticMethod, boolean ctor) {
|
||||
MethodHandle target = handle.asFixedArity();
|
||||
// special thing - we do not need the trailing array if we have 0 arguments anyway
|
||||
int paramCount = handle.type().parameterCount() - (ctor || staticMethod ? 0 : 1);
|
||||
MethodType methodType = MethodType.genericMethodType(ctor ? 0 : 1, true);
|
||||
// spread the arguments we give into the handle
|
||||
target = target.asSpreader(Object[].class, paramCount);
|
||||
// adds a leading 'this' argument which we can ignore
|
||||
if (staticMethod) {
|
||||
target = MethodHandles.dropArguments(target, 0, Object.class);
|
||||
}
|
||||
// convert the type to finish
|
||||
return target.asType(methodType);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.github.joelgodofwar.mmh.common.reflect.fuzzy;
|
||||
|
||||
/**
|
||||
* Represents a matcher for fields, methods, constructors and classes.
|
||||
* <p>
|
||||
* This class should ideally never expose mutable state. Its round number must be immutable.
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface AbstractFuzzyMatcher<T> {
|
||||
|
||||
/**
|
||||
* Determine if the given value is a match.
|
||||
*
|
||||
* @param value - the value to match.
|
||||
* @param parent - the parent container, or NULL if this value is the root.
|
||||
* @return TRUE if it is a match, FALSE otherwise.
|
||||
*/
|
||||
boolean isMatch(T value, Object parent);
|
||||
|
||||
/**
|
||||
* Create a fuzzy matcher that returns the opposite result of the current matcher.
|
||||
*
|
||||
* @return An inverted fuzzy matcher.
|
||||
*/
|
||||
default AbstractFuzzyMatcher<T> inverted() {
|
||||
return (value, parent) -> !this.isMatch(value, parent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Require that this and the given matcher be TRUE.
|
||||
*
|
||||
* @param other - the other fuzzy matcher.
|
||||
* @return A combined fuzzy matcher.
|
||||
*/
|
||||
default AbstractFuzzyMatcher<T> and(final AbstractFuzzyMatcher<T> other) {
|
||||
// They both have to be true
|
||||
return (value, parent) -> this.isMatch(value, parent) && other.isMatch(value, parent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Require that either this or the other given matcher be TRUE.
|
||||
*
|
||||
* @param other - the other fuzzy matcher.
|
||||
* @return A combined fuzzy matcher.
|
||||
*/
|
||||
default AbstractFuzzyMatcher<T> or(final AbstractFuzzyMatcher<T> other) {
|
||||
// Either can be true
|
||||
return (value, parent) -> this.isMatch(value, parent) || other.isMatch(value, parent);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
package com.github.joelgodofwar.mmh.common.reflect.fuzzy;
|
||||
|
||||
import java.lang.reflect.Member;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
import com.google.common.collect.Maps;
|
||||
|
||||
/**
|
||||
* Represents a matcher that matches members.
|
||||
*
|
||||
* @param <T> - type that it matches.
|
||||
* @author Kristian
|
||||
*/
|
||||
public abstract class AbstractFuzzyMember<T extends Member> implements AbstractFuzzyMatcher<T> {
|
||||
|
||||
// Accessibility matchers
|
||||
protected int modifiersRequired;
|
||||
protected int modifiersBanned;
|
||||
|
||||
protected Pattern nameRegex;
|
||||
protected AbstractFuzzyMatcher<Class<?>> declaringMatcher = ClassTypeMatcher.MATCH_ALL;
|
||||
|
||||
/**
|
||||
* Whether this contract can be modified.
|
||||
*/
|
||||
protected transient boolean sealed;
|
||||
|
||||
protected AbstractFuzzyMember() {
|
||||
// Only allow construction through the builder
|
||||
}
|
||||
|
||||
// Clone a given contract
|
||||
protected AbstractFuzzyMember(AbstractFuzzyMember<T> other) {
|
||||
this.modifiersRequired = other.modifiersRequired;
|
||||
this.modifiersBanned = other.modifiersBanned;
|
||||
this.nameRegex = other.nameRegex;
|
||||
this.declaringMatcher = other.declaringMatcher;
|
||||
this.sealed = true;
|
||||
}
|
||||
|
||||
private static String getBitView(int value, int bits) {
|
||||
if ((bits < 0) || (bits > 31)) {
|
||||
throw new IllegalArgumentException("Bits must be a value between 0 and 32");
|
||||
}
|
||||
|
||||
// Extract our needed bits
|
||||
int snipped = value & ((1 << bits) - 1);
|
||||
return Integer.toBinaryString(snipped);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called before a builder is building a member and copying its state.
|
||||
* <p>
|
||||
* Use this to prepare any special values.
|
||||
*/
|
||||
protected void prepareBuild() {
|
||||
// No need to prepare anything
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a bit field of every {@link java.lang.reflect.Modifier Modifier} that is required for the member to
|
||||
* match.
|
||||
*
|
||||
* @return A required modifier bit field.
|
||||
*/
|
||||
public int getModifiersRequired() {
|
||||
return this.modifiersRequired;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a bit field of every {@link java.lang.reflect.Modifier Modifier} that must not be present for the member
|
||||
* to match.
|
||||
*
|
||||
* @return A banned modifier bit field.
|
||||
*/
|
||||
public int getModifiersBanned() {
|
||||
return this.modifiersBanned;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the regular expression pattern that is used to match the name of a member.
|
||||
*
|
||||
* @return The regex matching a name, or NULL if everything matches.
|
||||
*/
|
||||
public Pattern getNameRegex() {
|
||||
return this.nameRegex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a class matcher for the declaring class of the member.
|
||||
*
|
||||
* @return An object matching the declaring class.
|
||||
*/
|
||||
public AbstractFuzzyMatcher<Class<?>> getDeclaringMatcher() {
|
||||
return this.declaringMatcher;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMatch(T value, Object parent) {
|
||||
int mods = value.getModifiers();
|
||||
|
||||
// Match accessibility and name
|
||||
return ((mods & this.modifiersRequired) == this.modifiersRequired)
|
||||
&& ((mods & this.modifiersBanned) == 0)
|
||||
&& this.declaringMatcher.isMatch(value.getDeclaringClass(), value)
|
||||
&& this.isNameMatch(value.getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a given name matches the current member matcher.
|
||||
*
|
||||
* @param name - the name to match.
|
||||
* @return TRUE if the name matches, FALSE otherwise.
|
||||
*/
|
||||
private boolean isNameMatch(String name) {
|
||||
if (this.nameRegex == null) {
|
||||
return true;
|
||||
} else {
|
||||
return this.nameRegex.matcher(name).matches();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.getKeyValueView().toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a view of this matcher as a key-value map.
|
||||
* <p>
|
||||
* Used by {@link #toString()} to print a representation of this object.
|
||||
*
|
||||
* @return A modifiable key-value view.
|
||||
*/
|
||||
protected Map<String, Object> getKeyValueView() {
|
||||
Map<String, Object> map = Maps.newLinkedHashMap();
|
||||
|
||||
// Build our representation
|
||||
if ((this.modifiersRequired != Integer.MAX_VALUE) || (this.modifiersBanned != 0)) {
|
||||
map.put("modifiers", String.format(
|
||||
"[required: %s, banned: %s]",
|
||||
getBitView(this.modifiersRequired, 16),
|
||||
getBitView(this.modifiersBanned, 16)));
|
||||
}
|
||||
|
||||
if (this.nameRegex != null) {
|
||||
map.put("name", this.nameRegex.pattern());
|
||||
}
|
||||
|
||||
if (this.declaringMatcher != ClassTypeMatcher.MATCH_ALL) {
|
||||
map.put("declaring", this.declaringMatcher);
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
} else if (obj instanceof AbstractFuzzyMember) {
|
||||
AbstractFuzzyMember<?> other = (AbstractFuzzyMember<?>) obj;
|
||||
return (this.modifiersBanned == other.modifiersBanned)
|
||||
&& (this.modifiersRequired == other.modifiersRequired)
|
||||
&& FuzzyMatchers.checkPattern(this.nameRegex, other.nameRegex)
|
||||
&& Objects.equals(this.declaringMatcher, other.declaringMatcher);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(
|
||||
this.modifiersBanned,
|
||||
this.modifiersRequired,
|
||||
this.nameRegex != null ? this.nameRegex.pattern() : null,
|
||||
this.declaringMatcher);
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a builder of a fuzzy member contract.
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
public static abstract class Builder<T extends AbstractFuzzyMember<?>> {
|
||||
|
||||
protected T member = this.initialMember();
|
||||
|
||||
/**
|
||||
* Add a given bit-field of required modifiers for every matching member.
|
||||
*
|
||||
* @param modifier - bit-field of modifiers that are required.
|
||||
* @return This builder, for chaining.
|
||||
*/
|
||||
public Builder<T> requireModifier(int modifier) {
|
||||
this.member.modifiersRequired |= modifier;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Require that every matching member is public.
|
||||
*
|
||||
* @return This builder, for chaining.
|
||||
*/
|
||||
public Builder<T> requirePublic() {
|
||||
return this.requireModifier(Modifier.PUBLIC);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a given bit-field of modifers that will skip or ignore members.
|
||||
*
|
||||
* @param modifier - bit-field of modifiers to skip or ignore.
|
||||
* @return This builder, for chaining.
|
||||
*/
|
||||
public Builder<T> banModifier(int modifier) {
|
||||
this.member.modifiersBanned |= modifier;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the regular expresson that matches a members name.
|
||||
*
|
||||
* @param regex - new regular expression of valid names.
|
||||
* @return This builder, for chaining.
|
||||
*/
|
||||
public Builder<T> nameRegex(String regex) {
|
||||
this.member.nameRegex = Pattern.compile(regex);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the regular expression pattern that matches a members name.
|
||||
*
|
||||
* @param pattern - regular expression pattern for a valid name.
|
||||
* @return This builder, for chaining.
|
||||
*/
|
||||
public Builder<T> nameRegex(Pattern pattern) {
|
||||
this.member.nameRegex = pattern;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the exact name of the member we are matching.
|
||||
* <p>
|
||||
* This will overwrite the regular expression rule.
|
||||
*
|
||||
* @param name - exact name.
|
||||
* @return This builder, for chaining.
|
||||
*/
|
||||
public Builder<T> nameExact(String name) {
|
||||
return this.nameRegex(Pattern.quote(name));
|
||||
}
|
||||
|
||||
/**
|
||||
* Require that a member is defined by this exact class.
|
||||
*
|
||||
* @param declaringClass - the declaring class of any matching member.
|
||||
* @return This builder, for chaining.
|
||||
*/
|
||||
public Builder<T> declaringClassExactType(Class<?> declaringClass) {
|
||||
this.member.declaringMatcher = FuzzyMatchers.matchExact(declaringClass);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Require that a member is defined by this exact class, or any super class.
|
||||
*
|
||||
* @param declaringClass - the declaring class.
|
||||
* @return This builder, for chaining.
|
||||
*/
|
||||
public Builder<T> declaringClassSuperOf(Class<?> declaringClass) {
|
||||
this.member.declaringMatcher = FuzzyMatchers.matchSuper(declaringClass);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Require that a member is defined by this exact class, or any super class.
|
||||
*
|
||||
* @param declaringClass - the declaring class.
|
||||
* @return This builder, for chaining.
|
||||
*/
|
||||
public Builder<T> declaringClassDerivedOf(Class<?> declaringClass) {
|
||||
this.member.declaringMatcher = FuzzyMatchers.matchDerived(declaringClass);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Require that a member is defined by a class that matches the given matcher.
|
||||
*
|
||||
* @param classMatcher - class matcher.
|
||||
* @return This builder, for chaining.
|
||||
*/
|
||||
public Builder<T> declaringClassMatching(AbstractFuzzyMatcher<Class<?>> classMatcher) {
|
||||
this.member.declaringMatcher = classMatcher;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a new instance of the current type.
|
||||
*
|
||||
* @return New instance.
|
||||
*/
|
||||
@Nonnull
|
||||
protected abstract T initialMember();
|
||||
|
||||
/**
|
||||
* Build a new instance of this type.
|
||||
* <p>
|
||||
* Builders should call {@link AbstractFuzzyMember#prepareBuild()} when constructing new objects.
|
||||
*
|
||||
* @return New instance of this type.
|
||||
*/
|
||||
public abstract T build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.github.joelgodofwar.mmh.common.reflect.fuzzy;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Determine if a class matches based on its name using a regular expression.
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
final class ClassRegexMatcher implements AbstractFuzzyMatcher<Class<?>> {
|
||||
|
||||
private final Pattern regex;
|
||||
|
||||
public ClassRegexMatcher(Pattern regex) {
|
||||
this.regex = regex;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMatch(Class<?> value, Object parent) {
|
||||
if ((value != null) && (this.regex != null)) {
|
||||
return this.regex.matcher(value.getCanonicalName()).matches();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "{ type matches \"" + this.regex.pattern() + "\" }";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.github.joelgodofwar.mmh.common.reflect.fuzzy;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Represents a class matcher that checks for equality using a given set of classes.
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
final class ClassSetMatcher implements AbstractFuzzyMatcher<Class<?>> {
|
||||
|
||||
private final Set<Class<?>> classes;
|
||||
|
||||
public ClassSetMatcher(Set<Class<?>> classes) {
|
||||
this.classes = classes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMatch(Class<?> value, Object parent) {
|
||||
return this.classes.contains(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "{ type any of " + this.classes + " }";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package com.github.joelgodofwar.mmh.common.reflect.fuzzy;
|
||||
|
||||
/**
|
||||
* Used to check class equality.
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
final class ClassTypeMatcher implements AbstractFuzzyMatcher<Class<?>> {
|
||||
|
||||
/**
|
||||
* Match any class.
|
||||
*/
|
||||
public static final ClassTypeMatcher MATCH_ALL = new ClassTypeMatcher(null, MatchVariant.MATCH_SUPER);
|
||||
|
||||
private final Class<?> matcher;
|
||||
private final MatchVariant variant;
|
||||
|
||||
/**
|
||||
* Constructs a new class matcher.
|
||||
*
|
||||
* @param matcher - the matching class, or NULL to represent anything.
|
||||
* @param variant - options specifying the matching rules.
|
||||
*/
|
||||
ClassTypeMatcher(Class<?> matcher, MatchVariant variant) {
|
||||
this.matcher = matcher;
|
||||
this.variant = variant;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a given class is equivalent.
|
||||
* <p>
|
||||
* If the matcher is NULL, the result will only be TRUE if we're not matching exactly.
|
||||
*
|
||||
* @param input - the input class defined in the source file.
|
||||
* @param parent - the container that holds a reference to this class.
|
||||
* @return TRUE if input matches according to the rules in {@link #getMatchVariant()}, FALSE otherwise.
|
||||
*/
|
||||
@Override
|
||||
public boolean isMatch(Class<?> input, Object parent) {
|
||||
if (input == null) {
|
||||
throw new IllegalArgumentException("Input class cannot be NULL.");
|
||||
}
|
||||
|
||||
// if no type to check against is given just ensure that we're not strict checking
|
||||
if (this.matcher == null) {
|
||||
return this.variant != MatchVariant.MATCH_EXACT;
|
||||
}
|
||||
|
||||
switch (this.variant) {
|
||||
case MATCH_EXACT:
|
||||
return this.matcher.equals(input);
|
||||
case MATCH_DERIVED:
|
||||
return this.matcher.isAssignableFrom(input);
|
||||
case MATCH_SUPER:
|
||||
return input.isAssignableFrom(this.matcher);
|
||||
default:
|
||||
// unknown option?
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the class we're comparing against.
|
||||
*
|
||||
* @return Class to compare against.
|
||||
*/
|
||||
public Class<?> getMatcher() {
|
||||
return this.matcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* The matching rules for this class matcher.
|
||||
*
|
||||
* @return The current matching option.
|
||||
*/
|
||||
public MatchVariant getMatchVariant() {
|
||||
return this.variant;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
switch (this.variant) {
|
||||
case MATCH_EXACT:
|
||||
return "{ type exactly " + this.matcher + " }";
|
||||
case MATCH_SUPER:
|
||||
return "{ type " + this.matcher + " instanceof input }";
|
||||
case MATCH_DERIVED:
|
||||
return "{ type input instanceof " + this.matcher + " }";
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown match variant " + this.variant);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Different matching rules.
|
||||
*/
|
||||
enum MatchVariant {
|
||||
|
||||
/**
|
||||
* Match classes exactly.
|
||||
*/
|
||||
MATCH_EXACT,
|
||||
|
||||
/**
|
||||
* A match if the input class is a superclass of the matcher class, or the same class.
|
||||
*/
|
||||
MATCH_SUPER,
|
||||
|
||||
/**
|
||||
* A match if the input class is a derived class of the matcher class, or the same class.
|
||||
*/
|
||||
MATCH_DERIVED
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
package com.github.joelgodofwar.mmh.common.reflect.fuzzy;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import com.github.joelgodofwar.mmh.common.reflect.FuzzyReflection;
|
||||
import com.github.joelgodofwar.mmh.common.reflect.MethodInfo;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
|
||||
/**
|
||||
* Determine if a given class implements a given fuzzy (duck typed) contract.
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
public class FuzzyClassContract implements AbstractFuzzyMatcher<Class<?>> {
|
||||
|
||||
private final ImmutableList<AbstractFuzzyMatcher<Field>> fieldContracts;
|
||||
private final ImmutableList<AbstractFuzzyMatcher<MethodInfo>> methodContracts;
|
||||
private final ImmutableList<AbstractFuzzyMatcher<MethodInfo>> constructorContracts;
|
||||
|
||||
private final ImmutableList<AbstractFuzzyMatcher<Class<?>>> baseclassContracts;
|
||||
private final ImmutableList<AbstractFuzzyMatcher<Class<?>>> interfaceContracts;
|
||||
|
||||
/**
|
||||
* Constructs a new fuzzy class contract with the given contracts.
|
||||
*
|
||||
* @param builder - the builder that is constructing us.
|
||||
*/
|
||||
private FuzzyClassContract(Builder builder) {
|
||||
this.fieldContracts = ImmutableList.copyOf(builder.fieldContracts);
|
||||
this.methodContracts = ImmutableList.copyOf(builder.methodContracts);
|
||||
this.constructorContracts = ImmutableList.copyOf(builder.constructorContracts);
|
||||
this.baseclassContracts = ImmutableList.copyOf(builder.baseclassContracts);
|
||||
this.interfaceContracts = ImmutableList.copyOf(builder.interfaceContracts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a new fuzzy class contract builder.
|
||||
*
|
||||
* @return A new builder.
|
||||
*/
|
||||
public static Builder newBuilder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve an immutable list of every field contract.
|
||||
* <p>
|
||||
* This list is ordered in descending order of priority.
|
||||
*
|
||||
* @return List of every field contract.
|
||||
*/
|
||||
public ImmutableList<AbstractFuzzyMatcher<Field>> getFieldContracts() {
|
||||
return this.fieldContracts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve an immutable list of every method contract.
|
||||
* <p>
|
||||
* This list is ordered in descending order of priority.
|
||||
*
|
||||
* @return List of every method contract.
|
||||
*/
|
||||
public ImmutableList<AbstractFuzzyMatcher<MethodInfo>> getMethodContracts() {
|
||||
return this.methodContracts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve an immutable list of every constructor contract.
|
||||
* <p>
|
||||
* This list is ordered in descending order of priority.
|
||||
*
|
||||
* @return List of every constructor contract.
|
||||
*/
|
||||
public ImmutableList<AbstractFuzzyMatcher<MethodInfo>> getConstructorContracts() {
|
||||
return this.constructorContracts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve an immutable list of every baseclass contract.
|
||||
* <p>
|
||||
* This list is ordered in descending order of priority.
|
||||
*
|
||||
* @return List of every baseclass contract.
|
||||
*/
|
||||
public ImmutableList<AbstractFuzzyMatcher<Class<?>>> getBaseclassContracts() {
|
||||
return this.baseclassContracts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve an immutable list of every interface contract.
|
||||
* <p>
|
||||
* This list is ordered in descending order of priority.
|
||||
*
|
||||
* @return List of every interface contract.
|
||||
*/
|
||||
public ImmutableList<AbstractFuzzyMatcher<Class<?>>> getInterfaceContracts() {
|
||||
return this.interfaceContracts;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMatch(Class<?> value, Object parent) {
|
||||
FuzzyReflection reflection = FuzzyReflection.fromClass(value, true);
|
||||
|
||||
// Make sure all the contracts are valid
|
||||
return this.processValue(value.getSuperclass(), parent, this.baseclassContracts)
|
||||
&& this.processContracts(Arrays.asList(value.getInterfaces()), parent, this.interfaceContracts)
|
||||
&& this.processContracts(reflection.getFields(), value, this.fieldContracts)
|
||||
&& this.processContracts(MethodInfo.fromMethods(reflection.getMethods()), value, this.methodContracts)
|
||||
&& this.processContracts(MethodInfo.fromConstructors(value.getDeclaredConstructors()), value,
|
||||
this.constructorContracts);
|
||||
}
|
||||
|
||||
private <T> boolean processContracts(Collection<T> values, Object parent, List<AbstractFuzzyMatcher<T>> matchers) {
|
||||
// they all match if no values are present
|
||||
if (values.isEmpty() || matchers.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// check if all contracts match the given objects
|
||||
int acceptingMatchers = 0;
|
||||
for (T value : values) {
|
||||
if (this.processValue(value, parent, matchers)) {
|
||||
acceptingMatchers++;
|
||||
// if all matchers found one match we're done
|
||||
if (acceptingMatchers == matchers.size()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private <T> boolean processValue(T value, Object parent, List<AbstractFuzzyMatcher<T>> matchers) {
|
||||
// check if all given contracts match the given value
|
||||
for (AbstractFuzzyMatcher<T> matcher : matchers) {
|
||||
if (!matcher.isMatch(value, parent)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// they all match
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder builder = new StringBuilder("FuzzyClassContract={\n");
|
||||
|
||||
// append all subcontracts
|
||||
if (!this.fieldContracts.isEmpty()) {
|
||||
builder.append(" fields=").append(this.fieldContracts).append("\n");
|
||||
}
|
||||
|
||||
if (!this.methodContracts.isEmpty()) {
|
||||
builder.append(" methods=").append(this.methodContracts).append("\n");
|
||||
}
|
||||
|
||||
if (!this.constructorContracts.isEmpty()) {
|
||||
builder.append(" constructors=").append(this.constructorContracts).append("\n");
|
||||
}
|
||||
|
||||
if (!this.baseclassContracts.isEmpty()) {
|
||||
builder.append(" baseClasses=").append(this.baseclassContracts).append("\n");
|
||||
}
|
||||
|
||||
if (!this.interfaceContracts.isEmpty()) {
|
||||
builder.append(" interfaceClasses=").append(this.interfaceContracts).append("\n");
|
||||
}
|
||||
|
||||
// finish off
|
||||
return builder.append("}").toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a class contract builder.
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
public static final class Builder {
|
||||
|
||||
private final List<AbstractFuzzyMatcher<Field>> fieldContracts = new ArrayList<>();
|
||||
private final List<AbstractFuzzyMatcher<MethodInfo>> methodContracts = new ArrayList<>();
|
||||
private final List<AbstractFuzzyMatcher<MethodInfo>> constructorContracts = new ArrayList<>();
|
||||
|
||||
private final List<AbstractFuzzyMatcher<Class<?>>> baseclassContracts = new ArrayList<>();
|
||||
private final List<AbstractFuzzyMatcher<Class<?>>> interfaceContracts = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Add a new field contract.
|
||||
*
|
||||
* @param matcher - new field contract.
|
||||
* @return This builder, for chaining.
|
||||
*/
|
||||
public Builder field(AbstractFuzzyMatcher<Field> matcher) {
|
||||
this.fieldContracts.add(matcher);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new field contract via a builder.
|
||||
*
|
||||
* @param builder - builder for the new field contract.
|
||||
* @return This builder, for chaining.
|
||||
*/
|
||||
public Builder field(FuzzyFieldContract.Builder builder) {
|
||||
return this.field(builder.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new method contract.
|
||||
*
|
||||
* @param matcher - new method contract.
|
||||
* @return This builder, for chaining.
|
||||
*/
|
||||
public Builder method(AbstractFuzzyMatcher<MethodInfo> matcher) {
|
||||
this.methodContracts.add(matcher);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new method contract via a builder.
|
||||
*
|
||||
* @param builder - builder for the new method contract.
|
||||
* @return This builder, for chaining.
|
||||
*/
|
||||
public Builder method(FuzzyMethodContract.Builder builder) {
|
||||
return this.method(builder.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new constructor contract.
|
||||
*
|
||||
* @param matcher - new constructor contract.
|
||||
* @return This builder, for chaining.
|
||||
*/
|
||||
public Builder constructor(AbstractFuzzyMatcher<MethodInfo> matcher) {
|
||||
this.constructorContracts.add(matcher);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new constructor contract via a builder.
|
||||
*
|
||||
* @param builder - builder for the new constructor contract.
|
||||
* @return This builder, for chaining.
|
||||
*/
|
||||
public Builder constructor(FuzzyMethodContract.Builder builder) {
|
||||
return this.constructor(builder.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new base class contract.
|
||||
*
|
||||
* @param matcher - new base class contract.
|
||||
* @return This builder, for chaining.
|
||||
*/
|
||||
public Builder baseclass(AbstractFuzzyMatcher<Class<?>> matcher) {
|
||||
this.baseclassContracts.add(matcher);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new base class contract.
|
||||
*
|
||||
* @param builder - builder for the new base class contract.
|
||||
* @return This builder, for chaining.
|
||||
*/
|
||||
public Builder baseclass(FuzzyClassContract.Builder builder) {
|
||||
return this.baseclass(builder.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new interface contract.
|
||||
*
|
||||
* @param matcher - new interface contract.
|
||||
* @return This builder, for chaining.
|
||||
*/
|
||||
public Builder interfaces(AbstractFuzzyMatcher<Class<?>> matcher) {
|
||||
this.interfaceContracts.add(matcher);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new interface contract.
|
||||
*
|
||||
* @param builder - builder for the new interface contract.
|
||||
* @return This builder, for chaining.
|
||||
*/
|
||||
public Builder interfaces(FuzzyClassContract.Builder builder) {
|
||||
return this.interfaces(builder.build());
|
||||
}
|
||||
|
||||
public FuzzyClassContract build() {
|
||||
// Construct a new class matcher
|
||||
return new FuzzyClassContract(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
package com.github.joelgodofwar.mmh.common.reflect.fuzzy;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
/**
|
||||
* Represents a field matcher.
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
public class FuzzyFieldContract extends AbstractFuzzyMember<Field> {
|
||||
|
||||
private AbstractFuzzyMatcher<Class<?>> typeMatcher = ClassTypeMatcher.MATCH_ALL;
|
||||
|
||||
private FuzzyFieldContract() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new field contract from the given contract.
|
||||
*
|
||||
* @param other - the contract to clone.
|
||||
*/
|
||||
private FuzzyFieldContract(FuzzyFieldContract other) {
|
||||
super(other);
|
||||
this.typeMatcher = other.typeMatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* Match a field by its type.
|
||||
*
|
||||
* @param matcher - the type to match.
|
||||
* @return The field contract.
|
||||
*/
|
||||
public static FuzzyFieldContract matchType(AbstractFuzzyMatcher<Class<?>> matcher) {
|
||||
return newBuilder().typeMatches(matcher).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new fuzzy field contract builder.
|
||||
*
|
||||
* @return New fuzzy field contract builder.
|
||||
*/
|
||||
public static Builder newBuilder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the class matcher that matches the type of a field.
|
||||
*
|
||||
* @return The class matcher.
|
||||
*/
|
||||
public AbstractFuzzyMatcher<Class<?>> getTypeMatcher() {
|
||||
return this.typeMatcher;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMatch(Field value, Object parent) {
|
||||
if (super.isMatch(value, parent)) {
|
||||
return this.typeMatcher.isMatch(value.getType(), value);
|
||||
}
|
||||
|
||||
// No match
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<String, Object> getKeyValueView() {
|
||||
Map<String, Object> member = super.getKeyValueView();
|
||||
if (this.typeMatcher != ClassTypeMatcher.MATCH_ALL) {
|
||||
member.put("type", this.typeMatcher);
|
||||
}
|
||||
|
||||
return member;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a builder for a field matcher.
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
public static class Builder extends AbstractFuzzyMember.Builder<FuzzyFieldContract> {
|
||||
|
||||
@Override
|
||||
public Builder requireModifier(int modifier) {
|
||||
super.requireModifier(modifier);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder banModifier(int modifier) {
|
||||
super.banModifier(modifier);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder requirePublic() {
|
||||
super.requirePublic();
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder nameRegex(String regex) {
|
||||
super.nameRegex(regex);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder nameRegex(Pattern pattern) {
|
||||
super.nameRegex(pattern);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder nameExact(String name) {
|
||||
super.nameExact(name);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override public Builder declaringClassExactType(Class<?> declaringClass) {
|
||||
super.declaringClassExactType(declaringClass);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder declaringClassSuperOf(Class<?> declaringClass) {
|
||||
super.declaringClassSuperOf(declaringClass);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder declaringClassDerivedOf(Class<?> declaringClass) {
|
||||
super.declaringClassDerivedOf(declaringClass);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder declaringClassMatching(AbstractFuzzyMatcher<Class<?>> classMatcher) {
|
||||
super.declaringClassMatching(classMatcher);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nonnull
|
||||
protected FuzzyFieldContract initialMember() {
|
||||
return new FuzzyFieldContract();
|
||||
}
|
||||
|
||||
public Builder typeExact(Class<?> type) {
|
||||
this.member.typeMatcher = FuzzyMatchers.matchExact(type);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder typeSuperOf(Class<?> type) {
|
||||
this.member.typeMatcher = FuzzyMatchers.matchSuper(type);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder typeDerivedOf(Class<?> type) {
|
||||
this.member.typeMatcher = FuzzyMatchers.matchDerived(type);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder typeMatches(AbstractFuzzyMatcher<Class<?>> matcher) {
|
||||
this.member.typeMatcher = matcher;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FuzzyFieldContract build() {
|
||||
this.member.prepareBuild();
|
||||
return new FuzzyFieldContract(this.member);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package com.github.joelgodofwar.mmh.common.reflect.fuzzy;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import com.github.joelgodofwar.mmh.common.reflect.fuzzy.ClassTypeMatcher.MatchVariant;
|
||||
import com.google.common.collect.Sets;
|
||||
|
||||
/**
|
||||
* Contains factory methods for matching classes.
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
public class FuzzyMatchers {
|
||||
|
||||
// Constant matchers
|
||||
private static final AbstractFuzzyMatcher<Class<?>> MATCH_ALL = (value, parent) -> true;
|
||||
|
||||
private FuzzyMatchers() {
|
||||
// Don't make this constructable
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a class matcher that matches an array with a given component matcher.
|
||||
*
|
||||
* @param componentMatcher - the component matcher.
|
||||
* @return A new array matcher.
|
||||
*/
|
||||
public static AbstractFuzzyMatcher<Class<?>> matchArray(AbstractFuzzyMatcher<Class<?>> componentMatcher) {
|
||||
return (value, parent) -> value.isArray() && componentMatcher.isMatch(value.getComponentType(), parent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a fuzzy matcher that will match any class.
|
||||
*
|
||||
* @return A class matcher.
|
||||
*/
|
||||
public static AbstractFuzzyMatcher<Class<?>> matchAll() {
|
||||
return MATCH_ALL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a class matcher that matches types exactly.
|
||||
*
|
||||
* @param matcher - the matching class.
|
||||
* @return A new class matcher.
|
||||
*/
|
||||
public static AbstractFuzzyMatcher<Class<?>> matchExact(Class<?> matcher) {
|
||||
return new ClassTypeMatcher(matcher, MatchVariant.MATCH_EXACT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a class matcher that matches any of the given classes exactly.
|
||||
*
|
||||
* @param classes - list of classes to match.
|
||||
* @return A new class matcher.
|
||||
*/
|
||||
public static AbstractFuzzyMatcher<Class<?>> matchAnyOf(Class<?>... classes) {
|
||||
return matchAnyOf(Sets.newHashSet(classes));
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a class matcher that matches any of the given classes exactly.
|
||||
*
|
||||
* @param classes - set of classes to match.
|
||||
* @return A new class matcher.
|
||||
*/
|
||||
public static AbstractFuzzyMatcher<Class<?>> matchAnyOf(Set<Class<?>> classes) {
|
||||
return new ClassSetMatcher(classes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a class matcher that matches super types of the given class.
|
||||
*
|
||||
* @param matcher - the matching type must be a super class of this type.
|
||||
* @return A new class matcher.
|
||||
*/
|
||||
public static AbstractFuzzyMatcher<Class<?>> matchSuper(Class<?> matcher) {
|
||||
return new ClassTypeMatcher(matcher, MatchVariant.MATCH_SUPER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a class matcher that matches derived types of the given class.
|
||||
*
|
||||
* @param matcher - the matching type must be a derived class of this type.
|
||||
* @return A new class matcher.
|
||||
*/
|
||||
public static AbstractFuzzyMatcher<Class<?>> matchDerived(Class<?> matcher) {
|
||||
return new ClassTypeMatcher(matcher, MatchVariant.MATCH_DERIVED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a class matcher based on the canonical names of classes.
|
||||
*
|
||||
* @param regex - regular expression pattern matching class names.
|
||||
* @return A fuzzy class matcher based on name.
|
||||
*/
|
||||
public static AbstractFuzzyMatcher<Class<?>> matchRegex(final Pattern regex) {
|
||||
return new ClassRegexMatcher(regex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a class matcher based on the canonical names of classes.
|
||||
*
|
||||
* @param regex - regular expression matching class names.
|
||||
* @return A fuzzy class matcher based on name.
|
||||
*/
|
||||
public static AbstractFuzzyMatcher<Class<?>> matchRegex(String regex) {
|
||||
return FuzzyMatchers.matchRegex(Pattern.compile(regex));
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if two patterns are the same.
|
||||
* <p>
|
||||
* Note that two patterns may be functionally the same, but nevertheless be different.
|
||||
*
|
||||
* @param a - the first pattern.
|
||||
* @param b - the second pattern.
|
||||
* @return TRUE if they are compiled from the same pattern, FALSE otherwise.
|
||||
*/
|
||||
static boolean checkPattern(Pattern a, Pattern b) {
|
||||
if (a == null) {
|
||||
return b == null;
|
||||
} else if (b == null) {
|
||||
return false;
|
||||
} else if (a == b) {
|
||||
return true;
|
||||
} else {
|
||||
return a.pattern().equals(b.pattern());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,564 @@
|
||||
package com.github.joelgodofwar.mmh.common.reflect.fuzzy;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
import com.github.joelgodofwar.mmh.common.reflect.MethodInfo;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
|
||||
/**
|
||||
* Represents a contract for matching methods or constructors.
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
public class FuzzyMethodContract extends AbstractFuzzyMember<MethodInfo> {
|
||||
|
||||
// Match return value
|
||||
private AbstractFuzzyMatcher<Class<?>> returnMatcher = ClassTypeMatcher.MATCH_ALL;
|
||||
// Handle parameters and exceptions
|
||||
private List<ParameterClassMatcher> paramMatchers;
|
||||
private List<ParameterClassMatcher> exceptionMatchers;
|
||||
// Expected parameter count
|
||||
private Integer paramCount;
|
||||
|
||||
private FuzzyMethodContract() {
|
||||
// Only allow construction from the builder
|
||||
this.paramMatchers = new ArrayList<>();
|
||||
this.exceptionMatchers = new ArrayList<>();
|
||||
}
|
||||
|
||||
private FuzzyMethodContract(FuzzyMethodContract other) {
|
||||
super(other);
|
||||
this.returnMatcher = other.returnMatcher;
|
||||
this.paramMatchers = other.paramMatchers;
|
||||
this.exceptionMatchers = other.exceptionMatchers;
|
||||
this.paramCount = other.paramCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a method contract builder.
|
||||
*
|
||||
* @return Method contract builder.
|
||||
*/
|
||||
public static Builder newBuilder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a new immutable copy of the given method contract.
|
||||
*
|
||||
* @param other - the contract to clone.
|
||||
* @return A immutable copy of the given contract.
|
||||
*/
|
||||
private static FuzzyMethodContract immutableCopy(FuzzyMethodContract other) {
|
||||
FuzzyMethodContract copy = new FuzzyMethodContract(other);
|
||||
|
||||
// Ensure that the lists are immutable
|
||||
copy.paramMatchers = ImmutableList.copyOf(copy.paramMatchers);
|
||||
copy.exceptionMatchers = ImmutableList.copyOf(copy.exceptionMatchers);
|
||||
return copy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the class matcher for the return type.
|
||||
*
|
||||
* @return Class matcher for the return type.
|
||||
*/
|
||||
public AbstractFuzzyMatcher<Class<?>> getReturnMatcher() {
|
||||
return this.returnMatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve an immutable list of every parameter matcher for this method.
|
||||
*
|
||||
* @return Immutable list of every parameter matcher.
|
||||
*/
|
||||
public ImmutableList<ParameterClassMatcher> getParamMatchers() {
|
||||
if (this.paramMatchers instanceof ImmutableList) {
|
||||
return (ImmutableList<ParameterClassMatcher>) this.paramMatchers;
|
||||
} else {
|
||||
throw new IllegalStateException("Lists haven't been sealed yet.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve an immutable list of every exception matcher for this method.
|
||||
*
|
||||
* @return Immutable list of every exception matcher.
|
||||
*/
|
||||
public List<ParameterClassMatcher> getExceptionMatchers() {
|
||||
if (this.exceptionMatchers instanceof ImmutableList) {
|
||||
return this.exceptionMatchers;
|
||||
} else {
|
||||
throw new IllegalStateException("Lists haven't been sealed yet.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the expected parameter count for this method.
|
||||
*
|
||||
* @return Expected parameter count, or NULL if anyting goes.
|
||||
*/
|
||||
public Integer getParamCount() {
|
||||
return this.paramCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMatch(MethodInfo value, Object parent) {
|
||||
if (super.isMatch(value, parent)) {
|
||||
// check the return type first (the easiest check)
|
||||
if (!this.returnMatcher.isMatch(value.getReturnType(), value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// check for the parameter types
|
||||
Class<?>[] params = value.getParameterTypes();
|
||||
if ((this.paramCount != null) && (this.paramCount != params.length)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// check parameters and exceptions
|
||||
return this.matchTypes(params, value, this.paramMatchers)
|
||||
&& this.matchTypes(value.getExceptionTypes(), value, this.exceptionMatchers);
|
||||
}
|
||||
|
||||
// No match
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean matchTypes(Class<?>[] types, MethodInfo parent, List<ParameterClassMatcher> matchers) {
|
||||
if (matchers.isEmpty()) {
|
||||
// no matchers - no show
|
||||
return true;
|
||||
}
|
||||
|
||||
// the amount of matchers which are ok with the parameter types
|
||||
int acceptingMatchers = 0;
|
||||
for (int i = 0; i < types.length; i++) {
|
||||
if (this.processValue(types[i], parent, i, matchers)) {
|
||||
acceptingMatchers++;
|
||||
// if all matchers accepted one type we are done
|
||||
if (acceptingMatchers == matchers.size()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean processValue(Class<?> value, MethodInfo parent, int index, List<ParameterClassMatcher> matchers) {
|
||||
// The order matters
|
||||
for (ParameterClassMatcher matcher : matchers) {
|
||||
// See if we got jackpot
|
||||
if (matcher.isParameterMatch(value, parent, index)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Failure
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<String, Object> getKeyValueView() {
|
||||
Map<String, Object> member = super.getKeyValueView();
|
||||
|
||||
// Only add fields that are actual constraints
|
||||
if (this.returnMatcher != ClassTypeMatcher.MATCH_ALL) {
|
||||
member.put("return", this.returnMatcher);
|
||||
}
|
||||
|
||||
if (!this.paramMatchers.isEmpty()) {
|
||||
member.put("params", this.paramMatchers);
|
||||
}
|
||||
|
||||
if (!this.exceptionMatchers.isEmpty()) {
|
||||
member.put("exceptions", this.exceptionMatchers);
|
||||
}
|
||||
|
||||
if (this.paramCount != null) {
|
||||
member.put("paramCount", this.paramCount);
|
||||
}
|
||||
|
||||
return member;
|
||||
}
|
||||
|
||||
private static final class ParameterClassMatcher implements AbstractFuzzyMatcher<Class<?>[]> {
|
||||
|
||||
/**
|
||||
* The expected index.
|
||||
*/
|
||||
private final AbstractFuzzyMatcher<Class<?>> typeMatcher;
|
||||
private final Integer indexMatch;
|
||||
|
||||
/**
|
||||
* Construct a new parameter class matcher.
|
||||
*
|
||||
* @param typeMatcher - class type matcher.
|
||||
*/
|
||||
public ParameterClassMatcher(@Nonnull AbstractFuzzyMatcher<Class<?>> typeMatcher) {
|
||||
this(typeMatcher, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a new parameter class matcher.
|
||||
*
|
||||
* @param typeMatcher - class type matcher.
|
||||
* @param indexMatch - parameter index to match, or NULL for anything.
|
||||
*/
|
||||
public ParameterClassMatcher(@Nonnull AbstractFuzzyMatcher<Class<?>> typeMatcher, Integer indexMatch) {
|
||||
this.typeMatcher = typeMatcher;
|
||||
this.indexMatch = indexMatch;
|
||||
}
|
||||
|
||||
/**
|
||||
* See if there's a match for this matcher.
|
||||
*
|
||||
* @param param - the type to match.
|
||||
* @param parent - the container (member) that holds a reference to this parameter.
|
||||
* @param index - the index of the current parameter.
|
||||
* @return TRUE if this matcher matches any of the given parameters, FALSE otherwise.
|
||||
*/
|
||||
public boolean isParameterMatch(Class<?> param, MethodInfo parent, int index) {
|
||||
// Make sure the index is valid (or NULL)
|
||||
if ((this.indexMatch == null) || (this.indexMatch == index)) {
|
||||
return this.typeMatcher.isMatch(param, parent);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMatch(Class<?>[] value, Object parent) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("{ Parameter Type: %s, Index: %s }", this.typeMatcher, this.indexMatch);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a builder for a fuzzy method contract.
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
public static final class Builder extends AbstractFuzzyMember.Builder<FuzzyMethodContract> {
|
||||
|
||||
@Override
|
||||
public Builder requireModifier(int modifier) {
|
||||
super.requireModifier(modifier);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder requirePublic() {
|
||||
super.requirePublic();
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder banModifier(int modifier) {
|
||||
super.banModifier(modifier);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder nameRegex(String regex) {
|
||||
super.nameRegex(regex);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder nameRegex(Pattern pattern) {
|
||||
super.nameRegex(pattern);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder nameExact(String name) {
|
||||
super.nameExact(name);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder declaringClassExactType(Class<?> declaringClass) {
|
||||
super.declaringClassExactType(declaringClass);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder declaringClassSuperOf(Class<?> declaringClass) {
|
||||
super.declaringClassSuperOf(declaringClass);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder declaringClassDerivedOf(Class<?> declaringClass) {
|
||||
super.declaringClassDerivedOf(declaringClass);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder declaringClassMatching(AbstractFuzzyMatcher<Class<?>> classMatcher) {
|
||||
super.declaringClassMatching(classMatcher);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new required parameter by type for any matching method.
|
||||
*
|
||||
* @param type - the exact type this parameter must match.
|
||||
* @return This builder, for chaining.
|
||||
*/
|
||||
public Builder parameterExactType(Class<?> type) {
|
||||
this.member.paramMatchers.add(new ParameterClassMatcher(FuzzyMatchers.matchExact(type)));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new required parameter whose type must be a superclass of the given type.
|
||||
* <p>
|
||||
* If a method parameter is of type Number, then any derived class here (Integer, Long, etc.) will match it.
|
||||
*
|
||||
* @param type - a type or less derived type of the matching parameter.
|
||||
* @return This builder, for chaining.
|
||||
*/
|
||||
public Builder parameterSuperOf(Class<?> type) {
|
||||
this.member.paramMatchers.add(new ParameterClassMatcher(FuzzyMatchers.matchSuper(type)));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new required parameter whose type must be a derived class of the given class.
|
||||
* <p>
|
||||
* If the method parameter has the type Integer, then the class Number here will match it.
|
||||
*
|
||||
* @param type - a type or more derived type of the matching parameter.
|
||||
* @return This builder, for chaining.
|
||||
*/
|
||||
public Builder parameterDerivedOf(Class<?> type) {
|
||||
this.member.paramMatchers.add(new ParameterClassMatcher(FuzzyMatchers.matchDerived(type)));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new required parameter whose type must match the given class matcher.
|
||||
*
|
||||
* @param classMatcher - the class matcher.
|
||||
* @return This builder, for chaining.
|
||||
*/
|
||||
public Builder parameterMatches(AbstractFuzzyMatcher<Class<?>> classMatcher) {
|
||||
this.member.paramMatchers.add(new ParameterClassMatcher(classMatcher));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new required parameter by type and position for any matching method.
|
||||
*
|
||||
* @param type - the exact type this parameter must match.
|
||||
* @param index - the expected position in the parameter list.
|
||||
* @return This builder, for chaining.
|
||||
*/
|
||||
public Builder parameterExactType(Class<?> type, int index) {
|
||||
this.member.paramMatchers.add(new ParameterClassMatcher(FuzzyMatchers.matchExact(type), index));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new required parameters by type and order for any matching method.
|
||||
*
|
||||
* @param types - the types of every parameters in order.
|
||||
* @return This builder, for chaining.
|
||||
*/
|
||||
public Builder parameterExactArray(Class<?>... types) {
|
||||
this.parameterCount(types.length);
|
||||
for (int i = 0; i < types.length; i++) {
|
||||
this.parameterExactType(types[i], i);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new required parameter whose type must be a superclass of the given type.
|
||||
* <p>
|
||||
* If a parameter is of type Number, any derived class (Integer, Long, etc.) will match it.
|
||||
*
|
||||
* @param type - a type or derived type of the matching parameter.
|
||||
* @param index - the expected position in the parameter list.
|
||||
* @return This builder, for chaining.
|
||||
*/
|
||||
public Builder parameterSuperOf(Class<?> type, int index) {
|
||||
this.member.paramMatchers.add(new ParameterClassMatcher(FuzzyMatchers.matchSuper(type), index));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new required parameter whose type must be a derived class of the given class.
|
||||
* <p>
|
||||
* If the method parameter has the type Integer, then the class Number here will match it.
|
||||
*
|
||||
* @param type - a type or more derived type of the matching parameter.
|
||||
* @param index - the expected position in the parameter list.
|
||||
* @return This builder, for chaining.
|
||||
*/
|
||||
public Builder parameterDerivedOf(Class<?> type, int index) {
|
||||
this.member.paramMatchers.add(new ParameterClassMatcher(FuzzyMatchers.matchDerived(type), index));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new required parameter whose type must match the given class matcher and index.
|
||||
*
|
||||
* @param classMatcher - the class matcher.
|
||||
* @param index - the expected position in the parameter list.
|
||||
* @return This builder, for chaining.
|
||||
*/
|
||||
public Builder parameterMatches(AbstractFuzzyMatcher<Class<?>> classMatcher, int index) {
|
||||
this.member.paramMatchers.add(new ParameterClassMatcher(classMatcher, index));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the expected number of parameters in the matching method.
|
||||
*
|
||||
* @param expectedCount - the number of parameters to expect.
|
||||
* @return This builder, for chaining.
|
||||
*/
|
||||
public Builder parameterCount(int expectedCount) {
|
||||
this.member.paramCount = expectedCount;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Require a void method.
|
||||
*
|
||||
* @return This builder, for chaining.
|
||||
*/
|
||||
public Builder returnTypeVoid() {
|
||||
return this.returnTypeExact(Void.TYPE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the return type of a matching method exactly.
|
||||
*
|
||||
* @param type - the exact return type.
|
||||
* @return This builder, for chaining.
|
||||
*/
|
||||
public Builder returnTypeExact(Class<?> type) {
|
||||
this.member.returnMatcher = FuzzyMatchers.matchExact(type);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the expected super class of the return type for every matching method.
|
||||
*
|
||||
* @param type - the return type, or a super class of it.
|
||||
* @return This builder, for chaining.
|
||||
*/
|
||||
public Builder returnDerivedOf(Class<?> type) {
|
||||
this.member.returnMatcher = FuzzyMatchers.matchDerived(type);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a matcher that must match the return type of a matching method.
|
||||
*
|
||||
* @param classMatcher - the exact return type.
|
||||
* @return This builder, for chaining.
|
||||
*/
|
||||
public Builder returnTypeMatches(AbstractFuzzyMatcher<Class<?>> classMatcher) {
|
||||
this.member.returnMatcher = classMatcher;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a throwable exception that must match the given type exactly.
|
||||
*
|
||||
* @param type - exception type.
|
||||
* @return This builder, for chaining.
|
||||
*/
|
||||
public Builder exceptionExactType(Class<?> type) {
|
||||
this.member.exceptionMatchers.add(new ParameterClassMatcher(FuzzyMatchers.matchExact(type)));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a throwable exception that must match the given type or be derived.
|
||||
*
|
||||
* @param type - exception type.
|
||||
* @return This builder, for chaining.
|
||||
*/
|
||||
public Builder exceptionSuperOf(Class<?> type) {
|
||||
this.member.exceptionMatchers.add(new ParameterClassMatcher(FuzzyMatchers.matchSuper(type)));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a throwable exception that must match the given matcher,
|
||||
*
|
||||
* @param classMatcher - the class matcher that must match.
|
||||
* @return This builder, for chaining.
|
||||
*/
|
||||
public Builder exceptionMatches(AbstractFuzzyMatcher<Class<?>> classMatcher) {
|
||||
this.member.exceptionMatchers.add(new ParameterClassMatcher(classMatcher));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a throwable exception that must match the given type exactly and index.
|
||||
*
|
||||
* @param type - exception type.
|
||||
* @param index - the position in the throwable list.
|
||||
* @return This builder, for chaining.
|
||||
*/
|
||||
public Builder exceptionExactType(Class<?> type, int index) {
|
||||
this.member.exceptionMatchers.add(new ParameterClassMatcher(FuzzyMatchers.matchExact(type), index));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a throwable exception that must match the given type or be derived and index.
|
||||
*
|
||||
* @param type - exception type.
|
||||
* @param index - the position in the throwable list.
|
||||
* @return This builder, for chaining.
|
||||
*/
|
||||
public Builder exceptionSuperOf(Class<?> type, int index) {
|
||||
this.member.exceptionMatchers.add(new ParameterClassMatcher(FuzzyMatchers.matchSuper(type), index));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a throwable exception that must match the given matcher and index.
|
||||
*
|
||||
* @param classMatcher - the class matcher that must match.
|
||||
* @param index - the position in the throwable list.
|
||||
* @return This builder, for chaining.
|
||||
*/
|
||||
public Builder exceptionMatches(AbstractFuzzyMatcher<Class<?>> classMatcher, int index) {
|
||||
this.member.exceptionMatchers.add(new ParameterClassMatcher(classMatcher, index));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nonnull
|
||||
protected FuzzyMethodContract initialMember() {
|
||||
// With mutable lists
|
||||
return new FuzzyMethodContract();
|
||||
}
|
||||
|
||||
@Override
|
||||
public FuzzyMethodContract build() {
|
||||
this.member.prepareBuild();
|
||||
return immutableCopy(this.member);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,72 @@
|
||||
package com.github.joelgodofwar.mmh.util;
|
||||
|
||||
//import org.bukkit.ChatColor;
|
||||
|
||||
public class Ansi {
|
||||
public static final String RESET = "\u001B[0m";
|
||||
|
||||
public static final String BOLD = "\u001B[1m";
|
||||
public static final String NORMAL = "\u001B[2m";
|
||||
|
||||
public static final String ITALIC = "\u001B[3m";
|
||||
public static final String UNDERLINE = "\u001B[4m";
|
||||
public static final String BLINK = "\u001B[5m";
|
||||
public static final String RAPID_BLINK = "\u001B[6m";
|
||||
|
||||
public static final String BLACK = "\u001B[30m";
|
||||
public static final String RED = "\u001B[31m";
|
||||
public static final String GREEN = "\u001B[32m";
|
||||
public static final String YELLOW = "\u001B[33m";
|
||||
public static final String BLUE = "\u001B[34m";
|
||||
public static final String MAGENTA = "\u001B[35m";
|
||||
public static final String CYAN = "\u001B[36m";
|
||||
public static final String WHITE = "\u001B[37m";
|
||||
|
||||
|
||||
public static final String LIGHT_BLACK = "\u001B[90m";
|
||||
public static final String LIGHT_RED = "\u001B[91m";
|
||||
public static final String LIGHT_GREEN = "\u001B[92m";
|
||||
public static final String LIGHT_YELLOW = "\u001B[93m";
|
||||
public static final String LIGHT_BLUE = "\u001B[94m";
|
||||
public static final String LIGHT_MAGENTA = "\u001B[95m";
|
||||
public static final String LIGHT_CYAN = "\u001B[96m";
|
||||
public static final String LIGHT_WHITE = "\u001B[97m";
|
||||
|
||||
public static String stripAnsi(String string) {
|
||||
string = string.replace("" + Ansi.BLACK , "").replace("" + Ansi.BLINK , "").replace("" + Ansi.BLUE , "").replace("" + Ansi.BOLD , "")
|
||||
.replace("" + Ansi.CYAN , "").replace("" + Ansi.GREEN , "").replace("" + Ansi.ITALIC , "").replace("" + Ansi.LIGHT_BLACK , "")
|
||||
.replace("" + Ansi.LIGHT_BLUE , "").replace("" + Ansi.LIGHT_CYAN , "").replace("" + Ansi.LIGHT_GREEN , "").replace("" + Ansi.LIGHT_MAGENTA , "")
|
||||
.replace("" + Ansi.LIGHT_RED , "").replace("" + Ansi.LIGHT_WHITE , "").replace("" + Ansi.LIGHT_YELLOW , "").replace("" + Ansi.MAGENTA , "")
|
||||
.replace("" + Ansi.NORMAL , "").replace("" + Ansi.RAPID_BLINK , "").replace("" + Ansi.RED , "").replace("" + Ansi.RESET , "").replace("" + Ansi.UNDERLINE , "")
|
||||
.replace("" + Ansi.WHITE , "").replace("" + Ansi.YELLOW , "");
|
||||
return string;
|
||||
}
|
||||
|
||||
|
||||
/**public static final ChatColor AQUA = ChatColor.AQUA;
|
||||
public static final ChatColor BLACK = ChatColor.BLACK;
|
||||
public static final ChatColor BLUE = ChatColor.BLUE;
|
||||
public static final ChatColor DARK_AQUA = ChatColor.DARK_AQUA;
|
||||
public static final ChatColor DARK_BLUE = ChatColor.DARK_BLUE;
|
||||
public static final ChatColor DARK_GRAY = ChatColor.DARK_GRAY;
|
||||
public static final ChatColor DARK_GREEN = ChatColor.DARK_GREEN;
|
||||
public static final ChatColor DARK_PURPLE = ChatColor.DARK_PURPLE;
|
||||
public static final ChatColor DARK_RED = ChatColor.DARK_RED;
|
||||
public static final ChatColor GOLD = ChatColor.GOLD;
|
||||
public static final ChatColor GRAY = ChatColor.GRAY;
|
||||
public static final ChatColor GREEN = ChatColor.GREEN;
|
||||
public static final ChatColor MAGENTA = ChatColor.LIGHT_PURPLE;
|
||||
public static final ChatColor RED = ChatColor.RED;
|
||||
public static final ChatColor WHITE = ChatColor.WHITE;
|
||||
public static final ChatColor YELLOW = ChatColor.YELLOW;
|
||||
|
||||
public static final ChatColor BOLD = ChatColor.BOLD;
|
||||
public static final ChatColor ITALIC = ChatColor.ITALIC;
|
||||
public static final ChatColor MAGIC = ChatColor.MAGIC;
|
||||
public static final ChatColor RESET = ChatColor.RESET;
|
||||
public static final ChatColor STRIKETHROUGH = ChatColor.STRIKETHROUGH;
|
||||
public static final ChatColor UNDERLINE = ChatColor.UNDERLINE;//*/
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package com.github.joelgodofwar.mmh.util;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.bukkit.ChatColor;
|
||||
|
||||
public class ChatColorUtils
|
||||
{
|
||||
public static String setNametoRGB(String s){
|
||||
String prefix = "§x";
|
||||
s = s.replace("<BLACK>", prefix + "000000").replace("<DARK_BLUE>", prefix + "0000AA")
|
||||
.replace("<DARK_GREEN>", prefix + "00AA00").replace("<DARK_AQUA>", prefix + "00AAAA")
|
||||
.replace("<DARK_RED>", prefix + "AA0000").replace("<DARK_PURPLE>", prefix + "AA00AA")
|
||||
.replace("<GOLD>", prefix + "FFAA00").replace("<GRAY>", prefix + "AAAAAA")
|
||||
.replace("<DARK_GRAY>", prefix + "555555").replace("<BLUE>", prefix + "5555FF")
|
||||
.replace("<GREEN>", prefix + "55FF55").replace("<AQUA>", prefix + "55FFFF")
|
||||
.replace("<RED>", prefix + "FF5555").replace("<LIGHT_PURPLE>", prefix + "FF55FF")
|
||||
.replace("<YELLOW>", prefix + "FFFF55").replace("<WHITE>", prefix + "FFFFFF");
|
||||
return s;
|
||||
}
|
||||
|
||||
public static String setColorsByCode(String s)
|
||||
{
|
||||
s = s.replace("&0", "" + ChatColor.BLACK).replace("&1", "" + ChatColor.DARK_BLUE)
|
||||
.replace("&2", "" + ChatColor.DARK_GREEN).replace("&3", "" + ChatColor.DARK_AQUA)
|
||||
.replace("&4", "" + ChatColor.DARK_RED).replace("&5", "" + ChatColor.DARK_PURPLE)
|
||||
.replace("&6", "" + ChatColor.GOLD).replace("&7", "" + ChatColor.GRAY)
|
||||
.replace("&8", "" + ChatColor.DARK_GRAY).replace("&9", "" + ChatColor.BLUE)
|
||||
.replace("&a", "" + ChatColor.GREEN).replace("&b", "" + ChatColor.AQUA)
|
||||
.replace("&c", "" + ChatColor.RED).replace("&d", "" + ChatColor.LIGHT_PURPLE)
|
||||
.replace("&e", "" + ChatColor.YELLOW).replace("&f", "" + ChatColor.WHITE)
|
||||
.replace("&k", "" + ChatColor.MAGIC).replace("&l", "" + ChatColor.BOLD)
|
||||
.replace("&m", "" + ChatColor.STRIKETHROUGH).replace("&n", "" + ChatColor.UNDERLINE)
|
||||
.replace("&o", "" + ChatColor.ITALIC).replace("&r", "" + ChatColor.RESET)
|
||||
.replace("&A", "" + ChatColor.GREEN).replace("&B", "" + ChatColor.AQUA)
|
||||
.replace("&C", "" + ChatColor.RED).replace("&D", "" + ChatColor.LIGHT_PURPLE)
|
||||
.replace("&E", "" + ChatColor.YELLOW).replace("&F", "" + ChatColor.WHITE)
|
||||
.replace("&K", "" + ChatColor.MAGIC).replace("&L", "" + ChatColor.BOLD)
|
||||
.replace("&M", "" + ChatColor.STRIKETHROUGH).replace("&N", "" + ChatColor.UNDERLINE)
|
||||
.replace("&O", "" + ChatColor.ITALIC).replace("&R", "" + ChatColor.RESET);
|
||||
return s;
|
||||
}
|
||||
public static String setColorsByName(String s){
|
||||
s = s.replace("<BLACK>", "" + ChatColor.BLACK).replace("<DARK_BLUE>", "" + ChatColor.DARK_BLUE)
|
||||
.replace("<DARK_GREEN>", "" + ChatColor.DARK_GREEN).replace("<DARK_AQUA>", "" + ChatColor.DARK_AQUA)
|
||||
.replace("<DARK_RED>", "" + ChatColor.DARK_RED).replace("<DARK_PURPLE>", "" + ChatColor.DARK_PURPLE)
|
||||
.replace("<GOLD>", "" + ChatColor.GOLD).replace("<GRAY>", "" + ChatColor.GRAY)
|
||||
.replace("<DARK_GRAY>", "" + ChatColor.DARK_GRAY).replace("<BLUE>", "" + ChatColor.BLUE)
|
||||
.replace("<GREEN>", "" + ChatColor.GREEN).replace("<AQUA>", "" + ChatColor.AQUA)
|
||||
.replace("<RED>", "" + ChatColor.RED).replace("<LIGHT_PURPLE>", "" + ChatColor.LIGHT_PURPLE)
|
||||
.replace("<YELLOW>", "" + ChatColor.YELLOW).replace("<WHITE>", "" + ChatColor.WHITE)
|
||||
.replace("<MAGIC>", "" + ChatColor.MAGIC).replace("<BOLD>", "" + ChatColor.BOLD)
|
||||
.replace("<STRIKETHROUGH>", "" + ChatColor.STRIKETHROUGH).replace("<UNDERLINE>", "" + ChatColor.UNDERLINE)
|
||||
.replace("<ITALIC>", "" + ChatColor.ITALIC).replace("<RESET>", "" + ChatColor.RESET);
|
||||
return s;
|
||||
}
|
||||
public static String setColors(String string) {
|
||||
// Replace color placeholders while ignoring case
|
||||
Pattern pattern = Pattern.compile("<(\\w+)>", Pattern.CASE_INSENSITIVE);
|
||||
Matcher matcher = pattern.matcher(string);
|
||||
while (matcher.find()) {
|
||||
String colorPlaceholder = matcher.group(1);
|
||||
String color = setColors2("<" + colorPlaceholder.toUpperCase() + ">");
|
||||
if (color != null) {
|
||||
string = string.replaceFirst(Pattern.quote(matcher.group()), color);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply color formatting
|
||||
string = ChatColor.translateAlternateColorCodes('&', string);
|
||||
|
||||
return string;
|
||||
}
|
||||
public static String setColors2(String string) {
|
||||
string = string.replace("<BLACK>", "&0" ) .replace("<DARK_BLUE>", "&1" )
|
||||
.replace("<DARK_GREEN>", "&2" ) .replace("<DARK_AQUA>", "&3" )
|
||||
.replace("<DARK_RED>", "&4" ) .replace("<DARK_PURPLE>", "&5" )
|
||||
.replace("<GOLD>", "&6" ) .replace("<GRAY>", "&7" )
|
||||
.replace("<DARK_GRAY>", "&8" ) .replace("<BLUE>", "&9" )
|
||||
.replace("<GREEN>", "&a" ) .replace("<AQUA>", "&b" )
|
||||
.replace("<RED>", "&c" ) .replace("<LIGHT_PURPLE>", "&d" )
|
||||
.replace("<YELLOW>", "&e" ) .replace("<WHITE>", "&f" )
|
||||
.replace("<MAGIC>", "&k" ) .replace("<BOLD>", "&l" )
|
||||
.replace("<STRIKETHROUGH>", "&m" ) .replace("<UNDERLINE>", "&n" )
|
||||
.replace("<ITALIC>", "&o" ) .replace("<RESET>", "&r" );
|
||||
return string;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.github.joelgodofwar.mmh.util;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.configuration.file.FileConfiguration;
|
||||
|
||||
import com.github.joelgodofwar.mmh.MoreMobHeads;
|
||||
|
||||
public class ConfigAPI {
|
||||
|
||||
public final static Logger logger = Logger.getLogger("Minecraft");
|
||||
|
||||
public void CheckForConfig(MoreMobHeads plugin){
|
||||
try{
|
||||
if(!plugin.getDataFolder().exists()){
|
||||
log(": Data Folder doesn't exist");
|
||||
log(": Creating Data Folder");
|
||||
plugin.getDataFolder().mkdirs();
|
||||
log(": Data Folder Created at " + plugin.getDataFolder());
|
||||
}
|
||||
File file = new File(plugin.getDataFolder(), "config.yml");
|
||||
plugin.getLogger().info("" + file);
|
||||
if(!file.exists()){
|
||||
log("config.yml not found, creating!");
|
||||
plugin.saveDefaultConfig();
|
||||
FileConfiguration config = plugin.getConfig();
|
||||
|
||||
config.options().copyDefaults(true);
|
||||
plugin.saveConfig();
|
||||
}
|
||||
}catch(Exception e){
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public static void Reloadconfig(MoreMobHeads plugin){
|
||||
// Load config.
|
||||
FileConfiguration config = plugin.getConfig();
|
||||
String daString = config.getString("debug").replace("'", "") + ",";
|
||||
|
||||
if(daString.contains("true")){
|
||||
plugin.debug = true;
|
||||
}else{
|
||||
plugin.debug = false;
|
||||
}
|
||||
String daString2 = config.getString("auto_update_check").replace("'", "") + ",";
|
||||
if(daString2.contains("true")){
|
||||
plugin.UpdateCheck = true;
|
||||
}else{
|
||||
plugin.UpdateCheck = false;
|
||||
}
|
||||
|
||||
if(plugin.debug){log("UpdateCheck = " + plugin.UpdateCheck);} //TODO: Logger
|
||||
}
|
||||
public static void log(String dalog){
|
||||
Bukkit.getLogger().info(dalog);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.github.joelgodofwar.mmh.util;
|
||||
|
||||
public class ConfigHelper {
|
||||
|
||||
public static Double Double(YmlConfiguration config, String get, Double def) {
|
||||
String str = config.getString(get);
|
||||
if (str == null || str.isEmpty()) {
|
||||
if(def != null) {
|
||||
return def;
|
||||
}else {
|
||||
return 0.00;
|
||||
}
|
||||
}
|
||||
if(str.indexOf('\'') >= 0) {
|
||||
str = removeQuotes(str);
|
||||
return Double.parseDouble(str);
|
||||
}
|
||||
return Double.parseDouble(str);
|
||||
}
|
||||
|
||||
public static String removeQuotes(String string) {
|
||||
if (string.length() >= 2 && string.charAt(0) == '"' && string.charAt(string.length() - 1) == '"'){
|
||||
string = string.substring(1, string.length() - 1);
|
||||
}
|
||||
if (string.length() >= 2 && string.charAt(0) == '\'' && string.charAt(string.length() - 1) == '\''){
|
||||
string = string.substring(1, string.length() - 1);
|
||||
}
|
||||
return string;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.github.joelgodofwar.mmh.util;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.event.entity.EntityDamageByEntityEvent;
|
||||
import org.bukkit.event.entity.EntityDamageEvent.DamageCause;
|
||||
import org.bukkit.inventory.EquipmentSlot;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.PlayerInventory;
|
||||
|
||||
/**
|
||||
* Utility for resolving the {@link EquipmentSlot} containing a player's weapon for an
|
||||
* {@link EntityDamageByEntityEvent}. This is used since that event type doesn't provide a
|
||||
* way for finding out the equipment slot.
|
||||
*/
|
||||
public class EquipmentSlotResolver {
|
||||
|
||||
public static Optional<ItemStack> resolveDamagingWeapon(PlayerInventory playerInventory, DamageCause damageCause) {
|
||||
//System.out.println("damageCause=" + damageCause.toString());
|
||||
switch (damageCause) {
|
||||
case ENTITY_ATTACK:
|
||||
// Check if the player is holding any item in the main hand
|
||||
ItemStack mainHandItem = playerInventory.getItemInMainHand();
|
||||
//System.out.println("mainHandItem=" + mainHandItem.getType().toString());
|
||||
if (!mainHandItem.getType().equals(Material.AIR)) {
|
||||
return Optional.of(mainHandItem);
|
||||
}
|
||||
break;
|
||||
case PROJECTILE:
|
||||
// Ranged damage
|
||||
Optional<ItemStack> bowItem = getWeaponItem(playerInventory, Material.BOW);
|
||||
if (bowItem.isPresent()) {
|
||||
return bowItem;
|
||||
}
|
||||
Optional<ItemStack> crossbowItem = getWeaponItem(playerInventory, Material.CROSSBOW);
|
||||
if (crossbowItem.isPresent()) {
|
||||
return crossbowItem;
|
||||
}
|
||||
Optional<ItemStack> tridentItem = getWeaponItem(playerInventory, Material.TRIDENT);
|
||||
if (tridentItem.isPresent()) {
|
||||
return tridentItem;
|
||||
}
|
||||
Optional<ItemStack> snowItem = getWeaponItem(playerInventory, Material.SNOWBALL);
|
||||
if (snowItem.isPresent()) {
|
||||
return snowItem;
|
||||
}
|
||||
Optional<ItemStack> eggItem = getWeaponItem(playerInventory, Material.EGG);
|
||||
if (eggItem.isPresent()) {
|
||||
return eggItem;
|
||||
}
|
||||
break;
|
||||
case MAGIC:
|
||||
// Potion
|
||||
return getWeaponItem(playerInventory, Material.SPLASH_POTION);
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
private static Optional<ItemStack> getWeaponItem(PlayerInventory playerInventory, Material material) {
|
||||
ItemStack mainHandItem = playerInventory.getItemInMainHand();
|
||||
ItemStack offHandItem = playerInventory.getItemInOffHand();
|
||||
|
||||
if (mainHandItem.getType() == material) {
|
||||
return Optional.of(mainHandItem);
|
||||
} else if (offHandItem.getType() == material) {
|
||||
return Optional.of(offHandItem);
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,558 @@
|
||||
package com.github.joelgodofwar.mmh.util;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.bukkit.ChatColor;
|
||||
|
||||
//This class is where all formatting methods are stored.
|
||||
@SuppressWarnings("unused")
|
||||
public class Format {
|
||||
//private static MineverseChat plugin = MineverseChat.getInstance();
|
||||
|
||||
public static final int LEGACY_COLOR_CODE_LENGTH = 2;
|
||||
public static final int HEX_COLOR_CODE_LENGTH = 14;
|
||||
public static final String DEFAULT_COLOR_CODE = String.valueOf(ChatColor.WHITE);
|
||||
public static final String HEX_COLOR_CODE_PREFIX = "#";
|
||||
public static final String BUKKIT_COLOR_CODE_PREFIX = "�";
|
||||
public static final String BUKKIT_HEX_COLOR_CODE_PREFIX = "x";
|
||||
|
||||
/**public static String convertToJson(MineverseChatPlayer sender, String format, String chat) {
|
||||
JsonFormat JSONformat = MineverseChat.jfInfo.getJsonFormat(sender.getJsonFormat());
|
||||
String f = format.replace("\\", "\\\\").replace("\"", "\\\"");
|
||||
String c = chat.replace("\\", "\\\\").replace("\"", "\\\"");
|
||||
String json = "[\"\",{\"text\":\"\",\"extra\":[";
|
||||
String prefix = "";
|
||||
String suffix = "";
|
||||
try {
|
||||
prefix = FormatStringAll(MineverseChat.chat.getPlayerPrefix(sender.getPlayer()));
|
||||
suffix = FormatStringAll(MineverseChat.chat.getPlayerSuffix(sender.getPlayer()));
|
||||
if(suffix.equals("")) {
|
||||
suffix = "venturechat_no_suffix_code";
|
||||
}
|
||||
if(prefix.equals("")) {
|
||||
prefix = "venturechat_no_prefix_code";
|
||||
}
|
||||
}
|
||||
catch(Exception e) {
|
||||
System.out.println("Exception?" + e.getLocalizedMessage());
|
||||
if(plugin.getConfig().getString("loglevel", "info").equals("debug")) {
|
||||
Bukkit.getConsoleSender().sendMessage(Format.FormatStringAll("&8[&eVentureChat&8]&e - Prefix and / or suffix don't exist, setting to nothing."));
|
||||
}
|
||||
suffix = "venturechat_no_suffix_code";
|
||||
prefix = "venturechat_no_prefix_code";
|
||||
}
|
||||
String nickname = "";
|
||||
if(sender.getPlayer() != null) {
|
||||
nickname = FormatStringAll(sender.getPlayer().getDisplayName());
|
||||
}
|
||||
json += convertPlaceholders(f, JSONformat, prefix, nickname, suffix, sender);
|
||||
json += "]}";
|
||||
json += "," + convertLinks(c);
|
||||
json += "]";
|
||||
if(plugin.getConfig().getString("loglevel", "info").equals("debug")) {
|
||||
System.out.println(json);
|
||||
System.out.println("END OF JSON");
|
||||
System.out.println("END OF JSON");
|
||||
System.out.println("END OF JSON");
|
||||
System.out.println("END OF JSON");
|
||||
System.out.println("END OF JSON");
|
||||
}
|
||||
return json;
|
||||
}*/
|
||||
|
||||
/**private static String convertPlaceholders(String s, JsonFormat format, String prefix, String nickname, String suffix, MineverseChatPlayer icp) {
|
||||
String remaining = s;
|
||||
String temp = "";
|
||||
int indexStart = -1;
|
||||
int indexEnd = -1;
|
||||
String placeholder = "";
|
||||
String lastCode = "�f";
|
||||
do {
|
||||
Pattern pattern = Pattern.compile("(" + escapeAllRegex(prefix) + "|" + escapeAllRegex(nickname) + "|" + escapeAllRegex(suffix) + ")");
|
||||
Matcher matcher = pattern.matcher(remaining);
|
||||
if(matcher.find()) {
|
||||
indexStart = matcher.start();
|
||||
indexEnd = matcher.end();
|
||||
placeholder = remaining.substring(indexStart, indexEnd);
|
||||
temp += convertToJsonColors(lastCode + remaining.substring(0, indexStart)) + ",";
|
||||
lastCode = getLastCode(lastCode + remaining.substring(0, indexStart));
|
||||
String action = "";
|
||||
String text = "";
|
||||
String hover = "";
|
||||
if(placeholder.contains(prefix)) {
|
||||
action = format.getClickPrefix();
|
||||
text = PlaceholderAPI.setBracketPlaceholders(icp.getPlayer(), format.getClickPrefixText());
|
||||
for(String st : format.getHoverTextPrefix()) {
|
||||
hover += Format.FormatStringAll(st) + "\n";
|
||||
}
|
||||
}
|
||||
if(placeholder.contains(nickname)) {
|
||||
action = format.getClickName();
|
||||
text = PlaceholderAPI.setBracketPlaceholders(icp.getPlayer(), format.getClickNameText());
|
||||
for(String st : format.getHoverTextName()) {
|
||||
hover += Format.FormatStringAll(st) + "\n";
|
||||
}
|
||||
}
|
||||
if(placeholder.contains(suffix)) {
|
||||
action = format.getClickSuffix();
|
||||
text = PlaceholderAPI.setBracketPlaceholders(icp.getPlayer(), format.getClickSuffixText());
|
||||
for(String st : format.getHoverTextSuffix()) {
|
||||
hover += Format.FormatStringAll(st) + "\n";
|
||||
}
|
||||
}
|
||||
hover = PlaceholderAPI.setBracketPlaceholders(icp.getPlayer(), hover.substring(0, hover.length() - 1));
|
||||
temp += convertToJsonColors(lastCode + placeholder, ",\"clickEvent\":{\"action\":\"" + action + "\",\"value\":\"" + text + "\"},\"hoverEvent\":{\"action\":\"show_text\",\"value\":{\"text\":\"\",\"extra\":[" + convertToJsonColors(hover) + "]}}") + ",";
|
||||
lastCode = getLastCode(lastCode + placeholder);
|
||||
remaining = remaining.substring(indexEnd);
|
||||
}
|
||||
else {
|
||||
temp += convertToJsonColors(lastCode + remaining);
|
||||
break;
|
||||
}
|
||||
}
|
||||
while(true);
|
||||
return temp;
|
||||
}*/
|
||||
|
||||
/**private static String convertLinks(String s) {
|
||||
String remaining = s;
|
||||
String temp = "";
|
||||
int indexLink = -1;
|
||||
int indexLinkEnd = -1;
|
||||
String link = "";
|
||||
String lastCode = "�f";
|
||||
do {
|
||||
Pattern pattern = Pattern.compile("([a-zA-Z0-9�\\-:/]+\\.[a-zA-Z/0-9�\\-:_#]+(\\.[a-zA-Z/0-9.�\\-:#\\?\\+=_]+)?)");
|
||||
Matcher matcher = pattern.matcher(remaining);
|
||||
if(matcher.find()) {
|
||||
indexLink = matcher.start();
|
||||
indexLinkEnd = matcher.end();
|
||||
link = remaining.substring(indexLink, indexLinkEnd);
|
||||
temp += convertToJsonColors(lastCode + remaining.substring(0, indexLink)) + ",";
|
||||
lastCode = getLastCode(lastCode + remaining.substring(0, indexLink));
|
||||
String https = "";
|
||||
if(ChatColor.stripColor(link).contains("https://"))
|
||||
https = "s";
|
||||
temp += convertToJsonColors(lastCode + link, ",\"underlined\":\"" + underlineURLs() + "\",\"clickEvent\":{\"action\":\"open_url\",\"value\":\"http" + https + "://" + ChatColor.stripColor(link.replace("http://", "").replace("https://", "")) + "\"},\"hoverEvent\":{\"action\":\"show_text\",\"value\":{\"text\":\"\",\"extra\":[" + convertToJsonColors(lastCode + link) + "]}}") + ",";
|
||||
lastCode = getLastCode(lastCode + link);
|
||||
remaining = remaining.substring(indexLinkEnd);
|
||||
}
|
||||
else {
|
||||
temp += convertToJsonColors(lastCode + remaining);
|
||||
break;
|
||||
}
|
||||
}
|
||||
while(true);
|
||||
return temp;
|
||||
}*/
|
||||
|
||||
public static String getLastCode(String s) {
|
||||
String ts = "";
|
||||
char[] ch = s.toCharArray();
|
||||
for(int a = 0; a < s.length() - 1; a ++) {
|
||||
if(String.valueOf(ch[a + 1]).matches("[lkomnLKOMN]") && ch[a] == '�') {
|
||||
ts += String.valueOf(ch[a]) + ch[a + 1];
|
||||
a ++;
|
||||
}
|
||||
else if(String.valueOf(ch[a + 1]).matches("[0123456789abcdefrABCDEFR]") && ch[a] == '�') {
|
||||
ts = String.valueOf(ch[a]) + ch[a + 1];
|
||||
a ++;
|
||||
}
|
||||
else if(ch[a + 1] == 'x' && ch[a] == '�') {
|
||||
if(ch.length > a + 13) {
|
||||
if(String.valueOf(ch[a + 3]).matches("[0123456789abcdefABCDEF]") && String.valueOf(ch[a + 5]).matches("[0123456789abcdefABCDEF]") && String.valueOf(ch[a + 7]).matches("[0123456789abcdefABCDEF]") && String.valueOf(ch[a + 9]).matches("[0123456789abcdefABCDEF]") && String.valueOf(ch[a + 11]).matches("[0123456789abcdefABCDEF]") && String.valueOf(ch[a + 13]).matches("[0123456789abcdefABCDEF]") && ch[a + 2] == '�' && ch[a + 4] == '�' && ch[a + 6] == '�' && ch[a + 8] == '�' && ch[a + 10] == '�' && ch[a + 12] == '�') {
|
||||
ts = String.valueOf(ch[a]) + ch[a + 1] + ch[a + 2] + ch[a + 3] + ch[a + 4] + ch[a + 5] + ch[a + 6] + ch[a + 7] + ch[a + 8] + ch[a + 9] + ch[a + 10] + ch[a + 11] + ch[a + 12] + ch[a + 13];
|
||||
a += 13;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ts;
|
||||
}
|
||||
|
||||
|
||||
private static String convertToJsonColors(String s) {
|
||||
return convertToJsonColors(s, "");
|
||||
}
|
||||
|
||||
private static String convertToJsonColors(String s, String extensions) {
|
||||
String remaining = s;
|
||||
String temp = "";
|
||||
int indexColor = -1;
|
||||
int indexNextColor = -1;
|
||||
String color = "";
|
||||
String modifier = "";
|
||||
boolean bold = false;
|
||||
boolean obfuscated = false;
|
||||
boolean italic = false;
|
||||
boolean strikethrough = false;
|
||||
boolean underlined = false;
|
||||
String previousColor = "";
|
||||
int colorLength = LEGACY_COLOR_CODE_LENGTH;
|
||||
do {
|
||||
if(remaining.length() < LEGACY_COLOR_CODE_LENGTH) {
|
||||
temp = "{\"text\":\"" + remaining + "\"},";
|
||||
break;
|
||||
}
|
||||
modifier = "";
|
||||
indexColor = remaining.indexOf(BUKKIT_COLOR_CODE_PREFIX);
|
||||
previousColor = color;
|
||||
|
||||
color = remaining.substring(1, indexColor + LEGACY_COLOR_CODE_LENGTH);
|
||||
if(color.equals(BUKKIT_HEX_COLOR_CODE_PREFIX)) {
|
||||
if(remaining.length() >= HEX_COLOR_CODE_LENGTH) {
|
||||
color = HEX_COLOR_CODE_PREFIX + remaining.substring(LEGACY_COLOR_CODE_LENGTH, indexColor + HEX_COLOR_CODE_LENGTH).replace(BUKKIT_COLOR_CODE_PREFIX, "");
|
||||
colorLength = HEX_COLOR_CODE_LENGTH;
|
||||
bold = false;
|
||||
obfuscated = false;
|
||||
italic = false;
|
||||
strikethrough = false;
|
||||
underlined = false;
|
||||
}
|
||||
}
|
||||
else if(!color.matches("[0123456789abcdefABCDEF]")) {
|
||||
switch(color) {
|
||||
case "l":
|
||||
case "L": {
|
||||
bold = true;
|
||||
break;
|
||||
}
|
||||
case "k":
|
||||
case "K": {
|
||||
obfuscated = true;
|
||||
break;
|
||||
}
|
||||
case "o":
|
||||
case "O": {
|
||||
italic = true;
|
||||
break;
|
||||
}
|
||||
case "m":
|
||||
case "M": {
|
||||
strikethrough = true;
|
||||
break;
|
||||
}
|
||||
case "n":
|
||||
case "N": {
|
||||
underlined = true;
|
||||
break;
|
||||
}
|
||||
case "r":
|
||||
case "R": {
|
||||
bold = false;
|
||||
obfuscated = false;
|
||||
italic = false;
|
||||
strikethrough = false;
|
||||
underlined = false;
|
||||
color = "f";
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(!color.equals("f"))
|
||||
color = previousColor;
|
||||
if(color.length() == 0)
|
||||
color = "f";
|
||||
}
|
||||
else {
|
||||
bold = false;
|
||||
obfuscated = false;
|
||||
italic = false;
|
||||
strikethrough = false;
|
||||
underlined = false;
|
||||
}
|
||||
if(bold)
|
||||
modifier += ",\"bold\":\"true\"";
|
||||
if(obfuscated)
|
||||
modifier += ",\"obfuscated\":\"true\"";
|
||||
if(italic)
|
||||
modifier += ",\"italic\":\"true\"";
|
||||
if(underlined)
|
||||
modifier += ",\"underlined\":\"true\"";
|
||||
if(strikethrough)
|
||||
modifier += ",\"strikethrough\":\"true\"";
|
||||
remaining = remaining.substring(colorLength);
|
||||
colorLength = LEGACY_COLOR_CODE_LENGTH;
|
||||
indexNextColor = remaining.indexOf(BUKKIT_COLOR_CODE_PREFIX);
|
||||
if(indexNextColor == -1) {
|
||||
indexNextColor = remaining.length();
|
||||
}
|
||||
temp += "{\"text\":\"" + remaining.substring(0, indexNextColor) + "\",\"color\":\"" + hexidecimalToJsonColorRGB(color) + "\"" + modifier + extensions + "},";
|
||||
remaining = remaining.substring(indexNextColor);
|
||||
}
|
||||
while(remaining.length() > 1 && indexColor != -1);
|
||||
if(temp.length() > 1)
|
||||
temp = temp.substring(0, temp.length() - 1);
|
||||
return temp;
|
||||
}
|
||||
|
||||
private static String hexidecimalToJsonColorRGB(String c) {
|
||||
if(c.length() == 1) {
|
||||
switch(c) {
|
||||
case "0": return "black";
|
||||
case "1": return "dark_blue";
|
||||
case "2": return "dark_green";
|
||||
case "3": return "dark_aqua";
|
||||
case "4": return "dark_red";
|
||||
case "5": return "dark_purple";
|
||||
case "6": return "gold";
|
||||
case "7": return "gray";
|
||||
case "8": return "dark_gray";
|
||||
case "9": return "blue";
|
||||
case "a":
|
||||
case "A": return "green";
|
||||
case "b":
|
||||
case "B": return "aqua";
|
||||
case "c":
|
||||
case "C": return "red";
|
||||
case "d":
|
||||
case "D": return "light_purple";
|
||||
case "e":
|
||||
case "E": return "yellow";
|
||||
case "f":
|
||||
case "F": return "white";
|
||||
default: return "white";
|
||||
}
|
||||
}
|
||||
if(isValidHexColor(c)) {
|
||||
return c;
|
||||
}
|
||||
return "white";
|
||||
}
|
||||
|
||||
/**public static String convertPlainTextToJson(String s, boolean convertURL) {
|
||||
if(convertURL) {
|
||||
return "[" + Format.convertLinks(s) + "]";
|
||||
}
|
||||
else {
|
||||
return "[" + convertToJsonColors("�f" + s) + "]";
|
||||
}
|
||||
}
|
||||
|
||||
/**public static String formatModerationGUI(String json, Player player, String sender, String channelName, int hash) {
|
||||
if(player.hasPermission("venturechat.gui")) {
|
||||
json = json.substring(0, json.length() - 1);
|
||||
json += "," + Format.convertToJsonColors(Format.FormatStringAll(plugin.getConfig().getString("guiicon")), ",\"clickEvent\":{\"action\":\"run_command\",\"value\":\"/vchatgui " + sender + " " + channelName + " " + hash +"\"},\"hoverEvent\":{\"action\":\"show_text\",\"value\":{\"text\":\"\",\"extra\":[" + Format.convertToJsonColors(Format.FormatStringAll(plugin.getConfig().getString("guitext"))) + "]}}") + "]";
|
||||
}
|
||||
return json;
|
||||
}*/
|
||||
|
||||
/**public static PacketContainer createPacketPlayOutChat(String json) {
|
||||
WrappedChatComponent component = WrappedChatComponent.fromJson(json);
|
||||
PacketContainer container = new PacketContainer(PacketType.Play.Server.CHAT);
|
||||
container.getModifier().writeDefaults();
|
||||
container.getChatComponents().write(0, component);
|
||||
return container;
|
||||
}
|
||||
|
||||
public static PacketContainer createPacketPlayOutChat(WrappedChatComponent component) {
|
||||
PacketContainer container = new PacketContainer(PacketType.Play.Server.CHAT);
|
||||
container.getModifier().writeDefaults();
|
||||
container.getChatComponents().write(0, component);
|
||||
return container;
|
||||
}
|
||||
|
||||
public static void sendPacketPlayOutChat(Player player, PacketContainer packet) {
|
||||
try {
|
||||
ProtocolLibrary.getProtocolManager().sendServerPacket(player, packet);
|
||||
}
|
||||
catch(Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public static String toPlainText(Object o, Class<?> c) {
|
||||
List<Object> finalList = new ArrayList<>();
|
||||
StringBuilder stringbuilder = new StringBuilder();
|
||||
try {
|
||||
splitComponents(finalList, o, c);
|
||||
for(Object component : finalList) {
|
||||
if(VersionHandler.is1_7_10()) {
|
||||
stringbuilder.append((String) component.getClass().getMethod("e").invoke(component));
|
||||
}
|
||||
else {
|
||||
stringbuilder.append((String) component.getClass().getMethod("getText").invoke(component));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
if(plugin.getConfig().getString("loglevel", "info").equals("debug")) {
|
||||
System.out.println("my string");
|
||||
System.out.println("my string");
|
||||
System.out.println("my string");
|
||||
System.out.println("my string");
|
||||
System.out.println("my string");
|
||||
System.out.println(stringbuilder.toString());
|
||||
}
|
||||
return stringbuilder.toString();
|
||||
}
|
||||
|
||||
private static void splitComponents(List<Object> finalList, Object o, Class<?> c) throws Exception {
|
||||
if(plugin.getConfig().getString("loglevel", "info").equals("debug")) {
|
||||
for(Method m : c.getMethods()) {
|
||||
System.out.println(m.getName());
|
||||
}
|
||||
}
|
||||
if(VersionHandler.is1_7() || VersionHandler.is1_8() || VersionHandler.is1_9() || VersionHandler.is1_10() || VersionHandler.is1_11() || VersionHandler.is1_12() || VersionHandler.is1_13() || (VersionHandler.is1_14() && !VersionHandler.is1_14_4())) {
|
||||
ArrayList<?> list = (ArrayList<?>) c.getMethod("a").invoke(o, new Object[0]);
|
||||
for(Object component : list) {
|
||||
ArrayList<?> innerList = (ArrayList<?>) c.getMethod("a").invoke(component, new Object[0]);
|
||||
if(innerList.size() > 0) {
|
||||
splitComponents(finalList, component, c);
|
||||
}
|
||||
else {
|
||||
finalList.add(component);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
ArrayList<?> list = (ArrayList<?>) c.getMethod("getSiblings").invoke(o, new Object[0]);
|
||||
for(Object component : list) {
|
||||
ArrayList<?> innerList = (ArrayList<?>) c.getMethod("getSiblings").invoke(component, new Object[0]);
|
||||
if(innerList.size() > 0) {
|
||||
splitComponents(finalList, component, c);
|
||||
}
|
||||
else {
|
||||
finalList.add(component);
|
||||
}
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
protected static Pattern chatHexColorPattern = Pattern.compile("(?i)&([X])");
|
||||
protected static Pattern chatColorPattern = Pattern.compile("(?i)&([0-9A-F])");
|
||||
protected static Pattern chatMagicPattern = Pattern.compile("(?i)&([K])");
|
||||
protected static Pattern chatBoldPattern = Pattern.compile("(?i)&([L])");
|
||||
protected static Pattern chatStrikethroughPattern = Pattern.compile("(?i)&([M])");
|
||||
protected static Pattern chatUnderlinePattern = Pattern.compile("(?i)&([N])");
|
||||
protected static Pattern chatItalicPattern = Pattern.compile("(?i)&([O])");
|
||||
protected static Pattern chatResetPattern = Pattern.compile("(?i)&([R])");
|
||||
|
||||
public static String FormatStringColor(String string) {
|
||||
String allFormated = string;
|
||||
allFormated = chatColorPattern.matcher(allFormated).replaceAll("\u00A7$1");
|
||||
allFormated = chatHexColorPattern.matcher(allFormated).replaceAll("\u00A7$1");
|
||||
allFormated = allFormated.replaceAll("%", "\\%");
|
||||
|
||||
allFormated = convertHexColorCodeStringToBukkitColorCodeString(allFormated);
|
||||
return allFormated;
|
||||
}
|
||||
|
||||
public static String FormatString(String string) {
|
||||
String allFormated = string;
|
||||
allFormated = chatMagicPattern.matcher(allFormated).replaceAll("\u00A7$1");
|
||||
allFormated = chatBoldPattern.matcher(allFormated).replaceAll("\u00A7$1");
|
||||
allFormated = chatStrikethroughPattern.matcher(allFormated).replaceAll("\u00A7$1");
|
||||
allFormated = chatUnderlinePattern.matcher(allFormated).replaceAll("\u00A7$1");
|
||||
allFormated = chatItalicPattern.matcher(allFormated).replaceAll("\u00A7$1");
|
||||
allFormated = chatResetPattern.matcher(allFormated).replaceAll("\u00A7$1");
|
||||
allFormated = allFormated.replaceAll("%", "\\%");
|
||||
return allFormated;
|
||||
}
|
||||
|
||||
public static String FormatStringAll(String string) {
|
||||
String allFormated = Format.FormatString(string);
|
||||
allFormated = Format.FormatStringColor(allFormated);
|
||||
return allFormated;
|
||||
}
|
||||
|
||||
public static String FormatPlayerName(String playerPrefix, String playerDisplayName, String playerSuffix) {
|
||||
playerPrefix = chatColorPattern.matcher(playerPrefix).replaceAll("\u00A7$1");
|
||||
playerPrefix = chatMagicPattern.matcher(playerPrefix).replaceAll("\u00A7$1");
|
||||
playerPrefix = chatBoldPattern.matcher(playerPrefix).replaceAll("\u00A7$1");
|
||||
playerPrefix = chatStrikethroughPattern.matcher(playerPrefix).replaceAll("\u00A7$1");
|
||||
playerPrefix = chatUnderlinePattern.matcher(playerPrefix).replaceAll("\u00A7$1");
|
||||
playerPrefix = chatItalicPattern.matcher(playerPrefix).replaceAll("\u00A7$1");
|
||||
playerPrefix = chatResetPattern.matcher(playerPrefix).replaceAll("\u00A7$1");
|
||||
|
||||
playerSuffix = chatColorPattern.matcher(playerSuffix).replaceAll("\u00A7$1");
|
||||
playerSuffix = chatMagicPattern.matcher(playerSuffix).replaceAll("\u00A7$1");
|
||||
playerSuffix = chatBoldPattern.matcher(playerSuffix).replaceAll("\u00A7$1");
|
||||
playerSuffix = chatStrikethroughPattern.matcher(playerSuffix).replaceAll("\u00A7$1");
|
||||
playerSuffix = chatUnderlinePattern.matcher(playerSuffix).replaceAll("\u00A7$1");
|
||||
playerSuffix = chatItalicPattern.matcher(playerSuffix).replaceAll("\u00A7$1");
|
||||
playerSuffix = chatResetPattern.matcher(playerSuffix).replaceAll("\u00A7$1");
|
||||
return playerPrefix + playerDisplayName.trim() + playerSuffix;
|
||||
}
|
||||
|
||||
/**public static String FilterChat(String msg) {
|
||||
int t = 0;
|
||||
List<String> filters = plugin.getConfig().getStringList("filters");
|
||||
for(String s : filters) {
|
||||
t = 0;
|
||||
String[] pparse = new String[2];
|
||||
pparse[0] = " ";
|
||||
pparse[1] = " ";
|
||||
StringTokenizer st = new StringTokenizer(s, ",");
|
||||
while(st.hasMoreTokens()) {
|
||||
if(t < 2) {
|
||||
pparse[t++] = st.nextToken();
|
||||
}
|
||||
}
|
||||
msg = msg.replaceAll("(?i)" + pparse[0], pparse[1]);
|
||||
}
|
||||
return msg;
|
||||
}*/
|
||||
|
||||
public static boolean isValidColor(String color) {
|
||||
Boolean bFound = false;
|
||||
for(ChatColor bkColors : ChatColor.values()) {
|
||||
if(color.equalsIgnoreCase(bkColors.name())) {
|
||||
bFound = true;
|
||||
}
|
||||
}
|
||||
return bFound;
|
||||
}
|
||||
|
||||
public static boolean isValidHexColor(String color) {
|
||||
Pattern pattern = Pattern.compile("(^#[0-9a-fA-F]{6}\\b)");
|
||||
Matcher matcher = pattern.matcher(color);
|
||||
return matcher.find();
|
||||
}
|
||||
|
||||
public static String convertHexColorCodeToBukkitColorCode(String color) {
|
||||
StringBuilder bukkitColorCode = new StringBuilder(BUKKIT_COLOR_CODE_PREFIX + BUKKIT_HEX_COLOR_CODE_PREFIX);
|
||||
for(int a = 1; a < color.length(); a++) {
|
||||
bukkitColorCode.append(BUKKIT_COLOR_CODE_PREFIX + color.charAt(a));
|
||||
}
|
||||
return bukkitColorCode.toString();
|
||||
}
|
||||
|
||||
public static String convertHexColorCodeStringToBukkitColorCodeString(String string) {
|
||||
Pattern pattern = Pattern.compile("(#[0-9a-fA-F]{6})");
|
||||
Matcher matcher = pattern.matcher(string);
|
||||
while(matcher.find()) {
|
||||
int indexStart = matcher.start();
|
||||
int indexEnd = matcher.end();
|
||||
String hexColor = string.substring(indexStart, indexEnd);
|
||||
String bukkitColor = convertHexColorCodeToBukkitColorCode(hexColor);
|
||||
string = string.replaceAll(hexColor, bukkitColor);
|
||||
matcher.reset(string);
|
||||
}
|
||||
return string;
|
||||
}
|
||||
|
||||
public static String escapeAllRegex(String input) {
|
||||
return input.replace("[", "\\[").replace("]", "\\]").replace("{", "\\{").replace("}", "\\}").replace("(", "\\(").replace(")", "\\)").replace("|", "\\|").replace("+", "\\+").replace("*", "\\*");
|
||||
}
|
||||
|
||||
/**public static boolean underlineURLs() {
|
||||
return plugin.getConfig().getBoolean("underlineurls", true);
|
||||
}*/
|
||||
public static final char colorChar = '\u00a7';
|
||||
//code taken from bukkit, so it can work on bungee too
|
||||
public static String color(String textToTranslate){
|
||||
if (textToTranslate == null) return null;
|
||||
if (!textToTranslate.contains("&")) return textToTranslate;
|
||||
char[] b = textToTranslate.toCharArray();
|
||||
for (int i = 0; i < b.length - 1; i++) {
|
||||
if ((b[i] == '&') && ("0123456789AaBbCcDdEeFfKkLlMmNnOoRr".indexOf(b[(i + 1)]) > -1)){
|
||||
b[i] = colorChar;
|
||||
b[(i + 1)] = Character.toLowerCase(b[(i + 1)]);
|
||||
}
|
||||
}
|
||||
return new String(b);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.github.joelgodofwar.mmh.util;
|
||||
|
||||
import org.apache.commons.lang.math.NumberUtils;
|
||||
import org.bukkit.Bukkit;
|
||||
|
||||
public class GetMCVersion {
|
||||
private String Version;
|
||||
private int Major;
|
||||
private int Minor;
|
||||
private int Patch;
|
||||
private int Build;
|
||||
public GetMCVersion(){
|
||||
Version = Bukkit.getVersion();
|
||||
Version = Version.substring(Version.indexOf("MC: "), Version.length()).replace("MC: ", "").replace(")", "");
|
||||
String[] strArray = Version.split(".");
|
||||
Major = NumberUtils.toInt(strArray[0]);
|
||||
Minor = NumberUtils.toInt(strArray[1]);
|
||||
Patch = NumberUtils.toInt(strArray[2], 0);
|
||||
}
|
||||
|
||||
public String VERSION(){
|
||||
return Version;
|
||||
}
|
||||
public int MAJOR(){
|
||||
return Major;
|
||||
}
|
||||
public int MINOR(){
|
||||
return Minor;
|
||||
}
|
||||
public int PATCH(){
|
||||
return Patch;
|
||||
}
|
||||
public int BUILD(){
|
||||
return Build;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package com.github.joelgodofwar.mmh.util;
|
||||
|
||||
/*
|
||||
* Copyright (C) 2012 p000ison
|
||||
*
|
||||
* This work is licensed under the Creative Commons
|
||||
* Attribution-NonCommercial-NoDerivs 3.0 Unported License. To view a copy of
|
||||
* this license, visit http://creativecommons.org/licenses/by-nc-nd/3.0/ or send
|
||||
* a letter to Creative Commons, 171 Second Street, Suite 300, San Francisco,
|
||||
* California, 94105, USA.
|
||||
*
|
||||
*/
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.logging.FileHandler;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.logging.SimpleFormatter;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
|
||||
/**
|
||||
* This should be get initialised when the plugin loads (It contains the Logging
|
||||
* system)
|
||||
*
|
||||
* @author Max
|
||||
*/
|
||||
public class Logging
|
||||
{
|
||||
|
||||
private static Logger log;
|
||||
private static final Logger debugLogger = Logger.getLogger("Logging");
|
||||
private FileHandler debugFileHandler;
|
||||
|
||||
/**
|
||||
* Gets the logger for your plugin
|
||||
*
|
||||
* @param plugin The plugin to apply
|
||||
*/
|
||||
public Logging(Plugin plugin)
|
||||
{
|
||||
|
||||
log = plugin.getLogger();
|
||||
|
||||
try {
|
||||
|
||||
debugFileHandler = new FileHandler(plugin.getDataFolder().getAbsolutePath() + File.pathSeparator + plugin.getName() + ".log", true);
|
||||
SimpleFormatter formatter = new SimpleFormatter();
|
||||
|
||||
debugLogger.addHandler(debugFileHandler);
|
||||
debugFileHandler.setFormatter(formatter);
|
||||
|
||||
} catch (IOException ex) {
|
||||
debug(null, ex, false);
|
||||
} catch (SecurityException ex) {
|
||||
debug(null, ex, false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message
|
||||
*
|
||||
* @param level The level to log
|
||||
* @param msg The message to log
|
||||
* @param toFile log to own log?
|
||||
*/
|
||||
public static void debug(Level level, String msg, boolean toFile){
|
||||
if (toFile) {
|
||||
if (debugLogger != null) {
|
||||
debugLogger.log(level, msg);
|
||||
}
|
||||
}
|
||||
log.log(level, msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a Exception
|
||||
*
|
||||
* @param msg The message to log
|
||||
* @param exception the exception
|
||||
* @param toFile log to own log?
|
||||
*/
|
||||
public static void debug(String msg, Throwable exception, boolean toFile){
|
||||
if (toFile) {
|
||||
if (debugLogger != null) {
|
||||
debugLogger.log(Level.SEVERE, msg, exception);
|
||||
}
|
||||
}
|
||||
log.log(Level.SEVERE, msg, exception);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,848 @@
|
||||
package com.github.joelgodofwar.mmh.util;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.zip.GZIPOutputStream;
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
public class Metrics {
|
||||
|
||||
private final Plugin plugin;
|
||||
|
||||
private final MetricsBase metricsBase;
|
||||
|
||||
/**
|
||||
* Creates a new Metrics instance.
|
||||
*
|
||||
* @param plugin Your plugin instance.
|
||||
* @param serviceId The id of the service. It can be found at <a
|
||||
* href="https://bstats.org/what-is-my-plugin-id">What is my plugin id?</a>
|
||||
*/
|
||||
public Metrics(JavaPlugin plugin, int serviceId) {
|
||||
this.plugin = plugin;
|
||||
// Get the config file
|
||||
File bStatsFolder = new File(plugin.getDataFolder().getParentFile(), "bStats");
|
||||
File configFile = new File(bStatsFolder, "config.yml");
|
||||
YamlConfiguration config = YamlConfiguration.loadConfiguration(configFile);
|
||||
if (!config.isSet("serverUuid")) {
|
||||
config.addDefault("enabled", true);
|
||||
config.addDefault("serverUuid", UUID.randomUUID().toString());
|
||||
config.addDefault("logFailedRequests", false);
|
||||
config.addDefault("logSentData", false);
|
||||
config.addDefault("logResponseStatusText", false);
|
||||
// Inform the server owners about bStats
|
||||
config
|
||||
.options()
|
||||
.header(
|
||||
"bStats (https://bStats.org) collects some basic information for plugin authors, like how\n"
|
||||
+ "many people use their plugin and their total player count. It's recommended to keep bStats\n"
|
||||
+ "enabled, but if you're not comfortable with this, you can turn this setting off. There is no\n"
|
||||
+ "performance penalty associated with having metrics enabled, and data sent to bStats is fully\n"
|
||||
+ "anonymous.")
|
||||
.copyDefaults(true);
|
||||
try {
|
||||
config.save(configFile);
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
// Load the data
|
||||
boolean enabled = config.getBoolean("enabled", true);
|
||||
String serverUUID = config.getString("serverUuid");
|
||||
boolean logErrors = config.getBoolean("logFailedRequests", false);
|
||||
boolean logSentData = config.getBoolean("logSentData", false);
|
||||
boolean logResponseStatusText = config.getBoolean("logResponseStatusText", false);
|
||||
metricsBase =
|
||||
new MetricsBase(
|
||||
"bukkit",
|
||||
serverUUID,
|
||||
serviceId,
|
||||
enabled,
|
||||
this::appendPlatformData,
|
||||
this::appendServiceData,
|
||||
submitDataTask -> Bukkit.getScheduler().runTask(plugin, submitDataTask),
|
||||
plugin::isEnabled,
|
||||
(message, error) -> this.plugin.getLogger().log(Level.WARNING, message, error),
|
||||
(message) -> this.plugin.getLogger().log(Level.INFO, message),
|
||||
logErrors,
|
||||
logSentData,
|
||||
logResponseStatusText);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a custom chart.
|
||||
*
|
||||
* @param chart The chart to add.
|
||||
*/
|
||||
public void addCustomChart(CustomChart chart) {
|
||||
metricsBase.addCustomChart(chart);
|
||||
}
|
||||
|
||||
private void appendPlatformData(JsonObjectBuilder builder) {
|
||||
builder.appendField("playerAmount", getPlayerAmount());
|
||||
builder.appendField("onlineMode", Bukkit.getOnlineMode() ? 1 : 0);
|
||||
builder.appendField("bukkitVersion", Bukkit.getVersion());
|
||||
builder.appendField("bukkitName", Bukkit.getName());
|
||||
builder.appendField("javaVersion", System.getProperty("java.version"));
|
||||
builder.appendField("osName", System.getProperty("os.name"));
|
||||
builder.appendField("osArch", System.getProperty("os.arch"));
|
||||
builder.appendField("osVersion", System.getProperty("os.version"));
|
||||
builder.appendField("coreCount", Runtime.getRuntime().availableProcessors());
|
||||
}
|
||||
|
||||
private void appendServiceData(JsonObjectBuilder builder) {
|
||||
builder.appendField("pluginVersion", plugin.getDescription().getVersion());
|
||||
}
|
||||
|
||||
private int getPlayerAmount() {
|
||||
try {
|
||||
// Around MC 1.8 the return type was changed from an array to a collection,
|
||||
// This fixes java.lang.NoSuchMethodError:
|
||||
// org.bukkit.Bukkit.getOnlinePlayers()Ljava/util/Collection;
|
||||
Method onlinePlayersMethod = Class.forName("org.bukkit.Server").getMethod("getOnlinePlayers");
|
||||
return onlinePlayersMethod.getReturnType().equals(Collection.class)
|
||||
? ((Collection<?>) onlinePlayersMethod.invoke(Bukkit.getServer())).size()
|
||||
: ((Player[]) onlinePlayersMethod.invoke(Bukkit.getServer())).length;
|
||||
} catch (Exception e) {
|
||||
// Just use the new method if the reflection failed
|
||||
return Bukkit.getOnlinePlayers().size();
|
||||
}
|
||||
}
|
||||
|
||||
public static class MetricsBase {
|
||||
|
||||
/** The version of the Metrics class. */
|
||||
public static final String METRICS_VERSION = "2.2.1";
|
||||
|
||||
private static final ScheduledExecutorService scheduler =
|
||||
Executors.newScheduledThreadPool(1, task -> new Thread(task, "bStats-Metrics"));
|
||||
|
||||
private static final String REPORT_URL = "https://bStats.org/api/v2/data/%s";
|
||||
|
||||
private final String platform;
|
||||
|
||||
private final String serverUuid;
|
||||
|
||||
private final int serviceId;
|
||||
|
||||
private final Consumer<JsonObjectBuilder> appendPlatformDataConsumer;
|
||||
|
||||
private final Consumer<JsonObjectBuilder> appendServiceDataConsumer;
|
||||
|
||||
private final Consumer<Runnable> submitTaskConsumer;
|
||||
|
||||
private final Supplier<Boolean> checkServiceEnabledSupplier;
|
||||
|
||||
private final BiConsumer<String, Throwable> errorLogger;
|
||||
|
||||
private final Consumer<String> infoLogger;
|
||||
|
||||
private final boolean logErrors;
|
||||
|
||||
private final boolean logSentData;
|
||||
|
||||
private final boolean logResponseStatusText;
|
||||
|
||||
private final Set<CustomChart> customCharts = new HashSet<>();
|
||||
|
||||
private final boolean enabled;
|
||||
|
||||
/**
|
||||
* Creates a new MetricsBase class instance.
|
||||
*
|
||||
* @param platform The platform of the service.
|
||||
* @param serviceId The id of the service.
|
||||
* @param serverUuid The server uuid.
|
||||
* @param enabled Whether or not data sending is enabled.
|
||||
* @param appendPlatformDataConsumer A consumer that receives a {@code JsonObjectBuilder} and
|
||||
* appends all platform-specific data.
|
||||
* @param appendServiceDataConsumer A consumer that receives a {@code JsonObjectBuilder} and
|
||||
* appends all service-specific data.
|
||||
* @param submitTaskConsumer A consumer that takes a runnable with the submit task. This can be
|
||||
* used to delegate the data collection to a another thread to prevent errors caused by
|
||||
* concurrency. Can be {@code null}.
|
||||
* @param checkServiceEnabledSupplier A supplier to check if the service is still enabled.
|
||||
* @param errorLogger A consumer that accepts log message and an error.
|
||||
* @param infoLogger A consumer that accepts info log messages.
|
||||
* @param logErrors Whether or not errors should be logged.
|
||||
* @param logSentData Whether or not the sent data should be logged.
|
||||
* @param logResponseStatusText Whether or not the response status text should be logged.
|
||||
*/
|
||||
public MetricsBase(
|
||||
String platform,
|
||||
String serverUuid,
|
||||
int serviceId,
|
||||
boolean enabled,
|
||||
Consumer<JsonObjectBuilder> appendPlatformDataConsumer,
|
||||
Consumer<JsonObjectBuilder> appendServiceDataConsumer,
|
||||
Consumer<Runnable> submitTaskConsumer,
|
||||
Supplier<Boolean> checkServiceEnabledSupplier,
|
||||
BiConsumer<String, Throwable> errorLogger,
|
||||
Consumer<String> infoLogger,
|
||||
boolean logErrors,
|
||||
boolean logSentData,
|
||||
boolean logResponseStatusText) {
|
||||
this.platform = platform;
|
||||
this.serverUuid = serverUuid;
|
||||
this.serviceId = serviceId;
|
||||
this.enabled = enabled;
|
||||
this.appendPlatformDataConsumer = appendPlatformDataConsumer;
|
||||
this.appendServiceDataConsumer = appendServiceDataConsumer;
|
||||
this.submitTaskConsumer = submitTaskConsumer;
|
||||
this.checkServiceEnabledSupplier = checkServiceEnabledSupplier;
|
||||
this.errorLogger = errorLogger;
|
||||
this.infoLogger = infoLogger;
|
||||
this.logErrors = logErrors;
|
||||
this.logSentData = logSentData;
|
||||
this.logResponseStatusText = logResponseStatusText;
|
||||
checkRelocation();
|
||||
if (enabled) {
|
||||
startSubmitting();
|
||||
}
|
||||
}
|
||||
|
||||
public void addCustomChart(CustomChart chart) {
|
||||
this.customCharts.add(chart);
|
||||
}
|
||||
|
||||
private void startSubmitting() {
|
||||
final Runnable submitTask =
|
||||
() -> {
|
||||
if (!enabled || !checkServiceEnabledSupplier.get()) {
|
||||
// Submitting data or service is disabled
|
||||
scheduler.shutdown();
|
||||
return;
|
||||
}
|
||||
if (submitTaskConsumer != null) {
|
||||
submitTaskConsumer.accept(this::submitData);
|
||||
} else {
|
||||
this.submitData();
|
||||
}
|
||||
};
|
||||
// Many servers tend to restart at a fixed time at xx:00 which causes an uneven distribution
|
||||
// of requests on the
|
||||
// bStats backend. To circumvent this problem, we introduce some randomness into the initial
|
||||
// and second delay.
|
||||
// WARNING: You must not modify and part of this Metrics class, including the submit delay or
|
||||
// frequency!
|
||||
// WARNING: Modifying this code will get your plugin banned on bStats. Just don't do it!
|
||||
long initialDelay = (long) (1000 * 60 * (3 + Math.random() * 3));
|
||||
long secondDelay = (long) (1000 * 60 * (Math.random() * 30));
|
||||
scheduler.schedule(submitTask, initialDelay, TimeUnit.MILLISECONDS);
|
||||
scheduler.scheduleAtFixedRate(
|
||||
submitTask, initialDelay + secondDelay, 1000 * 60 * 30, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
private void submitData() {
|
||||
final JsonObjectBuilder baseJsonBuilder = new JsonObjectBuilder();
|
||||
appendPlatformDataConsumer.accept(baseJsonBuilder);
|
||||
final JsonObjectBuilder serviceJsonBuilder = new JsonObjectBuilder();
|
||||
appendServiceDataConsumer.accept(serviceJsonBuilder);
|
||||
JsonObjectBuilder.JsonObject[] chartData =
|
||||
customCharts.stream()
|
||||
.map(customChart -> customChart.getRequestJsonObject(errorLogger, logErrors))
|
||||
.filter(Objects::nonNull)
|
||||
.toArray(JsonObjectBuilder.JsonObject[]::new);
|
||||
serviceJsonBuilder.appendField("id", serviceId);
|
||||
serviceJsonBuilder.appendField("customCharts", chartData);
|
||||
baseJsonBuilder.appendField("service", serviceJsonBuilder.build());
|
||||
baseJsonBuilder.appendField("serverUUID", serverUuid);
|
||||
baseJsonBuilder.appendField("metricsVersion", METRICS_VERSION);
|
||||
JsonObjectBuilder.JsonObject data = baseJsonBuilder.build();
|
||||
scheduler.execute(
|
||||
() -> {
|
||||
try {
|
||||
// Send the data
|
||||
sendData(data);
|
||||
} catch (Exception e) {
|
||||
// Something went wrong! :(
|
||||
if (logErrors) {
|
||||
errorLogger.accept("Could not submit bStats metrics data", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void sendData(JsonObjectBuilder.JsonObject data) throws Exception {
|
||||
if (logSentData) {
|
||||
infoLogger.accept("Sent bStats metrics data: " + data.toString());
|
||||
}
|
||||
String url = String.format(REPORT_URL, platform);
|
||||
HttpsURLConnection connection = (HttpsURLConnection) new URL(url).openConnection();
|
||||
// Compress the data to save bandwidth
|
||||
byte[] compressedData = compress(data.toString());
|
||||
connection.setRequestMethod("POST");
|
||||
connection.addRequestProperty("Accept", "application/json");
|
||||
connection.addRequestProperty("Connection", "close");
|
||||
connection.addRequestProperty("Content-Encoding", "gzip");
|
||||
connection.addRequestProperty("Content-Length", String.valueOf(compressedData.length));
|
||||
connection.setRequestProperty("Content-Type", "application/json");
|
||||
connection.setRequestProperty("User-Agent", "Metrics-Service/1");
|
||||
connection.setDoOutput(true);
|
||||
try (DataOutputStream outputStream = new DataOutputStream(connection.getOutputStream())) {
|
||||
outputStream.write(compressedData);
|
||||
}
|
||||
StringBuilder builder = new StringBuilder();
|
||||
try (BufferedReader bufferedReader =
|
||||
new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
|
||||
String line;
|
||||
while ((line = bufferedReader.readLine()) != null) {
|
||||
builder.append(line);
|
||||
}
|
||||
}
|
||||
if (logResponseStatusText) {
|
||||
infoLogger.accept("Sent data to bStats and received response: " + builder);
|
||||
}
|
||||
}
|
||||
|
||||
/** Checks that the class was properly relocated. */
|
||||
private void checkRelocation() {
|
||||
// You can use the property to disable the check in your test environment
|
||||
if (System.getProperty("bstats.relocatecheck") == null
|
||||
|| !System.getProperty("bstats.relocatecheck").equals("false")) {
|
||||
// Maven's Relocate is clever and changes strings, too. So we have to use this little
|
||||
// "trick" ... :D
|
||||
final String defaultPackage =
|
||||
new String(new byte[] {'o', 'r', 'g', '.', 'b', 's', 't', 'a', 't', 's'});
|
||||
final String examplePackage =
|
||||
new String(new byte[] {'y', 'o', 'u', 'r', '.', 'p', 'a', 'c', 'k', 'a', 'g', 'e'});
|
||||
// We want to make sure no one just copy & pastes the example and uses the wrong package
|
||||
// names
|
||||
if (MetricsBase.class.getPackage().getName().startsWith(defaultPackage)
|
||||
|| MetricsBase.class.getPackage().getName().startsWith(examplePackage)) {
|
||||
throw new IllegalStateException("bStats Metrics class has not been relocated correctly!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gzips the given string.
|
||||
*
|
||||
* @param str The string to gzip.
|
||||
* @return The gzipped string.
|
||||
*/
|
||||
private static byte[] compress(final String str) throws IOException {
|
||||
if (str == null) {
|
||||
return null;
|
||||
}
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
try (GZIPOutputStream gzip = new GZIPOutputStream(outputStream)) {
|
||||
gzip.write(str.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
return outputStream.toByteArray();
|
||||
}
|
||||
}
|
||||
|
||||
public static class AdvancedBarChart extends CustomChart {
|
||||
|
||||
private final Callable<Map<String, int[]>> callable;
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @param chartId The id of the chart.
|
||||
* @param callable The callable which is used to request the chart data.
|
||||
*/
|
||||
public AdvancedBarChart(String chartId, Callable<Map<String, int[]>> callable) {
|
||||
super(chartId);
|
||||
this.callable = callable;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JsonObjectBuilder.JsonObject getChartData() throws Exception {
|
||||
JsonObjectBuilder valuesBuilder = new JsonObjectBuilder();
|
||||
Map<String, int[]> map = callable.call();
|
||||
if (map == null || map.isEmpty()) {
|
||||
// Null = skip the chart
|
||||
return null;
|
||||
}
|
||||
boolean allSkipped = true;
|
||||
for (Map.Entry<String, int[]> entry : map.entrySet()) {
|
||||
if (entry.getValue().length == 0) {
|
||||
// Skip this invalid
|
||||
continue;
|
||||
}
|
||||
allSkipped = false;
|
||||
valuesBuilder.appendField(entry.getKey(), entry.getValue());
|
||||
}
|
||||
if (allSkipped) {
|
||||
// Null = skip the chart
|
||||
return null;
|
||||
}
|
||||
return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build();
|
||||
}
|
||||
}
|
||||
|
||||
public static class SimpleBarChart extends CustomChart {
|
||||
|
||||
private final Callable<Map<String, Integer>> callable;
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @param chartId The id of the chart.
|
||||
* @param callable The callable which is used to request the chart data.
|
||||
*/
|
||||
public SimpleBarChart(String chartId, Callable<Map<String, Integer>> callable) {
|
||||
super(chartId);
|
||||
this.callable = callable;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JsonObjectBuilder.JsonObject getChartData() throws Exception {
|
||||
JsonObjectBuilder valuesBuilder = new JsonObjectBuilder();
|
||||
Map<String, Integer> map = callable.call();
|
||||
if (map == null || map.isEmpty()) {
|
||||
// Null = skip the chart
|
||||
return null;
|
||||
}
|
||||
for (Map.Entry<String, Integer> entry : map.entrySet()) {
|
||||
valuesBuilder.appendField(entry.getKey(), new int[] {entry.getValue()});
|
||||
}
|
||||
return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build();
|
||||
}
|
||||
}
|
||||
|
||||
public static class MultiLineChart extends CustomChart {
|
||||
|
||||
private final Callable<Map<String, Integer>> callable;
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @param chartId The id of the chart.
|
||||
* @param callable The callable which is used to request the chart data.
|
||||
*/
|
||||
public MultiLineChart(String chartId, Callable<Map<String, Integer>> callable) {
|
||||
super(chartId);
|
||||
this.callable = callable;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JsonObjectBuilder.JsonObject getChartData() throws Exception {
|
||||
JsonObjectBuilder valuesBuilder = new JsonObjectBuilder();
|
||||
Map<String, Integer> map = callable.call();
|
||||
if (map == null || map.isEmpty()) {
|
||||
// Null = skip the chart
|
||||
return null;
|
||||
}
|
||||
boolean allSkipped = true;
|
||||
for (Map.Entry<String, Integer> entry : map.entrySet()) {
|
||||
if (entry.getValue() == 0) {
|
||||
// Skip this invalid
|
||||
continue;
|
||||
}
|
||||
allSkipped = false;
|
||||
valuesBuilder.appendField(entry.getKey(), entry.getValue());
|
||||
}
|
||||
if (allSkipped) {
|
||||
// Null = skip the chart
|
||||
return null;
|
||||
}
|
||||
return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build();
|
||||
}
|
||||
}
|
||||
|
||||
public static class AdvancedPie extends CustomChart {
|
||||
|
||||
private final Callable<Map<String, Integer>> callable;
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @param chartId The id of the chart.
|
||||
* @param callable The callable which is used to request the chart data.
|
||||
*/
|
||||
public AdvancedPie(String chartId, Callable<Map<String, Integer>> callable) {
|
||||
super(chartId);
|
||||
this.callable = callable;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JsonObjectBuilder.JsonObject getChartData() throws Exception {
|
||||
JsonObjectBuilder valuesBuilder = new JsonObjectBuilder();
|
||||
Map<String, Integer> map = callable.call();
|
||||
if (map == null || map.isEmpty()) {
|
||||
// Null = skip the chart
|
||||
return null;
|
||||
}
|
||||
boolean allSkipped = true;
|
||||
for (Map.Entry<String, Integer> entry : map.entrySet()) {
|
||||
if (entry.getValue() == 0) {
|
||||
// Skip this invalid
|
||||
continue;
|
||||
}
|
||||
allSkipped = false;
|
||||
valuesBuilder.appendField(entry.getKey(), entry.getValue());
|
||||
}
|
||||
if (allSkipped) {
|
||||
// Null = skip the chart
|
||||
return null;
|
||||
}
|
||||
return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build();
|
||||
}
|
||||
}
|
||||
|
||||
public abstract static class CustomChart {
|
||||
|
||||
private final String chartId;
|
||||
|
||||
protected CustomChart(String chartId) {
|
||||
if (chartId == null) {
|
||||
throw new IllegalArgumentException("chartId must not be null");
|
||||
}
|
||||
this.chartId = chartId;
|
||||
}
|
||||
|
||||
public JsonObjectBuilder.JsonObject getRequestJsonObject(
|
||||
BiConsumer<String, Throwable> errorLogger, boolean logErrors) {
|
||||
JsonObjectBuilder builder = new JsonObjectBuilder();
|
||||
builder.appendField("chartId", chartId);
|
||||
try {
|
||||
JsonObjectBuilder.JsonObject data = getChartData();
|
||||
if (data == null) {
|
||||
// If the data is null we don't send the chart.
|
||||
return null;
|
||||
}
|
||||
builder.appendField("data", data);
|
||||
} catch (Throwable t) {
|
||||
if (logErrors) {
|
||||
errorLogger.accept("Failed to get data for custom chart with id " + chartId, t);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
protected abstract JsonObjectBuilder.JsonObject getChartData() throws Exception;
|
||||
}
|
||||
|
||||
public static class SingleLineChart extends CustomChart {
|
||||
|
||||
private final Callable<Integer> callable;
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @param chartId The id of the chart.
|
||||
* @param callable The callable which is used to request the chart data.
|
||||
*/
|
||||
public SingleLineChart(String chartId, Callable<Integer> callable) {
|
||||
super(chartId);
|
||||
this.callable = callable;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JsonObjectBuilder.JsonObject getChartData() throws Exception {
|
||||
int value = callable.call();
|
||||
if (value == 0) {
|
||||
// Null = skip the chart
|
||||
return null;
|
||||
}
|
||||
return new JsonObjectBuilder().appendField("value", value).build();
|
||||
}
|
||||
}
|
||||
|
||||
public static class SimplePie extends CustomChart {
|
||||
|
||||
private final Callable<String> callable;
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @param chartId The id of the chart.
|
||||
* @param callable The callable which is used to request the chart data.
|
||||
*/
|
||||
public SimplePie(String chartId, Callable<String> callable) {
|
||||
super(chartId);
|
||||
this.callable = callable;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JsonObjectBuilder.JsonObject getChartData() throws Exception {
|
||||
String value = callable.call();
|
||||
if (value == null || value.isEmpty()) {
|
||||
// Null = skip the chart
|
||||
return null;
|
||||
}
|
||||
return new JsonObjectBuilder().appendField("value", value).build();
|
||||
}
|
||||
}
|
||||
|
||||
public static class DrilldownPie extends CustomChart {
|
||||
|
||||
private final Callable<Map<String, Map<String, Integer>>> callable;
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @param chartId The id of the chart.
|
||||
* @param callable The callable which is used to request the chart data.
|
||||
*/
|
||||
public DrilldownPie(String chartId, Callable<Map<String, Map<String, Integer>>> callable) {
|
||||
super(chartId);
|
||||
this.callable = callable;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonObjectBuilder.JsonObject getChartData() throws Exception {
|
||||
JsonObjectBuilder valuesBuilder = new JsonObjectBuilder();
|
||||
Map<String, Map<String, Integer>> map = callable.call();
|
||||
if (map == null || map.isEmpty()) {
|
||||
// Null = skip the chart
|
||||
return null;
|
||||
}
|
||||
boolean reallyAllSkipped = true;
|
||||
for (Map.Entry<String, Map<String, Integer>> entryValues : map.entrySet()) {
|
||||
JsonObjectBuilder valueBuilder = new JsonObjectBuilder();
|
||||
boolean allSkipped = true;
|
||||
for (Map.Entry<String, Integer> valueEntry : map.get(entryValues.getKey()).entrySet()) {
|
||||
valueBuilder.appendField(valueEntry.getKey(), valueEntry.getValue());
|
||||
allSkipped = false;
|
||||
}
|
||||
if (!allSkipped) {
|
||||
reallyAllSkipped = false;
|
||||
valuesBuilder.appendField(entryValues.getKey(), valueBuilder.build());
|
||||
}
|
||||
}
|
||||
if (reallyAllSkipped) {
|
||||
// Null = skip the chart
|
||||
return null;
|
||||
}
|
||||
return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An extremely simple JSON builder.
|
||||
*
|
||||
* <p>While this class is neither feature-rich nor the most performant one, it's sufficient enough
|
||||
* for its use-case.
|
||||
*/
|
||||
public static class JsonObjectBuilder {
|
||||
|
||||
private StringBuilder builder = new StringBuilder();
|
||||
|
||||
private boolean hasAtLeastOneField = false;
|
||||
|
||||
public JsonObjectBuilder() {
|
||||
builder.append("{");
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a null field to the JSON.
|
||||
*
|
||||
* @param key The key of the field.
|
||||
* @return A reference to this object.
|
||||
*/
|
||||
public JsonObjectBuilder appendNull(String key) {
|
||||
appendFieldUnescaped(key, "null");
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a string field to the JSON.
|
||||
*
|
||||
* @param key The key of the field.
|
||||
* @param value The value of the field.
|
||||
* @return A reference to this object.
|
||||
*/
|
||||
public JsonObjectBuilder appendField(String key, String value) {
|
||||
if (value == null) {
|
||||
throw new IllegalArgumentException("JSON value must not be null");
|
||||
}
|
||||
appendFieldUnescaped(key, "\"" + escape(value) + "\"");
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends an integer field to the JSON.
|
||||
*
|
||||
* @param key The key of the field.
|
||||
* @param value The value of the field.
|
||||
* @return A reference to this object.
|
||||
*/
|
||||
public JsonObjectBuilder appendField(String key, int value) {
|
||||
appendFieldUnescaped(key, String.valueOf(value));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends an object to the JSON.
|
||||
*
|
||||
* @param key The key of the field.
|
||||
* @param object The object.
|
||||
* @return A reference to this object.
|
||||
*/
|
||||
public JsonObjectBuilder appendField(String key, JsonObject object) {
|
||||
if (object == null) {
|
||||
throw new IllegalArgumentException("JSON object must not be null");
|
||||
}
|
||||
appendFieldUnescaped(key, object.toString());
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a string array to the JSON.
|
||||
*
|
||||
* @param key The key of the field.
|
||||
* @param values The string array.
|
||||
* @return A reference to this object.
|
||||
*/
|
||||
public JsonObjectBuilder appendField(String key, String[] values) {
|
||||
if (values == null) {
|
||||
throw new IllegalArgumentException("JSON values must not be null");
|
||||
}
|
||||
String escapedValues =
|
||||
Arrays.stream(values)
|
||||
.map(value -> "\"" + escape(value) + "\"")
|
||||
.collect(Collectors.joining(","));
|
||||
appendFieldUnescaped(key, "[" + escapedValues + "]");
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends an integer array to the JSON.
|
||||
*
|
||||
* @param key The key of the field.
|
||||
* @param values The integer array.
|
||||
* @return A reference to this object.
|
||||
*/
|
||||
public JsonObjectBuilder appendField(String key, int[] values) {
|
||||
if (values == null) {
|
||||
throw new IllegalArgumentException("JSON values must not be null");
|
||||
}
|
||||
String escapedValues =
|
||||
Arrays.stream(values).mapToObj(String::valueOf).collect(Collectors.joining(","));
|
||||
appendFieldUnescaped(key, "[" + escapedValues + "]");
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends an object array to the JSON.
|
||||
*
|
||||
* @param key The key of the field.
|
||||
* @param values The integer array.
|
||||
* @return A reference to this object.
|
||||
*/
|
||||
public JsonObjectBuilder appendField(String key, JsonObject[] values) {
|
||||
if (values == null) {
|
||||
throw new IllegalArgumentException("JSON values must not be null");
|
||||
}
|
||||
String escapedValues =
|
||||
Arrays.stream(values).map(JsonObject::toString).collect(Collectors.joining(","));
|
||||
appendFieldUnescaped(key, "[" + escapedValues + "]");
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a field to the object.
|
||||
*
|
||||
* @param key The key of the field.
|
||||
* @param escapedValue The escaped value of the field.
|
||||
*/
|
||||
private void appendFieldUnescaped(String key, String escapedValue) {
|
||||
if (builder == null) {
|
||||
throw new IllegalStateException("JSON has already been built");
|
||||
}
|
||||
if (key == null) {
|
||||
throw new IllegalArgumentException("JSON key must not be null");
|
||||
}
|
||||
if (hasAtLeastOneField) {
|
||||
builder.append(",");
|
||||
}
|
||||
builder.append("\"").append(escape(key)).append("\":").append(escapedValue);
|
||||
hasAtLeastOneField = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the JSON string and invalidates this builder.
|
||||
*
|
||||
* @return The built JSON string.
|
||||
*/
|
||||
public JsonObject build() {
|
||||
if (builder == null) {
|
||||
throw new IllegalStateException("JSON has already been built");
|
||||
}
|
||||
JsonObject object = new JsonObject(builder.append("}").toString());
|
||||
builder = null;
|
||||
return object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes the given string like stated in https://www.ietf.org/rfc/rfc4627.txt.
|
||||
*
|
||||
* <p>This method escapes only the necessary characters '"', '\'. and '\u0000' - '\u001F'.
|
||||
* Compact escapes are not used (e.g., '\n' is escaped as "\u000a" and not as "\n").
|
||||
*
|
||||
* @param value The value to escape.
|
||||
* @return The escaped value.
|
||||
*/
|
||||
private static String escape(String value) {
|
||||
final StringBuilder builder = new StringBuilder();
|
||||
for (int i = 0; i < value.length(); i++) {
|
||||
char c = value.charAt(i);
|
||||
if (c == '"') {
|
||||
builder.append("\\\"");
|
||||
} else if (c == '\\') {
|
||||
builder.append("\\\\");
|
||||
} else if (c <= '\u000F') {
|
||||
builder.append("\\u000").append(Integer.toHexString(c));
|
||||
} else if (c <= '\u001F') {
|
||||
builder.append("\\u00").append(Integer.toHexString(c));
|
||||
} else {
|
||||
builder.append(c);
|
||||
}
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* A super simple representation of a JSON object.
|
||||
*
|
||||
* <p>This class only exists to make methods of the {@link JsonObjectBuilder} type-safe and not
|
||||
* allow a raw string inputs for methods like {@link JsonObjectBuilder#appendField(String,
|
||||
* JsonObject)}.
|
||||
*/
|
||||
public static class JsonObject {
|
||||
|
||||
private final String value;
|
||||
|
||||
private JsonObject(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,732 @@
|
||||
package com.github.joelgodofwar.mmh.util;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.logging.Level;
|
||||
import java.util.zip.GZIPOutputStream;
|
||||
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import org.bukkit.plugin.RegisteredServiceProvider;
|
||||
import org.bukkit.plugin.ServicePriority;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
|
||||
/**
|
||||
* bStats collects some data for plugin authors.
|
||||
* <p>
|
||||
* Check out https://bStats.org/ to learn more about bStats!
|
||||
*/
|
||||
//@SuppressWarnings({"WeakerAccess", "unused"})
|
||||
public class Metrics2 {
|
||||
|
||||
static {
|
||||
// You can use the property to disable the check in your test environment
|
||||
if (System.getProperty("bstats.relocatecheck") == null || !System.getProperty("bstats.relocatecheck").equals("false")) {
|
||||
// Maven's Relocate is clever and changes strings, too. So we have to use this little "trick" ... :D
|
||||
final String defaultPackage = new String(
|
||||
new byte[]{'o', 'r', 'g', '.', 'b', 's', 't', 'a', 't', 's', '.', 'b', 'u', 'k', 'k', 'i', 't'});
|
||||
final String examplePackage = new String(new byte[]{'y', 'o', 'u', 'r', '.', 'p', 'a', 'c', 'k', 'a', 'g', 'e'});
|
||||
// We want to make sure nobody just copy & pastes the example and use the wrong package names
|
||||
if (Metrics2.class.getPackage().getName().equals(defaultPackage) || Metrics2.class.getPackage().getName().equals(examplePackage)) {
|
||||
throw new IllegalStateException("bStats Metrics class has not been relocated correctly!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The version of this bStats class
|
||||
public static final int B_STATS_VERSION = 1;
|
||||
|
||||
// The url to which the data is sent
|
||||
private static final String URL = "https://bStats.org/submitData/bukkit";
|
||||
|
||||
// Is bStats enabled on this server?
|
||||
private boolean enabled;
|
||||
|
||||
// Should failed requests be logged?
|
||||
private static boolean logFailedRequests;
|
||||
|
||||
// Should the sent data be logged?
|
||||
private static boolean logSentData;
|
||||
|
||||
// Should the response text be logged?
|
||||
private static boolean logResponseStatusText;
|
||||
|
||||
// The uuid of the server
|
||||
private static String serverUUID;
|
||||
|
||||
// The plugin
|
||||
private final Plugin plugin;
|
||||
|
||||
// A list with all custom charts
|
||||
private final List<CustomChart> charts = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @param plugin The plugin which stats should be submitted.
|
||||
*/
|
||||
public Metrics2(Plugin plugin) {
|
||||
if (plugin == null) {
|
||||
throw new IllegalArgumentException("Plugin cannot be null!");
|
||||
}
|
||||
this.plugin = plugin;
|
||||
|
||||
// Get the config file
|
||||
File bStatsFolder = new File(plugin.getDataFolder().getParentFile(), "bStats");
|
||||
File configFile = new File(bStatsFolder, "config.yml");
|
||||
YamlConfiguration config = YamlConfiguration.loadConfiguration(configFile);
|
||||
|
||||
// Check if the config file exists
|
||||
if (!config.isSet("serverUuid")) {
|
||||
|
||||
// Add default values
|
||||
config.addDefault("enabled", true);
|
||||
// Every server gets it's unique random id.
|
||||
config.addDefault("serverUuid", UUID.randomUUID().toString());
|
||||
// Should failed request be logged?
|
||||
config.addDefault("logFailedRequests", false);
|
||||
// Should the sent data be logged?
|
||||
config.addDefault("logSentData", false);
|
||||
// Should the response text be logged?
|
||||
config.addDefault("logResponseStatusText", false);
|
||||
|
||||
// Inform the server owners about bStats
|
||||
config.options().header(
|
||||
"bStats collects some data for plugin authors like how many servers are using their plugins.\n" +
|
||||
"To honor their work, you should not disable it.\n" +
|
||||
"This has nearly no effect on the server performance!\n" +
|
||||
"Check out https://bStats.org/ to learn more :)"
|
||||
).copyDefaults(true);
|
||||
try {
|
||||
config.save(configFile);
|
||||
} catch (IOException ignored) { }
|
||||
}
|
||||
|
||||
// Load the data
|
||||
enabled = config.getBoolean("enabled", true);
|
||||
serverUUID = config.getString("serverUuid");
|
||||
logFailedRequests = config.getBoolean("logFailedRequests", false);
|
||||
logSentData = config.getBoolean("logSentData", false);
|
||||
logResponseStatusText = config.getBoolean("logResponseStatusText", false);
|
||||
|
||||
if (enabled) {
|
||||
boolean found = false;
|
||||
// Search for all other bStats Metrics classes to see if we are the first one
|
||||
for (Class<?> service : Bukkit.getServicesManager().getKnownServices()) {
|
||||
try {
|
||||
service.getField("B_STATS_VERSION"); // Our identifier :)
|
||||
found = true; // We aren't the first
|
||||
break;
|
||||
} catch (NoSuchFieldException ignored) { }
|
||||
}
|
||||
// Register our service
|
||||
Bukkit.getServicesManager().register(Metrics2.class, this, plugin, ServicePriority.Normal);
|
||||
if (!found) {
|
||||
// We are the first!
|
||||
startSubmitting();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if bStats is enabled.
|
||||
*
|
||||
* @return Whether bStats is enabled or not.
|
||||
*/
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a custom chart.
|
||||
*
|
||||
* @param chart The chart to add.
|
||||
*/
|
||||
public void addCustomChart(CustomChart chart) {
|
||||
if (chart == null) {
|
||||
throw new IllegalArgumentException("Chart cannot be null!");
|
||||
}
|
||||
charts.add(chart);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the Scheduler which submits our data every 30 minutes.
|
||||
*/
|
||||
private void startSubmitting() {
|
||||
final Timer timer = new Timer(true); // We use a timer cause the Bukkit scheduler is affected by server lags
|
||||
timer.scheduleAtFixedRate(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (!plugin.isEnabled()) { // Plugin was disabled
|
||||
timer.cancel();
|
||||
return;
|
||||
}
|
||||
// Nevertheless we want our code to run in the Bukkit main thread, so we have to use the Bukkit scheduler
|
||||
// Don't be afraid! The connection to the bStats server is still async, only the stats collection is sync ;)
|
||||
Bukkit.getScheduler().runTask(plugin, () -> submitData());
|
||||
}
|
||||
}, 1000 * 60 * 5, 1000 * 60 * 30);
|
||||
// Submit the data every 30 minutes, first time after 5 minutes to give other plugins enough time to start
|
||||
// WARNING: Changing the frequency has no effect but your plugin WILL be blocked/deleted!
|
||||
// WARNING: Just don't do it!
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the plugin specific data.
|
||||
* This method is called using Reflection.
|
||||
*
|
||||
* @return The plugin specific data.
|
||||
*/
|
||||
public JsonObject getPluginData() {
|
||||
JsonObject data = new JsonObject();
|
||||
|
||||
String pluginName = plugin.getDescription().getName();
|
||||
String pluginVersion = plugin.getDescription().getVersion();
|
||||
|
||||
data.addProperty("pluginName", pluginName); // Append the name of the plugin
|
||||
data.addProperty("pluginVersion", pluginVersion); // Append the version of the plugin
|
||||
JsonArray customCharts = new JsonArray();
|
||||
for (CustomChart customChart : charts) {
|
||||
// Add the data of the custom charts
|
||||
JsonObject chart = customChart.getRequestJsonObject();
|
||||
if (chart == null) { // If the chart is null, we skip it
|
||||
continue;
|
||||
}
|
||||
customCharts.add(chart);
|
||||
}
|
||||
data.add("customCharts", customCharts);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the server specific data.
|
||||
*
|
||||
* @return The server specific data.
|
||||
*/
|
||||
private JsonObject getServerData() {
|
||||
// Minecraft specific data
|
||||
int playerAmount;
|
||||
try {
|
||||
// Around MC 1.8 the return type was changed to a collection from an array,
|
||||
// This fixes java.lang.NoSuchMethodError: org.bukkit.Bukkit.getOnlinePlayers()Ljava/util/Collection;
|
||||
Method onlinePlayersMethod = Class.forName("org.bukkit.Server").getMethod("getOnlinePlayers");
|
||||
playerAmount = onlinePlayersMethod.getReturnType().equals(Collection.class)
|
||||
? ((Collection<?>) onlinePlayersMethod.invoke(Bukkit.getServer())).size()
|
||||
: ((Player[]) onlinePlayersMethod.invoke(Bukkit.getServer())).length;
|
||||
} catch (Exception e) {
|
||||
playerAmount = Bukkit.getOnlinePlayers().size(); // Just use the new method if the Reflection failed
|
||||
}
|
||||
int onlineMode = Bukkit.getOnlineMode() ? 1 : 0;
|
||||
String bukkitVersion = Bukkit.getVersion();
|
||||
String bukkitName = Bukkit.getName();
|
||||
|
||||
// OS/Java specific data
|
||||
String javaVersion = System.getProperty("java.version");
|
||||
String osName = System.getProperty("os.name");
|
||||
String osArch = System.getProperty("os.arch");
|
||||
String osVersion = System.getProperty("os.version");
|
||||
int coreCount = Runtime.getRuntime().availableProcessors();
|
||||
|
||||
JsonObject data = new JsonObject();
|
||||
|
||||
data.addProperty("serverUUID", serverUUID);
|
||||
|
||||
data.addProperty("playerAmount", playerAmount);
|
||||
data.addProperty("onlineMode", onlineMode);
|
||||
data.addProperty("bukkitVersion", bukkitVersion);
|
||||
data.addProperty("bukkitName", bukkitName);
|
||||
|
||||
data.addProperty("javaVersion", javaVersion);
|
||||
data.addProperty("osName", osName);
|
||||
data.addProperty("osArch", osArch);
|
||||
data.addProperty("osVersion", osVersion);
|
||||
data.addProperty("coreCount", coreCount);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects the data and sends it afterwards.
|
||||
*/
|
||||
private void submitData() {
|
||||
final JsonObject data = getServerData();
|
||||
|
||||
JsonArray pluginData = new JsonArray();
|
||||
// Search for all other bStats Metrics classes to get their plugin data
|
||||
for (Class<?> service : Bukkit.getServicesManager().getKnownServices()) {
|
||||
try {
|
||||
service.getField("B_STATS_VERSION"); // Our identifier :)
|
||||
|
||||
for (RegisteredServiceProvider<?> provider : Bukkit.getServicesManager().getRegistrations(service)) {
|
||||
try {
|
||||
Object plugin = provider.getService().getMethod("getPluginData").invoke(provider.getProvider());
|
||||
if (plugin instanceof JsonObject) {
|
||||
pluginData.add((JsonObject) plugin);
|
||||
} else { // old bstats version compatibility
|
||||
try {
|
||||
Class<?> jsonObjectJsonSimple = Class.forName("org.json.simple.JSONObject");
|
||||
if (plugin.getClass().isAssignableFrom(jsonObjectJsonSimple)) {
|
||||
Method jsonStringGetter = jsonObjectJsonSimple.getDeclaredMethod("toJSONString");
|
||||
jsonStringGetter.setAccessible(true);
|
||||
String jsonString = (String) jsonStringGetter.invoke(plugin);
|
||||
JsonObject object = new JsonParser().parse(jsonString).getAsJsonObject();
|
||||
pluginData.add(object);
|
||||
}
|
||||
} catch (ClassNotFoundException e) {
|
||||
// minecraft version 1.14+
|
||||
if (logFailedRequests) {
|
||||
this.plugin.getLogger().log(Level.SEVERE, "Encountered unexpected exception", e);
|
||||
}
|
||||
continue; // continue looping since we cannot do any other thing.
|
||||
}
|
||||
}
|
||||
} catch (NullPointerException | NoSuchMethodException | IllegalAccessException | InvocationTargetException ignored) { }
|
||||
}
|
||||
} catch (NoSuchFieldException ignored) { }
|
||||
}
|
||||
|
||||
data.add("plugins", pluginData);
|
||||
|
||||
// Create a new thread for the connection to the bStats server
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
// Send the data
|
||||
sendData(plugin, data);
|
||||
} catch (Exception e) {
|
||||
// Something went wrong! :(
|
||||
if (logFailedRequests) {
|
||||
plugin.getLogger().log(Level.WARNING, "Could not submit plugin stats of " + plugin.getName(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the data to the bStats server.
|
||||
*
|
||||
* @param plugin Any plugin. It's just used to get a logger instance.
|
||||
* @param data The data to send.
|
||||
* @throws Exception If the request failed.
|
||||
*/
|
||||
private static void sendData(Plugin plugin, JsonObject data) throws Exception {
|
||||
if (data == null) {
|
||||
throw new IllegalArgumentException("Data cannot be null!");
|
||||
}
|
||||
if (Bukkit.isPrimaryThread()) {
|
||||
throw new IllegalAccessException("This method must not be called from the main thread!");
|
||||
}
|
||||
if (logSentData) {
|
||||
plugin.getLogger().info("Sending data to bStats: " + data.toString());
|
||||
}
|
||||
HttpsURLConnection connection = (HttpsURLConnection) new URL(URL).openConnection();
|
||||
|
||||
// Compress the data to save bandwidth
|
||||
byte[] compressedData = compress(data.toString());
|
||||
|
||||
// Add headers
|
||||
connection.setRequestMethod("POST");
|
||||
connection.addRequestProperty("Accept", "application/json");
|
||||
connection.addRequestProperty("Connection", "close");
|
||||
connection.addRequestProperty("Content-Encoding", "gzip"); // We gzip our request
|
||||
connection.addRequestProperty("Content-Length", String.valueOf(compressedData.length));
|
||||
connection.setRequestProperty("Content-Type", "application/json"); // We send our data in JSON format
|
||||
connection.setRequestProperty("User-Agent", "MC-Server/" + B_STATS_VERSION);
|
||||
|
||||
// Send data
|
||||
connection.setDoOutput(true);
|
||||
DataOutputStream outputStream = new DataOutputStream(connection.getOutputStream());
|
||||
outputStream.write(compressedData);
|
||||
outputStream.flush();
|
||||
outputStream.close();
|
||||
|
||||
InputStream inputStream = connection.getInputStream();
|
||||
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
|
||||
|
||||
StringBuilder builder = new StringBuilder();
|
||||
String line;
|
||||
while ((line = bufferedReader.readLine()) != null) {
|
||||
builder.append(line);
|
||||
}
|
||||
bufferedReader.close();
|
||||
if (logResponseStatusText) {
|
||||
plugin.getLogger().info("Sent data to bStats and received response: " + builder.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gzips the given String.
|
||||
*
|
||||
* @param str The string to gzip.
|
||||
* @return The gzipped String.
|
||||
* @throws IOException If the compression failed.
|
||||
*/
|
||||
private static byte[] compress(final String str) throws IOException {
|
||||
if (str == null) {
|
||||
return null;
|
||||
}
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
GZIPOutputStream gzip = new GZIPOutputStream(outputStream);
|
||||
gzip.write(str.getBytes(StandardCharsets.UTF_8));
|
||||
gzip.close();
|
||||
return outputStream.toByteArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a custom chart.
|
||||
*/
|
||||
public static abstract class CustomChart {
|
||||
|
||||
// The id of the chart
|
||||
final String chartId;
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @param chartId The id of the chart.
|
||||
*/
|
||||
CustomChart(String chartId) {
|
||||
if (chartId == null || chartId.isEmpty()) {
|
||||
throw new IllegalArgumentException("ChartId cannot be null or empty!");
|
||||
}
|
||||
this.chartId = chartId;
|
||||
}
|
||||
|
||||
private JsonObject getRequestJsonObject() {
|
||||
JsonObject chart = new JsonObject();
|
||||
chart.addProperty("chartId", chartId);
|
||||
try {
|
||||
JsonObject data = getChartData();
|
||||
if (data == null) {
|
||||
// If the data is null we don't send the chart.
|
||||
return null;
|
||||
}
|
||||
chart.add("data", data);
|
||||
} catch (Throwable t) {
|
||||
if (logFailedRequests) {
|
||||
Bukkit.getLogger().log(Level.WARNING, "Failed to get data for custom chart with id " + chartId, t);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return chart;
|
||||
}
|
||||
|
||||
protected abstract JsonObject getChartData() throws Exception;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a custom simple pie.
|
||||
*/
|
||||
public static class SimplePie extends CustomChart {
|
||||
|
||||
private final Callable<String> callable;
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @param chartId The id of the chart.
|
||||
* @param callable The callable which is used to request the chart data.
|
||||
*/
|
||||
public SimplePie(String chartId, Callable<String> callable) {
|
||||
super(chartId);
|
||||
this.callable = callable;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JsonObject getChartData() throws Exception {
|
||||
JsonObject data = new JsonObject();
|
||||
String value = callable.call();
|
||||
if (value == null || value.isEmpty()) {
|
||||
// Null = skip the chart
|
||||
return null;
|
||||
}
|
||||
data.addProperty("value", value);
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a custom advanced pie.
|
||||
*/
|
||||
public static class AdvancedPie extends CustomChart {
|
||||
|
||||
private final Callable<Map<String, Integer>> callable;
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @param chartId The id of the chart.
|
||||
* @param callable The callable which is used to request the chart data.
|
||||
*/
|
||||
public AdvancedPie(String chartId, Callable<Map<String, Integer>> callable) {
|
||||
super(chartId);
|
||||
this.callable = callable;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JsonObject getChartData() throws Exception {
|
||||
JsonObject data = new JsonObject();
|
||||
JsonObject values = new JsonObject();
|
||||
Map<String, Integer> map = callable.call();
|
||||
if (map == null || map.isEmpty()) {
|
||||
// Null = skip the chart
|
||||
return null;
|
||||
}
|
||||
boolean allSkipped = true;
|
||||
for (Map.Entry<String, Integer> entry : map.entrySet()) {
|
||||
if (entry.getValue() == 0) {
|
||||
continue; // Skip this invalid
|
||||
}
|
||||
allSkipped = false;
|
||||
values.addProperty(entry.getKey(), entry.getValue());
|
||||
}
|
||||
if (allSkipped) {
|
||||
// Null = skip the chart
|
||||
return null;
|
||||
}
|
||||
data.add("values", values);
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a custom drilldown pie.
|
||||
*/
|
||||
public static class DrilldownPie extends CustomChart {
|
||||
|
||||
private final Callable<Map<String, Map<String, Integer>>> callable;
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @param chartId The id of the chart.
|
||||
* @param callable The callable which is used to request the chart data.
|
||||
*/
|
||||
public DrilldownPie(String chartId, Callable<Map<String, Map<String, Integer>>> callable) {
|
||||
super(chartId);
|
||||
this.callable = callable;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonObject getChartData() throws Exception {
|
||||
JsonObject data = new JsonObject();
|
||||
JsonObject values = new JsonObject();
|
||||
Map<String, Map<String, Integer>> map = callable.call();
|
||||
if (map == null || map.isEmpty()) {
|
||||
// Null = skip the chart
|
||||
return null;
|
||||
}
|
||||
boolean reallyAllSkipped = true;
|
||||
for (Map.Entry<String, Map<String, Integer>> entryValues : map.entrySet()) {
|
||||
JsonObject value = new JsonObject();
|
||||
boolean allSkipped = true;
|
||||
for (Map.Entry<String, Integer> valueEntry : map.get(entryValues.getKey()).entrySet()) {
|
||||
value.addProperty(valueEntry.getKey(), valueEntry.getValue());
|
||||
allSkipped = false;
|
||||
}
|
||||
if (!allSkipped) {
|
||||
reallyAllSkipped = false;
|
||||
values.add(entryValues.getKey(), value);
|
||||
}
|
||||
}
|
||||
if (reallyAllSkipped) {
|
||||
// Null = skip the chart
|
||||
return null;
|
||||
}
|
||||
data.add("values", values);
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a custom single line chart.
|
||||
*/
|
||||
public static class SingleLineChart extends CustomChart {
|
||||
|
||||
private final Callable<Integer> callable;
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @param chartId The id of the chart.
|
||||
* @param callable The callable which is used to request the chart data.
|
||||
*/
|
||||
public SingleLineChart(String chartId, Callable<Integer> callable) {
|
||||
super(chartId);
|
||||
this.callable = callable;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JsonObject getChartData() throws Exception {
|
||||
JsonObject data = new JsonObject();
|
||||
int value = callable.call();
|
||||
if (value == 0) {
|
||||
// Null = skip the chart
|
||||
return null;
|
||||
}
|
||||
data.addProperty("value", value);
|
||||
return data;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a custom multi line chart.
|
||||
*/
|
||||
public static class MultiLineChart extends CustomChart {
|
||||
|
||||
private final Callable<Map<String, Integer>> callable;
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @param chartId The id of the chart.
|
||||
* @param callable The callable which is used to request the chart data.
|
||||
*/
|
||||
public MultiLineChart(String chartId, Callable<Map<String, Integer>> callable) {
|
||||
super(chartId);
|
||||
this.callable = callable;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JsonObject getChartData() throws Exception {
|
||||
JsonObject data = new JsonObject();
|
||||
JsonObject values = new JsonObject();
|
||||
Map<String, Integer> map = callable.call();
|
||||
if (map == null || map.isEmpty()) {
|
||||
// Null = skip the chart
|
||||
return null;
|
||||
}
|
||||
boolean allSkipped = true;
|
||||
for (Map.Entry<String, Integer> entry : map.entrySet()) {
|
||||
if (entry.getValue() == 0) {
|
||||
continue; // Skip this invalid
|
||||
}
|
||||
allSkipped = false;
|
||||
values.addProperty(entry.getKey(), entry.getValue());
|
||||
}
|
||||
if (allSkipped) {
|
||||
// Null = skip the chart
|
||||
return null;
|
||||
}
|
||||
data.add("values", values);
|
||||
return data;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a custom simple bar chart.
|
||||
*/
|
||||
public static class SimpleBarChart extends CustomChart {
|
||||
|
||||
private final Callable<Map<String, Integer>> callable;
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @param chartId The id of the chart.
|
||||
* @param callable The callable which is used to request the chart data.
|
||||
*/
|
||||
public SimpleBarChart(String chartId, Callable<Map<String, Integer>> callable) {
|
||||
super(chartId);
|
||||
this.callable = callable;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JsonObject getChartData() throws Exception {
|
||||
JsonObject data = new JsonObject();
|
||||
JsonObject values = new JsonObject();
|
||||
Map<String, Integer> map = callable.call();
|
||||
if (map == null || map.isEmpty()) {
|
||||
// Null = skip the chart
|
||||
return null;
|
||||
}
|
||||
for (Map.Entry<String, Integer> entry : map.entrySet()) {
|
||||
JsonArray categoryValues = new JsonArray();
|
||||
categoryValues.add(entry.getValue());
|
||||
values.add(entry.getKey(), categoryValues);
|
||||
}
|
||||
data.add("values", values);
|
||||
return data;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a custom advanced bar chart.
|
||||
*/
|
||||
public static class AdvancedBarChart extends CustomChart {
|
||||
|
||||
private final Callable<Map<String, int[]>> callable;
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @param chartId The id of the chart.
|
||||
* @param callable The callable which is used to request the chart data.
|
||||
*/
|
||||
public AdvancedBarChart(String chartId, Callable<Map<String, int[]>> callable) {
|
||||
super(chartId);
|
||||
this.callable = callable;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JsonObject getChartData() throws Exception {
|
||||
JsonObject data = new JsonObject();
|
||||
JsonObject values = new JsonObject();
|
||||
Map<String, int[]> map = callable.call();
|
||||
if (map == null || map.isEmpty()) {
|
||||
// Null = skip the chart
|
||||
return null;
|
||||
}
|
||||
boolean allSkipped = true;
|
||||
for (Map.Entry<String, int[]> entry : map.entrySet()) {
|
||||
if (entry.getValue().length == 0) {
|
||||
continue; // Skip this invalid
|
||||
}
|
||||
allSkipped = false;
|
||||
JsonArray categoryValues = new JsonArray();
|
||||
for (int categoryValue : entry.getValue()) {
|
||||
categoryValues.add(categoryValue);
|
||||
}
|
||||
values.add(entry.getKey(), categoryValues);
|
||||
}
|
||||
if (allSkipped) {
|
||||
// Null = skip the chart
|
||||
return null;
|
||||
}
|
||||
data.add("values", values);
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,445 @@
|
||||
package com.github.joelgodofwar.mmh.util;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Server;
|
||||
|
||||
import com.google.common.collect.ComparisonChain;
|
||||
import com.google.common.collect.Ordering;
|
||||
|
||||
/**
|
||||
* Determine the current Minecraft version.
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
public final class MinecraftVersion implements Comparable<MinecraftVersion>, Serializable {
|
||||
/**
|
||||
* The minimum version ProtocolLib has been tested with.
|
||||
*/
|
||||
public static final String MINIMUM_MINECRAFT_VERSION = "1.20";
|
||||
|
||||
/**
|
||||
* The maximum version ProtocolLib has been tested with.
|
||||
*/
|
||||
public static final String MAXIMUM_MINECRAFT_VERSION = "1.20.4";
|
||||
|
||||
/**
|
||||
* The date (with ISO 8601 or YYYY-MM-DD) when the most recent version (1.20.4) was released.
|
||||
*/
|
||||
public static final String MINECRAFT_LAST_RELEASE_DATE = "2023-12-07";
|
||||
/**
|
||||
* Version 1.20.4 - the decorated pot update
|
||||
*/
|
||||
public static final MinecraftVersion v1_20_4 = new MinecraftVersion("1.20.4");
|
||||
|
||||
/**
|
||||
* Version 1.20.2 - the update that added the configuration protocol phase.
|
||||
*/
|
||||
public static final MinecraftVersion CONFIG_PHASE_PROTOCOL_UPDATE = new MinecraftVersion("1.20.2");
|
||||
/**
|
||||
* Version 1.20 - the trails and tails update
|
||||
*/
|
||||
public static final MinecraftVersion TRAILS_AND_TAILS = new MinecraftVersion("1.20");
|
||||
|
||||
/**
|
||||
* Version 1.19.4 - the rest of the feature preview
|
||||
*/
|
||||
public static final MinecraftVersion FEATURE_PREVIEW_2 = new MinecraftVersion("1.19.4");
|
||||
|
||||
/**
|
||||
* Version 1.19.3 - introducing feature preview
|
||||
*/
|
||||
public static final MinecraftVersion FEATURE_PREVIEW_UPDATE = new MinecraftVersion("1.19.3");
|
||||
/**
|
||||
* Version 1.19 - the wild update
|
||||
*/
|
||||
public static final MinecraftVersion WILD_UPDATE = new MinecraftVersion("1.19");
|
||||
/**
|
||||
* Version 1.18 - caves and cliffs part 2
|
||||
*/
|
||||
public static final MinecraftVersion CAVES_CLIFFS_2 = new MinecraftVersion("1.18");
|
||||
/**
|
||||
* Version 1.17 - caves and cliffs part 1
|
||||
*/
|
||||
public static final MinecraftVersion CAVES_CLIFFS_1 = new MinecraftVersion("1.17");
|
||||
/**
|
||||
* Version 1.16.4
|
||||
*/
|
||||
public static final MinecraftVersion NETHER_UPDATE_4 = new MinecraftVersion("1.16.4");
|
||||
/**
|
||||
* Version 1.16.2 - breaking change to the nether update
|
||||
*/
|
||||
public static final MinecraftVersion NETHER_UPDATE_2 = new MinecraftVersion("1.16.2");
|
||||
/**
|
||||
* Version 1.16.0 - the nether update
|
||||
*/
|
||||
public static final MinecraftVersion NETHER_UPDATE = new MinecraftVersion("1.16");
|
||||
/**
|
||||
* Version 1.15 - the bee update
|
||||
*/
|
||||
public static final MinecraftVersion BEE_UPDATE = new MinecraftVersion("1.15");
|
||||
/**
|
||||
* Version 1.14 - village and pillage update.
|
||||
*/
|
||||
public static final MinecraftVersion VILLAGE_UPDATE = new MinecraftVersion("1.14");
|
||||
/**
|
||||
* Version 1.13 - update aquatic.
|
||||
*/
|
||||
public static final MinecraftVersion AQUATIC_UPDATE = new MinecraftVersion("1.13");
|
||||
/**
|
||||
* Version 1.12 - the world of color update.
|
||||
*/
|
||||
public static final MinecraftVersion COLOR_UPDATE = new MinecraftVersion("1.12");
|
||||
/**
|
||||
* Version 1.11 - the exploration update.
|
||||
*/
|
||||
public static final MinecraftVersion EXPLORATION_UPDATE = new MinecraftVersion("1.11");
|
||||
/**
|
||||
* Version 1.10 - the frostburn update.
|
||||
*/
|
||||
public static final MinecraftVersion FROSTBURN_UPDATE = new MinecraftVersion("1.10");
|
||||
/**
|
||||
* Version 1.9 - the combat update.
|
||||
*/
|
||||
public static final MinecraftVersion COMBAT_UPDATE = new MinecraftVersion("1.9");
|
||||
/**
|
||||
* Version 1.8 - the "bountiful" update.
|
||||
*/
|
||||
public static final MinecraftVersion BOUNTIFUL_UPDATE = new MinecraftVersion("1.8");
|
||||
/**
|
||||
* Version 1.7.8 - the update that changed the skin format (and distribution - R.I.P. player disguise)
|
||||
*/
|
||||
public static final MinecraftVersion SKIN_UPDATE = new MinecraftVersion("1.7.8");
|
||||
/**
|
||||
* Version 1.7.2 - the update that changed the world.
|
||||
*/
|
||||
public static final MinecraftVersion WORLD_UPDATE = new MinecraftVersion("1.7.2");
|
||||
/**
|
||||
* Version 1.6.1 - the horse update.
|
||||
*/
|
||||
public static final MinecraftVersion HORSE_UPDATE = new MinecraftVersion("1.6.1");
|
||||
/**
|
||||
* Version 1.5.0 - the redstone update.
|
||||
*/
|
||||
public static final MinecraftVersion REDSTONE_UPDATE = new MinecraftVersion("1.5.0");
|
||||
/**
|
||||
* Version 1.4.2 - the scary update (Wither Boss).
|
||||
*/
|
||||
public static final MinecraftVersion SCARY_UPDATE = new MinecraftVersion("1.4.2");
|
||||
|
||||
/**
|
||||
* The latest release version of minecraft.
|
||||
*/
|
||||
public static final MinecraftVersion LATEST = v1_20_4;
|
||||
|
||||
// used when serializing
|
||||
private static final long serialVersionUID = -8695133558996459770L;
|
||||
|
||||
/**
|
||||
* Regular expression used to parse version strings.
|
||||
*/
|
||||
private static final Pattern VERSION_PATTERN = Pattern.compile(".*\\(.*MC.\\s*([a-zA-z0-9\\-.]+).*");
|
||||
|
||||
/**
|
||||
* The current version of minecraft, lazy initialized by MinecraftVersion.currentVersion()
|
||||
*/
|
||||
private static MinecraftVersion currentVersion;
|
||||
|
||||
private final int major;
|
||||
private final int minor;
|
||||
private final int build;
|
||||
// The development stage
|
||||
private final String development;
|
||||
|
||||
// Snapshot?
|
||||
private final SnapshotVersion snapshot;
|
||||
private volatile Boolean atCurrentOrAbove;
|
||||
|
||||
/**
|
||||
* Determine the current Minecraft version.
|
||||
*
|
||||
* @param server - the Bukkit server that will be used to examine the MC version.
|
||||
*/
|
||||
public MinecraftVersion(Server server) {
|
||||
this(extractVersion(server.getVersion()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a version object from the format major.minor.build, or the snapshot format.
|
||||
*
|
||||
* @param versionOnly - the version in text form.
|
||||
*/
|
||||
public MinecraftVersion(String versionOnly) {
|
||||
this(versionOnly, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a version format from the standard release version or the snapshot verison.
|
||||
*
|
||||
* @param versionOnly - the version.
|
||||
* @param parseSnapshot - TRUE to parse the snapshot, FALSE otherwise.
|
||||
*/
|
||||
private MinecraftVersion(String versionOnly, boolean parseSnapshot) {
|
||||
String[] section = versionOnly.split("-");
|
||||
SnapshotVersion snapshot = null;
|
||||
int[] numbers = new int[3];
|
||||
|
||||
try {
|
||||
numbers = this.parseVersion(section[0]);
|
||||
} catch (NumberFormatException cause) {
|
||||
// Skip snapshot parsing
|
||||
if (!parseSnapshot) {
|
||||
throw cause;
|
||||
}
|
||||
|
||||
try {
|
||||
// Determine if the snapshot is newer than the current release version
|
||||
snapshot = new SnapshotVersion(section[0]);
|
||||
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd", Locale.US);
|
||||
|
||||
MinecraftVersion latest = new MinecraftVersion(MAXIMUM_MINECRAFT_VERSION, false);
|
||||
boolean newer = snapshot.getSnapshotDate().compareTo(
|
||||
format.parse(MINECRAFT_LAST_RELEASE_DATE)) > 0;
|
||||
|
||||
numbers[0] = latest.getMajor();
|
||||
numbers[1] = latest.getMinor() + (newer ? 1 : -1);
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("Cannot parse " + section[0], e);
|
||||
}
|
||||
}
|
||||
|
||||
this.major = numbers[0];
|
||||
this.minor = numbers[1];
|
||||
this.build = numbers[2];
|
||||
this.development = section.length > 1 ? section[1] : (snapshot != null ? "snapshot" : null);
|
||||
this.snapshot = snapshot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a version object directly.
|
||||
*
|
||||
* @param major - major version number.
|
||||
* @param minor - minor version number.
|
||||
* @param build - build version number.
|
||||
*/
|
||||
public MinecraftVersion(int major, int minor, int build) {
|
||||
this(major, minor, build, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a version object directly.
|
||||
*
|
||||
* @param major - major version number.
|
||||
* @param minor - minor version number.
|
||||
* @param build - build version number.
|
||||
* @param development - development stage.
|
||||
*/
|
||||
public MinecraftVersion(int major, int minor, int build, String development) {
|
||||
this.major = major;
|
||||
this.minor = minor;
|
||||
this.build = build;
|
||||
this.development = development;
|
||||
this.snapshot = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the Minecraft version from CraftBukkit itself.
|
||||
*
|
||||
* @param text - the server version in text form.
|
||||
* @return The underlying MC version.
|
||||
* @throws IllegalStateException If we could not parse the version string.
|
||||
*/
|
||||
public static String extractVersion(String text) {
|
||||
Matcher version = VERSION_PATTERN.matcher(text);
|
||||
|
||||
if (version.matches() && (version.group(1) != null)) {
|
||||
return version.group(1);
|
||||
} else {
|
||||
throw new IllegalStateException("Cannot parse version String '" + text + "'");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the given server version into a Minecraft version.
|
||||
*
|
||||
* @param serverVersion - the server version.
|
||||
* @return The resulting Minecraft version.
|
||||
*/
|
||||
public static MinecraftVersion fromServerVersion(String serverVersion) {
|
||||
return new MinecraftVersion(extractVersion(serverVersion));
|
||||
}
|
||||
|
||||
public static MinecraftVersion getCurrentVersion() {
|
||||
if (currentVersion == null) {
|
||||
currentVersion = fromServerVersion(Bukkit.getVersion());
|
||||
}
|
||||
|
||||
return currentVersion;
|
||||
}
|
||||
|
||||
public static void setCurrentVersion(MinecraftVersion version) {
|
||||
currentVersion = version;
|
||||
}
|
||||
|
||||
private static boolean atOrAbove(MinecraftVersion version) {
|
||||
return getCurrentVersion().isAtLeast(version);
|
||||
}
|
||||
|
||||
private int[] parseVersion(String version) {
|
||||
String[] elements = version.split("\\.");
|
||||
int[] numbers = new int[3];
|
||||
|
||||
// Make sure it's even a valid version
|
||||
if (elements.length < 1) {
|
||||
throw new IllegalStateException("Corrupt MC version: " + version);
|
||||
}
|
||||
|
||||
// The String 1 or 1.2 is interpreted as 1.0.0 and 1.2.0 respectively.
|
||||
for (int i = 0; i < Math.min(numbers.length, elements.length); i++) {
|
||||
numbers[i] = Integer.parseInt(elements[i].trim());
|
||||
}
|
||||
return numbers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Major version number
|
||||
*
|
||||
* @return Current major version number.
|
||||
*/
|
||||
public int getMajor() {
|
||||
return this.major;
|
||||
}
|
||||
|
||||
/**
|
||||
* Minor version number
|
||||
*
|
||||
* @return Current minor version number.
|
||||
*/
|
||||
public int getMinor() {
|
||||
return this.minor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build version number
|
||||
*
|
||||
* @return Current build version number.
|
||||
*/
|
||||
public int getBuild() {
|
||||
return this.build;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the development stage.
|
||||
*
|
||||
* @return Development stage, or NULL if this is a release.
|
||||
*/
|
||||
public String getDevelopmentStage() {
|
||||
return this.development;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the snapshot version, or NULL if this is a release.
|
||||
*
|
||||
* @return The snapshot version.
|
||||
*/
|
||||
public SnapshotVersion getSnapshot() {
|
||||
return this.snapshot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if this version is a snapshot.
|
||||
*
|
||||
* @return The snapshot version.
|
||||
*/
|
||||
public boolean isSnapshot() {
|
||||
return this.snapshot != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this version is at or above the current version the server is running.
|
||||
*
|
||||
* @return true if this version is equal or newer than the server version, false otherwise.
|
||||
*/
|
||||
public boolean atOrAbove() {
|
||||
if (this.atCurrentOrAbove == null) {
|
||||
this.atCurrentOrAbove = atOrAbove(this);
|
||||
}
|
||||
|
||||
return this.atCurrentOrAbove;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the version String (major.minor.build) only.
|
||||
*
|
||||
* @return A normal version string.
|
||||
*/
|
||||
public String getVersion() {
|
||||
if (this.getDevelopmentStage() == null) {
|
||||
return String.format("%s.%s.%s", this.getMajor(), this.getMinor(), this.getBuild());
|
||||
} else {
|
||||
return String.format("%s.%s.%s-%s%s", this.getMajor(), this.getMinor(), this.getBuild(),
|
||||
this.getDevelopmentStage(), this.isSnapshot() ? this.snapshot : "");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(MinecraftVersion o) {
|
||||
if (o == null) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return ComparisonChain.start()
|
||||
.compare(this.getMajor(), o.getMajor())
|
||||
.compare(this.getMinor(), o.getMinor())
|
||||
.compare(this.getBuild(), o.getBuild())
|
||||
.compare(this.getDevelopmentStage(), o.getDevelopmentStage(), Ordering.natural().nullsLast())
|
||||
.compare(this.getSnapshot(), o.getSnapshot(), Ordering.natural().nullsFirst())
|
||||
.result();
|
||||
}
|
||||
|
||||
public boolean isAtLeast(MinecraftVersion other) {
|
||||
if (other == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.compareTo(other) >= 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
if (obj == this) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (obj instanceof MinecraftVersion) {
|
||||
MinecraftVersion other = (MinecraftVersion) obj;
|
||||
|
||||
return (this.getMajor() == other.getMajor()) &&
|
||||
(this.getMinor() == other.getMinor()) &&
|
||||
(this.getBuild() == other.getBuild()) &&
|
||||
Objects.equals(this.getDevelopmentStage(), other.getDevelopmentStage());
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(this.getMajor(), this.getMinor(), this.getBuild());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
// Convert to a String that we can parse back again
|
||||
return String.format("(MC: %s)", this.getVersion());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package com.github.joelgodofwar.mmh.util;
|
||||
import java.io.Serializable;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import com.google.common.collect.ComparisonChain;
|
||||
|
||||
/**
|
||||
* Used to parse a snapshot version.
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
public class SnapshotVersion implements Comparable<SnapshotVersion>, Serializable {
|
||||
|
||||
private static final long serialVersionUID = 2778655372579322310L;
|
||||
private static final Pattern SNAPSHOT_PATTERN = Pattern.compile("(\\d{2}w\\d{2})([a-z])");
|
||||
|
||||
private final Date snapshotDate;
|
||||
private final int snapshotWeekVersion;
|
||||
|
||||
private transient String rawString;
|
||||
|
||||
public SnapshotVersion(String version) {
|
||||
Matcher matcher = SNAPSHOT_PATTERN.matcher(version.trim());
|
||||
|
||||
if (matcher.matches()) {
|
||||
try {
|
||||
this.snapshotDate = getDateFormat().parse(matcher.group(1));
|
||||
this.snapshotWeekVersion = matcher.group(2).charAt(0) - 'a';
|
||||
this.rawString = version;
|
||||
} catch (ParseException e) {
|
||||
throw new IllegalArgumentException("Date implied by snapshot version is invalid.", e);
|
||||
}
|
||||
} else {
|
||||
throw new IllegalArgumentException("Cannot parse " + version + " as a snapshot version.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the snapshot date parser.
|
||||
* <p>
|
||||
* We have to create a new instance of SimpleDateFormat every time as it is not thread safe.
|
||||
*
|
||||
* @return The date formatter.
|
||||
*/
|
||||
private static SimpleDateFormat getDateFormat() {
|
||||
SimpleDateFormat format = new SimpleDateFormat("yy'w'ww", Locale.US);
|
||||
format.setLenient(false);
|
||||
return format;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the snapshot version within a week, starting at zero.
|
||||
*
|
||||
* @return The weekly version
|
||||
*/
|
||||
public int getSnapshotWeekVersion() {
|
||||
return this.snapshotWeekVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the week this snapshot was released.
|
||||
*
|
||||
* @return The week.
|
||||
*/
|
||||
public Date getSnapshotDate() {
|
||||
return this.snapshotDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the raw snapshot string (yy'w'ww[a-z]).
|
||||
*
|
||||
* @return The snapshot string.
|
||||
*/
|
||||
public String getSnapshotString() {
|
||||
if (this.rawString == null) {
|
||||
// It's essential that we use the same locale
|
||||
Calendar current = Calendar.getInstance(Locale.US);
|
||||
current.setTime(this.snapshotDate);
|
||||
this.rawString = String.format("%02dw%02d%s",
|
||||
current.get(Calendar.YEAR) % 100,
|
||||
current.get(Calendar.WEEK_OF_YEAR),
|
||||
(char) ('a' + this.snapshotWeekVersion));
|
||||
}
|
||||
return this.rawString;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(SnapshotVersion o) {
|
||||
if (o == null) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return ComparisonChain.start()
|
||||
.compare(this.snapshotDate, o.getSnapshotDate())
|
||||
.compare(this.snapshotWeekVersion, o.getSnapshotWeekVersion())
|
||||
.result();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj == this) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (obj instanceof SnapshotVersion) {
|
||||
SnapshotVersion other = (SnapshotVersion) obj;
|
||||
return Objects.equals(this.snapshotDate, other.getSnapshotDate())
|
||||
&& (this.snapshotWeekVersion == other.getSnapshotWeekVersion());
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(this.snapshotDate, this.snapshotWeekVersion);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.getSnapshotString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package com.github.joelgodofwar.mmh.util;
|
||||
|
||||
import org.bukkit.ChatColor;
|
||||
|
||||
public class StrUtils {
|
||||
/**
|
||||
* Returns the rightmost characters of a string up to the specified length.
|
||||
*
|
||||
* @param input The input string.
|
||||
* @param chars The number of characters to retrieve from the right.
|
||||
* @return The rightmost characters of the input string, or the full string if its length is less than or equal to the specified length.
|
||||
*/
|
||||
public static String Right(String input, int chars){
|
||||
if (input.length() > chars){
|
||||
//System.out.println("Right input=" + input);
|
||||
return input.substring(input.length() - chars);
|
||||
}
|
||||
else{
|
||||
return input;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the leftmost characters of a string up to the specified length.
|
||||
*
|
||||
* @param input The input string.
|
||||
* @param chars The number of characters to retrieve from the left.
|
||||
* @return The leftmost characters of the input string, or the full string if its length is less than or equal to the specified length.
|
||||
*/
|
||||
public static String Left(String input, int chars){
|
||||
if (input.length() > chars){
|
||||
//System.out.println("Left input=" + input);
|
||||
//System.out.println("Left chars=" + chars);
|
||||
return input.substring(0, chars);
|
||||
}
|
||||
else{
|
||||
return input;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a string contains a specified substring, ignoring case.
|
||||
*
|
||||
* @param string The input string to search within (comma-separated values).
|
||||
* @param string2 The substring to check for.
|
||||
* @return True if the substring is found in the input string; otherwise, false.
|
||||
*/
|
||||
public static boolean stringContains(String string, String string2){
|
||||
String[] string3 = string.toUpperCase().split(", ");
|
||||
for(int i = 0; i < string3.length; i++){
|
||||
if(string3[i].equals(string2.toUpperCase())){
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two strings for equality after removing color codes.
|
||||
*
|
||||
* @param string1 The first string to compare.
|
||||
* @param string2 The second string to compare.
|
||||
* @return True if the strings are equal (ignoring color codes); otherwise, false.
|
||||
*/
|
||||
public static boolean stringEquals(String string1, String string2) {
|
||||
//string1 = ChatColor.stripColor(string1);
|
||||
//string2 = ChatColor.stripColor(string2);
|
||||
if(ChatColor.stripColor(string1).equals(ChatColor.stripColor(string2))) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the first character of a string to uppercase and the remaining characters to lowercase,
|
||||
* creating a title case format.
|
||||
*
|
||||
* @param input The input string to convert to title case.
|
||||
* @return The input string in Title Case.
|
||||
*/
|
||||
public static String toTitleCase(String input) {
|
||||
if ((input == null) || input.isEmpty()) {
|
||||
return input;
|
||||
}
|
||||
|
||||
return input.substring(0, 1).toUpperCase() + input.substring(1).toLowerCase();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package com.github.joelgodofwar.mmh.util;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
|
||||
import org.apache.commons.lang.math.NumberUtils;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
public class UpdateChecker {
|
||||
private static int project;
|
||||
private URL checkURL;
|
||||
private String newVersion;
|
||||
private String newMinVers;
|
||||
private String oldVersion;
|
||||
private String oldMinVers;
|
||||
private JavaPlugin plugin;
|
||||
String[] strVersionNew; // [0]=1.14 [1]=1.0.0.?
|
||||
String[] strVersionCurrent; // [0]=1.14 [1]=1.0.0.?
|
||||
|
||||
|
||||
public UpdateChecker(JavaPlugin plugin, int projectID, String verURL) {
|
||||
this.plugin = plugin;
|
||||
project = projectID;
|
||||
newVersion = plugin.getDescription().getVersion();
|
||||
try {
|
||||
checkURL = new URL(verURL.replace("{vers}", newVersion.substring(0, 4)));
|
||||
}catch(MalformedURLException e) {
|
||||
Bukkit.getLogger().warning(Ansi.RED + "Could not connect to update server.");
|
||||
//Bukkit.getPluginManager().disablePlugin(plugin);
|
||||
}
|
||||
}
|
||||
public UpdateChecker(String plugin, int projectID, String verURL) {
|
||||
newVersion = plugin;
|
||||
try {
|
||||
checkURL = new URL(verURL.replace("{vers}", newVersion.substring(0, 4)));
|
||||
}catch(MalformedURLException e) {
|
||||
Bukkit.getLogger().warning(Ansi.RED + "Could not connect to update server.");
|
||||
//Bukkit.getPluginManager().disablePlugin(plugin);
|
||||
}
|
||||
}
|
||||
public static String getResourceUrl() {return "https://spigotmc.org/resources/" + project;}
|
||||
public boolean checkForUpdates() throws Exception {
|
||||
boolean isOutdated = false;
|
||||
|
||||
URLConnection con = checkURL.openConnection();
|
||||
con.setConnectTimeout(30000);
|
||||
con.setReadTimeout(15000);
|
||||
newVersion = new BufferedReader(new InputStreamReader(con.getInputStream())).readLine();
|
||||
strVersionNew = newVersion.split("_"); // Split into minimum MC version and version
|
||||
|
||||
newMinVers = strVersionNew[0];
|
||||
newVersion = strVersionNew[1];
|
||||
//System.out.println("newVersion=" + newVersion);
|
||||
Version newVers = new Version(newVersion);
|
||||
|
||||
strVersionCurrent = plugin.getDescription().getVersion().split("_");
|
||||
|
||||
oldMinVers = strVersionCurrent[0];
|
||||
oldVersion = strVersionCurrent[1];
|
||||
//System.out.println("oldVersion=" + oldVersion);
|
||||
Version oldVers = new Version(oldVersion);
|
||||
|
||||
if(oldVers.isDev()){ /** If currently on Dev version, ignore build -1 from Patch */
|
||||
if(newVers.Major() > oldVers.Major()){isOutdated = true;}
|
||||
if(newVers.Major() == oldVers.Major()){/** same do nothing */}
|
||||
if(newVers.Minor() > oldVers.Minor()){isOutdated = true;}
|
||||
else if(newVers.Minor() == oldVers.Minor()){/** same do nothing */}
|
||||
if(newVers.Patch() > (oldVers.Patch() - 1)){isOutdated = true;}
|
||||
else if(newVers.Patch() == (oldVers.Patch() - 1)){/** same do nothing */}
|
||||
/**if(newVers.Build() > oldVers.Build()){isOutdated = true;}
|
||||
else if(newVers.Build() == oldVers.Build()){/** same do nothing }*/
|
||||
}else {
|
||||
if(newVers.Major() > oldVers.Major()){isOutdated = true;}
|
||||
if(newVers.Major() == oldVers.Major()){/** same do nothing */}
|
||||
if(newVers.Minor() > oldVers.Minor()){isOutdated = true;}
|
||||
else if(newVers.Minor() == oldVers.Minor()){/** same do nothing */}
|
||||
if(newVers.Patch() > oldVers.Patch()){isOutdated = true;}
|
||||
else if(newVers.Patch() == oldVers.Patch()){/** same do nothing */}
|
||||
if(newVers.Build() > oldVers.Build()){isOutdated = true;}
|
||||
else if(newVers.Build() == oldVers.Build()){/** same do nothing */}
|
||||
}
|
||||
return isOutdated; //plugin.getDescription().getVersion().equals(newVersion); TODO:
|
||||
}
|
||||
public String newVersion(){
|
||||
return newMinVers + "_" + newVersion;
|
||||
}
|
||||
public String oldVersion(){
|
||||
return oldMinVers + "_" + oldVersion;
|
||||
}
|
||||
|
||||
public class Version{
|
||||
private int Major; // 1
|
||||
private int Minor; // 16
|
||||
private int Patch; // 1
|
||||
private int Build; // ?
|
||||
private boolean isDev = false;
|
||||
private String[] string2 = {"0","0","0","0"};
|
||||
public Version(String string){
|
||||
//System.out.println("string=" + string);
|
||||
string2 = string.split("\\.");
|
||||
//for (String a : string2)
|
||||
//System.out.println(a);
|
||||
//System.out.println("string2=" + string2.toString());
|
||||
//System.out.println("string2.length=" + string2.length);
|
||||
this.Major = NumberUtils.toInt(string2[0]);
|
||||
this.Minor = NumberUtils.toInt(string2[1]);
|
||||
this.Patch = NumberUtils.toInt(string2[2]);
|
||||
if(string2.length >= 4){
|
||||
if(string2[3].toUpperCase().contains("D")){
|
||||
isDev = true;
|
||||
string2[3] = string2[3].toUpperCase().replace("D", "");
|
||||
}
|
||||
this.Build = NumberUtils.toInt(string2[3]);
|
||||
}else{
|
||||
this.Build = 0;
|
||||
}
|
||||
}
|
||||
public int Major(){return Major;}
|
||||
public int Minor(){return Minor;}
|
||||
public int Patch(){return Patch;}
|
||||
public int Build(){return Build;}
|
||||
public boolean isDev(){return isDev;}
|
||||
}
|
||||
}
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
package com.github.joelgodofwar.mmh.util;
|
||||
|
||||
import org.apache.commons.lang.math.NumberUtils;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.OfflinePlayer;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import me.clip.placeholderapi.PlaceholderAPI;
|
||||
|
||||
public class Utils {
|
||||
|
||||
public static void sendJson(CommandSender player, String string){
|
||||
Bukkit.getServer().dispatchCommand(Bukkit.getConsoleSender(), "tellraw \"" + player.getName() + "\" " + string);
|
||||
}
|
||||
|
||||
public static void sendJson(Player player, String string){
|
||||
Bukkit.getServer().dispatchCommand(Bukkit.getConsoleSender(), "tellraw \"" + player.getName() + "\" " + string);
|
||||
}
|
||||
|
||||
public String parsePAPI(String string) {
|
||||
if(Bukkit.getServer().getPluginManager().getPlugin("PlaceholderAPI") != null){
|
||||
return PlaceholderAPI.setPlaceholders(null, string);
|
||||
}else {
|
||||
return string;
|
||||
}
|
||||
}
|
||||
|
||||
public String parsePAPI(Player player, String string) {
|
||||
if(Bukkit.getServer().getPluginManager().getPlugin("PlaceholderAPI") != null){
|
||||
return PlaceholderAPI.setPlaceholders(player, string);
|
||||
}else {
|
||||
return string;
|
||||
}
|
||||
}
|
||||
|
||||
public String parsePAPI(OfflinePlayer player, String string) {
|
||||
if(Bukkit.getServer().getPluginManager().getPlugin("PlaceholderAPI") != null){
|
||||
return PlaceholderAPI.setPlaceholders(player, string);
|
||||
}else {
|
||||
return string;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the server version is supported based on minimum Minecraft version and build number.
|
||||
*
|
||||
* @param minMCVersion Minimum Minecraft version in the format "Major.Minor.BugFix.Build" (e.g., "1.20.2.3936").
|
||||
* @return True if the server version is not supported; otherwise, false.
|
||||
*/
|
||||
public static boolean isSupportedVersion(String minMCVersion) {
|
||||
// 3936-Spigot-dba3cdc-5a2d905 (MC: 1.20.2)
|
||||
// 0 1 2
|
||||
//String[] minVersion = minMCVersion.split("\\.");
|
||||
Version minVersion = new Version(minMCVersion);
|
||||
|
||||
String serverVersion = Bukkit.getVersion(); // Get the server version string
|
||||
String[] versionParts = serverVersion.split(" "); // Split by space to get the MC version and build number
|
||||
String daVersion = versionParts[2].replace(")", ".").concat(serverVersion.split("-")[0]);
|
||||
/**System.out.println("Bukkit.getVersion()=" + Bukkit.getVersion());
|
||||
System.out.println("versionParts.length=" + versionParts.length);
|
||||
System.out.println("versionParts.0=" + versionParts[0]);
|
||||
System.out.println("versionParts.1=" + versionParts[1]);
|
||||
System.out.println("versionParts.1=" + versionParts[2]);//*/
|
||||
Version checkVersion = new Version(daVersion);
|
||||
|
||||
if(minVersion.Major() > checkVersion.Major()){return true;}
|
||||
if(minVersion.Major() == checkVersion.Major()){/** same do nothing */}
|
||||
if(minVersion.Minor() > checkVersion.Minor()){return true;}
|
||||
else if(minVersion.Minor() == checkVersion.Minor()){/** same do nothing */}
|
||||
if(minVersion.Patch() > checkVersion.Patch()){return true;}
|
||||
else if(minVersion.Patch() == checkVersion.Patch()){/** same do nothing */}
|
||||
if(minVersion.Build() > checkVersion.Build()){return true;}
|
||||
else if(minVersion.Build() == checkVersion.Build()){/** same do nothing */}
|
||||
|
||||
return true; // Server version is not supported
|
||||
}
|
||||
|
||||
public static class Version{
|
||||
private int Major; // 1
|
||||
private int Minor; // 16
|
||||
private int Patch; // 1
|
||||
private int Build; // ?
|
||||
private boolean isDev = false;
|
||||
private String[] string2 = {"0","0","0","0"};
|
||||
public Version(String string){
|
||||
//System.out.println("string=" + string);
|
||||
string2 = string.split("\\.");
|
||||
/**for (String a : string2) {
|
||||
System.out.println(a);
|
||||
System.out.println("string2=" + string2.toString());
|
||||
System.out.println("string2.length=" + string2.length);
|
||||
}//*/
|
||||
this.Major = NumberUtils.toInt(string2[0]);
|
||||
this.Minor = NumberUtils.toInt(string2[1]);
|
||||
this.Patch = NumberUtils.toInt(string2[2]);
|
||||
if(string2.length >= 4){
|
||||
if(string2[3].toUpperCase().contains("D")){
|
||||
isDev = true;
|
||||
string2[3] = string2[3].toUpperCase().replace("D", "");
|
||||
}
|
||||
this.Build = NumberUtils.toInt(string2[3]);
|
||||
}else{
|
||||
this.Build = 0;
|
||||
}
|
||||
}
|
||||
public int Major(){return Major;}
|
||||
public int Minor(){return Minor;}
|
||||
public int Patch(){return Patch;}
|
||||
public int Build(){return Build;}
|
||||
public boolean isDev(){return isDev;}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package com.github.joelgodofwar.mmh.util;
|
||||
|
||||
import com.github.joelgodofwar.mmh.MoreMobHeads;
|
||||
|
||||
/**
|
||||
* Utility class for verifying and retrieving values from a FileConfiguration.
|
||||
*/
|
||||
public class VerifyConfig {
|
||||
MoreMobHeads mmh;
|
||||
|
||||
/**
|
||||
* Constructs a VerifyConfig object with the given FileConfiguration.
|
||||
*
|
||||
* @param config The FileConfiguration to be verified.
|
||||
*/
|
||||
public VerifyConfig(final MoreMobHeads plugin) {
|
||||
this.mmh = plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a string value from the configuration for the specified path.
|
||||
* If the value is null, the specified defaultValue is returned.
|
||||
*
|
||||
* @param path The path to the configuration value.
|
||||
* @param defaultValue The default value to be returned if the specified path is null.
|
||||
* @return The retrieved string value or the defaultValue if null.
|
||||
*/
|
||||
public String getString(String path, String defaultValue ) {
|
||||
try {
|
||||
String stringValue = mmh.getConfig().getString(path, null);
|
||||
if (stringValue == null) {
|
||||
throw new NullPointerException("Could not read value of: " + path);
|
||||
}
|
||||
return stringValue;
|
||||
}catch (NullPointerException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a boolean value from the configuration for the specified path.
|
||||
* If the value is null, the specified defaultValue is returned.
|
||||
*
|
||||
* @param path The path to the configuration value.
|
||||
* @param defaultValue The default value to be returned if the specified path is null.
|
||||
* @return The retrieved boolean value or the defaultValue if null.
|
||||
*/
|
||||
public boolean getBoolean(String path, boolean defaultValue ) {
|
||||
try {
|
||||
String stringValue = mmh.getConfig().getString(path, null);
|
||||
if (stringValue == null) {
|
||||
throw new NullPointerException("Could not read value of: " + path);
|
||||
}
|
||||
return Boolean.parseBoolean(stringValue);
|
||||
}catch (NullPointerException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves an integer value from the configuration for the specified path.
|
||||
* If the value is null, the specified defaultValue is returned.
|
||||
*
|
||||
* @param path The path to the configuration value.
|
||||
* @param defaultValue The default value to be returned if the specified path is null.
|
||||
* @return The retrieved integer value or the defaultValue if null.
|
||||
*/
|
||||
public int getInt(String path, int defaultValue ) {
|
||||
try {
|
||||
String stringValue = mmh.getConfig().getString(path, null);
|
||||
if (stringValue == null) {
|
||||
throw new NullPointerException("Could not read value of: " + path);
|
||||
}
|
||||
return Integer.parseInt(stringValue);
|
||||
}catch (NullPointerException | NumberFormatException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public boolean checkString(String path){
|
||||
try {
|
||||
String stringValue = mmh.getConfig().getString(path, null);
|
||||
if(stringValue != null) {
|
||||
return true;
|
||||
}else {
|
||||
return false;
|
||||
}
|
||||
}catch(Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void verifyValues() {
|
||||
mmh.LOGGER.log("plugin_settings.auto_update_check " + getBoolean("plugin_settings.auto_update_check", true));
|
||||
mmh.LOGGER.log("plugin_settings.debug " + getBoolean("plugin_settings.debug", false));
|
||||
mmh.LOGGER.log("plugin_settings.lang " + getString("plugin_settings.lang", "en_US"));
|
||||
mmh.LOGGER.log("global_settings.console.colorful_console " + getBoolean("global_settings.console.colorful_console", true));
|
||||
mmh.LOGGER.log("global_settings.console.silent_console " + getBoolean("global_settings.console.silent_console", true));
|
||||
mmh.LOGGER.log("global_settings.console.longpluginname " + getBoolean("global_settings.console.longpluginname", true));
|
||||
mmh.LOGGER.log("global_settings.world.whitelist " + getString("global_settings.world.whitelist", ""));
|
||||
mmh.LOGGER.log("global_settings.world.blacklist " + getString("global_settings.world.blacklist", ""));
|
||||
mmh.LOGGER.log("global_settings.event.piston_extend " + getBoolean("global_settings.event.piston_extend", true));
|
||||
mmh.LOGGER.log("head_settings.apply_looting " + getBoolean("head_settings.apply_looting", true));
|
||||
mmh.LOGGER.log("head_settings.lore.show_killer " + getBoolean("head_settings.lore.show_killer", true));
|
||||
mmh.LOGGER.log("head_settings.lore.show_plugin_name " + getBoolean("head_settings.lore.show_plugin_name", true));
|
||||
mmh.LOGGER.log("head_settings.player_heads.announce_kill.enabled" + getBoolean("head_settings.player_heads.announce_kill.enabled", false));
|
||||
mmh.LOGGER.log("head_settings.player_heads.announce_kill.displayname " + getBoolean("head_settings.player_heads.announce_kill.displayname", true));
|
||||
mmh.LOGGER.log("head_settings.player_heads.whitelist.enforce " + getBoolean("head_settings.player_heads.whitelist.enforce", true));
|
||||
mmh.LOGGER.log("head_settings.player_heads.whitelist.player_head_whitelist " + getString("head_settings.player_heads.whitelist.player_head_whitelist", ""));
|
||||
mmh.LOGGER.log("head_settings.player_heads.blacklist.enforce " + getBoolean("head_settings.player_heads.blacklist.enforce", true));
|
||||
mmh.LOGGER.log("head_settings.player_heads.blacklist.player_head_blacklist " + getString("head_settings.player_heads.blacklist.player_head_blacklist", ""));
|
||||
mmh.LOGGER.log("head_settings.mob_heads.announce_kill.enabled " + getBoolean("head_settings.mob_heads.announce_kill.enabled", false));
|
||||
mmh.LOGGER.log("head_settings.mob_heads.announce_kill.displayname " + getBoolean("head_settings.mob_heads.announce_kill.displayname", true));
|
||||
mmh.LOGGER.log("head_settings.mob_heads.whitelist " + getString("head_settings.mob_heads.whitelist", ""));
|
||||
mmh.LOGGER.log("head_settings.mob_heads.blacklist " + getString("head_settings.mob_heads.blacklist", ""));
|
||||
mmh.LOGGER.log("head_settings.mob_heads.nametag " + getBoolean("head_settings.mob_heads.nametag", false));
|
||||
mmh.LOGGER.log("head_settings.mob_heads.vanilla_heads.creepers " + getBoolean("head_settings.mob_heads.vanilla_heads.creepers", true));
|
||||
mmh.LOGGER.log("head_settings.mob_heads.vanilla_heads.ender_dragon " + getBoolean("head_settings.mob_heads.vanilla_heads.ender_dragon", true));
|
||||
mmh.LOGGER.log("head_settings.mob_heads.vanilla_heads.piglin " + getBoolean("head_settings.mob_heads.vanilla_heads.piglin", true));
|
||||
mmh.LOGGER.log("head_settings.mob_heads.vanilla_heads.skeleton " + getBoolean("head_settings.mob_heads.vanilla_heads.skeleton", true));
|
||||
mmh.LOGGER.log("head_settings.mob_heads.vanilla_heads.wither_skeleto " + getBoolean("head_settings.mob_heads.vanilla_heads.wither_skeleton", true));
|
||||
mmh.LOGGER.log("head_settings.mob_heads.vanilla_heads.zombie " + getBoolean("head_settings.mob_heads.vanilla_heads.zombie", true));
|
||||
mmh.LOGGER.log("wandering_trades.custom_wandering_trader " + getBoolean("wandering_trades.custom_wandering_trader", true));
|
||||
mmh.LOGGER.log("wandering_trades.keep_default_trades " + getBoolean("wandering_trades.keep_default_trades", true));
|
||||
mmh.LOGGER.log("wandering_trades.player_heads.enabled " + getBoolean("wandering_trades.player_heads.enabled", true));
|
||||
mmh.LOGGER.log("wandering_trades.player_heads.min " + getInt("wandering_trades.player_heads.min", 0));
|
||||
mmh.LOGGER.log("wandering_trades.player_heads.max " + getInt("wandering_trades.player_heads.max", 5));
|
||||
mmh.LOGGER.log("wandering_trades.block_heads.enabled " + getBoolean("wandering_trades.block_heads.enabled", true));
|
||||
mmh.LOGGER.log("wandering_trades.block_heads.pre_116.min" + getInt("wandering_trades.block_heads.pre_116.min", 0));
|
||||
mmh.LOGGER.log("wandering_trades.block_heads.pre_116.max " + getInt("wandering_trades.block_heads.pre_116.max", 5));
|
||||
mmh.LOGGER.log("wandering_trades.block_heads.is_116.min" + getInt("wandering_trades.block_heads.is_116.min", 0));
|
||||
mmh.LOGGER.log("wandering_trades.block_heads.is_116.max " + getInt("wandering_trades.block_heads.is_116.max", 5));
|
||||
mmh.LOGGER.log("wandering_trades.block_heads.is_117.min " + getInt("wandering_trades.block_heads.is_117.min", 0));
|
||||
mmh.LOGGER.log("wandering_trades.block_heads.is_117.max " + getInt("wandering_trades.block_heads.is_117.max", 5));
|
||||
mmh.LOGGER.log("wandering_trades.custom_trades.enabled " + getBoolean("wandering_trades.custom_trades.enabled", true));
|
||||
mmh.LOGGER.log("wandering_trades.custom_trades.min " + getInt("wandering_trades.custom_trades.min", 0));
|
||||
mmh.LOGGER.log("wandering_trades.custom_trades.max " + getInt("wandering_trades.custom_trades.max", 5));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
package com.github.joelgodofwar.mmh.util;
|
||||
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
|
||||
import com.github.joelgodofwar.mmh.MoreMobHeads;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class VersionChecker {
|
||||
private String pluginName;
|
||||
private JavaPlugin plugin;
|
||||
private int projectID;
|
||||
private String currentVersion;
|
||||
private String githubURL; // = "https://github.com/JoelGodOfwar/MoreMobHeads/raw/master/versioncheck/1.15/versions.xml";
|
||||
String versionType;
|
||||
private List<String> releaseList = new ArrayList<>();
|
||||
private List<String> developerList = new ArrayList<>();
|
||||
private String recommendedVersion = "uptodate";
|
||||
private MoreMobHeads mmh;
|
||||
|
||||
public VersionChecker(MoreMobHeads plugin, int projectID, String githubURL) {
|
||||
this.mmh = plugin;
|
||||
this.projectID = projectID;
|
||||
this.currentVersion = plugin.getDescription().getVersion();
|
||||
this.githubURL = githubURL;
|
||||
}
|
||||
|
||||
public VersionChecker(String plugin, int projectID, String githubURL) {
|
||||
this.currentVersion = plugin;
|
||||
this.projectID = projectID;
|
||||
this.githubURL = githubURL;
|
||||
}
|
||||
|
||||
public String getReleaseUrl() {return "https://dev.bukkit.org/projects/moremobheads2";}
|
||||
|
||||
public boolean checkForUpdates() throws Exception {
|
||||
URL url = new URL(githubURL);
|
||||
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
|
||||
DocumentBuilder db = dbf.newDocumentBuilder();
|
||||
URLConnection connection = url.openConnection();
|
||||
connection.setUseCaches(false);
|
||||
connection.setRequestProperty("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||
Document doc = db.parse(connection.getInputStream());
|
||||
doc.getDocumentElement().normalize();
|
||||
NodeList releaseNodes = doc.getElementsByTagName("release");
|
||||
for (int i = 0; i < releaseNodes.getLength(); i++) {
|
||||
Node node = releaseNodes.item(i);
|
||||
if (node.getNodeType() == Node.ELEMENT_NODE) {
|
||||
Element element = (Element) node;
|
||||
releaseList.add(element.getElementsByTagName("version").item(0).getTextContent().replace("<version>", "").replace("</version>", ""));
|
||||
releaseList.add(element.getElementsByTagName("notes").item(0).getTextContent().replace("<notes>", "").replace("</notes>", ""));
|
||||
releaseList.add(element.getElementsByTagName("link").item(0).getTextContent().replace("<link>", "").replace("</link>", ""));
|
||||
}
|
||||
}
|
||||
NodeList developerNodes = doc.getElementsByTagName("developer");
|
||||
for (int i = 0; i < developerNodes.getLength(); i++) {
|
||||
Node node = developerNodes.item(i);
|
||||
if (node.getNodeType() == Node.ELEMENT_NODE) {
|
||||
Element element = (Element) node;
|
||||
developerList.add(element.getElementsByTagName("version").item(0).getTextContent().replace("<version>", "").replace("</version>", ""));
|
||||
developerList.add(element.getElementsByTagName("notes").item(0).getTextContent().replace("<notes>", "").replace("</notes>", ""));
|
||||
developerList.add(element.getElementsByTagName("link").item(0).getTextContent().replace("<link>", "").replace("</link>", ""));
|
||||
}
|
||||
}
|
||||
|
||||
if (connection != null) {
|
||||
connection.getInputStream().close();
|
||||
}
|
||||
String releaseVersion = releaseList.get(0);
|
||||
String developerVersion = developerList.get(0);
|
||||
mmh.LOGGER.debug(ChatColor.RED + "currentVersion=" + currentVersion);
|
||||
mmh.LOGGER.debug(ChatColor.RED + "releaseVersion=" + releaseVersion);
|
||||
mmh.LOGGER.debug(ChatColor.RED + "developerVersion=" + developerVersion);
|
||||
if (currentVersion.compareTo(releaseVersion) < 0) {
|
||||
recommendedVersion = "release";
|
||||
return true;
|
||||
} else if (currentVersion.equals(releaseVersion)) {
|
||||
recommendedVersion = "uptodate";
|
||||
return false;
|
||||
} else if (currentVersion.contains(".D")) {
|
||||
String[] splitCurrentVersion = currentVersion.split("\\.D");
|
||||
String currentReleaseVersion = splitCurrentVersion[0];
|
||||
String[] splitDeveloperVersion = developerVersion.split("\\.D");
|
||||
String developerReleaseVersion = splitDeveloperVersion[0];
|
||||
if (currentReleaseVersion.equals(releaseVersion) && (developerReleaseVersion.compareTo(currentReleaseVersion) <= 0)) {
|
||||
recommendedVersion = "release";
|
||||
return true;
|
||||
} else if ((developerReleaseVersion.compareTo(releaseVersion) < 0) && developerReleaseVersion.equals(currentReleaseVersion)) {
|
||||
recommendedVersion = "You are Up To Date";
|
||||
return false;
|
||||
} else if (developerVersion.compareTo(currentVersion) > 0) {
|
||||
recommendedVersion = "developer";
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
public List<String> getReleaseList() {
|
||||
return releaseList;
|
||||
}
|
||||
public List<String> getDeveloperList() {
|
||||
return developerList;
|
||||
}
|
||||
public String getRecommendedVersion() {
|
||||
return recommendedVersion;
|
||||
}
|
||||
public String oldVersion() {
|
||||
return currentVersion;
|
||||
}
|
||||
public String newVersion() {
|
||||
if(recommendedVersion.equalsIgnoreCase("release")) {
|
||||
return releaseList.get(0);
|
||||
}else if(recommendedVersion.equalsIgnoreCase("developer")) {
|
||||
return developerList.get(0);
|
||||
}else {
|
||||
return "UpToDate";
|
||||
}
|
||||
}
|
||||
public String newVersionNotes() {
|
||||
if (recommendedVersion.equalsIgnoreCase("release")) {
|
||||
return releaseList.get(1);
|
||||
} else if (recommendedVersion.equalsIgnoreCase("developer")) {
|
||||
return developerList.get(1);
|
||||
} else {
|
||||
return "UpToDate";
|
||||
}
|
||||
}
|
||||
public String getDownloadLink() {
|
||||
if (recommendedVersion.equalsIgnoreCase("release")) {
|
||||
return releaseList.get(2);//"https://www.spigotmc.org/resources/no-enderman-grief2.71236/history";
|
||||
} else if (recommendedVersion.equalsIgnoreCase("developer")) {
|
||||
return developerList.get(2);//"https://discord.com/channels/654087250665144321/654444482372173875";
|
||||
} else {
|
||||
return "UpToDate";
|
||||
}
|
||||
}
|
||||
|
||||
/**public boolean checkForUpdates2() {
|
||||
if(currentVersion.contains(".D")){
|
||||
versionType = "developer";
|
||||
}else{
|
||||
versionType = "release";
|
||||
}
|
||||
String[] versionArray = getLatestVersion(versionType);
|
||||
|
||||
if (versionArray == null) {
|
||||
Bukkit.getLogger().warning(Ansi.RED + "Could not connect to update server.");
|
||||
return false;
|
||||
}
|
||||
|
||||
String latestVersion = versionArray[0];
|
||||
String latestNotes = versionArray[1];
|
||||
|
||||
if (latestVersion.equals(currentVersion)) {
|
||||
return false;
|
||||
} else {
|
||||
System.out.println("There is a new version of " + pluginName + " available!");
|
||||
System.out.println("Your version: " + currentVersion);
|
||||
System.out.println("Latest version: " + latestVersion);
|
||||
System.out.println("Update notes: " + latestNotes);
|
||||
System.out.println("Please update to the newest version.");
|
||||
System.out.println("Download: https://www.spigotmc.org/resources/" + pluginName + ".75749/history");
|
||||
System.out.println("Donate: <donation link>");
|
||||
return true;
|
||||
}
|
||||
}//*/
|
||||
|
||||
/**private String[] getLatestVersion(String versionType) {
|
||||
try {
|
||||
URL url = new URL(githubURL);
|
||||
InputStream stream = url.openStream();
|
||||
Scanner scanner = new Scanner(stream);
|
||||
scanner.useDelimiter("\\Z");
|
||||
String xmlContent = scanner.next();
|
||||
scanner.close();
|
||||
stream.close();
|
||||
|
||||
String versionTag = "<" + versionType + ">";
|
||||
String notesTag = "<" + versionType + "-notes>";
|
||||
String endTag = "</" + versionType + ">";
|
||||
|
||||
int versionStartIndex = xmlContent.indexOf(versionTag);
|
||||
int notesStartIndex = xmlContent.indexOf(notesTag);
|
||||
int versionEndIndex = xmlContent.indexOf(endTag);
|
||||
|
||||
if (versionStartIndex == -1 || notesStartIndex == -1 || versionEndIndex == -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String versionNumber = xmlContent.substring(versionStartIndex + versionTag.length(), versionEndIndex);
|
||||
String notes = xmlContent.substring(notesStartIndex + notesTag.length(), xmlContent.indexOf(endTag + "-notes>"));
|
||||
String[] versionArray = new String[2];
|
||||
versionArray[0] = versionNumber;
|
||||
versionArray[1] = notes;
|
||||
return versionArray;
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}//*/
|
||||
|
||||
/**public static void main(String[] args) {
|
||||
//String pluginName = "MyPlugin";
|
||||
String currentVersion = "1.14_1.0.4";
|
||||
String githubURL = "https://github.com/JoelGodOfwar/NoEndermanGrief/raw/master/versions/1.14/versions.xml";
|
||||
VersionChecker checker = new VersionChecker(currentVersion, 71236, githubURL);
|
||||
checker.checkForUpdates();
|
||||
}//*/
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package com.github.joelgodofwar.mmh.util;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.Reader;
|
||||
import java.io.Writer;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import org.apache.commons.lang.Validate;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.configuration.InvalidConfigurationException;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.bukkit.configuration.file.YamlConstructor;
|
||||
import org.bukkit.configuration.file.YamlRepresenter;
|
||||
import org.yaml.snakeyaml.DumperOptions;
|
||||
import org.yaml.snakeyaml.Yaml;
|
||||
import org.yaml.snakeyaml.representer.Representer;
|
||||
|
||||
import com.google.common.base.Charsets;
|
||||
|
||||
import jdk.internal.joptsimple.internal.Strings;
|
||||
|
||||
/*
|
||||
* @author: Aoife (Josh)
|
||||
* @date: 2020-04-25
|
||||
* @project: WarpConverter
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
public class YmlConfiguration extends YamlConfiguration {
|
||||
|
||||
private final DumperOptions yamlOptions = new DumperOptions();
|
||||
private final Representer yamlRepresenter = new YamlRepresenter();
|
||||
private final Yaml yaml = new Yaml(new YamlConstructor(), yamlRepresenter, yamlOptions);
|
||||
private Map<Integer, String> commentContainer = new HashMap<>();
|
||||
|
||||
@Override public void save(File file) throws IOException {
|
||||
Validate.notNull(file);
|
||||
try (Writer writer = new OutputStreamWriter(new FileOutputStream(file), Charsets.UTF_8)) {
|
||||
writer.write(saveToString());
|
||||
}
|
||||
}
|
||||
|
||||
public static void saveConfig(File file, YmlConfiguration config) {
|
||||
try {
|
||||
config.save(file);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String saveToString(){
|
||||
yamlOptions.setIndent(options().indent());
|
||||
yamlOptions.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
|
||||
yamlRepresenter.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
|
||||
String header = "";//buildHeader();
|
||||
String dump = yaml.dump(getValues(false));
|
||||
|
||||
if (dump.equals(BLANK_CONFIG)) {
|
||||
dump = Strings.EMPTY;
|
||||
} else {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
int line = 0;
|
||||
for(String s : dump.split("\n")) {
|
||||
line++;
|
||||
while (commentContainer.containsKey(line)) {
|
||||
sb.append(commentContainer.get(line)).append("\n");
|
||||
line++;
|
||||
}
|
||||
sb.append(s).append("\n");
|
||||
}
|
||||
|
||||
dump = sb.toString();
|
||||
}
|
||||
return header.length() > 0 ? header + dump : dump;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void load(Reader reader) throws IOException, InvalidConfigurationException {
|
||||
BufferedReader input = reader instanceof BufferedReader ? (BufferedReader) reader : new BufferedReader(reader);
|
||||
StringBuilder builder = new StringBuilder();
|
||||
try {
|
||||
String line;
|
||||
int count = 0;
|
||||
while ((line = input.readLine()) != null) {
|
||||
count++;
|
||||
if (line.contains(COMMENT_PREFIX)|| line.isEmpty()) {
|
||||
commentContainer.put(count, line);
|
||||
}
|
||||
builder.append(line).append('\n');
|
||||
}
|
||||
} finally {
|
||||
input.close();
|
||||
}
|
||||
loadFromString(builder.toString());
|
||||
}
|
||||
|
||||
public static YamlConfiguration loadConfiguration(File file, YmlConfiguration config) {
|
||||
Validate.notNull(file, "File cannot be null");
|
||||
try {
|
||||
config.load(file);
|
||||
} catch (FileNotFoundException ex) {
|
||||
} catch (IOException | InvalidConfigurationException ex) {
|
||||
Bukkit.getLogger().log(Level.SEVERE, "Cannot load " + file, ex);
|
||||
}
|
||||
return config;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.github.joelgodofwar.mmh.util.datatypes;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
import org.bukkit.persistence.PersistentDataAdapterContext;
|
||||
import org.bukkit.persistence.PersistentDataType;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
/**
|
||||
* Lets you store arbitrary data in PDC.
|
||||
* @param <T>
|
||||
*/
|
||||
public class JsonDataType<T> implements PersistentDataType<String, T> {
|
||||
|
||||
private static final Gson gson = new Gson();
|
||||
private final Class<T> typeClass;
|
||||
|
||||
public JsonDataType(Class<T> typeClass) {
|
||||
this.typeClass = typeClass;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nonnull Class<String> getPrimitiveType() {
|
||||
return String.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nonnull Class<T> getComplexType() {
|
||||
return typeClass;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public String toPrimitive(@Nonnull T complex, @Nonnull PersistentDataAdapterContext persistentDataAdapterContext) {
|
||||
return gson.toJson(complex);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public T fromPrimitive(@Nonnull String primitive, @Nonnull PersistentDataAdapterContext context) {
|
||||
return gson.fromJson(primitive, getComplexType());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.github.joelgodofwar.mmh.util.mob;
|
||||
|
||||
import org.bukkit.entity.Drowned;
|
||||
import org.bukkit.entity.Evoker;
|
||||
import org.bukkit.entity.Husk;
|
||||
import org.bukkit.entity.Illusioner;
|
||||
import org.bukkit.entity.LivingEntity;
|
||||
import org.bukkit.entity.PigZombie;
|
||||
import org.bukkit.entity.Pillager;
|
||||
import org.bukkit.entity.Skeleton;
|
||||
import org.bukkit.entity.Stray;
|
||||
import org.bukkit.entity.Vex;
|
||||
import org.bukkit.entity.Villager;
|
||||
import org.bukkit.entity.Vindicator;
|
||||
import org.bukkit.entity.WanderingTrader;
|
||||
import org.bukkit.entity.WitherSkeleton;
|
||||
import org.bukkit.entity.Zombie;
|
||||
import org.bukkit.entity.ZombieVillager;
|
||||
|
||||
import com.github.joelgodofwar.mmh.MoreMobHeads;
|
||||
|
||||
public class NameTag {
|
||||
|
||||
public static boolean canWearHead(LivingEntity mob) {
|
||||
if((mob instanceof Skeleton)||(mob instanceof Zombie)||(mob instanceof PigZombie)||(mob instanceof Villager)||
|
||||
(mob instanceof Drowned)||(mob instanceof Stray)||(mob instanceof Husk)||(mob instanceof WitherSkeleton)||
|
||||
(mob instanceof ZombieVillager)||(mob instanceof Evoker)||(mob instanceof WanderingTrader)||(mob instanceof Vex)||
|
||||
(mob instanceof Illusioner)||(mob instanceof Pillager)||(mob instanceof Vindicator)){
|
||||
return true;
|
||||
}
|
||||
double vers = Double.parseDouble( MoreMobHeads.getMCVersion().substring(2, 4) ); // 1.16.2
|
||||
if (vers >= 16) {
|
||||
if(NameTag_1_16.canWearHead(mob)) {
|
||||
return true;
|
||||
}
|
||||
if (vers >= 16.2) {
|
||||
if(NameTag_1_16_2.canWearHead(mob)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.github.joelgodofwar.mmh.util.mob;
|
||||
|
||||
import org.bukkit.entity.LivingEntity;
|
||||
import org.bukkit.entity.Piglin;
|
||||
|
||||
public class NameTag_1_16 {
|
||||
|
||||
public static boolean canWearHead(LivingEntity mob) {
|
||||
if(mob instanceof Piglin) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.github.joelgodofwar.mmh.util.mob;
|
||||
|
||||
import org.bukkit.entity.LivingEntity;
|
||||
import org.bukkit.entity.PiglinBrute;
|
||||
|
||||
public class NameTag_1_16_2 {
|
||||
|
||||
public static boolean canWearHead(LivingEntity mob) {
|
||||
if(mob instanceof PiglinBrute) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in new issue
Block a user