travail sur websocket

This commit is contained in:
pedro 2025-06-02 14:36:54 +02:00
parent f6e1b6b488
commit 4177bf8c52
16 changed files with 741 additions and 23 deletions

View file

@ -92,3 +92,46 @@
{ {
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,6 +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) prévoir l'utilisation en admin uniquement (spring security)
faire le jackarta de l'api faire le jackarta de l'api

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")
.setAllowedOriginPatterns("*") // Allow connections from any origin (adjust for production)
.withSockJS()
}
}

View file

@ -25,9 +25,9 @@ data class MatchBean(
val player2ID:Long?=null, val player2ID:Long?=null,
val refereeID: Long?=null, val refereeID: Long?=null,
@field:Min(value = 0, message = "Score must be at least 0") @field:Min(value = 0, message = "Score must be at least 0")
val score1:Int=0, var score1:Int=0,
@field:Min(value = 0, message = "Score must be at least 0") @field:Min(value = 0, message = "Score must be at least 0")
val score2:Int=0, var score2:Int=0,
val date: String?=null, val date: String?=null,
val state:String?=null val state:String?=null
) )

View file

@ -39,10 +39,11 @@ 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)
@ -54,7 +55,9 @@ class MatchService(
} }
val updatedMatch = newMatch.copy(id = id) val updatedMatch = newMatch.copy(id = id)
matchRepository.save(updatedMatch) val savedMatch = matchRepository.save(updatedMatch)
// Broadcast the updated match via WebSocket
matchWebSocketController?.broadcastMatchUpdate(savedMatch, fr.teamflash.fencerjudgeback.websocket.models.MatchUpdateMessage.UpdateType.SCORE_UPDATE)
return 1 return 1
} }
@ -62,10 +65,15 @@ class MatchService(
fun deleteMatchById(id: Long?) : Int? { fun deleteMatchById(id: Long?) : Int? {
println("MatchService.deleteMatchById : $id") println("MatchService.deleteMatchById : $id")
if (getById(id) == null) { val match = getById(id)
if (match == null) {
println("MatchService.deleteMatchById : Match not found") println("MatchService.deleteMatchById : Match not found")
return -1 return -1
} }
// Broadcast the match deletion via WebSocket before deleting
matchWebSocketController?.broadcastMatchUpdate(match, fr.teamflash.fencerjudgeback.websocket.models.MatchUpdateMessage.UpdateType.MATCH_CANCEL)
matchRepository.deleteById(id!!) matchRepository.deleteById(id!!)
return 1 return 1
} }
@ -83,4 +91,8 @@ class MatchService(
return matchRepository.findAll() return matchRepository.findAll()
.filter { it.country == country } .filter { it.country == country }
} }
fun addMatch(match:MatchBean) {
matchRepository.save(match)
}
} }

View file

@ -2,11 +2,17 @@ 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
@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("PlayerService.getAll") println("PlayerService.getAll")
@ -43,9 +49,12 @@ class PlayerService(@Autowired private val playerRepository: PlayerRepository) {
} }
// Ajouter un joueur (admin) // Ajouter un joueur (admin)
fun createPlayer(referee: PlayerBean) : PlayerBean { fun createPlayer(player: PlayerBean) : PlayerBean {
println("PlayerService.createPlayer : $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)
@ -57,7 +66,9 @@ class PlayerService(@Autowired private val playerRepository: PlayerRepository) {
} }
val updatedPlayer = newPlayer.copy(id = id) val updatedPlayer = newPlayer.copy(id = id)
playerRepository.save(updatedPlayer) val savedPlayer = playerRepository.save(updatedPlayer)
// Broadcast the updated player via WebSocket
playerWebSocketController?.broadcastPlayerUpdate(savedPlayer, PlayerUpdateMessage.UpdateType.PLAYER_UPDATE)
return 1 return 1
} }
@ -66,12 +77,20 @@ class PlayerService(@Autowired private val playerRepository: PlayerRepository) {
fun deletePlayerById(id:Long?): Int { fun deletePlayerById(id:Long?): Int {
println("PlayerService.deletePlayer : $id") println("PlayerService.deletePlayer : $id")
if (getById(id) == null) { val player = getById(id)
if (player == null) {
println("PlayerService.deletePlayer : Player not found") println("PlayerService.deletePlayer : Player not found")
return -1 return -1
} }
// Broadcast the player deletion via WebSocket before deleting
playerWebSocketController?.broadcastPlayerUpdate(player, PlayerUpdateMessage.UpdateType.PLAYER_DELETE)
playerRepository.deleteById(id!!) playerRepository.deleteById(id!!)
return 1 return 1
} }
}
fun addPlayer(player:PlayerBean) {
playerRepository.save(player)
}
}

View file

@ -2,11 +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.context.annotation.Lazy
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
@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")
@ -45,7 +51,10 @@ class RefereeService(@Autowired private val refereeRepository: RefereeRepository
// 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)
@ -55,8 +64,10 @@ class RefereeService(@Autowired private val refereeRepository: RefereeRepository
return -1 return -1
} }
val updatedMatch = newReferee.copy(id = id) val updatedReferee = newReferee.copy(id = id)
refereeRepository.save(updatedMatch) val savedReferee = refereeRepository.save(updatedReferee)
// Broadcast the updated referee via WebSocket
refereeWebSocketController?.broadcastRefereeUpdate(savedReferee, RefereeUpdateMessage.UpdateType.REFEREE_UPDATE)
return 1 return 1
} }
@ -64,12 +75,16 @@ class RefereeService(@Autowired private val refereeRepository: RefereeRepository
fun deleteRefereeById(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,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'));
const channel = "/topic/match.updates";
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("/app/match.update", {}, 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>

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,101 @@
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.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 MatchWebSocketController(
private val matchService: MatchService,
private val messagingTemplate: SimpMessagingTemplate
) {
private val messageHistory = ArrayList<MatchBean>()
@MessageMapping("/matches")
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("/match.update")
@SendTo("/topic/match.updates")
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)
}
}
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)
}
fun addMatchtoMainList(match:MatchBean) {
matchService.addMatch(match)
broadcastMatchUpdate(match, MatchUpdateMessage.UpdateType.NEW_MATCH)
}
}

View file

@ -0,0 +1,71 @@
package fr.teamflash.fencerjudgeback.websocket.controllers
import fr.teamflash.fencerjudgeback.config.CHANNEL_NAME
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)
)
}
}
}
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
)
}
}
}