Added Ping gathering and storage

This commit is contained in:
Rsl1122 2018-07-16 10:29:45 +03:00
parent 7b5588e19a
commit 9872c9d209
11 changed files with 903 additions and 8 deletions

View File

@ -0,0 +1,23 @@
package com.djrapitops.plan.data.container;
import com.djrapitops.plan.data.store.objects.DateObj;
import java.util.UUID;
public class Ping extends DateObj<Integer> {
private UUID serverUUID;
public Ping(long date, Integer value, UUID serverUUID) {
super(date, value);
this.serverUUID = serverUUID;
}
public Ping(long date, Integer value) {
super(date, value);
}
public UUID getServerUUID() {
return serverUUID;
}
}

View File

@ -5,10 +5,7 @@
package com.djrapitops.plan.system.database.databases.operation;
import com.djrapitops.plan.data.WebUser;
import com.djrapitops.plan.data.container.GeoInfo;
import com.djrapitops.plan.data.container.Session;
import com.djrapitops.plan.data.container.TPS;
import com.djrapitops.plan.data.container.UserInfo;
import com.djrapitops.plan.data.container.*;
import com.djrapitops.plan.data.store.objects.Nickname;
import com.djrapitops.plan.system.info.server.Server;
@ -70,4 +67,6 @@ public interface SaveOperations {
void serverInfoForThisServer(Server server);
void webUser(WebUser webUser);
void ping(UUID uuid, Ping ping);
}

View File

@ -22,6 +22,7 @@ import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.stream.Collectors;
@ -47,6 +48,7 @@ public abstract class SQLDB extends Database {
private final WorldTimesTable worldTimesTable;
private final ServerTable serverTable;
private final TransferTable transferTable;
private final PingTable pingTable;
private final SQLBackupOps backupOps;
private final SQLCheckOps checkOps;
@ -79,6 +81,7 @@ public abstract class SQLDB extends Database {
worldTable = new WorldTable(this);
worldTimesTable = new WorldTimesTable(this);
transferTable = new TransferTable(this);
pingTable = new PingTable(this);
backupOps = new SQLBackupOps(this);
checkOps = new SQLCheckOps(this);
@ -263,6 +266,7 @@ public abstract class SQLDB extends Database {
tpsTable.clean();
transferTable.clean();
geoInfoTable.clean();
pingTable.clean();
long now = System.currentTimeMillis();
long keepActiveAfter = now - TimeAmount.DAY.ms() * Settings.KEEP_INACTIVE_PLAYERS_DAYS.getNumber();
@ -420,6 +424,10 @@ public abstract class SQLDB extends Database {
return transferTable;
}
public PingTable getPingTable() {
return pingTable;
}
public boolean isUsingMySQL() {
return usingMySQL;
}
@ -463,4 +471,17 @@ public abstract class SQLDB extends Database {
public TransferOperations transfer() {
return transferOps;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
SQLDB sqldb = (SQLDB) o;
return usingMySQL == sqldb.usingMySQL && getName().equals(sqldb.getName());
}
@Override
public int hashCode() {
return Objects.hash(usingMySQL, getName());
}
}

View File

@ -20,6 +20,7 @@ public class SQLOps {
protected final WorldTimesTable worldTimesTable;
protected final ServerTable serverTable;
protected final TransferTable transferTable;
protected final PingTable pingTable;
public SQLOps(SQLDB db) {
this.db = db;
@ -37,5 +38,6 @@ public class SQLOps {
worldTimesTable = db.getWorldTimesTable();
serverTable = db.getServerTable();
transferTable = db.getTransferTable();
pingTable = db.getPingTable();
}
}

View File

@ -5,10 +5,7 @@
package com.djrapitops.plan.system.database.databases.sql.operation;
import com.djrapitops.plan.data.WebUser;
import com.djrapitops.plan.data.container.GeoInfo;
import com.djrapitops.plan.data.container.Session;
import com.djrapitops.plan.data.container.TPS;
import com.djrapitops.plan.data.container.UserInfo;
import com.djrapitops.plan.data.container.*;
import com.djrapitops.plan.data.store.objects.Nickname;
import com.djrapitops.plan.system.database.databases.operation.SaveOperations;
import com.djrapitops.plan.system.database.databases.sql.SQLDB;
@ -133,4 +130,9 @@ public class SQLSaveOps extends SQLOps implements SaveOperations {
public void webUser(WebUser webUser) {
securityTable.addNewUser(webUser);
}
@Override
public void ping(UUID uuid, Ping ping) {
pingTable.insertPing(uuid, ping);
}
}

View File

@ -0,0 +1,181 @@
package com.djrapitops.plan.system.database.databases.sql.tables;
import com.djrapitops.plan.api.exceptions.database.DBInitException;
import com.djrapitops.plan.data.container.Ping;
import com.djrapitops.plan.system.database.databases.sql.SQLDB;
import com.djrapitops.plan.system.database.databases.sql.processing.ExecStatement;
import com.djrapitops.plan.system.database.databases.sql.processing.QueryAllStatement;
import com.djrapitops.plan.system.database.databases.sql.processing.QueryStatement;
import com.djrapitops.plan.system.database.databases.sql.statements.Column;
import com.djrapitops.plan.system.database.databases.sql.statements.Sql;
import com.djrapitops.plan.system.database.databases.sql.statements.TableSqlParser;
import com.djrapitops.plan.system.info.server.ServerInfo;
import com.djrapitops.plugin.api.TimeAmount;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.*;
public class PingTable extends UserIDTable {
public static final String TABLE_NAME = "plan_ping";
private final String insertStatement;
private final ServerTable serverTable;
public PingTable(SQLDB db) {
super(TABLE_NAME, db);
serverTable = db.getServerTable();
insertStatement = "INSERT INTO " + tableName + " (" +
Col.USER_ID + ", " +
Col.SERVER_ID + ", " +
Col.DATE + ", " +
Col.MAX_PING +
") VALUES (" +
usersTable.statementSelectID + ", " +
serverTable.statementSelectServerID + ", ?, ?)";
}
@Override
public void createTable() throws DBInitException {
createTable(TableSqlParser.createTable(TABLE_NAME)
.primaryKeyIDColumn(usingMySQL, Col.ID)
.column(Col.USER_ID, Sql.INT).notNull()
.column(Col.SERVER_ID, Sql.INT).notNull()
.column(Col.DATE, Sql.LONG).notNull()
.column(Col.MAX_PING, Sql.INT).notNull()
.primaryKey(usingMySQL, Col.ID)
.foreignKey(Col.USER_ID, usersTable.getTableName(), UsersTable.Col.ID)
.foreignKey(Col.SERVER_ID, ServerTable.TABLE_NAME, ServerTable.Col.SERVER_ID)
.toString());
}
public void clean() {
String sql = "DELETE FROM " + tableName +
" WHERE (" + Col.DATE + "<?)";
execute(new ExecStatement(sql) {
@Override
public void prepare(PreparedStatement statement) throws SQLException {
long twoWeeks = TimeAmount.WEEK.ms() * 2L;
statement.setLong(1, System.currentTimeMillis() - twoWeeks);
}
});
}
public void insertPing(UUID uuid, Ping ping) {
execute(new ExecStatement(insertStatement) {
@Override
public void prepare(PreparedStatement statement) throws SQLException {
statement.setString(1, uuid.toString());
statement.setString(2, ServerInfo.getServerUUID().toString());
statement.setLong(3, ping.getDate());
statement.setInt(4, ping.getValue());
}
});
}
public List<Ping> getPing(UUID uuid) {
Map<Integer, UUID> serverUUIDs = serverTable.getServerUUIDsByID();
String sql = "SELECT * FROM " + tableName +
" WHERE " + Col.USER_ID + "=" + usersTable.statementSelectID;
return query(new QueryStatement<List<Ping>>(sql, 10000) {
@Override
public void prepare(PreparedStatement statement) throws SQLException {
statement.setString(1, uuid.toString());
}
@Override
public List<Ping> processResults(ResultSet set) throws SQLException {
List<Ping> pings = new ArrayList<>();
while (set.next()) {
pings.add(new Ping(
set.getLong(Col.DATE.get()),
set.getInt(Col.MAX_PING.get()),
serverUUIDs.get(set.getInt(Col.SERVER_ID.get()))));
}
return pings;
}
});
}
public Map<UUID, List<Ping>> getAllPings() {
String usersIDColumn = usersTable + "." + UsersTable.Col.ID;
String usersUUIDColumn = usersTable + "." + UsersTable.Col.UUID + " as uuid";
String serverIDColumn = serverTable + "." + ServerTable.Col.SERVER_ID;
String serverUUIDColumn = serverTable + "." + ServerTable.Col.SERVER_UUID + " as s_uuid";
String sql = "SELECT " +
Col.DATE + ", " +
Col.MAX_PING + ", " +
usersUUIDColumn + ", " +
serverUUIDColumn +
" FROM " + tableName +
" INNER JOIN " + usersTable + " on " + usersIDColumn + "=" + UserInfoTable.Col.USER_ID +
" INNER JOIN " + serverTable + " on " + serverIDColumn + "=" + UserInfoTable.Col.SERVER_ID;
return query(new QueryAllStatement<Map<UUID, List<Ping>>>(sql, 100000) {
@Override
public Map<UUID, List<Ping>> processResults(ResultSet set) throws SQLException {
Map<UUID, List<Ping>> userPings = new HashMap<>();
while (set.next()) {
UUID uuid = UUID.fromString(set.getString("uuid"));
UUID serverUUID = UUID.fromString(set.getString("s_uuid"));
long date = set.getLong(Col.DATE.get());
int maxPing = set.getInt(Col.MAX_PING.get());
List<Ping> pings = userPings.getOrDefault(uuid, new ArrayList<>());
pings.add(new Ping(date, maxPing, serverUUID));
userPings.put(uuid, pings);
}
return userPings;
}
});
}
public void insertAllPings(Map<UUID, List<Ping>> userPings) {
executeBatch(new ExecStatement(insertStatement) {
@Override
public void prepare(PreparedStatement statement) throws SQLException {
for (Map.Entry<UUID, List<Ping>> entry : userPings.entrySet()) {
UUID uuid = entry.getKey();
List<Ping> pings = entry.getValue();
for (Ping ping : pings) {
UUID serverUUID = ping.getServerUUID();
long date = ping.getDate();
int maxPing = ping.getValue();
statement.setString(1, uuid.toString());
statement.setString(2, serverUUID.toString());
statement.setLong(3, date);
statement.setInt(4, maxPing);
statement.addBatch();
}
}
}
});
}
public enum Col implements Column {
ID("id"),
USER_ID(UserIDTable.Col.USER_ID.get()),
SERVER_ID("server_id"),
DATE("date"),
MAX_PING("max_ping");
private final String name;
Col(String name) {
this.name = name;
}
@Override
public String get() {
return name;
}
}
}

View File

@ -84,6 +84,14 @@ public class BatchOperationTable extends Table {
copyNicknames(toDB);
copySessions(toDB);
copyUserInfo(toDB);
copyPings(toDB);
}
private void copyPings(BatchOperationTable toDB) {
if (toDB.equals(this)) {
return;
}
toDB.db.getPingTable().insertAllPings(db.getPingTable().getAllPings());
}
public void copyCommandUse(BatchOperationTable toDB) {

View File

@ -0,0 +1,53 @@
/*
* License is provided in the jar as LICENSE also here:
* https://github.com/Rsl1122/Plan-PlayerAnalytics/blob/master/Plan/src/main/resources/LICENSE
*/
package com.djrapitops.plan.system.processing.processors.player;
import com.djrapitops.plan.data.container.Ping;
import com.djrapitops.plan.data.store.objects.DateObj;
import com.djrapitops.plan.system.database.databases.Database;
import com.djrapitops.plan.system.info.server.ServerInfo;
import com.djrapitops.plan.system.processing.CriticalRunnable;
import java.util.List;
import java.util.OptionalInt;
import java.util.UUID;
/**
* Processes 60s average of a Ping list.
* <p>
* Ping list contains 30 values as ping is updated every 2 seconds.
*
* @author Rsl1122
*/
public class PingInsertProcessor implements CriticalRunnable {
private final UUID uuid;
private final List<Ping> pingList;
public PingInsertProcessor(UUID uuid, List<Ping> pingList) {
this.uuid = uuid;
this.pingList = pingList;
}
@Override
public void run() {
List<Ping> history = pingList;
long lastDate = history.get(history.size() - 1).getDate();
OptionalInt max = history.stream()
.mapToInt(DateObj::getValue)
.filter(i -> i != -1)
.max();
if (!max.isPresent()) {
return;
}
int maxValue = max.getAsInt();
Ping ping = new Ping(lastDate, maxValue, ServerInfo.getServerUUID());
Database.getActive().save().ping(uuid, ping);
}
}

View File

@ -7,7 +7,9 @@ package com.djrapitops.plan.system.tasks;
import com.djrapitops.plan.Plan;
import com.djrapitops.plan.system.tasks.server.BukkitTPSCountTimer;
import com.djrapitops.plan.system.tasks.server.PaperTPSCountTimer;
import com.djrapitops.plan.system.tasks.server.PingCountTimer;
import com.djrapitops.plugin.api.Check;
import com.djrapitops.plugin.task.RunnableFactory;
import org.bukkit.Bukkit;
/**
@ -25,6 +27,15 @@ public class BukkitTaskSystem extends ServerTaskSystem {
);
}
@Override
public void enable() {
super.enable();
PingCountTimer pingCountTimer = new PingCountTimer();
((Plan) plugin).registerListener(pingCountTimer);
RunnableFactory.createNew("PingCountTimer", pingCountTimer)
.runTaskTimer(20L, PingCountTimer.PING_INTERVAL);
}
@Override
public void disable() {
super.disable();

View File

@ -0,0 +1,169 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2016-2018
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.djrapitops.plan.system.tasks.server;
import com.djrapitops.plan.data.container.Ping;
import com.djrapitops.plan.system.processing.Processing;
import com.djrapitops.plan.system.processing.processors.player.PingInsertProcessor;
import com.djrapitops.plan.utilities.java.Reflection;
import com.djrapitops.plugin.api.utility.log.Log;
import com.djrapitops.plugin.task.AbsRunnable;
import com.djrapitops.plugin.task.RunnableFactory;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodHandles.Lookup;
import java.lang.reflect.Method;
import java.util.*;
/**
* Task that handles player ping calculation on Bukkit based servers.
* <p>
* Modified PingManager from LagMonitor plugin.
* https://github.com/games647/LagMonitor/blob/master/src/main/java/com/github/games647/lagmonitor/task/PingManager.java
*
* @author games647
*/
public class PingCountTimer extends AbsRunnable implements Listener {
//the server is pinging the client every 40 Ticks (2 sec) - so check it then
//https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/PlayerConnection.java#L178
public static final int PING_INTERVAL = 2 * 20;
private static final boolean pingMethodAvailable;
private static final MethodHandle pingField;
private static final MethodHandle getHandleMethod;
static {
pingMethodAvailable = isPingMethodAvailable();
MethodHandle localHandle = null;
MethodHandle localPing = null;
if (!pingMethodAvailable) {
Class<?> craftPlayerClass = Reflection.getCraftBukkitClass("entity.CraftPlayer");
Class<?> entityPlayer = Reflection.getMinecraftClass("EntityPlayer");
Lookup lookup = MethodHandles.publicLookup();
try {
Method getHandleMethod = craftPlayerClass.getDeclaredMethod("getHandle");
localHandle = lookup.unreflect(getHandleMethod);
localPing = lookup.findGetter(entityPlayer, "ping", Integer.TYPE);
} catch (NoSuchMethodException | IllegalAccessException | NoSuchFieldException reflectiveEx) {
Log.toLog(PingCountTimer.class, reflectiveEx);
}
}
getHandleMethod = localHandle;
pingField = localPing;
}
private final Map<UUID, List<Ping>> playerHistory = new HashMap<>();
private static boolean isPingMethodAvailable() {
try {
//Only available in Paper
Player.Spigot.class.getDeclaredMethod("getPing");
return true;
} catch (NoSuchMethodException noSuchMethodEx) {
return false;
}
}
@Override
public void run() {
List<UUID> loggedOut = new ArrayList<>();
long time = System.currentTimeMillis();
playerHistory.forEach((uuid, history) -> {
Player player = Bukkit.getPlayer(uuid);
if (player != null) {
int ping = getPing(player);
history.add(new Ping(time, ping));
if (history.size() >= 30) {
Processing.submit(new PingInsertProcessor(uuid, new ArrayList<>(history)));
history.clear();
}
} else {
loggedOut.add(uuid);
}
});
loggedOut.forEach(playerHistory::remove);
}
public void addPlayer(Player player) {
playerHistory.put(player.getUniqueId(), new ArrayList<>());
}
public void removePlayer(Player player) {
playerHistory.remove(player.getUniqueId());
}
private int getPing(Player player) {
if (pingMethodAvailable) {
return player.spigot().getPing();
}
return getReflectionPing(player);
}
private int getReflectionPing(Player player) {
try {
Object entityPlayer = getHandleMethod.invoke(player);
return (int) pingField.invoke(entityPlayer);
} catch (Exception ex) {
return -1;
} catch (Throwable throwable) {
throw (Error) throwable;
}
}
@EventHandler
public void onPlayerJoin(PlayerJoinEvent joinEvent) {
Player player = joinEvent.getPlayer();
RunnableFactory.createNew("Add Player to Ping list", new AbsRunnable() {
@Override
public void run() {
if (player.isOnline()) {
addPlayer(player);
}
}
}).runTaskLater(PING_INTERVAL);
}
@EventHandler
public void onPlayerQuit(PlayerQuitEvent quitEvent) {
removePlayer(quitEvent.getPlayer());
}
public void clear() {
playerHistory.clear();
}
}

View File

@ -0,0 +1,426 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2016-2018
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.djrapitops.plan.utilities.java;
import org.bukkit.Bukkit;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* An utility class that simplifies reflection in Bukkit plugins.
* <p>
* Modified Reflection utility from LagMonitor plugin.
* https://github.com/games647/LagMonitor/blob/master/src/main/java/com/github/games647/lagmonitor/traffic/Reflection.java
*
* @author Kristian
*/
public final class Reflection {
// Deduce the net.minecraft.server.v* package
private static final String OBC_PREFIX = Bukkit.getServer().getClass().getPackage().getName();
private static final String NMS_PREFIX = OBC_PREFIX.replace("org.bukkit.craftbukkit", "net.minecraft.server");
private static final String VERSION = OBC_PREFIX.replace("org.bukkit.craftbukkit", "").replace(".", "");
// Variable replacement
private static final Pattern MATCH_VARIABLE = Pattern.compile("\\{([^\\}]+)\\}");
private Reflection() {
// Seal class
}
/**
* Retrieve a field accessor for a specific field type and name.
*
* @param target - the target type.
* @param name - the name of the field, or NULL to ignore.
* @param fieldType - a compatible field type.
* @return The field accessor.
*/
public static <T> FieldAccessor<T> getField(Class<?> target, String name, Class<T> fieldType) {
return getField(target, name, fieldType, 0);
}
/**
* Retrieve a field accessor for a specific field type and name.
*
* @param className - lookup name of the class, see {@link #getClass(String)}.
* @param name - the name of the field, or NULL to ignore.
* @param fieldType - a compatible field type.
* @return The field accessor.
*/
public static <T> FieldAccessor<T> getField(String className, String name, Class<T> fieldType) {
return getField(getClass(className), name, fieldType, 0);
}
/**
* Retrieve a field accessor for a specific field type and name.
*
* @param target - the target type.
* @param fieldType - a compatible field type.
* @param index - the number of compatible fields to skip.
* @return The field accessor.
*/
public static <T> FieldAccessor<T> getField(Class<?> target, Class<T> fieldType, int index) {
return getField(target, null, fieldType, index);
}
/**
* Retrieve a field accessor for a specific field type and name.
*
* @param className - lookup name of the class, see {@link #getClass(String)}.
* @param fieldType - a compatible field type.
* @param index - the number of compatible fields to skip.
* @return The field accessor.
*/
public static <T> FieldAccessor<T> getField(String className, Class<T> fieldType, int index) {
return getField(getClass(className), fieldType, index);
}
// Common method
private static <T> FieldAccessor<T> getField(Class<?> target, String name, Class<T> fieldType, int index) {
for (final Field field : target.getDeclaredFields()) {
if ((name == null || field.getName().equals(name)) && fieldType.isAssignableFrom(field.getType()) && index-- <= 0) {
field.setAccessible(true);
// A function for retrieving a specific field value
return new FieldAccessor<T>() {
@Override
@SuppressWarnings("unchecked")
public T get(Object target) {
try {
return (T) field.get(target);
} catch (IllegalAccessException e) {
throw new RuntimeException("Cannot access reflection.", e);
}
}
@Override
public void set(Object target, Object value) {
try {
field.set(target, value);
} catch (IllegalAccessException e) {
throw new RuntimeException("Cannot access reflection.", e);
}
}
@Override
public boolean hasField(Object target) {
// target instanceof DeclaringClass
return field.getDeclaringClass().isAssignableFrom(target.getClass());
}
};
}
}
// Search in parent classes
if (target.getSuperclass() != null) {
return getField(target.getSuperclass(), name, fieldType, index);
}
throw new IllegalArgumentException("Cannot find field with type " + fieldType);
}
/**
* Search for the first publicly and privately defined method of the given name and parameter count.
*
* @param className - lookup name of the class, see {@link #getClass(String)}.
* @param methodName - the method name, or NULL to skip.
* @param params - the expected parameters.
* @return An object that invokes this specific method.
* @throws IllegalStateException If we cannot find this method.
*/
public static MethodInvoker getMethod(String className, String methodName, Class<?>... params) {
return getTypedMethod(getClass(className), methodName, null, params);
}
/**
* Search for the first publicly and privately defined method of the given name and parameter count.
*
* @param clazz - a class to start with.
* @param methodName - the method name, or NULL to skip.
* @param params - the expected parameters.
* @return An object that invokes this specific method.
* @throws IllegalStateException If we cannot find this method.
*/
public static MethodInvoker getMethod(Class<?> clazz, String methodName, Class<?>... params) {
return getTypedMethod(clazz, methodName, null, params);
}
/**
* Search for the first publicly and privately defined method of the given name and parameter count.
*
* @param clazz - a class to start with.
* @param methodName - the method name, or NULL to skip.
* @param returnType - the expected return type, or NULL to ignore.
* @param params - the expected parameters.
* @return An object that invokes this specific method.
* @throws IllegalStateException If we cannot find this method.
*/
public static MethodInvoker getTypedMethod(Class<?> clazz, String methodName, Class<?> returnType, Class<?>... params) {
for (final Method method : clazz.getDeclaredMethods()) {
if ((methodName == null || method.getName().equals(methodName)) && (returnType == null) || method.getReturnType().equals(returnType) && Arrays.equals(method.getParameterTypes(), params)) {
method.setAccessible(true);
return (target, arguments) -> {
try {
return method.invoke(target, arguments);
} catch (Exception e) {
throw new RuntimeException("Cannot invoke method " + method, e);
}
};
}
}
// Search in every superclass
if (clazz.getSuperclass() != null) {
return getMethod(clazz.getSuperclass(), methodName, params);
}
throw new IllegalStateException(String.format("Unable to find method %s (%s).", methodName, Arrays.asList(params)));
}
/**
* Search for the first publicly and privately defined constructor of the given name and parameter count.
*
* @param className - lookup name of the class, see {@link #getClass(String)}.
* @param params - the expected parameters.
* @return An object that invokes this constructor.
* @throws IllegalStateException If we cannot find this method.
*/
public static ConstructorInvoker getConstructor(String className, Class<?>... params) {
return getConstructor(getClass(className), params);
}
/**
* Search for the first publicly and privately defined constructor of the given name and parameter count.
*
* @param clazz - a class to start with.
* @param params - the expected parameters.
* @return An object that invokes this constructor.
* @throws IllegalStateException If we cannot find this method.
*/
public static ConstructorInvoker getConstructor(Class<?> clazz, Class<?>... params) {
for (final Constructor<?> constructor : clazz.getDeclaredConstructors()) {
if (Arrays.equals(constructor.getParameterTypes(), params)) {
constructor.setAccessible(true);
return arguments -> {
try {
return constructor.newInstance(arguments);
} catch (Exception e) {
throw new RuntimeException("Cannot invoke constructor " + constructor, e);
}
};
}
}
throw new IllegalStateException(String.format("Unable to find constructor for %s (%s).", clazz, Arrays.asList(params)));
}
/**
* Retrieve a class from its full name, without knowing its type on compile time.
* <p>
* This is useful when looking up fields by a NMS or OBC type.
* <p>
*
* @param lookupName - the class name with variables.
* @return The class.
* @see Object#getClass()
*/
public static Class<Object> getUntypedClass(String lookupName) {
@SuppressWarnings({"rawtypes", "unchecked"})
Class<Object> clazz = (Class) getClass(lookupName);
return clazz;
}
/**
* Retrieve a class from its full name.
* <p>
* Strings enclosed with curly brackets - such as {TEXT} - will be replaced according to the following table:
* </p>
* <table border="1">
* <tr>
* <th>Variable</th>
* <th>Content</th>
* </tr>
* <tr>
* <td>{nms}</td>
* <td>Actual package name of net.minecraft.server.VERSION</td>
* </tr>
* <tr>
* <td>{obc}</td>
* <td>Actual package name of org.bukkit.craftbukkit.VERSION</td>
* </tr>
* <tr>
* <td>{version}</td>
* <td>The current Minecraft package VERSION, if any.</td>
* </tr>
* </table>
*
* @param lookupName - the class name with variables.
* @return The looked up class.
* @throws IllegalArgumentException If a variable or class could not be found.
*/
public static Class<?> getClass(String lookupName) {
return getCanonicalClass(expandVariables(lookupName));
}
/**
* Retrieve a class in the net.minecraft.server.VERSION.* package.
*
* @param name - the name of the class, excluding the package.
* @throws IllegalArgumentException If the class doesn't exist.
*/
public static Class<?> getMinecraftClass(String name) {
return getCanonicalClass(NMS_PREFIX + '.' + name);
}
/**
* Retrieve a class in the org.bukkit.craftbukkit.VERSION.* package.
*
* @param name - the name of the class, excluding the package.
* @throws IllegalArgumentException If the class doesn't exist.
*/
public static Class<?> getCraftBukkitClass(String name) {
return getCanonicalClass(OBC_PREFIX + '.' + name);
}
/**
* Retrieve a class by its canonical name.
*
* @param canonicalName - the canonical name.
* @return The class.
*/
private static Class<?> getCanonicalClass(String canonicalName) {
try {
return Class.forName(canonicalName);
} catch (ClassNotFoundException e) {
throw new IllegalArgumentException("Cannot find " + canonicalName, e);
}
}
/**
* Expand variables such as "{nms}" and "{obc}" to their corresponding packages.
*
* @param name - the full name of the class.
* @return The expanded string.
*/
private static String expandVariables(String name) {
StringBuffer output = new StringBuffer();
Matcher matcher = MATCH_VARIABLE.matcher(name);
while (matcher.find()) {
String variable = matcher.group(1);
String replacement;
// Expand all detected variables
if ("nms".equalsIgnoreCase(variable)) {
replacement = NMS_PREFIX;
} else if ("obc".equalsIgnoreCase(variable)) {
replacement = OBC_PREFIX;
} else if ("version".equalsIgnoreCase(variable)) {
replacement = VERSION;
} else {
throw new IllegalArgumentException("Unknown variable: " + variable);
}
// Assume the expanded variables are all packages, and append a dot
if (!replacement.isEmpty() && matcher.end() < name.length() && name.charAt(matcher.end()) != '.') {
replacement += ".";
}
matcher.appendReplacement(output, Matcher.quoteReplacement(replacement));
}
matcher.appendTail(output);
return output.toString();
}
/**
* An interface for invoking a specific constructor.
*/
@FunctionalInterface
public interface ConstructorInvoker {
/**
* Invoke a constructor for a specific class.
*
* @param arguments - the arguments to pass to the constructor.
* @return The constructed object.
*/
Object invoke(Object... arguments);
}
/**
* An interface for invoking a specific method.
*/
@FunctionalInterface
public interface MethodInvoker {
/**
* Invoke a method on a specific target object.
*
* @param target - the target object, or NULL for a static method.
* @param arguments - the arguments to pass to the method.
* @return The return value, or NULL if is void.
*/
Object invoke(Object target, Object... arguments);
}
/**
* An interface for retrieving the field content.
*
* @param <T> - field type.
*/
public interface FieldAccessor<T> {
/**
* Retrieve the content of a field.
*
* @param target - the target object, or NULL for a static field.
* @return The value of the field.
*/
T get(Object target);
/**
* Set the content of a field.
*
* @param target - the target object, or NULL for a static field.
* @param value - the new value of the field.
*/
void set(Object target, Object value);
/**
* Determine if the given object has this field.
*
* @param target - the object to test.
* @return TRUE if it does, FALSE otherwise.
*/
boolean hasField(Object target);
}
}