Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ val micrometerVersion = "1.4.1"
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-actuator")
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:$springDocOpenApiVersion")
implementation("io.micrometer:micrometer-tracing-bridge-otel:$micrometerVersion")
Expand All @@ -44,6 +45,7 @@ dependencies {

compileOnly("org.projectlombok:lombok")
annotationProcessor("org.projectlombok:lombok")
testAnnotationProcessor("org.projectlombok:lombok")

// Testing
testImplementation("org.springframework.boot:spring-boot-starter-test")
Expand Down
4 changes: 4 additions & 0 deletions gradle.lockfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.18.2=compileClasspath
com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.2=compileClasspath
com.fasterxml.jackson.module:jackson-module-parameter-names:2.18.2=compileClasspath
com.fasterxml.jackson:jackson-bom:2.18.2=compileClasspath
com.fasterxml:classmate:1.7.0=compileClasspath
io.micrometer:context-propagation:1.1.2=compileClasspath
io.micrometer:micrometer-commons:1.14.2=compileClasspath
io.micrometer:micrometer-core:1.14.2=compileClasspath
Expand Down Expand Up @@ -48,6 +49,8 @@ org.apache.logging.log4j:log4j-to-slf4j:2.24.3=compileClasspath
org.apache.tomcat.embed:tomcat-embed-core:10.1.34=compileClasspath
org.apache.tomcat.embed:tomcat-embed-el:10.1.34=compileClasspath
org.apache.tomcat.embed:tomcat-embed-websocket:10.1.34=compileClasspath
org.hibernate.validator:hibernate-validator:8.0.2.Final=compileClasspath
org.jboss.logging:jboss-logging:3.6.1.Final=compileClasspath
org.jspecify:jspecify:1.0.0=compileClasspath
org.openapitools:jackson-databind-nullable:0.2.6=compileClasspath
org.projectlombok:lombok:1.18.36=compileClasspath
Expand All @@ -63,6 +66,7 @@ org.springframework.boot:spring-boot-starter-actuator:3.4.1=compileClasspath
org.springframework.boot:spring-boot-starter-json:3.4.1=compileClasspath
org.springframework.boot:spring-boot-starter-logging:3.4.1=compileClasspath
org.springframework.boot:spring-boot-starter-tomcat:3.4.1=compileClasspath
org.springframework.boot:spring-boot-starter-validation:3.4.1=compileClasspath
org.springframework.boot:spring-boot-starter-web:3.4.1=compileClasspath
org.springframework.boot:spring-boot-starter:3.4.1=compileClasspath
org.springframework.boot:spring-boot:3.4.1=compileClasspath
Expand Down
15 changes: 15 additions & 0 deletions openapi/template-payments-java-repository.openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,18 @@ paths:
error:
type: string
example: "Internal Server Error"
components:
schemas:
ErrorDTO:
type: object
required:
- code
- message
properties:
code:
type: string
enum:
- BAD_REQUEST
- GENERIC_ERROR
message:
type: string
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package it.gov.pagopa.template.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,24 @@
package it.gov.pagopa.template.config.json;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import org.springframework.context.annotation.Configuration;

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

@Configuration
public class LocalDateTimeToOffsetDateTimeSerializer extends JsonSerializer<LocalDateTime> {

@Override
public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
if (value != null) {
OffsetDateTime offsetDateTime = value.atZone(ZoneId.systemDefault()).toOffsetDateTime();
gen.writeString(offsetDateTime.toString());
}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package it.gov.pagopa.template.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
@@ -0,0 +1,96 @@
package it.gov.pagopa.template.exception;

import com.fasterxml.jackson.databind.JsonMappingException;
import it.gov.pagopa.template.dto.generated.ErrorDTO;
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
@Order(Ordered.HIGHEST_PRECEDENCE)
public class ControllerExceptionHandler {

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

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

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

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

String message = buildReturnedMessage(ex);

return ResponseEntity
.status(httpStatus)
.body(new ErrorDTO(errorEnum, message));
}

private static void logException(Exception ex, HttpServletRequest request, HttpStatusCode httpStatus) {
log.info("A {} occurred handling request {}: HttpStatus {} - {}",
ex.getClass(),
getRequestDetails(request),
httpStatus.value(),
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) {
return "%s %s".formatted(request.getMethod(), request.getRequestURI());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package it.gov.pagopa.template.config.json;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

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

import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;

@ExtendWith(MockitoExtension.class)
class LocalDateTimeToOffsetDateTimeSerializerTest {

@Mock
private JsonGenerator jsonGenerator;

@Mock
private SerializerProvider serializerProvider;

private LocalDateTimeToOffsetDateTimeSerializer dateTimeSerializer;

@BeforeEach
public void setUp() {
dateTimeSerializer = new LocalDateTimeToOffsetDateTimeSerializer();
}

@Test
void testDateSerializer() throws IOException {
LocalDateTime localDateTime = LocalDateTime.of(2025, 1, 16, 9, 15, 20);

TimeZone.setDefault(TimeZone.getTimeZone("Europe/Rome"));

dateTimeSerializer.serialize(localDateTime, jsonGenerator, serializerProvider);

verify(jsonGenerator).writeString("2025-01-16T09:15:20+01:00");
}

@Test
void testNullDateSerializer() throws IOException {
dateTimeSerializer.serialize(null, jsonGenerator, serializerProvider);

verifyNoInteractions(jsonGenerator);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package it.gov.pagopa.template.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
Loading