/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package sander;
import java.io.IOException;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import org.bukkit.Bukkit;
import org.bukkit.plugin.Plugin;
import org.bukkit.scheduler.BukkitRunnable;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
/**
*
* @author Fernando
*/
public class UUIDResolver {
private static final String USERNAME_DATA = "https://api.mojang.com/users/profiles/minecraft/%USERNAME%";
private static final String USERNAME_DATA_TIME = "https://api.mojang.com/users/profiles/minecraft/%USERNAME%?at=%TIMESTAMP%";
private static final String UUID_PREVIOUS_NAMES = "https://api.mojang.com/user/profiles/%UUID%/names";
private static final String UUID_SKIN_INFO = "https://sessionserver.mojang.com/session/minecraft/profile/%UUID%?unsigned=false";
private final Plugin plugin;
public UUIDResolver(Plugin plugin) {
this.plugin = plugin;
}
/**
* The cache used to store player names to uuid's
*/
private final Map<String, Pair<FutureTask<UUID>, List<Callback<UUID>>>> playerNameCache = new HashMap<>();
public UUID getUUIDAsync(String username) throws InterruptedException, ExecutionException
{
return Bukkit.getScheduler().<Future<UUID>>callSyncMethod(plugin, new Callable<Future<UUID>>() {
@Override
public Future<UUID> call() throws Exception {
return getUUID(username);
}
}).get().get();
}
/**
* Gets the UUID from a username, This method needs to be called from the
* main thread
*
* @param userName Thge username to lookup
* @return Returns a Future object, do not call get() on it from the main
* bukkit thread
*/
public Future<UUID> getUUID(String userName) {
return this.getUUID(userName, null);
}
/**
* Gets the UUID from a username, This method needs to be called from the
* main thread
*
* @param userName Thge username to lookup
* @param callBack This will be called when the lookup is done
* @return Returns a Future object, do not call get() on it from the main
* bukkit thread
*/
public Future<UUID> getUUID(String userName, Callback<UUID> callBack) {
return this.<UUID, String>resolve(userName, playerNameCache, callBack,
uuidConverter,
"https://api.mojang.com/users/profiles/minecraft/%s", userName);
}
/**
* The cache used to store player names and times to uuid's
*/
private final Map<Pair<String, Long>, Pair<FutureTask<UUID>, List<Callback<UUID>>>> playerNameTimeCache = new HashMap<>();
/**
* Gets the UUID from a username, This method needs to be called from the
* main thread
*
* @param userName Thge username to lookup
* @param unixTime Time to lookup
* @return Returns a Future object, do not call get() on it from the main
* bukkit thread
*/
public Future<UUID> getUUID(String userName, long unixTime) {
return this.getUUID(userName, unixTime, null);
}
/**
* Gets the UUID from a username, This method needs to be called from the
* main thread
*
* @param userName Thge username to lookup
* @param unixTime Time to lookup
* @param callBack This will be called when the lookup is done
* @return Returns a Future object, do not call get() on it from the main
* bukkit thread
*/
public Future<UUID> getUUID(String userName, long unixTime, Callback<UUID> callBack) {
return this.<UUID, Pair<String, Long>>resolve(new Pair<>(userName, unixTime), playerNameTimeCache, callBack,
uuidConverter,
"https://api.mojang.com/users/profiles/minecraft/%s?at=%d",
userName, unixTime);
}
/**
* The cache used to store player names to uuid's
*/
private final Map<UUID, Pair<FutureTask<JSONObject>, List<Callback<JSONObject>>>> playerDataCache = new HashMap<>();
public JSONObject getInternalDataAsync(UUID uuid) throws InterruptedException, ExecutionException
{
return Bukkit.getScheduler().<Future<JSONObject>>callSyncMethod(plugin, new Callable<Future<JSONObject>>() {
@Override
public Future<JSONObject> call() throws Exception {
return getInternalData(uuid);
}
}).get().get();
}
public Future<JSONObject> getInternalData(UUID userName)
{
return this.getInternalData(userName,null);
}
/**
* This method needs to be called from the
* main thread
*
* @param userName The UUID to lookup
* @param callBack This will be called when the lookup is done
* @return Returns a Future object, do not call get() on it from the main
* bukkit thread
*/
public Future<JSONObject> getInternalData(UUID userName, Callback<JSONObject> callBack) {
return this.<JSONObject, UUID>resolve(userName, playerDataCache, callBack,
new ResponseResolver<JSONObject>() {
@Override
public JSONObject resolveResult(JSONObject obj) {
return obj;
}
},
"https://api.mojang.com/users/profiles/minecraft/%s", userName.toString().replaceAll("-", ""));
}
/**
* Read a json object from a url
*
* @param url
* @return
* @throws ParseException
* @throws IOException
*/
private JSONObject readJSONFromURL(URL url)
throws ParseException, IOException {
URLConnection uc = url.openConnection();
uc.setUseCaches(true);
uc.setDefaultUseCaches(true);
uc.addRequestProperty("User-Agent", "Java Bigteddy98 UUID Library");
try (Scanner scanner = new Scanner(uc.getInputStream())) {
scanner.useDelimiter("\\A");
return (JSONObject) new JSONParser().parse(scanner.next());
}
}
/**
* Private class used to convert the json response to the correct object
*/
private interface ResponseResolver<R> {
public R resolveResult(JSONObject obj);
}
/**
* Private class used to convert the json response to a uuid
*/
private static final ResponseResolver<UUID> uuidConverter
= new ResponseResolver<UUID>() {
@Override
public UUID resolveResult(JSONObject obj) {
return UUID.fromString(obj.get("id").toString().replaceAll(
"(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})",
"$1-$2-$3-$4-$5"));
}
};
/**
* Private method used to resolve the queries
*
* @param <R> The result of the operation
* @param <I> The unique input of the operation, this will be used to lookup
* the cache result
* @param cacheIndex This is the unique value of the result in the cache
* @param cache The Map<...> object used to hold the cached values
* @param callback This object will be called when the request is done
* @param request Its the job of the <code>RequestResolver</code> to convert
* the <code>JSONObject</code> back to the object the code needs
* @param baseUrl The main url used for this operation, this will be passed
* to <code>String.format</code>
* @param formats The arguments for <code>String.format</code>
* @return This method returns a Future<R> to see what the result of the
* request is
*/
private <R, I> Future<R> resolve(
I cacheIndex, Map<I, Pair<FutureTask<R>, List<Callback<R>>>> cache,
Callback<R> callback, ResponseResolver<R> request,
String baseUrl, Object... formats) {
if (!Bukkit.isPrimaryThread()) {
throw new IllegalStateException(
"You need to call this method from the main thread "
+ "because of the request limits Mojang has configured");
}
if (!cache.containsKey(cacheIndex)) {
final List<Callback<R>> callBackList = new LinkedList<>();
FutureTask<R> task = new FutureTask<R>(new Callable<R>() {
@Override
public R call() throws Exception {
assert !Bukkit.isPrimaryThread();
return request.resolveResult(UUIDResolver.this.readJSONFromURL(
new URL(String.format(baseUrl, (Object[]) formats))
));
}
}) {
@Override
protected void done() {
FutureTask<R> th = this;
new BukkitRunnable() {
@Override
public void run() {
assert Bukkit.isPrimaryThread();
for (Callback<R> id : callBackList) {
id.onCallBack(th);
}
callBackList.clear();
}
}.runTask(plugin);
}
};
cache.put(cacheIndex, new Pair<>(task, callBackList));
Bukkit.getScheduler().runTaskAsynchronously(plugin, task);
}
assert cache.containsKey(cacheIndex);
Pair<FutureTask<R>, List<Callback<R>>> result
= cache.get(cacheIndex);
if (result.first.isDone()) {
if (!result.second.isEmpty()) {
for (Callback<R> c : result.second) {
c.onCallBack(result.first);
}
result.second = Collections.emptyList();
}
if (callback != null) {
callback.onCallBack(result.first);
}
} else {
if (callback != null) {
result.second.add(callback);
}
}
return result.first;
}
/**
* This class contains a pair of other objects
* <p>
* Taken from: <br/>
* <a href="http://stackoverflow.com/a/677248/1542723">StackOverflow</a>
*
* @param <A> first type
* @param <B> second type
*/
private static class Pair<A, B> {
private A first;
private B second;
public Pair(A first, B second) {
super();
this.first = first;
this.second = second;
}
@Override
public int hashCode() {
int hashFirst = first != null ? first.hashCode() : 0;
int hashSecond = second != null ? second.hashCode() : 0;
return (hashFirst + hashSecond) * hashSecond + hashFirst;
}
@Override
public boolean equals(Object other) {
if (other instanceof Pair) {
Pair otherPair = (Pair) other;
return ((this.first == otherPair.first
|| (this.first != null && otherPair.first != null
&& this.first.equals(otherPair.first)))
&& (this.second == otherPair.second
|| (this.second != null && otherPair.second != null
&& this.second.equals(otherPair.second))));
}
return false;
}
@Override
public String toString() {
return "(" + first + ", " + second + ")";
}
public A getFirst() {
return first;
}
public void setFirst(A first) {
this.first = first;
}
public B getSecond() {
return second;
}
public void setSecond(B second) {
this.second = second;
}
}
/**
* Class used to pass the results back to the parent stack
*
* @param <E>
*/
public interface Callback<E> {
/**
* This method is called when the lookup is done
*
* @param result The result of the operation
*/
public void onCallBack(FutureTask<E> result);
}
}
Comments