296 lines
10 KiB
Java
296 lines
10 KiB
Java
package net.socialgamer.cah.cardcast;
|
|
|
|
import java.io.BufferedReader;
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.io.InputStreamReader;
|
|
import java.lang.ref.SoftReference;
|
|
import java.net.HttpURLConnection;
|
|
import java.net.URL;
|
|
import java.security.cert.X509Certificate;
|
|
import java.util.ArrayList;
|
|
import java.util.Collections;
|
|
import java.util.HashMap;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.concurrent.TimeUnit;
|
|
import java.util.regex.Pattern;
|
|
|
|
import javax.net.ssl.HostnameVerifier;
|
|
import javax.net.ssl.HttpsURLConnection;
|
|
import javax.net.ssl.SSLContext;
|
|
import javax.net.ssl.SSLSession;
|
|
import javax.net.ssl.TrustManager;
|
|
import javax.net.ssl.X509TrustManager;
|
|
|
|
import net.socialgamer.cah.cardcast.CardcastModule.CardcastCardId;
|
|
|
|
import org.apache.commons.lang3.StringUtils;
|
|
import org.apache.log4j.Logger;
|
|
import org.json.simple.JSONArray;
|
|
import org.json.simple.JSONObject;
|
|
import org.json.simple.JSONValue;
|
|
|
|
import com.google.inject.Inject;
|
|
import com.google.inject.Provider;
|
|
|
|
|
|
public class CardcastService {
|
|
private static final Logger LOG = Logger.getLogger(CardcastService.class);
|
|
|
|
private static final String HOSTNAME = "api.cardcastgame.com";
|
|
|
|
/**
|
|
* Base URL to the Cardcast API.
|
|
*/
|
|
private static final String BASE_URL = "https://" + HOSTNAME + "/v1/decks/";
|
|
/**
|
|
* URL to the Cardcast API for information about a card set. The only format replacement is the
|
|
* string deck ID.
|
|
*/
|
|
private static final String CARD_SET_INFO_URL_FORMAT_STRING = BASE_URL + "%s";
|
|
/**
|
|
* URL to the Cardcast API for cards in a card set. The only format replacement is the string
|
|
* deck ID.
|
|
*/
|
|
private static final String CARD_SET_CARDS_URL_FORMAT_STRING = CARD_SET_INFO_URL_FORMAT_STRING
|
|
+ "/cards";
|
|
|
|
private static final int GET_TIMEOUT = (int) TimeUnit.SECONDS.toMillis(3);
|
|
|
|
/**
|
|
* How long to cache nonexistent card sets, or after an error occurs while querying for the card
|
|
* set. We need to do this to prevent DoS attacks.
|
|
*/
|
|
private static final long INVALID_SET_CACHE_LIFETIME = TimeUnit.SECONDS.toMillis(30);
|
|
|
|
/**
|
|
* How long to cache valid card sets.
|
|
*/
|
|
private static final long VALID_SET_CACHE_LIFETIME = TimeUnit.MINUTES.toMillis(15);
|
|
|
|
private static final Pattern validIdPattern = Pattern.compile("[A-Z0-9]{5}");
|
|
|
|
private static final Map<String, SoftReference<CardcastCacheEntry>> cache = Collections
|
|
.synchronizedMap(new HashMap<String, SoftReference<CardcastCacheEntry>>());
|
|
|
|
private final Provider<Integer> cardIdProvider;
|
|
|
|
@Inject
|
|
public CardcastService(@CardcastCardId final Provider<Integer> cardIdProvider) {
|
|
this.cardIdProvider = cardIdProvider;
|
|
}
|
|
|
|
private class CardcastCacheEntry {
|
|
final long expires;
|
|
final CardcastDeck deck;
|
|
|
|
CardcastCacheEntry(final long cacheLifetime, final CardcastDeck deck) {
|
|
this.expires = System.currentTimeMillis() + cacheLifetime;
|
|
this.deck = deck;
|
|
}
|
|
}
|
|
|
|
private CardcastCacheEntry checkCache(final String setId) {
|
|
final SoftReference<CardcastCacheEntry> soft = cache.get(setId);
|
|
if (null == soft) {
|
|
return null;
|
|
}
|
|
return soft.get();
|
|
}
|
|
|
|
public CardcastDeck loadSet(final String setId) {
|
|
if (!validIdPattern.matcher(setId).matches()) {
|
|
return null;
|
|
}
|
|
final CardcastCacheEntry cached = checkCache(setId);
|
|
if (null != cached && cached.expires > System.currentTimeMillis()) {
|
|
LOG.info(String.format("Using cache: %s=%s", setId, cached.deck));
|
|
return cached.deck;
|
|
} else if (null != cached) {
|
|
LOG.info(String.format("Cache stale: %s", setId));
|
|
} else {
|
|
LOG.info(String.format("Cache miss: %s", setId));
|
|
}
|
|
|
|
try {
|
|
final String infoContent = getUrlContent(String
|
|
.format(CARD_SET_INFO_URL_FORMAT_STRING, setId));
|
|
if (null == infoContent) {
|
|
// failed to load
|
|
cacheMissingSet(setId);
|
|
return null;
|
|
}
|
|
final JSONObject info = (JSONObject) JSONValue.parse(infoContent);
|
|
|
|
final String cardContent = getUrlContent(String.format(
|
|
CARD_SET_CARDS_URL_FORMAT_STRING, setId));
|
|
if (null == cardContent) {
|
|
// failed to load
|
|
cacheMissingSet(setId);
|
|
return null;
|
|
}
|
|
final JSONObject cards = (JSONObject) JSONValue.parse(cardContent);
|
|
|
|
final String name = (String) info.get("name");
|
|
final String description = (String) info.get("description");
|
|
if (null == name || null == description || name.isEmpty()) {
|
|
// We require a name. Blank description is acceptable, but cannot be null.
|
|
cacheMissingSet(setId);
|
|
return null;
|
|
}
|
|
final CardcastDeck deck = new CardcastDeck(name, setId, description);
|
|
|
|
// load up the cards
|
|
final JSONArray blacks = (JSONArray) cards.get("calls");
|
|
if (null != blacks) {
|
|
for (final Object black : blacks) {
|
|
final JSONArray texts = (JSONArray) ((JSONObject) black).get("text");
|
|
if (null != texts) {
|
|
// TODO this is going to need some work to look pretty.
|
|
final List<String> strs = new ArrayList<String>(texts.size());
|
|
for (final Object o : texts) {
|
|
strs.add((String) o);
|
|
}
|
|
final String text = StringUtils.join(strs, "____");
|
|
final int pick = strs.size() - 1;
|
|
final int draw = (pick >= 3 ? pick - 1 : 0);
|
|
final CardcastBlackCard card = new CardcastBlackCard(cardIdProvider.get(), text, draw,
|
|
pick, setId);
|
|
deck.getBlackCards().add(card);
|
|
}
|
|
}
|
|
}
|
|
|
|
final JSONArray whites = (JSONArray) cards.get("responses");
|
|
if (null != whites) {
|
|
for (final Object white : whites) {
|
|
final JSONArray texts = (JSONArray) ((JSONObject) white).get("text");
|
|
if (null != texts) {
|
|
// The white cards should only ever have one element in text, but let's be safe.
|
|
final List<String> strs = new ArrayList<String>(texts.size());
|
|
for (final Object o : texts) {
|
|
final String cardCastString = (String) o;
|
|
final StringBuilder pyxString = new StringBuilder();
|
|
|
|
// Cardcast's recommended format is to not capitalize the first letter
|
|
pyxString.append(cardCastString.substring(0, 1).toUpperCase());
|
|
pyxString.append(cardCastString.substring(1));
|
|
|
|
// Cardcast's recommended format is to not include a period
|
|
if (Character.isLetterOrDigit(cardCastString.charAt(cardCastString.length() - 1))) {
|
|
pyxString.append('.');
|
|
}
|
|
|
|
// Cardcast's white cards are now formatted consistently with pyx cards
|
|
strs.add(pyxString.toString());
|
|
}
|
|
final String text = StringUtils.join(strs, "");
|
|
final CardcastWhiteCard card = new CardcastWhiteCard(cardIdProvider.get(), text, setId);
|
|
deck.getWhiteCards().add(card);
|
|
}
|
|
}
|
|
}
|
|
|
|
cacheSet(setId, deck);
|
|
return deck;
|
|
} catch (final Exception e) {
|
|
LOG.error(String.format("Unable to load deck %s from Cardcast", setId), e);
|
|
e.printStackTrace();
|
|
cacheMissingSet(setId);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private void cachePut(final String setId, final CardcastDeck deck, final long timeout) {
|
|
LOG.info(String.format("Caching %s=%s for %d ms", setId, deck, timeout));
|
|
cache.put(setId, new SoftReference<CardcastCacheEntry>(new CardcastCacheEntry(timeout, deck)));
|
|
}
|
|
|
|
private void cacheSet(final String setId, final CardcastDeck deck) {
|
|
cachePut(setId, deck, VALID_SET_CACHE_LIFETIME);
|
|
}
|
|
|
|
private void cacheMissingSet(final String setId) {
|
|
cachePut(setId, null, INVALID_SET_CACHE_LIFETIME);
|
|
}
|
|
|
|
private String getUrlContent(final String urlStr) throws IOException {
|
|
final URL url = new URL(urlStr);
|
|
final HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
|
conn.setDoInput(true);
|
|
conn.setDoOutput(false);
|
|
conn.setRequestMethod("GET");
|
|
conn.setInstanceFollowRedirects(true);
|
|
conn.setReadTimeout(GET_TIMEOUT);
|
|
conn.setConnectTimeout(GET_TIMEOUT);
|
|
|
|
final int code = conn.getResponseCode();
|
|
if (HttpURLConnection.HTTP_OK != code) {
|
|
LOG.error(String.format("Got HTTP response code %d from Cardcast for %s", code, urlStr));
|
|
return null;
|
|
}
|
|
final String contentType = conn.getContentType();
|
|
if (!"application/json".equals(contentType)) {
|
|
LOG.error(String.format("Got content-type %s from Cardcast for %s", contentType, urlStr));
|
|
return null;
|
|
}
|
|
|
|
final InputStream is = conn.getInputStream();
|
|
final InputStreamReader isr = new InputStreamReader(is);
|
|
final BufferedReader reader = new BufferedReader(isr);
|
|
final StringBuilder builder = new StringBuilder(4096);
|
|
String line;
|
|
while ((line = reader.readLine()) != null) {
|
|
builder.append(line);
|
|
builder.append('\n');
|
|
}
|
|
reader.close();
|
|
isr.close();
|
|
is.close();
|
|
|
|
return builder.toString();
|
|
}
|
|
|
|
public static void hackSslVerifier() {
|
|
// FIXME: My JVM doesn't like the certificate. I should go add StartSSL's root certificate to
|
|
// its trust store, and document steps. For now, I'm going to disable SSL certificate checking.
|
|
|
|
// Create a trust manager that does not validate certificate chains
|
|
final TrustManager[] trustAllCerts = new TrustManager[] {
|
|
new X509TrustManager() {
|
|
@Override
|
|
public X509Certificate[] getAcceptedIssuers() {
|
|
return null;
|
|
}
|
|
|
|
@Override
|
|
public void checkClientTrusted(final X509Certificate[] certs, final String authType) {
|
|
}
|
|
|
|
@Override
|
|
public void checkServerTrusted(final X509Certificate[] certs, final String authType) {
|
|
}
|
|
}
|
|
};
|
|
|
|
try {
|
|
final SSLContext sc = SSLContext.getInstance("SSL");
|
|
sc.init(null, trustAllCerts, new java.security.SecureRandom());
|
|
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
|
|
} catch (final Exception e) {
|
|
LOG.error("Unable to install trust-all security manager", e);
|
|
}
|
|
|
|
// Create host name verifier that only trusts cardcast
|
|
final HostnameVerifier allHostsValid = new HostnameVerifier() {
|
|
@Override
|
|
public boolean verify(final String hostname, final SSLSession session) {
|
|
return HOSTNAME.equals(hostname);
|
|
}
|
|
};
|
|
|
|
HttpsURLConnection.setDefaultHostnameVerifier(allHostsValid);
|
|
}
|
|
}
|