Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dev #41

Merged
merged 12 commits into from
Dec 18, 2024
Merged

Dev #41

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
4 changes: 2 additions & 2 deletions .github/workflows/gradle-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ env:
GITHUB_REGISTRY: ghcr.io/${{ github.actor }}
DOCKER_REGISTRY: docker.io/alexbob
tags: |
type=semver,pattern={{version}},value=v0.0.2
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{version}},value=v0.0.3
type=semver,pattern={{major}}.{{minor}},enable=${{ !startsWith(github.ref, 'refs/tags/v0.') }}
type=semver,pattern={{major}},enable=${{ !startsWith(github.ref, 'refs/tags/v0.') }}
type=ref,event=tag
jobs:
Expand Down
1 change: 0 additions & 1 deletion boot/platform/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ dependencies {

implementation("org.springframework.boot:spring-boot-starter-data-r2dbc")
implementation("org.postgresql:r2dbc-postgresql")
implementation("io.r2dbc:r2dbc-spi")

runtimeOnly ('io.netty.incubator:netty-incubator-codec-http3:0.0.28.Final')
implementation('io.netty:netty-tcnative-boringssl-static')
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,24 +1,17 @@
package com.plate.boot.commons;

import com.google.common.collect.Lists;
import com.plate.boot.commons.exception.RestServerException;
import io.r2dbc.spi.R2dbcException;
import lombok.extern.log4j.Log4j2;
import lombok.NonNull;
import org.springframework.dao.DataAccessException;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.r2dbc.BadSqlGrammarException;
import org.springframework.validation.ObjectError;
import org.springframework.http.*;
import org.springframework.web.ErrorResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.support.WebExchangeBindException;
import org.springframework.web.reactive.result.method.annotation.ResponseEntityExceptionHandler;
import org.springframework.web.server.ServerErrorException;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.ServerWebInputException;

import java.util.List;

import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR;
import reactor.core.publisher.Mono;

/**
* GlobalExceptionHandler is a centralized exception handler that intercepts and processes
Expand All @@ -27,85 +20,18 @@
* and informative error messages are returned to the client.
* <p>
* Key Features:
* - Handles {@link ServerWebInputException} to manage binding errors, including validation failures.
* - Handles {@link ResponseEntityExceptionHandler} to manage binding errors, including validation failures.
* - Manages data access exceptions such as {@link DataAccessException} and {@link R2dbcException}.
* - Custom exception handling for {@link RestServerException}, designed for application-specific errors.
* - Generic exception handling for uncaught {@link Exception}s to maintain robustness.
* <p>
* Each exception handler method transforms the exception into a standardized {@link ErrorResponse}
* format before returning it in the body of a {@link ResponseEntity} with the appropriate HTTP status code.
*/
@Log4j2
@ControllerAdvice
public class GlobalExceptionHandler {

/**
* Handles exceptions related to binding issues by creating an appropriate error response.
* This method is specifically designed to process {@link ServerWebInputException} and its subclasses,
* extracting error details to construct an {@link ErrorResponse} instance, which is then returned
* as part of a {@link ResponseEntity} with a status of {@link HttpStatus#EXPECTATION_FAILED}.
*
* @param exchange The current server web exchange containing request/response details.
* @param ex The exception that has been thrown during the binding process, providing insights into the failure.
* @return A {@link ResponseEntity} containing an {@link ErrorResponse} which encapsulates
* the error details including the request ID, request path, HTTP status code, reason for the failure,
* and a list of error messages detailing the fields in error and their respective messages.
*/
@ExceptionHandler(ServerWebInputException.class)
public ResponseEntity<ErrorResponse> handleBindException(ServerWebExchange exchange, ServerWebInputException ex) {
List<String> errors = Lists.newArrayList(ex.getReason());
if (ex instanceof WebExchangeBindException bindException) {
for (ObjectError objectError : bindException.getBindingResult().getAllErrors()) {
errors.add("Error field: %s, msg: %s.".formatted(objectError.getObjectName(),
objectError.getDefaultMessage()));
}
} else {
errors.add("Exception reason %s".formatted(ex.getReason()));
errors.add("Cause message %s.".formatted(ex.getCause().getMessage()));
}
if (log.isDebugEnabled()) {
log.error(ex.getReason(), ex);
}
return ResponseEntity.status(HttpStatus.EXPECTATION_FAILED).contentType(MediaType.APPLICATION_JSON)
.body(ErrorResponse.of(exchange.getRequest().getId(), HttpStatus.BAD_REQUEST.value(),
exchange.getRequest().getPath().value(), ex.getReason(), errors));
}
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {


/**
* Handles specific types of failure exceptions by creating an appropriate error response.
* This method is designed to catch and process {@link DataAccessException} and {@link R2dbcException},
* extracting useful information for logging and client response.
*
* @param exchange The current server web exchange containing the request and response details.
* @param ex The runtime exception that has been caught, which should be either a
* {@link DataAccessException} or an {@link R2dbcException}.
* @return A {@link ResponseEntity} containing an {@link ErrorResponse} with details about the failure,
* including an HTTP status of 507 (INSUFFICIENT_STORAGE), application/json content type,
* and specifics of the error extracted from the exception and exchange.
*/
@ExceptionHandler({DataAccessException.class, R2dbcException.class})
public ResponseEntity<ErrorResponse> handleFailureException(ServerWebExchange exchange, RuntimeException ex) {
List<String> errors = Lists.newArrayList("Database exec exception!");
if (ex instanceof R2dbcException r2dbcException) {
errors.add(r2dbcException.getSql());
errors.add(r2dbcException.getSqlState());
errors.add(r2dbcException.getMessage());
} else if (ex instanceof BadSqlGrammarException grammarException) {
errors.add(grammarException.getMessage());
errors.add(grammarException.getSql());
} else {
errors.add(ex.getLocalizedMessage());
}
if (log.isDebugEnabled()) {
log.error(ex.getLocalizedMessage(), ex);
log.error(ex.getCause().getMessage(), ex.getCause());
}
return ResponseEntity.status(HttpStatus.INSUFFICIENT_STORAGE).contentType(MediaType.APPLICATION_JSON)
.body(ErrorResponse.of(exchange.getRequest().getId(), HttpStatus.INSUFFICIENT_STORAGE.value(),
exchange.getRequest().getPath().value(), ex.getLocalizedMessage(), errors));
}

/**
* Handles exceptions of type {@link RestServerException} by creating an appropriate error response.
* This method is designed to be used within a Spring MVC controller advice context to manage
Expand All @@ -121,35 +47,28 @@ public ResponseEntity<ErrorResponse> handleFailureException(ServerWebExchange ex
* The error response includes the request ID, the request path, the error code,
* the localized error message, and any custom message provided by the exception.
*/
@ExceptionHandler(RestServerException.class)
public ResponseEntity<ErrorResponse> handleRestServerException(ServerWebExchange exchange, RestServerException ex) {
if (log.isDebugEnabled()) {
log.error(ex.getLocalizedMessage(), ex);
@Override
protected @NonNull Mono<ResponseEntity<Object>> handleServerErrorException(@NonNull ServerErrorException ex,
@NonNull HttpHeaders headers,
@NonNull HttpStatusCode status,
@NonNull ServerWebExchange exchange) {
if (logger.isDebugEnabled()) {
logger.error(ex.getLocalizedMessage(), ex);
}
return ResponseEntity.status(INTERNAL_SERVER_ERROR).contentType(MediaType.APPLICATION_JSON)
.body(ErrorResponse.of(exchange.getRequest().getId(), ex.getStatusCode().value(),
exchange.getRequest().getPath().value(), ex.getLocalizedMessage(), ex.getErrors()));
return handleExceptionInternal(ex, null, headers, status, exchange);
}

/**
* Handles exceptions of type {@link IllegalArgumentException} by creating an appropriate error response.
* This method is designed to be used within a Spring MVC controller advice context to manage
* exceptions that occur during the execution of RESTful server operations.
*
* @param exchange The current server web exchange containing request and response information.
* This is used to extract details necessary for constructing the
* error response.
*/
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<ErrorResponse> handleIllegalArgumentException(ServerWebExchange exchange,
IllegalArgumentException ex) {
if (log.isDebugEnabled()) {
log.error(ex.getLocalizedMessage(), ex);
@ExceptionHandler(DataAccessException.class)
public Mono<ResponseEntity<Object>> handleDataAccessException(DataAccessException ex, ServerWebExchange exchange) {
if (logger.isDebugEnabled()) {
logger.error(ex.getLocalizedMessage(), ex);
}
return ResponseEntity.status(INTERNAL_SERVER_ERROR).contentType(MediaType.APPLICATION_JSON)
.body(ErrorResponse.of(exchange.getRequest().getId(), HttpStatus.BAD_REQUEST.value(),
exchange.getRequest().getPath().value(), ex.getLocalizedMessage(), ex.getStackTrace()));
ProblemDetail problemDetail = ProblemDetail
.forStatusAndDetail(HttpStatus.INTERNAL_SERVER_ERROR, ex.getMessage());
problemDetail.setTitle("Bad Sql Grammar Data Access Exception");
problemDetail.setType(exchange.getRequest().getURI());
return handleExceptionInternal(ex, problemDetail, exchange.getRequest().getHeaders(),
HttpStatus.INSUFFICIENT_STORAGE, exchange);
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.plate.boot.security.core.UserAuditor;
import lombok.Data;
import org.springframework.data.annotation.*;
import org.springframework.data.relational.core.mapping.InsertOnlyProperty;

import java.time.LocalDateTime;
import java.util.Map;
Expand Down Expand Up @@ -56,6 +57,7 @@ public abstract class AbstractEntity<T> implements BaseEntity<T> {
* use User.class code property
*/
@CreatedBy
@InsertOnlyProperty
protected UserAuditor creator;

/**
Expand All @@ -75,6 +77,7 @@ public abstract class AbstractEntity<T> implements BaseEntity<T> {
* Data entity create time, timestamp column
*/
@CreatedDate
@InsertOnlyProperty
protected LocalDateTime createdTime;

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,7 @@ public Collection<?> convert(@NonNull Json source) {
return ContextUtils.OBJECT_MAPPER.readValue(source.asString(), new TypeReference<>() {
});
} catch (JsonProcessingException e) {
throw JsonException.withMsg("Json converter to collection error",
"Object Json converter to Collection error, message: " + e.getMessage());
throw JsonException.withMsg("Object Json converter to Collection error", e);
}
}
}
Expand All @@ -120,7 +119,7 @@ public static class CollectionWriteConverter implements Converter<Collection<?>,

/**
* Converts a given collection into its JSON representation.
*
* <p>
* This method takes a collection of any type and attempts to serialize it into a JSON object
* using the Jackson {@link ObjectMapper}. If the serialization process encounters a
* {@link JsonProcessingException}, it is caught and rethrown as a {@link JsonException} with a
Expand All @@ -135,8 +134,7 @@ public Json convert(@NonNull Collection<?> source) {
try {
return Json.of(ContextUtils.OBJECT_MAPPER.writeValueAsBytes(source));
} catch (JsonProcessingException e) {
throw JsonException.withMsg("Collection converter to Json error",
"Collection converter to Json error, message: " + e.getMessage());
throw JsonException.withMsg("Collection converter to Json error", e);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package com.plate.boot.commons.exception;

import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import lombok.Getter;
import org.springframework.core.MethodParameter;
import org.springframework.lang.Nullable;
import org.springframework.web.server.ServerErrorException;

import java.io.Serializable;
import java.lang.reflect.Method;

/**
* Represents a custom exception class for handling REST server errors, which includes an error message, a status code,
Expand All @@ -22,29 +24,37 @@
* <li>Convenience static factory methods for instantiation.</li>
* </ul>
*/
@Data
@Getter
@EqualsAndHashCode(callSuper = true)
public class RestServerException extends RuntimeException implements Serializable {
public class RestServerException extends ServerErrorException {

protected final HttpStatusCode statusCode;
protected final Object errors;

public RestServerException(HttpStatusCode code, String message, Object msg) {
super(message);
this.statusCode = code;
this.errors = msg;
public RestServerException(String reason, Throwable cause) {
super(reason, cause);
}

public RestServerException(String message, Throwable throwable) {
this(HttpStatus.INTERNAL_SERVER_ERROR, message, throwable.fillInStackTrace());
public RestServerException(String reason, Method handlerMethod, Throwable cause) {
super(reason, handlerMethod, cause);
}

public static RestServerException withMsg(String message, Object errors) {
return withMsg(HttpStatusCode.valueOf(500), message, errors);
public RestServerException(String reason, MethodParameter parameter, Throwable cause) {
super(reason, parameter, cause);
}

public static RestServerException withMsg(HttpStatusCode code, String message, Object errors) {
return new RestServerException(code, message, errors);
public static RestServerException withMsg(String reason, Throwable cause) {
var ex = new RestServerException(reason, cause);
ex.setTitle(reason);
ex.setDetail(cause.getMessage());
ex.setStackTrace(cause.getStackTrace());
return ex;
}

public static RestServerException withMsg(String reason, Method handlerMethod, @Nullable Throwable cause) {
return new RestServerException(reason, handlerMethod, cause);
}

public static RestServerException withMsg(String reason, MethodParameter parameter, Throwable cause) {
return new RestServerException(reason, parameter, cause);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -60,18 +60,19 @@ public final class BeanUtils implements InitializingBean {
* or if there is an issue converting the JsonNode to the target class.
*/
public static <T> T jsonPathToBean(JsonNode json, String path, Class<T> clazz) {

String[] paths = StringUtils.commaDelimitedListToStringArray(path);
StringJoiner pathJoiner = new StringJoiner("/");
for (String p : paths) {
pathJoiner.add(p);
}
JsonPointer jsonPointer = JsonPointer.valueOf(pathJoiner.toString());
JsonNode valueNode = json.at(jsonPointer);
if (valueNode.isMissingNode()) {
throw JsonPointerException.withError("Json pointer path error, path: {}" + pathJoiner,
new IllegalArgumentException(pathJoiner + " is not found in the json [" + json + "]"));
}
try {
String[] paths = StringUtils.commaDelimitedListToStringArray(path);
StringJoiner pathJoiner = new StringJoiner("/");
for (String p : paths) {
pathJoiner.add(p);
}
JsonPointer jsonPointer = JsonPointer.valueOf(pathJoiner.toString());
JsonNode valueNode = json.at(jsonPointer);
if (valueNode.isMissingNode()) {
throw JsonPointerException.withMsg("Json pointer path error",
"JsonPointer path is not exist!");
}
return ContextUtils.OBJECT_MAPPER.convertValue(valueNode, clazz);
} catch (IllegalArgumentException e) {
throw JsonPointerException.withError("Json pointer covert error", e);
Expand Down
Loading
Loading