diff --git a/src/main/java/fr/moribus/imageonmap/migration/UUIDFetcher.java b/src/main/java/fr/moribus/imageonmap/migration/UUIDFetcher.java index b0ba595..65ee55c 100644 --- a/src/main/java/fr/moribus/imageonmap/migration/UUIDFetcher.java +++ b/src/main/java/fr/moribus/imageonmap/migration/UUIDFetcher.java @@ -23,6 +23,8 @@ import java.io.InputStreamReader; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; +import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -34,20 +36,72 @@ import org.json.simple.parser.ParseException; abstract public class UUIDFetcher { - static private final String PROFILE_URL = "https://api.mojang.com/profiles/minecraft"; //The URI of the name->UUID API from Mojang - static private final JSONParser jsonParser = new JSONParser(); - - static public Map fetch(List names) throws IOException + /** + * The maximal amount of usernames to send to mojang per request + * This allows not to overload mojang's service with too many usernames at a time + */ + static private final int MOJANG_USERNAMES_PER_REQUEST = 100; + + /** + * The maximal amount of requests to send to Mojang + * The time limit for this amount is MOJANG_MAX_REQUESTS_TIME + * Read : You can only send MOJANG_MAX_REQUESTS in MOJANG_MAX_REQUESTS_TIME seconds + */ + static private final int MOJANG_MAX_REQUESTS = 600; + + /** + * The timeframe for the Mojang request limit (in seconds) + */ + static private final int MOJANG_MAX_REQUESTS_TIME = 600; + + /** + * The minimum time between two requests to mojang (in milliseconds) + */ + static private final int TIME_BETWEEN_REQUESTS = 200; + + /** + * The (approximative) timestamp of the date when Mojang name changing feature + * was announced to be released + */ + static private final int NAME_CHANGE_TIMESTAMP = 1420844400; + + static private final String PROFILE_URL = "https://api.mojang.com/profiles/minecraft"; + static private final String TIMED_PROFILE_URL = "https://api.mojang.com/users/profiles/minecraft/"; + + static public Map fetch(List names) throws IOException, InterruptedException + { + return fetch(names, MOJANG_USERNAMES_PER_REQUEST); + } + + static public Map fetch(List names, int limitByRequest) throws IOException, InterruptedException + { + Map UUIDs = new HashMap(); + int requests = (names.size() / limitByRequest) + 1; + + List tempNames; + Map tempUUIDs; + + for(int i = 0; i < requests; i++) + { + tempNames = names.subList(limitByRequest * i, Math.min((limitByRequest * (i+1)) - 1, names.size())); + tempUUIDs = rawFetch(tempNames); + UUIDs.putAll(tempUUIDs); + Thread.sleep(TIME_BETWEEN_REQUESTS); + } + + return UUIDs; + } + + static private Map rawFetch(List names) throws IOException { Map uuidMap = new HashMap(); - HttpURLConnection connection = createConnection(); + HttpURLConnection connection = getPOSTConnection(PROFILE_URL); writeBody(connection, names); - JSONArray array; try { - array = (JSONArray) jsonParser.parse(new InputStreamReader(connection.getInputStream())); + array = (JSONArray) readResponse(connection); } catch(ParseException ex) { @@ -62,33 +116,63 @@ abstract public class UUIDFetcher uuidMap.put(name, fromMojangUUID(id)); } - return uuidMap; } - static public Map fetch(List names, int limitByRequest) throws IOException, InterruptedException + static public void fetchRemaining(Collection names, Map uuids) throws IOException, InterruptedException { - Map UUIDs = new HashMap(); - int requests = (names.size() / limitByRequest) + 1; + ArrayList remainingNames = new ArrayList<>(); - List tempNames; - Map tempUUIDs; - - for(int i = 0; i < requests; i++) + for(String name : names) { - tempNames = names.subList(limitByRequest * i, Math.min((limitByRequest * (i+1)) - 1, names.size())); - tempUUIDs = fetch(tempNames); - UUIDs.putAll(tempUUIDs); - Thread.sleep(400); + if(!uuids.containsKey(name)) remainingNames.add(name); + } + + int timeBetweenRequests; + if(remainingNames.size() > MOJANG_MAX_REQUESTS) + { + timeBetweenRequests = (MOJANG_MAX_REQUESTS / MOJANG_MAX_REQUESTS_TIME) * 1000; + } + else + { + timeBetweenRequests = TIME_BETWEEN_REQUESTS; + } + + User user; + for(String name : remainingNames) + { + user = fetchOriginalUUID(name); + uuids.put(user.name, user.uuid); + Thread.sleep(timeBetweenRequests); } - return UUIDs; } - - private static HttpURLConnection createConnection() throws IOException + + static private User fetchOriginalUUID(String name) throws IOException { - URL url = new URL(PROFILE_URL); - HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + HttpURLConnection connection = getGETConnection(TIMED_PROFILE_URL + name + "?at=" + NAME_CHANGE_TIMESTAMP); + sendRequest(connection); + + JSONObject object; + + try + { + object = (JSONObject) readResponse(connection); + } + catch(ParseException ex) + { + throw new IOException("Invalid response from server, unable to parse received JSON : " + ex.toString()); + } + + User user = new User(); + user.name = (String) object.get("name"); + user.uuid = fromMojangUUID((String)object.get("id")); + return user; + } + + static private HttpURLConnection getPOSTConnection(String url) throws IOException + { + HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); connection.setRequestMethod("POST"); connection.setRequestProperty("Content-Type", "application/json"); connection.setUseCaches(false); @@ -96,7 +180,22 @@ abstract public class UUIDFetcher connection.setDoOutput(true); return connection; } - + + static private HttpURLConnection getGETConnection(String url) throws IOException + { + HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); + connection.setRequestMethod("GET"); + connection.setUseCaches(false); + connection.setDoInput(true); + return connection; + } + + static private void sendRequest(HttpURLConnection connection) throws IOException + { + OutputStream stream = connection.getOutputStream(); + stream.flush(); + stream.close(); + } private static void writeBody(HttpURLConnection connection, List names) throws IOException { @@ -106,7 +205,11 @@ abstract public class UUIDFetcher stream.flush(); stream.close(); } - + + private static Object readResponse(HttpURLConnection connection) throws IOException, ParseException + { + return new JSONParser().parse(new InputStreamReader(connection.getInputStream())); + } private static UUID fromMojangUUID(String id) //Mojang sends string UUIDs without dashes ... { @@ -114,6 +217,12 @@ abstract public class UUIDFetcher id.substring(12, 16) + "-" + id.substring(16, 20) + "-" + id.substring(20, 32)); } - + + static private class User + { + public String name; + public UUID uuid; + } + } diff --git a/src/main/java/fr/moribus/imageonmap/migration/V3Migrator.java b/src/main/java/fr/moribus/imageonmap/migration/V3Migrator.java index f36bc2d..930f163 100644 --- a/src/main/java/fr/moribus/imageonmap/migration/V3Migrator.java +++ b/src/main/java/fr/moribus/imageonmap/migration/V3Migrator.java @@ -71,12 +71,6 @@ public class V3Migrator */ static private final String BACKUPS_POSTV3_DIRECTORY_NAME = "backups_post-v3"; - /** - * The maximal amount of usernames to send to mojang per request - * This allows not to overload mojang's service with too many usernames at a time - */ - static private final int MOJANG_USERNAMES_PER_REQUEST = 100; - /** * Returns the former images directory of a given plugin * @param plugin The plugin. @@ -167,9 +161,8 @@ public class V3Migrator if(checkForExistingBackups()) return; if(!loadOldFiles()) return; backupMapData(); - loadOldFiles(); fetchUUIDs(); - if(!checkMissingUUIDs()) return; + if(!fetchMissingUUIDs()) return; } catch(Exception ex) { @@ -341,7 +334,7 @@ public class V3Migrator logInfo("Fetching UUIDs from Mojang ..."); try { - usersUUIDs = UUIDFetcher.fetch(new ArrayList(userNamesToFetch), MOJANG_USERNAMES_PER_REQUEST); + usersUUIDs = UUIDFetcher.fetch(new ArrayList(userNamesToFetch)); } catch(IOException ex) { @@ -357,14 +350,30 @@ public class V3Migrator } /** - * Check if any UUID has been retreived. + * Fetches the UUIDs that could not be retreived via Mojang's standard API * @return true if at least one UUID has been retreived, false otherwise */ - private boolean checkMissingUUIDs() + private boolean fetchMissingUUIDs() throws IOException, InterruptedException { if(usersUUIDs.size() == userNamesToFetch.size()) return true; - logInfo("Mojang did not find UUIDs for all the registered players."); - logInfo("This means some of the users do not actually exist, or they have changed names before migrating."); + int remainingUsersCount = userNamesToFetch.size() - usersUUIDs.size(); + logInfo("Mojang did not find UUIDs for "+remainingUsersCount+" players."); + logInfo("The Mojang servers limit requests rate at one per second, this may take some time..."); + + try + { + UUIDFetcher.fetchRemaining(userNamesToFetch, usersUUIDs); + } + catch(IOException ex) + { + logError("An error occured while fetching the UUIDs from Mojang", ex); + throw ex; + } + catch(InterruptedException ex) + { + logError("The migration worker has been interrupted", ex); + throw ex; + } if(usersUUIDs.size() <= 0) { @@ -373,15 +382,6 @@ public class V3Migrator return false; } - String missingUsersList = ""; - - for(String user : userNamesToFetch) - { - if(!usersUUIDs.containsKey(user)) missingUsersList += user + ","; - } - missingUsersList = missingUsersList.substring(0, missingUsersList.length()); - - logInfo("Here are the missing players : " + missingUsersList); return true; }