Skip to content
This repository was archived by the owner on Dec 27, 2024. It is now read-only.

Commit 2be27e2

Browse files
dev/codeforces/ Добавление Codeforces Api (#7)
* dev/codeforces/ Добавил RestClient * dev/codeforces/ Добавил шаблон для работы с Codeforces Api * dev/codeforces/ Разделил Codeforces пакеты репозиториев и моделей на пакеты teams и users * dev/codeforces/ Сделал бины из сервисов CfTeamService и CfUserService * dev/codeforces/ Удалил неактуальные файлы * dev/codeforces/ Добавил новые модели * dev/codeforces/ Обновил базу данных * dev/codeforces/ Обновил базу данных 2 * dev/codeforces/ Сделал репозитории для новых моделей * dev/codeforces/ Добавил payload для новых моделей * dev/codeforces/ Добавил сервисы для новых моделей * dev/codeforces/ Добавил контроллеры для новых моделей * dev/codeforces/ Добавил маппер для Group * dev/codeforces/ Добавил исключения для Group * dev/codeforces/ Добавил приватный конструктор для GroupMapper * dev/codeforces/ Исправил классы по checkStyle * dev/codeforces/ Добавил ControllerAdvice для Codeforces Exceptions * dev/codeforces/ Удалил лишние импорты * dev/codeforces/ Отрефакторил util классы * dev/codeforces/test Добавил модульные тесты для GroupService * dev/codeforces/test Добавил модульные тесты для GroupMapper * dev/codeforces/test Немного отрефакторил GroupServiceTest * dev/codeforces/test Добавил unit тесты для GroupController * dev/codeforces/test Добавил lombock для тестов и замеил throws на @SneakyThrows * dev/codeforces/test Написал интеграционные тесты для GroupController * dev/codeforces/test Исправил интеграционные тесты и код под checkStyle * dev/codeforces/ Добавил Response классы для моделей * dev/codeforces/ Добавил кэширование для запроса получения рейтинга пользователя * dev/codeforces/ Добавил ошибки для работы с API * dev/codeforces/ Немного изменил модели бд * dev/codeforces/ Добавил мапперы для превращения модели в response * dev/codeforces/ Добавил сервисы для моделей * dev/codeforces/ Добавил DTO для api * dev/codeforces/ Добавил контроллеры * dev/codeforces/ Актуализировал тесты * dev/codeforces/ Исправил код под checkStyle * dev/codeforces/ Добавил логику для Team * dev/codeforces/ Добавил логику для Players * dev/codeforces/ Отрефакторил код под checkStyle * dev/codeforces/ Добавил тесты для TeamMapper * dev/codeforces/ Добавил тесты для PlayerMapper * dev/codeforces/ Добавил тесты для RatingCalculator * dev/codeforces/test Удалил лишние импорты * dev/codeforces/test Удалил лишние импорты * dev/codeforces/test Написать unit тесты для TeamController * dev/codeforces/test Написать unit тесты для PlayerController * dev/codeforces/test Отрефакторил GroupServiceTest * dev/codeforces/test Написал unit тесты для Teamservice * dev/codeforces/test Удалил лишние импорты * dev/codeforces/test Исправил ошибку в названии теста * dev/codeforces/test Написат unit тесты для PlayerService * dev/codeforces/ref удалил лишние импорты * dev/codeforces/test Написал интеграционные тесты для TeamController * dev/codeforces/test Исправил тесты и код под checkStyle * dev/codeforces/test Исправил тест deleteTeam_successв TeamControllerIntegrationTest * dev/codeforces/test Написал тесты для CodeforcesClient * dev/codeforces/test Немного отрефакторил код * dev/codeforces/test Добавил профиль test тесту CodeforcesClientTest * dev/codeforces/test попытка исправить CodeforcesClientTest * dev/codeforces/test Написал интеграционные тесты для PlayerControllerIntegration
1 parent b512d08 commit 2be27e2

File tree

75 files changed

+3899
-210
lines changed

Some content is hidden

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

75 files changed

+3899
-210
lines changed

build.gradle

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,18 @@ dependencies {
4444
implementation 'io.jsonwebtoken:jjwt-impl:0.12.6'
4545
implementation 'io.jsonwebtoken:jjwt-jackson:0.12.6'
4646

47+
// cache
48+
implementation 'com.github.ben-manes.caffeine:caffeine:3.1.8'
49+
4750
// test
4851
testImplementation 'org.springframework.boot:spring-boot-starter-test'
4952
testImplementation 'org.testcontainers:junit-jupiter:1.20.1'
5053
testImplementation 'org.wiremock:wiremock-standalone:3.9.1'
5154
testImplementation 'org.wiremock.integrations.testcontainers:wiremock-testcontainers-module:1.0-alpha-14'
5255
testImplementation 'org.testcontainers:postgresql:1.20.3'
5356
testImplementation 'org.liquibase:liquibase-core:4.29.2'
57+
testImplementation 'org.projectlombok:lombok'
58+
testAnnotationProcessor 'org.projectlombok:lombok'
5459
}
5560

5661
tasks.named('test') {

config/checkstyle/checkstyle.xml

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,12 @@
5656

5757
<!-- https://checkstyle.org/config_misc.html -->
5858
<!-- Check for trailing spaces. -->
59-
<module name="RegexpSingleline">
60-
<property name="format" value="\s+$"/>
61-
<property name="minimum" value="0"/>
62-
<property name="maximum" value="0"/>
63-
<property name="message" value="Line has trailing spaces."/>
64-
</module>
59+
<!-- <module name="RegexpSingleline">-->
60+
<!-- <property name="format" value="\s+$"/>-->
61+
<!-- <property name="minimum" value="0"/>-->
62+
<!-- <property name="maximum" value="0"/>-->
63+
<!-- <property name="message" value="Line has trailing spaces."/>-->
64+
<!-- </module>-->
6565

6666

6767
<!-- https://checkstyle.org/config_filters.html -->
@@ -277,12 +277,12 @@
277277
<!-- <module name="IllegalTokenText"/> -->
278278
<!-- <module name="IllegalType"/> -->
279279
<module name="InnerAssignment"/>
280-
<module name="MagicNumber">
281-
<property name="ignoreHashCodeMethod" value="true"/>
282-
<property name="ignoreAnnotation" value="true"/>
283-
<message key="magic.number"
284-
value="Most likely the value ''{0}'' is a configuration one. You should move it to configuration file instead of source code."/>
285-
</module>
280+
<!-- <module name="MagicNumber">-->
281+
<!-- <property name="ignoreHashCodeMethod" value="true"/>-->
282+
<!-- <property name="ignoreAnnotation" value="true"/>-->
283+
<!-- <message key="magic.number"-->
284+
<!-- value="Most likely the value ''{0}'' is a configuration one. You should move it to configuration file instead of source code."/>-->
285+
<!-- </module>-->
286286
<!-- <module name="MissingCtor"/> -->
287287
<module name="MissingSwitchDefault"/>
288288
<module name="ModifiedControlVariable"/>
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package com.cf.cfteam.advicers.codeforces;
2+
3+
import com.cf.cfteam.exceptions.codeforces.*;
4+
import com.cf.cfteam.utils.ErrorResponseBuilder;
5+
import org.springframework.http.HttpStatus;
6+
import org.springframework.http.ResponseEntity;
7+
import org.springframework.web.bind.annotation.ControllerAdvice;
8+
import org.springframework.web.bind.annotation.ExceptionHandler;
9+
10+
import java.util.HashMap;
11+
import java.util.Map;
12+
13+
@ControllerAdvice
14+
public class CodeforcesExceptionHandler {
15+
private static final String ID = "id";
16+
private static final String LOGIN = "login";
17+
private static final String TEAM_ID = "teamId";
18+
private static final String PLAYER_ID = "playerId";
19+
20+
@ExceptionHandler(GroupNotFoundException.class)
21+
public ResponseEntity<Object> handleUserNotFoundException(GroupNotFoundException ex) {
22+
return ErrorResponseBuilder.buildErrorResponse(ex.getMessage(), HttpStatus.NOT_FOUND, Map.of(ID, ex.getId()));
23+
}
24+
25+
@ExceptionHandler(TeamNotFoundException.class)
26+
public ResponseEntity<Object> handleTeamNotFoundException(TeamNotFoundException ex) {
27+
return ErrorResponseBuilder.buildErrorResponse(ex.getMessage(), HttpStatus.NOT_FOUND, Map.of(ID, ex.getId()));
28+
}
29+
30+
@ExceptionHandler(PlayerNotFoundException.class)
31+
public ResponseEntity<Object> handlePayerNotFoundException(PlayerNotFoundException ex) {
32+
return ErrorResponseBuilder.buildErrorResponse(ex.getMessage(), HttpStatus.NOT_FOUND, Map.of(ID, ex.getId()));
33+
}
34+
35+
@ExceptionHandler(PlayerAlreadyInTeamException.class)
36+
public ResponseEntity<Object> handlePlayerAlreadyInTeamException(PlayerAlreadyInTeamException ex) {
37+
return ErrorResponseBuilder.buildErrorResponse(ex.getMessage(), HttpStatus.CONFLICT,
38+
Map.of(LOGIN, ex.getLogin()));
39+
}
40+
41+
@ExceptionHandler(PlayerNotFromTeamException.class)
42+
public ResponseEntity<Object> handlePlayerNotFromTeamException(PlayerNotFromTeamException ex) {
43+
Map<String, Object> details = new HashMap<>();
44+
if (ex.getPlayerId() != null) {
45+
details.put(PLAYER_ID, ex.getPlayerId());
46+
}
47+
if (ex.getTeamId() != null) {
48+
details.put(TEAM_ID, ex.getTeamId());
49+
}
50+
51+
return ErrorResponseBuilder.buildErrorResponse(
52+
ex.getMessage(),
53+
HttpStatus.CONFLICT,
54+
details
55+
);
56+
}
57+
}

src/main/java/com/cf/cfteam/advicers/security/SecurityExceptionHandler.java

Lines changed: 20 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@
44
import com.cf.cfteam.exceptions.security.TokenRevokedException;
55
import com.cf.cfteam.exceptions.security.UserAlreadyRegisterException;
66
import com.cf.cfteam.exceptions.security.UserNotFoundException;
7+
import com.cf.cfteam.utils.ErrorResponseBuilder;
78
import org.springframework.http.HttpStatus;
89
import org.springframework.http.ResponseEntity;
910
import org.springframework.web.bind.annotation.ControllerAdvice;
1011
import org.springframework.web.bind.annotation.ExceptionHandler;
1112

12-
import java.time.LocalDateTime;
1313
import java.util.HashMap;
1414
import java.util.Map;
1515

@@ -18,44 +18,39 @@ public class SecurityExceptionHandler {
1818

1919
private static final String LOGIN = "login";
2020
private static final String TOKEN = "token";
21-
private static final String UNEXPECTED_ERROR = "unexpected.error";
21+
private static final String ID = "id";
2222

2323
@ExceptionHandler(UserAlreadyRegisterException.class)
2424
public ResponseEntity<Object> handleUserAlreadyRegisterException(UserAlreadyRegisterException ex) {
25-
return buildErrorResponse(ex.getMessage(), HttpStatus.CONFLICT, Map.of(LOGIN, ex.getLogin()));
25+
return ErrorResponseBuilder.buildErrorResponse(ex.getMessage(), HttpStatus.CONFLICT, Map.of(LOGIN,
26+
ex.getLogin()));
2627
}
2728

2829
@ExceptionHandler(UserNotFoundException.class)
2930
public ResponseEntity<Object> handleUserNotFoundException(UserNotFoundException ex) {
30-
return buildErrorResponse(ex.getMessage(), HttpStatus.NOT_FOUND, Map.of(LOGIN, ex.getLogin()));
31+
Map<String, Object> details = new HashMap<>();
32+
if (ex.getLogin() != null) {
33+
details.put(LOGIN, ex.getLogin());
34+
}
35+
if (ex.getId() != null) {
36+
details.put(ID, ex.getId());
37+
}
38+
39+
return ErrorResponseBuilder.buildErrorResponse(
40+
ex.getMessage(),
41+
HttpStatus.NOT_FOUND,
42+
details
43+
);
3144
}
3245

3346
@ExceptionHandler(TokenRevokedException.class)
3447
public ResponseEntity<Object> handleTokenRevokedException(TokenRevokedException ex) {
35-
return buildErrorResponse(ex.getMessage(), HttpStatus.UNAUTHORIZED, Map.of(TOKEN, ex.getToken()));
48+
return ErrorResponseBuilder.buildErrorResponse(ex.getMessage(), HttpStatus.UNAUTHORIZED, Map.of(TOKEN,
49+
ex.getToken()));
3650
}
3751

3852
@ExceptionHandler(InvalidTwoFactorCodeException.class)
3953
public ResponseEntity<Object> handleInvalidTwoFactorCodeException(InvalidTwoFactorCodeException ex) {
40-
return buildErrorResponse(ex.getMessage(), HttpStatus.BAD_REQUEST, null);
41-
}
42-
43-
@ExceptionHandler(Exception.class)
44-
public ResponseEntity<Object> handleGenericException(Exception ex) {
45-
return buildErrorResponse(UNEXPECTED_ERROR, HttpStatus.INTERNAL_SERVER_ERROR, null);
46-
}
47-
48-
private ResponseEntity<Object> buildErrorResponse(String message, HttpStatus status, Map<String, Object> details) {
49-
Map<String, Object> errorResponse = new HashMap<>();
50-
errorResponse.put("timestamp", LocalDateTime.now());
51-
errorResponse.put("status", status.value());
52-
errorResponse.put("error", status.getReasonPhrase());
53-
errorResponse.put("message", message);
54-
55-
if (details != null) {
56-
errorResponse.put("details", details);
57-
}
58-
59-
return new ResponseEntity<>(errorResponse, status);
54+
return ErrorResponseBuilder.buildErrorResponse(ex.getMessage(), HttpStatus.BAD_REQUEST, null);
6055
}
6156
}

src/main/java/com/cf/cfteam/config/AppConfig.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,19 @@
22

33
import com.cf.cfteam.services.security.MyUserDetailsService;
44
import lombok.RequiredArgsConstructor;
5+
import org.springframework.beans.factory.annotation.Value;
56
import org.springframework.context.annotation.Bean;
67
import org.springframework.context.annotation.Configuration;
8+
import org.springframework.http.HttpHeaders;
9+
import org.springframework.http.MediaType;
710
import org.springframework.security.authentication.AuthenticationManager;
811
import org.springframework.security.authentication.AuthenticationProvider;
912
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
1013
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
1114
import org.springframework.security.core.userdetails.UserDetailsService;
1215
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
1316
import org.springframework.security.crypto.password.PasswordEncoder;
17+
import org.springframework.web.client.RestClient;
1418

1519

1620
@Configuration
@@ -41,4 +45,12 @@ public UserDetailsService userDetailsService() {
4145
public PasswordEncoder passwordEncoder() {
4246
return new BCryptPasswordEncoder();
4347
}
48+
49+
@Bean
50+
public RestClient restClient(@Value("${codeforces.api.base.url}") String url) {
51+
return RestClient.builder()
52+
.baseUrl(url)
53+
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
54+
.build();
55+
}
4456
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.cf.cfteam.config;
2+
3+
import com.cf.cfteam.services.client.CodeforcesClient;
4+
import com.github.benmanes.caffeine.cache.Caffeine;
5+
import com.github.benmanes.caffeine.cache.LoadingCache;
6+
import lombok.RequiredArgsConstructor;
7+
import org.springframework.context.annotation.Bean;
8+
import org.springframework.context.annotation.Configuration;
9+
10+
import java.util.concurrent.TimeUnit;
11+
12+
@Configuration
13+
@RequiredArgsConstructor
14+
public class CacheConfig {
15+
16+
private final CodeforcesClient codeforcesClient;
17+
18+
@Bean
19+
public LoadingCache<String, Double> ratingCache() {
20+
return Caffeine.newBuilder()
21+
.expireAfterWrite(1, TimeUnit.HOURS)
22+
.maximumSize(1000)
23+
.build(codeforcesClient::fetchRatingFromApi);
24+
}
25+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package com.cf.cfteam.controllers.codeforces;
2+
3+
import com.cf.cfteam.services.codeforces.GroupService;
4+
import com.cf.cfteam.transfer.payloads.codeforces.GroupPayload;
5+
import com.cf.cfteam.transfer.responses.codeforces.GroupResponse;
6+
import lombok.RequiredArgsConstructor;
7+
import org.springframework.http.ResponseEntity;
8+
import org.springframework.security.core.Authentication;
9+
import org.springframework.web.bind.annotation.*;
10+
11+
import java.util.List;
12+
13+
@RestController
14+
@RequiredArgsConstructor
15+
@RequestMapping("api/cf/groups")
16+
public class GroupController {
17+
18+
private final GroupService groupService;
19+
20+
@GetMapping("/user/{userId}")
21+
public ResponseEntity<List<GroupResponse>> getAllGroupsByUser(@PathVariable Long userId,
22+
Authentication authentication) {
23+
List<GroupResponse> groups = groupService.getAllGroupsByUser(userId);
24+
return ResponseEntity.ok(groups);
25+
}
26+
27+
@GetMapping("/{groupId}")
28+
public ResponseEntity<GroupResponse> getGroupById(@PathVariable Long groupId, Authentication authentication) {
29+
GroupResponse group = groupService.getGroupById(groupId);
30+
return ResponseEntity.ok(group);
31+
}
32+
33+
@PostMapping("/user/{userId}")
34+
public ResponseEntity<GroupResponse> addGroupToUser(@PathVariable Long userId,
35+
@RequestBody GroupPayload groupPayload,
36+
Authentication authentication) {
37+
GroupResponse createdGroup = groupService.addGroupToUser(userId, groupPayload);
38+
return ResponseEntity.ok(createdGroup);
39+
}
40+
41+
@PutMapping("/{groupId}")
42+
public ResponseEntity<GroupResponse> updateGroup(@PathVariable Long groupId, @RequestBody GroupPayload groupPayload,
43+
Authentication authentication) {
44+
GroupResponse group = groupService.updateGroup(groupId, groupPayload);
45+
return ResponseEntity.ok(group);
46+
}
47+
48+
@DeleteMapping("/{groupId}")
49+
public ResponseEntity<Void> deleteGroup(@PathVariable Long groupId, Authentication authentication) {
50+
groupService.deleteGroup(groupId);
51+
return ResponseEntity.noContent().build();
52+
}
53+
54+
@DeleteMapping("/user/{userId}")
55+
public ResponseEntity<Void> deleteAllGroupsByUser(@PathVariable Long userId, Authentication authentication) {
56+
groupService.deleteAllGroupsByUser(userId);
57+
return ResponseEntity.noContent().build();
58+
}
59+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package com.cf.cfteam.controllers.codeforces;
2+
3+
import com.cf.cfteam.services.codeforces.PlayerService;
4+
import com.cf.cfteam.transfer.payloads.codeforces.PlayerPayload;
5+
import com.cf.cfteam.transfer.responses.codeforces.PlayerResponse;
6+
import lombok.RequiredArgsConstructor;
7+
import org.springframework.http.ResponseEntity;
8+
import org.springframework.security.core.Authentication;
9+
import org.springframework.web.bind.annotation.*;
10+
11+
import java.util.List;
12+
13+
@RestController
14+
@RequiredArgsConstructor
15+
@RequestMapping("api/cf/players")
16+
public class PlayerController {
17+
18+
private final PlayerService playerService;
19+
20+
@GetMapping("/team/{teamId}")
21+
public ResponseEntity<List<PlayerResponse>> getAllPlayersByTeam(@PathVariable Long teamId,
22+
Authentication authentication) {
23+
List<PlayerResponse> players = playerService.getAllPlayersByTeam(teamId);
24+
return ResponseEntity.ok(players);
25+
}
26+
27+
@GetMapping("/{playerId}")
28+
public ResponseEntity<PlayerResponse> getPlayerById(@PathVariable Long playerId, Authentication authentication) {
29+
PlayerResponse player = playerService.getPlayerById(playerId);
30+
return ResponseEntity.ok(player);
31+
}
32+
33+
@PostMapping("/team/{teamId}")
34+
public ResponseEntity<PlayerResponse> addPlayerToTeam(@PathVariable Long teamId,
35+
@RequestBody PlayerPayload playerPayload,
36+
Authentication authentication) {
37+
PlayerResponse player = playerService.addPlayerToTeam(teamId, playerPayload);
38+
return ResponseEntity.ok(player);
39+
}
40+
41+
@PutMapping("/players/{playerId}/teams/{teamId}")
42+
public ResponseEntity<PlayerResponse> updatePlayerInTeam(@PathVariable Long playerId,
43+
@PathVariable Long teamId,
44+
@RequestBody PlayerPayload playerPayload,
45+
Authentication authentication) {
46+
PlayerResponse player = playerService.updatePlayerInTeam(playerId, teamId, playerPayload);
47+
return ResponseEntity.ok(player);
48+
}
49+
50+
@DeleteMapping("/players/{playerId}/teams/{teamId}")
51+
public ResponseEntity<Void> deletePlayerFromTeam(@PathVariable Long playerId,
52+
@PathVariable Long teamId,
53+
Authentication authentication) {
54+
playerService.deletePlayerFromTeam(playerId, teamId);
55+
return ResponseEntity.noContent().build();
56+
}
57+
58+
@DeleteMapping("/team/{teamId}")
59+
public ResponseEntity<Void> deleteAllPlayersFromTeam(@PathVariable Long teamId, Authentication authentication) {
60+
playerService.deleteAllPlayersFromTeam(teamId);
61+
return ResponseEntity.noContent().build();
62+
}
63+
}

0 commit comments

Comments
 (0)