commit 5d7a2467717b192a0d33c23c83a10bea178e01d4 Author: d1novnowo_ Date: Sun May 16 10:42:40 2021 +0700 First commit diff --git a/Libraries/Bungeecord.jar b/Libraries/Bungeecord.jar new file mode 100644 index 00000000..c7bc87ca Binary files /dev/null and b/Libraries/Bungeecord.jar differ diff --git a/Libraries/commons-dbcp2-2.0.1.jar b/Libraries/commons-dbcp2-2.0.1.jar new file mode 100644 index 00000000..5e8d142d Binary files /dev/null and b/Libraries/commons-dbcp2-2.0.1.jar differ diff --git a/Libraries/commons-pool2-2.9.0.jar b/Libraries/commons-pool2-2.9.0.jar new file mode 100644 index 00000000..a038b36c Binary files /dev/null and b/Libraries/commons-pool2-2.9.0.jar differ diff --git a/Libraries/craftbukkit.jar b/Libraries/craftbukkit.jar new file mode 100644 index 00000000..8264b8c0 Binary files /dev/null and b/Libraries/craftbukkit.jar differ diff --git a/Libraries/gson-2.2.1.jar b/Libraries/gson-2.2.1.jar new file mode 100644 index 00000000..5c2d52e8 Binary files /dev/null and b/Libraries/gson-2.2.1.jar differ diff --git a/Libraries/httpclient-4.5.2.jar b/Libraries/httpclient-4.5.2.jar new file mode 100644 index 00000000..701609fc Binary files /dev/null and b/Libraries/httpclient-4.5.2.jar differ diff --git a/Libraries/jd-gui-1.6.6.jar b/Libraries/jd-gui-1.6.6.jar new file mode 100644 index 00000000..58fd0da2 Binary files /dev/null and b/Libraries/jd-gui-1.6.6.jar differ diff --git a/Libraries/jedis-2.8.1.jar b/Libraries/jedis-2.8.1.jar new file mode 100644 index 00000000..dbb8ae95 Binary files /dev/null and b/Libraries/jedis-2.8.1.jar differ diff --git a/Libraries/jooq-3.5.2.jar b/Libraries/jooq-3.5.2.jar new file mode 100644 index 00000000..32a69e7d Binary files /dev/null and b/Libraries/jooq-3.5.2.jar differ diff --git a/Libraries/jooq-codegen-3.5.2.jar b/Libraries/jooq-codegen-3.5.2.jar new file mode 100644 index 00000000..56f8c99e Binary files /dev/null and b/Libraries/jooq-codegen-3.5.2.jar differ diff --git a/Mineplexer/.idea/.gitignore b/Mineplexer/.idea/.gitignore new file mode 100644 index 00000000..26d33521 --- /dev/null +++ b/Mineplexer/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/Mineplexer/.idea/Mineplexer.iml b/Mineplexer/.idea/Mineplexer.iml new file mode 100644 index 00000000..d6ebd480 --- /dev/null +++ b/Mineplexer/.idea/Mineplexer.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/Mineplexer/.idea/artifacts/Mineplex_Bungee_Mineplexer_jar.xml b/Mineplexer/.idea/artifacts/Mineplex_Bungee_Mineplexer_jar.xml new file mode 100644 index 00000000..890664fd --- /dev/null +++ b/Mineplexer/.idea/artifacts/Mineplex_Bungee_Mineplexer_jar.xml @@ -0,0 +1,17 @@ + + + $PROJECT_DIR$/../ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Mineplexer/.idea/artifacts/Mineplex_Cache_jar.xml b/Mineplexer/.idea/artifacts/Mineplex_Cache_jar.xml new file mode 100644 index 00000000..ff041f58 --- /dev/null +++ b/Mineplexer/.idea/artifacts/Mineplex_Cache_jar.xml @@ -0,0 +1,9 @@ + + + $PROJECT_DIR$/out/artifacts/Mineplex_Cache_jar + + + + + + \ No newline at end of file diff --git a/Mineplexer/.idea/artifacts/Mineplex_ServerData_jar.xml b/Mineplexer/.idea/artifacts/Mineplex_ServerData_jar.xml new file mode 100644 index 00000000..5c772245 --- /dev/null +++ b/Mineplexer/.idea/artifacts/Mineplex_ServerData_jar.xml @@ -0,0 +1,8 @@ + + + $PROJECT_DIR$/out/artifacts/Mineplex_ServerData_jar + + + + + \ No newline at end of file diff --git a/Mineplexer/.idea/misc.xml b/Mineplexer/.idea/misc.xml new file mode 100644 index 00000000..bf84b09d --- /dev/null +++ b/Mineplexer/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Mineplexer/.idea/modules.xml b/Mineplexer/.idea/modules.xml new file mode 100644 index 00000000..77d46451 --- /dev/null +++ b/Mineplexer/.idea/modules.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/Mineplexer/Mineplex.Bungee.Mineplexer/Mineplex.Bungee.Mineplexer.iml b/Mineplexer/Mineplex.Bungee.Mineplexer/Mineplex.Bungee.Mineplexer.iml new file mode 100644 index 00000000..2cc42996 --- /dev/null +++ b/Mineplexer/Mineplex.Bungee.Mineplexer/Mineplex.Bungee.Mineplexer.iml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Mineplexer/Mineplex.Bungee.Mineplexer/plugin.yml b/Mineplexer/Mineplex.Bungee.Mineplexer/plugin.yml new file mode 100644 index 00000000..e2beec57 --- /dev/null +++ b/Mineplexer/Mineplex.Bungee.Mineplexer/plugin.yml @@ -0,0 +1,4 @@ +name: Mineplexer +main: mineplex.bungee.Mineplexer +version: 1 +author: defek7 diff --git a/Mineplexer/Mineplex.Bungee.Mineplexer/src/mineplex/bungee/FileUpdater.java b/Mineplexer/Mineplex.Bungee.Mineplexer/src/mineplex/bungee/FileUpdater.java new file mode 100644 index 00000000..06a6a0dc --- /dev/null +++ b/Mineplexer/Mineplex.Bungee.Mineplexer/src/mineplex/bungee/FileUpdater.java @@ -0,0 +1,171 @@ +package mineplex.bungee; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FilenameFilter; +import java.io.IOException; +import java.util.HashMap; +import java.util.concurrent.TimeUnit; + +import net.md_5.bungee.BungeeCord; +import net.md_5.bungee.api.ChatColor; +import net.md_5.bungee.api.plugin.Plugin; + +import org.apache.commons.codec.digest.DigestUtils; + +public class FileUpdater implements Runnable +{ + private Plugin _plugin; + private HashMap _jarMd5Map = new HashMap(); + + private boolean _needUpdate; + private boolean _enabled = true; + private int _timeTilRestart = 5; + + public FileUpdater(Plugin plugin) + { + _plugin = plugin; + + getPluginMd5s(); + + if (new File("IgnoreUpdates.dat").exists()) + _enabled = false; + + _plugin.getProxy().getScheduler().schedule(_plugin, this, 2L, 2L, TimeUnit.MINUTES); + } + + public void checkForNewFiles() + { + if (_needUpdate || !_enabled) + return; + + boolean windows = System.getProperty("os.name").startsWith("Windows"); + + File updateDir = new File((windows ? "C:" : File.separator + "home" + File.separator + "mineplex") + File.separator + "update"); + + updateDir.mkdirs(); + + FilenameFilter statsFilter = new FilenameFilter() + { + public boolean accept(File paramFile, String paramString) + { + if (paramString.endsWith("jar")) + { + return true; + } + + return false; + } + }; + + for (File f : updateDir.listFiles(statsFilter)) + { + FileInputStream fis = null; + + try + { + if (_jarMd5Map.containsKey(f.getName())) + { + fis = new FileInputStream(f); + String md5 = DigestUtils.md5Hex(fis); + + if (!md5.equals(_jarMd5Map.get(f.getName()))) + { + System.out.println(f.getName() + " old jar : " + _jarMd5Map.get(f.getName())); + System.out.println(f.getName() + " new jar : " + md5); + _needUpdate = true; + } + } + } + catch (Exception ex) + { + ex.printStackTrace(); + } + finally + { + if (fis != null) + { + try + { + fis.close(); + } + catch (IOException e) + { + e.printStackTrace(); + } + } + } + } + } + + private void getPluginMd5s() + { + File pluginDir = new File("plugins"); + + pluginDir.mkdirs(); + + FilenameFilter statsFilter = new FilenameFilter() + { + public boolean accept(File paramFile, String paramString) + { + if (paramString.endsWith("jar")) + { + return true; + } + + return false; + } + }; + + for (File f : pluginDir.listFiles(statsFilter)) + { + FileInputStream fis = null; + + try + { + fis = new FileInputStream(f); + _jarMd5Map.put(f.getName(), DigestUtils.md5Hex(fis)); + } + catch (Exception ex) + { + ex.printStackTrace(); + } + finally + { + if (fis != null) + { + try + { + fis.close(); + } + catch (IOException e) + { + e.printStackTrace(); + } + } + } + } + } + + @Override + public void run() + { + checkForNewFiles(); + + if (_needUpdate) + { + BungeeCord.getInstance().broadcast(ChatColor.RED + "Connection Node" + ChatColor.DARK_GRAY + ">" + ChatColor.YELLOW + "This connection node will be restarting in " + _timeTilRestart + " minutes."); + } + else + { + return; + } + + _timeTilRestart -= 2; + + if (_timeTilRestart < 0 || !_enabled) + { + BungeeCord.getInstance().stop(); + } + } +} diff --git a/Mineplexer/Mineplex.Bungee.Mineplexer/src/mineplex/bungee/Mineplexer.java b/Mineplexer/Mineplex.Bungee.Mineplexer/src/mineplex/bungee/Mineplexer.java new file mode 100644 index 00000000..6f95e34d --- /dev/null +++ b/Mineplexer/Mineplex.Bungee.Mineplexer/src/mineplex/bungee/Mineplexer.java @@ -0,0 +1,21 @@ +package mineplex.bungee; + +import mineplex.bungee.lobbyBalancer.LobbyBalancer; +import mineplex.bungee.motd.MotdManager; +import mineplex.bungee.playerCount.PlayerCount; +import mineplex.bungee.playerStats.PlayerStats; +import mineplex.bungee.playerTracker.PlayerTracker; +import net.md_5.bungee.api.plugin.Plugin; + +public class Mineplexer extends Plugin +{ + @Override + public void onEnable() { + new MotdManager(this); + new LobbyBalancer(this); + new PlayerCount(this); + //new FileUpdater(this); + //TODO: new PlayerStats(this); + new PlayerTracker(this); + } +} diff --git a/Mineplexer/Mineplex.Bungee.Mineplexer/src/mineplex/bungee/lobbyBalancer/LobbyBalancer.java b/Mineplexer/Mineplex.Bungee.Mineplexer/src/mineplex/bungee/lobbyBalancer/LobbyBalancer.java new file mode 100644 index 00000000..a422ff82 --- /dev/null +++ b/Mineplexer/Mineplex.Bungee.Mineplexer/src/mineplex/bungee/lobbyBalancer/LobbyBalancer.java @@ -0,0 +1,133 @@ +package mineplex.bungee.lobbyBalancer; + +import java.io.File; +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import net.md_5.bungee.api.event.ServerConnectEvent; +import net.md_5.bungee.api.plugin.Listener; +import net.md_5.bungee.api.plugin.Plugin; +import net.md_5.bungee.event.EventHandler; + +import mineplex.serverdata.Region; +import mineplex.serverdata.data.MinecraftServer; +import mineplex.serverdata.servers.ServerManager; +import mineplex.serverdata.servers.ServerRepository; + +public class LobbyBalancer implements Listener, Runnable +{ + private Plugin _plugin; + private ServerRepository _repository; + + private final Map> _sortedLobbyMap = new EnumMap<>(LobbyType.class); + private final Map _nextIndexMap = new EnumMap<>(LobbyType.class); + private static final LobbySorter LOBBY_SORTER = new LobbySorter(); + private static final Object _serverLock = new Object(); + + public LobbyBalancer(Plugin plugin) + { + _plugin = plugin; + + Region region = !new File("eu.dat").exists() ? Region.US : Region.EU; + _repository = ServerManager.getServerRepository(region); + + run(); + + _plugin.getProxy().getPluginManager().registerListener(_plugin, this); + _plugin.getProxy().getScheduler().schedule(_plugin, this, 500L, 500L, TimeUnit.MILLISECONDS); + } + + @EventHandler + public void playerConnect(ServerConnectEvent event) + { + Arrays.stream(LobbyType.values()) + .filter(type -> type.getConnectName().equalsIgnoreCase(event.getTarget().getName())) + .findFirst() + .ifPresent(lobbyType -> + { + synchronized (_serverLock) + { + List lobbies = _sortedLobbyMap.get(lobbyType); + /* + if(lobbies.size() == 0){ + event.getPlayer().disconnect("Sorry! There aren't any servers that are currently active right now. Come back later :3"); + System.out.println("NOTICE! The network doesn't have any active servers. Are we in development status?"); + return; + } + */ + int nextIndex = _nextIndexMap.getOrDefault(lobbyType, 0); + if (nextIndex >= lobbies.size()) + { + nextIndex = 0; + } + MinecraftServer server = lobbies.get(nextIndex); + event.setTarget(_plugin.getProxy().getServerInfo(server.getName())); + server.incrementPlayerCount(1); + System.out.println("Sending " + event.getPlayer().getName() + " to " + server.getName() + "(" + server.getPublicAddress() + ")"); + _nextIndexMap.put(lobbyType, ++nextIndex); + } + }); + } + + public void run() + { + loadServers(); + + for (LobbyType type : LobbyType.values()) + { + if (!_plugin.getProxy().getServers().containsKey(type.getConnectName())) + { + _plugin.getProxy().getServers().put(type.getConnectName(), _plugin.getProxy().constructServerInfo(type.getConnectName(), new InetSocketAddress("lobby.mineplex.com", 25565), "LobbyBalancer", false)); + } + } + } + + public void loadServers() + { + Collection servers = _repository.getServerStatuses(); + synchronized (_serverLock) + { + long startTime = System.currentTimeMillis(); + _sortedLobbyMap.clear(); + for (LobbyType type : LobbyType.values()) + { + _sortedLobbyMap.put(type, new ArrayList<>()); + } + + for (MinecraftServer server : servers) + { + if (server.getName() == null) + continue; + + InetSocketAddress socketAddress = new InetSocketAddress(server.getPublicAddress(), server.getPort()); + _plugin.getProxy().getServers().put(server.getName(), _plugin.getProxy().constructServerInfo(server.getName(), socketAddress, "LobbyBalancer", false)); + + if (server.getMotd() != null && server.getMotd().contains("Restarting")) + { + continue; + } + + Arrays.stream(LobbyType.values()) + .filter(type -> server.getName().toUpperCase().startsWith(type.getUppercasePrefix())) + .findFirst() + .ifPresent(type -> _sortedLobbyMap.get(type).add(server)); + } + + _sortedLobbyMap.values().forEach(lobbies -> Collections.sort(lobbies, LOBBY_SORTER)); + + long timeSpentInLock = System.currentTimeMillis() - startTime; + + if (timeSpentInLock > 50) + System.out.println("[==] TIMING [==] Locked loading servers for " + timeSpentInLock + "ms"); + + _nextIndexMap.clear(); + } + } +} diff --git a/Mineplexer/Mineplex.Bungee.Mineplexer/src/mineplex/bungee/lobbyBalancer/LobbySorter.java b/Mineplexer/Mineplex.Bungee.Mineplexer/src/mineplex/bungee/lobbyBalancer/LobbySorter.java new file mode 100644 index 00000000..dd975173 --- /dev/null +++ b/Mineplexer/Mineplex.Bungee.Mineplexer/src/mineplex/bungee/lobbyBalancer/LobbySorter.java @@ -0,0 +1,48 @@ +package mineplex.bungee.lobbyBalancer; + +import java.util.Comparator; + +import mineplex.serverdata.data.MinecraftServer; + +public class LobbySorter implements Comparator +{ + @Override + public int compare(MinecraftServer first, MinecraftServer second) + { + if (second.getPlayerCount() == 999) + return -1; + + if (first.getPlayerCount() == 999) + return 1; + + if (first.getPlayerCount() < (first.getMaxPlayerCount() / 2) && second.getPlayerCount() >= (second.getMaxPlayerCount() / 2)) + return -1; + + if (second.getPlayerCount() < (second.getMaxPlayerCount() / 2) && first.getPlayerCount() >= (first.getMaxPlayerCount() / 2)) + return 1; + + if (first.getPlayerCount() < (first.getMaxPlayerCount() / 2)) + { + if (first.getPlayerCount() > second.getPlayerCount()) + return -1; + + if (second.getPlayerCount() > first.getPlayerCount()) + return 1; + } + else + { + if (first.getPlayerCount() < second.getPlayerCount()) + return -1; + + if (second.getPlayerCount() < first.getPlayerCount()) + return 1; + } + + if (Integer.parseInt(first.getName().split("-")[1]) < Integer.parseInt(second.getName().split("-")[1])) + return -1; + else if (Integer.parseInt(second.getName().split("-")[1]) < Integer.parseInt(first.getName().split("-")[1])) + return 1; + + return 0; + } +} diff --git a/Mineplexer/Mineplex.Bungee.Mineplexer/src/mineplex/bungee/lobbyBalancer/LobbyType.java b/Mineplexer/Mineplex.Bungee.Mineplexer/src/mineplex/bungee/lobbyBalancer/LobbyType.java new file mode 100644 index 00000000..8b4e4fb5 --- /dev/null +++ b/Mineplexer/Mineplex.Bungee.Mineplexer/src/mineplex/bungee/lobbyBalancer/LobbyType.java @@ -0,0 +1,34 @@ +package mineplex.bungee.lobbyBalancer; + +public enum LobbyType +{ + NORMAL("Lobby", "LOBBY-", "MainMotd"), + CLANS("ClansHub", "CLANSHUB-", "ClansMotd"), + BETA("BetaHub","BETAHUB-", "BetaMotd"), + ; + private final String _connectName; // The name of the server the player is connecting to + private final String _uppercasePrefix; // The (toUpperCase()) prefix given to servers of this lobby type + private final String _redisMotdKey; + + LobbyType(String connectName, String uppercasePrefix, String redisMotdKey) + { + _connectName = connectName; + _uppercasePrefix = uppercasePrefix; + _redisMotdKey = redisMotdKey; + } + + public String getConnectName() + { + return _connectName; + } + + public String getUppercasePrefix() + { + return _uppercasePrefix; + } + + public String getRedisMotdKey() + { + return _redisMotdKey; + } +} diff --git a/Mineplexer/Mineplex.Bungee.Mineplexer/src/mineplex/bungee/motd/GlobalMotd.java b/Mineplexer/Mineplex.Bungee.Mineplexer/src/mineplex/bungee/motd/GlobalMotd.java new file mode 100644 index 00000000..f8de8c52 --- /dev/null +++ b/Mineplexer/Mineplex.Bungee.Mineplexer/src/mineplex/bungee/motd/GlobalMotd.java @@ -0,0 +1,43 @@ +package mineplex.bungee.motd; + +import java.util.List; + +import mineplex.serverdata.data.Data; + +/** + * A GlobalMotd represents a set of MOTD packaged lines. + * @author MrTwiggy + * + */ +public class GlobalMotd implements Data +{ + // The unique name representing this MOTD set + private String _name; + + private String _headline; + public String getHeadline() { return _headline; } + + // List of lines describing the MOTD + private List _motd; + public List getMotd() { return _motd; } + + /** + * Constructor + * @param name + * @param motd + */ + public GlobalMotd(String name, String headline, List motd) + { + _name = name; + _headline = headline; + _motd = motd; + } + + /** + * Unique identifying ID associated with this {@link GlobalMotd}. + */ + public String getDataId() + { + return _name; + } +} diff --git a/Mineplexer/Mineplex.Bungee.Mineplexer/src/mineplex/bungee/motd/MotdManager.java b/Mineplexer/Mineplex.Bungee.Mineplexer/src/mineplex/bungee/motd/MotdManager.java new file mode 100644 index 00000000..233f334c --- /dev/null +++ b/Mineplexer/Mineplex.Bungee.Mineplexer/src/mineplex/bungee/motd/MotdManager.java @@ -0,0 +1,84 @@ +package mineplex.bungee.motd; + +import java.util.Arrays; +import java.util.EnumMap; +import java.util.Map; +import java.util.Optional; +import java.util.Random; +import java.util.concurrent.TimeUnit; + +import net.md_5.bungee.api.event.ProxyPingEvent; +import net.md_5.bungee.api.plugin.Listener; +import net.md_5.bungee.api.plugin.Plugin; +import net.md_5.bungee.event.EventHandler; + +import mineplex.bungee.lobbyBalancer.LobbyType; +import mineplex.serverdata.Region; +import mineplex.serverdata.data.DataRepository; +import mineplex.serverdata.redis.RedisDataRepository; +import mineplex.serverdata.servers.ServerManager; + +public class MotdManager implements Listener, Runnable +{ + private static final String DEFAULT_HEADLINE = " §b§l§m §8§l§m[ §r §9§lThieunang.club§r §f§lGames§r §8§l§m ]§b§l§m §r"; + + private final DataRepository _repository; + private final Random _random = new Random(); + private final Map motds = new EnumMap<>(LobbyType.class); + + public MotdManager(Plugin plugin) + { + plugin.getProxy().getScheduler().schedule(plugin, this, 5L, 30L, TimeUnit.SECONDS); + plugin.getProxy().getPluginManager().registerListener(plugin, this); + + _repository = new RedisDataRepository(ServerManager.getConnection(true, ServerManager.SERVER_STATUS_LABEL), ServerManager.getConnection(false, ServerManager.SERVER_STATUS_LABEL), + Region.ALL, GlobalMotd.class, "globalMotd"); + run(); + } + + @EventHandler + public void serverPing(ProxyPingEvent event) + { + + net.md_5.bungee.api.ServerPing serverPing = event.getResponse(); + Optional maybeType = Optional.empty(); + + if (event.getConnection().getListener() != null) + { + maybeType = Arrays.stream(LobbyType.values()) + .filter(type -> event.getConnection().getListener().getDefaultServer().equalsIgnoreCase(type.getConnectName())) + .findFirst(); + } + + LobbyType lobbyType = maybeType.orElse(LobbyType.NORMAL); + GlobalMotd globalMotd = motds.get(lobbyType); + + String motd = DEFAULT_HEADLINE; + if (globalMotd != null && globalMotd.getHeadline() != null) + { + motd = globalMotd.getHeadline() == null ? DEFAULT_HEADLINE : globalMotd.getHeadline(); + if (globalMotd.getMotd() != null) + { + motd += "\n" + globalMotd.getMotd().get(_random.nextInt(globalMotd.getMotd().size())); + } + } + + event.setResponse(new net.md_5.bungee.api.ServerPing(serverPing.getVersion(), serverPing.getPlayers(), motd, serverPing.getFaviconObject())); + } + + @Override + public void run() + { + /* + for (LobbyType type : LobbyType.values()) + { + GlobalMotd motd = _repository.getElement(type.getRedisMotdKey()); + + if (motd != null) + { + motds.put(type, motd); + } + } + */ + } +} diff --git a/Mineplexer/Mineplex.Bungee.Mineplexer/src/mineplex/bungee/playerCount/PlayerCount.java b/Mineplexer/Mineplex.Bungee.Mineplexer/src/mineplex/bungee/playerCount/PlayerCount.java new file mode 100644 index 00000000..196c8485 --- /dev/null +++ b/Mineplexer/Mineplex.Bungee.Mineplexer/src/mineplex/bungee/playerCount/PlayerCount.java @@ -0,0 +1,118 @@ +package mineplex.bungee.playerCount; + +import java.io.File; +import java.util.concurrent.TimeUnit; + +import mineplex.bungee.status.InternetStatus; +import mineplex.serverdata.Region; +import mineplex.serverdata.data.BungeeServer; +import mineplex.serverdata.data.DataRepository; +import mineplex.serverdata.redis.RedisDataRepository; +import mineplex.serverdata.servers.ConnectionData; +import mineplex.serverdata.servers.ConnectionData.ConnectionType; +import mineplex.serverdata.servers.ServerManager; +import net.md_5.bungee.api.ServerPing.Players; +import net.md_5.bungee.api.config.ListenerInfo; +import net.md_5.bungee.api.event.ProxyPingEvent; +import net.md_5.bungee.api.plugin.Listener; +import net.md_5.bungee.api.plugin.Plugin; +import net.md_5.bungee.event.EventHandler; + +public class PlayerCount implements Listener, Runnable +{ + private DataRepository _repository; + private DataRepository _secondRepository; + private Region _region; + + private ListenerInfo _listenerInfo; + private Plugin _plugin; + + private int _totalPlayers = -1; + + public PlayerCount(Plugin plugin) + { + _region = !new File("eu.dat").exists() ? Region.US : Region.EU; + _plugin = plugin; + + _plugin.getProxy().getScheduler().schedule(_plugin, this, 4L, 4L, TimeUnit.SECONDS); + _plugin.getProxy().getPluginManager().registerListener(_plugin, this); + + for (ListenerInfo info : _plugin.getProxy().getConfigurationAdapter().getListeners()) + { + if (info.getDefaultServer().equalsIgnoreCase("Lobby")) + { + _listenerInfo = info; + } + } + + _repository = new RedisDataRepository(ServerManager.getConnection(true, ServerManager.SERVER_STATUS_LABEL), ServerManager.getConnection(false, ServerManager.SERVER_STATUS_LABEL), + Region.ALL, BungeeServer.class, "bungeeServers"); + + if (_region == Region.US) + _secondRepository = new RedisDataRepository(new ConnectionData("127.0.0.1", 6379, ConnectionType.MASTER, "ServerStatus"), new ConnectionData("127.0.0.1", 6379, ConnectionType.SLAVE, "ServerStatus"), + Region.ALL, BungeeServer.class, "bungeeServers"); + else + _secondRepository = new RedisDataRepository(new ConnectionData("127.0.0.1", 6379, ConnectionType.MASTER, "ServerStatus"), new ConnectionData("127.0.0.1", 6379, ConnectionType.SLAVE, "ServerStatus"), + Region.ALL, BungeeServer.class, "bungeeServers"); + } + + public void run() + { + BungeeServer snapshot = generateSnapshot(); + if (snapshot != null) + { + _repository.addElement(snapshot, 15); // Update with a 15 second expiry on session + } + + _totalPlayers = fetchPlayerCount(); + } + + /** + * @return an up-to-date total player count across all active Bungee Servers. + */ + private int fetchPlayerCount() + { + int totalPlayers = 0; + for (BungeeServer server : _repository.getElements()) + { + totalPlayers += server.getPlayerCount(); + } + + for (BungeeServer server : _secondRepository.getElements()) + { + totalPlayers += server.getPlayerCount(); + } + + return totalPlayers; + } + + /** + * @return a newly instantiated {@link BungeeServer} snapshot of the current state of this server. + */ + private BungeeServer generateSnapshot() + { + if (_listenerInfo == null) + { + return null; + } + String name = _listenerInfo.getHost().getAddress().getHostAddress(); + String host = _listenerInfo.getHost().getAddress().getHostAddress(); + int port = _listenerInfo.getHost().getPort(); + boolean connected = InternetStatus.isConnected(); + int playerCount = _plugin.getProxy().getOnlineCount(); + return new BungeeServer(name, _region, host, port, playerCount, connected); + } + + @EventHandler + public void ServerPing(ProxyPingEvent event) + { + net.md_5.bungee.api.ServerPing serverPing = event.getResponse(); + + event.setResponse(new net.md_5.bungee.api.ServerPing(serverPing.getVersion(), new Players(_totalPlayers + 1, _totalPlayers, null), serverPing.getDescription(), serverPing.getFaviconObject())); + } + + public int getTotalPlayers() + { + return _totalPlayers; + } +} diff --git a/Mineplexer/Mineplex.Bungee.Mineplexer/src/mineplex/bungee/playerStats/PlayerStats.java b/Mineplexer/Mineplex.Bungee.Mineplexer/src/mineplex/bungee/playerStats/PlayerStats.java new file mode 100644 index 00000000..525d201c --- /dev/null +++ b/Mineplexer/Mineplex.Bungee.Mineplexer/src/mineplex/bungee/playerStats/PlayerStats.java @@ -0,0 +1,141 @@ +package mineplex.bungee.playerStats; + +import java.util.HashSet; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +import mineplex.bungee.playerStats.data.IpInfo; +import mineplex.cache.player.PlayerCache; +import mineplex.cache.player.PlayerInfo; +import net.md_5.bungee.api.event.PlayerDisconnectEvent; +import net.md_5.bungee.api.event.PostLoginEvent; +import net.md_5.bungee.api.plugin.Listener; +import net.md_5.bungee.api.plugin.Plugin; +import net.md_5.bungee.event.EventHandler; + +public class PlayerStats implements Listener, Runnable +{ + private Plugin _plugin; + private PlayerStatsRepository _repository; + + private HashSet _retrievingPlayerInfo = new HashSet(); + + public PlayerStats(Plugin plugin) + { + _plugin = plugin; + + _plugin.getProxy().getScheduler().schedule(_plugin, this, 5L, 5L, TimeUnit.MINUTES); + _plugin.getProxy().getPluginManager().registerListener(_plugin, this); + + _repository = new PlayerStatsRepository(); + } + + @EventHandler + public void playerConnect(final PostLoginEvent event) + { + _plugin.getProxy().getScheduler().runAsync(_plugin, new Runnable() + { + public void run() + { + String address = event.getPlayer().getPendingConnection().getAddress().getAddress().getHostAddress(); + UUID uuid = event.getPlayer().getUniqueId(); + String name = event.getPlayer().getName(); + int version = event.getPlayer().getPendingConnection().getVersion(); + + try + { + PlayerInfo playerInfo = null; + IpInfo ipInfo = _repository.getIp(address); + + boolean addOrUpdatePlayer = false; + + playerInfo = PlayerCache.getInstance().getPlayer(uuid); + + if (playerInfo == null) + { + addOrUpdatePlayer = true; + _retrievingPlayerInfo.add(uuid); + } + + if (!addOrUpdatePlayer) + { + if (playerInfo.getVersion() != version) + addOrUpdatePlayer = true; + else if (!playerInfo.getName().equalsIgnoreCase(name)) + addOrUpdatePlayer = true; + } + + if (addOrUpdatePlayer) + { + // Just update? what about other properties? + PlayerInfo updatedPlayerInfo = _repository.getPlayer(uuid, name, version); + + if (playerInfo != null) + { + playerInfo.setName(updatedPlayerInfo.getName()); + playerInfo.setVersion(updatedPlayerInfo.getVersion()); + } + else + playerInfo = updatedPlayerInfo; + } + + playerInfo.setSessionId(_repository.updatePlayerStats(playerInfo.getId(), ipInfo.id)); + playerInfo.updateLoginTime(); + PlayerCache.getInstance().addPlayer(playerInfo); + } + finally + { + _retrievingPlayerInfo.remove(uuid); + } + } + }); + } + + @EventHandler + public void playerDisconnect(final PlayerDisconnectEvent event) + { + _plugin.getProxy().getScheduler().runAsync(_plugin, new Runnable() + { + public void run() + { + UUID uuid = event.getPlayer().getUniqueId(); + + PlayerInfo playerInfo = null; + + playerInfo = PlayerCache.getInstance().getPlayer(uuid); + + int timeout = 5; + + while (playerInfo == null && _retrievingPlayerInfo.contains(uuid) && timeout <= 5) + { + playerInfo = PlayerCache.getInstance().getPlayer(uuid); + + if (playerInfo != null) + break; + + System.out.println("ERROR - Player disconnecting and isn't in cache... sleeping"); + + try + { + Thread.sleep(500); + } + catch (InterruptedException e) + { + e.printStackTrace(); + } + + timeout++; + } + + System.out.println(playerInfo.getName() + ":" + playerInfo.getSessionId()); + _repository.updatePlayerSession(playerInfo.getSessionId()); + } + }); + } + + @Override + public void run() + { + PlayerCache.getInstance().clean(); + } +} diff --git a/Mineplexer/Mineplex.Bungee.Mineplexer/src/mineplex/bungee/playerStats/PlayerStatsRepository.java b/Mineplexer/Mineplex.Bungee.Mineplexer/src/mineplex/bungee/playerStats/PlayerStatsRepository.java new file mode 100644 index 00000000..177972ad --- /dev/null +++ b/Mineplexer/Mineplex.Bungee.Mineplexer/src/mineplex/bungee/playerStats/PlayerStatsRepository.java @@ -0,0 +1,295 @@ +package mineplex.bungee.playerStats; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.UUID; + +import mineplex.bungee.playerStats.data.IpInfo; +import mineplex.cache.player.PlayerInfo; +import mineplex.serverdata.database.DBPool; +import mineplex.serverdata.database.RepositoryBase; + +import javax.sql.DataSource; + +public class PlayerStatsRepository extends RepositoryBase +{ + private static String INSERT_PLAYERINFO = "INSERT INTO playerInfo (uuid, name, version) VALUES (?, ?, ?);"; + private static String SELECT_PLAYERINFO = "SELECT id, name, version FROM playerInfo WHERE uuid = ?;"; + private static String UPDATE_PLAYERINFO = "UPDATE playerInfo SET name = ?, version = ? WHERE id = ?;"; + + private static String INSERT_IPINFO = "INSERT INTO ipInfo (ipAddress) VALUES (?);"; + private static String SELECT_IPINFO = "SELECT id FROM ipInfo WHERE ipAddress = ?;"; + + private static String UPDATE_PLAYERSTATS = "INSERT IGNORE INTO playerIps (playerInfoId, ipInfoId, date) VALUES (?, ?, curdate());" + + "INSERT IGNORE INTO playerUniqueLogins (playerInfoId, day) values(?, curdate());" + + "INSERT IGNORE INTO playerLoginSessions (playerInfoId, loginTime) values(?, now());"; + + private static String UPDATE_LOGINSESSION = "UPDATE playerLoginSessions SET timeInGame = TIME_TO_SEC(TIMEDIFF(now(), loginTime)) / 60 WHERE id = ?;"; + + public PlayerStatsRepository() + { + super(DBPool.getPlayerStats()); + } + + public PlayerInfo getPlayer(UUID uuid, String name, int version) + { + PlayerInfo playerInfo = null; + PreparedStatement preparedStatement = null; + ResultSet resultSet = null; + + try(Connection connection = getConnection()) + { + preparedStatement = connection.prepareStatement(SELECT_PLAYERINFO); + + preparedStatement.setString(1, uuid.toString()); + + resultSet = preparedStatement.executeQuery(); + + while (resultSet.next()) + { + playerInfo = new PlayerInfo(resultSet.getInt(1), uuid, resultSet.getString(2), resultSet.getInt(3)); + } + + resultSet.close(); + preparedStatement.close(); + + if (playerInfo == null) + { + preparedStatement = connection.prepareStatement(INSERT_PLAYERINFO, Statement.RETURN_GENERATED_KEYS); + preparedStatement.setString(1, uuid.toString()); + preparedStatement.setString(2, name); + preparedStatement.setInt(3, version); + + preparedStatement.executeUpdate(); + + int id = 0; + + resultSet = preparedStatement.getGeneratedKeys(); + + while (resultSet.next()) + { + id = resultSet.getInt(1); + } + + playerInfo = new PlayerInfo(id, uuid, name, version); + + resultSet.close(); + preparedStatement.close(); + } + else if (!playerInfo.getName().equalsIgnoreCase(name) || playerInfo.getVersion() != version) + { + preparedStatement = connection.prepareStatement(UPDATE_PLAYERINFO); + preparedStatement.setString(1, name); + preparedStatement.setInt(2, version); + preparedStatement.setInt(3, playerInfo.getId()); + + preparedStatement.executeUpdate(); + preparedStatement.close(); + } + } + catch (Exception exception) + { + exception.printStackTrace(); + } + finally + { + if (preparedStatement != null) + { + try + { + preparedStatement.close(); + } + catch (SQLException e) + { + e.printStackTrace(); + } + } + + if (resultSet != null) + { + try + { + resultSet.close(); + } + catch (SQLException e) + { + e.printStackTrace(); + } + } + } + + return playerInfo; + } + + public IpInfo getIp(String ipAddress) + { + IpInfo ipInfo = null; + PreparedStatement preparedStatement = null; + ResultSet resultSet = null; + + try(Connection connection = getConnection()) + { + preparedStatement = connection.prepareStatement(SELECT_IPINFO); + preparedStatement.setString(1, ipAddress); + + resultSet = preparedStatement.executeQuery(); + + while (resultSet.next()) + { + ipInfo = new IpInfo(); + ipInfo.id = resultSet.getInt(1); + ipInfo.ipAddress = ipAddress; + } + + resultSet.close(); + preparedStatement.close(); + + if (ipInfo == null) + { + preparedStatement = connection.prepareStatement(INSERT_IPINFO, Statement.RETURN_GENERATED_KEYS); + preparedStatement.setString(1, ipAddress); + + preparedStatement.executeUpdate(); + + int id = 0; + + resultSet = preparedStatement.getGeneratedKeys(); + + while (resultSet.next()) + { + id = resultSet.getInt(1); + } + + ipInfo = new IpInfo(); + ipInfo.id = id; + ipInfo.ipAddress = ipAddress; + + resultSet.close(); + preparedStatement.close(); + } + } + catch (Exception exception) + { + exception.printStackTrace(); + } + finally + { + if (preparedStatement != null) + { + try + { + preparedStatement.close(); + } + catch (SQLException e) + { + e.printStackTrace(); + } + } + + if (resultSet != null) + { + try + { + resultSet.close(); + } + catch (SQLException e) + { + e.printStackTrace(); + } + } + } + + return ipInfo; + } + + public int updatePlayerStats(int playerId, int ipId) + { + Statement statement = null; + ResultSet resultSet= null; + + try(Connection connection = getConnection()) + { + statement = connection.createStatement(); + + String queryString = UPDATE_PLAYERSTATS; + queryString = queryString.replaceFirst("\\?", playerId + ""); + queryString = queryString.replaceFirst("\\?", ipId + ""); + queryString = queryString.replaceFirst("\\?", playerId + ""); + queryString = queryString.replaceFirst("\\?", playerId + ""); + + statement.executeUpdate(queryString, Statement.RETURN_GENERATED_KEYS); + + statement.getMoreResults(); + statement.getMoreResults(); + resultSet = statement.getGeneratedKeys(); + + while (resultSet.next()) + { + return resultSet.getInt(1); + } + } + catch (Exception exception) + { + exception.printStackTrace(); + } + finally + { + try + { + if (statement != null) + statement.close(); + } + catch (Exception exception) + { + exception.printStackTrace(); + } + + try + { + if (resultSet != null) + resultSet.close(); + } + catch (Exception exception) + { + exception.printStackTrace(); + } + } + + return -1; + } + + public void updatePlayerSession(int loginSessionId) + { + PreparedStatement preparedStatement = null; + + try(Connection connection = getConnection()) + { + preparedStatement = connection.prepareStatement(UPDATE_LOGINSESSION); + preparedStatement.setInt(1, loginSessionId); + + preparedStatement.executeUpdate(); + } + catch (Exception exception) + { + exception.printStackTrace(); + } + finally + { + if (preparedStatement != null) + { + try + { + preparedStatement.close(); + } + catch (SQLException e) + { + e.printStackTrace(); + } + } + } + } +} diff --git a/Mineplexer/Mineplex.Bungee.Mineplexer/src/mineplex/bungee/playerStats/data/IpInfo.java b/Mineplexer/Mineplex.Bungee.Mineplexer/src/mineplex/bungee/playerStats/data/IpInfo.java new file mode 100644 index 00000000..ab2c0996 --- /dev/null +++ b/Mineplexer/Mineplex.Bungee.Mineplexer/src/mineplex/bungee/playerStats/data/IpInfo.java @@ -0,0 +1,7 @@ +package mineplex.bungee.playerStats.data; + +public class IpInfo +{ + public int id; + public String ipAddress; +} diff --git a/Mineplexer/Mineplex.Bungee.Mineplexer/src/mineplex/bungee/playerTracker/PlayerJoinHandler.java b/Mineplexer/Mineplex.Bungee.Mineplexer/src/mineplex/bungee/playerTracker/PlayerJoinHandler.java new file mode 100644 index 00000000..19c52c0a --- /dev/null +++ b/Mineplexer/Mineplex.Bungee.Mineplexer/src/mineplex/bungee/playerTracker/PlayerJoinHandler.java @@ -0,0 +1,26 @@ +package mineplex.bungee.playerTracker; + +import java.util.UUID; +import mineplex.serverdata.commands.CommandCallback; +import mineplex.serverdata.commands.PlayerJoinCommand; +import mineplex.serverdata.commands.ServerCommand; + +public class PlayerJoinHandler implements CommandCallback +{ + private PlayerTracker _playerTracker; + + public PlayerJoinHandler(PlayerTracker playerTracker) + { + _playerTracker = playerTracker; + } + + @Override + public void run(ServerCommand command) + { + if (command instanceof PlayerJoinCommand) + { + PlayerJoinCommand joinCommand = (PlayerJoinCommand)command; + _playerTracker.kickPlayerIfOnline(UUID.fromString(joinCommand.getUuid())); + } + } +} diff --git a/Mineplexer/Mineplex.Bungee.Mineplexer/src/mineplex/bungee/playerTracker/PlayerTracker.java b/Mineplexer/Mineplex.Bungee.Mineplexer/src/mineplex/bungee/playerTracker/PlayerTracker.java new file mode 100644 index 00000000..e968020c --- /dev/null +++ b/Mineplexer/Mineplex.Bungee.Mineplexer/src/mineplex/bungee/playerTracker/PlayerTracker.java @@ -0,0 +1,106 @@ +package mineplex.bungee.playerTracker; + +import java.util.List; +import java.util.UUID; + +import com.google.common.collect.Lists; +import com.google.gson.Gson; + +import mineplex.serverdata.Region; +import mineplex.serverdata.commands.PlayerJoinCommand; +import mineplex.serverdata.commands.ServerCommandManager; +import mineplex.serverdata.data.DataRepository; +import mineplex.serverdata.data.PlayerStatus; +import mineplex.serverdata.redis.RedisDataRepository; +import mineplex.serverdata.servers.ServerManager; +import net.md_5.bungee.api.chat.TextComponent; +import net.md_5.bungee.api.connection.ProxiedPlayer; +import net.md_5.bungee.api.event.PlayerDisconnectEvent; +import net.md_5.bungee.api.event.PostLoginEvent; +import net.md_5.bungee.api.event.ServerConnectedEvent; +import net.md_5.bungee.api.plugin.Listener; +import net.md_5.bungee.api.plugin.Plugin; +import net.md_5.bungee.event.EventHandler; + +public class PlayerTracker implements Listener +{ + private static final int DEFAULT_STATUS_TIMEOUT = 60 * 60 * 8; + + private DataRepository _repository; + + private Plugin _plugin; + + private final List _ignoreKick = Lists.newArrayList(); + + public PlayerTracker(Plugin plugin) + { + _plugin = plugin; + + _plugin.getProxy().getPluginManager().registerListener(_plugin, this); + + _repository = new RedisDataRepository(ServerManager.getMasterConnection(), ServerManager.getSlaveConnection(), + Region.currentRegion(), PlayerStatus.class, "playerStatus"); + + ServerCommandManager.getInstance().initializeServer("BUNGEE ENABLE - " + System.currentTimeMillis(), new Gson()); + ServerCommandManager.getInstance().registerCommandType(mineplex.serverdata.commands.PlayerJoinCommand.class, new PlayerJoinHandler(this)); + + System.out.println("Initialized PlayerTracker."); + } + + public Plugin getPlugin() + { + return _plugin; + } + + @EventHandler + public void playerConnect(final ServerConnectedEvent event) + { + _plugin.getProxy().getScheduler().runAsync(_plugin, new Runnable() + { + public void run() + { + PlayerStatus snapshot = new PlayerStatus(event.getPlayer().getUniqueId(), event.getPlayer().getName(), event.getServer().getInfo().getName()); + _repository.addElement(snapshot, DEFAULT_STATUS_TIMEOUT); + } + }); + } + + @EventHandler + public void playerDisconnect(final PlayerDisconnectEvent event) + { + _plugin.getProxy().getScheduler().runAsync(_plugin, new Runnable() + { + public void run() + { + _repository.removeElement(event.getPlayer().getUniqueId().toString()); + } + }); + } + + @EventHandler + public void playerConnect(final PostLoginEvent event) + { + _ignoreKick.add(event.getPlayer().getUniqueId()); + PlayerJoinCommand command = new PlayerJoinCommand(event.getPlayer().getUniqueId(), event.getPlayer().getName()); + command.publish(); + } + + public boolean isPlayerOnline(UUID uuid) + { + return _plugin.getProxy().getPlayer(uuid) != null; + } + + public void kickPlayerIfOnline(UUID uuid) + { + if (_ignoreKick.remove(uuid)) + { + return; + } + if (isPlayerOnline(uuid)) + { + ProxiedPlayer player = _plugin.getProxy().getPlayer(uuid); + + player.disconnect(new TextComponent("You have logged in from another location.")); + } + } +} diff --git a/Mineplexer/Mineplex.Bungee.Mineplexer/src/mineplex/bungee/status/InternetStatus.java b/Mineplexer/Mineplex.Bungee.Mineplexer/src/mineplex/bungee/status/InternetStatus.java new file mode 100644 index 00000000..f4ee8ef8 --- /dev/null +++ b/Mineplexer/Mineplex.Bungee.Mineplexer/src/mineplex/bungee/status/InternetStatus.java @@ -0,0 +1,55 @@ +package mineplex.bungee.status; + +import java.io.File; +import java.io.IOException; +import java.net.Socket; +import java.util.concurrent.TimeUnit; + +import net.md_5.bungee.api.config.ListenerInfo; +import net.md_5.bungee.api.plugin.Plugin; + +public class InternetStatus implements Runnable +{ + // Current internet connectivity status + private static boolean _connected = true; + public static boolean isConnected() { return _connected; } + + private Plugin _plugin; + + public InternetStatus(Plugin plugin) + { + _plugin = plugin; + _plugin.getProxy().getScheduler().schedule(_plugin, this, 1L, 1L, TimeUnit.MINUTES); + + System.out.println("Initialized InternetStatus."); + } + + @Override + public void run() + { + _connected = isOnline(); // Update _connected flag. + } + + private boolean isOnline() + { + return testUrl("www.google.com") + || testUrl("www.espn.com") + || testUrl("www.bing.com"); + } + + private boolean testUrl(String url) + { + boolean reachable = false; + + try (Socket socket = new Socket(url, 80)) + { + reachable = true; + } + catch (Exception e) + { + // Meh i don't care + } + + return reachable; + } +} diff --git a/Mineplexer/Mineplex.Cache/Mineplex.Cache.iml b/Mineplexer/Mineplex.Cache/Mineplex.Cache.iml new file mode 100644 index 00000000..54892e83 --- /dev/null +++ b/Mineplexer/Mineplex.Cache/Mineplex.Cache.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/Mineplexer/Mineplex.Cache/src/mineplex/cache/player/PlayerCache.java b/Mineplexer/Mineplex.Cache/src/mineplex/cache/player/PlayerCache.java new file mode 100644 index 00000000..d980a92d --- /dev/null +++ b/Mineplexer/Mineplex.Cache/src/mineplex/cache/player/PlayerCache.java @@ -0,0 +1,110 @@ +package mineplex.cache.player; + +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +import mineplex.serverdata.Region; +import mineplex.serverdata.redis.RedisDataRepository; +import mineplex.serverdata.redis.atomic.RedisStringRepository; +import mineplex.serverdata.servers.ServerManager; + +public enum PlayerCache +{ + INSTANCE; + + public static PlayerCache getInstance() + { + return INSTANCE; + } + + private final RedisDataRepository _playerInfoRepository; + private final RedisStringRepository _accountIdRepository; + + PlayerCache() + { + _playerInfoRepository = new RedisDataRepository( + ServerManager.getMasterConnection(), + ServerManager.getSlaveConnection(), + Region.ALL, + PlayerInfo.class, + "playercache"); + + _accountIdRepository = new RedisStringRepository( + ServerManager.getMasterConnection(), + ServerManager.getSlaveConnection(), + Region.ALL, + "accountid", + (int) TimeUnit.HOURS.toSeconds(6) + ); + } + + public void addPlayer(PlayerInfo player) + { + try + { + _playerInfoRepository.addElement(player, 60 * 60 * 6); // 6 Hours + } + catch (Exception exception) + { + System.out.println("Error adding player info in PlayerCache : " + exception.getMessage()); + exception.printStackTrace(); + } + } + + public PlayerInfo getPlayer(UUID uuid) + { + try + { + return _playerInfoRepository.getElement(uuid.toString()); + } + catch (Exception exception) + { + System.out.println("Error retrieving player info in PlayerCache : " + exception.getMessage()); + exception.printStackTrace(); + } + + return null; + } + + /** + * Attempts to grab a player's account ID from the cache + * + * @param uuid Minecraft Account UUID + * @return The account id of the player, or -1 if the player is not in the cache + */ + public int getAccountId(UUID uuid) + { + String accountIdStr = _accountIdRepository.get(uuid.toString()); + + if (accountIdStr == null) + return -1; + + try + { + int accountId = Integer.parseInt(accountIdStr); + if (accountId <= 0) + { + // remove invalid account id + _accountIdRepository.del(uuid.toString()); + return -1; + } + return accountId; + } + catch (NumberFormatException ex) + { + // remove invalid account id + _accountIdRepository.del(uuid.toString()); + return -1; + } + } + + public void updateAccountId(UUID uuid, int newId) + { + _accountIdRepository.set(uuid.toString(), String.valueOf(newId)); + } + + public void clean() + { + _playerInfoRepository.clean(); + } +} diff --git a/Mineplexer/Mineplex.Cache/src/mineplex/cache/player/PlayerInfo.java b/Mineplexer/Mineplex.Cache/src/mineplex/cache/player/PlayerInfo.java new file mode 100644 index 00000000..5719f661 --- /dev/null +++ b/Mineplexer/Mineplex.Cache/src/mineplex/cache/player/PlayerInfo.java @@ -0,0 +1,93 @@ +package mineplex.cache.player; + +import java.util.UUID; + +import mineplex.serverdata.Utility; +import mineplex.serverdata.data.Data; + +public class PlayerInfo implements Data +{ + private int _id; + private int _accountId; + private UUID _uuid; + private String _name; + private boolean _online; + private long _lastUniqueLogin; + private long _loginTime; + private int _sessionId; + private int _version; + + public PlayerInfo(int id, UUID uuid, String name, int version) + { + _id = id; + _uuid = uuid; + _name = name; + _version = version; + } + + @Override + public String getDataId() + { + return _uuid.toString(); + } + + public int getId() + { + return _id; + } + + public UUID getUUID() + { + return _uuid; + } + + public String getName() + { + return _name; + } + + public boolean getOnline() + { + return _online; + } + + public long getLastUniqueLogin() + { + return _lastUniqueLogin; + } + + public long getLoginTime() + { + return _loginTime; + } + + public int getSessionId() + { + return _sessionId; + } + + public int getVersion() + { + return _version; + } + + public void setSessionId(int sessionId) + { + _sessionId = sessionId; + } + + public void setName(String name) + { + _name = name; + } + + public void setVersion(int version) + { + _version = version; + } + + public void updateLoginTime() + { + _loginTime = Utility.currentTimeMillis(); + } +} diff --git a/Mineplexer/Mineplex.ServerData/Mineplex.ServerData.iml b/Mineplexer/Mineplex.ServerData/Mineplex.ServerData.iml new file mode 100644 index 00000000..5bc772d2 --- /dev/null +++ b/Mineplexer/Mineplex.ServerData/Mineplex.ServerData.iml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/Region.java b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/Region.java new file mode 100644 index 00000000..957c7a47 --- /dev/null +++ b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/Region.java @@ -0,0 +1,25 @@ +package mineplex.serverdata; + +import java.io.File; + +/** + * Region enumerates the various geographical regions where Mineplex servers are + * hosted. + * @author Ty + * + */ +public enum Region +{ + US, + EU, + ALL; + + + /** + * @return the geographical {@link Region} of the current running process. + */ + public static Region currentRegion() + { + return !new File("eu.dat").exists() ? Region.US : Region.EU; + } +} diff --git a/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/Utility.java b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/Utility.java new file mode 100644 index 00000000..1a4c1ef7 --- /dev/null +++ b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/Utility.java @@ -0,0 +1,195 @@ +package mineplex.serverdata; + +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Logger; + +import com.google.gson.Gson; + +import mineplex.serverdata.servers.ConnectionData; +import mineplex.serverdata.servers.ServerManager; + +import net.md_5.bungee.api.connection.Server; +import net.md_5.bungee.api.plugin.Plugin; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.JedisPoolConfig; + +/** + * Utility offers various necessary utility-based methods for use in Mineplex.ServerData. + * @author Ty + * + */ +public class Utility +{ + private static boolean _retrievedRedisTime = false; + private static long _millisTimeDifference; + + // The Gson instance used to serialize/deserialize objects in JSON form. + private static Gson _gson = new Gson(); + public static Gson getGson() { return _gson; } + + // map of all instantiated connection pools, distinguished by their ip:port combination + private static final ConcurrentHashMap _pools = new ConcurrentHashMap(); + + // Public static jedis pool for interacting with central default jedis repo. + private static JedisPool _masterPool; + private static JedisPool _slavePool; + private static final Object _poolLock = new Object(); + + /** + * @param object - the (non-null) object to serialize + * @return the serialized form of {@code object}. + */ + public static String serialize(Object object) + { + return _gson.toJson(object); + } + + /** + * @param serializedData - the serialized data to be deserialized + * @param type - the resulting class type of the object to be deserialized + * @return the deserialized form of {@code serializedData} for class {@code type}. + */ + public static T deserialize(String serializedData, Class type) + { + if (serializedData == null) return null; + + return _gson.fromJson(serializedData, type); + } + + /** + * @param delimiter - the delimiter character used to separate the concatenated elements + * @param elements - the set of string elements to be concatenated and returned. + * @return the concatenated string of all {@code elements} separated by the {@code delimiter}. + */ + public static String concatenate(char delimiter, String... elements) + { + int length = elements.length; + String result = length > 0 ? elements[0] : new String(); + + for (int i = 1; i < length; i++) + { + result += delimiter + elements[i]; + } + + return result; + } + + /** + * @return the current timestamp (in seconds) fetched from the central jedis repository + * for synced timestamps. + */ + public static long currentTimeSeconds() + { + if (!_retrievedRedisTime) + setTimeDifference(); + + return (System.currentTimeMillis() + _millisTimeDifference) / 1000; + } + + /** + * @return the current timestamp (in milliseconds) fetched from the central jedis repository + * for synced timestamps. + */ + public static long currentTimeMillis() + { + if (!_retrievedRedisTime) + setTimeDifference(); + + return System.currentTimeMillis() + _millisTimeDifference; + } + + /** + * @param connData - the connection data specifying the database to be connected to. + * @return a newly instantiated {@link JedisPool} connected to the provided {@link ConnectionData} repository. + */ + public static JedisPool generatePool(ConnectionData connData) + { + synchronized(_poolLock) + { + String key = "127.0.0.1:6379"; + if(connData != null){ + key = getConnKey(connData); + } + JedisPool pool = _pools.get(key); + + if (pool == null) + { + JedisPoolConfig jedisPoolConfig = new JedisPoolConfig(); + + jedisPoolConfig.setMaxWaitMillis(1000); + jedisPoolConfig.setMinIdle(5); + jedisPoolConfig.setTestOnBorrow(true); + + jedisPoolConfig.setMaxTotal(20); + jedisPoolConfig.setBlockWhenExhausted(true); + + if(connData != null){ + pool = new JedisPool(jedisPoolConfig, connData.getHost(), connData.getPort()); + }else{ + pool = new JedisPool(jedisPoolConfig, "127.0.0.1", 6379); + } + _pools.put(key, pool); + } + + return pool; + } + } + + /** + * @param writeable - whether or not the Jedis connections returned should be writeable to. + * @return a globally available {@link JedisPool} + */ + public static JedisPool getPool(boolean writeable) + { + if (writeable) + { + if (_masterPool == null) + { + _masterPool = generatePool(ServerManager.getMasterConnection()); + } + + return _masterPool; + } + else + { + if (_slavePool == null) + { + ConnectionData slave = ServerManager.getSlaveConnection(); + + _slavePool = generatePool(slave); + } + + return _slavePool; + } + } + + private static String getConnKey(ConnectionData connData) + { + return connData.getHost() + ":" + connData.getPort(); + } + + private static void setTimeDifference() + { + long currentTime = 0; + JedisPool pool = getPool(false); + + try (Jedis jedis = pool.getResource()) + { + // Try multiple times in case one isn't valid + // Addresses an error in sentry + List times = jedis.time(); + for (String time : times.subList(0, Math.min(5, times.size()))) + { + try + { + currentTime = Long.parseLong(time); + break; + } catch (NumberFormatException ex) { } + } + } + + _millisTimeDifference = (currentTime * 1000) - System.currentTimeMillis(); + } +} diff --git a/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/commands/AddPunishCommand.java b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/commands/AddPunishCommand.java new file mode 100644 index 00000000..6844e430 --- /dev/null +++ b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/commands/AddPunishCommand.java @@ -0,0 +1,32 @@ +package mineplex.serverdata.commands; + +import java.util.UUID; + +public class AddPunishCommand extends ServerCommand +{ + private final String _target; + private final String _category; + private final String _sentence; + private final String _reason; + private final long _duration; + private final String _admin; + private final String _adminUUID; + private final int _severity; + + public AddPunishCommand(String finalPlayerName, int severity, String category, String sentence, String reason, long duration, String finalCallerName, String uuid) + { + this._target = finalPlayerName; + this._severity = severity; + this._category = category; + this._sentence = sentence; + this._reason = reason; + this._duration = duration; + this._admin = finalCallerName; + this._adminUUID = uuid; + } + + @Override + public void run() + { + } +} diff --git a/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/commands/AnnouncementCommand.java b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/commands/AnnouncementCommand.java new file mode 100644 index 00000000..ebb8804a --- /dev/null +++ b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/commands/AnnouncementCommand.java @@ -0,0 +1,26 @@ +package mineplex.serverdata.commands; + + +public class AnnouncementCommand extends ServerCommand +{ + private boolean _displayTitle; + private String _rank; + private String _message; + + public boolean getDisplayTitle() { return _displayTitle; } + public String getRank() { return _rank; } + public String getMessage() { return _message; } + + public AnnouncementCommand(boolean displayTitle, String rank, String message) + { + _displayTitle = displayTitle; + _rank = rank; + _message = message; + } + + @Override + public void run() + { + // Utilitizes a callback functionality to seperate dependencies + } +} diff --git a/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/commands/CommandCallback.java b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/commands/CommandCallback.java new file mode 100644 index 00000000..fcf12abc --- /dev/null +++ b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/commands/CommandCallback.java @@ -0,0 +1,6 @@ +package mineplex.serverdata.commands; + +public interface CommandCallback +{ + void run(T command); +} \ No newline at end of file diff --git a/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/commands/CommandType.java b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/commands/CommandType.java new file mode 100644 index 00000000..4fe680f4 --- /dev/null +++ b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/commands/CommandType.java @@ -0,0 +1,16 @@ +package mineplex.serverdata.commands; + +public class CommandType +{ + private Class _commandClazz; + public Class getCommandType() { return _commandClazz; } + + private CommandCallback _commandCallback; + public CommandCallback getCallback() { return _commandCallback; } + + public CommandType(Class commandClazz, CommandCallback commandCallback) + { + _commandClazz = commandClazz; + _commandCallback = commandCallback; + } +} \ No newline at end of file diff --git a/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/commands/PlayerJoinCommand.java b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/commands/PlayerJoinCommand.java new file mode 100644 index 00000000..424ae0c3 --- /dev/null +++ b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/commands/PlayerJoinCommand.java @@ -0,0 +1,31 @@ +package mineplex.serverdata.commands; + +import java.util.UUID; + +public class PlayerJoinCommand extends ServerCommand +{ + private String _uuid; + private String _name; + + public PlayerJoinCommand(UUID uuid, String name) + { + _uuid = uuid.toString(); + _name = name; + } + + @Override + public void run() + { + // Utilitizes a callback functionality to seperate dependencies + } + + public String getUuid() + { + return _uuid; + } + + public String getName() + { + return _name; + } +} \ No newline at end of file diff --git a/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/commands/PunishCommand.java b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/commands/PunishCommand.java new file mode 100644 index 00000000..ee7d9163 --- /dev/null +++ b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/commands/PunishCommand.java @@ -0,0 +1,29 @@ +package mineplex.serverdata.commands; + + +public class PunishCommand extends ServerCommand +{ + private String _playerName; + private boolean _ban; + private boolean _mute; + private String _message; + + public String getPlayerName() { return _playerName; } + public boolean getBan() { return _ban; } + public boolean getMute() { return _mute; } + public String getMessage() { return _message; } + + public PunishCommand(String playerName, boolean ban, boolean mute, String message) + { + _playerName = playerName; + _ban = ban; + _mute = mute; + _message = message; + } + + @Override + public void run() + { + // Utilitizes a callback functionality to seperate dependencies + } +} diff --git a/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/commands/RemovePunishCommand.java b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/commands/RemovePunishCommand.java new file mode 100644 index 00000000..d34aad2f --- /dev/null +++ b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/commands/RemovePunishCommand.java @@ -0,0 +1,28 @@ +package mineplex.serverdata.commands; + +import java.util.UUID; + +import com.google.gson.JsonObject; + +public class RemovePunishCommand extends ServerCommand +{ + private final JsonObject _punishment; + private final String _target; + private final String _admin; + private final String _adminUUID; + private final String _reason; + + public RemovePunishCommand(JsonObject punishment, String target, String admin, UUID adminUUID, String reason) + { + _punishment = punishment; + _target = target; + _admin = admin; + _adminUUID = adminUUID.toString(); + _reason = reason; + } + + @Override + public void run() + { + } +} diff --git a/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/commands/RestartCommand.java b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/commands/RestartCommand.java new file mode 100644 index 00000000..c38cda76 --- /dev/null +++ b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/commands/RestartCommand.java @@ -0,0 +1,38 @@ +package mineplex.serverdata.commands; + +import mineplex.serverdata.Region; + +public class RestartCommand extends ServerCommand +{ + + private final String _server; + private final Region _region; + private final boolean _groupRestart; + + public RestartCommand(String server, Region region, boolean groupRestart) + { + _server = server; + _region = region; + _groupRestart = groupRestart; + } + + @Override + public void run() + { + } + + public String getServerName() + { + return _server; + } + + public Region getRegion() + { + return _region; + } + + public boolean isGroupRestart() + { + return _groupRestart; + } +} diff --git a/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/commands/ServerCommand.java b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/commands/ServerCommand.java new file mode 100644 index 00000000..de588f1d --- /dev/null +++ b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/commands/ServerCommand.java @@ -0,0 +1,102 @@ +package mineplex.serverdata.commands; + + +import java.util.UUID; + +public abstract class ServerCommand +{ + private final UUID _commandId = UUID.randomUUID(); + private final String _fromServer = ServerCommandManager.getInstance().getServerName(); + + // The names of servers targetted to receive this ServerCommand. + private String[] _targetServers; + + public ServerCommand() + { + _targetServers = new String[0]; + } + + public ServerCommand(String... targetServers) + { + _targetServers = targetServers; + } + + public void setTargetServers(String... targetServers) + { + _targetServers = targetServers; + } + + public String[] getTargetServers() + { + if (_targetServers == null) + { + _targetServers = new String[0]; + } + + return _targetServers; + } + + public String getFromServer() + { + return this._fromServer; + } + + /** + * Run the command on it's destination target server. + */ + public void run() + { + // Not yet implemented in base + } + + /** + * @param serverName - the name of the server to be checked for whether they are a target + * @return true, if {@code serverName} is one of the {@code targetServers} of this + * {@link ServerCommand}, false otherwise. + */ + public boolean isTargetServer(String serverName) + { + if (getTargetServers().length == 0) + return true; + + for (String targetServer : getTargetServers()) + { + if (targetServer.equalsIgnoreCase(serverName)) + { + return true; + } + } + + return false; + } + + /** + * Publish the {@link ServerCommand} across the network to {@code targetServers}. + */ + public void publish() + { + ServerCommandManager.getInstance().publishCommand(this); + } + + public boolean wasSentFromThisServer() + { + return ServerCommandManager.getInstance().getServerName().equals(_fromServer); + } + + public boolean isSentToThisServer() + { + for (String targetServer : _targetServers) + { + if (ServerCommandManager.getInstance().getServerName().equals(targetServer)) + { + return true; + } + } + return false; + } + + public UUID getCommandId() + { + return _commandId; + } +} diff --git a/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/commands/ServerCommandListener.java b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/commands/ServerCommandListener.java new file mode 100644 index 00000000..835b58d2 --- /dev/null +++ b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/commands/ServerCommandListener.java @@ -0,0 +1,60 @@ +package mineplex.serverdata.commands; + +import redis.clients.jedis.JedisPubSub; + +/** + * The ServerCommandListener listens for published Redis network messages + * and deserializes them into their {@link ServerCommand} form, then registers + * it's arrival at the {@link ServerCommandManager}. + * @author Ty + * + */ +public class ServerCommandListener extends JedisPubSub +{ + + @Override + public void onPMessage(String pattern, String channelName, String message) + { + try + { + String commandType = channelName.split(":")[1]; + ServerCommandManager.getInstance().handleCommand(commandType, message); + } + catch (Exception exception) + { + exception.printStackTrace(); + } + } + + + @Override + public void onMessage(String channelName, String message) + { + + } + + @Override + public void onPSubscribe(String pattern, int subscribedChannels) + { + + } + + @Override + public void onPUnsubscribe(String pattern, int subscribedChannels) + { + + } + + @Override + public void onSubscribe(String channelName, int subscribedChannels) + { + + } + + @Override + public void onUnsubscribe(String channelName, int subscribedChannels) + { + + } + +} diff --git a/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/commands/ServerCommandManager.java b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/commands/ServerCommandManager.java new file mode 100644 index 00000000..72f86461 --- /dev/null +++ b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/commands/ServerCommandManager.java @@ -0,0 +1,165 @@ +package mineplex.serverdata.commands; + +import java.util.HashMap; +import java.util.Map; + +import com.google.gson.Gson; + +import mineplex.serverdata.Utility; +import mineplex.serverdata.servers.ServerManager; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisPool; + +public class ServerCommandManager +{ + + // The singleton instance of ServerCommandManager + private static ServerCommandManager _instance; + + public final String SERVER_COMMANDS_CHANNEL = "commands.server"; + + private JedisPool _writePool; + private JedisPool _readPool; + private Map _commandTypes; + + private String _localServerName; + private Gson _gson; + public void initializeServer(String serverName, Gson gson) + { + _localServerName = serverName; + _gson = gson; + } + + public boolean isServerInitialized() { return _localServerName != null; } + public String getServerName() + { + return _localServerName; + } + + /** + * Private class constructor to prevent non-singleton instances. + */ + private ServerCommandManager() + { + _writePool = Utility.generatePool(ServerManager.getMasterConnection()); // Publish to master instance + _readPool = Utility.generatePool(ServerManager.getSlaveConnection()); // Read from slave instance + + _commandTypes = new HashMap<>(); + + initialize(); + } + + /** + * Initialize the ServerCommandManager by subscribing to the + * redis network. + */ + private void initialize() + { + // Spin up a new thread and subscribe to the Redis pubsub network + Thread thread = new Thread("Redis Manager") + { + public void run() + { + try (Jedis jedis = _readPool.getResource()) + { + jedis.psubscribe(new ServerCommandListener(), SERVER_COMMANDS_CHANNEL + ":*"); + } + } + }; + + thread.start(); + } + + /** + * Publish a {@link ServerCommand} across the network to all live servers. + * @param serverCommand - the {@link ServerCommand} to issue to all servers. + */ + public void publishCommand(final ServerCommand serverCommand) + { + new Thread(new Runnable() + { + public void run() + { + String commandType = serverCommand.getClass().getSimpleName(); + String serializedCommand = _gson.toJson(serverCommand); + + try(Jedis jedis = _writePool.getResource()) + { + jedis.publish(SERVER_COMMANDS_CHANNEL + ":" + commandType, serializedCommand); + } + } + }).start(); + } + + /** + * Handle an incoming (serialized) {@link ServerCommand}. + * @param commandType - the type of command being received + * @param serializedCommand - the serialized {@link ServerCommand} data. + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + public void handleCommand(final String commandType, String serializedCommand) + { + if (!isServerInitialized()) + { + // TODO: Log un-initialized server receiving command? + return; + } + + if (_commandTypes.containsKey(commandType)) + { + Class commandClazz = _commandTypes.get(commandType).getCommandType(); + final ServerCommand serverCommand = _gson.fromJson(serializedCommand, commandClazz); + + if (serverCommand.isTargetServer(_localServerName)) + { + // TODO: Run synchronously? + CommandCallback callback = _commandTypes.get(commandType).getCallback(); + serverCommand.run(); // Run server command without callback + + if (callback != null) + { + callback.run(serverCommand); // Run callback + } + } + } + } + + /** + * Register a new type of {@link ServerCommand}. + * @param commandType - the {@link ServerCommand} type to register. + */ + public void registerCommandType(String commandName, Class commandType, CommandCallback callback) + { + if (_commandTypes.containsKey(commandName)) + { + // Log overwriting of command type? + } + + CommandType cmdType = new CommandType(commandType, callback); + _commandTypes.put(commandName, cmdType); + System.out.println("Registered : " + commandName); + } + + public void registerCommandType(Class commandType, CommandCallback callback) + { + registerCommandType(commandType.getSimpleName(), commandType, callback); + } + + public void registerCommandType(String commandName, Class commandType) + { + registerCommandType(commandName, commandType, null); + } + + /** + * @return the singleton instance of ServerCommandManager + */ + public static ServerCommandManager getInstance() + { + if (_instance == null) + { + _instance = new ServerCommandManager(); + } + + return _instance; + } +} \ No newline at end of file diff --git a/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/commands/ServerTransfer.java b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/commands/ServerTransfer.java new file mode 100644 index 00000000..cd16165b --- /dev/null +++ b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/commands/ServerTransfer.java @@ -0,0 +1,24 @@ +package mineplex.serverdata.commands; + +public class ServerTransfer +{ + + // The name of the player who is being transferred. + private String _playerName; + public String getPlayerName() { return _playerName; } + + // The name of the destination server in this ServerTransfer. + private String _serverName; + public String getServerName() { return _serverName; } + + /** + * Class constructor + * @param playerName - the name of the player being transferred. + * @param serverName - the name of the server to transfer the player to (destination). + */ + public ServerTransfer(String playerName, String serverName) + { + _playerName = playerName; + _serverName = serverName; + } +} diff --git a/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/commands/SuicideCommand.java b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/commands/SuicideCommand.java new file mode 100644 index 00000000..7bbc0ac6 --- /dev/null +++ b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/commands/SuicideCommand.java @@ -0,0 +1,24 @@ +package mineplex.serverdata.commands; + +import mineplex.serverdata.Region; + +public class SuicideCommand extends ServerCommand +{ + private String _server; + public String getServerName() { return _server; } + + private Region _region; + public Region getRegion() { return _region; } + + public SuicideCommand(String server, Region region) + { + _server = server; + _region = region; + } + + @Override + public void run() + { + // Utilitizes a callback functionality to seperate dependencies + } +} diff --git a/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/commands/TransferCommand.java b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/commands/TransferCommand.java new file mode 100644 index 00000000..84abfa91 --- /dev/null +++ b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/commands/TransferCommand.java @@ -0,0 +1,24 @@ +package mineplex.serverdata.commands; + + +public class TransferCommand extends ServerCommand +{ + private final String _playerName; + private final String _targetServer; + + public TransferCommand(String playerName, String targetServer) + { + _playerName = playerName; + _targetServer = targetServer; + } + + public String getPlayerName() + { + return _playerName; + } + + public String getTargetServer() + { + return _targetServer; + } +} diff --git a/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/commands/TransferUUIDCommand.java b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/commands/TransferUUIDCommand.java new file mode 100644 index 00000000..78ee7328 --- /dev/null +++ b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/commands/TransferUUIDCommand.java @@ -0,0 +1,25 @@ +package mineplex.serverdata.commands; + +import java.util.UUID; + +public class TransferUUIDCommand extends ServerCommand +{ + private final UUID _playerUUID; + private final String _targetServer; + + public TransferUUIDCommand(UUID playerUUID, String targetServer) + { + _playerUUID = playerUUID; + _targetServer = targetServer; + } + + public UUID getPlayerUUID() + { + return _playerUUID; + } + + public String getTargetServer() + { + return _targetServer; + } +} diff --git a/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/commands/TwoFactorResetCommand.java b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/commands/TwoFactorResetCommand.java new file mode 100644 index 00000000..45c698e5 --- /dev/null +++ b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/commands/TwoFactorResetCommand.java @@ -0,0 +1,17 @@ +package mineplex.serverdata.commands; + +public class TwoFactorResetCommand extends ServerCommand +{ + private String _adminName; + private String _adminUUID; + private String _targetName; + private String _targetUUID; + + public TwoFactorResetCommand(String adminName, String adminUUID, String targetName, String targetUUID) + { + _adminName = adminName; + _adminUUID = adminUUID; + _targetName = targetName; + _targetUUID = targetUUID; + } +} diff --git a/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/data/BungeeServer.java b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/data/BungeeServer.java new file mode 100644 index 00000000..669e6e5f --- /dev/null +++ b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/data/BungeeServer.java @@ -0,0 +1,57 @@ +package mineplex.serverdata.data; + +import mineplex.serverdata.Region; +import mineplex.serverdata.data.Data; + +public class BungeeServer implements Data +{ + // The name of this server. + private String _name; + public String getName() { return _name; } + + // The geographical region of this Bungee Server. + private Region _region; + public Region getRegion() { return _region; } + + // The number of players currently online. + private int _playerCount; + public int getPlayerCount() { return _playerCount; } + + // The public I.P address used by players to connect to the server. + private String _publicAddress; + public String getPublicAddress() { return _publicAddress; } + + // The port the server is currently running/listening on. + private int _port; + public int getPort() { return _port; } + + // Whether the Bungee server can connect to the internet. + private boolean _connected; + public boolean isConnected() { return _connected; } + + /** + * Class constructor + * @param name + * @param publicAddress + * @param port + * @param playerCount + * @param connected + */ + public BungeeServer(String name, Region region, String publicAddress, int port, int playerCount, boolean connected) + { + _name = name; + _region = region; + _playerCount = playerCount; + _publicAddress = publicAddress; + _port = port; + _connected = connected; + } + + /** + * Unique identifying ID for this Bungee Server. + */ + public String getDataId() + { + return _name; + } +} diff --git a/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/data/Data.java b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/data/Data.java new file mode 100644 index 00000000..d0e0c943 --- /dev/null +++ b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/data/Data.java @@ -0,0 +1,9 @@ +package mineplex.serverdata.data; + +public interface Data +{ + /** + * @return the unique id key representing this {@link Data} object in {@link DataRepository}s. + */ + public String getDataId(); +} diff --git a/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/data/DataRepository.java b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/data/DataRepository.java new file mode 100644 index 00000000..03500ca2 --- /dev/null +++ b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/data/DataRepository.java @@ -0,0 +1,36 @@ +package mineplex.serverdata.data; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * DataRepository is used to store {@link Data} objects in a central database + * for real-time fetching/modification. + * @author Ty + * + * @param - the type of {@link Data} object stored in this repository. + */ +public interface DataRepository +{ + + public Collection getElements(); + + public T getElement(String dataId); + + public Collection getElements(Collection dataIds); + + public Map getElementsMap(List dataIds); + + public void addElement(T element, int timeout); + + public void addElement(T element); + + public void removeElement(T element); + + public void removeElement(String dataId); + + public boolean elementExists(String dataId); + + public int clean(); +} diff --git a/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/data/DedicatedServer.java b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/data/DedicatedServer.java new file mode 100644 index 00000000..59191a7f --- /dev/null +++ b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/data/DedicatedServer.java @@ -0,0 +1,111 @@ +package mineplex.serverdata.data; + +import java.util.HashMap; +import java.util.Map; + +import mineplex.serverdata.Region; + +public class DedicatedServer +{ + + // The default amount of available CPU usage. + public static final int DEFAULT_CPU = 32; + + // The default amount of available ram usage. + public static final int DEFAULT_RAM = 14000; + + // The unique name representing this server + private String _name; + public String getName() { return _name; } + + // The public I.P address used to connect to this server + private String _publicAddress; + public String getPublicAddress() { return _publicAddress; } + + // The private I.P address of this server + private String _privateAddress; + public String getPrivateAddress() { return _privateAddress; } + + // The geographical region that this dedicated server is located in + private Region _region; + public Region getRegion() { return _region; } + public boolean isUsRegion() { return _region == Region.US; } + + // The amount of available CPU usage on this server box. + private int _availableCpu; + public int getAvailableCpu() { return _availableCpu; } + + // The amount of available ram usage on this server box. + private int _availableRam; + public int getAvailableRam() { return _availableRam; } + + // The amount of available CPU usage on this server box. + private int _maxCpu; + public int getMaxCpu() { return _maxCpu; } + + // The amount of available ram usage on this server box. + private int _maxRam; + public int getMaxRam() { return _maxRam; } + + // A mapping of server group names (Key) to the number of server instances (Value) + private Map _serverCounts; + + /** + * Class constructor + * @param data - the set of serialized data values representing + * the internal state of this DedicatedServer. + */ + public DedicatedServer(Map data) + { + _name = data.get("name"); + _publicAddress = data.get("publicAddress"); + _privateAddress = data.get("privateAddress"); + _region = Region.valueOf(data.get("region").toUpperCase()); + _availableCpu = Integer.valueOf(data.get("cpu")); + _availableRam = Integer.valueOf(data.get("ram")); + _maxCpu = Integer.valueOf(data.get("cpu")); + _maxRam = Integer.valueOf(data.get("ram")); + _serverCounts = new HashMap(); + } + + /** + * Set the number of {@link MinecraftServer} instances on this server + * for a specific {@link ServerGroup} type. + * @param serverGroup - the {@link ServerGroup} whose server instance count is being set. + * @param serverCount - the number of {@link MinecraftServer} instances active on this server. + */ + public void setServerCount(ServerGroup serverGroup, int serverCount) + { + if (_serverCounts.containsKey(serverGroup.getName())) + { + int currentAmount = _serverCounts.get(serverGroup.getName()); + _availableCpu += serverGroup.getRequiredCpu() * currentAmount; + _availableRam += serverGroup.getRequiredRam() * currentAmount; + } + + _serverCounts.put(serverGroup.getName(), serverCount); + _availableCpu -= serverGroup.getRequiredCpu() * serverCount; + _availableRam -= serverGroup.getRequiredRam() * serverCount; + } + + /** + * @param serverGroup - the server group whose server count on this dedicated server is being fetched. + * @return the number of active {@link MinecraftServer}s on this dedicated server + * that belong to {@code serverGroup}. + */ + public int getServerCount(ServerGroup serverGroup) + { + String groupName = serverGroup.getName(); + return _serverCounts.containsKey(groupName) ? _serverCounts.get(groupName) : 0; + } + + /** + * Increment the number of {@link MinecraftServer} instances on this server + * for a specific {@link ServerGroup} type by 1. + * @param serverGroup - the {@link ServerGroup} whose server instance count is being incremented + */ + public void incrementServerCount(ServerGroup serverGroup) + { + setServerCount(serverGroup, getServerCount(serverGroup) + 1); + } +} diff --git a/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/data/MinecraftServer.java b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/data/MinecraftServer.java new file mode 100644 index 00000000..a9dea82a --- /dev/null +++ b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/data/MinecraftServer.java @@ -0,0 +1,135 @@ +package mineplex.serverdata.data; + +public class MinecraftServer +{ + // The name of this server. + private String _name; + public String getName() { return _name; } + + // The ServerGroup that this MinecraftServer belongs to. + private String _group; + public String getGroup() { return _group; } + + // The current message of the day (MOTD) of the server. + private String _motd; + public String getMotd() { return _motd; } + + // The number of players currently online. + private int _playerCount; + public int getPlayerCount() { return _playerCount; } + public void incrementPlayerCount(int amount) { this._playerCount += amount; } + + // The maximum number of players allowed on the server. + private int _maxPlayerCount; + public int getMaxPlayerCount() { return _maxPlayerCount; } + + // The ticks per second (TPS) of the server. + private int _tps; + public int getTps() { return _tps; } + + // The current amount of RAM allocated to the server. + private int _ram; + public int getRam() { return _ram; } + + // The maximum amount of available RAM that can be allocated to the server. + private int _maxRam; + public int getMaxRam() { return _maxRam; } + + // The public I.P address used by players to connect to the server. + private String _publicAddress; + public String getPublicAddress() { return _publicAddress; } + + // The port the server is currently running/listening on. + private int _port; + public int getPort() { return _port; } + + private int _donorsOnline; + public int getDonorsOnline() { return _donorsOnline; } + + private long _startUpDate; + + private long _currentTime; + public long getCurrentTime() + { + return this._currentTime; + } + + /** + * Class constructor + * @param name + * @param group + * @param motd + * @param publicAddress + * @param port + * @param playerCount + * @param maxPlayerCount + * @param tps + * @param ram + * @param maxRam + */ + public MinecraftServer(String name, String group, String motd, String publicAddress, int port, + int playerCount, int maxPlayerCount, int tps, int ram, int maxRam, long startUpDate, int donorsOnline) + { + _name = name; + _group = group; + _motd = motd; + _playerCount = playerCount; + _maxPlayerCount = maxPlayerCount; + _tps = tps; + _ram = ram; + _maxRam = maxRam; + _publicAddress = publicAddress; + _port = port; + _donorsOnline = donorsOnline; + _startUpDate = startUpDate; + _currentTime = System.currentTimeMillis(); + } + + /** + * @return true, if {@value _playerCount} equals 0, false otherwise. + */ + public boolean isEmpty() + { + return _playerCount == 0; + } + + /** + * @return the amount of time (in seconds) that this {@link MinecraftServer} has been online for. + */ + public double getUptime() + { + return (System.currentTimeMillis() / 1000d - _startUpDate); + } + + /** + * @return true, if this server is currently joinable by players, false otherwise. + */ + public boolean isJoinable() + { + if (_motd == null) + { + return false; + } + + // This is super dodgy, this is the only way around monitor not killing game servers with the new MOTD system + if (_motd.isEmpty() || _motd.contains("VOTING") || _motd.contains("STARTING") || _motd.contains("WAITING") || _motd.contains("ALWAYS_OPEN")) + { + if (_playerCount < _maxPlayerCount) + { + int availableSlots = _maxPlayerCount - _playerCount; + + return !_motd.isEmpty() || (availableSlots > 20); + } + } + return false; + } + + public void setGroup(String group) + { + _group = group; + } + public void setName(String name) + { + _name = name; + } +} diff --git a/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/data/PlayerServerInfo.java b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/data/PlayerServerInfo.java new file mode 100644 index 00000000..f4a0be4f --- /dev/null +++ b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/data/PlayerServerInfo.java @@ -0,0 +1,25 @@ +package mineplex.serverdata.data; + +public class PlayerServerInfo implements Data +{ + + private String _playerName; + public String getPlayerName() { return _playerName; } + + private String _lastServer; + public String getLastServer() { return _lastServer; } + public void setLastServer(String lastServer) { _lastServer = lastServer; } + + public PlayerServerInfo(String playerName, String lastServer) + { + _playerName = playerName; + _lastServer = lastServer; + } + + @Override + public String getDataId() + { + return _playerName; + } + +} diff --git a/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/data/PlayerStatus.java b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/data/PlayerStatus.java new file mode 100644 index 00000000..ee9eb3ec --- /dev/null +++ b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/data/PlayerStatus.java @@ -0,0 +1,38 @@ +package mineplex.serverdata.data; + +import java.util.UUID; + +public class PlayerStatus implements Data +{ + // The uuid of this player. + private UUID _uuid; + public UUID getUUID() { return _uuid; } + + // The name of this player. + private String _name; + public String getName() { return _name; } + + // The current server occupied by this player. + private String _server; + public String getServer() { return _server; } + + /** + * Class constructor + * @param name + * @param server + */ + public PlayerStatus(UUID uuid, String name, String server) + { + _uuid = uuid; + _name = name; + _server = server; + } + + /** + * Unique identifying String ID associated with this {@link PlayerStatus}. + */ + public String getDataId() + { + return _uuid.toString(); + } +} \ No newline at end of file diff --git a/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/data/ServerGroup.java b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/data/ServerGroup.java new file mode 100644 index 00000000..a760c23e --- /dev/null +++ b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/data/ServerGroup.java @@ -0,0 +1,417 @@ +package mineplex.serverdata.data; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import mineplex.serverdata.Region; + +public class ServerGroup +{ + private HashMap _dataMap = null; + + private String _name; + private String _host; + private String _prefix; + + private int _minPlayers; + private int _maxPlayers; + + private int _requiredRam; + private int _requiredCpu; + private int _requiredTotalServers; + private int _requiredJoinableServers; + + private String _uptimes = ""; + + private boolean _arcadeGroup; + private String _worldZip; + private String _plugin; + private String _configPath; + private int _portSection; + + private boolean _pvp; + private boolean _tournament; + private boolean _tournamentPoints; + private boolean _teamRejoin; + private boolean _teamAutoJoin; + private boolean _teamForceBalance; + + private boolean _gameAutoStart; + private boolean _gameTimeout; + private boolean _gameVoting, _mapVoting; + private boolean _rewardGems; + private boolean _rewardItems; + private boolean _rewardStats; + private boolean _rewardAchievements; + + private boolean _hotbarInventory; + private boolean _hotbarHubClock; + private boolean _playerKickIdle; + private boolean _hardMaxPlayerCap; + + private String _games; + private String _modes; + private String _boosterGroup; + private String _serverType; + private boolean _addNoCheat; + private boolean _addWorldEdit; + private boolean _whitelist; + private boolean _staffOnly; + private String _resourcePack = ""; + + private String _npcName = ""; + private String _portalBottomCornerLocation = ""; + private String _portalTopCornerLocation = ""; + + private String _teamServerKey = ""; + + private Region _region; + + private Set _servers; + + public ServerGroup(Map data, Collection serverStatuses) + { + _name = data.get("name"); + _prefix = data.get("prefix"); + _requiredRam = Integer.valueOf(data.get("ram")); + _requiredCpu = Integer.valueOf(data.get("cpu")); + _requiredTotalServers = Integer.valueOf(data.get("totalServers")); + _requiredJoinableServers = Integer.valueOf(data.get("joinableServers")); + _portSection = Integer.valueOf(data.get("portSection")); + _uptimes = data.getOrDefault("uptimes", ""); + _arcadeGroup = Boolean.valueOf(data.get("arcadeGroup")); + _worldZip = data.get("worldZip"); + _plugin = data.get("plugin"); + _configPath = data.get("configPath"); + _minPlayers = Integer.valueOf(data.get("minPlayers")); + _maxPlayers = Integer.valueOf(data.get("maxPlayers")); + _pvp = Boolean.valueOf(data.get("pvp")); + _tournament = Boolean.valueOf(data.get("tournament")); + _tournamentPoints = Boolean.valueOf(data.get("tournamentPoints")); + _hardMaxPlayerCap = Boolean.valueOf(data.get("hardMaxPlayerCap")); + _games = data.get("games"); + _modes = data.get("modes"); + _boosterGroup = data.get("boosterGroup"); + _serverType = data.get("serverType"); + _addNoCheat = Boolean.valueOf(data.get("addNoCheat")); + _addWorldEdit = Boolean.valueOf(data.get("addWorldEdit")); + _teamRejoin = Boolean.valueOf(data.get("teamRejoin")); + _teamAutoJoin = Boolean.valueOf(data.get("teamAutoJoin")); + _teamForceBalance = Boolean.valueOf(data.get("teamForceBalance")); + _gameAutoStart = Boolean.valueOf(data.get("gameAutoStart")); + _gameTimeout = Boolean.valueOf(data.get("gameTimeout")); + _gameVoting = Boolean.valueOf(data.get("gameVoting")); + _mapVoting = Boolean.valueOf(data.get("mapVoting")); + _rewardGems = Boolean.valueOf(data.get("rewardGems")); + _rewardItems = Boolean.valueOf(data.get("rewardItems")); + _rewardStats = Boolean.valueOf(data.get("rewardStats")); + _rewardAchievements = Boolean.valueOf(data.get("rewardAchievements")); + _hotbarInventory = Boolean.valueOf(data.get("hotbarInventory")); + _hotbarHubClock = Boolean.valueOf(data.get("hotbarHubClock")); + _playerKickIdle = Boolean.valueOf(data.get("playerKickIdle")); + _staffOnly = Boolean.valueOf(data.get("staffOnly")); + _whitelist = Boolean.valueOf(data.get("whitelist")); + _resourcePack = data.getOrDefault("resourcePack", ""); + _host = data.get("host"); + _region = data.containsKey("region") ? Region.valueOf(data.get("region")) : Region.ALL; + _teamServerKey = data.getOrDefault("teamServerKey", ""); + _portalBottomCornerLocation = data.getOrDefault("portalBottomCornerLocation", ""); + _portalTopCornerLocation = data.getOrDefault("portalTopCornerLocation", ""); + _npcName = data.getOrDefault("npcName", ""); + + if (serverStatuses != null) + parseServers(serverStatuses); + } + + public ServerGroup(String name, String prefix, String host, int ram, int cpu, int totalServers, int joinable, int portSection, String uptimes, boolean arcade, String worldZip, String plugin, String configPath + , int minPlayers, int maxPlayers, boolean pvp, boolean tournament, boolean tournamentPoints, String games, String modes, String boosterGroup, String serverType, boolean noCheat, boolean worldEdit, boolean teamRejoin + , boolean teamAutoJoin, boolean teamForceBalance, boolean gameAutoStart, boolean gameTimeout, boolean gameVoting, boolean mapVoting, boolean rewardGems, boolean rewardItems, boolean rewardStats + , boolean rewardAchievements, boolean hotbarInventory, boolean hotbarHubClock, boolean playerKickIdle, boolean hardMaxPlayerCap, boolean staffOnly, boolean whitelist, String resourcePack, Region region + , String teamServerKey, String portalBottomCornerLocation, String portalTopCornerLocation, String npcName) + { + _name = name; + _prefix = prefix; + _host = host; + _requiredRam = ram; + _requiredCpu = cpu; + _requiredTotalServers = totalServers; + _requiredJoinableServers = joinable; + _portSection = portSection; + _uptimes = uptimes; + _arcadeGroup = arcade; + _worldZip = worldZip; + _plugin = plugin; + _configPath = configPath; + _minPlayers = minPlayers; + _maxPlayers = maxPlayers; + _pvp = pvp; + _tournament = tournament; + _tournamentPoints = tournamentPoints; + _games = games; + _modes = modes; + _boosterGroup = boosterGroup; + _serverType = serverType; + _addNoCheat = noCheat; + _addWorldEdit = worldEdit; + _teamRejoin = teamRejoin; + _teamAutoJoin = teamAutoJoin; + _teamForceBalance = teamForceBalance; + _gameAutoStart = gameAutoStart; + _gameTimeout = gameTimeout; + _gameVoting = gameVoting; + _mapVoting = mapVoting; + _rewardGems = rewardGems; + _rewardItems = rewardItems; + _rewardStats = rewardStats; + _rewardAchievements = rewardAchievements; + _hotbarInventory = hotbarInventory; + _hotbarHubClock = hotbarHubClock; + _playerKickIdle = playerKickIdle; + _hardMaxPlayerCap = hardMaxPlayerCap; + _staffOnly = staffOnly; + _whitelist = whitelist; + _resourcePack = resourcePack; + _region = region; + _teamServerKey = teamServerKey; + _portalBottomCornerLocation = portalBottomCornerLocation; + _portalTopCornerLocation = portalTopCornerLocation; + _npcName = npcName; + } + + public ServerGroup(String name, String npcName, String prefix) + { + _name = name; + _npcName = npcName; + _prefix = prefix; + } + + public String getName() { return _name; } + public String getHost() { return _host; } + public String getPrefix() { return _prefix; } + + public int getMinPlayers() { return _minPlayers; } + public int getMaxPlayers() { return _maxPlayers; } + + public int getRequiredRam() { return _requiredRam; } + public int getRequiredCpu() { return _requiredCpu; } + public int getRequiredTotalServers() { return _requiredTotalServers; } + public int getRequiredJoinableServers() { return _requiredJoinableServers; } + public int getPortSection() { return _portSection; } + + public boolean getArcadeGroup() { return _arcadeGroup; } + public String getWorldZip() { return _worldZip; } + public String getPlugin() { return _plugin; } + public String getConfigPath() { return _configPath; } + + public boolean getPvp() { return _pvp; } + public boolean getTournament() { return _tournament; } + public boolean getTournamentPoints() { return _tournamentPoints; } + public boolean getTeamRejoin() { return _teamRejoin; } + public boolean getTeamAutoJoin() { return _teamAutoJoin; } + + public boolean getTeamForceBalance() { return _teamForceBalance; } + public boolean getGameAutoStart() { return _gameAutoStart; } + public boolean getGameTimeout() { return _gameTimeout; } + public boolean getGameVoting() { return _gameVoting; } + public boolean getMapVoting() { return _mapVoting; } + public boolean getRewardGems() { return _rewardGems; } + public boolean getRewardItems() { return _rewardItems; } + public boolean getRewardStats() { return _rewardStats; } + public boolean getRewardAchievements() { return _rewardAchievements; } + + public boolean getHotbarInventory() { return _hotbarInventory; } + public boolean getHotbarHubClock() { return _hotbarHubClock; } + public boolean getPlayerKickIdle() { return _playerKickIdle; } + public boolean getHardMaxPlayerCap() { return _hardMaxPlayerCap; } + + public String getGames() { return _games; } + public String getModes() { return _modes; } + public String getBoosterGroup() { return _boosterGroup; } + + public String getServerType() { return _serverType; } + public boolean getAddNoCheat() { return _addNoCheat; } + public boolean getAddWorldEdit() { return _addWorldEdit; } + public boolean getWhitelist() { return _whitelist; } + public boolean getStaffOnly() { return _staffOnly; } + public String getResourcePack() { return _resourcePack; } + public Region getRegion() { return _region; } + + public String getTeamServerKey() { return _teamServerKey; } + + public String getServerNpcName() { return _npcName; } + public String getPortalBottomCornerLocation() { return _portalBottomCornerLocation; } + public String getPortalTopCornerLocation() { return _portalTopCornerLocation; } + public String getUptimes() { return _uptimes; } + + public Set getServers() { return _servers; } + + public int getServerCount() + { + return _servers.size(); + } + + public int getJoinableCount() + { + int joinable = 0; + + for (MinecraftServer server : _servers) + { + if (server.isJoinable()) + { + joinable++; + } + } + + return joinable; + } + + public int getPlayerCount() + { + int playerCount = 0; + + for (MinecraftServer server : _servers) + { + playerCount += server.getPlayerCount(); + } + + return playerCount; + } + + public int getMaxPlayerCount() + { + int maxPlayerCount = 0; + + for (MinecraftServer server : _servers) + { + maxPlayerCount += server.getMaxPlayerCount(); + } + + return maxPlayerCount; + } + + public Collection getEmptyServers() + { + Collection emptyServers = new HashSet(); + + for (MinecraftServer server : _servers) + { + if (server.isEmpty() && server.getUptime() >= 150) // Only return empty servers that have been online for >150 seconds + { + emptyServers.add(server); + } + } + + return emptyServers; + } + + private void parseServers(Collection servers) + { + _servers = new HashSet<>(); + + for (MinecraftServer server : servers) + { + if (_name.equalsIgnoreCase(server.getGroup())) + { + _servers.add(server); + } + } + } + + public int generateUniqueId(int startId) + { + int id = startId; + + while (true) + { + boolean uniqueId = true; + + for (MinecraftServer server : _servers) + { + String serverName = server.getName(); + try + { + String[] nameArgs = serverName.split("-"); + int serverNum = Integer.parseInt(nameArgs[nameArgs.length - 1]); + + if (serverNum == id) + { + uniqueId = false; + break; + } + } + catch (Exception exception) + { + exception.printStackTrace(); + } + } + + if (uniqueId) + { + return id; + } + else + { + id++; + } + } + } + + public HashMap getDataMap() + { + if (_dataMap == null) + { + _dataMap = new HashMap<>(); + + _dataMap.put("name", _name); + _dataMap.put("prefix", _prefix); + _dataMap.put("ram", _requiredRam + ""); + _dataMap.put("cpu", _requiredCpu + ""); + _dataMap.put("totalServers", _requiredTotalServers + ""); + _dataMap.put("joinableServers", _requiredJoinableServers + ""); + _dataMap.put("portSection", _portSection + ""); + _dataMap.put("uptimes", _uptimes); + _dataMap.put("arcadeGroup", _arcadeGroup + ""); + _dataMap.put("worldZip", _worldZip); + _dataMap.put("plugin", _plugin); + _dataMap.put("configPath", _configPath); + _dataMap.put("minPlayers", _minPlayers + ""); + _dataMap.put("maxPlayers", _maxPlayers + ""); + _dataMap.put("pvp", _pvp + ""); + _dataMap.put("tournament", _tournament + ""); + _dataMap.put("tournamentPoints", _tournamentPoints + ""); + _dataMap.put("games", _games); + _dataMap.put("modes", _modes); + _dataMap.put("serverType", _serverType); + _dataMap.put("addNoCheat", _addNoCheat + ""); + _dataMap.put("teamRejoin", _teamRejoin + ""); + _dataMap.put("teamAutoJoin", _teamAutoJoin + ""); + _dataMap.put("teamForceBalance", _teamForceBalance + ""); + _dataMap.put("gameAutoStart", _gameAutoStart + ""); + _dataMap.put("gameTimeout", _gameTimeout + ""); + _dataMap.put("gameVoting", String.valueOf(_gameVoting)); + _dataMap.put("mapVoting", String.valueOf(_mapVoting)); + _dataMap.put("rewardGems", _rewardGems + ""); + _dataMap.put("rewardItems", _rewardItems + ""); + _dataMap.put("rewardStats", _rewardStats + ""); + _dataMap.put("rewardAchievements", _rewardAchievements + ""); + _dataMap.put("hotbarInventory", _hotbarInventory + ""); + _dataMap.put("hotbarHubClock", _hotbarHubClock + ""); + _dataMap.put("playerKickIdle", _playerKickIdle + ""); + _dataMap.put("staffOnly", _staffOnly + ""); + _dataMap.put("whitelist", _whitelist + ""); + _dataMap.put("resourcePack", _resourcePack); + _dataMap.put("host", _host); + _dataMap.put("region", _region.name()); + _dataMap.put("teamServerKey", _teamServerKey); + _dataMap.put("boosterGroup", _boosterGroup); + _dataMap.put("portalBottomCornerLocation", _portalBottomCornerLocation); + _dataMap.put("portalTopCornerLocation", _portalTopCornerLocation); + _dataMap.put("npcName", _npcName); + } + + return _dataMap; + } +} diff --git a/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/database/DBPool.java b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/database/DBPool.java new file mode 100644 index 00000000..9e42af17 --- /dev/null +++ b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/database/DBPool.java @@ -0,0 +1,179 @@ +package mineplex.serverdata.database; + +import javax.sql.DataSource; + +import java.io.File; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.sql.Connection; +import java.util.Arrays; +import java.util.List; + +import org.apache.commons.dbcp2.BasicDataSource; + +public final class DBPool +{ + private static DataSource ACCOUNT; + private static DataSource QUEUE; + private static DataSource MINEPLEX; + private static DataSource MINEPLEX_STATS; + private static DataSource PLAYER_STATS; + private static DataSource SERVER_STATS; + private static DataSource MSSQL_MOCK; + + private static DataSource openDataSource(String url, String username, String password) + { + BasicDataSource source = new BasicDataSource(); + source.addConnectionProperty("autoReconnect", "true"); + source.addConnectionProperty("allowMultiQueries", "true"); + source.addConnectionProperty("zeroDateTimeBehavior", "convertToNull"); + source.setDefaultTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); + source.setDriverClassName("com.mysql.jdbc.Driver"); + source.setUrl(url); + source.setUsername(username); + source.setPassword(password); + source.setMaxTotal(5); + source.setMaxIdle(5); + source.setTimeBetweenEvictionRunsMillis(180 * 1000); + source.setSoftMinEvictableIdleTimeMillis(180 * 1000); + + return source; + } + + public static DataSource getMssqlMock() + { + if (MSSQL_MOCK == null) + loadDataSources(); + + return MSSQL_MOCK; + } + + public static DataSource getAccount() + { + if (ACCOUNT == null) + loadDataSources(); + + return ACCOUNT; + } + + public static DataSource getQueue() + { + if (QUEUE == null) + loadDataSources(); + + return QUEUE; + } + + public static DataSource getMineplex() + { + if (MINEPLEX == null) + loadDataSources(); + + return MINEPLEX; + } + + public static DataSource getMineplexStats() + { + if (MINEPLEX_STATS == null) + loadDataSources(); + + return MINEPLEX_STATS; + } + + public static DataSource getPlayerStats() + { + if (PLAYER_STATS == null) + loadDataSources(); + + return PLAYER_STATS; + } + + public static DataSource getServerStats() + { + if (SERVER_STATS == null) + loadDataSources(); + + return SERVER_STATS; + } + + private static void loadDataSources() + { + try + { + File configFile = new File("database-config.dat"); + + if (configFile.exists()) + { + List lines = Files.readAllLines(configFile.toPath(), Charset.defaultCharset()); + + for (String line : lines) + { + deserializeConnection(line); + } + } + else + { + System.out.println("database-config.dat not found at " + configFile.toPath().toString()); + } + } + catch (Exception exception) + { + exception.printStackTrace(); + System.out.println("---Unable To Parse DBPOOL Configuration File---"); + } + } + + private static void deserializeConnection(String line) + { + String[] args = line.split(" "); + if (args.length == 3) + { + String dbHost = args[0]; + String userName = args[1]; + String password = args[2]; + ACCOUNT = openDataSource("jdbc:mysql://" + dbHost + "/account", userName, password); + QUEUE = openDataSource("jdbc:mysql://" + dbHost + "/queue", userName, password); + MINEPLEX = openDataSource("jdbc:mysql://" + dbHost + "/mineplex", userName, password); + MINEPLEX_STATS = openDataSource("jdbc:mysql://" + dbHost + "/mineplex_stats", userName, password); + PLAYER_STATS = openDataSource("jdbc:mysql://" + dbHost + "/player_stats", userName, password); + SERVER_STATS = openDataSource("jdbc:mysql://" + dbHost + "/server_stats", userName, password); + } + /* + String[] args = line.split(" "); + + if (args.length == 4) + { + String dbHost = args[0]; + String userName = args[1]; + String password = args[2]; + + System.out.println(userName.toString()); + System.out.println(dbHost.toString()); + System.out.println(password.toString()); + + ACCOUNT = openDataSource("jdbc:mysql://" + dbHost, userName, password); + QUEUE = openDataSource("jdbc:mysql://" + dbHost, userName, password); + MINEPLEX = openDataSource("jdbc:mysql://" + dbHost, userName, password); + MINEPLEX_STATS = openDataSource("jdbc:mysql://" + dbHost, userName, password); + PLAYER_STATS = openDataSource("jdbc:mysql://" + dbHost, userName, password); + SERVER_STATS = openDataSource("jdbc:mysql://" + dbHost, userName, password); + MSSQL_MOCK = openDataSource("jdbc:mysql://" + dbHost, userName, password); + + if (dbSource.toUpperCase().equalsIgnoreCase("ACCOUNT")) + ACCOUNT = openDataSource("jdbc:mysql://" + dbHost, userName, password); + else if (dbSource.toUpperCase().equalsIgnoreCase("QUEUE")) + QUEUE = openDataSource("jdbc:mysql://" + dbHost, userName, password); + else if (dbSource.toUpperCase().equalsIgnoreCase("MINEPLEX")) + MINEPLEX = openDataSource("jdbc:mysql://" + dbHost, userName, password); + else if (dbSource.toUpperCase().equalsIgnoreCase("MINEPLEX_STATS")) + MINEPLEX_STATS = openDataSource("jdbc:mysql://" + dbHost, userName, password); + else if (dbSource.toUpperCase().equalsIgnoreCase("PLAYER_STATS")) + PLAYER_STATS = openDataSource("jdbc:mysql://" + dbHost, userName, password); + else if (dbSource.toUpperCase().equalsIgnoreCase("SERVER_STATS")) + SERVER_STATS = openDataSource("jdbc:mysql://" + dbHost, userName, password); + else if (dbSource.toUpperCase().equalsIgnoreCase("MSSQL_MOCK")) + MSSQL_MOCK = openDataSource("jdbc:mysql://" + dbHost, userName, password); + } + */ + } +} diff --git a/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/database/DatabaseRunnable.java b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/database/DatabaseRunnable.java new file mode 100644 index 00000000..6aa95c8f --- /dev/null +++ b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/database/DatabaseRunnable.java @@ -0,0 +1,38 @@ +package mineplex.serverdata.database; + +import java.util.concurrent.atomic.AtomicInteger; + +import org.omg.CORBA.ACTIVITY_REQUIRED; + +public class DatabaseRunnable +{ + private Runnable _runnable; + private String _errorMessage; + private AtomicInteger _failedAttempts = new AtomicInteger(0); + + public DatabaseRunnable(Runnable runnable, String error) + { + _runnable = runnable; + _errorMessage = error; + } + + public void run() + { + _runnable.run(); + } + + public String getErrorMessage() + { + return _errorMessage; + } + + public void incrementFailCount() + { + _failedAttempts.getAndIncrement(); + } + + public int getFailedCounts() + { + return _failedAttempts.get(); + } +} diff --git a/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/database/RepositoryBase.java b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/database/RepositoryBase.java new file mode 100644 index 00000000..d183b931 --- /dev/null +++ b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/database/RepositoryBase.java @@ -0,0 +1,314 @@ +package mineplex.serverdata.database; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import javax.sql.DataSource; + +import com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException; + +import org.jooq.DSLContext; +import org.jooq.SQLDialect; +import org.jooq.impl.DSL; + +import mineplex.serverdata.database.column.Column; + +public abstract class RepositoryBase +{ + static + { + try + { + Class.forName("com.mysql.jdbc.Driver"); + } catch (ClassNotFoundException e) + { + e.printStackTrace(); + } + } + + private DataSource _dataSource; // Connection pool + + /** + * Constructor + * @param dataSource - the {@link DataSource} responsible for providing the connection pool to this repository. + */ + public RepositoryBase(DataSource dataSource) + { + _dataSource = dataSource; + + new Thread(() -> { + initialize(); + update(); + }).start(); + } + + protected void initialize() + { + + } + + protected void update() + { + + } + + /** + * @return the {@link DataSource} used by the repository for connection pooling. + */ + protected DataSource getConnectionPool() + { + return _dataSource; + } + + /** + * Requirements: {@link Connection}s must be closed after usage so they may be returned to the pool! + * @see Connection#close() + * @return a newly fetched {@link Connection} from the connection pool, if a connection can be made, null otherwise. + */ + protected Connection getConnection() + { + try + { + return _dataSource.getConnection(); + } + catch (SQLException e) + { + e.printStackTrace(); + // TODO: Log connection failures? + return null; + } + } + + protected int executeUpdate(Connection connection, String query, Runnable onSQLError, Column...columns) + { + return executeInsert(connection, query, null, onSQLError, columns); + } + + protected int executeUpdate(String query, Runnable onSQLError, Column...columns) + { + return executeInsert(query, null, onSQLError, columns); + } + + /** + * Execute a query against the repository. + * @param query - the concatenated query to execute in string form. + * @param columns - the column data values used for insertion into the query. + * @return the number of rows affected by this query in the repository. + */ + protected int executeUpdate(String query, Column...columns) + { + return executeInsert(query, null, columns); + } + + protected int executeInsert(Connection connection, String query, ResultSetCallable callable, Runnable onSQLError, Column...columns) + { + int affectedRows = 0; + + // Automatic resource management for handling/closing objects. + try ( + PreparedStatement preparedStatement = connection.prepareStatement(query, Statement.RETURN_GENERATED_KEYS) + ) + { + for (int i=0; i < columns.length; i++) + { + columns[i].setValue(preparedStatement, i+1); + } + + affectedRows = preparedStatement.executeUpdate(); + + if (callable != null) + { + callable.processResultSet(preparedStatement.getGeneratedKeys()); + } + } + catch (SQLException exception) + { + exception.printStackTrace(); + if (onSQLError != null) + { + onSQLError.run(); + } + } + catch (Exception exception) + { + exception.printStackTrace(); + } + + return affectedRows; + } + + protected int executeInsert(String query, ResultSetCallable callable, Runnable onSQLError, Column...columns) + { + int affectedRows = 0; + + // Automatic resource management for handling/closing objects. + try ( + Connection connection = getConnection(); + ) + { + affectedRows = executeInsert(connection, query, callable, onSQLError, columns); + } + catch (SQLException exception) + { + exception.printStackTrace(); + if (onSQLError != null) + { + onSQLError.run(); + } + } + catch (Exception exception) + { + exception.printStackTrace(); + } + + return affectedRows; + } + + protected int executeInsert(String query, ResultSetCallable callable, Column...columns) + { + int affectedRows = 0; + + // Automatic resource management for handling/closing objects. + try ( + Connection connection = getConnection(); + ) + { + affectedRows = executeInsert(connection, query, callable, null, columns); + } + catch (Exception exception) + { + exception.printStackTrace(); + } + + return affectedRows; + } + + protected void executeQuery(PreparedStatement statement, ResultSetCallable callable, Runnable onSQLError, Column...columns) + { + try + { + for (int i=0; i < columns.length; i++) + { + columns[i].setValue(statement, i+1); + } + + try (ResultSet resultSet = statement.executeQuery()) + { + if (callable != null) + { + callable.processResultSet(resultSet); + } + } + } + catch (SQLException exception) + { + exception.printStackTrace(); + if (onSQLError != null) + { + onSQLError.run(); + } + } + catch (Exception exception) + { + exception.printStackTrace(); + } + } + + protected void executeQuery(PreparedStatement statement, ResultSetCallable callable, Column...columns) + { + executeQuery(statement, callable, null, columns); + } + + protected void executeQuery(Connection connection, String query, ResultSetCallable callable, Runnable onSQLError, Column...columns) + { + // Automatic resource management for handling/closing objects. + try ( + PreparedStatement preparedStatement = connection.prepareStatement(query) + ) + { + executeQuery(preparedStatement, callable, onSQLError, columns); + } + catch (SQLException exception) + { + exception.printStackTrace(); + if (onSQLError != null) + { + onSQLError.run(); + } + } + catch (Exception exception) + { + exception.printStackTrace(); + } + } + + protected void executeQuery(Connection connection, String query, ResultSetCallable callable, Column...columns) + { + // Automatic resource management for handling/closing objects. + try ( + PreparedStatement preparedStatement = connection.prepareStatement(query) + ) + { + executeQuery(preparedStatement, callable, columns); + } + catch (MySQLSyntaxErrorException syntaxException) + { + System.err.println("Query \"" + query + "\" contained a syntax error:"); + syntaxException.printStackTrace(); + } + catch (Exception exception) + { + exception.printStackTrace(); + } + } + + protected void executeQuery(String query, ResultSetCallable callable, Runnable onSQLError, Column...columns) + { + // Automatic resource management for handling/closing objects. + try ( + Connection connection = getConnection(); + ) + { + executeQuery(connection, query, callable, onSQLError, columns); + } + catch (SQLException exception) + { + exception.printStackTrace(); + if (onSQLError != null) + { + onSQLError.run(); + } + } + catch (Exception exception) + { + exception.printStackTrace(); + } + } + + protected void executeQuery(String query, ResultSetCallable callable, Column...columns) + { + // Automatic resource management for handling/closing objects. + try ( + Connection connection = getConnection(); + ) + { + executeQuery(connection, query, callable, columns); + } + catch (SQLException exception) + { + exception.printStackTrace(); + } + catch (Exception exception) + { + exception.printStackTrace(); + } + } + + protected DSLContext jooq() + { + return DSL.using(DBPool.getAccount(), SQLDialect.MYSQL); + } +} \ No newline at end of file diff --git a/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/database/ResultSetCallable.java b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/database/ResultSetCallable.java new file mode 100644 index 00000000..adac7876 --- /dev/null +++ b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/database/ResultSetCallable.java @@ -0,0 +1,9 @@ +package mineplex.serverdata.database; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public interface ResultSetCallable +{ + public void processResultSet(ResultSet resultSet) throws SQLException; +} diff --git a/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/database/column/Column.java b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/database/column/Column.java new file mode 100644 index 00000000..939e29a8 --- /dev/null +++ b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/database/column/Column.java @@ -0,0 +1,30 @@ +package mineplex.serverdata.database.column; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +public abstract class Column +{ + public String Name; + public Type Value; + + public Column(String name) + { + Name = name; + } + + public Column(String name, Type value) + { + Name = name; + Value = value; + } + + public abstract String getCreateString(); + + public abstract Type getValue(ResultSet resultSet) throws SQLException; + + public abstract void setValue(PreparedStatement preparedStatement, int columnNumber) throws SQLException; + + public abstract Column clone(); +} diff --git a/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/database/column/ColumnBoolean.java b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/database/column/ColumnBoolean.java new file mode 100644 index 00000000..4eb6a48c --- /dev/null +++ b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/database/column/ColumnBoolean.java @@ -0,0 +1,42 @@ +package mineplex.serverdata.database.column; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +public class ColumnBoolean extends Column +{ + public ColumnBoolean(String name) + { + super(name); + } + + public ColumnBoolean(String name, boolean value) + { + super(name, value); + } + + @Override + public String getCreateString() + { + return Name + " BOOLEAN"; + } + + @Override + public Boolean getValue(ResultSet resultSet) throws SQLException + { + return resultSet.getBoolean(Name); + } + + @Override + public void setValue(PreparedStatement preparedStatement, int columnNumber) throws SQLException + { + preparedStatement.setBoolean(columnNumber, Value); + } + + @Override + public ColumnBoolean clone() + { + return new ColumnBoolean(Name, Value); + } +} diff --git a/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/database/column/ColumnByte.java b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/database/column/ColumnByte.java new file mode 100644 index 00000000..9a92bdb3 --- /dev/null +++ b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/database/column/ColumnByte.java @@ -0,0 +1,43 @@ +package mineplex.serverdata.database.column; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +public class ColumnByte extends Column +{ + public ColumnByte(String name) + { + super(name); + Value = (byte)0; + } + + public ColumnByte(String name, Byte value) + { + super(name, value); + } + + @Override + public String getCreateString() + { + return Name + " TINYINT"; + } + + @Override + public Byte getValue(ResultSet resultSet) throws SQLException + { + return resultSet.getByte(Name); + } + + @Override + public void setValue(PreparedStatement preparedStatement, int columnNumber) throws SQLException + { + preparedStatement.setLong(columnNumber, Value); + } + + @Override + public ColumnByte clone() + { + return new ColumnByte(Name, Value); + } +} diff --git a/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/database/column/ColumnDouble.java b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/database/column/ColumnDouble.java new file mode 100644 index 00000000..2c7fe76c --- /dev/null +++ b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/database/column/ColumnDouble.java @@ -0,0 +1,43 @@ +package mineplex.serverdata.database.column; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +public class ColumnDouble extends Column +{ + public ColumnDouble(String name) + { + super(name); + Value = 0.0; + } + + public ColumnDouble(String name, Double value) + { + super(name, value); + } + + @Override + public String getCreateString() + { + return Name + " DOUBLE"; + } + + @Override + public Double getValue(ResultSet resultSet) throws SQLException + { + return resultSet.getDouble(Name); + } + + @Override + public void setValue(PreparedStatement preparedStatement, int columnNumber) throws SQLException + { + preparedStatement.setDouble(columnNumber, Value); + } + + @Override + public ColumnDouble clone() + { + return new ColumnDouble(Name, Value); + } +} diff --git a/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/database/column/ColumnInt.java b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/database/column/ColumnInt.java new file mode 100644 index 00000000..348aeb1d --- /dev/null +++ b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/database/column/ColumnInt.java @@ -0,0 +1,43 @@ +package mineplex.serverdata.database.column; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +public class ColumnInt extends Column +{ + public ColumnInt(String name) + { + super(name); + Value = 0; + } + + public ColumnInt(String name, int value) + { + super(name, value); + } + + @Override + public String getCreateString() + { + return Name + " INT"; + } + + @Override + public Integer getValue(ResultSet resultSet) throws SQLException + { + return resultSet.getInt(Name); + } + + @Override + public void setValue(PreparedStatement preparedStatement, int columnNumber) throws SQLException + { + preparedStatement.setInt(columnNumber, Value); + } + + @Override + public ColumnInt clone() + { + return new ColumnInt(Name, Value); + } +} diff --git a/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/database/column/ColumnLong.java b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/database/column/ColumnLong.java new file mode 100644 index 00000000..d3eec3a9 --- /dev/null +++ b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/database/column/ColumnLong.java @@ -0,0 +1,43 @@ +package mineplex.serverdata.database.column; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +public class ColumnLong extends Column +{ + public ColumnLong(String name) + { + super(name); + Value = 0L; + } + + public ColumnLong(String name, Long value) + { + super(name, value); + } + + @Override + public String getCreateString() + { + return Name + " LONG"; + } + + @Override + public Long getValue(ResultSet resultSet) throws SQLException + { + return resultSet.getLong(Name); + } + + @Override + public void setValue(PreparedStatement preparedStatement, int columnNumber) throws SQLException + { + preparedStatement.setLong(columnNumber, Value); + } + + @Override + public ColumnLong clone() + { + return new ColumnLong(Name, Value); + } +} diff --git a/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/database/column/ColumnTimestamp.java b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/database/column/ColumnTimestamp.java new file mode 100644 index 00000000..6ce17f63 --- /dev/null +++ b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/database/column/ColumnTimestamp.java @@ -0,0 +1,43 @@ +package mineplex.serverdata.database.column; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; + +public class ColumnTimestamp extends Column +{ + public ColumnTimestamp(String name) + { + super(name); + } + + public ColumnTimestamp(String name, Timestamp value) + { + super(name, value); + } + + @Override + public String getCreateString() + { + return Name + " TIMESTAMP"; + } + + @Override + public Timestamp getValue(ResultSet resultSet) throws SQLException + { + return resultSet.getTimestamp(Name); + } + + @Override + public void setValue(PreparedStatement preparedStatement, int columnNumber) throws SQLException + { + preparedStatement.setTimestamp(columnNumber, Value); + } + + @Override + public ColumnTimestamp clone() + { + return new ColumnTimestamp(Name, Value); + } +} diff --git a/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/database/column/ColumnVarChar.java b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/database/column/ColumnVarChar.java new file mode 100644 index 00000000..a60b3b19 --- /dev/null +++ b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/database/column/ColumnVarChar.java @@ -0,0 +1,46 @@ +package mineplex.serverdata.database.column; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +public class ColumnVarChar extends Column +{ + public int Length = 25; + + public ColumnVarChar(String name, int length) + { + this(name, length, ""); + } + + public ColumnVarChar(String name, int length, String value) + { + super(name); + + Length = length; + Value = value; + } + + public String getCreateString() + { + return Name + " VARCHAR(" + Length + ")"; + } + + @Override + public String getValue(ResultSet resultSet) throws SQLException + { + return resultSet.getString(Name); + } + + @Override + public void setValue(PreparedStatement preparedStatement, int columnNumber) throws SQLException + { + preparedStatement.setString(columnNumber, Value); + } + + @Override + public ColumnVarChar clone() + { + return new ColumnVarChar(Name, Length, Value); + } +} diff --git a/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/redis/RedisConfig.java b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/redis/RedisConfig.java new file mode 100644 index 00000000..e1df2f49 --- /dev/null +++ b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/redis/RedisConfig.java @@ -0,0 +1,79 @@ +package mineplex.serverdata.redis; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +import mineplex.serverdata.servers.ConnectionData; +import mineplex.serverdata.servers.ConnectionData.ConnectionType; + +public class RedisConfig +{ + // Failsafe values in case configuration is not provided + private static final String DEFAULT_IP = "127.0.0.1"; + private static final int DEFAULT_PORT = 6379; + private static Random random = new Random(); // Utility random + + // The connections managed by this configuration + private List _connections; + + /** + * Class constructor + * @param connections + */ + public RedisConfig(List connections) + { + _connections = connections; + } + + /** + * Class constructor + * Produces a default-value based RedisConfig. + */ + public RedisConfig() + { + _connections = new ArrayList(); + _connections.add(new ConnectionData(DEFAULT_IP, DEFAULT_PORT, ConnectionType.MASTER, "DefaultConnection")); + } + + /** + * {@code writeable} defaults to {@literal true}. + */ + public ConnectionData getConnection() + { + return getConnection(true, null); + } + + /** + * @param writeable - whether the returned connection reference can receive write-requests. + * @return a {@link ConnectionData} referencing a valid redis-connection from this configuration. + */ + public ConnectionData getConnection(boolean writeable, String name) + { + List connections = getConnections(writeable, name); + + if (connections.size() > 0) + { + int index = random.nextInt(connections.size()); + return connections.get(index); + } + + return null; + } + + public List getConnections(boolean writeable, String name) + { + List connections = new ArrayList(); + ConnectionType type = (writeable) ? ConnectionType.MASTER : ConnectionType.SLAVE; + + for (ConnectionData connection : _connections) + { + if (connection.getType() == type && connection.nameMatches(name)) + { + connections.add(connection); + } + } + + return connections; + } +} diff --git a/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/redis/RedisDataRepository.java b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/redis/RedisDataRepository.java new file mode 100644 index 00000000..55f3a2d5 --- /dev/null +++ b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/redis/RedisDataRepository.java @@ -0,0 +1,268 @@ +package mineplex.serverdata.redis; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import mineplex.serverdata.Region; +import mineplex.serverdata.Utility; +import mineplex.serverdata.data.Data; +import mineplex.serverdata.data.DataRepository; +import mineplex.serverdata.servers.ConnectionData; +import mineplex.serverdata.servers.ServerManager; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.Pipeline; +import redis.clients.jedis.Response; +import redis.clients.jedis.Transaction; + +public class RedisDataRepository extends RedisRepository implements DataRepository +{ + // The class type of the elements stored in this repository + private Class _elementType; + // A unique label designating the elements and this repository. + private String _elementLabel; + + /** + * Class constructor + * @param writeConn + * @param readConn + * @param region + */ + public RedisDataRepository(ConnectionData writeConn, ConnectionData readConn, Region region, + Class elementType, String elementLabel) + { + super(writeConn, readConn, region); + _elementType = elementType; + _elementLabel = elementLabel; + } + + public RedisDataRepository(ConnectionData conn, Region region, Class elementType, String elementLabel) + { + this(conn, conn, region, elementType, elementLabel); + } + + public RedisDataRepository(Region region, Class elementType, String elementLabel) + { + this(ServerManager.getMasterConnection(), ServerManager.getSlaveConnection(), region, + elementType, elementLabel); + } + + public String getElementSetKey() + { + return concatenate("data", _elementLabel, getRegion().toString()); + } + + public String generateKey(T element) + { + return generateKey(element.getDataId()); + } + + public String generateKey(String dataId) + { + return concatenate(getElementSetKey(), dataId); + } + + @Override + public Collection getElements() + { + return getElements(getActiveElements()); + } + + @Override + public Collection getElements(Collection dataIds) + { + Collection elements = new HashSet(); + + try(Jedis jedis = getResource(false)) + { + Pipeline pipeline = jedis.pipelined(); + + List> responses = new ArrayList>(); + for (String dataId : dataIds) + { + responses.add(pipeline.get(generateKey(dataId))); + } + + // Block until all requests have received pipelined responses + pipeline.sync(); + + for (Response response : responses) + { + String serializedData = response.get(); + T element = deserialize(serializedData); + + if (element != null) + { + elements.add(element); + } + } + } + + return elements; + } + + @Override + public Map getElementsMap(List dataIds) + { + Map elements = new HashMap<>(); + + try(Jedis jedis = getResource(false)) + { + Pipeline pipeline = jedis.pipelined(); + + List> responses = new ArrayList<>(); + for (String dataId : dataIds) + { + responses.add(pipeline.get(generateKey(dataId))); + } + + // Block until all requests have received pipelined responses + pipeline.sync(); + + for (int i = 0; i < responses.size(); i++) + { + String key = dataIds.get(i); + + Response response = responses.get(i); + String serializedData = response.get(); + T element = deserialize(serializedData); + + elements.put(key, element); + } + } + + return elements; + } + + @Override + public T getElement(String dataId) + { + T element = null; + + try(Jedis jedis = getResource(false)) + { + String key = generateKey(dataId); + String serializedData = jedis.get(key); + element = deserialize(serializedData); + } + + return element; + } + + @Override + public void addElement(T element, int timeout) + { + try(Jedis jedis = getResource(true)) + { + String serializedData = serialize(element); + String dataId = element.getDataId(); + String setKey = getElementSetKey(); + String dataKey = generateKey(element); + long expiry = currentTime() + timeout; + + Transaction transaction = jedis.multi(); + transaction.set(dataKey, serializedData); + transaction.zadd(setKey, expiry, dataId.toString()); + transaction.exec(); + } + } + + @Override + public void addElement(T element) + { + addElement(element, 60 * 60 * 24 * 7 * 4 * 12 * 10); // Set the timeout to 10 years + } + + @Override + public void removeElement(T element) + { + removeElement(element.getDataId()); + } + + @Override + public void removeElement(String dataId) + { + try(Jedis jedis = getResource(true)) + { + String setKey = getElementSetKey(); + String dataKey = generateKey(dataId); + + Transaction transaction = jedis.multi(); + transaction.del(dataKey); + transaction.zrem(setKey, dataId); + transaction.exec(); + } + } + + @Override + public boolean elementExists(String dataId) + { + return getElement(dataId) != null; + } + + @Override + public int clean() + { + try(Jedis jedis = getResource(true)) + { + for (String dataId : getDeadElements()) + { + String dataKey = generateKey(dataId); + + Transaction transaction = jedis.multi(); + transaction.del(dataKey); + transaction.zrem(getElementSetKey(), dataId); + transaction.exec(); + } + } + + return 0; + } + + protected Set getActiveElements() + { + Set dataIds = new HashSet(); + + try(Jedis jedis = getResource(false)) + { + String min = "(" + currentTime(); + String max = "+inf"; + dataIds = jedis.zrangeByScore(getElementSetKey(), min, max); + } + + return dataIds; + } + + protected Set getDeadElements() + { + Set dataIds = new HashSet(); + + try(Jedis jedis = getResource(false)) + { + String min = "-inf"; + String max = currentTime() + ""; + dataIds = jedis.zrangeByScore(getElementSetKey(), min, max); + } + + return dataIds; + } + + protected T deserialize(String serializedData) + { + return Utility.deserialize(serializedData, _elementType); + } + + protected String serialize(T element) + { + return Utility.serialize(element); + } + + protected Long currentTime() + { + return Utility.currentTimeSeconds(); + } +} diff --git a/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/redis/RedisRepository.java b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/redis/RedisRepository.java new file mode 100644 index 00000000..dd43e1fb --- /dev/null +++ b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/redis/RedisRepository.java @@ -0,0 +1,70 @@ +package mineplex.serverdata.redis; + +import mineplex.serverdata.Region; +import mineplex.serverdata.Utility; +import mineplex.serverdata.servers.ConnectionData; +import mineplex.serverdata.servers.ServerManager; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisPool; + +/** + * Repository for managing Redis Connections + * @author Shaun Bennett + */ +public class RedisRepository +{ + protected static final char KEY_DELIMITER = '.'; + + private JedisPool _writePool; + private JedisPool _readPool; + private Region _region; + + public RedisRepository(ConnectionData writeConn, ConnectionData readConn, Region region) + { + _writePool = Utility.generatePool(writeConn); + _readPool = (writeConn == readConn) ? _writePool : Utility.generatePool(readConn); + _region = region; + } + + public RedisRepository(Region region) + { + this(ServerManager.getMasterConnection(), ServerManager.getSlaveConnection(), region); + } + + /** + * Get a Jedis Resource from the pool. This Jedis instance needs to be closed when you are done with using it. + * Call jedis.close() or use try with resources when using getResource() + * + * @param writeable If we need to be able to write to redis. Trying to write to a non writeable jedis instance will + * throw an error. + * @return {@link Jedis} instance from pool + */ + protected Jedis getResource(boolean writeable) + { + return (writeable ? _writePool : _readPool).getResource(); + } + + /** + * Get the server region that this redis repository is for. The region will affect the keys for redis + * @return server region + */ + public Region getRegion() + { + return _region; + } + + protected String getKey(String dataKey) + { + return concatenate("minecraft", "data", _region.name(), dataKey); + } + + /** + * @param elements - the elements to concatenate together + * @return the concatenated form of all {@code elements} + * separated by the delimiter {@value KEY_DELIMITER}. + */ + protected String concatenate(String... elements) + { + return Utility.concatenate(KEY_DELIMITER, elements); + } +} diff --git a/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/redis/RedisServerRepository.java b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/redis/RedisServerRepository.java new file mode 100644 index 00000000..113f6836 --- /dev/null +++ b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/redis/RedisServerRepository.java @@ -0,0 +1,394 @@ +package mineplex.serverdata.redis; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import mineplex.serverdata.Region; +import mineplex.serverdata.Utility; +import mineplex.serverdata.data.DedicatedServer; +import mineplex.serverdata.data.MinecraftServer; +import mineplex.serverdata.data.ServerGroup; +import mineplex.serverdata.servers.ConnectionData; +import mineplex.serverdata.servers.ServerRepository; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.Pipeline; +import redis.clients.jedis.Response; +import redis.clients.jedis.Transaction; +import redis.clients.jedis.Tuple; +import redis.clients.jedis.exceptions.JedisConnectionException; + +/** + * RedisServerRepository offers a Redis-based implementation of {@link ServerRepository} + * using a mixture of hash and JSON encoded storage. + * @author Ty + * + */ +public class RedisServerRepository extends RedisRepository implements ServerRepository +{ + public RedisServerRepository(ConnectionData writeConn, ConnectionData readConn, Region region) + { + super(writeConn, readConn, region); + } + + @Override + public Collection getServerStatuses() + { + return getServerStatusesByPrefix(""); + } + + @Override + public Collection getServerStatusesByPrefix(String prefix) + { + Collection servers = new HashSet(); + + try(Jedis jedis = getResource(false)) + { + String setKey = concatenate("serverstatus", "minecraft", getRegion().toString()); + Pipeline pipeline = jedis.pipelined(); + List> responses = new ArrayList>(); + for (String serverName : getActiveNames(setKey)) + { + if (prefix.isEmpty() || serverName.startsWith(prefix)) + { + String dataKey = concatenate(setKey, serverName); + responses.add(pipeline.get(dataKey)); + } + } + + pipeline.sync(); + + for (Response response : responses) + { + String serializedData = response.get(); + MinecraftServer server = Utility.deserialize(serializedData, MinecraftServer.class); + + if (server != null) + { + servers.add(server); + } + } + } + return servers; + } + + @Override + public Collection getServersByGroup(String serverGroup) + { + Collection servers = new HashSet(); + + for (MinecraftServer server : getServerStatuses()) + { + if (server.getGroup().equalsIgnoreCase(serverGroup)) + { + servers.add(server); + } + } + + return servers; + } + + @Override + public MinecraftServer getServerStatus(String serverName) + { + MinecraftServer server = null; + + try(Jedis jedis = getResource(false)) + { + String setKey = concatenate("serverstatus", "minecraft", getRegion().toString()); + String dataKey = concatenate(setKey, serverName); + String serializedData = jedis.get(dataKey); + server = Utility.deserialize(serializedData, MinecraftServer.class); + } + + return server; + } + + @Override + public void updataServerStatus(MinecraftServer serverData, int timeout) + { + try(Jedis jedis = getResource(true)) + { + String serializedData = Utility.serialize(serverData); + String serverName = serverData.getName(); + String setKey = concatenate("serverstatus", "minecraft", getRegion().toString()); + String dataKey = concatenate(setKey, serverName); + long expiry = Utility.currentTimeSeconds() + timeout; + + Transaction transaction = jedis.multi(); + transaction.set(dataKey, serializedData); + transaction.zadd(setKey, expiry, serverName); + transaction.exec(); + } + } + + @Override + public void removeServerStatus(MinecraftServer serverData) + { + try(Jedis jedis = getResource(true)) + { + String serverName = serverData.getName(); + String setKey = concatenate("serverstatus", "minecraft", getRegion().toString()); + String dataKey = concatenate(setKey, serverName); + + Transaction transaction = jedis.multi(); + transaction.del(dataKey); + transaction.zrem(setKey, serverName); + transaction.exec(); + } + } + + @Override + public boolean serverExists(String serverName) + { + return getServerStatus(serverName) != null; + } + + @Override + public Collection getDedicatedServers() + { + Collection servers = new HashSet(); + + try(Jedis jedis = getResource(false)) + { + String key = concatenate("serverstatus", "dedicated"); + Set serverNames = jedis.smembers(key); + HashMap>> serverDatas = new HashMap>>(); + + Pipeline pipeline = jedis.pipelined(); + + for (String serverName : serverNames) + { + String dataKey = concatenate(key, serverName); + serverDatas.put(serverName, pipeline.hgetAll(dataKey)); + } + + pipeline.sync(); + + for (Entry>> responseEntry : serverDatas.entrySet()) + { + Map data = responseEntry.getValue().get(); + + try + { + DedicatedServer server = new DedicatedServer(data); + + if (server.getRegion() == getRegion()) + servers.add(server); + } + catch (Exception ex) + { + System.out.println(responseEntry.getKey() + " Errored"); + throw ex; + } + } + } + + return servers; + } + + @Override + public Collection getServerGroups(Collection serverStatuses) + { + Collection servers = new HashSet(); + + try(Jedis jedis = getResource(false)) + { + String key = "servergroups"; + Set names = jedis.smembers(key); + Set>> serverDatas = new HashSet>>(); + + Pipeline pipeline = jedis.pipelined(); + + for (String serverName : names) + { + String dataKey = concatenate(key, serverName); + serverDatas.add(pipeline.hgetAll(dataKey)); + } + + pipeline.sync(); + + for (Response> response : serverDatas) + { + Map data = response.get(); + + if (data.entrySet().size() == 0) + { + // please no +// System.out.println("Encountered empty map! Skipping..."); + continue; + } + + try + { + ServerGroup serverGroup = new ServerGroup(data, serverStatuses); + + if (serverGroup.getRegion() == Region.ALL || serverGroup.getRegion() == getRegion()) + servers.add(serverGroup); + } + catch (Exception exception) + { + System.out.println("Error parsing ServerGroup : " + data.get("name")); + exception.printStackTrace(); + } + } + } + + return servers; + } + + /** + * @param key - the key where the sorted set of server sessions is stored + * @return the {@link Set} of active server names stored at {@code key} for non-expired + * servers. + */ + protected Set getActiveNames(String key) + { + Set names = new HashSet(); + + try(Jedis jedis = getResource(false)) + { + String min = "(" + Utility.currentTimeSeconds(); + String max = "+inf"; + names = jedis.zrangeByScore(key, min, max); + } + return names; + } + + /** + * @param key - the key where the sorted set of server sessions is stored + * @return the {@link Set} of dead (expired) server names stored at {@code key}. + */ + protected Set getDeadNames(String key) + { + Set names = new HashSet(); + + try(Jedis jedis = getResource(false)) + { + String min = "-inf"; + String max = Utility.currentTimeSeconds() + ""; + names = jedis.zrangeByScore(key, min, max); + } + + return names; + } + + @Override + public Collection getDeadServers() + { + Set servers = new HashSet(); + + try(Jedis jedis = getResource(false)) + { + Pipeline pipeline = jedis.pipelined(); + String setKey = concatenate("serverstatus", "minecraft", getRegion().toString()); + String min = "-inf"; + String max = Utility.currentTimeSeconds() + ""; + + List> responses = new ArrayList>(); + for (Tuple serverName : jedis.zrangeByScoreWithScores(setKey, min, max)) + { + String dataKey = concatenate(setKey, serverName.getElement()); + responses.add(pipeline.get(dataKey)); + } + + pipeline.sync(); + + for (Response response : responses) + { + String serializedData = response.get(); + MinecraftServer server = Utility.deserialize(serializedData, MinecraftServer.class); + + if (server != null) + servers.add(server); + } + } + + return servers; + } + + @Override + public void updateServerGroup(ServerGroup serverGroup) + { + try(Jedis jedis = getResource(true)) + { + HashMap serializedData = serverGroup.getDataMap(); + System.out.println(serializedData); + String serverGroupName = serverGroup.getName(); + String key = "servergroups"; + String dataKey = concatenate(key, serverGroupName); + + Transaction transaction = jedis.multi(); + transaction.hmset(dataKey, serializedData); + transaction.sadd(key, serverGroupName); + transaction.exec(); + } + } + + @Override + public void removeServerGroup(ServerGroup serverGroup) + { + try(Jedis jedis = getResource(true)) + { + String serverName = serverGroup.getName(); + String setKey = "servergroups"; + String dataKey = concatenate(setKey, serverName); + + Transaction transaction = jedis.multi(); + transaction.del(dataKey); + transaction.srem(setKey, serverName); + transaction.exec(); + } + } + + @Override + public ServerGroup getServerGroup(String serverGroup) + { + ServerGroup server = null; + try(Jedis jedis = getResource(false)) + { + String key = concatenate("servergroups", serverGroup); + Map data = jedis.hgetAll(key); + + server = new ServerGroup(data, null); + } + + return server; + } + + /* + * = "US" or "EU" + * serverstatus.minecraft.. stores the JSON encoded information of an active MinecraftServer instance. + * serverstatus.minecraft. stores a sorted set with the set of name's for MinecraftServers + * with a value of their expiry date (in ms) + * + * ----------------------- + * + * serverstatus.dedicated. stores the hash containing information of an active dedicated server instance + * serverstatus.dedicated stores the set of active dedicated server names. + * serverstatus.dedicated uses a hash with the following keys: + * name, publicAddress, privateAddress, region, cpu, ram + * + * Example commands for adding/creating a new dedicated server: + * 1. HMSET serverstatus.dedicated. name publicAddress privateAddress region cpu ram + * 2. SADD serverstatus.dedicated + * + * ------------------------ + * + * servergroups. stores the hash-set containing information for the server group type. + * servergroups stores the set of active server group names. + * servergroups. stores a hash of the following key name/values + * name, prefix, scriptName, ram, cpu, totalServers, joinableServers + * + * Example commands for adding/creating a new server group: + * + * 1. HMSET servergroups. name prefix scriptName ram cpu totalServers joinableServers + * 2. SADD servergroups + */ +} diff --git a/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/redis/atomic/RedisStringRepository.java b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/redis/atomic/RedisStringRepository.java new file mode 100644 index 00000000..b637b491 --- /dev/null +++ b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/redis/atomic/RedisStringRepository.java @@ -0,0 +1,70 @@ +package mineplex.serverdata.redis.atomic; + +import mineplex.serverdata.Region; +import mineplex.serverdata.redis.RedisRepository; +import mineplex.serverdata.servers.ConnectionData; + +import redis.clients.jedis.Jedis; + +public class RedisStringRepository extends RedisRepository +{ + private final String _dataKey; + private final int _expiration; + + public RedisStringRepository(ConnectionData writeConn, ConnectionData readConn, Region region, String dataKey, int expiryInSeconds) + { + super(writeConn, readConn, region); + this._dataKey = dataKey; + this._expiration = expiryInSeconds; + } + + public RedisStringRepository(ConnectionData writeConn, ConnectionData readConn, Region region, String dataKey) + { + this(writeConn, readConn, region, dataKey, -1); + } + + public void set(String key, String value) + { + try (Jedis jedis = getResource(true)) + { + if (_expiration == -1) + { + jedis.set(generateKey(key), value); + } + else + { + jedis.setex(generateKey(key), _expiration, value); + } + } + } + + public String get(String key) + { + String element; + + try (Jedis jedis = getResource(false)) + { + element = jedis.get(generateKey(key)); + } + + return element; + } + + public void del(String key) + { + try (Jedis jedis = getResource(true)) + { + jedis.del(generateKey(key)); + } + } + + private String getElementSetKey() + { + return concatenate("data", _dataKey, getRegion().toString()); + } + + private String generateKey(String dataId) + { + return concatenate(getElementSetKey(), dataId); + } +} diff --git a/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/redis/counter/Counter.java b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/redis/counter/Counter.java new file mode 100644 index 00000000..af6a0504 --- /dev/null +++ b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/redis/counter/Counter.java @@ -0,0 +1,103 @@ +package mineplex.serverdata.redis.counter; + +import mineplex.serverdata.Region; +import mineplex.serverdata.servers.ConnectionData; + +import java.util.concurrent.atomic.AtomicLong; + +/** + * A counter represents an incrementing atomic number that is stored and updated through redis. This allows for + * multiple servers to share and update the same counter + * + * @author Shaun Bennett + */ +public class Counter +{ + // Cached count of the counter + private final AtomicLong _count = new AtomicLong(0); + // The System.currentTimeMillis() when cached count was last updated + private volatile long _lastUpdated; + // The unique key to reference this counter + private final String _dataKey; + + // Redis repository to store the count + private final CounterRedisRepository _redisRepository; + + public Counter(ConnectionData writeConnection, ConnectionData readConnection, Region region, String dataKey) + { + _dataKey = dataKey; + _redisRepository = new CounterRedisRepository(writeConnection, readConnection, region, dataKey); + } + + public Counter(String dataKey) + { + _dataKey = dataKey; + _redisRepository = new CounterRedisRepository(dataKey); + } + + + /** + * Add a value to the counter and return the new counter value. This method is thread-safe and interacts + * directly with the atomic value stored in redis. The value returned from redis is then returned + * + * addAndGet will also update the cached counter value so we don't need to make extra trips to redis + * + * @param amount the amount to add to the counter + * @return the updated value of the counter from redis repository + */ + public long addAndGet(long amount) + { + long newCount = _redisRepository.incrementCount(amount); + updateCount(newCount); + return newCount; + } + + /** + * Get the latest cached count from the counter. This value will not be changed until {@link #addAndGet(long)} + * or {@link #updateCount} is called. + * + * @return The counter count + */ + public long getCount() + { + return _count.get(); + } + + /** + * Update the cached count to reflect the count in redis. This should be called async + */ + public void updateCount() + { + updateCount(_redisRepository.getCount()); + } + + /** + * Reset the counter back to 0. Immediately updates the redis repository. + * + * @return The value of the counter before it was reset + */ + public long reset() + { + updateCount(0); + return _redisRepository.reset(); + } + + /** + * Get the data key for this counter. The data key is used as the redis repository key + * @return The data key for this counter + */ + public String getDataKey() + { + return _dataKey; + } + + /** + * Update the cached count with a new value + * @param newCount updated count + */ + protected void updateCount(long newCount) + { + _count.set(newCount); + _lastUpdated = System.currentTimeMillis(); + } +} diff --git a/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/redis/counter/CounterRedisRepository.java b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/redis/counter/CounterRedisRepository.java new file mode 100644 index 00000000..69561aed --- /dev/null +++ b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/redis/counter/CounterRedisRepository.java @@ -0,0 +1,100 @@ +package mineplex.serverdata.redis.counter; + +import mineplex.serverdata.Region; +import mineplex.serverdata.redis.RedisRepository; +import mineplex.serverdata.servers.ConnectionData; +import redis.clients.jedis.Jedis; + +/** + * Redis repository to store the count for {@link Counter} + * @author Shaun Bennett + */ +public class CounterRedisRepository extends RedisRepository +{ + private String _dataKey; + + public CounterRedisRepository(ConnectionData writeConnection, ConnectionData readConnection, Region region, String dataKey) + { + super(writeConnection, readConnection, region); + + _dataKey = dataKey; + } + + public CounterRedisRepository(String dataKey) + { + super(Region.ALL); + + _dataKey = dataKey; + } + + /** + * Get the current count inside the fountain + * @return The current count for the fountain + */ + public long getCount() + { + long count = 0; + + try (Jedis jedis = getResource(false)) + { + count = Long.parseLong(jedis.get(getKey())); + } + catch (NumberFormatException ex) + { + ex.printStackTrace(); + } + + return count; + } + + /** + * Increment the current count by {@code increment} and then return the latest + * count from redis. This is handled in an atomic process + * @param increment Amount to increment counter by + * @return The updated count from redis + */ + public long incrementCount(long increment) + { + long count = 0; + + try (Jedis jedis = getResource(true)) + { + count = jedis.incrBy(getKey(), increment); + } + + return count; + } + + /** + * Reset the counter back to 0 + * @return the value of the counter before it was reset + */ + public long reset() + { + long count = -1; + + try (Jedis jedis = getResource(true)) + { + count = Long.parseLong(jedis.getSet(getKey(), "0")); + } + + return count; + } + + /** + * Get the key for this counter + * @return The key is used to store the value in redis + */ + private String getKey() + { + return getKey(_dataKey); + } + +// private void setNX() +// { +// try (Jedis jedis = getResource(true)) +// { +// jedis.setnx(getKey(), "0"); +// } +// } +} diff --git a/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/redis/counter/GoalCounter.java b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/redis/counter/GoalCounter.java new file mode 100644 index 00000000..38bb33d1 --- /dev/null +++ b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/redis/counter/GoalCounter.java @@ -0,0 +1,118 @@ +package mineplex.serverdata.redis.counter; + +import mineplex.serverdata.Region; +import mineplex.serverdata.servers.ConnectionData; + +import java.util.ArrayList; +import java.util.List; + +/** + * A redis counter that is aiming to reach a goal + * + * @author Shaun Bennett + */ +public class GoalCounter extends Counter +{ + private int _lastMilestone; + // Has the goal been completed? + private boolean _completed; + // The goal we are trying to reach + private long _goal; + + private List _listeners; + + public GoalCounter(ConnectionData writeConnection, ConnectionData readConnection, Region region, String dataKey, long goal) + { + super(writeConnection, readConnection, region, dataKey); + + init(goal); + } + + public GoalCounter(String dataKey, long goal) + { + super(dataKey); + + init(goal); + } + + private void init(long goal) + { + _completed = false; + _goal = goal; + _listeners = new ArrayList<>(); + + updateCount(); + + _lastMilestone = (int) getFillPercent(); + + updateMilestone(); + } + + /** + * Get the progress towards the goal as a percent ranging from 0 to 1. + * @return the percent progress towards goal + */ + public double getFillPercent() + { + return (((double) getCount()) / _goal); + } + + /** + * Has the goal been completed? + * @return + */ + public boolean isCompleted() + { + return _completed; + } + + /** + * Get the goal for this GoalCounter + * @return the goal of this counter + */ + public long getGoal() + { + return _goal; + } + + /** + * Add a listener to this GoalCounter + * @param listener the listener to be added + */ + public void addListener(GoalCounterListener listener) + { + _listeners.add(listener); + } + + /** + * Clear all the listeners + */ + public void clearListeners() + { + _listeners.clear(); + } + + /** + * Update {@link #_completed} and notify listeners if it has completed + */ + private void updateMilestone() + { + int currentMilestone = (int) getFillPercent(); + + if (currentMilestone != _lastMilestone && currentMilestone > _lastMilestone) + { + _listeners.forEach(listener -> listener.onMilestone(this, currentMilestone)); + } + + _lastMilestone = currentMilestone; + } + + @Override + protected void updateCount(long newCount) + { + super.updateCount(newCount); + + updateMilestone(); + } + +} diff --git a/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/redis/counter/GoalCounterListener.java b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/redis/counter/GoalCounterListener.java new file mode 100644 index 00000000..dc502046 --- /dev/null +++ b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/redis/counter/GoalCounterListener.java @@ -0,0 +1,9 @@ +package mineplex.serverdata.redis.counter; + +/** + * @author Shaun Bennett + */ +public interface GoalCounterListener +{ + public void onMilestone(GoalCounter counter, int milestone); +} diff --git a/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/redis/messaging/PubSubJedisClient.java b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/redis/messaging/PubSubJedisClient.java new file mode 100644 index 00000000..5a8cfa8a --- /dev/null +++ b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/redis/messaging/PubSubJedisClient.java @@ -0,0 +1,356 @@ +package mineplex.serverdata.redis.messaging; + +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; + +import mineplex.serverdata.Utility; +import mineplex.serverdata.servers.ConnectionData; + +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.JedisPubSub; +import redis.clients.jedis.exceptions.JedisConnectionException; +import redis.clients.jedis.exceptions.JedisDataException; + +/** + * A subscription to a Redis pub/sub system through a Jedis client. Includes a publishing mechanism + * as well. + *

+ * Subscribes to Jedis and offers a variety of methods to edit the channels that this listens to + * over time. Does not support pattern-matching, even though Jedis can. Takes a single subscriber to + * inform of incoming messages (on all channels this is subscribed to). + *

+ * For the sake of internal efficiency, this does not protect the sanity or unchangeability of + * arguments passed into its methods. Clients should not generally interact with this directly. + *

+ * The Jedis pub/sub interface is a little confusing, especially when it comes to multithreading. At + * any given time, if this class is subscribed to any channels at all, it will have a single thread + * that is listening for incoming messages from redis with a blocking method. After that listening + * thread is created, we can add and remove subscriptions as desired, but the initial subscription + * and actual listening must be done on its own thread. When all channels are unsubscribed from, the + * listening thread returns. Note that this is stated with about 70% certainty, as the internals of + * the pub/sub mechanism are not entirely clear to me. + *

+ * This class maintains a constant connection to its redis server by subscribing to a base channel. + * This makes it much easier to protect its operation from potentially insane client commands. + *

+ * If the connection to the given redis instance fails or is interrupted, will keep attempting to + * reconnect periodically until destroyed. Publishers and subscribers are not informed of failure in + * any way. + *

+ * When {@link #unsubscribe()} or {@link #destroy()} is called, this class ceases operation. + */ +public class PubSubJedisClient extends JedisPubSub implements PubSubLibraryClient +{ + + private static final long RECONNECT_PERIOD_MILLIS = 800; + private static final String BASE_CHANNEL = "pG8n5jp#"; + + private static final String BOLD = "\u001B[1m"; + private static final String RESET = "\u001B[0m"; + + private final String _id; + private JedisPool _writePool; + private final ConnectionData _readConn; + private JedisPool _readPool; + private final ExecutorService _threadPool = Executors.newCachedThreadPool(); + private volatile Subscriber _sub; + + private final Set _channels = Collections + .newSetFromMap(new ConcurrentHashMap()); + private final Map> _pendingFutures = new ConcurrentHashMap>(); + + private volatile boolean _subscribed; // Is there a base subscription yet? + private volatile boolean _destroyed; // has this been deliberately destroyed? + + /** + * Class constructor. + * + * @param writeTo The connection info for the redis instance this client should publish to. + * @param readFrom The connection info for the redis instance this client to subscribe to. + */ + public PubSubJedisClient(ConnectionData writeTo, ConnectionData readFrom) + { + if (writeTo == null) + { + throw new IllegalArgumentException("redis connection info cannot be null"); + } + + _id = writeTo.getName(); + _writePool = Utility.generatePool(writeTo); + + _readConn = readFrom; + _readPool = Utility.generatePool(readFrom); + + createSubscription(BASE_CHANNEL); + } + + @Override + public final synchronized void setSubscriber(Subscriber sub) + { + _sub = sub; + } + + @Override + public final ListenableFuture addChannel(String channel) + { + SettableFuture ret = _pendingFutures.get(channel); + if (ret == null) + { + ret = SettableFuture.create(); + _pendingFutures.put(channel, ret); + } + + try + { + _channels.add(channel); + + if (_subscribed) + { // Already has a subscription thread and can just add a new channel to it. + subscribe(channel); + } + } catch (Exception ex) + { + log("Encountered issue subscribing to a channel."); + ex.printStackTrace(); + ret.setException(ex); + } + + return ret; + } + + @Override + public final void removeChannel(String channel) + { + if (BASE_CHANNEL.equals(channel)) + { // Protects the base subscription + return; + } + + _channels.remove(channel); + + if (_subscribed) + { + unsubscribe(channel); + } + } + + @Override + public final void unsubscribe() + { + destroy(); + } + + @Override + public final void destroy() + { + _destroyed = true; + try + { + super.unsubscribe(); + } catch (NullPointerException e) + { + } + + for (SettableFuture fut : _pendingFutures.values()) + { + fut.set(false); + } + } + + @Override + public final void onMessage(String channel, String message) + { + if (_sub == null || BASE_CHANNEL.equals(channel)) + { + return; + } + + try + { + _sub.onMessage(channel, message); + } catch (Exception e) + { + e.printStackTrace(); + } + } + + @Override + public final ListenableFuture publish(final String channel, final String message) + { + final SettableFuture ret = SettableFuture.create(); + _threadPool.execute(new Runnable() + { + @Override + public void run() + { + Jedis bJedis = null; + try + { + bJedis = _writePool.getResource(); + bJedis.publish(channel, message); + _writePool.returnResource((Jedis) bJedis); + ret.set(true); + + } catch (Exception e) + { + log("Encountered issue while publishing a message."); + e.printStackTrace(); + if (bJedis != null) + { + _writePool.returnBrokenResource((Jedis) bJedis); + } + ret.set(false); + } + } + }); + return ret; + } + + // Confirms successful subscriptions/unsubscriptions. + @Override + public void onSubscribe(String channel, int subscribedChannels) + { + + // informs subscriber that this subscription completed successfully + SettableFuture fut = _pendingFutures.remove(channel); + if (fut != null) + { + fut.set(true); + } + + if (!_subscribed) + { + for (String subscribeTo : _channels) + { + subscribe(subscribeTo); + } + } + _subscribed = true; + + log("Subscribed to channel: " + channel); + } + + @Override + public void onUnsubscribe(String channel, int subscribedChannels) + { + log("Unsubscribed from channel: " + channel); + } + + /** + * Creates the initial listening thread which blocks as it polls redis for new messages. + * Subsequent subscriptions can simply be added using {@link #subscribe(String...)} after the + * subscription thread has been created. + * + * @param firstChannel The first channel to initially subscribe to. If you do not have a first + * channel, there is no reason to create a subscriber thread yet. + */ + private void createSubscription(final String firstChannel) + { + + final JedisPubSub pubsub = this; + + new Thread(new Runnable() + { + @Override + public void run() + { + + boolean first = true; + + while (!_destroyed) + { + + if (!first) + { + log("Jedis connection to " + _readConn.getHost() + ":" + + _readConn.getPort() + + " failed or was interrupted, attempting to reconnect"); + } + first = false; + + Jedis jedisInstance = null; + + try + { + // gets a non-thread-safe jedis instance from the thread-safe pool. + jedisInstance = _readPool.getResource(); + + log("Creating initial jedis subscription to channel " + firstChannel); + // this will block as long as there are subscriptions + jedisInstance.subscribe(pubsub, firstChannel); + + log("jedisInstance.subscribe() returned, subscription over."); + + // when the subscription ends (subscribe() returns), returns the instance to + // the pool + _readPool.returnResource(jedisInstance); + + } catch (JedisConnectionException e) + { + log("Jedis connection encountered an issue."); + e.printStackTrace(); + if (jedisInstance != null) + { + _readPool.returnBrokenResource((Jedis) jedisInstance); + } + + } catch (JedisDataException e) + { + log("Jedis connection encountered an issue."); + e.printStackTrace(); + if (jedisInstance != null) + { + _readPool.returnBrokenResource((Jedis) jedisInstance); + } + } + _subscribed = false; + + // sleeps for a short pause, rather than constantly retrying connection + if (!_destroyed) + { + try + { + Thread.sleep(RECONNECT_PERIOD_MILLIS); + } catch (InterruptedException e) + { + _destroyed = true; + log("Reconnection pause thread was interrupted."); + e.printStackTrace(); + } + } + } + } + }).start(); + } + + // This implementation does not support pattern-matching subscriptions + @Override + public void onPMessage(String pattern, String channel, String message) + { + } + + @Override + public void onPSubscribe(String pattern, int subscribedChannels) + { + } + + @Override + public void onPUnsubscribe(String pattern, int subscribedChannels) + { + } + + private void log(String msg) + { + System.out.println(BOLD + "[" + getClass().getSimpleName() + + (_id != null && !_id.isEmpty() ? " " + _id : "") + "] " + RESET + msg); + } + +} diff --git a/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/redis/messaging/PubSubLibraryClient.java b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/redis/messaging/PubSubLibraryClient.java new file mode 100644 index 00000000..4a9f2820 --- /dev/null +++ b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/redis/messaging/PubSubLibraryClient.java @@ -0,0 +1,61 @@ +package mineplex.serverdata.redis.messaging; + +import com.google.common.util.concurrent.ListenableFuture; + +/** + * A multi-channel subscription and publisher to a pub/sub messaging implementation. An interface to + * the actual low-level pub/sub library, whatever it may be. + * + * For the sake of internal efficiency, this makes no guarantees for the sanity or unchangeability + * of arguments passed into its methods. Clients should not generally interact with this directly. + */ +public interface PubSubLibraryClient +{ + + /** + * Publishes a message to all subscribers of a given channel. + * + * @param channel The channel to publish the message on. + * @param message The message to send. + * @return A future object that will complete after an unknown amount of time with + * false if for some locally known reason the message definitely could not + * be published, else completes with true. + */ + ListenableFuture publish(String channel, String message); + + /** + * Adds a channel to this subscription. + * + * @param channel The channel to add. Should not change after being passed in. + * @return The asynchronous, future result of this attempt to add the channel. Will have + * true when the subscription starts successfully. + */ + ListenableFuture addChannel(String channel); + + /** + * Removes a channel from this subscription. + * + * @param channel The channel to remove. Should not change after being passed in. + */ + void removeChannel(String channel); + + /** + * Removes all channels from this subscription, kills its connections, and relinquishes any + * resources it was occupying. + *

+ * Depending on the implementation, once a subscription has been destroyed, it may not be + * reusable and it may be necessary to construct a new one in order to resume. + *

+ * Call this when the subscription is no longer being used. Holding unnecessary connections can + * cause serious performance and other issues on both ends. + */ + void destroy(); + + /** + * Sets the subscriber to inform of messages received by this subscription. + * + * @param sub The listener for this subscription. + */ + void setSubscriber(Subscriber sub); + +} diff --git a/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/redis/messaging/PubSubMessager.java b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/redis/messaging/PubSubMessager.java new file mode 100644 index 00000000..ea45109e --- /dev/null +++ b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/redis/messaging/PubSubMessager.java @@ -0,0 +1,56 @@ +package mineplex.serverdata.redis.messaging; + +import com.google.common.util.concurrent.ListenableFuture; + +/** + * Messager for standard pub/sub model. Handles multiple publishers and subscribers. + *

+ * All messaging is asynchronous and non-blocking, even to local subscribers. + *

+ * For more about the pub/sub messaging paradigm, see http://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern + */ +public interface PubSubMessager +{ + + /** + * Publishes a message to all subscribers of a given channel. + *

+ * Publishes to all connected subscribers, including local ones. + * + * @param channel The channel to publish the message on. + * @param message The message to send. + * @return A future object that will complete after an unknown amount of time with + * false if for some locally known reason the message definitely could not + * be published, else completes with true (which does not guarantee it + * succeeded 100%). + */ + ListenableFuture publish(String channel, String message); + + /** + * Subscribes to a messaging channel. + *

+ * When incoming messages arrive, the subscriber is called from an arbitrary new thread. + * + * @param channel The channel to subscribe to. + * @param sub The subscriber to inform of incoming messages. + * @return The asynchronous, future result of this attempt to subscribe to the channel. Will + * have true when the subscription starts successfully. + */ + ListenableFuture subscribe(String channel, Subscriber sub); + + /** + * Unsubscribes from a messaging channel. + * + * @param channel The channel to unsubscribe from. + * @param sub The subscriber to stop informing of incoming messages. + */ + void unsubscribe(String channel, Subscriber sub); + + /** + * Attempts to gracefully shut down this messager. Generally irreversible. + */ + void shutdown(); + +} diff --git a/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/redis/messaging/PubSubRouter.java b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/redis/messaging/PubSubRouter.java new file mode 100644 index 00000000..453b9074 --- /dev/null +++ b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/redis/messaging/PubSubRouter.java @@ -0,0 +1,165 @@ +package mineplex.serverdata.redis.messaging; + +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; + +/** + * A pub/sub messager that simply routes messages to some underlying pub/sub implementation, which + * is in turn represented by a multi-channel subscription and a publishing mechanism. + *

+ * This class handles: + *

    + *
  1. Providing a modular messaging interface that is thread-safe. + *
  2. Protecting pub/sub implementations from some bad client behavior/data. + *
  3. Routing messages for multiple subscribers to the same channel(s). + *
+ */ +public class PubSubRouter implements PubSubMessager, Subscriber +{ + + private final PubSubLibraryClient _pubSubClient; + private final Map> _subscribers; + + private final ExecutorService _threadPool; + + public PubSubRouter(PubSubLibraryClient client) + { + if (client == null) + { + throw new IllegalArgumentException("pubsub client cannot be null"); + } + + this._pubSubClient = client; + this._pubSubClient.setSubscriber(this); + this._subscribers = new ConcurrentHashMap>(); + + this._threadPool = Executors.newCachedThreadPool(); + } + + @Override + public final ListenableFuture publish(String channel, String message) + { + if (channel == null || channel.isEmpty()) + { + throw new IllegalArgumentException("channel cannot be null or empty"); + } + + // messages of 0 length are allowed. Null messages are treated as messages of 0 length. + if (message == null) + { + message = ""; + } + + return _pubSubClient.publish(channel, message); + } + + @Override + public final ListenableFuture subscribe(String channel, Subscriber sub) + { + if (channel == null || channel.isEmpty() || sub == null) + { + throw new IllegalArgumentException("params cannot be null and channel cannot be empty"); + } + + ListenableFuture ret = null; + + Set channelSubs = _subscribers.get(channel); + if (channelSubs == null) + { + // uses CopyOnWriteArraySet for fast and consistent iteration (forwarding messages to + // subscribers) but slow writes (adding/removing subscribers). + // See a discussion of the issue here: + // http://stackoverflow.com/questions/6720396/different-types-of-thread-safe-sets-in-java + channelSubs = new CopyOnWriteArraySet(); + _subscribers.put(channel, channelSubs); + + // starts a jedis subscription to the channel if there were no subscribers before + ret = _pubSubClient.addChannel(channel); + + } else + { + ret = SettableFuture.create(); + ((SettableFuture) ret).set(true); // already subscribed, calls back immediately + } + + channelSubs.add(sub); + + return ret; + } + + @Override + public final void unsubscribe(String channel, Subscriber sub) + { + if (channel == null || channel.isEmpty() || sub == null) + { + throw new IllegalArgumentException("params cannot be null and channel cannot be empty"); + } + + Set channelSubs = _subscribers.get(channel); + if (channelSubs == null) + { // no subscribers for the channel to begin with. + return; + } + + channelSubs.remove(sub); + + // stops the subscription to this channel if the unsubscribed was the last subscriber + if (channelSubs.isEmpty()) + { + _subscribers.remove(channel); + _pubSubClient.removeChannel(channel); + } + } + + @Override + public final void onMessage(final String channel, final String message) + { + if (channel == null || message == null || channel.isEmpty()) + { + return; + } + + Set channelSubs = _subscribers.get(channel); + + if (channelSubs == null) + { // We should not still be listening + _pubSubClient.removeChannel(channel); + return; + } else if (channelSubs.isEmpty()) + { + _subscribers.remove(channel); + _pubSubClient.removeChannel(channel); + return; + } + + for (final Subscriber sub : channelSubs) + { + + // Gives subscribers their own thread from the thread pool in which to react to the + // message. + // Avoids interruptions and other problems while iterating over the subscriber set. + _threadPool.execute(new Runnable() + { + @Override + public void run() + { + sub.onMessage(channel, message); + } + }); + } + } + + @Override + public void shutdown() + { + _pubSubClient.destroy(); + } + +} diff --git a/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/redis/messaging/Subscriber.java b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/redis/messaging/Subscriber.java new file mode 100644 index 00000000..5ecc0979 --- /dev/null +++ b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/redis/messaging/Subscriber.java @@ -0,0 +1,19 @@ +package mineplex.serverdata.redis.messaging; + +/** + * A subscriber to a pub/sub channel. + */ +public interface Subscriber +{ + + /** + * Called when a message is sent on a channel that this is subscribed to. + *

+ * No guarantees are made about what thread this will be called from. + * + * @param channel The channel that the message was sent on. + * @param message The message that was received. + */ + void onMessage(String channel, String message); + +} diff --git a/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/serialization/RuntimeTypeAdapterFactory.java b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/serialization/RuntimeTypeAdapterFactory.java new file mode 100644 index 00000000..5a623861 --- /dev/null +++ b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/serialization/RuntimeTypeAdapterFactory.java @@ -0,0 +1,245 @@ +/* + * Copyright (C) 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package mineplex.serverdata.serialization; + +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonPrimitive; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.internal.Streams; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; + + +/** + * Adapts values whose runtime type may differ from their declaration type. This + * is necessary when a field's type is not the same type that GSON should create + * when deserializing that field. For example, consider these types: + *

   {@code
+ *   abstract class Shape {
+ *     int x;
+ *     int y;
+ *   }
+ *   class Circle extends Shape {
+ *     int radius;
+ *   }
+ *   class Rectangle extends Shape {
+ *     int width;
+ *     int height;
+ *   }
+ *   class Diamond extends Shape {
+ *     int width;
+ *     int height;
+ *   }
+ *   class Drawing {
+ *     Shape bottomShape;
+ *     Shape topShape;
+ *   }
+ * }
+ *

Without additional type information, the serialized JSON is ambiguous. Is + * the bottom shape in this drawing a rectangle or a diamond?

   {@code
+ *   {
+ *     "bottomShape": {
+ *       "width": 10,
+ *       "height": 5,
+ *       "x": 0,
+ *       "y": 0
+ *     },
+ *     "topShape": {
+ *       "radius": 2,
+ *       "x": 4,
+ *       "y": 1
+ *     }
+ *   }}
+ * This class addresses this problem by adding type information to the + * serialized JSON and honoring that type information when the JSON is + * deserialized:
   {@code
+ *   {
+ *     "bottomShape": {
+ *       "type": "Diamond",
+ *       "width": 10,
+ *       "height": 5,
+ *       "x": 0,
+ *       "y": 0
+ *     },
+ *     "topShape": {
+ *       "type": "Circle",
+ *       "radius": 2,
+ *       "x": 4,
+ *       "y": 1
+ *     }
+ *   }}
+ * Both the type field name ({@code "type"}) and the type labels ({@code + * "Rectangle"}) are configurable. + * + *

Registering Types

+ * Create a {@code RuntimeTypeAdapter} by passing the base type and type field + * name to the {@link #of} factory method. If you don't supply an explicit type + * field name, {@code "type"} will be used.
   {@code
+ *   RuntimeTypeAdapter shapeAdapter
+ *       = RuntimeTypeAdapter.of(Shape.class, "type");
+ * }
+ * Next register all of your subtypes. Every subtype must be explicitly + * registered. This protects your application from injection attacks. If you + * don't supply an explicit type label, the type's simple name will be used. + *
   {@code
+ *   shapeAdapter.registerSubtype(Rectangle.class, "Rectangle");
+ *   shapeAdapter.registerSubtype(Circle.class, "Circle");
+ *   shapeAdapter.registerSubtype(Diamond.class, "Diamond");
+ * }
+ * Finally, register the type adapter in your application's GSON builder: + *
   {@code
+ *   Gson gson = new GsonBuilder()
+ *       .registerTypeAdapter(Shape.class, shapeAdapter)
+ *       .create();
+ * }
+ * Like {@code GsonBuilder}, this API supports chaining:
   {@code
+ *   RuntimeTypeAdapter shapeAdapter = RuntimeTypeAdapterFactory.of(Shape.class)
+ *       .registerSubtype(Rectangle.class)
+ *       .registerSubtype(Circle.class)
+ *       .registerSubtype(Diamond.class);
+ * }
+ */ +public final class RuntimeTypeAdapterFactory implements TypeAdapterFactory { + private final Class baseType; + private final String typeFieldName; + private final Map> labelToSubtype = new LinkedHashMap>(); + private final Map, String> subtypeToLabel = new LinkedHashMap, String>(); + + private RuntimeTypeAdapterFactory(Class baseType, String typeFieldName) { + if (typeFieldName == null || baseType == null) { + throw new NullPointerException(); + } + this.baseType = baseType; + this.typeFieldName = typeFieldName; + } + + /** + * Creates a new runtime type adapter using for {@code baseType} using {@code + * typeFieldName} as the type field name. Type field names are case sensitive. + */ + public static RuntimeTypeAdapterFactory of(Class baseType, String typeFieldName) { + return new RuntimeTypeAdapterFactory(baseType, typeFieldName); + } + + /** + * Creates a new runtime type adapter for {@code baseType} using {@code "type"} as + * the type field name. + */ + public static RuntimeTypeAdapterFactory of(Class baseType) { + return new RuntimeTypeAdapterFactory(baseType, "type"); + } + + /** + * Registers {@code type} identified by {@code label}. Labels are case + * sensitive. + * + * @throws IllegalArgumentException if either {@code type} or {@code label} + * have already been registered on this type adapter. + */ + public RuntimeTypeAdapterFactory registerSubtype(Class type, String label) { + if (type == null || label == null) { + throw new NullPointerException(); + } + if (subtypeToLabel.containsKey(type) || labelToSubtype.containsKey(label)) { + throw new IllegalArgumentException("types and labels must be unique"); + } + labelToSubtype.put(label, type); + subtypeToLabel.put(type, label); + return this; + } + + /** + * Registers {@code type} identified by its {@link Class#getSimpleName simple + * name}. Labels are case sensitive. + * + * @throws IllegalArgumentException if either {@code type} or its simple name + * have already been registered on this type adapter. + */ + public RuntimeTypeAdapterFactory registerSubtype(Class type) { + return registerSubtype(type, type.getSimpleName()); + } + + public TypeAdapter create(Gson gson, TypeToken type) { + if (type.getRawType() != baseType) { + return null; + } + + final Map> labelToDelegate + = new LinkedHashMap>(); + final Map, TypeAdapter> subtypeToDelegate + = new LinkedHashMap, TypeAdapter>(); + for (Map.Entry> entry : labelToSubtype.entrySet()) { + TypeAdapter delegate = gson.getDelegateAdapter(this, TypeToken.get(entry.getValue())); + labelToDelegate.put(entry.getKey(), delegate); + subtypeToDelegate.put(entry.getValue(), delegate); + } + + return new TypeAdapter() { + @Override public R read(JsonReader in) throws IOException { + JsonElement jsonElement = Streams.parse(in); + JsonElement labelJsonElement = jsonElement.getAsJsonObject().remove(typeFieldName); + if (labelJsonElement == null) { + throw new JsonParseException("cannot deserialize " + baseType + + " because it does not define a field named " + typeFieldName); + } + String label = labelJsonElement.getAsString(); + @SuppressWarnings("unchecked") // registration requires that subtype extends T + TypeAdapter delegate = (TypeAdapter) labelToDelegate.get(label); + if (delegate == null) { + throw new JsonParseException("cannot deserialize " + baseType + " subtype named " + + label + "; did you forget to register a subtype?"); + } + return delegate.fromJsonTree(jsonElement); + } + + @Override public void write(JsonWriter out, R value) throws IOException { + if(value!=null) { + Class srcType = value.getClass(); + String label = subtypeToLabel.get(srcType); + @SuppressWarnings("unchecked") // registration requires that subtype extends T + TypeAdapter delegate = (TypeAdapter) subtypeToDelegate.get(srcType); + if (delegate == null) { + throw new JsonParseException("cannot serialize " + srcType.getName() + + "; did you forget to register a subtype?"); + } + JsonObject jsonObject = delegate.toJsonTree(value).getAsJsonObject(); + if (jsonObject.has(typeFieldName)) { + throw new JsonParseException("cannot serialize " + srcType.getName() + + " because it already defines a field named " + typeFieldName); + } + JsonObject clone = new JsonObject(); + clone.add(typeFieldName, new JsonPrimitive(label)); + for (Map.Entry e : jsonObject.entrySet()) { + clone.add(e.getKey(), e.getValue()); + } + Streams.write(clone, out); + }else{ + out.nullValue(); + } + } + }; + } +} \ No newline at end of file diff --git a/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/servers/ConnectionData.java b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/servers/ConnectionData.java new file mode 100644 index 00000000..e7c8676d --- /dev/null +++ b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/servers/ConnectionData.java @@ -0,0 +1,54 @@ +package mineplex.serverdata.servers; + +/** + * ConnectionData stores information relevant for initiating a connection to a repository. + * @author MrTwiggy + * + */ +public class ConnectionData +{ + + public enum ConnectionType + { + MASTER, + SLAVE; + } + + private ConnectionType _type; // The type of connection available + public ConnectionType getType() { return _type; } + + private String _name; // The name associated with this connection + public String getName() { return _name; } + + private String _host; // The host URL to connect to repository + public String getHost() { return _host; } + + private int _port; // The port to connect to repository + public int getPort() { return _port; } + + /** + * Constructor + * @param host - the host URL defining the repository + * @param port - the port used for connection to repository + * @param type - the type of connection referenced by this ConnectionData + * @param name - the name associated with ConnectionData + */ + public ConnectionData(String host, int port, ConnectionType type, String name) + { + _host = host; + _port = port; + _type = type; + _name = name; + } + + /** + * @param name + * @return true, if {@code name} is null or it matches (case-insensitive) the {@code _name} associated + * with this ConnectionData, false otherwise. + */ + public boolean nameMatches(String name) + { + return (name == null || name.equalsIgnoreCase(_name)); + } +} + diff --git a/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/servers/DedicatedServerSorter.java b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/servers/DedicatedServerSorter.java new file mode 100644 index 00000000..8f5b6853 --- /dev/null +++ b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/servers/DedicatedServerSorter.java @@ -0,0 +1,20 @@ +package mineplex.serverdata.servers; + +import java.util.Comparator; + +import mineplex.serverdata.data.DedicatedServer; + +public class DedicatedServerSorter implements Comparator +{ + @Override + public int compare(DedicatedServer first, DedicatedServer second) + { + if (second.getAvailableRam() <= 1024) return -1; + else if (first.getAvailableRam() <= 1024) return 1; + else if (first.getAvailableRam() > second.getAvailableRam()) return -1; + else if (second.getAvailableRam() > first.getAvailableRam()) return 1; + else if (first.getAvailableCpu() > second.getAvailableCpu()) return -1; + else if (second.getAvailableCpu() > first.getAvailableCpu()) return 1; + else return 0; + } +} \ No newline at end of file diff --git a/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/servers/ServerManager.java b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/servers/ServerManager.java new file mode 100644 index 00000000..20324fe1 --- /dev/null +++ b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/servers/ServerManager.java @@ -0,0 +1,170 @@ +package mineplex.serverdata.servers; + +import java.io.File; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; + +import mineplex.serverdata.Region; +import mineplex.serverdata.redis.RedisConfig; +import mineplex.serverdata.redis.RedisServerRepository; +import mineplex.serverdata.servers.ConnectionData.ConnectionType; + +/** + * ServerManager handles the creation/management of {@link ServerRepository}s for use. + * @author Ty + * + */ +public class ServerManager +{ + public static final String SERVER_STATUS_LABEL = "ServerStatus"; // Label differentiating ServerStatus related servers + private static final String DEFAULT_CONFIG = "redis-config.dat"; + + // Configuration determining connection information + private static RedisConfig _config; + + // The cached repository instances + private static Map repositories = new HashMap(); + + /** + * @param host - the host url used to connect to the database + * @param port - the port to connect to the repository + * @param region - the geographical region of the {@link ServerRepository}. + * @return a newly instanced (or cached) {@link ServerRepository} for the specified {@code region}. + */ + private static ServerRepository getServerRepository(ConnectionData writeConn, ConnectionData readConn, Region region) + { + if (repositories.containsKey(region)) return repositories.get(region); + + ServerRepository repository = new RedisServerRepository(writeConn, readConn, region); + repositories.put(region, repository); + return repository; + } + + /** + * {@code host} defaults to {@value DEFAULT_REDIS_HOST} and + * {@code port} defaults to {@value DEFAULT_REDIS_PORT}. + * + * @see #getServerRepository(String, int, Region) + */ + public static ServerRepository getServerRepository(Region region) + { + return getServerRepository(getConnection(true, SERVER_STATUS_LABEL), getConnection(false, SERVER_STATUS_LABEL), region); + } + + /** + * @return the {@link ConnectionData} associated with the master instance connection. + */ + public static ConnectionData getMasterConnection() + { + return getConnection(true); + } + + /** + * Non-Deterministic: Generates random slave instance connection. + * @return the {@link ConnectionData} associated with a random slave connection. + */ + public static ConnectionData getSlaveConnection() + { + return getConnection(false); + } + + public static ConnectionData getConnection(boolean writeable, String name) + { + return getDefaultConfig().getConnection(writeable, name); + } + + /** + * @param writeable - whether the connection referenced in return can receive write-requests + * @return a newly generated {@code ConnectionData} pointing to a valid connection. + */ + public static ConnectionData getConnection(boolean writeable) + { + return getConnection(writeable, "DefaultConnection"); + } + + /** + * @return the default {@link RedisConfig} associated with this manager, providing appropriate connections. + */ + public static RedisConfig getDefaultConfig() + { + return getConfig(DEFAULT_CONFIG); + } + + /** + * @return the {@link RedisConfig} associated with this manager, providing appropriate connections. + */ + public static RedisConfig getConfig(String fileName) + { + if (_config == null) + _config = loadConfig(fileName); + + return _config; + } + + public static RedisConfig loadConfig(String fileName) + { + try + { + File configFile = new File(fileName); + + if (configFile.exists()) + { + List connections = new ArrayList(); + List lines = Files.readAllLines(configFile.toPath(), Charset.defaultCharset()); + + for (String line : lines) + { + ConnectionData connection = deserializeConnection(line); + connections.add(connection); + + } + + return new RedisConfig(connections); + } + else + { + log(fileName + " not found at " + configFile.toPath().toString()); + return new RedisConfig(); + } + } + catch (Exception exception) + { + exception.printStackTrace(); + log("---Unable To Parse Redis Configuration File---"); + } + + return null; + } + + /** + * @param line - the serialized line representing a valid {@link ConnectionData} object. + * @return a deserialized {@link ConnectionData} referenced by the {@code line} passed in. + */ + private static ConnectionData deserializeConnection(String line) + { + String[] args = line.split(" "); + + if (args.length >= 2) + { + String ip = args[0]; + int port = Integer.parseInt(args[1]); + String typeName = (args.length >= 3) ? args[2].toUpperCase() : "MASTER"; // Defaults to MASTER if omitted. + ConnectionType type = ConnectionType.valueOf(typeName); + String name = (args.length >= 4) ? args[3] : "DefaultConnection"; // Defaults to DefaultConnection if omitted. + + return new ConnectionData(ip, port, type, name); + } + + return null; + } + + private static void log(String message) + { + System.out.println(String.format("[ServerManager] %s", message)); + } +} diff --git a/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/servers/ServerRepository.java b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/servers/ServerRepository.java new file mode 100644 index 00000000..51f5ad8b --- /dev/null +++ b/Mineplexer/Mineplex.ServerData/src/mineplex/serverdata/servers/ServerRepository.java @@ -0,0 +1,81 @@ +package mineplex.serverdata.servers; + +import java.util.Collection; +import java.util.List; + +import mineplex.serverdata.data.DedicatedServer; +import mineplex.serverdata.data.MinecraftServer; +import mineplex.serverdata.data.ServerGroup; + +/** + * The ServerRepository is used for storing/retrieving active sessions + * for {@link MinecraftServer}s, {@link DedicatedServer}s, and {@link ServerGroup}s + * from a persistent database/repoistory. + * @author Ty + * + */ +public interface ServerRepository +{ + + + /** + * @return a newly instanced snapshot {@link Collection} of all currently active + * {@link MinecraftServer}s in the repository. + */ + public Collection getServerStatuses(); + + public Collection getServerStatusesByPrefix(String prefix); + + public Collection getServersByGroup(String serverGroup); + + /** + * @param serverName - the name of the {@link MinecraftServer} to be fetched. + * @return the currently active {@link MinecraftServer} with a matching {@code serverName}, + * if an active one exists, null otherwise. + */ + public MinecraftServer getServerStatus(String serverName); + + /** + * Update (or add, if it doesn't already exist) a {@link MinecraftServer}s data + * in the repository. + * + * A {@link MinecraftServer} must be updated within {@code timeout} milliseconds before + * it expires and is removed from the repository. + * @param serverData - the {@link MinecraftServer} to add/update in the repository. + * @param timeout - the timeout (in milliseconds) before the {@link MinecraftServer} session expires. + */ + public void updataServerStatus(MinecraftServer serverData, int timeout); + + /** + * Remove an active {@link MinecraftServer} from the repository. + * @param serverData - the {@link MinecraftServer} to be removed. + */ + public void removeServerStatus(MinecraftServer serverData); + + /** + * @param serverName - the name of the server whose existence is being checked. + * @return true, if there exists an active {@link MinecraftServer} session with a + * matching {@code serverName}, false otherwise. + */ + public boolean serverExists(String serverName); + + /** + * @return a newly instanced snapshot {@link Collection} of all the + * currently active {@link DedicatedServer}s in the repository. + */ + public Collection getDedicatedServers(); + + /** + * @return a newly instanced snapshot {@link Collection} of all the + * currently active {@link ServerGroup}s in the repository. + */ + public Collection getServerGroups(Collection servers); + + public ServerGroup getServerGroup(String serverGroup); + + public Collection getDeadServers(); + + void updateServerGroup(ServerGroup serverGroup); + + public void removeServerGroup(ServerGroup serverGroup); +} diff --git a/sqldumps/player_stats.sql b/sqldumps/player_stats.sql new file mode 100644 index 00000000..34a232b3 --- /dev/null +++ b/sqldumps/player_stats.sql @@ -0,0 +1,139 @@ +-- MariaDB dump 10.17 Distrib 10.4.14-MariaDB, for Win64 (AMD64) +-- +-- Host: localhost Database: player_stats +-- ------------------------------------------------------ +-- Server version 10.4.14-MariaDB + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!40101 SET NAMES utf8mb4 */; +/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; +/*!40103 SET TIME_ZONE='+00:00' */; +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; + +-- +-- Table structure for table `ipinfo` +-- + +DROP TABLE IF EXISTS `ipinfo`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `ipinfo` ( + `id` int(11) NOT NULL, + `ipAddress` text COLLATE latin1_bin NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_bin; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `ipinfo` +-- + +LOCK TABLES `ipinfo` WRITE; +/*!40000 ALTER TABLE `ipinfo` DISABLE KEYS */; +/*!40000 ALTER TABLE `ipinfo` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `playerinfo` +-- + +DROP TABLE IF EXISTS `playerinfo`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `playerinfo` ( + `uuid` varchar(100) NOT NULL, + `name` varchar(40) NOT NULL, + `version` text NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `playerinfo` +-- + +LOCK TABLES `playerinfo` WRITE; +/*!40000 ALTER TABLE `playerinfo` DISABLE KEYS */; +/*!40000 ALTER TABLE `playerinfo` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `playerips` +-- + +DROP TABLE IF EXISTS `playerips`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `playerips` ( + `playerInfoId` int(11) NOT NULL, + `ipInfoId` int(11) NOT NULL, + `date` text NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `playerips` +-- + +LOCK TABLES `playerips` WRITE; +/*!40000 ALTER TABLE `playerips` DISABLE KEYS */; +/*!40000 ALTER TABLE `playerips` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `playerloginsessions` +-- + +DROP TABLE IF EXISTS `playerloginsessions`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `playerloginsessions` ( + `playerInfoId` int(11) NOT NULL, + `loginTime` text NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `playerloginsessions` +-- + +LOCK TABLES `playerloginsessions` WRITE; +/*!40000 ALTER TABLE `playerloginsessions` DISABLE KEYS */; +/*!40000 ALTER TABLE `playerloginsessions` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `playeruniquelogins` +-- + +DROP TABLE IF EXISTS `playeruniquelogins`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `playeruniquelogins` ( + `playerInfoId` int(11) NOT NULL, + `day` text NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `playeruniquelogins` +-- + +LOCK TABLES `playeruniquelogins` WRITE; +/*!40000 ALTER TABLE `playeruniquelogins` DISABLE KEYS */; +/*!40000 ALTER TABLE `playeruniquelogins` ENABLE KEYS */; +UNLOCK TABLES; +/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; + +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; +/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; + +-- Dump completed on 2021-05-16 10:39:44 \ No newline at end of file