/*
* SquirrelID, a UUID library for Minecraft
* Copyright (C) sk89q <http://www.sk89q.com>
* Copyright (C) SquirrelID team and contributors
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU Lesser 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 Lesser General Public License
* for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.sk89q.squirrelid.resolver;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableList.Builder;
import com.google.common.collect.Iterables;
import com.sk89q.squirrelid.Profile;
import com.sk89q.squirrelid.util.HttpRequest;
import com.sk89q.squirrelid.util.UUIDs;
import javax.annotation.Nullable;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* Resolves names in bulk to UUIDs using Mojang's profile HTTP API.
*/
public class HttpRepositoryService implements ProfileService {
public static final String MINECRAFT_AGENT = "Minecraft";
private static final Logger log = Logger.getLogger(HttpRepositoryService.class.getCanonicalName());
private static final int MAX_NAMES_PER_REQUEST = 100;
private final URL profilesURL;
private int maxRetries = 5;
private long retryDelay = 50;
/**
* Create a new resolver.
*
* <p>For Minecraft, use the {@link #MINECRAFT_AGENT} constant. The UUID
* to name mapping is only available if a user owns the game for the
* provided "agent," so an incorrect agent may return zero results or
* incorrect results.</p>
*
* @param agent the agent (i.e. the game)
*/
public HttpRepositoryService(String agent) {
checkNotNull(agent);
profilesURL = HttpRequest.url("https://api.mojang.com/profiles/" + agent);
}
/**
* Get the maximum number of HTTP request retries.
*
* @return the maximum number of retries
*/
public int getMaxRetries() {
return maxRetries;
}
/**
* Set the maximum number of HTTP request retries.
*
* @param maxRetries the maximum number of retries
*/
public void setMaxRetries(int maxRetries) {
checkArgument(maxRetries > 0, "maxRetries must be >= 0");
this.maxRetries = maxRetries;
}
/**
* Get the number of milliseconds to delay after each failed HTTP request,
* doubling each time until success or total failure.
*
* @return delay in milliseconds
*/
public long getRetryDelay() {
return retryDelay;
}
/**
* Set the number of milliseconds to delay after each failed HTTP request,
* doubling each time until success or total failure.
*
* @param retryDelay delay in milliseconds
*/
public void setRetryDelay(long retryDelay) {
this.retryDelay = retryDelay;
}
@Override
public int getIdealRequestLimit() {
return MAX_NAMES_PER_REQUEST;
}
@Nullable
@Override
public Profile findByName(String name) throws IOException, InterruptedException {
ImmutableList<Profile> profiles = findAllByName(Arrays.asList(name));
if (!profiles.isEmpty()) {
return profiles.get(0);
} else {
return null;
}
}
@Override
public void findAllByName(Iterable<String> names, Predicate<Profile> consumer) throws IOException, InterruptedException {
for (List<String> partition : Iterables.partition(names, MAX_NAMES_PER_REQUEST)) {
for (Profile profile : query(partition)) {
consumer.apply(profile);
}
}
}
@Override
public ImmutableList<Profile> findAllByName(Iterable<String> names) throws IOException, InterruptedException {
Builder<Profile> builder = ImmutableList.builder();
for (List<String> partition : Iterables.partition(names, MAX_NAMES_PER_REQUEST)) {
builder.addAll(query(partition));
}
return builder.build();
}
/**
* Perform a query for profiles without partitioning the queries.
*
* @param names an iterable of names
* @return a list of results
* @throws IOException thrown on I/O error
* @throws InterruptedException thrown on interruption
*/
protected ImmutableList<Profile> query(Iterable<String> names) throws IOException, InterruptedException {
List<Profile> profiles = new ArrayList<Profile>();
Object result;
int retriesLeft = maxRetries;
long retryDelay = this.retryDelay;
while (true) {
try {
result = HttpRequest
.post(profilesURL)
.bodyJson(names)
.execute()
.returnContent()
.asJson();
break;
} catch (IOException e) {
if (retriesLeft == 0) {
throw e;
}
log.log(Level.WARNING, "Failed to query profile service -- retrying...", e);
Thread.sleep(retryDelay);
}
retryDelay *= 2;
retriesLeft--;
}
if (result instanceof Iterable) {
for (Object entry : (Iterable) result) {
Profile profile = decodeResult(entry);
if (profile != null) {
profiles.add(profile);
}
}
}
return ImmutableList.copyOf(profiles);
}
@Nullable
@SuppressWarnings("unchecked")
private static Profile decodeResult(Object entry) {
try {
if (entry instanceof Map) {
Map<Object, Object> mapEntry = (Map<Object, Object>) entry;
Object rawUuid = mapEntry.get("id");
Object rawName = mapEntry.get("name");
if (rawUuid != null && rawName != null) {
UUID uuid = UUID.fromString(UUIDs.addDashes(String.valueOf(rawUuid)));
String name = String.valueOf(rawName);
return new Profile(uuid, name);
}
}
} catch (ClassCastException e) {
log.log(Level.WARNING, "Got invalid value from UUID lookup service", e);
} catch (IllegalArgumentException e) {
log.log(Level.WARNING, "Got invalid value from UUID lookup service", e);
}
return null;
}
/**
* Create a resolver for Minecraft.
*
* @return a UUID resolver
*/
public static ProfileService forMinecraft() {
return new HttpRepositoryService(MINECRAFT_AGENT);
}
}