diff --git a/webapp/src/main/kotlin/org/jeudego/pairgoth/api/PairingHandler.kt b/webapp/src/main/kotlin/org/jeudego/pairgoth/api/PairingHandler.kt index e12351e..e997637 100644 --- a/webapp/src/main/kotlin/org/jeudego/pairgoth/api/PairingHandler.kt +++ b/webapp/src/main/kotlin/org/jeudego/pairgoth/api/PairingHandler.kt @@ -4,9 +4,9 @@ import com.republicate.kson.Json import com.republicate.kson.toJsonArray import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest import org.jeudego.pairgoth.model.Pairing -import org.jeudego.pairgoth.model.Tournament import org.jeudego.pairgoth.model.toJson -import org.jeudego.pairgoth.store.Store +import org.jeudego.pairgoth.web.Event +import org.jeudego.pairgoth.web.Event.* import javax.servlet.http.HttpServletRequest object PairingHandler: PairgothApiHandler { @@ -42,6 +42,27 @@ object PairingHandler: PairgothApiHandler { } ?: badRequest("invalid pairable id: #$id") } val games = tournament.pair(round, pairables) - return games.map { it.toJson() }.toJsonArray() + val ret = games.map { it.toJson() }.toJsonArray() + Event.dispatch(gamesAdded, Json.Object("tournament" to tournament.id, "round" to round, "data" to ret)) + return ret + } + + override fun delete(request: HttpServletRequest): Json { + val tournament = getTournament(request) + val round = getSubSelector(request)?.toIntOrNull() ?: badRequest("invalid round number") + // only allow last round (if players have not been paired in the last round, it *may* be possible to be more laxist...) + if (round != tournament.games.size) badRequest("cannot delete games in other rounds but the last") + val payload = getArrayPayload(request) + val allPlayers = payload.size == 1 && payload[0] == "all" + if (allPlayers) { + tournament.games.removeLast() + } else { + payload.forEach { + val id = (it as Number).toInt() + tournament.games[round].remove(id) + } + } + Event.dispatch(gamesDeleted, Json.Object("tournament" to tournament.id, "round" to round, "data" to payload)) + return Json.Object("success" to true) } } diff --git a/webapp/src/main/kotlin/org/jeudego/pairgoth/api/PlayerHandler.kt b/webapp/src/main/kotlin/org/jeudego/pairgoth/api/PlayerHandler.kt index dd8d0d9..70691f5 100644 --- a/webapp/src/main/kotlin/org/jeudego/pairgoth/api/PlayerHandler.kt +++ b/webapp/src/main/kotlin/org/jeudego/pairgoth/api/PlayerHandler.kt @@ -5,6 +5,8 @@ import com.republicate.kson.toJsonArray import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest import org.jeudego.pairgoth.model.Player import org.jeudego.pairgoth.model.fromJson +import org.jeudego.pairgoth.web.Event +import org.jeudego.pairgoth.web.Event.* import javax.servlet.http.HttpServletRequest object PlayerHandler: PairgothApiHandler { @@ -23,7 +25,7 @@ object PlayerHandler: PairgothApiHandler { // player parsing (CB TODO - team handling, based on tournament type) val player = Player.fromJson(payload) tournament.pairables[player.id] = player - // CB TODO - handle event broadcasting + Event.dispatch(playerAdded, Json.Object("tournament" to tournament.id, "data" to player.toJson())) return Json.Object("success" to true, "id" to player.id) } @@ -34,10 +36,16 @@ object PlayerHandler: PairgothApiHandler { val payload = getObjectPayload(request) val updated = Player.fromJson(payload, player as Player) tournament.pairables[updated.id] = updated + Event.dispatch(playerUpdated, Json.Object("tournament" to tournament.id, "data" to player.toJson())) return Json.Object("success" to true) } override fun delete(request: HttpServletRequest): Json { - return super.delete(request) + val tournament = getTournament(request) ?: badRequest("invalid tournament") + val id = getSubSelector(request)?.toIntOrNull() ?: badRequest("missing or invalid player selector") + val player = tournament.pairables[id] ?: badRequest("invalid player id") + tournament.pairables.remove(id) + Event.dispatch(playerDeleted, Json.Object("tournament" to tournament.id, "data" to id)) + return Json.Object("success" to true) } } diff --git a/webapp/src/main/kotlin/org/jeudego/pairgoth/api/ResultsHandler.kt b/webapp/src/main/kotlin/org/jeudego/pairgoth/api/ResultsHandler.kt index 1dbe1a6..ed3b869 100644 --- a/webapp/src/main/kotlin/org/jeudego/pairgoth/api/ResultsHandler.kt +++ b/webapp/src/main/kotlin/org/jeudego/pairgoth/api/ResultsHandler.kt @@ -7,6 +7,7 @@ import org.jeudego.pairgoth.model.Game import org.jeudego.pairgoth.model.Tournament import org.jeudego.pairgoth.model.toJson import org.jeudego.pairgoth.store.Store +import org.jeudego.pairgoth.web.Event import javax.servlet.http.HttpServletRequest object ResultsHandler: PairgothApiHandler { @@ -18,30 +19,13 @@ object ResultsHandler: PairgothApiHandler { return games.map { it.toJson() }.toJsonArray() } - override fun post(request: HttpServletRequest): Json { - val tournament = getTournament(request) - val round = getSubSelector(request)?.toIntOrNull() ?: badRequest("invalid round number") - val payload = getObjectPayload(request) - val game = tournament.games[round][payload.getInt("id")] ?: badRequest("invalid game id") - game.result = Game.Result.valueOf(payload.getString("result") ?: badRequest("missing result")) - return Json.Object("success" to true) - } - override fun put(request: HttpServletRequest): Json { val tournament = getTournament(request) val round = getSubSelector(request)?.toIntOrNull() ?: badRequest("invalid round number") val payload = getObjectPayload(request) val game = tournament.games[round][payload.getInt("id")] ?: badRequest("invalid game id") game.result = Game.Result.valueOf(payload.getString("result") ?: badRequest("missing result")) - return Json.Object("success" to true) - } - - override fun delete(request: HttpServletRequest): Json { - val tournament = getTournament(request) - val round = getSubSelector(request)?.toIntOrNull() ?: badRequest("invalid round number") - val payload = getObjectPayload(request) - val game = tournament.games[round][payload.getInt("id")] ?: badRequest("invalid game id") - tournament.games[round].remove(payload.getInt("id") ?: badRequest("invalid game id")) ?: badRequest("invalid game id") + Event.dispatch(Event.resultUpdated, Json.Object("tournament" to tournament.id, "round" to round, "data" to game)) return Json.Object("success" to true) } } diff --git a/webapp/src/main/kotlin/org/jeudego/pairgoth/api/TournamentHandler.kt b/webapp/src/main/kotlin/org/jeudego/pairgoth/api/TournamentHandler.kt index a9f2c06..715ee7a 100644 --- a/webapp/src/main/kotlin/org/jeudego/pairgoth/api/TournamentHandler.kt +++ b/webapp/src/main/kotlin/org/jeudego/pairgoth/api/TournamentHandler.kt @@ -8,6 +8,8 @@ import org.jeudego.pairgoth.model.Tournament import org.jeudego.pairgoth.model.fromJson import org.jeudego.pairgoth.model.toJson import org.jeudego.pairgoth.store.Store +import org.jeudego.pairgoth.web.Event +import org.jeudego.pairgoth.web.Event.* import org.w3c.dom.Element import javax.servlet.http.HttpServletRequest @@ -26,8 +28,8 @@ object TournamentHandler: PairgothApiHandler { is Element -> OpenGotha.import(payload) else -> badRequest("missing or invalid payload") } - Store.addTournament(tournament) + Event.dispatch(tournamentAdded, tournament.toJson()) return Json.Object("success" to true, "id" to tournament.id) } @@ -42,12 +44,14 @@ object TournamentHandler: PairgothApiHandler { updated.games.addAll(tournament.games) updated.criteria.addAll(tournament.criteria) Store.replaceTournament(updated) + Event.dispatch(tournamentUpdated, tournament.toJson()) return Json.Object("success" to true) } override fun delete(request: HttpServletRequest): Json { val tournament = getTournament(request) ?: badRequest("missing or invalid tournament id") Store.deleteTournament(tournament) + Event.dispatch(tournamentDeleted, tournament.id) return Json.Object("success" to true) } } diff --git a/webapp/src/main/kotlin/org/jeudego/pairgoth/web/Event.kt b/webapp/src/main/kotlin/org/jeudego/pairgoth/web/Event.kt new file mode 100644 index 0000000..9ead03f --- /dev/null +++ b/webapp/src/main/kotlin/org/jeudego/pairgoth/web/Event.kt @@ -0,0 +1,27 @@ +package org.jeudego.pairgoth.web + +import info.macias.sse.events.MessageEvent + +enum class Event { + tournamentAdded, + tournamentUpdated, + tournamentDeleted, + playerAdded, + playerUpdated, + playerDeleted, + gamesAdded, + gamesDeleted, + resultUpdated, + ; + + companion object { + private val sse: SSEServlet by lazy { SSEServlet.getInstance() } + private fun buildEvent(event: Event, data: T) = MessageEvent.Builder() + .setEvent(event.name) + .setData(data.toString()) + .build() + internal fun dispatch(event: Event, data: T) { + sse.broadcast(buildEvent(event, data)) + } + } +} diff --git a/webapp/src/main/kotlin/org/jeudego/pairgoth/web/sse/SSEServlet.kt b/webapp/src/main/kotlin/org/jeudego/pairgoth/web/SSEServlet.kt similarity index 59% rename from webapp/src/main/kotlin/org/jeudego/pairgoth/web/sse/SSEServlet.kt rename to webapp/src/main/kotlin/org/jeudego/pairgoth/web/SSEServlet.kt index c294f1d..cb8b02c 100644 --- a/webapp/src/main/kotlin/org/jeudego/pairgoth/web/sse/SSEServlet.kt +++ b/webapp/src/main/kotlin/org/jeudego/pairgoth/web/SSEServlet.kt @@ -1,6 +1,7 @@ -package org.jeudego.pairgoth.web.sse +package org.jeudego.pairgoth.web import info.macias.sse.EventBroadcast +import info.macias.sse.events.MessageEvent import info.macias.sse.servlet3.ServletEventTarget import org.slf4j.LoggerFactory import javax.servlet.http.HttpServlet @@ -11,6 +12,12 @@ import javax.servlet.http.HttpServletResponse class SSEServlet: HttpServlet() { companion object { private val logger = LoggerFactory.getLogger("sse") + private var zeInstance: SSEServlet? = null + internal fun getInstance(): SSEServlet = zeInstance ?: throw Error("SSE servlet not ready") + } + init { + if (zeInstance != null) throw Error("Multiple instances of SSE servlet found!") + zeInstance = this } private val broadcast = EventBroadcast() @@ -18,4 +25,6 @@ class SSEServlet: HttpServlet() { logger.trace("<< new channel") broadcast.addSubscriber(ServletEventTarget(req), req.getHeader("Last-Event-Id")) } + + internal fun broadcast(message: MessageEvent) = broadcast.broadcast(message) } diff --git a/webapp/src/main/webapp/WEB-INF/web.xml b/webapp/src/main/webapp/WEB-INF/web.xml index 474232b..efb7db6 100644 --- a/webapp/src/main/webapp/WEB-INF/web.xml +++ b/webapp/src/main/webapp/WEB-INF/web.xml @@ -33,7 +33,7 @@ sse - org.jeudego.pairgoth.web.sse.SSEServlet + org.jeudego.pairgoth.web.SSEServlet 1 true diff --git a/webapp/src/test/kotlin/TestUtils.kt b/webapp/src/test/kotlin/TestUtils.kt index ab2b22d..7d6dd78 100644 --- a/webapp/src/test/kotlin/TestUtils.kt +++ b/webapp/src/test/kotlin/TestUtils.kt @@ -3,6 +3,7 @@ package org.jeudego.pairgoth.test import com.republicate.kson.Json import org.jeudego.pairgoth.api.ApiHandler import org.jeudego.pairgoth.web.ApiServlet +import org.jeudego.pairgoth.web.SSEServlet import org.jeudego.pairgoth.web.WebappManager import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.doAnswer @@ -31,6 +32,7 @@ object TestAPI { fun Any?.toUnit() = Unit private val apiServlet = ApiServlet() + private val sseServlet = SSEServlet() private fun testRequest(reqMethod: String, uri: String, payload: T? = null): Json {