package tv.dyndns.kishibe.qmaclone.client;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.zschech.gwt.websockets.client.CloseHandler;
import net.zschech.gwt.websockets.client.ErrorHandler;
import net.zschech.gwt.websockets.client.MessageEvent;
import net.zschech.gwt.websockets.client.MessageHandler;
import net.zschech.gwt.websockets.client.WebSocket;
import tv.dyndns.kishibe.qmaclone.client.constant.Constant;
import tv.dyndns.kishibe.qmaclone.client.packet.PacketUserData.WebSocketUsage;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.gwt.core.client.JavaScriptException;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.RepeatingCommand;
import com.google.gwt.user.client.rpc.AsyncCallback;
/**
* ポーリングを行うための補助クラス。タイマー+RPCによるポーリングとWebSocketによるポーリングエミュレーションの両方に対応している。
*
* @author nodchip
* @param <T>
*/
public abstract class StatusUpdater<T> {
@VisibleForTesting
enum Status {
WEB_SOCKET, RPC, CLOSED
}
private static final Logger logger = Logger.getLogger(StatusUpdater.class.getName());
private static final int ENABLED_USER_CODE_LOWER_DIGIT = 10;
private static final int MAX_WEBSOCKET_FAILED_COUNT = 2;
private static final int MAX_RESPONSE_RECIEVE_FAILED_COUNT = 5;
private final String path;
private final int intervalMs;
private WebSocket webSocket;
private int webSocketFailedCount = 0;
private int responseRecieveFailedCount = 0;
@VisibleForTesting
Status status;
@VisibleForTesting
final RepeatingCommand commandUpdate = new RepeatingCommand() {
@Override
public boolean execute() {
if (status != Status.RPC) {
return false;
}
try {
request(callback);
} catch (Exception e) {
logger.log(Level.WARNING, "リクエスト中にエラーが発生しました", e);
}
return true;
}
};
@VisibleForTesting
final AsyncCallback<T> callback = new AsyncCallback<T>() {
@Override
public void onSuccess(T result) {
try {
onReceived(result);
} catch (Exception e) {
logger.log(Level.WARNING, "レスポンス処理中にエラーが発生しました(RPC)", e);
}
};
@Override
public void onFailure(Throwable caught) {
if (++responseRecieveFailedCount < MAX_RESPONSE_RECIEVE_FAILED_COUNT) {
logger.log(Level.WARNING, "レスポンス取得中にエラーが発生しました", caught);
} else {
logger.log(Level.SEVERE,
"レスポンス取得中にエラーが発生しました。致命的なエラーを避けるため通信を終了します。ページをリロードして下さい。", caught);
stop();
}
}
};
public StatusUpdater(String path, int intervalMs) {
this.path = Preconditions.checkNotNull(path);
this.intervalMs = intervalMs;
}
public void start() {
if (isWebSocketUsed()) {
try {
webSocket = WebSocket.create(Constant.WEB_SOCKET_URL + path);
} catch (JavaScriptException e) {
// WebSocket is not supported.
}
}
if (webSocket == null) {
status = Status.RPC;
Scheduler.get().scheduleFixedDelay(commandUpdate, intervalMs);
} else {
status = Status.WEB_SOCKET;
webSocket.setOnMessage(messageHandler);
webSocket.setOnClose(closeHandler);
webSocket.setOnError(errorHandler);
}
}
@VisibleForTesting
boolean isWebSocketUsed() {
WebSocketUsage webSocketUsage = UserData.get().getWebSocketUsage();
return webSocketUsage == WebSocketUsage.On || webSocketUsage == WebSocketUsage.Default
&& UserData.get().getUserCode() % 10 <= ENABLED_USER_CODE_LOWER_DIGIT;
}
private final MessageHandler messageHandler = new MessageHandler() {
@Override
public void onMessage(WebSocket webSocket, MessageEvent event) {
String data = event.getData();
// pingは無視する
if (data.isEmpty()) {
return;
}
// System.out.println(data);
T status;
try {
status = parse(data);
} catch (Exception e) {
if (++webSocketFailedCount >= MAX_WEBSOCKET_FAILED_COUNT) {
logger.log(Level.WARNING, "レスポンスのパース中にエラーが発生しました。フォールバックします。", e);
fallback(webSocket);
} else {
logger.log(Level.WARNING, "レスポンスのパース中にエラーが発生しました。続行します。", e);
}
return;
}
try {
onReceived(status);
} catch (Exception e) {
if (++webSocketFailedCount >= MAX_WEBSOCKET_FAILED_COUNT) {
logger.log(Level.WARNING, "レスポンス処理中にエラーが発生しました(WebSocket)。フォールバックします。", e);
fallback(webSocket);
} else {
logger.log(Level.WARNING, "レスポンス処理中にエラーが発生しました(WebSocket)。続行します。", e);
}
}
}
};
private final CloseHandler closeHandler = new CloseHandler() {
@Override
public void onClose(WebSocket webSocket) {
if (status == Status.WEB_SOCKET) {
fallback(webSocket);
}
}
};
private final ErrorHandler errorHandler = new ErrorHandler() {
@Override
public void onError(WebSocket webSocket) {
if (++webSocketFailedCount >= MAX_WEBSOCKET_FAILED_COUNT) {
logger.log(Level.WARNING, "WebSocket通信中にエラーが発生しました。フォールバックします。");
fallback(webSocket);
} else {
logger.log(Level.WARNING, "WebSocket通信中にエラーが発生しました。続行します。");
}
}
};
private void fallback(WebSocket webSocket) {
if (status == Status.WEB_SOCKET) {
status = Status.RPC;
webSocket.close();
webSocket = null;
Scheduler.get().scheduleFixedDelay(commandUpdate, intervalMs);
}
}
public void stop() {
if (status == Status.WEB_SOCKET) {
status = Status.CLOSED;
webSocket.close();
webSocket = null;
}
status = Status.CLOSED;
}
/**
* RPCによるリクエストを行う。コールバックメソッドとして第一引数のcallbackを渡さなければならない。
*
* @param callback
*/
protected abstract void request(AsyncCallback<T> callback);
/**
* メッセージを取得した後の動作を行う
*
* @param status
* ステータス
*/
protected abstract void onReceived(T status);
/**
* jsonをパースしてメッセージに変換する
*
* @param json
* json文字列
* @return メッセージ
*/
protected abstract T parse(String json);
}