fork download
  1. /* package whatever; // don't place package name! */
  2.  
  3. import java.util.*;
  4. import java.lang.*;
  5. import java.io.*;
  6.  
  7. /* Name of the class has to be "Main" only if the class is public. */
  8. class Ideone
  9. {
  10. public static void main (String[] args) throws java.lang.Exception
  11. {
  12. // your code goes here
  13. }
  14. }
Success #stdin #stdout 0.07s 54588KB
stdin
package com.inmobi.game;

import com.fasterxml.jackson.annotation.JsonProperty;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import jakarta.persistence.*;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import lombok.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.*;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.stream.Collectors;

// ==========================================
// 1. MAIN APPLICATION
// ==========================================
@SpringBootApplication
@EnableCaching
class GameApplication {
    public static void main(String[] args) {
        SpringApplication.run(GameApplication.class, args);
    }
}

// ==========================================
// 2. DOMAIN ENTITY
// ==========================================
@Entity
@Table(name = "users", indexes = {
        @Index(name = "idx_user_score", columnList = "score")
})
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true, nullable = false)
    private String username;

    @Column(nullable = false)
    private String password;

    @Builder.Default
    @Column(nullable = false)
    private Integer score = 0;

    @Builder.Default
    @Column(nullable = false)
    private Integer turns = 0;
}

// ==========================================
// 3. REPOSITORY
// ==========================================
interface UserRepository extends JpaRepository<User, Long> {

    java.util.Optional<User> findByUsername(String username);

    // Lock by username directly to avoid stale entity loaded by a previous non-locking query.
    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query("SELECT u FROM User u WHERE u.username = :username")
    java.util.Optional<User> findByUsernameWithLock(@Param("username") String username);

    List<User> findTop10ByOrderByScoreDesc();
}

// ==========================================
// 4. DTOs
// ==========================================
class AuthDto {
    @Data
    public static class Request {
        @NotBlank(message = "Username không được để trống")
        private String username;

        @NotBlank(message = "Password không được để trống")
        @ToString.Exclude
        private String password;
    }

    @Data
    @AllArgsConstructor
    public static class Response {
        private String token;
        private String username;
    }
}

class GameDto {
    @Data
    public static class GuessRequest {
        @Min(value = 1, message = "Số đoán nằm trong khoảng 1-5")
        @Max(value = 5, message = "Số đoán nằm trong khoảng 1-5")
        private int number;
    }

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    @Builder
    public static class GuessResponse {
        @JsonProperty("isWin")
        private boolean isWin;

        private int userGuess;
        private int serverNumber;
        private int remainingTurns;
        private int currentScore;
        private String message;
    }

    @Data
    @AllArgsConstructor
    @Builder
    public static class UserProfileResponse {
        private String username;
        private int score;
        private int turns;
    }

    @Data
    @AllArgsConstructor
    public static class LeaderboardResponse {
        private String username;
        private int score;
    }
}

// ==========================================
// 5. SECURITY & JWT
// ==========================================
@Component
class JwtTokenProvider {

    private final Key key;
    private final long expirationMs;

    public JwtTokenProvider(
            @Value("${jwt.secret:9a6e8f12b45c78d90e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d}") String secret,
            @Value("${jwt.expiration-ms:86400000}") long expirationMs) {
        this.key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
        this.expirationMs = expirationMs;
    }

    public String generateToken(String username) {
        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + expirationMs);

        return Jwts.builder()
                .setSubject(username)
                .setIssuedAt(now)
                .setExpiration(expiryDate)
                .signWith(key, SignatureAlgorithm.HS256)
                .compact();
    }

    public String getUsernameFromToken(String token) {
        Claims claims = Jwts.parserBuilder()
                .setSigningKey(key)
                .build()
                .parseClaimsJws(token)
                .getBody();
        return claims.getSubject();
    }

    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
            return true;
        } catch (JwtException | IllegalArgumentException e) {
            return false;
        }
    }
}

@Component
class JwtFilter extends org.springframework.web.filter.OncePerRequestFilter {

    private final JwtTokenProvider tokenProvider;

    public JwtFilter(JwtTokenProvider tokenProvider) {
        this.tokenProvider = tokenProvider;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain)
            throws ServletException, IOException {

        String bearerToken = request.getHeader("Authorization");
        String token = null;

        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            token = bearerToken.substring(7);
        }

        if (StringUtils.hasText(token)) {
            try {
                String username = tokenProvider.getUsernameFromToken(token);
                UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
                        username, null, Collections.emptyList()
                );
                SecurityContextHolder.getContext().setAuthentication(auth);
            } catch (JwtException | IllegalArgumentException ignored) {
                SecurityContextHolder.clearContext();
            }
        }

        filterChain.doFilter(request, response);
    }
}

@Configuration
@EnableWebSecurity
class SecurityConfig {

    private final JwtFilter jwtFilter;

    public SecurityConfig(JwtFilter jwtFilter) {
        this.jwtFilter = jwtFilter;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .csrf(csrf -> csrf.disable())
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/register", "/login", "/h2-console/**").permitAll()
                        .anyRequest().authenticated()
                )
                .headers(headers -> headers.frameOptions(frame -> frame.disable()));

        http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

// ==========================================
// 6. SERVICE LAYER
// ==========================================
@Service
class GameService {

    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;
    private final JwtTokenProvider tokenProvider;
    private final Random random = new Random();

    public GameService(UserRepository userRepository,
                       PasswordEncoder passwordEncoder,
                       JwtTokenProvider tokenProvider) {
        this.userRepository = userRepository;
        this.passwordEncoder = passwordEncoder;
        this.tokenProvider = tokenProvider;
    }

    @Transactional
    @CacheEvict(value = "leaderboard", allEntries = true)
    public String register(AuthDto.Request request) {
        if (userRepository.findByUsername(request.getUsername()).isPresent()) {
            throw new RuntimeException("Tên đăng nhập đã tồn tại!");
        }

        User user = User.builder()
                .username(request.getUsername())
                .password(passwordEncoder.encode(request.getPassword()))
                .turns(5)
                .score(0)
                .build();

        userRepository.save(user);
        return "Đăng ký thành công!";
    }

    @Transactional(readOnly = true)
    public AuthDto.Response login(AuthDto.Request request) {
        User user = userRepository.findByUsername(request.getUsername())
                .orElseThrow(() -> new RuntimeException("Sai tài khoản hoặc mật khẩu"));

        if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) {
            throw new RuntimeException("Sai tài khoản hoặc mật khẩu");
        }

        String token = tokenProvider.generateToken(user.getUsername());
        return new AuthDto.Response(token, user.getUsername());
    }

    @Transactional(readOnly = true)
    public GameDto.UserProfileResponse getProfile(String username) {
        User user = userRepository.findByUsername(username)
                .orElseThrow(() -> new RuntimeException("User không tồn tại"));

        return GameDto.UserProfileResponse.builder()
                .username(user.getUsername())
                .score(user.getScore())
                .turns(user.getTurns())
                .build();
    }

    @Transactional
    @CacheEvict(value = "leaderboard", allEntries = true,
            condition = "#result != null && #result.win")
    public GameDto.GuessResponse guessNumber(String username, int userGuess) {
        if (userGuess < 1 || userGuess > 5) {
            throw new IllegalArgumentException("Số đoán phải nằm trong khoảng 1-5");
        }

        User user = userRepository.findByUsernameWithLock(username)
                .orElseThrow(() -> new RuntimeException("User không tồn tại"));

        if (user.getTurns() <= 0) {
            throw new RuntimeException("Bạn đã hết lượt chơi! Vui lòng mua thêm lượt.");
        }

        user.setTurns(user.getTurns() - 1);

        boolean isWin = random.nextInt(100) < 5;
        int serverNumber;

        if (isWin) {
            serverNumber = userGuess;
            user.setScore(user.getScore() + 1);
        } else {
            do {
                serverNumber = random.nextInt(5) + 1;
            } while (serverNumber == userGuess);
        }

        // No explicit save needed: entity is managed and dirty-checked inside @Transactional.

        return GameDto.GuessResponse.builder()
                .isWin(isWin)
                .userGuess(userGuess)
                .serverNumber(serverNumber)
                .remainingTurns(user.getTurns())
                .currentScore(user.getScore())
                .message(isWin
                        ? "Chúc mừng! Bạn đã đoán đúng (+1 điểm)"
                        : "Rất tiếc! Chúc bạn may mắn lần sau.")
                .build();
    }

    @Transactional
    public GameDto.UserProfileResponse buyTurns(String username, int additionalTurns) {
        if (additionalTurns <= 0) {
            throw new IllegalArgumentException("Số lượt mua phải lớn hơn 0");
        }

        User user = userRepository.findByUsernameWithLock(username)
                .orElseThrow(() -> new RuntimeException("User không tồn tại"));

        user.setTurns(user.getTurns() + additionalTurns);

        return GameDto.UserProfileResponse.builder()
                .username(user.getUsername())
                .score(user.getScore())
                .turns(user.getTurns())
                .build();
    }

    @Transactional(readOnly = true)
    @Cacheable(value = "leaderboard")
    public List<GameDto.LeaderboardResponse> getLeaderboard() {
        return userRepository.findTop10ByOrderByScoreDesc().stream()
                .map(u -> new GameDto.LeaderboardResponse(u.getUsername(), u.getScore()))
                .collect(Collectors.toList());
    }
}

// ==========================================
// 7. CONTROLLER
// ==========================================
@RestController
@RequestMapping
class GameController {

    private final GameService gameService;

    public GameController(GameService gameService) {
        this.gameService = gameService;
    }

    @PostMapping("/register")
    public ResponseEntity<String> register(@Valid @RequestBody AuthDto.Request request) {
        return ResponseEntity.ok(gameService.register(request));
    }

    @PostMapping("/login")
    public ResponseEntity<AuthDto.Response> login(@Valid @RequestBody AuthDto.Request request) {
        return ResponseEntity.ok(gameService.login(request));
    }

    @GetMapping("/me")
    public ResponseEntity<GameDto.UserProfileResponse> getProfile(Authentication authentication) {
        return ResponseEntity.ok(gameService.getProfile(authentication.getName()));
    }

    @PostMapping("/guess")
    public ResponseEntity<GameDto.GuessResponse> guess(
            Authentication authentication,
            @Valid @RequestBody GameDto.GuessRequest request) {
        return ResponseEntity.ok(gameService.guessNumber(authentication.getName(), request.getNumber()));
    }

    @PostMapping("/buy-turns")
    public ResponseEntity<GameDto.UserProfileResponse> buyTurns(Authentication authentication) {
        return ResponseEntity.ok(gameService.buyTurns(authentication.getName(), 5));
    }

    @GetMapping("/leaderboard")
    public ResponseEntity<List<GameDto.LeaderboardResponse>> getLeaderboard() {
        return ResponseEntity.ok(gameService.getLeaderboard());
    }
}

// ==========================================
// 8. GLOBAL EXCEPTION HANDLER
// ==========================================
@RestControllerAdvice
class GlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, String>> handleValidation(MethodArgumentNotValidException ex) {
        String message = ex.getBindingResult().getFieldErrors().stream()
                .map(err -> err.getField() + ": " + err.getDefaultMessage())
                .collect(Collectors.joining(", "));
        return ResponseEntity.badRequest().body(Collections.singletonMap("error", message));
    }

    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<Map<String, String>> handleIllegalArgument(IllegalArgumentException ex) {
        return ResponseEntity.badRequest().body(Collections.singletonMap("error", ex.getMessage()));
    }

    @ExceptionHandler(RuntimeException.class)
    public ResponseEntity<Map<String, String>> handleRuntime(RuntimeException ex) {
        return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                .body(Collections.singletonMap("error", ex.getMessage()));
    }
}
stdout
Standard output is empty