Skip to content

Commit

Permalink
feat: new approach for Exception Handling using sealed interfaces
Browse files Browse the repository at this point in the history
  • Loading branch information
thisdudkin committed Oct 5, 2024
1 parent bd6fa05 commit 6cbf202
Show file tree
Hide file tree
Showing 16 changed files with 146 additions and 56 deletions.
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package dev.earlspilner.users.service;
package dev.earlspilner.users.configuration;

import java.math.BigInteger;
import java.security.SecureRandom;

/**
* @author Alexander Dudkin
*/
public class KeyGen {
public class KeyGeneration {

private static final SecureRandom secureRandom = new SecureRandom();
private static final int BIT_LENGTH = 256;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package dev.earlspilner.users.configuration;

import dev.earlspilner.users.rest.advice.Failure;
import dev.earlspilner.users.rest.advice.Result;
import dev.earlspilner.users.rest.advice.Success;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;

/**
* @author Alexander Dudkin
*/
@Component
public class ResponseHandler {

public <T> ResponseEntity<?> handleResult(Result<T> result) {
if (result instanceof Success<T> success) {
return new ResponseEntity<>(success.value(), HttpStatus.OK);
} else if (result instanceof Failure<?> failure) {
return new ResponseEntity<>(failure.toProblemDetail(), HttpStatus.BAD_REQUEST);
}

return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}

public <T> ResponseEntity<?> handleResult(Result<T> result, HttpStatus httpStatus) {
if (result instanceof Success<T> success) {
return new ResponseEntity<>(success.value(), HttpStatus.OK);
} else if (result instanceof Failure<?> failure) {
return new ResponseEntity<>(failure.toProblemDetail(), httpStatus);
}
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package dev.earlspilner.users.rest.advice;

import org.springframework.http.HttpStatusCode;
import org.springframework.http.ProblemDetail;

/**
* @author Alexander Dudkin
*/
public record Failure<T>(String exMessage, int httpStatus) implements Result<T> {

public ProblemDetail toProblemDetail() {
return ProblemDetail.forStatusAndDetail(HttpStatusCode.valueOf(httpStatus), exMessage);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package dev.earlspilner.users.rest.advice;

/**
* @author Alexander Dudkin
*/
public sealed interface Result<T> permits Success, Failure { }
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package dev.earlspilner.users.rest.advice;

/**
* @author Alexander Dudkin
*/
public record Success<T>(T value) implements Result<T> { }
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package dev.earlspilner.users.rest.controller;

import dev.earlspilner.users.configuration.ResponseHandler;
import dev.earlspilner.users.dto.UserDto;
import dev.earlspilner.users.rest.advice.Result;
import dev.earlspilner.users.service.UserService;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
Expand All @@ -19,44 +21,50 @@
public class UserRestController implements UsersApi {

private final UserService userService;
private final ResponseHandler responseHandler;

@Autowired
public UserRestController(UserService userService) {
public UserRestController(UserService userService, ResponseHandler responseHandler) {
this.userService = userService;
this.responseHandler = responseHandler;
}

@Override
@PostMapping
public ResponseEntity<UserDto> addUser(@Valid @RequestBody UserDto userDto) {
return new ResponseEntity<>(userService.saveUser(userDto), HttpStatus.CREATED);
@ResponseStatus(HttpStatus.CREATED)
public ResponseEntity<?> addUser(@Valid @RequestBody UserDto userDto) {
Result<UserDto> result = this.userService.saveUser(userDto);
return responseHandler.handleResult(result);
}

@Override
@GetMapping("/{username}")
public ResponseEntity<UserDto> getUser(@PathVariable String username) {
return new ResponseEntity<>(userService.getUser(username), HttpStatus.OK);
public ResponseEntity<?> getUser(@PathVariable String username) {
Result<UserDto> result = this.userService.getUser(username);
return responseHandler.handleResult(result, HttpStatus.NOT_FOUND);
}

@Override
@GetMapping
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<Page<UserDto>> getUsers(Pageable pageable) {
return new ResponseEntity<>(userService.getUsers(pageable), HttpStatus.OK);
public ResponseEntity<Page<?>> getUsers(Pageable pageable) {
return new ResponseEntity<>(this.userService.getUsers(pageable), HttpStatus.OK);
}

@Override
@PutMapping("/{username}")
public ResponseEntity<UserDto> updateUser(@PathVariable String username,
@Valid @RequestBody UserDto userDto) {
return new ResponseEntity<>(userService.updateUser(username, userDto), HttpStatus.OK);
public ResponseEntity<?> updateUser(@PathVariable String username,
@Valid @RequestBody UserDto userDto) {
Result<UserDto> result = userService.updateUser(username, userDto);
return responseHandler.handleResult(result, HttpStatus.NOT_FOUND);
}

@Override
@DeleteMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<Void> deleteUser(@PathVariable Integer id) {
userService.deleteUser(id);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
public ResponseEntity<?> deleteUser(@PathVariable Integer id) {
Result<Void> result = userService.deleteUser(id);
return responseHandler.handleResult(result, HttpStatus.NOT_FOUND);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
* @author Alexander Dudkin
*/
public interface UsersApi {
ResponseEntity<UserDto> addUser(UserDto userDto);
ResponseEntity<UserDto> getUser(String username);
ResponseEntity<Page<UserDto>> getUsers(Pageable pageable);
ResponseEntity<UserDto> updateUser(String username, UserDto userDto);
ResponseEntity<Void> deleteUser(Integer id);
ResponseEntity<?> addUser(UserDto userDto);
ResponseEntity<?> getUser(String username);
ResponseEntity<Page<?>> getUsers(Pageable pageable);
ResponseEntity<?> updateUser(String username, UserDto userDto);
ResponseEntity<?> deleteUser(Integer id);
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package dev.earlspilner.users.rest.advice;
package dev.earlspilner.users.rest.old;

import dev.earlspilner.users.rest.advice.custom.UnauthorizedOperationException;
import dev.earlspilner.users.rest.advice.custom.UserExistsException;
import dev.earlspilner.users.rest.advice.custom.UserNotFoundException;
import dev.earlspilner.users.rest.old.custom.UnauthorizedOperationException;
import dev.earlspilner.users.rest.old.custom.UserExistsException;
import dev.earlspilner.users.rest.old.custom.UserNotFoundException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
import org.springframework.http.ResponseEntity;
Expand All @@ -20,6 +20,7 @@
/**
* @author Alexander Dudkin
*/
@Deprecated(forRemoval = true)
@RestControllerAdvice
public class GlobalRestExceptionHandler {

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package dev.earlspilner.users.rest.advice.custom;
package dev.earlspilner.users.rest.old.custom;

import lombok.Getter;
import org.springframework.http.HttpStatus;

import java.io.Serial;

@Deprecated
public class CustomJwtException extends RuntimeException {

@Serial
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package dev.earlspilner.users.rest.advice.custom;
package dev.earlspilner.users.rest.old.custom;

@Deprecated
public class UnauthorizedOperationException extends RuntimeException {
public UnauthorizedOperationException(String message) {
super(message);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package dev.earlspilner.users.rest.advice.custom;
package dev.earlspilner.users.rest.old.custom;

import org.springframework.web.bind.annotation.ResponseStatus;

import static org.springframework.http.HttpStatus.BAD_REQUEST;

@Deprecated
@ResponseStatus(BAD_REQUEST)
public class UserExistsException extends RuntimeException {
public UserExistsException(String message) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package dev.earlspilner.users.rest.advice.custom;
package dev.earlspilner.users.rest.old.custom;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@Deprecated
@ResponseStatus(HttpStatus.NOT_FOUND)
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(String message) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package dev.earlspilner.users.security;

import dev.earlspilner.users.rest.advice.custom.CustomJwtException;
import dev.earlspilner.users.rest.old.custom.CustomJwtException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package dev.earlspilner.users.security;

import dev.earlspilner.users.rest.advice.custom.CustomJwtException;
import dev.earlspilner.users.rest.old.custom.CustomJwtException;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
package dev.earlspilner.users.service;

import dev.earlspilner.users.dto.UserDto;
import dev.earlspilner.users.rest.advice.Result;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

/**
* @author Alexander Dudkin
*/
public interface UserService {
UserDto saveUser(UserDto dto);
UserDto getUser(String username);
Page<UserDto> getUsers(Pageable pageable);
UserDto updateUser(String username, UserDto dto);
void deleteUser(Integer id);
Result<UserDto> saveUser(UserDto dto);
Result<UserDto> getUser(String username);
Page<Result<UserDto>> getUsers(Pageable pageable);
Result<UserDto> updateUser(String username, UserDto dto);
Result<Void> deleteUser(Integer id);
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
package dev.earlspilner.users.service;

import dev.earlspilner.users.dto.UserDto;
import dev.earlspilner.users.model.User;
import dev.earlspilner.users.mapper.UserMapper;
import dev.earlspilner.users.model.User;
import dev.earlspilner.users.repository.UserRepository;
import dev.earlspilner.users.rest.advice.custom.UnauthorizedOperationException;
import dev.earlspilner.users.rest.advice.custom.UserExistsException;
import dev.earlspilner.users.rest.advice.custom.UserNotFoundException;
import dev.earlspilner.users.rest.advice.Failure;
import dev.earlspilner.users.rest.advice.Result;
import dev.earlspilner.users.rest.advice.Success;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Optional;

/**
* @author Alexander Dudkin
*/
Expand All @@ -35,61 +38,72 @@ public UserServiceImpl(UserMapper userMapper, UserRepository userRepository, Pas

@Override
@Transactional
public UserDto saveUser(UserDto dto) {
public Result<UserDto> saveUser(UserDto dto) {
if (userRepository.existsByUsername(dto.username()))
throw new UserExistsException("User already exists with username: " + dto.username());
return new Failure<>("User already exists with username: " + dto.username(), HttpStatus.CONFLICT.value());

if (userRepository.existsByEmail(dto.email()))
throw new UserExistsException("User already exists with email: " + dto.email());
return new Failure<>("User already exists with email: " + dto.email(), HttpStatus.CONFLICT.value());

User user = userMapper.toUserEntity(dto);
user.setPassword(passwordEncoder.encode(dto.password()));
userRepository.save(user);
return userMapper.toRegisterResponse(user);

return new Success<>(userMapper.toRegisterResponse(user));
}

@Override
@Transactional(readOnly = true)
public UserDto getUser(String username) {
public Result<UserDto> getUser(String username) {
return userRepository.findByUsername(username)
.map(userMapper::toUserDto)
.orElseThrow(() -> new UserNotFoundException("User not found with username: " + username));
.<Result<UserDto>>map(Success::new)
.orElse(new Failure<>("User not found with username: " + username, HttpStatus.NOT_FOUND.value()));
}

@Override
@Transactional(readOnly = true)
public Page<UserDto> getUsers(Pageable pageable) {
public Page<Result<UserDto>> getUsers(Pageable pageable) {
return userRepository.findAll(pageable)
.map(userMapper::toUserDto);
.map(userMapper::toUserDto)
.map(Success::new);
}

@Override
@Transactional
public UserDto updateUser(String username, UserDto dto) {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UserNotFoundException("User not found with username: " + username));
public Result<UserDto> updateUser(String username, UserDto dto) {
Optional<User> optionalUser = userRepository.findByUsername(username);

if (optionalUser.isEmpty()) {
return new Failure<>("User not found with username: " + username, HttpStatus.NOT_FOUND.value());
}

User user = optionalUser.get();
String authenticatedUsername = getAuthenticatedUsername();

if (!authenticatedUsername.equals(username)) {
throw new UnauthorizedOperationException("You are not allowed to update this user.");
return new Failure<>("You are not allowed to update this user.", HttpStatus.FORBIDDEN.value());
}

user.setName(dto.name());
user.setUsername(dto.username());
user.setEmail(dto.email());
user.setPassword(passwordEncoder.encode(dto.password()));
User savedUser = userRepository.save(user);

return userMapper.toUserDto(userRepository.save(user));
return new Success<>(userMapper.toUserDto(savedUser));
}

@Override
@Transactional
public void deleteUser(Integer id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException("User not found with id: " + id));
public Result<Void> deleteUser(Integer id) {
Optional<User> optionalUser = userRepository.findById(id);
if (optionalUser.isEmpty()) {
return new Failure<>("User not found with ID: " + id, HttpStatus.NOT_FOUND.value());
}

userRepository.delete(user);
userRepository.delete(optionalUser.get());
return new Success<>(null);
}

private String getAuthenticatedUsername() {
Expand Down

0 comments on commit 6cbf202

Please sign in to comment.