Skip to content

Commit b13f2ae

Browse files
committed
Introduce BoardDynamicMetadata
1 parent 0160342 commit b13f2ae

File tree

58 files changed

+891
-449
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+891
-449
lines changed

authorization/pom.xml

+6
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@
3535
<artifactId>spring-security-oauth2-resource-server</artifactId>
3636
</dependency>
3737

38+
<dependency>
39+
<!-- Provides ServerHttpSecurity -->
40+
<groupId>org.springframework.security</groupId>
41+
<artifactId>spring-security-config</artifactId>
42+
</dependency>
43+
3844
<dependency>
3945
<groupId>org.springframework.security</groupId>
4046
<artifactId>spring-security-oauth2-jose</artifactId>

authorization/src/main/java/eu/solven/kumite/oauth2/IKumiteOAuth2Constants.java

+3
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,7 @@ public interface IKumiteOAuth2Constants {
44
// https://connect2id.com/products/server/docs/api/token#url
55
String KEY_OAUTH2_ISSUER = "kumite.oauth2.issuer-base-url";
66
String KEY_JWT_SIGNINGKEY = "kumite.oauth2.signing-key";
7+
8+
// Used to generate a signingKey on the fly. Useful for integrationTests
9+
String GENERATE = "GENERATE";
710
}

server/src/main/java/eu/solven/kumite/security/JwtWebFluxSecurity.java authorization/src/main/java/eu/solven/kumite/oauth2/resourceserver/JwtWebFluxSecurity.java

+1-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package eu.solven.kumite.security;
1+
package eu.solven.kumite.oauth2.resourceserver;
22

33
import org.springframework.context.annotation.Bean;
44
import org.springframework.core.Ordered;
@@ -15,7 +15,6 @@
1515
import com.nimbusds.jwt.JWT;
1616

1717
import eu.solven.kumite.app.IKumiteSpringProfiles;
18-
import eu.solven.kumite.oauth2.resourceserver.KumiteResourceServerConfiguration;
1918
import lombok.RequiredArgsConstructor;
2019
import lombok.extern.slf4j.Slf4j;
2120

@@ -95,7 +94,6 @@ public SecurityWebFilterChain configureApi(Environment env,
9594
e.authenticationEntryPoint(authenticationEntryPoint);
9695
})
9796

98-
// .anonymous(a -> a.principal("AnonymousKarateka"))
9997
.build();
10098
}
10199

authorization/src/main/java/eu/solven/kumite/oauth2/resourceserver/KumiteResourceServerConfiguration.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,12 @@ public static OctetSequenceKey loadOAuth2SigningKey(Environment env, IUuidGenera
5454
throw new IllegalStateException("Lack proper `" + IKumiteOAuth2Constants.KEY_JWT_SIGNINGKEY
5555
+ "` or spring.profiles.active="
5656
+ IKumiteSpringProfiles.P_UNSAFE_SERVER);
57-
} else if ("GENERATE".equals(secretKeySpec)) {
57+
} else if (IKumiteOAuth2Constants.GENERATE.equals(secretKeySpec)) {
5858
if (env.acceptsProfiles(Profiles.of(IKumiteSpringProfiles.P_PRDMODE))) {
5959
throw new IllegalStateException("Can not GENERATE oauth2 signingKey in `prodmode`");
6060
}
61+
// Ensure we generate a signingKey only once, so that the key in IJwtDecoder and the token in Bearer token
62+
// are based on the same signingKey
6163
synchronized (secretKeySpec) {
6264
if (GENERATED_SIGNINGKEY.get() == null) {
6365
log.warn("We generate a random signingKey");

server/src/main/java/eu/solven/kumite/security/RsaKeyConfigProperties.java authorization/src/test/java/authorization/RsaKeyConfigProperties.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package eu.solven.kumite.security;
1+
package authorization;
22

33
import java.security.interfaces.RSAPrivateKey;
44
import java.security.interfaces.RSAPublicKey;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package authorization;
2+
3+
import java.text.ParseException;
4+
5+
import org.assertj.core.api.Assertions;
6+
import org.junit.jupiter.api.Test;
7+
import org.springframework.mock.env.MockEnvironment;
8+
9+
import com.nimbusds.jose.jwk.OctetSequenceKey;
10+
11+
import eu.solven.kumite.oauth2.IKumiteOAuth2Constants;
12+
import eu.solven.kumite.oauth2.resourceserver.KumiteResourceServerConfiguration;
13+
import eu.solven.kumite.tools.JdkUuidGenerator;
14+
15+
public class TestKumiteResourceServerConfiguration {
16+
KumiteResourceServerConfiguration conf = new KumiteResourceServerConfiguration();
17+
18+
@Test
19+
public void testGenerateMultipleTimes() throws ParseException {
20+
MockEnvironment env = new MockEnvironment();
21+
env.setProperty(IKumiteOAuth2Constants.KEY_JWT_SIGNINGKEY, IKumiteOAuth2Constants.GENERATE);
22+
23+
OctetSequenceKey key1 = KumiteResourceServerConfiguration.loadOAuth2SigningKey(env, JdkUuidGenerator.INSTANCE);
24+
OctetSequenceKey key2 = KumiteResourceServerConfiguration.loadOAuth2SigningKey(env, JdkUuidGenerator.INSTANCE);
25+
26+
Assertions.assertThat(key1.toJSONString()).isEqualTo(key2.toJSONString());
27+
}
28+
}

server/src/test/java/eu/solven/kumite/account/login/TestKumiteTokenService.java authorization/src/test/java/eu/solven/kumite/oauth2/authorizationserver/TestKumiteTokenService.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package eu.solven.kumite.account.login;
1+
package eu.solven.kumite.oauth2.authorizationserver;
22

33
import java.text.ParseException;
44
import java.time.Duration;
@@ -23,8 +23,8 @@
2323
import com.nimbusds.jwt.SignedJWT;
2424

2525
import eu.solven.kumite.account.internal.KumiteUser;
26+
import eu.solven.kumite.account.login.IKumiteTestConstants;
2627
import eu.solven.kumite.oauth2.IKumiteOAuth2Constants;
27-
import eu.solven.kumite.oauth2.authorizationserver.KumiteTokenService;
2828
import eu.solven.kumite.oauth2.resourceserver.KumiteResourceServerConfiguration;
2929
import eu.solven.kumite.tools.JdkUuidGenerator;
3030

contest-core/src/main/java/eu/solven/kumite/app/persistence/InMemoryKumiteConfiguration.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import eu.solven.kumite.account.InMemoryUserRepository;
77
import eu.solven.kumite.app.IKumiteSpringProfiles;
8+
import eu.solven.kumite.board.persistence.InMemoryBoardMetadataRepository;
89
import eu.solven.kumite.board.persistence.InMemoryBoardRepository;
910
import eu.solven.kumite.contest.persistence.InMemoryContestRepository;
1011
import eu.solven.kumite.player.persistence.InMemoryAccountPlayersRegistry;
@@ -16,10 +17,11 @@
1617
InMemoryAccountPlayersRegistry.class,
1718

1819
InMemoryBoardRepository.class,
20+
InMemoryBoardMetadataRepository.class,
1921
InMemoryContestRepository.class,
2022

2123
})
2224
@Profile(IKumiteSpringProfiles.P_INMEMORY)
2325
public class InMemoryKumiteConfiguration {
24-
26+
2527
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package eu.solven.kumite.board;
2+
3+
import java.time.OffsetDateTime;
4+
import java.time.ZoneOffset;
5+
import java.util.Map;
6+
import java.util.TreeMap;
7+
import java.util.UUID;
8+
9+
import lombok.Builder;
10+
import lombok.Builder.Default;
11+
import lombok.Value;
12+
import lombok.extern.jackson.Jacksonized;
13+
14+
/**
15+
* The set of board metadata which could evolve, and are not stored into the {@link IKumiteBoard}, independently of
16+
* current player.
17+
*
18+
* @author Benoit Lacelle
19+
*
20+
*/
21+
@Value
22+
@Builder
23+
@Jacksonized
24+
public class BoardDynamicMetadata {
25+
26+
@Default
27+
Map<UUID, OffsetDateTime> playerIdToLastMove = new TreeMap<>();
28+
29+
OffsetDateTime gameOverTs;
30+
31+
public BoardDynamicMetadata setGameOver() {
32+
if (gameOverTs != null) {
33+
// We are already gameOver: do not update its value
34+
return this;
35+
}
36+
37+
return BoardDynamicMetadata.builder()
38+
.playerIdToLastMove(playerIdToLastMove)
39+
.gameOverTs(OffsetDateTime.now().atZoneSameInstant(ZoneOffset.UTC).toOffsetDateTime())
40+
.build();
41+
}
42+
43+
}

contest-core/src/main/java/eu/solven/kumite/board/BoardLifecycleManager.java

+73-44
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import java.util.concurrent.Executor;
1010
import java.util.concurrent.TimeUnit;
1111
import java.util.concurrent.atomic.AtomicReference;
12+
import java.util.function.Consumer;
1213
import java.util.random.RandomGenerator;
1314
import java.util.stream.Collectors;
1415

@@ -48,20 +49,22 @@ public class BoardLifecycleManager {
4849
*
4950
* @param contestId
5051
* @param runnable
52+
* @return
5153
*/
52-
protected void executeBoardChange(UUID contestId, Runnable runnable) {
54+
protected BoardSnapshotPostEvent executeBoardChange(UUID contestId, Consumer<IKumiteBoard> runnable) {
5355
if (isDirect(boardEvolutionExecutor)) {
54-
boardEvolutionExecutor.execute(runnable);
56+
return executeBoardMutation(contestId, runnable);
5557
} else {
5658
CountDownLatch cdl = new CountDownLatch(1);
5759
AtomicReference<Throwable> refT = new AtomicReference<>();
60+
AtomicReference<BoardSnapshotPostEvent> refBoard = new AtomicReference<>();
5861

5962
log.trace("Submitting task for contestId={}", contestId);
6063
getExecutor(contestId).execute(() -> {
6164
try {
62-
log.trace("Runnning task for contestId={}", contestId);
63-
runnable.run();
64-
log.trace("Runnned task for contestId={}", contestId);
65+
BoardSnapshotPostEvent snapshot = executeBoardMutation(contestId, runnable);
66+
67+
refBoard.set(snapshot);
6568
} catch (Throwable t) {
6669
refT.compareAndSet(null, t);
6770
} finally {
@@ -90,9 +93,33 @@ protected void executeBoardChange(UUID contestId, Runnable runnable) {
9093
// BEWARE WHAT DOES IT MEAN? THE BOARD IS CORRUPTED?
9194
throw new IllegalStateException("One move has been too slow to be processed");
9295
}
96+
97+
return refBoard.get();
9398
}
9499
}
95100

101+
private BoardSnapshotPostEvent executeBoardMutation(UUID contestId, Consumer<IKumiteBoard> runnable) {
102+
IHasBoard hasBoard = boardRegistry.makeDynamicBoardHolder(contestId);
103+
IKumiteBoard board = hasBoard.get();
104+
Set<UUID> playerCanMoveBefore = playersCanMove(contestId, board);
105+
106+
log.trace("Runnning task for contestId={}", contestId);
107+
runnable.accept(board);
108+
109+
Set<UUID> enabledPlayersIds = new HashSet<>();
110+
{
111+
Set<UUID> playerCanMoveAfter = playersCanMove(contestId, board);
112+
113+
// This does an intersection: players turned movable can now move, through they could not move
114+
// before
115+
enabledPlayersIds.addAll(playerCanMoveAfter);
116+
enabledPlayersIds.removeAll(playerCanMoveBefore);
117+
}
118+
119+
log.trace("Runnned task for contestId={}", contestId);
120+
return BoardSnapshotPostEvent.builder().board(board).enabledPlayerIds(enabledPlayersIds).build();
121+
}
122+
96123
/**
97124
*
98125
* @param contestId
@@ -118,25 +145,11 @@ public IKumiteBoardView registerPlayer(Contest contest, PlayerJoinRaw playerRegi
118145

119146
AtomicReference<IKumiteBoardView> refBoardView = new AtomicReference<>();
120147

121-
Set<UUID> enabledPlayersIds = new HashSet<>();
122-
123-
executeBoardChange(contestId, () -> {
124-
IKumiteBoard boardBefore = boardRegistry.makeDynamicBoardHolder(contestId).get();
125-
126-
Set<UUID> playerCanMoveBefore = playersCanMove(contestId, boardBefore);
127-
128-
// The registry takes in charge the registration in the board
148+
BoardSnapshotPostEvent boardSnapshot = executeBoardChange(contestId, board -> {
149+
// contestPlayersRegistry takes in charge the registration in the board
129150
contestPlayersRegistry.registerPlayer(contest, playerRegistrationRaw);
130151

131-
IKumiteBoard boardAfter = boardRegistry.makeDynamicBoardHolder(contestId).get();
132-
133-
Set<UUID> playerCanMoveAfter = playersCanMove(contestId, boardBefore);
134-
135-
// This does an intersection: players turned movable can now move, through they could not move before
136-
enabledPlayersIds.addAll(playerCanMoveAfter);
137-
enabledPlayersIds.removeAll(playerCanMoveBefore);
138-
139-
refBoardView.set(boardAfter.asView(playerId));
152+
refBoardView.set(board.asView(playerId));
140153
});
141154

142155
IKumiteBoardView boardViewPostMove = refBoardView.get();
@@ -151,10 +164,14 @@ public IKumiteBoardView registerPlayer(Contest contest, PlayerJoinRaw playerRegi
151164
// Hence we do not guarantee other events interleaved when the event is processed
152165
eventBus.post(PlayerJoinedBoard.builder().contestId(contestId).playerId(playerId).build());
153166

154-
enabledPlayersIds.forEach(enabledPlayerId -> {
167+
boardSnapshot.getEnabledPlayerIds().forEach(enabledPlayerId -> {
155168
eventBus.post(PlayerCanMove.builder().contestId(contestId).playerId(playerId).build());
156169
});
157170

171+
if (boardRegistry.hasGameover(contest.getGame(), contestId).isGameOver()) {
172+
eventBus.post(ContestIsGameover.builder().contestId(contestId).build());
173+
}
174+
158175
return boardViewPostMove;
159176
}
160177

@@ -178,11 +195,7 @@ public IKumiteBoardView onPlayerMove(Contest contest, PlayerMoveRaw playerMove)
178195
UUID contestId = contest.getContestId();
179196
UUID playerId = playerMove.getPlayerId();
180197

181-
AtomicReference<IKumiteBoard> refBoard = new AtomicReference<>();
182-
183-
Set<UUID> enabledPlayersIds = new HashSet<>();
184-
185-
executeBoardChange(contestId, () -> {
198+
BoardSnapshotPostEvent boardSnapshot = executeBoardChange(contestId, currentBoard -> {
186199
if (!contestPlayersRegistry.isRegisteredPlayer(contestId, playerId)) {
187200
List<UUID> contestPlayers = contestPlayersRegistry.makeDynamicHasPlayers(contestId)
188201
.getPlayers()
@@ -196,36 +209,28 @@ public IKumiteBoardView onPlayerMove(Contest contest, PlayerMoveRaw playerMove)
196209
+ contestPlayers);
197210
}
198211

199-
IKumiteBoard currentBoard = boardRegistry.makeDynamicBoardHolder(contestId).get();
200-
201-
Set<UUID> playerCanMoveBefore = playersCanMove(contestId, currentBoard);
202-
203212
// First `.checkMove`: these are generic checks (e.g. is the gamerOver?)
204213
try {
205214
contest.checkValidMove(playerMove);
206215
} catch (IllegalArgumentException e) {
207-
throw new IllegalArgumentException("Issue on contest=" + contest, e);
216+
throw new IllegalArgumentException("Issue on contest=" + contest + " for move=" + playerMove, e);
208217
}
209218

210219
log.info("Registering move for contestId={} by playerId={}", contestId, playerId);
211220

212221
// This may still fail (e.g. the move is illegal given game rules)
213222
currentBoard.registerMove(playerMove);
214223

215-
Set<UUID> playerCanMoveAfter = playersCanMove(contestId, currentBoard);
216-
217-
// This does an intersection: players turned movable can now move, through they could not move before
218-
enabledPlayersIds.addAll(playerCanMoveAfter);
219-
enabledPlayersIds.removeAll(playerCanMoveBefore);
220-
221224
// Persist the board (e.g. for concurrent changes)
222225
boardRegistry.updateBoard(contestId, currentBoard);
223226

224-
refBoard.set(currentBoard);
225-
227+
if (boardRegistry.hasGameover(contest.getGame(), contestId).isGameOver()) {
228+
log.info("playerMove led to gameOver for contestId={}", contestId);
229+
doGameOver(contestId);
230+
}
226231
});
227232

228-
IKumiteBoard boardAfter = refBoard.get();
233+
IKumiteBoard boardAfter = boardSnapshot.getBoard();
229234
IKumiteBoardView boardViewPostMove = boardAfter.asView(playerId);
230235

231236
if (boardViewPostMove == null) {
@@ -234,14 +239,38 @@ public IKumiteBoardView onPlayerMove(Contest contest, PlayerMoveRaw playerMove)
234239

235240
eventBus.post(PlayerMoved.builder().contestId(contestId).playerId(playerId).build());
236241

237-
enabledPlayersIds.forEach(enabledPlayerId -> {
242+
boardSnapshot.getEnabledPlayerIds().forEach(enabledPlayerId -> {
238243
eventBus.post(PlayerCanMove.builder().contestId(contestId).playerId(playerId).build());
239244
});
240245

241-
if (contest.getGame().makeDynamicGameover(() -> boardAfter).isGameOver()) {
246+
if (boardRegistry.hasGameover(contest.getGame(), contestId).isGameOver()) {
242247
eventBus.post(ContestIsGameover.builder().contestId(contestId).build());
243248
}
244249

245250
return boardViewPostMove;
246251
}
252+
253+
public void forceGameOver(Contest contest) {
254+
UUID contestId = contest.getContestId();
255+
256+
if (boardRegistry.hasGameover(contest.getGame(), contestId).isGameOver()) {
257+
log.info("contestId={} is already gameOver", contestId);
258+
}
259+
260+
executeBoardChange(contestId, board -> {
261+
log.info("Registering forcedGameOver for contestId={}", contestId);
262+
263+
doGameOver(contestId);
264+
});
265+
266+
if (boardRegistry.hasGameover(contest.getGame(), contestId).isGameOver()) {
267+
eventBus.post(ContestIsGameover.builder().contestId(contestId).build());
268+
}
269+
}
270+
271+
private void doGameOver(UUID contestId) {
272+
boardRegistry.forceGameover(contestId);
273+
contestsRegistry.deleteContest(contestId);
274+
contestPlayersRegistry.forceGameover(contestId);
275+
}
247276
}

0 commit comments

Comments
 (0)