Skip to content

Commit

Permalink
fix: ErrorHandling and LocalDateTime json serialization (#46)
Browse files Browse the repository at this point in the history
  • Loading branch information
antonioT90 authored Feb 5, 2025
1 parent 654ff4c commit 60beed8
Show file tree
Hide file tree
Showing 11 changed files with 360 additions and 74 deletions.
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ val podamVersion = "8.0.2.RELEASE"
dependencies {
implementation("org.springframework.boot:spring-boot-starter")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server")
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("org.springframework.boot:spring-boot-starter-data-rest")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package it.gov.pagopa.pu.debtpositions.config.json;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.time.LocalDateTime;
import java.util.TimeZone;

@Configuration
public class JsonConfig {

@Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(configureDateTimeModule());
mapper.registerModule(new Jdk8Module());
mapper.registerModule(new ParameterNamesModule(JsonCreator.Mode.DEFAULT));
mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE);
mapper.setVisibility(PropertyAccessor.GETTER, JsonAutoDetect.Visibility.PUBLIC_ONLY);
mapper.setVisibility(PropertyAccessor.IS_GETTER, JsonAutoDetect.Visibility.PUBLIC_ONLY);
mapper.setVisibility(PropertyAccessor.SETTER, JsonAutoDetect.Visibility.PUBLIC_ONLY);
mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
mapper.setVisibility(PropertyAccessor.CREATOR, JsonAutoDetect.Visibility.ANY);
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
mapper.setTimeZone(TimeZone.getDefault());
return mapper;
}

/** openApi is documenting LocalDateTime as date-time, which is interpreted as an OffsetDateTime by openApiGenerator */
private static SimpleModule configureDateTimeModule() {
return new JavaTimeModule()
.addSerializer(LocalDateTime.class, new LocalDateTimeToOffsetDateTimeSerializer())
.addDeserializer(LocalDateTime.class, new OffsetDateTimeToLocalDateTimeDeserializer());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package it.gov.pagopa.pu.debtpositions.config.json;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import org.springframework.context.annotation.Configuration;

import java.io.IOException;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;


@Configuration
public class OffsetDateTimeToLocalDateTimeDeserializer extends JsonDeserializer<LocalDateTime> {

@Override
public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {

String dateString = p.getValueAsString();
if(dateString.contains("+")){
return OffsetDateTime.parse(dateString).toLocalDateTime();
} else {
return LocalDateTime.parse(dateString);
}
}
}

Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
package it.gov.pagopa.pu.debtpositions.exception;

import com.fasterxml.jackson.databind.JsonMappingException;
import it.gov.pagopa.pu.debtpositions.dto.generated.DebtPositionErrorDTO;
import it.gov.pagopa.pu.debtpositions.exception.custom.*;
import it.gov.pagopa.pu.debtpositions.exception.custom.ConflictErrorException;
import it.gov.pagopa.pu.debtpositions.exception.custom.InvalidValueException;
import it.gov.pagopa.pu.debtpositions.exception.custom.NotFoundException;
import it.gov.pagopa.pu.debtpositions.exception.custom.OperatorNotAuthorizedException;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.ValidationException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.FieldError;
import org.springframework.web.ErrorResponse;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.stream.Collectors;


@RestControllerAdvice
@Slf4j
Expand All @@ -23,45 +29,93 @@ public class DebtPositionExceptionHandler {

@ExceptionHandler({InvalidValueException.class})
public ResponseEntity<DebtPositionErrorDTO> handleInternalError(RuntimeException ex, HttpServletRequest request){
return handleWorkflowErrorException(ex, request, HttpStatus.BAD_REQUEST, DebtPositionErrorDTO.CodeEnum.BAD_REQUEST);
return handleException(ex, request, HttpStatus.BAD_REQUEST, DebtPositionErrorDTO.CodeEnum.BAD_REQUEST);
}

@ExceptionHandler({OperatorNotAuthorizedException.class})
public ResponseEntity<DebtPositionErrorDTO> handleForbiddenError(RuntimeException ex, HttpServletRequest request){
return handleWorkflowErrorException(ex, request, HttpStatus.FORBIDDEN, DebtPositionErrorDTO.CodeEnum.FORBIDDEN);
return handleException(ex, request, HttpStatus.FORBIDDEN, DebtPositionErrorDTO.CodeEnum.FORBIDDEN);
}

@ExceptionHandler({ConflictErrorException.class})
public ResponseEntity<DebtPositionErrorDTO> handleConflictError(RuntimeException ex, HttpServletRequest request){
return handleWorkflowErrorException(ex, request, HttpStatus.CONFLICT, DebtPositionErrorDTO.CodeEnum.CONFLICT);
return handleException(ex, request, HttpStatus.CONFLICT, DebtPositionErrorDTO.CodeEnum.CONFLICT);
}

@ExceptionHandler({NotFoundException.class})
public ResponseEntity<DebtPositionErrorDTO> handleNotFoundError(RuntimeException ex, HttpServletRequest request){
return handleWorkflowErrorException(ex, request, HttpStatus.NOT_FOUND, DebtPositionErrorDTO.CodeEnum.NOT_FOUND);
return handleException(ex, request, HttpStatus.NOT_FOUND, DebtPositionErrorDTO.CodeEnum.NOT_FOUND);
}

@ExceptionHandler({InvalidStatusTransitionException.class})
public ResponseEntity<DebtPositionErrorDTO> handleInvalidStatusTransitionException(RuntimeException ex, HttpServletRequest request){
return handleWorkflowErrorException(ex, request, HttpStatus.BAD_REQUEST, DebtPositionErrorDTO.CodeEnum.BAD_REQUEST);
return handleException(ex, request, HttpStatus.BAD_REQUEST, DebtPositionErrorDTO.CodeEnum.BAD_REQUEST);
}


@ExceptionHandler({ValidationException.class, HttpMessageNotReadableException.class, MethodArgumentNotValidException.class})
public ResponseEntity<DebtPositionErrorDTO> handleViolationException(Exception ex, HttpServletRequest request) {
return handleException(ex, request, HttpStatus.BAD_REQUEST, DebtPositionErrorDTO.CodeEnum.BAD_REQUEST);
}

static ResponseEntity<DebtPositionErrorDTO> handleWorkflowErrorException(RuntimeException ex, HttpServletRequest request, HttpStatus httpStatus, DebtPositionErrorDTO.CodeEnum errorEnum) {
String message = logException(ex, request, httpStatus);
@ExceptionHandler({ServletException.class})
public ResponseEntity<DebtPositionErrorDTO> handleServletException(ServletException ex, HttpServletRequest request) {
HttpStatusCode httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
DebtPositionErrorDTO.CodeEnum errorCode = DebtPositionErrorDTO.CodeEnum.GENERIC_ERROR;
if (ex instanceof ErrorResponse errorResponse) {
httpStatus = errorResponse.getStatusCode();
if (httpStatus.is4xxClientError()) {
errorCode = DebtPositionErrorDTO.CodeEnum.BAD_REQUEST;
}
}
return handleException(ex, request, httpStatus, errorCode);
}

@ExceptionHandler({RuntimeException.class})
public ResponseEntity<DebtPositionErrorDTO> handleRuntimeException(RuntimeException ex, HttpServletRequest request) {
return handleException(ex, request, HttpStatus.INTERNAL_SERVER_ERROR, DebtPositionErrorDTO.CodeEnum.GENERIC_ERROR);
}

static ResponseEntity<DebtPositionErrorDTO> handleException(Exception ex, HttpServletRequest request, HttpStatusCode httpStatus, DebtPositionErrorDTO.CodeEnum errorEnum) {
logException(ex, request, httpStatus);

String message = buildReturnedMessage(ex);

return ResponseEntity
.status(httpStatus)
.body(DebtPositionErrorDTO.builder().code(errorEnum).message(message).build());
.body(new DebtPositionErrorDTO(errorEnum, message));
}

private static String logException(RuntimeException ex, HttpServletRequest request, HttpStatus httpStatus) {
String message = ex.getMessage();
private static void logException(Exception ex, HttpServletRequest request, HttpStatusCode httpStatus) {
log.info("A {} occurred handling request {}: HttpStatus {} - {}",
ex.getClass(),
getRequestDetails(request),
httpStatus.value(),
message);
return message;
ex.getMessage());
}

private static String buildReturnedMessage(Exception ex) {
if (ex instanceof HttpMessageNotReadableException) {
if(ex.getCause() instanceof JsonMappingException jsonMappingException){
return "Cannot parse body: " +
jsonMappingException.getPath().stream()
.map(JsonMappingException.Reference::getFieldName)
.collect(Collectors.joining(".")) +
": " + jsonMappingException.getOriginalMessage();
}
return "Required request body is missing";
} else if (ex instanceof MethodArgumentNotValidException methodArgumentNotValidException) {
return "Invalid request content:" +
methodArgumentNotValidException.getBindingResult()
.getAllErrors().stream()
.map(e -> " " +
(e instanceof FieldError fieldError? fieldError.getField(): e.getObjectName()) +
": " + e.getDefaultMessage())
.sorted()
.collect(Collectors.joining(";"));
} else {
return ex.getMessage();
}
}

static String getRequestDetails(HttpServletRequest request) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package it.gov.pagopa.pu.debtpositions.model;

import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import it.gov.pagopa.pu.debtpositions.config.json.LocalDateTimeToOffsetDateTimeSerializer;
import jakarta.persistence.Column;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
Expand All @@ -26,10 +24,8 @@
public abstract class BaseEntity implements Serializable {
@Column(updatable = false)
@CreatedDate
@JsonSerialize(using = LocalDateTimeToOffsetDateTimeSerializer.class)
private LocalDateTime creationDate;
@LastModifiedDate
@JsonSerialize(using = LocalDateTimeToOffsetDateTimeSerializer.class)
private LocalDateTime updateDate;
@LastModifiedBy
private String updateOperatorExternalId;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
package it.gov.pagopa.pu.debtpositions.model.view.debtpositiontype;

import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import it.gov.pagopa.pu.debtpositions.config.json.LocalDateTimeToOffsetDateTimeSerializer;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.io.Serializable;
import java.time.LocalDateTime;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.Formula;

import java.io.Serializable;
import java.time.LocalDateTime;

@Entity
@Table(name = "debt_position_type")
@AllArgsConstructor
Expand All @@ -25,7 +24,6 @@ public class DebtPositionTypeWithCount implements Serializable {
private Long debtPositionTypeId;
private String code;
private String description;
@JsonSerialize(using = LocalDateTimeToOffsetDateTimeSerializer.class)
private LocalDateTime updateDate;
@Formula("(SELECT COUNT(*) "
+ "FROM debt_position_type_org org "
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package it.gov.pagopa.pu.debtpositions.util;

import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;

public class Utilities {

Expand All @@ -20,7 +20,7 @@ public static boolean isValidEmail(final String email) {

public static OffsetDateTime localDatetimeToOffsetDateTime(LocalDateTime localDateTime){
return localDateTime != null
? localDateTime.atOffset(ZoneOffset.UTC)
? localDateTime.atOffset(ZoneId.systemDefault().getRules().getOffset(localDateTime))
: null;
}
public static boolean isValidIban(String iban) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
package it.gov.pagopa.pu.debtpositions.config;
package it.gov.pagopa.pu.debtpositions.config.json;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import it.gov.pagopa.pu.debtpositions.config.json.LocalDateTimeToOffsetDateTimeSerializer;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package it.gov.pagopa.pu.debtpositions.config.json;

import com.fasterxml.jackson.core.JsonParser;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

import java.io.IOException;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;

class OffsetDateTimeToLocalDateTimeDeserializerTest {

private final OffsetDateTimeToLocalDateTimeDeserializer deserializer = new OffsetDateTimeToLocalDateTimeDeserializer();

@Test
void givenOffsetDateTimeWhenThenOk() throws IOException {
// Given
OffsetDateTime offsetDateTime = OffsetDateTime.now();
JsonParser parser = Mockito.mock(JsonParser.class);
Mockito.when(parser.getValueAsString())
.thenReturn(offsetDateTime.toString());

// When
LocalDateTime result = deserializer.deserialize(parser, null);

// Then
Assertions.assertEquals(offsetDateTime.toLocalDateTime(), result);
}

@Test
void givenLocalDateTimeWhenThenOk() throws IOException {
// Given
LocalDateTime localDateTime = LocalDateTime.now();
JsonParser parser = Mockito.mock(JsonParser.class);
Mockito.when(parser.getValueAsString())
.thenReturn(localDateTime.toString());

// When
LocalDateTime result = deserializer.deserialize(parser, null);

// Then
Assertions.assertEquals(localDateTime, result);
}
}
Loading

0 comments on commit 60beed8

Please sign in to comment.