From 7dad243735f7ab98b3cb03b4e633922d68ceed90 Mon Sep 17 00:00:00 2001
From: egg82 <eggys82@gmail.com>
Date: Fri, 16 Aug 2019 18:04:19 -0600
Subject: [PATCH 1/2] Added PlayerProfile support

---
 src/main/java/io/papermc/lib/PaperLib.java    |  31 +++
 .../papermc/lib/environments/Environment.java |  20 ++
 .../playerprofile/BukkitPlayerInfo.java       | 185 ++++++++++++++++++
 .../playerprofile/PaperPlayerInfo.java        |  75 +++++++
 .../features/playerprofile/PlayerInfo.java    |  20 ++
 5 files changed, 331 insertions(+)
 create mode 100644 src/main/java/io/papermc/lib/features/playerprofile/BukkitPlayerInfo.java
 create mode 100644 src/main/java/io/papermc/lib/features/playerprofile/PaperPlayerInfo.java
 create mode 100644 src/main/java/io/papermc/lib/features/playerprofile/PlayerInfo.java

diff --git a/src/main/java/io/papermc/lib/PaperLib.java b/src/main/java/io/papermc/lib/PaperLib.java
index 53e4265..ecc0357 100644
--- a/src/main/java/io/papermc/lib/PaperLib.java
+++ b/src/main/java/io/papermc/lib/PaperLib.java
@@ -5,6 +5,9 @@
 import io.papermc.lib.environments.PaperEnvironment;
 import io.papermc.lib.environments.SpigotEnvironment;
 import io.papermc.lib.features.blockstatesnapshot.BlockStateSnapshotResult;
+import io.papermc.lib.features.playerprofile.PlayerInfo;
+import java.io.IOException;
+import java.util.UUID;
 import org.bukkit.Chunk;
 import org.bukkit.Location;
 import org.bukkit.World;
@@ -163,6 +166,34 @@ public static BlockStateSnapshotResult getBlockState(@Nonnull Block block, boole
         return ENVIRONMENT.getBlockState(block, useSnapshot);
     }
 
+    /**
+     * Gets information about a potentially offline player, such as their current name or UUID.
+     * Note: Calling this method may contact Mojang's API and will block the current thread with the web request if it does.
+     * Additionally, it's possible that the name or UUID returned is null and/or an IOException is thrown.
+     *
+     * @param playerName The real name of the player to get
+     * @return A PlayerInfo object containing information about a player
+     * @throws IOException Thrown when an IOException is encountered while querying the Mojang API.
+     */
+    @Nonnull
+    public static PlayerInfo getPlayerInfo(@Nonnull String playerName) throws IOException {
+        return ENVIRONMENT.getPlayerInfo(playerName);
+    }
+
+    /**
+     * Gets information about a potentially offline player, such as their current name or UUID.
+     * Note: Calling this method may contact Mojang's API and will block the current thread with the web request if it does.
+     * Additionally, it's possible that the name or UUID returned is null and/or an IOException is thrown.
+     *
+     * @param playerUUID The UUID of the player to get
+     * @return A PlayerInfo object containing information about a player
+     * @throws IOException Thrown when an IOException is encountered while querying the Mojang API.
+     */
+    @Nonnull
+    public static PlayerInfo getPlayerInfo(@Nonnull UUID playerUUID) throws IOException {
+        return ENVIRONMENT.getPlayerInfo(playerUUID);
+    }
+
     /**
      * Detects if the current MC version is at least the following version.
      *
diff --git a/src/main/java/io/papermc/lib/environments/Environment.java b/src/main/java/io/papermc/lib/environments/Environment.java
index b498571..c10d361 100644
--- a/src/main/java/io/papermc/lib/environments/Environment.java
+++ b/src/main/java/io/papermc/lib/environments/Environment.java
@@ -11,6 +11,11 @@
 import io.papermc.lib.features.chunkisgenerated.ChunkIsGenerated;
 import io.papermc.lib.features.chunkisgenerated.ChunkIsGeneratedApiExists;
 import io.papermc.lib.features.chunkisgenerated.ChunkIsGeneratedUnknown;
+import io.papermc.lib.features.playerprofile.BukkitPlayerInfo;
+import io.papermc.lib.features.playerprofile.PaperPlayerInfo;
+import io.papermc.lib.features.playerprofile.PlayerInfo;
+import java.io.IOException;
+import java.util.UUID;
 import org.bukkit.Bukkit;
 import org.bukkit.Chunk;
 import org.bukkit.Location;
@@ -34,6 +39,7 @@ public abstract class Environment {
     protected AsyncTeleport asyncTeleportHandler = new AsyncTeleportSync();
     protected ChunkIsGenerated isGeneratedHandler = new ChunkIsGeneratedUnknown();
     protected BlockStateSnapshot blockStateSnapshotHandler;
+    protected boolean hasPlayerProfile = true;
 
     public Environment() {
         Pattern versionPattern = Pattern.compile("\\(MC: (\\d)\\.(\\d+)\\.?(\\d+?)?\\)");
@@ -67,6 +73,12 @@ public Environment() {
         } else {
             blockStateSnapshotHandler = new BlockStateSnapshotNoOption();
         }
+
+        try {
+            Class.forName("com.destroystokyo.paper.profile.PlayerProfile");
+        } catch (ClassNotFoundException ignored) {
+            hasPlayerProfile = false;
+        }
     }
 
     public abstract String getName();
@@ -87,6 +99,14 @@ public BlockStateSnapshotResult getBlockState(Block block, boolean useSnapshot)
         return blockStateSnapshotHandler.getBlockState(block, useSnapshot);
     }
 
+    public PlayerInfo getPlayerInfo(String playerName) throws IOException {
+        return (hasPlayerProfile) ? new PaperPlayerInfo(playerName) : new BukkitPlayerInfo(playerName);
+    }
+
+    public PlayerInfo getPlayerInfo(UUID playerUUID) throws IOException {
+        return (hasPlayerProfile) ? new PaperPlayerInfo(playerUUID) : new BukkitPlayerInfo(playerUUID);
+    }
+
     public boolean isVersion(int minor) {
         return isVersion(minor, 0);
     }
diff --git a/src/main/java/io/papermc/lib/features/playerprofile/BukkitPlayerInfo.java b/src/main/java/io/papermc/lib/features/playerprofile/BukkitPlayerInfo.java
new file mode 100644
index 0000000..2028aa7
--- /dev/null
+++ b/src/main/java/io/papermc/lib/features/playerprofile/BukkitPlayerInfo.java
@@ -0,0 +1,185 @@
+package io.papermc.lib.features.playerprofile;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import org.bukkit.Bukkit;
+import org.bukkit.entity.Player;
+import org.json.simple.JSONArray;
+import org.json.simple.JSONObject;
+import org.json.simple.parser.JSONParser;
+import org.json.simple.parser.ParseException;
+
+public class BukkitPlayerInfo implements PlayerInfo {
+    private UUID uuid;
+    private String name;
+
+    private static ConcurrentMap<UUID, String> uuidCache = new ConcurrentHashMap<>();
+    private static ConcurrentMap<String, UUID> nameCache = new ConcurrentHashMap<>();
+
+    private static final Object uuidCacheLock = new Object();
+    private static final Object nameCacheLock = new Object();
+
+    private static final JSONParser uuidParser = new JSONParser(); // Thread-safe access via synchronized lock
+    private static final JSONParser nameParser = new JSONParser(); // Thread-safe access via synchronized lock
+
+    public BukkitPlayerInfo(UUID uuid) throws IOException {
+        this.uuid = uuid;
+
+        Optional<String> name = Optional.ofNullable(uuidCache.getOrDefault(uuid, null));
+        if (!name.isPresent()) {
+            synchronized (uuidCacheLock) { // Synchronize for thread-safe JSONParser access + defeating potential race conditions causing multiple lookups
+                name = Optional.ofNullable(uuidCache.getOrDefault(uuid, null));
+                if (!name.isPresent()) {
+                    name = Optional.ofNullable(nameExpensive(uuid));
+                    name.ifPresent(v -> uuidCache.put(uuid, v));
+                }
+            }
+        }
+
+        this.name = name.orElse(null);
+    }
+
+    public BukkitPlayerInfo(String name) throws IOException {
+        this.name = name;
+
+        Optional<UUID> uuid = Optional.ofNullable(nameCache.getOrDefault(name, null));
+        if (!uuid.isPresent()) {
+            synchronized (nameCacheLock) { // Synchronize for thread-safe JSONParser access + defeating potential race conditions causing multiple lookups
+                uuid = Optional.ofNullable(nameCache.getOrDefault(name, null));
+                if (!uuid.isPresent()) {
+                    uuid = Optional.ofNullable(uuidExpensive(name));
+                    uuid.ifPresent(v -> nameCache.put(name, v));
+                }
+            }
+        }
+
+        this.uuid = uuid.orElse(null);
+    }
+
+    public UUID getUUID() { return uuid; }
+
+    public String getName() { return name; }
+
+    private static String nameExpensive(UUID uuid) throws IOException {
+        // Currently-online lookup
+        Player player = Bukkit.getPlayer(uuid);
+        if (player != null) {
+            return player.getName();
+        }
+
+        // Network lookup
+        HttpURLConnection conn = getConnection(new URL("https://api.mojang.com/user/profiles/" + uuid.toString().replace("-", "") + "/names"));
+
+        int code = conn.getResponseCode();
+        try (
+                InputStream in = (code == 200) ? conn.getInputStream() : conn.getErrorStream(); // Ensure we always get some text
+                InputStreamReader reader = new InputStreamReader(in);
+                BufferedReader buffer = new BufferedReader(reader)
+        ) {
+            StringBuilder builder = new StringBuilder();
+            String line;
+            while ((line = buffer.readLine()) != null) {
+                builder.append(line);
+            }
+
+            if (code == 200) {
+                JSONArray json = (JSONArray) nameParser.parse(builder.toString());
+                JSONObject last = (JSONObject) json.get(json.size() - 1);
+                String name = (String) last.get("name");
+
+                nameCache.put(name, uuid);
+            } else if (code == 204) {
+                // No data exists
+                return null;
+            }
+        } catch (ParseException ex) {
+            throw new IOException(ex.getMessage(), ex);
+        }
+
+        throw new IOException("Could not load player data from Mojang (rate-limited?)");
+    }
+
+    private static UUID uuidExpensive(String name) throws IOException {
+        // Currently-online lookup
+        Player player = Bukkit.getPlayer(name);
+        if (player != null) {
+            return player.getUniqueId();
+        }
+
+        // Network lookup
+        HttpURLConnection conn = getConnection(new URL("https://api.mojang.com/users/profiles/minecraft/" + name));
+
+        int code = conn.getResponseCode();
+        try (
+                InputStream in = (code == 200) ? conn.getInputStream() : conn.getErrorStream(); // Ensure we always get some text
+                InputStreamReader reader = new InputStreamReader(in);
+                BufferedReader buffer = new BufferedReader(reader)
+        ) {
+            StringBuilder builder = new StringBuilder();
+            String line;
+            while ((line = buffer.readLine()) != null) {
+                builder.append(line);
+            }
+
+            if (code == 200) {
+                JSONObject json = (JSONObject) uuidParser.parse(builder.toString());
+                UUID uuid = UUID.fromString(((String) json.get("id")).replaceFirst("(\\p{XDigit}{8})(\\p{XDigit}{4})(\\p{XDigit}{4})(\\p{XDigit}{4})(\\p{XDigit}+)", "$1-$2-$3-$4-$5")); // Normalize UUID for fromString (expects dashes, Mojang returns non-dashed result)
+                name = (String) json.get("name");
+
+                uuidCache.put(uuid, name);
+            } else if (code == 204) {
+                // No data exists
+                return null;
+            }
+        } catch (ParseException ex) {
+            throw new IOException(ex.getMessage(), ex);
+        }
+
+        throw new IOException("Could not load player data from Mojang (rate-limited?)");
+    }
+
+    private static HttpURLConnection getConnection(URL url) throws IOException {
+        HttpURLConnection conn = getBaseConnection(url);
+        conn.setInstanceFollowRedirects(true);
+
+        int status;
+        boolean redirect;
+
+        do {
+            status = conn.getResponseCode();
+            redirect = status == HttpURLConnection.HTTP_MOVED_TEMP || status == HttpURLConnection.HTTP_MOVED_PERM || status == HttpURLConnection.HTTP_SEE_OTHER; // Follow redirects
+
+            if (redirect) {
+                // Set cookies on redirect and follow redirect URL
+                String newUrl = conn.getHeaderField("Location");
+                String cookies = conn.getHeaderField("Set-Cookie");
+
+                conn = getBaseConnection(new URL(newUrl));
+                conn.setRequestProperty("Cookie", cookies);
+                conn.addRequestProperty("Accept-Language", "en-US,en;q=0.8");
+            }
+        } while (redirect);
+
+        return conn;
+    }
+
+    private static HttpURLConnection getBaseConnection(URL url) throws IOException {
+        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+
+        // Set standard headers
+        conn.setRequestProperty("Accept", "application/json");
+        conn.setRequestProperty("Connection", "close");
+        conn.setRequestProperty("User-Agent", "PaperMC/PaperLib");
+        conn.setRequestMethod("GET");
+
+        return conn;
+    }
+}
diff --git a/src/main/java/io/papermc/lib/features/playerprofile/PaperPlayerInfo.java b/src/main/java/io/papermc/lib/features/playerprofile/PaperPlayerInfo.java
new file mode 100644
index 0000000..f684c98
--- /dev/null
+++ b/src/main/java/io/papermc/lib/features/playerprofile/PaperPlayerInfo.java
@@ -0,0 +1,75 @@
+package io.papermc.lib.features.playerprofile;
+
+import com.destroystokyo.paper.profile.PlayerProfile;
+import java.io.IOException;
+import java.util.Optional;
+import java.util.UUID;
+import org.bukkit.Bukkit;
+import org.bukkit.entity.Player;
+
+public class PaperPlayerInfo implements PlayerInfo {
+    private UUID uuid;
+    private String name;
+
+    public PaperPlayerInfo(UUID uuid) throws IOException {
+        this.uuid = uuid;
+
+        Optional<String> name = Optional.ofNullable(nameExpensive(uuid));
+        this.name = name.orElse(null);
+    }
+
+    public PaperPlayerInfo(String name) throws IOException {
+        this.name = name;
+
+        Optional<UUID> uuid = Optional.ofNullable(uuidExpensive(name));
+        this.uuid = uuid.orElse(null);
+    }
+
+    public UUID getUUID() { return uuid; }
+
+    public String getName() { return name; }
+
+    private static String nameExpensive(UUID uuid) throws IOException {
+        // Currently-online lookup
+        Player player = Bukkit.getPlayer(uuid);
+        if (player != null) {
+            return player.getName();
+        }
+
+        // Cached profile lookup
+        PlayerProfile profile = Bukkit.createProfile(uuid);
+        if ((profile.isComplete() || profile.completeFromCache()) && profile.getName() != null && profile.getId() != null) {
+            return profile.getName();
+        }
+
+        // Network lookup
+        if (profile.complete(false) && profile.getName() != null && profile.getId() != null) {
+            return profile.getName();
+        }
+
+        // Sorry, nada
+        throw new IOException("Could not load player data from Mojang (rate-limited?)");
+    }
+
+    private static UUID uuidExpensive(String name) throws IOException {
+        // Currently-online lookup
+        Player player = Bukkit.getPlayer(name);
+        if (player != null) {
+            return player.getUniqueId();
+        }
+
+        // Cached profile lookup
+        PlayerProfile profile = Bukkit.createProfile(name);
+        if ((profile.isComplete() || profile.completeFromCache()) && profile.getName() != null && profile.getId() != null) {
+            return profile.getId();
+        }
+
+        // Network lookup
+        if (profile.complete(false) && profile.getName() != null && profile.getId() != null) {
+            return profile.getId();
+        }
+
+        // Sorry, nada
+        throw new IOException("Could not load player data from Mojang (rate-limited?)");
+    }
+}
diff --git a/src/main/java/io/papermc/lib/features/playerprofile/PlayerInfo.java b/src/main/java/io/papermc/lib/features/playerprofile/PlayerInfo.java
new file mode 100644
index 0000000..4a02a4a
--- /dev/null
+++ b/src/main/java/io/papermc/lib/features/playerprofile/PlayerInfo.java
@@ -0,0 +1,20 @@
+package io.papermc.lib.features.playerprofile;
+
+import java.util.UUID;
+import javax.annotation.Nullable;
+
+public interface PlayerInfo {
+    /**
+     * The name of the player.
+     *
+     * @return The name of the payer.
+     */
+    @Nullable String getName();
+
+    /**
+     * The UUID of the player.
+     *
+     * @return The UUID of the player.
+     */
+    @Nullable UUID getUUID();
+}

From fbe40a01be93c2f6977ddf7607578ada2be72760 Mon Sep 17 00:00:00 2001
From: egg82 <eggys82@gmail.com>
Date: Fri, 16 Aug 2019 18:37:55 -0600
Subject: [PATCH 2/2] Fixed name/UUID caching

---
 .../io/papermc/lib/features/playerprofile/BukkitPlayerInfo.java | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/main/java/io/papermc/lib/features/playerprofile/BukkitPlayerInfo.java b/src/main/java/io/papermc/lib/features/playerprofile/BukkitPlayerInfo.java
index 2028aa7..204fcb6 100644
--- a/src/main/java/io/papermc/lib/features/playerprofile/BukkitPlayerInfo.java
+++ b/src/main/java/io/papermc/lib/features/playerprofile/BukkitPlayerInfo.java
@@ -72,6 +72,7 @@ private static String nameExpensive(UUID uuid) throws IOException {
         // Currently-online lookup
         Player player = Bukkit.getPlayer(uuid);
         if (player != null) {
+            nameCache.put(player.getName(), uuid);
             return player.getName();
         }
 
@@ -111,6 +112,7 @@ private static UUID uuidExpensive(String name) throws IOException {
         // Currently-online lookup
         Player player = Bukkit.getPlayer(name);
         if (player != null) {
+            uuidCache.put(player.getUniqueId(), name);
             return player.getUniqueId();
         }