Java tutorial
/******************************************************************************* * Copyright (c) 2014, 2015 Scott Clarke (scott@dawg6.com). * * This file is part of Dawg6's battle.net Diablo 3 API. * * Dawg6's Demon Hunter DPS Calculator 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. * * Dawg6's Demon Hunter DPS Calculator 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 com.dawg6.d3api.server; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL; import java.util.LinkedList; import java.util.List; import java.util.Vector; import java.util.logging.Level; import java.util.logging.Logger; import org.apache.http.HttpResponse; import org.apache.http.NameValuePair; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.utils.URIBuilder; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.message.BasicNameValuePair; import com.dawg6.d3api.server.oauth.Account; import com.dawg6.d3api.server.oauth.Token; import com.dawg6.d3api.shared.CareerProfile; import com.dawg6.d3api.shared.HeroProfile; import com.dawg6.d3api.shared.ItemInformation; import com.dawg6.d3api.shared.Leaderboard; import com.dawg6.d3api.shared.Realm; import com.dawg6.d3api.shared.Season; import com.dawg6.d3api.shared.SeasonIndex; import com.fasterxml.jackson.core.JsonParseException; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.ObjectMapper; public class D3IO { protected static final Logger log = Logger.getLogger(D3IO.class.getName()); public static final Integer DEFAULT_MAX_REQUESTS_PER_SECOND = 85; public static final int DEFAULT_CONNECT_TIMEOUT = 30000; public static final int DEFAULT_READ_TIMEOUT = 30000; public static final String TOKEN_SERVER_URL = "https://us.battle.net/oauth/token"; public static final String ACCOUNT_API_URL = "https://us.api.battle.net/account/user"; public static final Realm DEFAULT_ITEM_REALM = Realm.US; public static final int DEFAULT_TIMEOUT_RETRIES = 10; protected int connectTimeout = DEFAULT_CONNECT_TIMEOUT; protected Token token = null; protected int readTimeout = DEFAULT_READ_TIMEOUT; protected long token_expires = 0L; protected int timeoutRetries = DEFAULT_TIMEOUT_RETRIES; protected Realm itemRealm = DEFAULT_ITEM_REALM; protected int maxRequests = DEFAULT_MAX_REQUESTS_PER_SECOND; protected String apiKey = null; protected String apiSecret = null; protected final List<Long> requests = new LinkedList<Long>(); protected long numRequests = 0; protected final Cache<String, ItemInformation> itemCache = new Cache<String, ItemInformation>(true); protected long cacheHit = 0; protected long cacheMiss = 0; protected long errors = 0; protected long retryAttempts = 0; protected D3IO() { } public void throttle() { synchronized (requests) { if (requests.size() >= maxRequests) { long last = 0; if (requests.size() > 0) last = requests.get(0); long now = System.currentTimeMillis(); long delta = now - last; if (delta < 1000) { // log.info("Throttling: " + (1000 - delta)); try { Thread.sleep(1000 - delta); } catch (Exception ie) { } } while (requests.size() >= maxRequests) requests.remove(0); } long finish = System.currentTimeMillis(); requests.add(finish); numRequests++; } } protected synchronized String getApiKey() { return "&apikey=" + apiKey; } public long getCacheHits() { synchronized (itemCache) { return cacheHit; } } public long getCacheMisses() { synchronized (itemCache) { return cacheMiss; } } public void clearItemCache() { synchronized (itemCache) { itemCache.clear(); cacheHit = 0; cacheMiss = 0; } } protected synchronized Token getToken() { if ((this.token == null) || (this.token_expires < System.currentTimeMillis())) { try { this.token = requestToken(); this.token_expires = System.currentTimeMillis() + ((token.expires_in - (24L * 60L * 60L)) * 1000L); } catch (RuntimeException e) { log.log(Level.SEVERE, "Exception", e); throw e; } catch (Exception e2) { log.log(Level.SEVERE, "Exception", e2); throw new RuntimeException(e2); } } return token; } public String getAccessToken() { return "?access_token=" + getToken().access_token; } protected Token requestToken() throws Exception { CloseableHttpClient client = HttpClientBuilder.create().build(); List<NameValuePair> params = new Vector<NameValuePair>(); params.add(new BasicNameValuePair("client_id", apiKey)); params.add(new BasicNameValuePair("client_secret", apiSecret)); params.add(new BasicNameValuePair("grant_type", "client_credentials")); HttpPost request = new HttpPost(TOKEN_SERVER_URL); request.setEntity(new UrlEncodedFormEntity(params)); HttpResponse response = client.execute(request); if (response.getStatusLine().getStatusCode() != 200) { log.log(Level.SEVERE, "HTTP Server Response: " + response.getStatusLine().getStatusCode()); throw new RuntimeException("HTTP Server Response: " + response.getStatusLine().getStatusCode()); } BufferedReader rd = new BufferedReader(new InputStreamReader(response.getEntity().getContent())); StringBuffer result = new StringBuffer(); String line = ""; while ((line = rd.readLine()) != null) { result.append(line); } client.close(); ObjectMapper mapper = new ObjectMapper(); mapper = mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); Token token = mapper.readValue(result.toString(), Token.class); if ((token != null) && (token.error != null)) throw new RuntimeException(token.error_description); return token; } protected Token requestToken(String code, String redirectUrl, String scope) throws Exception { CloseableHttpClient client = HttpClientBuilder.create().build(); List<NameValuePair> params = new Vector<NameValuePair>(); params.add(new BasicNameValuePair("client_id", apiKey)); params.add(new BasicNameValuePair("client_secret", apiSecret)); params.add(new BasicNameValuePair("grant_type", "authorization_code")); params.add(new BasicNameValuePair("redirect_uri", redirectUrl)); params.add(new BasicNameValuePair("scope", scope)); params.add(new BasicNameValuePair("code", code)); HttpPost request = new HttpPost(TOKEN_SERVER_URL); request.setEntity(new UrlEncodedFormEntity(params)); HttpResponse response = client.execute(request); if (response.getStatusLine().getStatusCode() != 200) { log.log(Level.SEVERE, "HTTP Server Response: " + response.getStatusLine().getStatusCode()); throw new RuntimeException("HTTP Server Response: " + response.getStatusLine().getStatusCode()); } BufferedReader rd = new BufferedReader(new InputStreamReader(response.getEntity().getContent())); StringBuffer result = new StringBuffer(); String line = ""; while ((line = rd.readLine()) != null) { result.append(line); } client.close(); ObjectMapper mapper = new ObjectMapper(); mapper = mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); Token token = mapper.readValue(result.toString(), Token.class); if ((token != null) && (token.error != null)) throw new RuntimeException(token.error_description); return token; } public Account getAccount(Token token) throws Exception { CloseableHttpClient client = HttpClientBuilder.create().build(); URIBuilder builder = new URIBuilder(ACCOUNT_API_URL); builder.addParameter("access_token", token.access_token); // log.info("Request = " + builder.toString()); HttpGet request = new HttpGet(builder.toString()); HttpResponse response = client.execute(request); if (response.getStatusLine().getStatusCode() != 200) { log.log(Level.SEVERE, "HTTP Server Response: " + response.getStatusLine().getStatusCode()); throw new RuntimeException("HTTP Server Response: " + response.getStatusLine().getStatusCode()); } BufferedReader rd = new BufferedReader(new InputStreamReader(response.getEntity().getContent())); StringBuffer result = new StringBuffer(); String line = ""; while ((line = rd.readLine()) != null) { result.append(line); } client.close(); ObjectMapper mapper = new ObjectMapper(); mapper = mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); Account account = mapper.readValue(result.toString(), Account.class); if ((account != null) && (account.error != null)) throw new RuntimeException(token.error_description); return account; } public CareerProfile readCareerProfile(String string) throws JsonParseException, JsonMappingException, IOException { return readCareerProfile(string, true); } public HeroProfile readHeroProfile(String string) throws JsonParseException, JsonMappingException, IOException { return readHeroProfile(string, true); } public ItemInformation readItemInformation(String string) throws JsonParseException, JsonMappingException, IOException { return readItemInformation(string, true); } public CareerProfile readCareerProfile(String string, boolean ignoreUnknown) throws JsonParseException, JsonMappingException, IOException { ObjectMapper mapper = new ObjectMapper(); if (ignoreUnknown) { mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); } CareerProfile out = mapper.readValue(new File(string), CareerProfile.class); return out; } public HeroProfile readHeroProfile(String string, boolean ignoreUnknown) throws JsonParseException, JsonMappingException, IOException { ObjectMapper mapper = new ObjectMapper(); if (ignoreUnknown) { mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); } HeroProfile out = mapper.readValue(new File(string), HeroProfile.class); return out; } public ItemInformation readItemInformation(String string, boolean ignoreUnknown) throws JsonParseException, JsonMappingException, IOException { ObjectMapper mapper = new ObjectMapper(); if (ignoreUnknown) { mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); } ItemInformation out = mapper.readValue(new File(string), ItemInformation.class); return out; } public CareerProfile readCareerProfile(Realm realm, String name, int code) throws JsonParseException, IOException { ObjectMapper mapper = new ObjectMapper(); throttle(); URL url = new URL(UrlHelper.careerProfileUrl(realm.getApiHost(), name, code) + getApiKey()); CareerProfile out = readValue(mapper, url, CareerProfile.class); return out; } private <T> T readValue(ObjectMapper mapper, URL url, Class<T> clazz) throws JsonParseException, JsonMappingException, IOException { return readValue(mapper, url, clazz, this.timeoutRetries); } private <T> T readValue(ObjectMapper mapper, URL url, Class<T> clazz, int retries) throws JsonParseException, JsonMappingException, IOException { // log.info("URL " + url); HttpURLConnection c = (HttpURLConnection) url.openConnection(); c.setRequestMethod("GET"); c.setRequestProperty("Content-length", "0"); c.setUseCaches(false); c.setAllowUserInteraction(false); c.setConnectTimeout(connectTimeout); c.setReadTimeout(readTimeout); c.connect(); int status = c.getResponseCode(); switch (status) { case 200: case 201: BufferedReader br = new BufferedReader(new InputStreamReader(c.getInputStream())); StringBuilder sb = new StringBuilder(); String line; while ((line = br.readLine()) != null) { sb.append(line + "\n"); } br.close(); try { mapper = mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); return mapper.readValue(sb.toString(), clazz); } catch (Exception e) { log.severe("JSON = " + sb.toString()); log.log(Level.SEVERE, e.getMessage()); return null; } case 408: case 504: log.info("HTTP Response: " + status + ", retries = " + retries + ", URL: " + url); errors++; onError(status); if (retries > 0) { retryAttempts++; onRetry(); return readValue(mapper, url, clazz, retries - 1); } else return null; default: log.severe("HTTP Response: " + status + ", URL: " + url); return null; } } private void onRetry() { } private void onError(int status) { } public HeroProfile readHeroProfile(Realm realm, String name, int code, int id) throws JsonParseException, IOException { ObjectMapper mapper = new ObjectMapper(); throttle(); HeroProfile out = readValue(mapper, new URL(UrlHelper.heroProfileUrl(realm.getApiHost(), name, code, id) + getApiKey()), HeroProfile.class); return out; } public ItemInformation readItemInformation(Realm realm, String tooltipParams) throws JsonParseException, IOException { URL url = new URL(UrlHelper.itemInformationUrl(this.itemRealm, tooltipParams) + getApiKey()); String urlString = url.toExternalForm(); synchronized (itemCache) { ItemInformation item = itemCache.get(urlString); if (item != null) { // log.info("Item Cache hit: " + server + "/" + tooltipParams); cacheHit++; return item; } else { cacheMiss++; // log.info("Item Cache miss: " + server + "/" + tooltipParams); } } ObjectMapper mapper = new ObjectMapper(); throttle(); ItemInformation out = readValue(mapper, url, ItemInformation.class); if (out.code == null) { synchronized (itemCache) { itemCache.put(urlString, out); } newItem(out); } return out; } protected void newItem(ItemInformation item) { } public SeasonIndex readSeasonIndex(Realm realm) throws JsonParseException, IOException { URL url = new URL(UrlHelper.seasonIndexUrl(realm.getApiHost()) + getAccessToken()); ObjectMapper mapper = new ObjectMapper(); throttle(); SeasonIndex out = readValue(mapper, url, SeasonIndex.class); return out; } public Season readSeason(Realm realm, int season) throws JsonParseException, IOException { URL url = new URL(UrlHelper.seasonUrl(realm.getApiHost(), season) + getAccessToken()); ObjectMapper mapper = new ObjectMapper(); throttle(); Season out = readValue(mapper, url, Season.class); return out; } public Leaderboard readSeasonLeaderboard(Realm realm, int season, String leaderboard) throws JsonParseException, IOException { URL url = new URL( UrlHelper.seasonLeaderboardUrl(realm.getApiHost(), season, leaderboard) + getAccessToken()); ObjectMapper mapper = new ObjectMapper(); throttle(); Leaderboard out = readValue(mapper, url, Leaderboard.class); return out; } public SeasonIndex readEraIndex(Realm realm) throws JsonParseException, IOException { URL url = new URL(UrlHelper.eraIndexUrl(realm.getApiHost()) + getAccessToken()); ObjectMapper mapper = new ObjectMapper(); throttle(); SeasonIndex out = readValue(mapper, url, SeasonIndex.class); return out; } public Season readEra(Realm realm, int era) throws JsonParseException, IOException { URL url = new URL(UrlHelper.eraUrl(realm.getApiHost(), era) + getAccessToken()); ObjectMapper mapper = new ObjectMapper(); throttle(); Season out = readValue(mapper, url, Season.class); return out; } public Leaderboard readEraLeaderboard(Realm realm, int era, String leaderboard) throws JsonParseException, IOException { URL url = new URL(UrlHelper.eraLeaderboardUrl(realm.getApiHost(), era, leaderboard) + getAccessToken()); ObjectMapper mapper = new ObjectMapper(); throttle(); Leaderboard out = readValue(mapper, url, Leaderboard.class); return out; } public long getErrors() { return errors; } public long getRetryAttempts() { return retryAttempts; } }