diff --git a/src/main/java/com/videogamescatalogue/backend/controller/GameController.java b/src/main/java/com/videogamescatalogue/backend/controller/GameController.java index ef62b38..bcbcd65 100644 --- a/src/main/java/com/videogamescatalogue/backend/controller/GameController.java +++ b/src/main/java/com/videogamescatalogue/backend/controller/GameController.java @@ -3,8 +3,6 @@ import com.videogamescatalogue.backend.dto.internal.GameSearchParameters; import com.videogamescatalogue.backend.dto.internal.game.GameDto; import com.videogamescatalogue.backend.dto.internal.game.GameWithStatusDto; -import com.videogamescatalogue.backend.model.Genre; -import com.videogamescatalogue.backend.model.Platform; import com.videogamescatalogue.backend.model.User; import com.videogamescatalogue.backend.service.game.GameService; import io.swagger.v3.oas.annotations.Operation; @@ -12,9 +10,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; -import java.util.Arrays; import java.util.Map; -import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -130,7 +126,6 @@ public Page search( @PageableDefault(size = DEFAULT_PAGE_SIZE) Pageable pageable ) { - validateSearchParams(searchParameters); return gameService.search(searchParameters, pageable); } @@ -149,31 +144,4 @@ public Page apiSearch( ) { return gameService.apiSearch(searchParams); } - - private void validateSearchParams(GameSearchParameters searchParameters) { - if (searchParameters.platforms() != null) { - try { - searchParameters.platforms() - .forEach( - p -> Platform.GeneralName.valueOf(p.toUpperCase()) - ); - } catch (IllegalArgumentException e) { - throw new IllegalArgumentException( - "Invalid platforms provided. Valid platforms: " - + Arrays.stream(Platform.GeneralName.values()) - .map(Enum::toString) - .collect(Collectors.joining(", ")), e); - } - } - if (searchParameters.genres() != null) { - try { - searchParameters.genres().forEach(g -> Genre.Name.valueOf(g.toUpperCase())); - } catch (IllegalArgumentException e) { - throw new IllegalArgumentException("Invalid genres provided. Valid genres: " - + Arrays.stream(Genre.Name.values()) - .map(Enum::toString) - .collect(Collectors.joining(", ")), e); - } - } - } } diff --git a/src/main/java/com/videogamescatalogue/backend/model/Game.java b/src/main/java/com/videogamescatalogue/backend/model/Game.java index 1fd14e8..010f878 100644 --- a/src/main/java/com/videogamescatalogue/backend/model/Game.java +++ b/src/main/java/com/videogamescatalogue/backend/model/Game.java @@ -13,6 +13,7 @@ import java.math.BigDecimal; import java.util.HashSet; import java.util.Set; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import org.hibernate.annotations.SQLDelete; @@ -39,6 +40,7 @@ public class Game { private String backgroundImage; + @EqualsAndHashCode.Exclude @ManyToMany @JoinTable( name = "games_platforms", @@ -48,6 +50,7 @@ public class Game { ) private Set platforms = new HashSet<>(); + @EqualsAndHashCode.Exclude @ManyToMany @JoinTable( name = "games_genres", @@ -57,6 +60,7 @@ public class Game { ) private Set genres = new HashSet<>(); + @EqualsAndHashCode.Exclude @ManyToMany @JoinTable( name = "games_developers", diff --git a/src/main/java/com/videogamescatalogue/backend/repository/GameRepository.java b/src/main/java/com/videogamescatalogue/backend/repository/GameRepository.java index cf88999..c11a615 100644 --- a/src/main/java/com/videogamescatalogue/backend/repository/GameRepository.java +++ b/src/main/java/com/videogamescatalogue/backend/repository/GameRepository.java @@ -3,11 +3,18 @@ import com.videogamescatalogue.backend.model.Game; import java.util.List; import java.util.Optional; +import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; public interface GameRepository extends JpaRepository, JpaSpecificationExecutor { + @EntityGraph(attributePaths = { + "platforms", "genres", "developers" + }) Optional findByApiId(Long apiId); + @EntityGraph(attributePaths = { + "platforms", "genres", "developers" + }) List findAllByApiIdIn(List apiIds); } diff --git a/src/main/java/com/videogamescatalogue/backend/security/JwtUtil.java b/src/main/java/com/videogamescatalogue/backend/security/JwtUtil.java index 87d254f..a51e9e5 100644 --- a/src/main/java/com/videogamescatalogue/backend/security/JwtUtil.java +++ b/src/main/java/com/videogamescatalogue/backend/security/JwtUtil.java @@ -41,13 +41,6 @@ public boolean isValidToken(String token) { } } - private Jws getAllClaims(String token) { - return Jwts.parserBuilder() - .setSigningKey(secret) - .build() - .parseClaimsJws(token); - } - public String getUsername(String token) { return getSpecificClaim(token, Claims::getSubject); } @@ -56,4 +49,11 @@ private T getSpecificClaim(String token, Function claimsResolver) Jws allClaims = getAllClaims(token); return claimsResolver.apply(allClaims.getBody()); } + + private Jws getAllClaims(String token) { + return Jwts.parserBuilder() + .setSigningKey(secret) + .build() + .parseClaimsJws(token); + } } diff --git a/src/main/java/com/videogamescatalogue/backend/service/RawgApiClient.java b/src/main/java/com/videogamescatalogue/backend/service/RawgApiClient.java index d22dacb..d47504e 100644 --- a/src/main/java/com/videogamescatalogue/backend/service/RawgApiClient.java +++ b/src/main/java/com/videogamescatalogue/backend/service/RawgApiClient.java @@ -45,6 +45,9 @@ public class RawgApiClient { private static final String DATES_BETWEEN_URL_PART = "&dates=" + LocalDate.now() + "%2C" + LocalDate.now().minusMonths(12); private static final String METACRITIC_URL_PART = "metacritic=80%2C100"; + private static final int NUMBER_OF_DOWNLOAD_PORTIONS = 11; + private static final String REQUEST_HEADER_NAME = "User-Agent"; + private static final String REQUEST_HEADER_VALUE = "VideoGamesCatalogue"; @Value("${rawg.key}") private String apiKey; @@ -56,7 +59,7 @@ public List getBestGames() { ArrayList result = new ArrayList<>(); - for (int i = 1; i < 11; i++) { + for (int i = 1; i < NUMBER_OF_DOWNLOAD_PORTIONS; i++) { log.info("Create request for page {}", i); String url = BASE_URL + GAME_URL_PART @@ -70,7 +73,7 @@ public List getBestGames() { HttpRequest httpRequest = HttpRequest.newBuilder() .GET() .uri(URI.create(url)) - .header("User-Agent", "VideoGamesCatalogue") + .header(REQUEST_HEADER_NAME, REQUEST_HEADER_VALUE) .build(); ApiResponseGames responseObject = getResponseGamesList(httpRequest); @@ -95,7 +98,7 @@ public Page getAllGames(Pageable pageable) { HttpRequest httpRequest = HttpRequest.newBuilder() .GET() .uri(URI.create(url)) - .header("User-Agent", "VideoGamesCatalogue") + .header(REQUEST_HEADER_NAME, REQUEST_HEADER_VALUE) .build(); ApiResponseGames responseObject = getResponseGamesList(httpRequest); @@ -112,7 +115,7 @@ public ApiResponseFullGameDto getGameById(Long id) { HttpRequest httpRequest = HttpRequest.newBuilder() .GET() .uri(URI.create(url)) - .header("User-Agent", "VideoGamesCatalogue") + .header(REQUEST_HEADER_NAME, REQUEST_HEADER_VALUE) .build(); ApiResponseFullGameDto game = getIndividualGame(httpRequest); @@ -129,7 +132,7 @@ public Page search(Map searchParams) { HttpRequest httpRequest = HttpRequest.newBuilder() .GET() .uri(URI.create(url.toString())) - .header("User-Agent", "VideoGamesCatalogue") + .header(REQUEST_HEADER_NAME, REQUEST_HEADER_VALUE) .build(); ApiResponseGames responseObject = getResponseGamesList(httpRequest); diff --git a/src/main/java/com/videogamescatalogue/backend/service/comment/CommentServiceImpl.java b/src/main/java/com/videogamescatalogue/backend/service/comment/CommentServiceImpl.java index 8c15d2d..841e9f1 100644 --- a/src/main/java/com/videogamescatalogue/backend/service/comment/CommentServiceImpl.java +++ b/src/main/java/com/videogamescatalogue/backend/service/comment/CommentServiceImpl.java @@ -21,6 +21,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @RequiredArgsConstructor @Service @@ -31,6 +32,7 @@ public class CommentServiceImpl implements CommentService { private final GameMapper gameMapper; private final CommentRepository commentRepository; + @Transactional @Override public CommentDto create(Long gameApiId, CreateCommentRequestDto requestDto, User user) { Comment comment = commentMapper.toModel(requestDto); @@ -61,11 +63,6 @@ public Page getUserComments( return findCommentsByUserId(userId, pageable); } - private Page findCommentsByUserId(Long userId, Pageable pageable) { - Page userComments = commentRepository.findAllByUserId(userId, pageable); - return userComments.map(commentMapper::toDto); - } - @Override public CommentDto update(Long commentId, UpdateCommentRequestDto requestDto, Long userId) { Comment comment = commentRepository.findById(commentId) @@ -94,6 +91,11 @@ public void delete(Long commentId, Long userId) { commentRepository.deleteById(commentId); } + private Page findCommentsByUserId(Long userId, Pageable pageable) { + Page userComments = commentRepository.findAllByUserId(userId, pageable); + return userComments.map(commentMapper::toDto); + } + private void existsByIdAndUserId(Long commentId, Long userId) { if (!commentRepository.existsByIdAndUserId(commentId, userId)) { throw new AccessNotAllowedException("User with id: " + userId diff --git a/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java b/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java index 8e4f841..8aa5eb3 100644 --- a/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java +++ b/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java @@ -6,9 +6,12 @@ import com.videogamescatalogue.backend.dto.internal.game.GameDto; import com.videogamescatalogue.backend.dto.internal.game.GameWithStatusDto; import com.videogamescatalogue.backend.mapper.developer.DeveloperMapper; +import com.videogamescatalogue.backend.mapper.developer.DeveloperProvider; import com.videogamescatalogue.backend.mapper.game.GameMapper; import com.videogamescatalogue.backend.model.Developer; import com.videogamescatalogue.backend.model.Game; +import com.videogamescatalogue.backend.model.Genre; +import com.videogamescatalogue.backend.model.Platform; import com.videogamescatalogue.backend.model.User; import com.videogamescatalogue.backend.model.UserGame; import com.videogamescatalogue.backend.repository.DeveloperRepository; @@ -18,6 +21,7 @@ import com.videogamescatalogue.backend.service.RawgApiClient; import jakarta.transaction.Transactional; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Optional; @@ -42,6 +46,7 @@ public class GameServiceImpl implements GameService { private final SpecificationBuilder specificationBuilder; private final UserGameRepository userGameRepository; private final DeveloperRepository developerRepository; + private final DeveloperProvider developerProvider; @Override public void fetchBestGames() { @@ -108,6 +113,7 @@ public Page getAllGamesFromApi(Pageable pageable) { @Override public Page search(GameSearchParameters searchParameters, Pageable pageable) { + validateSearchParams(searchParameters); Specification specification = specificationBuilder.build(searchParameters); return gameRepository.findAll(specification, pageable) @@ -157,35 +163,56 @@ private UserGame.GameStatus getGameStatus(Long apiId, User user) { private Game findOrUpdate(Long apiId) { Optional gameOptional = gameRepository.findByApiId(apiId); + if (gameOptional.isPresent() + && gameOptional.get().getDescription() != null + && !gameOptional.get().getDevelopers().isEmpty()) { + return gameOptional.get(); + } + ApiResponseFullGameDto apiGame = apiClient.getGameById(apiId); if (gameOptional.isEmpty()) { - return findFromApi(apiId); + return gameMapper.toModel(apiGame); } Game game = gameOptional.get(); + updateGameInfo(game, apiGame); + return game; + } + + private void updateGameInfo(Game game, ApiResponseFullGameDto apiGame) { if (game.getDescription() == null) { - updateGameDescription(apiId, game); + game.setDescription(apiGame.description()); } if (game.getDevelopers().isEmpty()) { - updateGameDevelopers(apiId, game); + Set developersSet = developerProvider.toDevelopersSet(apiGame.developers()); + developerRepository.saveAll(developersSet); + game.setDevelopers(developersSet); } - return game; - } - - private void updateGameDescription(Long apiId, Game game) { - ApiResponseFullGameDto apiGame = apiClient.getGameById(apiId); - game.setDescription(apiGame.description()); gameRepository.save(game); } - private void updateGameDevelopers(Long apiId, Game game) { - ApiResponseFullGameDto apiGame = apiClient.getGameById(apiId); - Set developers = developerMapper.toModelSet(apiGame.developers()); - developerRepository.saveAll(developers); - game.setDevelopers(developers); - gameRepository.save(game); - } - - private Game findFromApi(Long apiId) { - ApiResponseFullGameDto apiGame = apiClient.getGameById(apiId); - return gameMapper.toModel(apiGame); + private void validateSearchParams(GameSearchParameters searchParameters) { + if (searchParameters.platforms() != null) { + try { + searchParameters.platforms() + .forEach( + p -> Platform.GeneralName.valueOf(p.toUpperCase()) + ); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException( + "Invalid platforms provided. Valid platforms: " + + Arrays.stream(Platform.GeneralName.values()) + .map(Enum::toString) + .collect(Collectors.joining(", ")), e); + } + } + if (searchParameters.genres() != null) { + try { + searchParameters.genres().forEach(g -> Genre.Name.valueOf(g.toUpperCase())); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Invalid genres provided. Valid genres: " + + Arrays.stream(Genre.Name.values()) + .map(Enum::toString) + .collect(Collectors.joining(", ")), e); + } + } } } diff --git a/src/main/java/com/videogamescatalogue/backend/service/user/UserServiceImpl.java b/src/main/java/com/videogamescatalogue/backend/service/user/UserServiceImpl.java index cea8ec2..eb7a181 100644 --- a/src/main/java/com/videogamescatalogue/backend/service/user/UserServiceImpl.java +++ b/src/main/java/com/videogamescatalogue/backend/service/user/UserServiceImpl.java @@ -7,6 +7,7 @@ import com.videogamescatalogue.backend.dto.internal.user.UserRegistrationRequestDto; import com.videogamescatalogue.backend.dto.internal.user.UserRegistrationResponseDto; import com.videogamescatalogue.backend.dto.internal.user.UserResponseDto; +import com.videogamescatalogue.backend.exception.AccessNotAllowedException; import com.videogamescatalogue.backend.exception.AuthenticationRequiredException; import com.videogamescatalogue.backend.exception.EntityNotFoundException; import com.videogamescatalogue.backend.exception.InvalidInputException; @@ -78,6 +79,14 @@ public UserResponseDto getUserInfo(Long userId, User authenticatedUser) { @Override public UserResponseDto updateUserInfo(UpdateUserRequestDto requestDto, User authenticatedUser) { + if (authenticatedUser == null) { + throw new AccessNotAllowedException( + "You are not allowed to modify this user info. Please log in." + ); + } + if (requestDto.profileName() != null) { + checkProfileNameAlreadyExists(requestDto.profileName()); + } User updatedUser = userMapper.updateProfileInfo(authenticatedUser, requestDto); User savedUser = userRepository.save(updatedUser); @@ -90,6 +99,11 @@ public UserResponseDto updateUserInfo(UpdateUserRequestDto requestDto, User auth public UserResponseDto changePassword( ChangePasswordRequestDto requestDto, User authenticatedUser ) { + if (authenticatedUser == null) { + throw new AccessNotAllowedException( + "You are not allowed to modify this user info. Please log in." + ); + } validateCurrentPassword(requestDto, authenticatedUser); checkPasswordsMatch(requestDto); @@ -127,8 +141,8 @@ private void checkUserAlreadyExistsByEmail(String email) { private void checkProfileNameAlreadyExists(String profileName) { if (userRepository.existsByProfileNameIgnoreCase(profileName)) { - throw new RegistrationException("Can't register user with profileName " - + profileName + ". This profileName is already in use."); + throw new RegistrationException("Profile Name " + + profileName + " is already in use."); } }