Events model and SSE servlet ready

This commit is contained in:
Claude Brisson
2023-05-19 18:54:39 +02:00
parent 45873d6014
commit 605b39123e
8 changed files with 81 additions and 26 deletions

View File

@@ -4,9 +4,9 @@ import com.republicate.kson.Json
import com.republicate.kson.toJsonArray import com.republicate.kson.toJsonArray
import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest
import org.jeudego.pairgoth.model.Pairing import org.jeudego.pairgoth.model.Pairing
import org.jeudego.pairgoth.model.Tournament
import org.jeudego.pairgoth.model.toJson 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 import javax.servlet.http.HttpServletRequest
object PairingHandler: PairgothApiHandler { object PairingHandler: PairgothApiHandler {
@@ -42,6 +42,27 @@ object PairingHandler: PairgothApiHandler {
} ?: badRequest("invalid pairable id: #$id") } ?: badRequest("invalid pairable id: #$id")
} }
val games = tournament.pair(round, pairables) 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)
} }
} }

View File

@@ -5,6 +5,8 @@ import com.republicate.kson.toJsonArray
import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest
import org.jeudego.pairgoth.model.Player import org.jeudego.pairgoth.model.Player
import org.jeudego.pairgoth.model.fromJson import org.jeudego.pairgoth.model.fromJson
import org.jeudego.pairgoth.web.Event
import org.jeudego.pairgoth.web.Event.*
import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletRequest
object PlayerHandler: PairgothApiHandler { object PlayerHandler: PairgothApiHandler {
@@ -23,7 +25,7 @@ object PlayerHandler: PairgothApiHandler {
// player parsing (CB TODO - team handling, based on tournament type) // player parsing (CB TODO - team handling, based on tournament type)
val player = Player.fromJson(payload) val player = Player.fromJson(payload)
tournament.pairables[player.id] = player 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) return Json.Object("success" to true, "id" to player.id)
} }
@@ -34,10 +36,16 @@ object PlayerHandler: PairgothApiHandler {
val payload = getObjectPayload(request) val payload = getObjectPayload(request)
val updated = Player.fromJson(payload, player as Player) val updated = Player.fromJson(payload, player as Player)
tournament.pairables[updated.id] = updated tournament.pairables[updated.id] = updated
Event.dispatch(playerUpdated, Json.Object("tournament" to tournament.id, "data" to player.toJson()))
return Json.Object("success" to true) return Json.Object("success" to true)
} }
override fun delete(request: HttpServletRequest): Json { 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)
} }
} }

View File

@@ -7,6 +7,7 @@ import org.jeudego.pairgoth.model.Game
import org.jeudego.pairgoth.model.Tournament import org.jeudego.pairgoth.model.Tournament
import org.jeudego.pairgoth.model.toJson import org.jeudego.pairgoth.model.toJson
import org.jeudego.pairgoth.store.Store import org.jeudego.pairgoth.store.Store
import org.jeudego.pairgoth.web.Event
import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletRequest
object ResultsHandler: PairgothApiHandler { object ResultsHandler: PairgothApiHandler {
@@ -18,30 +19,13 @@ object ResultsHandler: PairgothApiHandler {
return games.map { it.toJson() }.toJsonArray() 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 { override fun put(request: HttpServletRequest): Json {
val tournament = getTournament(request) val tournament = getTournament(request)
val round = getSubSelector(request)?.toIntOrNull() ?: badRequest("invalid round number") val round = getSubSelector(request)?.toIntOrNull() ?: badRequest("invalid round number")
val payload = getObjectPayload(request) val payload = getObjectPayload(request)
val game = tournament.games[round][payload.getInt("id")] ?: badRequest("invalid game id") val game = tournament.games[round][payload.getInt("id")] ?: badRequest("invalid game id")
game.result = Game.Result.valueOf(payload.getString("result") ?: badRequest("missing result")) game.result = Game.Result.valueOf(payload.getString("result") ?: badRequest("missing result"))
return Json.Object("success" to true) Event.dispatch(Event.resultUpdated, Json.Object("tournament" to tournament.id, "round" to round, "data" to game))
}
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")
return Json.Object("success" to true) return Json.Object("success" to true)
} }
} }

View File

@@ -8,6 +8,8 @@ import org.jeudego.pairgoth.model.Tournament
import org.jeudego.pairgoth.model.fromJson import org.jeudego.pairgoth.model.fromJson
import org.jeudego.pairgoth.model.toJson import org.jeudego.pairgoth.model.toJson
import org.jeudego.pairgoth.store.Store import org.jeudego.pairgoth.store.Store
import org.jeudego.pairgoth.web.Event
import org.jeudego.pairgoth.web.Event.*
import org.w3c.dom.Element import org.w3c.dom.Element
import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletRequest
@@ -26,8 +28,8 @@ object TournamentHandler: PairgothApiHandler {
is Element -> OpenGotha.import(payload) is Element -> OpenGotha.import(payload)
else -> badRequest("missing or invalid payload") else -> badRequest("missing or invalid payload")
} }
Store.addTournament(tournament) Store.addTournament(tournament)
Event.dispatch(tournamentAdded, tournament.toJson())
return Json.Object("success" to true, "id" to tournament.id) return Json.Object("success" to true, "id" to tournament.id)
} }
@@ -42,12 +44,14 @@ object TournamentHandler: PairgothApiHandler {
updated.games.addAll(tournament.games) updated.games.addAll(tournament.games)
updated.criteria.addAll(tournament.criteria) updated.criteria.addAll(tournament.criteria)
Store.replaceTournament(updated) Store.replaceTournament(updated)
Event.dispatch(tournamentUpdated, tournament.toJson())
return Json.Object("success" to true) return Json.Object("success" to true)
} }
override fun delete(request: HttpServletRequest): Json { override fun delete(request: HttpServletRequest): Json {
val tournament = getTournament(request) ?: badRequest("missing or invalid tournament id") val tournament = getTournament(request) ?: badRequest("missing or invalid tournament id")
Store.deleteTournament(tournament) Store.deleteTournament(tournament)
Event.dispatch(tournamentDeleted, tournament.id)
return Json.Object("success" to true) return Json.Object("success" to true)
} }
} }

View File

@@ -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 <T> buildEvent(event: Event, data: T) = MessageEvent.Builder()
.setEvent(event.name)
.setData(data.toString())
.build()
internal fun <T> dispatch(event: Event, data: T) {
sse.broadcast(buildEvent(event, data))
}
}
}

View File

@@ -1,6 +1,7 @@
package org.jeudego.pairgoth.web.sse package org.jeudego.pairgoth.web
import info.macias.sse.EventBroadcast import info.macias.sse.EventBroadcast
import info.macias.sse.events.MessageEvent
import info.macias.sse.servlet3.ServletEventTarget import info.macias.sse.servlet3.ServletEventTarget
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import javax.servlet.http.HttpServlet import javax.servlet.http.HttpServlet
@@ -11,6 +12,12 @@ import javax.servlet.http.HttpServletResponse
class SSEServlet: HttpServlet() { class SSEServlet: HttpServlet() {
companion object { companion object {
private val logger = LoggerFactory.getLogger("sse") 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() private val broadcast = EventBroadcast()
@@ -18,4 +25,6 @@ class SSEServlet: HttpServlet() {
logger.trace("<< new channel") logger.trace("<< new channel")
broadcast.addSubscriber(ServletEventTarget(req), req.getHeader("Last-Event-Id")) broadcast.addSubscriber(ServletEventTarget(req), req.getHeader("Last-Event-Id"))
} }
internal fun broadcast(message: MessageEvent) = broadcast.broadcast(message)
} }

View File

@@ -33,7 +33,7 @@
</servlet> </servlet>
<servlet> <servlet>
<servlet-name>sse</servlet-name> <servlet-name>sse</servlet-name>
<servlet-class>org.jeudego.pairgoth.web.sse.SSEServlet</servlet-class> <servlet-class>org.jeudego.pairgoth.web.SSEServlet</servlet-class>
<load-on-startup>1</load-on-startup> <load-on-startup>1</load-on-startup>
<async-supported>true</async-supported> <async-supported>true</async-supported>
</servlet> </servlet>

View File

@@ -3,6 +3,7 @@ package org.jeudego.pairgoth.test
import com.republicate.kson.Json import com.republicate.kson.Json
import org.jeudego.pairgoth.api.ApiHandler import org.jeudego.pairgoth.api.ApiHandler
import org.jeudego.pairgoth.web.ApiServlet import org.jeudego.pairgoth.web.ApiServlet
import org.jeudego.pairgoth.web.SSEServlet
import org.jeudego.pairgoth.web.WebappManager import org.jeudego.pairgoth.web.WebappManager
import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.doAnswer import org.mockito.kotlin.doAnswer
@@ -31,6 +32,7 @@ object TestAPI {
fun Any?.toUnit() = Unit fun Any?.toUnit() = Unit
private val apiServlet = ApiServlet() private val apiServlet = ApiServlet()
private val sseServlet = SSEServlet()
private fun <T> testRequest(reqMethod: String, uri: String, payload: T? = null): Json { private fun <T> testRequest(reqMethod: String, uri: String, payload: T? = null): Json {