package logbook.server.proxy;
import java.io.UnsupportedEncodingException;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import logbook.config.AppConfig;
import logbook.constants.AppConstants;
import logbook.data.UndefinedData;
import logbook.gui.ApplicationMain;
import logbook.internal.LoggerHolder;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.util.StringContentProvider;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.util.UrlEncoded;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.eclipse.swt.widgets.Display;
/**
* 艦これ統計データベースに送信する
* @author Nekopanda
*/
public class DatabaseClient extends Thread {
private static final LoggerHolder LOG = new LoggerHolder(DatabaseClient.class);
private static DatabaseClient instance = null;
private static final String[] sendDatabaseUrls = new String[]
{
"api_port/port",
"api_get_member/kdock",
"api_get_member/ship2",
"api_get_member/ship3",
"api_get_member/slot_item",
"api_get_member/mapinfo",
"api_req_hensei/change",
"api_req_kousyou/createship",
"api_req_kousyou/getship",
"api_req_kousyou/createitem",
"api_req_map/start",
"api_req_map/next",
"api_req_map/select_eventmap_rank",
"api_req_sortie/battle",
"api_req_battle_midnight/battle",
"api_req_battle_midnight/sp_midnight",
"api_req_sortie/night_to_day",
"api_req_sortie/battleresult",
"api_req_combined_battle/airbattle",
"api_req_combined_battle/battle",
"api_req_combined_battle/midnight_battle",
"api_req_combined_battle/sp_midnight",
"api_req_combined_battle/battleresult",
"api_req_sortie/airbattle",
"api_req_combined_battle/battle_water",
"api_req_combined_battle/sp_midnight"
};
private static class QueueItem {
public UndefinedData data;
public QueueItem(UndefinedData data) {
this.data = data;
}
}
private static synchronized DatabaseClient getInstance() {
if (instance == null) {
instance = new DatabaseClient();
instance.start();
}
return instance;
}
public static void send(UndefinedData data) {
if (AppConfig.get().isSendDatabase() && (AppConfig.get().getAccessKey().length() > 0)) {
for (String entry : sendDatabaseUrls)
{
if (data.getUrl().endsWith(entry))
{
getInstance().dataQueue.offer(new QueueItem(data));
break;
}
}
}
}
public static synchronized void end() {
if (instance != null) {
instance.endRequested = true;
instance.dataQueue.offer(new QueueItem(null));
try {
instance.join();
instance = null;
} catch (InterruptedException e) {
LOG.get().fatal("DatabaseClientスレッド終了時に何かのエラー", e);
}
}
}
/** この正規表現イミフになりつつある・・・ */
private final Pattern apiTokenPattern = Pattern
.compile("&api(_|%5F)token=[0-9a-f]+|api(_|%5F)token=[0-9a-f]+&?");
private final BlockingQueue<QueueItem> dataQueue = new ArrayBlockingQueue<>(32);
private HttpClient httpClient = null;
private boolean endRequested = false;
private Request createRequest(UndefinedData data) throws UnsupportedEncodingException {
// api_tokenを取り除く
String origRequest = new String(data.getRequest(), "UTF-8");
String sendRequestBody = this.apiTokenPattern.matcher(origRequest).replaceAll("");
String sendResponseBody = new String(data.getResponse(), "UTF-8");
UrlEncoded body = new UrlEncoded();
body.add("token", AppConfig.get().getAccessKey());
// このクライアントのエージェントキー
body.add("agent", "6nENnnGzRgSTVeuU652r");
body.add("url", data.getFullUrl());
body.add("requestbody", sendRequestBody);
body.add("responsebody", sendResponseBody); //
return this.httpClient.POST("http://api.kancolle-db.net/2/")
.agent("logbook/v" + AppConstants.VERSION)
.content(new StringContentProvider(body.encode()), "application/x-www-form-urlencoded");
}
/* (非 Javadoc)
* @see java.lang.Thread#run()
*/
@Override
public void run() {
try {
int skipCount = 0;
int errorCount = 0;
this.httpClient = new HttpClient();
this.httpClient.setExecutor(new QueuedThreadPool(2, 1));
this.httpClient.setMaxConnectionsPerDestination(2);
this.httpClient.start();
while (true) {
final UndefinedData data = this.dataQueue.take().data;
if (this.endRequested) {
return;
}
if (skipCount > 0) {
--skipCount;
continue;
}
for (int retly = 0;; ++retly) {
String errorReason = null;
try {
// 60秒でタイムアウト
Request request = this.createRequest(data).timeout(60, TimeUnit.SECONDS);
ContentResponse response = request.send();
if (this.endRequested) {
return;
}
if (HttpStatus.isSuccess(response.getStatus())) {
// 成功したらエラーカウンタをリセット
skipCount = errorCount = 0;
// ログに出す
if (AppConfig.get().isDatabaseSendLog()) {
Display.getDefault().asyncExec(new Runnable() {
@Override
public void run() {
if (!ApplicationMain.main.getShell().isDisposed()) {
String url = data.getUrl();
ApplicationMain.main.printMessage("DBへ送信しました("
+ url.substring(url.lastIndexOf('/') + 1) + ")");
}
}
});
}
break;
}
else {
errorReason = response.getReason();
}
} catch (Exception e) {
errorReason = e.getMessage();
}
if (errorReason != null) {
// 少し時間をおいてリトライ
Thread.sleep(1000);
if (this.endRequested) {
return;
}
if (retly >= 4) {
// リトライが多すぎたらエラーにする
skipCount = (errorCount++) * 4;
LOG.get().warn("データベースへの送信に失敗しました. " + errorReason);
if (skipCount > 0) {
LOG.get().warn("以降 " + skipCount + " 個の送信をスキップします.");
}
break;
}
}
}
}
} catch (Exception e) {
if (!this.endRequested) {
LOG.get().fatal("スレッドが異常終了しました", e);
}
} finally {
if (this.httpClient != null) {
try {
this.httpClient.stop();
} catch (Exception e) {
LOG.get().fatal("HttpClientの終了に失敗", e);
}
}
}
}
}