Merge pull request 'back-lucien' (#8) from back-lucien into feature/back

Reviewed-on: #8
This commit is contained in:
ExostFlash 2025-06-02 14:16:00 +00:00
commit b1c1b0d8af
24 changed files with 1080 additions and 153 deletions

View file

@ -1,6 +1,9 @@
# Entités # Entités
## MATCHES (MatchBean) ## MATCHES (MatchBean)
- id -> Long - id -> Long
- country -> String
- city -> String
- weapon -> String
- player1ID -> Long - player1ID -> Long
- player2ID -> Long - player2ID -> Long
- refereeID (arbitre) -> Long - refereeID (arbitre) -> Long
@ -18,11 +21,13 @@
- id -> Long - id -> Long
- name (nom de famille) -> String - name (nom de famille) -> String
- firstName (prénom) -> String - firstName (prénom) -> String
- qualification -> String
## JOUEURS (PlayerBean) ## JOUEURS (PlayerBean)
- id -> Long - id -> Long
- name (nom de famille) -> String - name (nom de famille) -> String
- firstName (prénom) -> String - firstName (prénom) -> String
- club -> String
# Actions REST sur les entités # Actions REST sur les entités
## matches (MatchRestController, "/matches") ## matches (MatchRestController, "/matches")
@ -42,9 +47,13 @@
- Ajouter un match -> **POST "/matches/add-match"** - Ajouter un match -> **POST "/matches/add-match"**
{ {
date=$date, date=$date,
country=$country
city=$city,
weapon=$weapon,
refereeID=$refereeID, refereeID=$refereeID,
player1ID=$player1ID, player1ID=$player1ID,
player2ID=$player2ID player2ID=$player2ID,
} }
-> MatchBean -> MatchBean
- Supprimer un match (supprimer élément de MatchRepository en récupérant l'id) -> **POST "/matches/delete-match"** - Supprimer un match (supprimer élément de MatchRepository en récupérant l'id) -> **POST "/matches/delete-match"**
@ -59,7 +68,8 @@
- Ajouter un arbitre ->**POST "/referees/add-referee"** - Ajouter un arbitre ->**POST "/referees/add-referee"**
{ {
name=$name, name=$name,
firstName=$firstName firstName=$firstName,
qualification=$qualification
} }
-> RefereeBean -> RefereeBean
- Supprimer un arbitre ->**POST "/referees/delete-referee/"** - Supprimer un arbitre ->**POST "/referees/delete-referee/"**
@ -74,10 +84,54 @@
- Ajouter un joueur ->**POST "/players/add-player"** - Ajouter un joueur ->**POST "/players/add-player"**
{ {
name=$name, name=$name,
firstName=$firstName firstName=$firstName,
club=$club
} }
-> PlayerBean -> PlayerBean
- Supprimer un joueur ->**POST "/players/delete-player"** - Supprimer un joueur ->**POST "/players/delete-player"**
{ {
id=$id id=$id
} }
# Websocket (http://localhost:8080/ws)
- Vider le channel
- Insérer une donnée dans le channel
- Modifier une donnée dans le channel
- Supprimer une donnée du channel
## MATCHES
## matches (MatchRestController, "/matches")
- Lister tous les matches -> dans le channel Websocket
- Lister tous les matches à partir d'une certaine date -> dans le channel Websocket
- Lister les matches en cours (état : en cours) -> **GET "/matches/active"** -> List<MatchBean>
- Lister les matches terminés (état : terminé) -> **GET "/matches/over"** -> List<MatchBean>
- Lister les matches non commencés (état : non commencé) -> **GET "/matches/not-started"** -> List<MatchBean>
- Afficher un match par id -> **GET "/matches/id=&id"** -> MatchBean
- Mettre à jour les score d'un match récupéré par id -> **POST "/matches/update-match"**
{
id=$id,
score1=$score1,
score2=$score2
}
-> MatchBean
- Ajouter un match -> **POST "/matches/add-match"**
{
date=$date,
country=$country
city=$city,
weapon=$weapon,
refereeID=$refereeID,
player1ID=$player1ID,
player2ID=$player2ID,
}
-> MatchBean
- Supprimer un match (supprimer élément de MatchRepository en récupérant l'id) -> **POST "/matches/delete-match"**
{
id=$id
}
## REFEREES
## PLAYERS

View file

@ -1,4 +1,6 @@
corriger les services - ~ corriger les services - V
tester les restcontrollers via des requêtes http - ~ tester les restcontrollers via des requêtes http - V
créer la bdd avec toutes les tables (repositories) créer la bdd avec toutes les tables (repositories) - V
tester le websocket tester le websocket -
prévoir l'utilisation en admin uniquement (spring security)
faire le jackarta de l'api

View file

@ -27,13 +27,16 @@ dependencies {
testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
testRuntimeOnly("org.junit.platform:junit-platform-launcher") testRuntimeOnly("org.junit.platform:junit-platform-launcher")
implementation("org.springframework.boot:spring-boot-starter-validation")
// WEBSOCKET // WEBSOCKET
implementation("org.springframework.boot:spring-boot-starter-websocket") implementation("org.springframework.boot:spring-boot-starter-websocket")
// SECURITY // SECURITY
implementation("org.springframework.boot:spring-boot-starter-security") // implementation("org.springframework.boot:spring-boot-starter-security")
// H2 Database
runtimeOnly("com.h2database:h2")
} }
kotlin { kotlin {

View file

@ -0,0 +1,30 @@
package fr.teamflash.fencerjudgeback.config
import org.springframework.context.annotation.Configuration
import org.springframework.messaging.simp.config.MessageBrokerRegistry
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker
import org.springframework.web.socket.config.annotation.StompEndpointRegistry
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer
const val CHANNEL_NAME: String = "/ws/topic"
@Configuration
@EnableWebSocketMessageBroker
open class WebSocketConfig : WebSocketMessageBrokerConfigurer {
override fun configureMessageBroker(registry: MessageBrokerRegistry) {
// Enable a simple memory-based message broker to send messages to clients
// Prefix for messages FROM server TO client
registry.enableSimpleBroker(CHANNEL_NAME)
// Prefix for messages FROM client TO server
registry.setApplicationDestinationPrefixes("/ws")
}
override fun registerStompEndpoints(registry: StompEndpointRegistry) {
// Register the "/ws" endpoint, enabling SockJS fallback options
registry.addEndpoint("/ws/matches-app")
.setAllowedOriginPatterns("*") // Allow connections from any origin (adjust for production)
.withSockJS()
}
}

View file

@ -1,19 +1,33 @@
package fr.teamflash.fencerjudgeback.entities package fr.teamflash.fencerjudgeback.entities
import jakarta.persistence.Entity import jakarta.persistence.Entity
import jakarta.persistence.GeneratedValue
import jakarta.persistence.GenerationType
import jakarta.persistence.Id import jakarta.persistence.Id
import jakarta.persistence.SequenceGenerator
import jakarta.persistence.Table import jakarta.persistence.Table
import java.util.Date import jakarta.validation.constraints.*
@Entity @Entity
@Table(name="matchs") @Table(name="matches")
data class MatchBean( data class MatchBean(
@Id @Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "match_sequence")
@SequenceGenerator(name = "match_sequence", sequenceName = "match_seq", allocationSize = 1)
val id:Long?=null, val id:Long?=null,
val weapon:String?=null,
val country:String?=null,
val city:String?=null,
@field:NotNull(message = "Player 1 ID must not be null")
val player1ID:Long?=null, val player1ID:Long?=null,
@field:NotNull(message = "Player 2 ID must not be null")
val player2ID:Long?=null, val player2ID:Long?=null,
val refereeID: Long?=null, val refereeID: Long?=null,
val score1:Int=0, @field:Min(value = 0, message = "Score must be at least 0")
val score2:Int=0, var score1:Int=0,
val date: Date?=null @field:Min(value = 0, message = "Score must be at least 0")
var score2:Int=0,
val date: String?=null,
val state:String?=null
) )

View file

@ -4,13 +4,17 @@ import jakarta.persistence.Entity
import jakarta.persistence.GeneratedValue import jakarta.persistence.GeneratedValue
import jakarta.persistence.GenerationType import jakarta.persistence.GenerationType
import jakarta.persistence.Id import jakarta.persistence.Id
import jakarta.persistence.SequenceGenerator
import jakarta.persistence.Table import jakarta.persistence.Table
@Entity @Entity
@Table(name="players") @Table(name="players")
data class PlayerBean( data class PlayerBean(
@Id @Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "player_sequence")
@SequenceGenerator(name = "player_sequence", sequenceName = "player_seq", allocationSize = 1)
val id:Long?=null, val id:Long?=null,
val name:String?="", val name:String?="",
val firstName:String?="" val firstName:String?="",
val club:String?=""
) )

View file

@ -4,13 +4,17 @@ import jakarta.persistence.Entity
import jakarta.persistence.GeneratedValue import jakarta.persistence.GeneratedValue
import jakarta.persistence.GenerationType import jakarta.persistence.GenerationType
import jakarta.persistence.Id import jakarta.persistence.Id
import jakarta.persistence.SequenceGenerator
import jakarta.persistence.Table import jakarta.persistence.Table
@Entity @Entity
@Table(name="referees") @Table(name="referees")
data class RefereeBean( data class RefereeBean(
@Id @Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "referee_sequence")
@SequenceGenerator(name = "referee_sequence", sequenceName = "referee_seq", allocationSize = 1)
val id:Long?=null, val id:Long?=null,
val name:String?=null, val name:String?=null,
val firstName:String?=null val firstName:String?=null,
val qualification:String?=null
) )

View file

@ -2,10 +2,8 @@ package fr.teamflash.fencerjudgeback.restControllers
import fr.teamflash.fencerjudgeback.entities.MatchBean import fr.teamflash.fencerjudgeback.entities.MatchBean
import fr.teamflash.fencerjudgeback.services.MatchService import fr.teamflash.fencerjudgeback.services.MatchService
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.* import org.springframework.web.bind.annotation.*
import kotlin.random.Random
@RestController @RestController
@RequestMapping("/matches") @RequestMapping("/matches")
@ -13,55 +11,74 @@ class MatchRestController(private val matchService: MatchService) {
// Lister tous les matchs // Lister tous les matchs
@GetMapping("/") @GetMapping("/")
fun getAll(): ResponseEntity<List<MatchBean>> { fun getAll(): ResponseEntity<List<MatchBean>?> {
return ResponseEntity.ok(matchService.getAll()) return ResponseEntity.ok(matchService.getAll())
} }
// Lister tous les matchs à partir d'une certaine date // Lister tous les matchs à partir d'une certaine date
@GetMapping("/{date}") @GetMapping("/date/{date}")
fun getAllFromDate(date:Date): ResponseEntity<List<MatchBean>?> { fun getAllFromDate(@PathVariable date:String): ResponseEntity<List<MatchBean?>?> {
return ResponseEntity.ok(matchService.getAll().filter { it.date == date }) return ResponseEntity.ok(matchService.getAll().filter { it.date == date })
} }
// Lister tous les matchs par pays
@GetMapping("/country/{country}")
fun getAllFromCountry(@PathVariable country:String): ResponseEntity<List<MatchBean>?> {
return ResponseEntity.ok(matchService.getAll().filter { it.country == country })
}
// Lister tous les matchs par ville
@GetMapping("/city/{city}")
fun getAllFromCity(@PathVariable city:String): ResponseEntity<List<MatchBean>?> {
return ResponseEntity.ok(matchService.getAll().filter { it.city == city })
}
// Lister tous les matchs par joueurs
@GetMapping("/players/{player1ID}/{player2ID}")
fun getAllWithPlayers(@PathVariable player1ID: Long?, @PathVariable player2ID: Long?): ResponseEntity<List<MatchBean>?> {
return ResponseEntity.ok(matchService.getByPlayers(player1ID, player2ID))
}
// Lister les matchs en cours // Lister les matchs en cours
@GetMapping("/active") @GetMapping("/active")
fun getAllActive(): ResponseEntity<List<MatchBean>?> { fun getAllActive(): ResponseEntity<List<MatchBean>?> {
return ResponseEntity.ok(matchService.getAll().filter { it.state == ACTIVE }) return ResponseEntity.ok(matchService.getAll().filter { it.state == "ACTIVE" })
} }
// Lister les matchs terminés // Lister les matchs terminés
@GetMapping("/over") @GetMapping("/over")
fun getAllFinished(): ResponseEntity<List<MatchBean>?> { fun getAllFinished(): ResponseEntity<List<MatchBean>?> {
return ResponseEntity.ok(matchService.getAll().filter { }) return ResponseEntity.ok(matchService.getAll().filter { it.state == "FINISHED" })
} }
// Lister les matchs non commencés // Lister les matchs non commencés
@GetMapping("/not-started") @GetMapping("/not-started")
fun getAllNotStarted(): ResponseEntity<List<MatchBean>?> { fun getAllNotStarted(): ResponseEntity<List<MatchBean>?> {
return ResponseEntity.ok(matchService.getAll().filter { it.state == "NOT STARTED" })
} }
// Afficher un match par id // Afficher un match par id
@GetMapping("/{id}") @GetMapping("/{id}")
fun getMatchById(@PathVariable id: Long): ResponseEntity<MatchBean?> { fun getById(@PathVariable id: Long): ResponseEntity<MatchBean?> {
return ResponseEntity.ok(matchService.getById(id)) return ResponseEntity.ok(matchService.getById(id))
} }
// Ajouter un match // Ajouter un match
@PostMapping("/create-match") @PostMapping("/create-match")
fun createMatch(@RequestBody match: MatchBean): ResponseEntity<MatchBean> { fun createMatch(@RequestBody match: MatchBean): ResponseEntity<MatchBean> {
return ResponseEntity.status(HttpStatus.CREATED).body(matchService.createMatch(match)) // return ResponseEntity.status(HttpStatus.CREATED).body(matchService.createMatch(match))
return ResponseEntity.ok(matchService.createMatch(match))
} }
// Mettre à jour un match // Mettre à jour un match
@PutMapping("/update-match/{id}") @PutMapping("/update-match/{id}")
fun updateMatch(@PathVariable id: Long, @RequestBody refereeID: Long, @RequestBody): ResponseEntity<MatchBean?> { fun updateMatch(@PathVariable id: Long, @RequestBody match: MatchBean): ResponseEntity<Int> {
return ResponseEntity.ok(matchService.updateMatch(match)) return ResponseEntity.ok(matchService.updateMatch(id, match))
} }
// Supprimer un match // Supprimer un match
@DeleteMapping("/delete-match/{id}") @DeleteMapping("/delete-match/{id}")
fun deleteMatch(@PathVariable id: Long): ResponseEntity<Int> { fun deleteMatch(@PathVariable id: Long): ResponseEntity<Int> {
return ResponseEntity.ok(matchService.deleteMatch(id)) return ResponseEntity.ok(matchService.deleteMatchById(id))
} }
} }

View file

@ -5,55 +5,62 @@ import fr.teamflash.fencerjudgeback.services.PlayerService
import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.DeleteMapping import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.PutMapping import org.springframework.web.bind.annotation.PutMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController import org.springframework.web.bind.annotation.RestController
import java.util.Optional
@RestController @RestController
@RequestMapping("/players") @RequestMapping("/players")
class PlayerRestController(private val playerService: PlayerService) { class PlayerRestController(private val playerService: PlayerService) {
// Lister tous les joueurs // Lister tous les joueurs
@GetMapping @GetMapping("/")
fun getAll(): ResponseEntity<List<PlayerBean>> { fun getAll(): ResponseEntity<List<PlayerBean>> {
return ResponseEntity.ok(playerService.getAll()) return ResponseEntity.ok(playerService.getAll())
} }
// Afficher un joueur par id // Afficher un joueur par id
@GetMapping("/{id}") @GetMapping("/{id}")
fun getById(id: Long): ResponseEntity<Optional<PlayerBean?>?> { fun getById(@PathVariable id: Long): ResponseEntity<PlayerBean?> {
return ResponseEntity.ok(playerService.getById(id)) return ResponseEntity.ok(playerService.getById(id))
} }
// Afficher un joueur par nom // Afficher un ou plusieurs joueur(s) par nom
@GetMapping("/{name}") @GetMapping("/name/{name}")
fun getByName(name: String): ResponseEntity<List<PlayerBean?>?> { fun getByName(@PathVariable name: String): ResponseEntity<List<PlayerBean?>?> {
return ResponseEntity.ok(playerService.getByName(name)) return ResponseEntity.ok(playerService.getByName(name))
} }
// Afficher un joueur par prénom // Afficher un ou plusieurs joueur(s) par prénom
@GetMapping("/{firstName}") @GetMapping("/firstName/{firstName}")
fun getByFirstName(firstName: String): ResponseEntity<List<PlayerBean?>?> { fun getByFirstName(@PathVariable firstName: String): ResponseEntity<List<PlayerBean?>?> {
return ResponseEntity.ok(playerService.getByFirstName(firstName)) return ResponseEntity.ok(playerService.getByFirstName(firstName))
} }
// Afficher un ou plusieurs joueur(s) par club
@GetMapping("/club/{club}")
fun getByClub(@PathVariable club: String): ResponseEntity<List<PlayerBean?>?> {
return ResponseEntity.ok(playerService.getByClub(club))
}
// Ajouter un joueur // Ajouter un joueur
@PostMapping("/add-player") @PostMapping("/create-player")
fun createPlayer(player: PlayerBean): ResponseEntity<PlayerBean> { fun createPlayer(@RequestBody player: PlayerBean): ResponseEntity<PlayerBean> {
return ResponseEntity.ok(playerService.createPlayer(player)) return ResponseEntity.ok(playerService.createPlayer(player))
} }
// Modifier un joueur // Modifier un joueur
@PutMapping("/update-player") @PutMapping("/update-player/{id}")
fun updatePlayer(id: Long, name: String, firstName: String): ResponseEntity<Int> { fun updatePlayer(@PathVariable id: Long, @RequestBody player: PlayerBean): ResponseEntity<Int> {
return ResponseEntity.ok(playerService.updatePlayer(id, name, firstName)) return ResponseEntity.ok(playerService.updatePlayer(id, player))
} }
// Supprimer un joueur // Supprimer un joueur
@DeleteMapping("/delete-player") @DeleteMapping("/delete-player/{id}")
fun deletePlayer(id: Long): ResponseEntity<Int> { fun deletePlayer(@PathVariable id: Long): ResponseEntity<Int> {
return ResponseEntity.ok(playerService.deletePlayer(id)) return ResponseEntity.ok(playerService.deletePlayerById(id))
} }
} }

View file

@ -8,9 +8,9 @@ import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.PutMapping import org.springframework.web.bind.annotation.PutMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController import org.springframework.web.bind.annotation.RestController
import java.util.Optional
@RestController @RestController
@RequestMapping("/referees") @RequestMapping("/referees")
@ -24,38 +24,44 @@ class RefereeRestController(private val refereeService: RefereeService) {
// Afficher un arbitre par id // Afficher un arbitre par id
@GetMapping("/{id}") @GetMapping("/{id}")
fun getById(id: Long): ResponseEntity<Optional<RefereeBean?>?> { fun getById(@PathVariable id: Long): ResponseEntity<RefereeBean?> {
return ResponseEntity.ok(refereeService.getById(id)) return ResponseEntity.ok(refereeService.getById(id))
} }
// Afficher un ou plusieurs arbitre(s) par nom // Afficher un ou plusieurs arbitre(s) par nom
@GetMapping("/name/{name}") @GetMapping("/name/{name}")
fun getByName(name:String): ResponseEntity<List<RefereeBean?>?> { fun getByName(@PathVariable name:String): ResponseEntity<List<RefereeBean?>?> {
return ResponseEntity.ok(refereeService.getByName(name)) return ResponseEntity.ok(refereeService.getByName(name))
} }
// Afficher ou plusieurs arbitre(s) par prénom // Afficher un ou plusieurs arbitre(s) par prénom
@GetMapping("/first-name/{firstName}") @GetMapping("/firstname/{firstName}")
fun getByFirstName(firstName:String): ResponseEntity<List<RefereeBean?>?> { fun getByFirstName(@PathVariable firstName:String): ResponseEntity<List<RefereeBean?>?> {
return ResponseEntity.ok(refereeService.getByFirstName(firstName)) return ResponseEntity.ok(refereeService.getByFirstName(firstName))
} }
// Afficher un ou plusieurs arbitre(s) par qualification
@GetMapping("/qualification/{qualification}")
fun getByQualification(@PathVariable qualification:String): ResponseEntity<List<RefereeBean?>?> {
return ResponseEntity.ok(refereeService.getByQualification(qualification))
}
// Ajouter un arbitre // Ajouter un arbitre
@PostMapping("/create-referee") @PostMapping("/create-referee")
fun createReferee(referee: RefereeBean): ResponseEntity<RefereeBean> { fun createReferee(@RequestBody referee: RefereeBean): ResponseEntity<RefereeBean> {
return ResponseEntity.ok(refereeService.createReferee(referee)) return ResponseEntity.ok(refereeService.createReferee(referee))
} }
// Modifier un arbitre // Modifier un arbitre
@PutMapping("/update-referee/{id}") @PutMapping("/update-referee/{id}")
fun updateReferee(@PathVariable id: Long, name: String, firstName: String) : ResponseEntity<Int> { fun updateReferee(@PathVariable id: Long, @RequestBody referee: RefereeBean) : ResponseEntity<Int> {
return ResponseEntity.ok(refereeService.updateReferee(id, name, firstName)) return ResponseEntity.ok(refereeService.updateReferee(id, referee))
} }
// Supprimer un arbitre // Supprimer un arbitre
@DeleteMapping("/delete-referee/{id}") @DeleteMapping("/delete-referee/{id}")
fun deleteReferee(id:Long): ResponseEntity<Int> { fun deleteReferee(@PathVariable id:Long): ResponseEntity<Int> {
return ResponseEntity.ok(refereeService.deleteReferee(id)) return ResponseEntity.ok(refereeService.deleteRefereeById(id))
} }
} }

View file

@ -1,15 +1,11 @@
package fr.teamflash.fencerjudgeback.services package fr.teamflash.fencerjudgeback.services
import fr.teamflash.fencerjudgeback.entities.MatchBean import fr.teamflash.fencerjudgeback.entities.MatchBean
import fr.teamflash.fencerjudgeback.entities.PlayerBean
import fr.teamflash.fencerjudgeback.repositories.MatchRepository import fr.teamflash.fencerjudgeback.repositories.MatchRepository
import fr.teamflash.fencerjudgeback.websocket.controllers.MatchWebSocketController import fr.teamflash.fencerjudgeback.websocket.controllers.MatchWebSocketController
import fr.teamflash.fencerjudgeback.websocket.models.MatchUpdateMessage
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.Lazy import org.springframework.context.annotation.Lazy
import org.springframework.http.ResponseEntity
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import java.util.Date
@Service @Service
class MatchService( class MatchService(
@ -23,13 +19,19 @@ class MatchService(
} }
// Obtenir un match par id (public) // Obtenir un match par id (public)
fun getById(id:Long): MatchBean? { fun getById(id: Long?): MatchBean? {
println("MatchService.getMatchById : $id") println("MatchService.getMatchById : $id")
if (id == null) {
println("MatchService.getMatchById : Match not found")
return null
}
return matchRepository.findById(id).get() return matchRepository.findById(id).get()
} }
// Obtenir un ou plusieurs match(s) par joueurs (id) (public) // Obtenir un ou plusieurs match(s) par joueurs (id) (public)
fun getByPlayers(player1ID: Long, player2ID: Long): List<MatchBean> { fun getByPlayers(player1ID: Long?, player2ID: Long?): List<MatchBean> {
println("MatchService.getMatchByPlayers : $player1ID - $player2ID") println("MatchService.getMatchByPlayers : $player1ID - $player2ID")
return matchRepository.findAll().filter { it.player1ID == player1ID && it.player2ID == player2ID } return matchRepository.findAll().filter { it.player1ID == player1ID && it.player2ID == player2ID }
} }
@ -37,68 +39,60 @@ class MatchService(
// Ajouter un match (admin) // Ajouter un match (admin)
fun createMatch(newMatch: MatchBean): MatchBean { fun createMatch(newMatch: MatchBean): MatchBean {
println("MatchService.createMatch : $newMatch") println("MatchService.createMatch : $newMatch")
val savedMatch = matchRepository.save(newMatch)
// Broadcast the new match via WebSocket // Broadcast the new match via WebSocket
// matchWebSocketController?.broadcastMatchUpdate(savedMatch, MatchUpdateMessage.UpdateType.MATCH_START) matchWebSocketController?.broadcastMatchUpdate(savedMatch, fr.teamflash.fencerjudgeback.websocket.models.MatchUpdateMessage.UpdateType.MATCH_START)
return matchRepository.save(newMatch) return savedMatch
} }
// Modifier un match (admin) // Modifier un match (admin)
fun updateMatch(id: Long, date: Date, refereeID: Long, player1ID: Long, player2ID: Long, scorePlayer1:Int, scorePlayer2:Int) : Int { fun updateMatch(id: Long?, newMatch: MatchBean): Int {
println("MatchService.updateMatch : $id - $refereeID - $player1ID - $player2ID - $scorePlayer1 - $scorePlayer2") println("MatchService.updateMatch : $newMatch")
if (getById(id) == null) { if (getById(id) == null) {
println("MatchService.updateMatch : Match not found")
return -1 return -1
} }
// Broadcast the update via WebSocket val updatedMatch = newMatch.copy(id = id)
// matchWebSocketController?.broadcastMatchUpdate(savedMatch, MatchUpdateMessage.UpdateType.SCORE_UPDATE) val savedMatch = matchRepository.save(updatedMatch)
// Broadcast the updated match via WebSocket
// Créer nouveau MatchBean à l'id renseigné matchWebSocketController?.broadcastMatchUpdate(savedMatch, fr.teamflash.fencerjudgeback.websocket.models.MatchUpdateMessage.UpdateType.SCORE_UPDATE)
val newMatch = MatchBean(
id = id,
date = date,
refereeID = refereeID,
player1ID = player1ID,
player2ID = player2ID,
score1 = scorePlayer1,
score2 = scorePlayer2
)
// if (
// // Supprimer le MatchBean à l'id en paramètre
// deleteMatch(id) == 1
// && createMatch(newMatch) == 1) {
//
// // Insérer le nouveau MatchBean à l'id en paramètre
//
//
// return 1
// }
// Supprimer le MatchBean à l'id en paramètre
if (deleteMatch(id) == 1) {
// Insérer le nouveau MatchBean à l'id en paramètre
createMatch(newMatch)
}
return 1 return 1
} }
// Supprimer un match (admin) // Supprimer un match (admin)
fun deleteMatch(id:Long) : Int { fun deleteMatchById(id: Long?) : Int? {
println("MatchService.deleteMatch : $id") println("MatchService.deleteMatchById : $id")
try { val match = getById(id)
matchRepository.deleteById(id) if (match == null) {
} catch (e: Exception) { println("MatchService.deleteMatchById : Match not found")
println("Error deleting match: ${e.message}")
return -1 return -1
} }
// Broadcast the match cancellation via WebSocket before deleting // Broadcast the match deletion via WebSocket before deleting
// matchWebSocketController?.broadcastMatchUpdate(matchID, MatchUpdateMessage.UpdateType.MATCH_CANCEL) matchWebSocketController?.broadcastMatchUpdate(match, fr.teamflash.fencerjudgeback.websocket.models.MatchUpdateMessage.UpdateType.MATCH_CANCEL)
matchRepository.deleteById(id!!)
return 1 return 1
} }
fun getMatchesByCity(city: String): List<MatchBean>? {
println("MatchService.getMatchesByCity : $city")
return matchRepository.findAll()
.filter { it.city == city }
}
fun getMatchesByCountry(country: String): List<MatchBean>? {
println("MatchService.getMatchesByCountry : $country")
return matchRepository.findAll()
.filter { it.country == country }
}
fun addMatch(match:MatchBean) {
matchRepository.save(match)
}
} }

View file

@ -2,72 +2,95 @@ package fr.teamflash.fencerjudgeback.services
import fr.teamflash.fencerjudgeback.entities.PlayerBean import fr.teamflash.fencerjudgeback.entities.PlayerBean
import fr.teamflash.fencerjudgeback.repositories.PlayerRepository import fr.teamflash.fencerjudgeback.repositories.PlayerRepository
import fr.teamflash.fencerjudgeback.websocket.controllers.PlayerWebSocketController
import fr.teamflash.fencerjudgeback.websocket.models.PlayerUpdateMessage
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.Lazy
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import java.util.Optional
@Service @Service
class PlayerService(@Autowired private val playerRepository: PlayerRepository) { class PlayerService(
@Autowired private val playerRepository: PlayerRepository,
@Lazy private val playerWebSocketController: PlayerWebSocketController? = null
) {
// Obtenir tous les joueurs (public) // Obtenir tous les joueurs (public)
fun getAll(): List<PlayerBean> { fun getAll(): List<PlayerBean> {
println("RefereeService.getReferees") println("PlayerService.getAll")
return playerRepository.findAll() return playerRepository.findAll()
} }
// Obtenir un joueur par id (public) // Obtenir un joueur par id (public)
fun getById(id:Long) : Optional<PlayerBean?> { fun getById(id:Long?) : PlayerBean? {
println("RefereeService.getRefereeById : $id") println("PlayerService.getById : $id")
return playerRepository.findById(id)
if (id == null) {
return null
}
return playerRepository.findById(id).get()
} }
// Obtenir un ou plusieurs joueur(s) par nom (public) // Obtenir un ou plusieurs joueur(s) par nom (public)
fun getByName(name:String): List<PlayerBean?>? { fun getByName(name:String): List<PlayerBean?>? {
println("RefereeService.getRefereeByName : $name") println("PlayerService.getByName : $name")
return playerRepository.findAll().filter{ it.name == name } return playerRepository.findAll().filter{ it.name == name }
} }
// Obtenir un ou plusieurs joueur(s) par prénom // Obtenir un ou plusieurs joueur(s) par prénom
fun getByFirstName(firstName:String): List<PlayerBean?>? { fun getByFirstName(firstName:String): List<PlayerBean?>? {
println("RefereeService.getRefereeByFirstName : $firstName") println("PlayerService.getByFirstName : $firstName")
return playerRepository.findAll().filter{ it.firstName == firstName } return playerRepository.findAll().filter{ it.firstName == firstName }
} }
// Obtenir un ou plusieurs joueur(s) par club
fun getByClub(club:String): List<PlayerBean?>? {
println("PlayerService.getByClub : $club")
return playerRepository.findAll().filter{ it.club == club }
}
// Ajouter un joueur (admin) // Ajouter un joueur (admin)
fun createPlayer(referee: PlayerBean) : PlayerBean { fun createPlayer(player: PlayerBean) : PlayerBean {
println("RefereeService.createReferee : $referee") println("PlayerService.createPlayer : $player")
return playerRepository.save(referee) val savedPlayer = playerRepository.save(player)
// Broadcast the new player via WebSocket
playerWebSocketController?.broadcastPlayerUpdate(savedPlayer, PlayerUpdateMessage.UpdateType.PLAYER_CREATE)
return savedPlayer
} }
// Modifier un joueur (admin) // Modifier un joueur (admin)
fun updatePlayer(id:Long, name:String, firstName: String) : Int { fun updatePlayer(id:Long, newPlayer: PlayerBean) : Int {
// Vérifier si le joueur existe à l'id renseigné // Vérifier si le joueur existe à l'id renseigné
if (getById(id) == null) { if (getById(id) == null) {
println("RefereeService.updateReferee : Referee not found") println("PlayerService.updatePlayer : Player not found")
return -1 return -1
} }
// Créer nouveau PlayerBean à l'id renseigné val updatedPlayer = newPlayer.copy(id = id)
val newPlayer = PlayerBean(id, name, firstName) val savedPlayer = playerRepository.save(updatedPlayer)
// Broadcast the updated player via WebSocket
// Supprimer le PlayerBean à l'id en paramètre playerWebSocketController?.broadcastPlayerUpdate(savedPlayer, PlayerUpdateMessage.UpdateType.PLAYER_UPDATE)
deletePlayer(id)
// Insérer le nouveau PlayerBean
createPlayer(newPlayer)
return 1 return 1
} }
// Supprimer un joueur (admin) // Supprimer un joueur (admin)
fun deletePlayer(id:Long): Int { fun deletePlayerById(id:Long?): Int {
println("RefereeService.deleteReferee : $id") println("PlayerService.deletePlayer : $id")
if (getById(id) == null) { val player = getById(id)
println("RefereeService.deleteReferee : Referee not found") if (player == null) {
println("PlayerService.deletePlayer : Player not found")
return -1 return -1
} }
playerRepository.deleteById(id) // Broadcast the player deletion via WebSocket before deleting
playerWebSocketController?.broadcastPlayerUpdate(player, PlayerUpdateMessage.UpdateType.PLAYER_DELETE)
playerRepository.deleteById(id!!)
return 1 return 1
} }
fun addPlayer(player:PlayerBean) {
playerRepository.save(player)
}
} }

View file

@ -2,13 +2,17 @@ package fr.teamflash.fencerjudgeback.services
import fr.teamflash.fencerjudgeback.entities.RefereeBean import fr.teamflash.fencerjudgeback.entities.RefereeBean
import fr.teamflash.fencerjudgeback.repositories.RefereeRepository import fr.teamflash.fencerjudgeback.repositories.RefereeRepository
import fr.teamflash.fencerjudgeback.websocket.controllers.RefereeWebSocketController
import fr.teamflash.fencerjudgeback.websocket.models.RefereeUpdateMessage
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
import org.springframework.data.jpa.domain.AbstractPersistable_.id import org.springframework.context.annotation.Lazy
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import java.util.Optional
@Service @Service
class RefereeService(@Autowired private val refereeRepository: RefereeRepository) { class RefereeService(
@Autowired private val refereeRepository: RefereeRepository,
@Lazy private val refereeWebSocketController: RefereeWebSocketController? = null
) {
// Obtenir tous les arbitres (public) // Obtenir tous les arbitres (public)
fun getAll(): List<RefereeBean> { fun getAll(): List<RefereeBean> {
println("RefereeService.getReferees") println("RefereeService.getReferees")
@ -16,9 +20,14 @@ class RefereeService(@Autowired private val refereeRepository: RefereeRepository
} }
// Obtenir un arbitre par id (public) // Obtenir un arbitre par id (public)
fun getById(id:Long) : Optional<RefereeBean?> { fun getById(id:Long?) : RefereeBean? {
println("RefereeService.getRefereeById : $id") println("RefereeService.getRefereeById : $id")
return refereeRepository.findById(id)
if (id == null) {
return null
}
return refereeRepository.findById(id).get()
} }
// Obtenir un ou plusieurs arbitre(s) par nom (public) // Obtenir un ou plusieurs arbitre(s) par nom (public)
@ -33,41 +42,48 @@ class RefereeService(@Autowired private val refereeRepository: RefereeRepository
return refereeRepository.findAll().filter{ it.firstName == firstName } return refereeRepository.findAll().filter{ it.firstName == firstName }
} }
// Obtenir un ou plusieurs arbitre(s) par qualification
fun getByQualification(qualification:String): List<RefereeBean?>? {
println("RefereeService.getRefereeByQualification : $qualification")
return refereeRepository.findAll().filter{ it.qualification == qualification }
}
// Ajouter un arbitre (admin) // Ajouter un arbitre (admin)
fun createReferee(referee: RefereeBean) : RefereeBean { fun createReferee(referee: RefereeBean) : RefereeBean {
println("RefereeService.createReferee : $referee") println("RefereeService.createReferee : $referee")
return refereeRepository.save(referee) val savedReferee = refereeRepository.save(referee)
// Broadcast the new referee via WebSocket
refereeWebSocketController?.broadcastRefereeUpdate(savedReferee, RefereeUpdateMessage.UpdateType.REFEREE_CREATE)
return savedReferee
} }
// Modifier un arbitre (admin) // Modifier un arbitre (admin)
fun updateReferee(id:Long, name:String, firstName: String) : Int { fun updateReferee(id:Long, newReferee: RefereeBean) : Int? {
// Vérifier si l'arbitre existe à l'id renseigné // Vérifier si l'arbitre existe à l'id renseigné
if (getById(id) == null) { if (getById(id) == null) {
println("RefereeService.updateReferee : Referee not found")
return -1 return -1
} }
// Créer nouveau RefereeBean à l'id renseigné val updatedReferee = newReferee.copy(id = id)
val newReferee = RefereeBean(id, name, firstName) val savedReferee = refereeRepository.save(updatedReferee)
// Broadcast the updated referee via WebSocket
// Supprimer le RefereeBean à l'id en paramètre refereeWebSocketController?.broadcastRefereeUpdate(savedReferee, RefereeUpdateMessage.UpdateType.REFEREE_UPDATE)
deleteReferee(id)
// Insérer le nouveau RefereeBean à l'id en paramètre
createReferee(newReferee)
return 1 return 1
} }
// Supprimer un arbitre (admin) // Supprimer un arbitre (admin)
fun deleteReferee(id:Long): Int { fun deleteRefereeById(id:Long): Int {
println("RefereeService.deleteReferee : $id") println("RefereeService.deleteReferee : $id")
if (getById(id) == null) { val referee = getById(id)
if (referee == null) {
println("RefereeService.deleteReferee : Referee not found") println("RefereeService.deleteReferee : Referee not found")
return -1 return -1
} }
// Broadcast the referee deletion via WebSocket before deleting
refereeWebSocketController?.broadcastRefereeUpdate(referee, RefereeUpdateMessage.UpdateType.REFEREE_DELETE)
refereeRepository.deleteById(id) refereeRepository.deleteById(id)
return 1 return 1
} }

View file

@ -0,0 +1,120 @@
# WebSocket Implementation for FencerJudge
This document explains how to use the WebSocket functionality in the FencerJudge application for real-time match updates.
## Overview
The WebSocket implementation allows for real-time updates of match information, including:
- Match creation
- Score updates
- Match completion
- Match cancellation
## WebSocket Endpoints
### Connection Endpoint
```
ws://localhost:8080/ws
```
The WebSocket endpoint supports SockJS for fallback in browsers that don't support WebSockets natively.
## Message Topics
### Subscribe to Match Updates
To receive real-time match updates, subscribe to:
```
/topic/match.updates
```
### Send Match Updates
To send match updates from the client to the server:
```
/app/match.update
```
## Message Format
Match update messages use the following JSON format:
```json
{
"matchId": 1,
"player1Id": 101,
"player2Id": 102,
"refereeId": 201,
"score1": 5,
"score2": 3,
"date": "2023-06-01",
"type": "SCORE_UPDATE"
}
```
The `type` field can have the following values:
- `SCORE_UPDATE`: When a match score is updated
- `MATCH_START`: When a new match is created
- `MATCH_END`: When a match is completed
- `MATCH_CANCEL`: When a match is cancelled
## Integration with REST API
The WebSocket functionality is integrated with the REST API. Any changes made through the REST endpoints will automatically trigger WebSocket messages to all connected clients.
REST endpoints:
- `GET /api/matches`: Get all matches
- `GET /api/matches/{id}`: Get a match by ID
- `POST /api/matches`: Create a new match (triggers MATCH_START WebSocket message)
- `PUT /api/matches/{id}`: Update a match (triggers SCORE_UPDATE WebSocket message)
- `PATCH /api/matches/{id}/score`: Update just the score (triggers SCORE_UPDATE WebSocket message)
- `DELETE /api/matches/{id}`: Delete a match (triggers MATCH_CANCEL WebSocket message)
## Client-Side Example (JavaScript)
```javascript
// Using SockJS and STOMP client
import SockJS from 'sockjs-client';
import { Client } from '@stomp/stompjs';
// Create a new STOMP client
const socket = new SockJS('http://localhost:8080/ws');
const stompClient = new Client({
webSocketFactory: () => socket,
debug: (str) => console.log(str)
});
// Connect to the WebSocket server
stompClient.activate();
// Subscribe to match updates
stompClient.onConnect = (frame) => {
console.log('Connected to WebSocket');
// Subscribe to match updates
stompClient.subscribe('/topic/match.updates', (message) => {
const matchUpdate = JSON.parse(message.body);
console.log('Received match update:', matchUpdate);
// Handle the update in your application
// e.g., update UI, play sound, etc.
});
};
// Send a match update (e.g., when a referee updates a score)
function sendMatchUpdate(matchId, score1, score2) {
if (stompClient.connected) {
stompClient.publish({
destination: '/app/match.update',
body: JSON.stringify({
matchId: matchId,
score1: score1,
score2: score2,
type: 'SCORE_UPDATE'
})
});
}
}
```
## Security Considerations
The WebSocket endpoint is currently configured to allow connections from any origin (`*`). For production environments, this should be restricted to specific allowed origins.

View file

@ -0,0 +1,41 @@
package fr.teamflash.fencerjudgeback.websocket
import org.springframework.messaging.handler.annotation.MessageMapping
import org.springframework.messaging.simp.SimpMessagingTemplate
import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.RequestMapping
data class Data(val id: String, val value: Any)
@Controller
@RequestMapping("/ws")
class WebSocketController(private val messagingTemplate: SimpMessagingTemplate) {
// Insérer une donnée dans le channel
@MessageMapping("/insert")
fun insertData(data: Data) {
// Logique d'insertion
messagingTemplate.convertAndSend("/topic/data", data)
}
// Modifier une donnée existente dans le channel
@MessageMapping("/modify")
fun modifyData(data: Data) {
// Logique de modification
messagingTemplate.convertAndSend("/topic/data", data)
}
// Supprimer une donnée existante dans le channel
@MessageMapping("/delete")
fun deleteData(id: String) {
// Logique de suppression
messagingTemplate.convertAndSend("/topic/data/delete", id)
}
// Vider le channel
@MessageMapping("/clear")
fun clearChannel() {
// Logique pour vider le channel
messagingTemplate.convertAndSend("/topic/data/clear", null)
}
}

View file

@ -0,0 +1,102 @@
package fr.teamflash.fencerjudgeback.websocket.controllers
import fr.teamflash.fencerjudgeback.config.CHANNEL_NAME
import fr.teamflash.fencerjudgeback.entities.MatchBean
import fr.teamflash.fencerjudgeback.services.MatchService
import fr.teamflash.fencerjudgeback.websocket.models.MatchUpdateMessage
import org.springframework.context.event.EventListener
import org.springframework.messaging.handler.annotation.MessageMapping
import org.springframework.messaging.simp.SimpMessagingTemplate
import org.springframework.messaging.simp.stomp.StompHeaderAccessor
import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.socket.messaging.SessionSubscribeEvent
@Controller
@RequestMapping("/ws/matches")
class MatchWebSocketController(
private val matchService: MatchService,
private val messagingTemplate: SimpMessagingTemplate
) {
private val messageHistory = ArrayList<MatchBean>()
@MessageMapping("/all")
fun receiveMessage(message: MatchBean) {
println("/ws/matches $message")
messageHistory.add(message)
// Envoyer la liste des messages sur le channel
//Si la variable est dans le même package il faut enlever WebSocketConfig.
messagingTemplate.convertAndSend(CHANNEL_NAME, messageHistory)
}
/**
* Handle match update requests from clients
* Client sends to: /app/match.update
* Server broadcasts to: /topic/match.updates
*/
@MessageMapping("/update")
fun handleMatchUpdate(matchUpdateMessage: MatchUpdateMessage): MatchUpdateMessage {
// Create a MatchBean from the update message
val matchBean = MatchBean(
id = matchUpdateMessage.matchId,
player1ID = matchUpdateMessage.player1Id,
player2ID = matchUpdateMessage.player2Id,
refereeID = matchUpdateMessage.refereeId,
score1 = matchUpdateMessage.score1,
score2 = matchUpdateMessage.score2,
date = matchUpdateMessage.date
)
// Update the match in the database
matchService.updateMatch(matchBean.id, matchBean)
// Return the update message to be broadcast to all subscribers
return matchUpdateMessage
}
/**
* Broadcast a match update to all connected clients
* This method can be called from other services
*/
fun broadcastMatchUpdate(matchBean: MatchBean, type: MatchUpdateMessage.UpdateType = MatchUpdateMessage.UpdateType.SCORE_UPDATE) {
val updateMessage = MatchUpdateMessage.fromMatchBean(matchBean, type)
messagingTemplate.convertAndSend("/topic/match.updates", updateMessage)
}
//A mettre dans le controller
@EventListener
fun handleWebSocketSubscribeListener(event: SessionSubscribeEvent) {
val headerAccessor = StompHeaderAccessor.wrap(event.message)
if (CHANNEL_NAME == headerAccessor.destination) {
messagingTemplate.convertAndSend(CHANNEL_NAME, messageHistory)
println("Lancement...")
}
}
fun addPoint(match:MatchBean, playerId:Long) {
when (playerId) {
match.player1ID -> match.score1 += 1
match.player2ID -> match.score2 += 1
}
matchService.updateMatch(match.id, match)
broadcastMatchUpdate(match)
}
fun minusPoint(match:MatchBean, playerId:Long) {
when (playerId) {
match.player1ID -> if (match.score1 > 0) match.score1 -= 1
match.player2ID -> if (match.score2 > 0) match.score2 -= 1
}
matchService.updateMatch(match.id, match)
broadcastMatchUpdate(match)
}
@MessageMapping("/add")
fun addMatchtoMainList(match:MatchBean) {
matchService.addMatch(match)
messageHistory.add(match)
broadcastMatchUpdate(match, MatchUpdateMessage.UpdateType.NEW_MATCH)
}
}

View file

@ -0,0 +1,71 @@
package fr.teamflash.fencerjudgeback.websocket.controllers
import fr.teamflash.fencerjudgeback.entities.PlayerBean
import fr.teamflash.fencerjudgeback.services.PlayerService
import fr.teamflash.fencerjudgeback.websocket.models.PlayerUpdateMessage
import org.springframework.context.event.EventListener
import org.springframework.messaging.handler.annotation.MessageMapping
import org.springframework.messaging.handler.annotation.SendTo
import org.springframework.messaging.simp.SimpMessagingTemplate
import org.springframework.messaging.simp.stomp.StompHeaderAccessor
import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.socket.messaging.SessionSubscribeEvent
@Controller
@RequestMapping("/ws")
class PlayerWebSocketController(
private val playerService: PlayerService,
private val messagingTemplate: SimpMessagingTemplate
) {
private val messageHistory = ArrayList<PlayerBean>()
@MessageMapping("/player.update")
@SendTo("/topic/player.updates")
fun handlePlayerUpdate(playerUpdateMessage: PlayerUpdateMessage): PlayerUpdateMessage {
// Create a PlayerBean from the update message
val playerBean = PlayerBean(
id = playerUpdateMessage.playerId,
name = playerUpdateMessage.name,
firstName = playerUpdateMessage.firstName,
club = playerUpdateMessage.club
)
// Update the player in the database
playerService.updatePlayer(playerBean.id ?: 0, playerBean)
// Return the update message to be broadcast to all subscribers
return playerUpdateMessage
}
/**
* Broadcast a player update to all connected clients
* This method can be called from other services
*/
fun broadcastPlayerUpdate(playerBean: PlayerBean, type: PlayerUpdateMessage.UpdateType = PlayerUpdateMessage.UpdateType.PLAYER_UPDATE) {
val updateMessage = PlayerUpdateMessage.fromPlayerBean(playerBean, type)
messagingTemplate.convertAndSend("/topic/player.updates", updateMessage)
}
@EventListener
fun handleWebSocketSubscribeListener(event: SessionSubscribeEvent) {
val headerAccessor = StompHeaderAccessor.wrap(event.message)
if ("/topic/player.updates" == headerAccessor.destination) {
// When a client subscribes, send them the current players
val players = playerService.getAll()
players.forEach { player ->
messagingTemplate.convertAndSend(
"/topic/player.updates",
PlayerUpdateMessage.fromPlayerBean(player, PlayerUpdateMessage.UpdateType.PLAYER_UPDATE)
)
}
}
}
@MessageMapping
fun addPlayerToMainList(playerBean: PlayerBean) {
playerService.addPlayer(playerBean)
broadcastPlayerUpdate(playerBean, PlayerUpdateMessage.UpdateType.PLAYER_CREATE)
}
}

View file

@ -0,0 +1,66 @@
package fr.teamflash.fencerjudgeback.websocket.controllers
import fr.teamflash.fencerjudgeback.config.CHANNEL_NAME
import fr.teamflash.fencerjudgeback.entities.RefereeBean
import fr.teamflash.fencerjudgeback.services.RefereeService
import fr.teamflash.fencerjudgeback.websocket.models.RefereeUpdateMessage
import org.springframework.context.event.EventListener
import org.springframework.messaging.handler.annotation.MessageMapping
import org.springframework.messaging.handler.annotation.SendTo
import org.springframework.messaging.simp.SimpMessagingTemplate
import org.springframework.messaging.simp.stomp.StompHeaderAccessor
import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.socket.messaging.SessionSubscribeEvent
@Controller
@RequestMapping("/ws")
class RefereeWebSocketController(
private val refereeService: RefereeService,
private val messagingTemplate: SimpMessagingTemplate
) {
private val messageHistory = ArrayList<RefereeBean>()
@MessageMapping("/referee.update")
@SendTo("/topic/referee.updates")
fun handleRefereeUpdate(refereeUpdateMessage: RefereeUpdateMessage): RefereeUpdateMessage {
// Create a RefereeBean from the update message
val refereeBean = RefereeBean(
id = refereeUpdateMessage.refereeId,
name = refereeUpdateMessage.name,
firstName = refereeUpdateMessage.firstName,
qualification = refereeUpdateMessage.qualification
)
// Update the referee in the database
refereeService.updateReferee(refereeBean.id ?: 0, refereeBean)
// Return the update message to be broadcast to all subscribers
return refereeUpdateMessage
}
/**
* Broadcast a referee update to all connected clients
* This method can be called from other services
*/
fun broadcastRefereeUpdate(refereeBean: RefereeBean, type: RefereeUpdateMessage.UpdateType = RefereeUpdateMessage.UpdateType.REFEREE_UPDATE) {
val updateMessage = RefereeUpdateMessage.fromRefereeBean(refereeBean, type)
messagingTemplate.convertAndSend("/topic/referee.updates", updateMessage)
}
@EventListener
fun handleWebSocketSubscribeListener(event: SessionSubscribeEvent) {
val headerAccessor = StompHeaderAccessor.wrap(event.message)
if ("/topic/referee.updates" == headerAccessor.destination) {
// When a client subscribes, send them the current referees
val referees = refereeService.getAll()
referees.forEach { referee ->
messagingTemplate.convertAndSend(
"/topic/referee.updates",
RefereeUpdateMessage.fromRefereeBean(referee, RefereeUpdateMessage.UpdateType.REFEREE_UPDATE)
)
}
}
}
}

View file

@ -0,0 +1,43 @@
package fr.teamflash.fencerjudgeback.websocket.models
import fr.teamflash.fencerjudgeback.entities.MatchBean
/**
* Message model for match updates sent through WebSocket
*/
data class MatchUpdateMessage(
val matchId: Long,
val player1Id: Long?,
val player2Id: Long?,
val refereeId: Long?,
val score1: Int,
val score2: Int,
val date: String?,
val type: UpdateType = UpdateType.SCORE_UPDATE
) {
enum class UpdateType {
SCORE_UPDATE,
MATCH_START,
MATCH_END,
MATCH_CANCEL,
NEW_MATCH,
}
companion object {
/**
* Create a MatchUpdateMessage from a MatchBean
*/
fun fromMatchBean(matchBean: MatchBean, type: UpdateType = UpdateType.SCORE_UPDATE): MatchUpdateMessage {
return MatchUpdateMessage(
matchId = matchBean.id ?: 0,
player1Id = matchBean.player1ID,
player2Id = matchBean.player2ID,
refereeId = matchBean.refereeID,
score1 = matchBean.score1,
score2 = matchBean.score2,
date = matchBean.date,
type = type
)
}
}
}

View file

@ -0,0 +1,35 @@
package fr.teamflash.fencerjudgeback.websocket.models
import fr.teamflash.fencerjudgeback.entities.PlayerBean
/**
* Message model for player updates sent through WebSocket
*/
data class PlayerUpdateMessage(
val playerId: Long,
val name: String?,
val firstName: String?,
val club: String?,
val type: UpdateType = UpdateType.PLAYER_UPDATE
) {
enum class UpdateType {
PLAYER_UPDATE,
PLAYER_CREATE,
PLAYER_DELETE
}
companion object {
/**
* Create a PlayerUpdateMessage from a PlayerBean
*/
fun fromPlayerBean(playerBean: PlayerBean, type: UpdateType = UpdateType.PLAYER_UPDATE): PlayerUpdateMessage {
return PlayerUpdateMessage(
playerId = playerBean.id ?: 0,
name = playerBean.name,
firstName = playerBean.firstName,
club = playerBean.club,
type = type
)
}
}
}

View file

@ -0,0 +1,35 @@
package fr.teamflash.fencerjudgeback.websocket.models
import fr.teamflash.fencerjudgeback.entities.RefereeBean
/**
* Message model for referee updates sent through WebSocket
*/
data class RefereeUpdateMessage(
val refereeId: Long,
val name: String?,
val firstName: String?,
val qualification: String?,
val type: UpdateType = UpdateType.REFEREE_UPDATE
) {
enum class UpdateType {
REFEREE_UPDATE,
REFEREE_CREATE,
REFEREE_DELETE
}
companion object {
/**
* Create a RefereeUpdateMessage from a RefereeBean
*/
fun fromRefereeBean(refereeBean: RefereeBean, type: UpdateType = UpdateType.REFEREE_UPDATE): RefereeUpdateMessage {
return RefereeUpdateMessage(
refereeId = refereeBean.id ?: 0,
name = refereeBean.name,
firstName = refereeBean.firstName,
qualification = refereeBean.qualification,
type = type
)
}
}
}

View file

@ -1 +1,16 @@
spring.application.name=FencerJudgeBack spring.application.name=FencerJudgeBack
# H2 Database Configuration
spring.datasource.url=jdbc:h2:file:./db;AUTO_SERVER=true
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
# Enable H2 Console
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
# Hibernate Configuration
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true

View file

@ -0,0 +1,87 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Test WebSocket Match</title>
<style>
#messageArea {
width: 100%;
height: 300px;
border: 1px solid #ddd;
overflow-y: auto;
padding: 10px;
margin-bottom: 10px;
font-family: monospace;
}
input, button {
padding: 8px;
margin: 4px;
}
.input-group {
margin-bottom: 10px;
}
</style>
</head>
<body>
<h2>Test WebSocket Match</h2>
<div id="messageArea">Connexion...</div>
<div class="input-group">
<input type="number" id="matchId" placeholder="Match ID">
<input type="number" id="player1Id" placeholder="Joueur 1 ID">
<input type="number" id="player2Id" placeholder="Joueur 2 ID">
<input type="number" id="refereeId" placeholder="Arbitre ID">
</div>
<div class="input-group">
<input type="number" id="score1" placeholder="Score Joueur 1">
<input type="number" id="score2" placeholder="Score Joueur 2">
<input type="datetime-local" id="matchDate">
</div>
<button onclick="sendMatch()">Envoyer Match</button>
<script src="https://cdn.jsdelivr.net/npm/sockjs-client/dist/sockjs.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>
<script>
const stompClient = Stomp.over(new SockJS('/ws/matches-app'));
const channel = "/ws/topic";
stompClient.connect({}, function () {
stompClient.subscribe(channel, function (message) {
const msg = JSON.parse(message.body);
console.log("Match reçu :", msg);
displayMessage(msg);
});
document.getElementById('messageArea').textContent = 'Connecté au WebSocket';
}, function (error) {
document.getElementById('messageArea').textContent = 'Erreur WebSocket : ' + error;
});
function sendMatch() {
const match = {
matchId: parseInt(document.getElementById("matchId").value),
player1Id: parseInt(document.getElementById("player1Id").value),
player2Id: parseInt(document.getElementById("player2Id").value),
refereeId: parseInt(document.getElementById("refereeId").value),
score1: parseInt(document.getElementById("score1").value),
score2: parseInt(document.getElementById("score2").value),
date: document.getElementById("matchDate").value
};
stompClient.send("/ws/matches/add", {}, JSON.stringify(match));
}
function displayMessage(match) {
const area = document.getElementById("messageArea");
const div = document.createElement("div");
div.textContent = `Match ${match.matchId}: ${match.player1Id} (${match.score1}) vs ${match.player2Id} (${match.score2})`;
area.appendChild(div);
}
</script>
</body>
</html>

138
FencerJudgeBack/test.http Normal file
View file

@ -0,0 +1,138 @@
### Get all matches
GET http://localhost:8080/matches/
### Get matches by players --> TODO
GET http://localhost:8080/matches/players/1/2
### Get matches by date
GET http://localhost:8080/matches/date/2025-06-02
### Get matches by country
GET http://localhost:8080/matches/country/France
### Get matches by city
GET http://localhost:8080/matches/city/Paris
### Get active matches
GET http://localhost:8080/matches/active
### Get finished matches
GET http://localhost:8080/matches/over
### Get not started matches
GET http://localhost:8080/matches/not-started
### Get match by ID
GET http://localhost:8080/matches/2
### Create new match
POST http://localhost:8080/matches/create-match
Content-Type: application/json
{
"weapon": "Épée",
"country": "France",
"city": "Paris",
"player1ID": 1,
"player2ID": 2,
"refereeID": 1,
"score1": 0,
"score2": 0,
"date": "2025-06-02",
"state": "NOT STARTED"
}
### Update match
PUT http://localhost:8080/matches/update-match/4
Content-Type: application/json
{
"weapon": "Épée",
"country": "France",
"city": "Paris",
"player1ID": 1,
"player2ID": 2,
"refereeID": 1,
"score1": 5,
"score2": 3,
"date": "2025-06-02",
"state": "OVER"
}
### Delete match
DELETE http://localhost:8080/matches/delete-match/3
### Get all referees
GET http://localhost:8080/referees/
### Get referee by ID
GET http://localhost:8080/referees/1
### Get referees by name
GET http://localhost:8080/referees/name/Smith
### Get referees by firstname
GET http://localhost:8080/referees/firstname/John
### Get referees by qualification
GET http://localhost:8080/referees/qualification/NATIONAL
### Create new referee
POST http://localhost:8080/referees/create-referee
Content-Type: application/json
{
"name": "Smith",
"firstName": "John",
"qualification": "NATIONAL"
}
### Update referee
PUT http://localhost:8080/referees/update-referee/6
Content-Type: application/json
{
"name": "Smith",
"firstName": "John",
"qualification": "INTERNATIONAL"
}
### Delete referee
DELETE http://localhost:8080/referees/delete-referee/5
### Get all players
GET http://localhost:8080/players/
### Get player by ID
GET http://localhost:8080/players/10
### Get players by name
GET http://localhost:8080/players/name/Doe
### Get players by firstname
GET http://localhost:8080/players/firstname/Jane
### Create new player
POST http://localhost:8080/players/create-player
Content-Type: application/json
{
"name": "Doe",
"firstName": "Jane",
"club": "Paris Escrime Club"
}
### Update player
PUT http://localhost:8080/players/update-player/10
Content-Type: application/json
{
"name": "Doe",
"firstName": "Jane",
"club": "Un autre club"
}
### Delete player
DELETE http://localhost:8080/players/delete-player/10