ApiExceptionHandler.java
/*
* Copyright 2019-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.bremersee.exception.spring.boot.autoconfigure.reactive;
import static java.util.Objects.nonNull;
import static java.util.Objects.requireNonNullElse;
import static org.springframework.util.StringUtils.hasText;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.List;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.bremersee.exception.RestApiExceptionConstants;
import org.bremersee.exception.RestApiExceptionMapper;
import org.bremersee.exception.RestApiResponseType;
import org.bremersee.exception.model.RestApiException;
import org.springframework.boot.autoconfigure.web.WebProperties;
import org.springframework.boot.autoconfigure.web.reactive.error.AbstractErrorWebExceptionHandler;
import org.springframework.boot.web.reactive.error.ErrorAttributes;
import org.springframework.context.ApplicationContext;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.lang.NonNull;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
/**
* The reactive api exception handler.
*
* @author Christian Bremer
*/
@Slf4j
public class ApiExceptionHandler extends AbstractErrorWebExceptionHandler {
@Getter(AccessLevel.PROTECTED)
private final List<String> apiPaths;
@Getter(AccessLevel.PROTECTED)
@Setter
private PathMatcher pathMatcher = new AntPathMatcher();
@Getter(AccessLevel.PROTECTED)
private final RestApiExceptionMapper restApiExceptionMapper;
/**
* Instantiates a new api exception handler.
*
* @param apiPaths the api paths
* @param errorAttributes the error attributes
* @param resources the resources
* @param applicationContext the application context
* @param serverCodecConfigurer the server codec configurer
* @param restApiExceptionMapper the rest api exception mapper
*/
public ApiExceptionHandler(
List<String> apiPaths,
ErrorAttributes errorAttributes,
WebProperties.Resources resources,
ApplicationContext applicationContext,
ServerCodecConfigurer serverCodecConfigurer,
RestApiExceptionMapper restApiExceptionMapper) {
super(errorAttributes, resources, applicationContext);
if (serverCodecConfigurer != null) {
setMessageReaders(serverCodecConfigurer.getReaders());
setMessageWriters(serverCodecConfigurer.getWriters());
}
this.apiPaths = nonNull(apiPaths) ? apiPaths : List.of();
this.restApiExceptionMapper = restApiExceptionMapper;
}
@Override
protected RouterFunction<ServerResponse> getRoutingFunction(
ErrorAttributes errorAttributes) {
return RouterFunctions.route(this::isResponsibleExceptionHandler, this::renderErrorResponse);
}
/**
* Is this exception handler responsible.
*
* @param request the request
* @return {@code true} if it is responsible, otherwise {@code false}
*/
protected boolean isResponsibleExceptionHandler(ServerRequest request) {
return apiPaths.stream().anyMatch(
path -> getPathMatcher().match(path, request.path()));
}
/**
* Render error response.
*
* @param request the request
* @return the server response
*/
@NonNull
protected Mono<ServerResponse> renderErrorResponse(ServerRequest request) {
RestApiException response = getRestApiExceptionMapper()
.build(getError(request), request.path(), null);
RestApiResponseType restApiResponseType = RestApiResponseType
.detectByAccepted(request.headers().accept());
if (RestApiResponseType.HEADER == restApiResponseType) {
return emptyWithHeaders(response, restApiResponseType.getContentType());
} else {
return ServerResponse
.status(requireNonNullElse(
response.getStatus(),
HttpStatus.INTERNAL_SERVER_ERROR.value()))
.contentType(restApiResponseType.getContentType())
.body(BodyInserters.fromValue(response));
}
}
private Mono<ServerResponse> emptyWithHeaders(
RestApiException response,
MediaType contentType) {
ServerResponse.BodyBuilder builder = ServerResponse
.status(requireNonNullElse(
response.getStatus(),
HttpStatus.INTERNAL_SERVER_ERROR.value()));
if (hasText(response.getId())) {
builder = builder.header(RestApiExceptionConstants.ID_HEADER_NAME, response.getId());
}
String timestamp;
if (nonNull(response.getTimestamp())) {
timestamp = response.getTimestamp()
.format(RestApiExceptionConstants.TIMESTAMP_FORMATTER);
} else {
timestamp = OffsetDateTime.now(ZoneOffset.UTC)
.format(RestApiExceptionConstants.TIMESTAMP_FORMATTER);
}
builder = builder.header(RestApiExceptionConstants.TIMESTAMP_HEADER_NAME, timestamp);
if (hasText(response.getErrorCode())) {
builder = builder.header(
RestApiExceptionConstants.CODE_HEADER_NAME,
response.getErrorCode());
builder = builder.header(
RestApiExceptionConstants.CODE_INHERITED_HEADER_NAME,
String.valueOf(response.getErrorCodeInherited()));
}
if (hasText(response.getMessage())) {
builder = builder.header(
RestApiExceptionConstants.MESSAGE_HEADER_NAME,
response.getMessage());
}
if (hasText(response.getException())) {
builder = builder.header(
RestApiExceptionConstants.EXCEPTION_HEADER_NAME,
response.getException());
}
if (hasText(response.getApplication())) {
builder = builder.header(
RestApiExceptionConstants.APPLICATION_HEADER_NAME,
response.getApplication());
}
if (hasText(response.getPath())) {
builder = builder.header(
RestApiExceptionConstants.PATH_HEADER_NAME,
response.getPath());
}
return builder
.contentType(contentType)
.body(BodyInserters.empty());
}
}