From c37023405fe058eacec866a857d48f38c9576987 Mon Sep 17 00:00:00 2001 From: Claude Brisson Date: Sun, 14 May 2023 16:34:02 +0200 Subject: [PATCH] Players registration --- test.sh | 24 ++++++++-- .../org/jeudego/pairgoth/api/ApiHandler.kt | 19 +++++++- .../org/jeudego/pairgoth/api/PlayerHandler.kt | 22 +++++++--- .../pairgoth/api/RegistrationHandler.kt | 43 ++++++++++++++++++ .../jeudego/pairgoth/api/TournamentHandler.kt | 6 +-- .../org/jeudego/pairgoth/model/Pairable.kt | 4 +- .../org/jeudego/pairgoth/model/Tournament.kt | 5 ++- .../org/jeudego/pairgoth/store/Store.kt | 5 +++ .../org/jeudego/pairgoth/web/ApiServlet.kt | 44 ++++++++++++------- .../src/main/webapp/WEB-INF/logger.properties | 2 +- 10 files changed, 140 insertions(+), 34 deletions(-) create mode 100644 webapp/src/main/kotlin/org/jeudego/pairgoth/api/RegistrationHandler.kt diff --git a/test.sh b/test.sh index eafa7ec..155e247 100755 --- a/test.sh +++ b/test.sh @@ -1,11 +1,27 @@ #!/bin/bash -curl -s -D - --header "Accept: application/json" http://localhost:8080/api/tournament - curl -s --header "Accept: application/json" --header "Content-Type: application/json" \ --request POST \ - --data '{"type":"INDIVIDUAL","name":"Mon Tournoi", "shortName": "mon-tournoi", "startDate": "2023-05-10", "endDate": "2023-05-12", "country": "FR", "location": "Marseille", "online": false, "timeSystem": { "type": "fisher", "mainTime": "1200", "increment": "10" }, "pairing": { "type": "ROUNDROBIN" } }' \ + --data '{ "type":"INDIVIDUAL","name":"Mon Tournoi", "shortName": "mon-tournoi", "startDate": "2023-05-10", "endDate": "2023-05-12", "country": "FR", "location": "Marseille", "online": false, "timeSystem": { "type": "fisher", "mainTime": "1200", "increment": "10" }, "pairing": { "type": "ROUNDROBIN" } }' \ http://localhost:8080/api/tournament -curl -s -D - --header "Accept: application/json" http://localhost:8080/api/tournament/1 +curl -s --header "Accept: application/json" http://localhost:8080/api/tournament + +curl -s --header "Accept: application/json" http://localhost:8080/api/tournament/1 + +curl -s --header "Accept: application/json" --header "Content-Type: application/json" \ + --request POST \ + --data '{ "name": "Burma", "firstname": "Nestor", "rating": 1600, "rank": -2, "country": "FR", "club": "13Ma" }' \ + http://localhost:8080/api/player + +curl -s --header "Accept: application/json" http://localhost:8080/api/player + +curl -s --header "Accept: application/json" --header "Content-Type: application/json" \ + --request POST \ + --data '{ "id": 1 }' \ + http://localhost:8080/api/tournament/1/registration + +curl -s --header "Accept: application/json" http://localhost:8080/api/tournament/1/registration + + diff --git a/webapp/src/main/kotlin/org/jeudego/pairgoth/api/ApiHandler.kt b/webapp/src/main/kotlin/org/jeudego/pairgoth/api/ApiHandler.kt index 8a27be0..17eb25e 100644 --- a/webapp/src/main/kotlin/org/jeudego/pairgoth/api/ApiHandler.kt +++ b/webapp/src/main/kotlin/org/jeudego/pairgoth/api/ApiHandler.kt @@ -38,16 +38,33 @@ interface ApiHandler { } fun getPayload(request: HttpServletRequest): Json { - return request.getAttribute(PAYLOAD_KEY) as Json ?: throw ApiException(HttpServletResponse.SC_BAD_REQUEST, "no payload") + return request.getAttribute(PAYLOAD_KEY) as Json? ?: throw ApiException(HttpServletResponse.SC_BAD_REQUEST, "no payload") + } + + fun getObjectPayload(request: HttpServletRequest): Json.Object { + val json = request.getAttribute(PAYLOAD_KEY) as Json? ?: throw ApiException(HttpServletResponse.SC_BAD_REQUEST, "no payload") + if (!json.isObject) badRequest("expecting a json object") + return json.asObject() + } + + fun getArrayPayload(request: HttpServletRequest): Json.Array { + val json = request.getAttribute(PAYLOAD_KEY) as Json? ?: throw ApiException(HttpServletResponse.SC_BAD_REQUEST, "no payload") + if (!json.isArray) badRequest("expecting a json array") + return json.asArray() } fun getSelector(request: HttpServletRequest): String? { return request.getAttribute(SELECTOR_KEY) as String? } + fun getSubSelector(request: HttpServletRequest): String? { + return request.getAttribute(SUBSELECTOR_KEY) as String? + } + companion object { const val PAYLOAD_KEY = "PAYLOAD" const val SELECTOR_KEY = "SELECTOR" + const val SUBSELECTOR_KEY = "SUBSELECTOR" val logger = LoggerFactory.getLogger("api") fun badRequest(msg: String = "bad request"): Nothing = throw ApiException(HttpServletResponse.SC_BAD_REQUEST, msg) } 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 e933937..c8a168a 100644 --- a/webapp/src/main/kotlin/org/jeudego/pairgoth/api/PlayerHandler.kt +++ b/webapp/src/main/kotlin/org/jeudego/pairgoth/api/PlayerHandler.kt @@ -4,15 +4,14 @@ import com.republicate.kson.Json import org.jeudego.pairgoth.model.Player 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 javax.servlet.http.HttpServletRequest -class PlayerHandler: ApiHandler { +object PlayerHandler: ApiHandler { override fun post(request: HttpServletRequest): Json { - val json = getPayload(request) - if (!json.isObject) ApiHandler.badRequest("expecting a json object") - val payload = json.asObject() + val payload = getObjectPayload(request) // player parsing val player = Player.fromJson(payload) @@ -20,4 +19,17 @@ class PlayerHandler: ApiHandler { Store.addPlayer(player) return Json.Object("success" to true, "id" to player.id) } -} \ No newline at end of file + + override fun get(request: HttpServletRequest): Json { + return when (val id = getSelector(request)?.toIntOrNull()) { + null -> Json.Array(Store.getPlayersIDs()) + else -> Store.getPlayer(id)?.toJson() ?: ApiHandler.badRequest("no player with id #${id}") + } + } + + override fun put(request: HttpServletRequest): Json { + val id = getSelector(request)?.toIntOrNull() ?: ApiHandler.badRequest("missing or invalid player selector") + TODO() + } + +} diff --git a/webapp/src/main/kotlin/org/jeudego/pairgoth/api/RegistrationHandler.kt b/webapp/src/main/kotlin/org/jeudego/pairgoth/api/RegistrationHandler.kt new file mode 100644 index 0000000..6e26cc5 --- /dev/null +++ b/webapp/src/main/kotlin/org/jeudego/pairgoth/api/RegistrationHandler.kt @@ -0,0 +1,43 @@ +package org.jeudego.pairgoth.api + +import com.republicate.kson.Json +import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest +import org.jeudego.pairgoth.model.Tournament +import org.jeudego.pairgoth.store.Store +import javax.servlet.http.HttpServletRequest + +object RegistrationHandler: ApiHandler { + + private fun getTournament(request: HttpServletRequest): Tournament { + val tournamentId = getSelector(request)?.toIntOrNull() ?: badRequest("invalid tournament id") + return Store.getTournament(tournamentId) ?: badRequest("unknown tournament id") + } + + override fun get(request: HttpServletRequest): Json { + val tournament = getTournament(request) + return when (val pairableId = getSubSelector(request)?.toIntOrNull()) { + null -> when (val round = request.getParameter("round")?.toIntOrNull()) { + null -> Json.Array(tournament.pairables.map { + Json.Object( + "id" to it.key, + "skip" to Json.Array(it.value) + ) + }) + else -> Json.Array(tournament.pairables.filter { !it.value.contains(round) }.keys) + } + else -> Json.Array(tournament.pairables[pairableId]) + } + } + + override fun post(request: HttpServletRequest): Json { + val tournament = getTournament(request) + val payload = getObjectPayload(request) + val pairableId = payload.getInt("id") ?: badRequest("missing player id") + val skip = ( payload.getArray("skip") ?: Json.Array() ).map { Json.TypeUtils.toInt(it) ?: badRequest("invalid round number") } + if (tournament.pairables.contains(pairableId)) badRequest("already registered player: $pairableId") + /* CB TODO - update action for SSE channel */ + tournament.pairables[pairableId] = skip.toMutableSet() + 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 fb894b7..7f93c69 100644 --- a/webapp/src/main/kotlin/org/jeudego/pairgoth/api/TournamentHandler.kt +++ b/webapp/src/main/kotlin/org/jeudego/pairgoth/api/TournamentHandler.kt @@ -16,12 +16,10 @@ import org.jeudego.pairgoth.model.toJson import org.jeudego.pairgoth.store.Store import javax.servlet.http.HttpServletRequest -class TournamentHandler(): ApiHandler { +object TournamentHandler: ApiHandler { override fun post(request: HttpServletRequest): Json { - val json = getPayload(request) - if (!json.isObject) badRequest("expecting a json object") - val payload = json.asObject() + val payload = getObjectPayload(request) // tournament parsing val tournament = Tournament.fromJson(payload) diff --git a/webapp/src/main/kotlin/org/jeudego/pairgoth/model/Pairable.kt b/webapp/src/main/kotlin/org/jeudego/pairgoth/model/Pairable.kt index 18f7e28..c4ab39a 100644 --- a/webapp/src/main/kotlin/org/jeudego/pairgoth/model/Pairable.kt +++ b/webapp/src/main/kotlin/org/jeudego/pairgoth/model/Pairable.kt @@ -1,6 +1,7 @@ package org.jeudego.pairgoth.model -sealed class Pairable(val id: Int, val name: String, val rating: Double, val rank: Int) +sealed class Pairable(val id: Int, val name: String, val rating: Double, val rank: Int) { +} fun Pairable.displayRank(): String = when { rank < 0 -> "${-rank}k" @@ -22,3 +23,4 @@ fun Pairable.setRank(rankStr: String): Int { else -> throw Error("impossible") } } + diff --git a/webapp/src/main/kotlin/org/jeudego/pairgoth/model/Tournament.kt b/webapp/src/main/kotlin/org/jeudego/pairgoth/model/Tournament.kt index 6ef34ba..44ce84f 100644 --- a/webapp/src/main/kotlin/org/jeudego/pairgoth/model/Tournament.kt +++ b/webapp/src/main/kotlin/org/jeudego/pairgoth/model/Tournament.kt @@ -33,8 +33,9 @@ data class Tournament( var gobanSize: Int = 19, var komi: Double = 7.5 ) { - companion object {} - val pairables = mutableMapOf() + companion object + // player/team id -> set of skipped rounds + val pairables = mutableMapOf>() } // Serialization diff --git a/webapp/src/main/kotlin/org/jeudego/pairgoth/store/Store.kt b/webapp/src/main/kotlin/org/jeudego/pairgoth/store/Store.kt index 108251c..d4267f9 100644 --- a/webapp/src/main/kotlin/org/jeudego/pairgoth/store/Store.kt +++ b/webapp/src/main/kotlin/org/jeudego/pairgoth/store/Store.kt @@ -22,8 +22,13 @@ object Store { fun getTournament(id: Int) = tournaments[id] fun getTournamentsIDs(): Set = tournaments.keys + fun addPlayer(player: Player) { if (players.containsKey(player.id)) throw Error("player id #${player.id} already exists") players[player.id] = player } + + fun getPlayer(id: Int) = players[id] + + fun getPlayersIDs(): Set = players.keys } diff --git a/webapp/src/main/kotlin/org/jeudego/pairgoth/web/ApiServlet.kt b/webapp/src/main/kotlin/org/jeudego/pairgoth/web/ApiServlet.kt index 7bcbdc7..e1470dc 100644 --- a/webapp/src/main/kotlin/org/jeudego/pairgoth/web/ApiServlet.kt +++ b/webapp/src/main/kotlin/org/jeudego/pairgoth/web/ApiServlet.kt @@ -2,28 +2,22 @@ package org.jeudego.pairgoth.web import com.republicate.kson.Json import org.jeudego.pairgoth.api.ApiHandler +import org.jeudego.pairgoth.api.RegistrationHandler import org.jeudego.pairgoth.api.PlayerHandler import org.jeudego.pairgoth.api.TournamentHandler -import org.jeudego.pairgoth.util.Colorizer import org.jeudego.pairgoth.util.Colorizer.green import org.jeudego.pairgoth.util.Colorizer.red import org.jeudego.pairgoth.util.parse import org.jeudego.pairgoth.util.toString -import org.jeudego.pairgoth.web.ApiException import org.slf4j.LoggerFactory import java.io.IOException -import java.io.StringWriter import java.util.* -import javax.servlet.ServletException import javax.servlet.http.HttpServlet import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletResponse class ApiServlet : HttpServlet() { - val tournamentHandler = TournamentHandler() - val playerHandler = PlayerHandler() - public override fun doGet(request: HttpServletRequest, response: HttpServletResponse) { doRequest(request, response) } @@ -47,29 +41,47 @@ class ApiServlet : HttpServlet() { var payload: Json? = null var reason = "OK" try { + + // validate request + if ("dev" == WebappManager.getProperty("webapp.env")) { response.addHeader("Access-Control-Allow-Origin", "*") } validateContentType(request) validateAccept(request); - val parts = uri.split("/".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() - if (parts.size < 3 || parts.size > 5) throw ApiException(HttpServletResponse.SC_BAD_REQUEST) - if (parts.size >= 4) { - request.setAttribute(ApiHandler.SELECTOR_KEY, parts[3]) - } - val entity = parts[2] + // parse request URI + + val parts = uri.split("/").filter { !it.isEmpty() } + if (parts.size !in 2..5 || parts[0] != "api") throw ApiException(HttpServletResponse.SC_BAD_REQUEST) + + val entity = parts[1] + val selector = parts.getOrNull(2)?.also { request.setAttribute(ApiHandler.SELECTOR_KEY, it) } + val subEntity = parts.getOrNull(3) + val subSelector = parts.getOrNull(4)?.also { request.setAttribute(ApiHandler.SUBSELECTOR_KEY, it) } + + // choose handler + val handler = when (entity) { - "tournament" -> tournamentHandler - "player" -> playerHandler - else -> ApiHandler.badRequest("unknown entity") + "tournament" -> + when (subEntity) { + null -> TournamentHandler + "registration" -> RegistrationHandler + else -> ApiHandler.badRequest("unknown sub-entity: $subEntity") + } + "player" -> PlayerHandler + else -> ApiHandler.badRequest("unknown entity: $entity") } + + // call handler + payload = handler.route(request, response) // if payload is null, it means the handler already sent the response if (payload != null) { setContentType(response) payload.toString(response.writer) } + } catch (apiException: ApiException) { reason = apiException.message ?: "unknown API error" if (reason == null) error(response, apiException.code) else error( diff --git a/webapp/src/main/webapp/WEB-INF/logger.properties b/webapp/src/main/webapp/WEB-INF/logger.properties index c88eccb..1626e4c 100644 --- a/webapp/src/main/webapp/WEB-INF/logger.properties +++ b/webapp/src/main/webapp/WEB-INF/logger.properties @@ -1,2 +1,2 @@ -format = %date [%level] %ip [%logger] %message (@%file:%line:%column) +format = [%level] %ip [%logger] %message (@%file:%line:%column) level = trace