at.pcgamingfreaks.UUIDConverter.java Source code

Java tutorial

Introduction

Here is the source code for at.pcgamingfreaks.UUIDConverter.java

Source

/*
 *   Copyright (C) 2014-2016 GeorgH93
 *
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

package at.pcgamingfreaks;

import com.google.common.base.Charsets;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.stream.JsonReader;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.*;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;

/**
 * Functions to get UUIDs to player names. This library doesn't cache the results! You will have to do this on your own!
 * Works with Bukkit and BungeeCord!
 */
public final class UUIDConverter {
    private static final String UUID_FORMAT_REGEX = "(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})";
    private static final String UUID_FORMAT_REPLACE_TO = "$1-$2-$3-$4-$5";
    private static final long MOJANG_QUERY_RETRY_TIME;

    private static final Gson GSON = new Gson();
    private static final UUIDCacheMap UUID_CACHE; // Cache object for resolved UUIDs

    static {
        MOJANG_QUERY_RETRY_TIME = 600000L;
        UUID_CACHE = new UUIDCacheMap();
        System.out.println("Loading local uuid cache.");
        int loaded = 0;
        //noinspection SpellCheckingInspection
        File uuidCache = new File("usercache.json");
        if (uuidCache.exists()) {
            try (JsonReader reader = new JsonReader(new FileReader(uuidCache))) {
                CacheData[] dat = new Gson().fromJson(reader, CacheData[].class);
                Date now = new Date();
                for (CacheData d : dat) {
                    if (now.before(d.getExpiresDate())) {
                        loaded++;
                        UUID_CACHE.put(d.name, d.uuid);
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        System.out.println("Loaded " + loaded + " UUIDs from local cache.");
    }

    /**
     * Gets the current name of the player from the Mojang servers.
     * Only works for Mojang-UUIDs, not for Bukkit-Offline-UUIDs.
     *
     * @param uuid The UUID of the player.
     * @return The name of the player.
     */
    public static String getNameFromUUID(@NotNull UUID uuid) {
        return getNameFromUUID(uuid.toString());
    }

    /**
     * Gets the current name of the player from the Mojang servers.
     * Only works for Mojang-UUIDs, not for Bukkit-Offline-UUIDs.
     *
     * @param uuid The UUID of the player.
     * @return The name of the player.
     */
    public static String getNameFromUUID(@NotNull String uuid) {
        NameChange[] names = getNamesFromUUID(uuid);
        return names[names.length - 1].name;
    }

    /**
     * Gets the name history of a player from the Mojang servers.
     * Only works for Mojang-UUIDs, not for Bukkit-Offline-UUIDs.
     *
     * @param uuid The UUID of the player.
     * @return The names and name change dates of the player.
     */
    public static NameChange[] getNamesFromUUID(@NotNull UUID uuid) {
        return getNamesFromUUID(uuid.toString());
    }

    /**
     * Gets the name history of a player from the Mojang servers.
     * Only works for Mojang-UUIDs, not for Bukkit-Offline-UUIDs.
     *
     * @param uuid The UUID of the player.
     * @return The names and name change dates of the player.
     */
    public static NameChange[] getNamesFromUUID(@NotNull String uuid) {
        NameChange[] names = null;
        try {
            Scanner jsonScanner = new Scanner(
                    (new URL("https://api.mojang.com/user/profiles/" + uuid.replaceAll("-", "") + "/names"))
                            .openConnection().getInputStream(),
                    "UTF-8");
            names = GSON.fromJson(jsonScanner.next(), NameChange[].class);
            jsonScanner.close();
        } catch (IOException e) {
            System.out
                    .println("Looks like there is a problem with the connection with Mojang. Please retry later.");
            if (e.getMessage().contains("HTTP response code: 429")) {
                System.out.println("You have reached the request limit of the Mojang api! Please retry later!");
            }
            e.printStackTrace();
        } catch (Exception e) {
            System.out.println("Looks like there is no player with this uuid!\n UUID: \"" + uuid + "\"");
            e.printStackTrace();
        }
        return names;
    }

    /**
     * @param name       The name of the player you want the UUID from.
     * @param onlineMode True the UUID should be an online mode UUID (from Mojang). False if it should be an offline mode UUID (from Bukkit).
     * @return The requested UUID (without separators).
     */
    public static String getUUIDFromName(@NotNull String name, boolean onlineMode) {
        return getUUIDFromName(name, onlineMode, false, false, null);
    }

    /**
     * @param name          The name of the player you want the UUID from.
     * @param onlineMode    True the UUID should be an online mode UUID (from Mojang). False if it should be an offline mode UUID (from Bukkit).
     * @param lastKnownDate The last time you know that the player had this name.
     * @return The requested UUID (without separators).
     */
    public static String getUUIDFromName(@NotNull String name, boolean onlineMode, @Nullable Date lastKnownDate) {
        return getUUIDFromName(name, onlineMode, false, false, lastKnownDate);
    }

    /**
     * @param name           The name of the player you want the UUID from.
     * @param onlineMode     True the UUID should be an online mode UUID (from Mojang). False if it should be an offline mode UUID (from Bukkit).
     * @param withSeparators True will return the UUID with '-' separators (format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx).
     *                       False will return the UUID without the '-' separator (format: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx).
     * @return The requested UUID.
     */
    public static String getUUIDFromName(@NotNull String name, boolean onlineMode, boolean withSeparators) {
        return getUUIDFromName(name, onlineMode, withSeparators, false, null);
    }

    /**
     * @param name           The name of the player you want the UUID from.
     * @param onlineMode     True the UUID should be an online mode UUID (from Mojang). False if it should be an offline mode UUID (from Bukkit).
     * @param withSeparators True will return the UUID with '-' separators (format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx).
     *                       False will return the UUID without the '-' separator (format: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx).
     * @param lastKnownDate  The last time you know that the player had this name.
     * @return The requested UUID.
     */
    public static String getUUIDFromName(@NotNull String name, boolean onlineMode, boolean withSeparators,
            @Nullable Date lastKnownDate) {
        return getUUIDFromName(name, onlineMode, withSeparators, false, lastKnownDate);
    }

    /**
     * @param name              The name of the player you want the UUID from.
     * @param onlineMode        True the UUID should be an online mode UUID (from Mojang). False if it should be an offline mode UUID (from Bukkit).
     * @param withSeparators    True will return the UUID with '-' separators (format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx).
     *                          False will return the UUID without the '-' separator (format: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx).
     * @param offlineUUIDonFail True if an offline UUID should be returned if the Mojang server can't resolve the name.
     *                          False if null should be returned if the Mojang server doesn't return an UUID.
     * @return The requested UUID.
     */
    public static String getUUIDFromName(@NotNull String name, boolean onlineMode, boolean withSeparators,
            boolean offlineUUIDonFail) {
        return getUUIDFromName(name, onlineMode, withSeparators, offlineUUIDonFail, null);
    }

    /**
     * @param name              The name of the player you want the UUID from.
     * @param onlineMode        True the UUID should be an online mode UUID (from Mojang). False if it should be an offline mode UUID (from Bukkit).
     * @param withSeparators    True will return the UUID with '-' separators (format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx).
     *                          False will return the UUID without the '-' separator (format: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx).
     * @param offlineUUIDonFail True if an offline UUID should be returned if the Mojang server can't resolve the name.
     *                          False if null should be returned if the Mojang server doesn't return an UUID.
     * @param lastKnownDate     The last time you know that the player had this name.
     * @return The requested UUID.
     */
    public static String getUUIDFromName(@NotNull String name, boolean onlineMode, boolean withSeparators,
            boolean offlineUUIDonFail, @Nullable Date lastKnownDate) {
        String uuid;
        if (onlineMode) {
            uuid = getOnlineUUID(name, lastKnownDate);
            if (uuid == null) {
                if (offlineUUIDonFail) {
                    System.out.println("Using offline uuid for '" + name + "'.");
                    uuid = UUID.nameUUIDFromBytes(("OfflinePlayer:" + name).getBytes(Charsets.UTF_8)).toString();
                } else {
                    return null;
                }
            }
        } else {
            uuid = UUID.nameUUIDFromBytes(("OfflinePlayer:" + name).getBytes(Charsets.UTF_8)).toString();
        }
        // Fixing the separators depending on setting.
        if (withSeparators) {
            if (!uuid.contains("-")) {
                uuid = uuid.replaceAll(UUID_FORMAT_REGEX, UUID_FORMAT_REPLACE_TO);
            }
        } else {
            if (uuid.contains("-")) {
                uuid = uuid.replaceAll("-", "");
            }
        }
        return uuid;
    }

    /**
     * @param name       The name of the player you want to retrieve the UUID from.
     * @param onlineMode True if the UUID should be an online mode UUID (from Mojang). False if it should be an offline mode UUID (from Bukkit).
     * @return The requested UUID object.
     */
    public static UUID getUUIDFromNameAsUUID(@NotNull String name, boolean onlineMode) {
        return getUUIDFromNameAsUUID(name, onlineMode, false);
    }

    /**
     * @param name          The name of the player you want to retrieve the UUID from.
     * @param onlineMode    True if the UUID should be an online mode UUID (from Mojang). False if it should be an offline mode UUID (from Bukkit).
     * @param lastKnownDate The last time you know that the player had this name.
     * @return The requested UUID object.
     */
    public static UUID getUUIDFromNameAsUUID(@NotNull String name, boolean onlineMode,
            @Nullable Date lastKnownDate) {
        return getUUIDFromNameAsUUID(name, onlineMode, false, lastKnownDate);
    }

    /**
     * @param name              The name of the player you want to retrieve the UUID from.
     * @param onlineMode        True the UUID should be an online mode UUID (from Mojang). False if it should be an offline mode UUID (from Bukkit).
     * @param offlineUUIDonFail True if an offline UUID should be returned if the Mojang server can't resolve the name.
     *                          False if null should be returned if the Mojang server doesn't return an UUID.
     * @return The requested UUID object.
     */
    public static UUID getUUIDFromNameAsUUID(@NotNull String name, boolean onlineMode, boolean offlineUUIDonFail) {
        return getUUIDFromNameAsUUID(name, onlineMode, offlineUUIDonFail, null);
    }

    /**
     * @param name              The name of the player you want to retrieve the UUID from.
     * @param onlineMode        True the UUID should be an online mode UUID (from Mojang). False if it should be an offline mode UUID (from Bukkit).
     * @param offlineUUIDonFail True if an offline UUID should be returned if the Mojang server can't resolve the name.
     *                          False if null should be returned if the Mojang server doesn't return an UUID.
     * @param lastKnownDate     The last time you know that the player had this name.
     * @return The requested UUID object.
     */
    public static UUID getUUIDFromNameAsUUID(@NotNull String name, boolean onlineMode, boolean offlineUUIDonFail,
            @Nullable Date lastKnownDate) {
        UUID uuid = null;
        if (onlineMode) {
            String sUUID = getOnlineUUID(name, lastKnownDate);
            if (sUUID != null) {
                uuid = UUID.fromString(sUUID.replaceAll(UUID_FORMAT_REGEX, UUID_FORMAT_REPLACE_TO));
            } else if (offlineUUIDonFail) {
                uuid = UUID.nameUUIDFromBytes(("OfflinePlayer:" + name).getBytes(Charsets.UTF_8));
            }
        } else {
            uuid = UUID.nameUUIDFromBytes(("OfflinePlayer:" + name).getBytes(Charsets.UTF_8));
        }
        return uuid;
    }

    private static String getOnlineUUID(@NotNull String name, @Nullable Date at) {
        if ((at == null || at.after(new Date(new Date().getTime() - 1000L * 24 * 3600 * 30)))
                && UUID_CACHE.containsKey(name)) {
            return UUID_CACHE.get(name);
        }
        String uuid = null;
        try (BufferedReader in = new BufferedReader(
                new InputStreamReader(new URL("https://api.mojang.com/users/profiles/minecraft/" + name
                        + ((at != null) ? "?at=" + at.getTime() : "")).openStream()))) {
            uuid = (((JsonObject) new JsonParser().parse(in)).get("id")).getAsString();
            if (uuid != null && (at == null || at.after(new Date(new Date().getTime() - 1000L * 24 * 3600 * 30)))) {
                UUID_CACHE.put(name, uuid);
            }
        } catch (MalformedURLException e) // There is something going wrong!
        {
            System.out.println("Failed to get uuid cause of a malformed url!\n Name: \"" + name + "\" Date: "
                    + ((at != null) ? "?at=" + at.getTime() : "null"));
            e.printStackTrace();
        } catch (IOException e) {
            System.out
                    .println("Looks like there is a problem with the connection with mojang. Please retry later.");
            if (e.getMessage().contains("HTTP response code: 429")) //TODO: more reliable detection
            {
                System.out.println("You have reached the request limit of the mojang api! Please retry later!");
            }
            e.printStackTrace();
        } catch (Exception e) {
            if (at == null) // We can't resolve the uuid for the player
            {
                System.out.println("Unable to get UUID for: " + name + "!");
            } else if (at.getTime() == 0) // If it's not his first name maybe it's his current name
            {
                System.out.println(
                        "Unable to get UUID for: " + name + " at " + at.getTime() + "! Trying without date!");
                uuid = getOnlineUUID(name, null);
            } else // If we cant get the player with the date he was here last time it's likely that it is his first name
            {
                System.out.println("Unable to get UUID for: " + name + " at " + at.getTime() + "! Trying at=0!");
                uuid = getOnlineUUID(name, new Date(0));
            }
            //e.printStackTrace();
        }
        return uuid;
    }

    //region Multi querys
    //TODO: JavaDoc Exception handling, more parameters, fallback
    private static final int BATCH_SIZE = 100; // Limit from Mojang

    public static Map<String, String> getUUIDsFromNames(@NotNull Collection<String> names, boolean onlineMode,
            boolean withSeparators) {
        Map<String, String> result = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
        for (Map.Entry<String, UUID> entry : getUUIDsFromNamesAsUUIDs(names, onlineMode).entrySet()) {
            result.put(entry.getKey(), (withSeparators) ? entry.getValue().toString()
                    : entry.getValue().toString().replaceAll("-", ""));
        }
        return result;
    }

    public static Map<String, UUID> getUUIDsFromNamesAsUUIDs(@NotNull Collection<String> names,
            boolean onlineMode) {
        if (onlineMode) {
            return getUUIDsFromNamesAsUUIDs(names);
        }
        Map<String, UUID> result = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
        for (String name : names) {
            result.put(name, getUUIDFromNameAsUUID(name, false));
        }
        return result;
    }

    public static Map<String, UUID> getUUIDsFromNamesAsUUIDs(@NotNull Collection<String> names) {
        List<String> batch = new LinkedList<>();
        Iterator<String> players = names.iterator();
        Map<String, UUID> result = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
        boolean success;
        int fromCache = 0, fromWeb = 0;
        while (players.hasNext()) {
            while (players.hasNext() && batch.size() < BATCH_SIZE) {
                String name = players.next();
                if (UUID_CACHE.containsKey(name)) {
                    result.put(name, UUID.fromString(
                            UUID_CACHE.get(name).replaceAll(UUID_FORMAT_REGEX, UUID_FORMAT_REPLACE_TO)));
                    fromCache++;
                } else {
                    batch.add(name);
                }
            }
            do {
                HttpURLConnection connection = null;
                try {
                    connection = (HttpURLConnection) new URL("https://api.mojang.com/profiles/minecraft")
                            .openConnection();
                    connection.setRequestMethod("POST");
                    connection.setRequestProperty("Content-Type", "application/json; encoding=UTF-8");
                    connection.setUseCaches(false);
                    connection.setDoInput(true);
                    connection.setDoOutput(true);
                    try (OutputStream out = connection.getOutputStream()) {
                        out.write(GSON.toJson(batch).getBytes(Charsets.UTF_8));
                    }
                    Profile[] profiles;
                    try (Reader in = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
                        profiles = GSON.fromJson(in, Profile[].class);
                    }
                    for (Profile profile : profiles) {
                        result.put(profile.name, profile.getUUID());
                        UUID_CACHE.put(profile.name, profile.getUUID().toString());
                        fromWeb++;
                    }
                } catch (IOException e) {
                    try {
                        if (connection != null && connection.getResponseCode() == 429) {
                            System.out.println(
                                    "Reached the request limit of the mojang api!\nConverting will be paused for 10 minutes and then continue!");
                            //TODO: better fail handling
                            Thread.sleep(MOJANG_QUERY_RETRY_TIME);
                            success = false;
                            continue;
                        }
                    } catch (Exception ignore) {
                    }
                    e.printStackTrace();
                    return result;
                }
                batch.clear();
                success = true;
            } while (!success);
        }
        System.out.println("Converted " + (fromCache + fromWeb) + "/" + names.size() + " UUIDs (" + fromCache
                + " of them from the cache and " + fromWeb + " from Mojang).");
        return result;
    }
    //endregion

    //region Helper Classes
    /**
     * A helper class to store the name changes and dates
     */
    public static class NameChange {
        /**
         * The name to which the name was changed
         */
        public String name;

        /**
         * DateTime of the name change in UNIX time (without milliseconds)
         */
        public long changedToAt;

        /**
         * Gets the date of a name change
         *
         * @return Date of the name change
         */
        public Date getChangeDate() {
            return new Date(changedToAt);
        }
    }

    private static class Profile {
        public String id;
        public String name;

        public UUID getUUID() {
            return UUID.fromString(id.replaceAll(UUID_FORMAT_REGEX, UUID_FORMAT_REPLACE_TO));
        }
    }

    private static class CacheData {
        public String name, uuid, expiresOn;

        public Date getExpiresDate() {
            //noinspection SpellCheckingInspection
            DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z");
            try {
                return format.parse(expiresOn);
            } catch (ParseException e) {
                e.printStackTrace();
            }
            return new Date(); // When we failed to parse the date we return the current time stamp
        }
    }
    //endregion
}