diff --git a/spring-security-rest/pom.xml b/spring-security-rest/pom.xml index 2046b38810..5650f9c700 100644 --- a/spring-security-rest/pom.xml +++ b/spring-security-rest/pom.xml @@ -139,7 +139,26 @@ - + + org.springframework + spring-test + ${org.springframework.version} + test + + + + com.jayway.restassured + rest-assured + ${rest-assured.version} + test + + + commons-logging + commons-logging + + + + junit junit @@ -276,6 +295,7 @@ 1.3 4.12 1.10.19 + 2.4.1 2.2.2 diff --git a/spring-security-rest/src/main/java/org/baeldung/web/ApiError.java b/spring-security-rest/src/main/java/org/baeldung/web/ApiError.java new file mode 100644 index 0000000000..8352ca4ccd --- /dev/null +++ b/spring-security-rest/src/main/java/org/baeldung/web/ApiError.java @@ -0,0 +1,63 @@ +package org.baeldung.web; + +import java.util.Arrays; +import java.util.List; + +import org.springframework.http.HttpStatus; + +public class ApiError { + + private HttpStatus status; + private String message; + private List errors; + + // + + public ApiError() { + super(); + } + + public ApiError(final HttpStatus status, final String message, final List errors) { + super(); + this.status = status; + this.message = message; + this.errors = errors; + } + + public ApiError(final HttpStatus status, final String message, final String error) { + super(); + this.status = status; + this.message = message; + errors = Arrays.asList(error); + } + + // + + public HttpStatus getStatus() { + return status; + } + + public void setStatus(final HttpStatus status) { + this.status = status; + } + + public String getMessage() { + return message; + } + + public void setMessage(final String message) { + this.message = message; + } + public List getErrors() { + return errors; + } + + public void setErrors(final List errors) { + this.errors = errors; + } + + public void setError(final String error) { + errors = Arrays.asList(error); + } + +} diff --git a/spring-security-rest/src/main/java/org/baeldung/web/CustomRestExceptionHandler.java b/spring-security-rest/src/main/java/org/baeldung/web/CustomRestExceptionHandler.java new file mode 100644 index 0000000000..b6e7a919b9 --- /dev/null +++ b/spring-security-rest/src/main/java/org/baeldung/web/CustomRestExceptionHandler.java @@ -0,0 +1,152 @@ +package org.baeldung.web; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.beans.TypeMismatchException; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindException; +import org.springframework.validation.FieldError; +import org.springframework.validation.ObjectError; +import org.springframework.web.HttpMediaTypeNotSupportedException; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingServletRequestParameterException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import org.springframework.web.multipart.support.MissingServletRequestPartException; +import org.springframework.web.servlet.NoHandlerFoundException; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +@ControllerAdvice +public class CustomRestExceptionHandler extends ResponseEntityExceptionHandler { + + // 400 + + @Override + protected ResponseEntity handleMethodArgumentNotValid(final MethodArgumentNotValidException ex, final HttpHeaders headers, final HttpStatus status, final WebRequest request) { + logger.info(ex.getClass().getName()); + // + final List errors = new ArrayList(); + for (final FieldError error : ex.getBindingResult().getFieldErrors()) { + errors.add(error.getField() + ": " + error.getDefaultMessage()); + } + for (final ObjectError error : ex.getBindingResult().getGlobalErrors()) { + errors.add(error.getObjectName() + ": " + error.getDefaultMessage()); + } + final ApiError apiError = new ApiError(HttpStatus.BAD_REQUEST, ex.getLocalizedMessage(), errors); + return handleExceptionInternal(ex, apiError, headers, apiError.getStatus(), request); + } + + @Override + protected ResponseEntity handleBindException(final BindException ex, final HttpHeaders headers, final HttpStatus status, final WebRequest request) { + logger.info(ex.getClass().getName()); + // + final List errors = new ArrayList(); + for (final FieldError error : ex.getBindingResult().getFieldErrors()) { + errors.add(error.getField() + ": " + error.getDefaultMessage()); + } + for (final ObjectError error : ex.getBindingResult().getGlobalErrors()) { + errors.add(error.getObjectName() + ": " + error.getDefaultMessage()); + } + final ApiError apiError = new ApiError(HttpStatus.BAD_REQUEST, ex.getLocalizedMessage(), errors); + return handleExceptionInternal(ex, apiError, headers, apiError.getStatus(), request); + } + + @Override + protected ResponseEntity handleTypeMismatch(final TypeMismatchException ex, final HttpHeaders headers, final HttpStatus status, final WebRequest request) { + logger.info(ex.getClass().getName()); + // + final String error = ex.getValue() + " value for " + ex.getPropertyName() + " should be of type " + ex.getRequiredType(); + + final ApiError apiError = new ApiError(HttpStatus.BAD_REQUEST, ex.getLocalizedMessage(), error); + return new ResponseEntity(apiError, new HttpHeaders(), apiError.getStatus()); + } + + @ExceptionHandler({ MethodArgumentTypeMismatchException.class }) + public ResponseEntity handleMethodArgumentTypeMismatch(final MethodArgumentTypeMismatchException ex, final WebRequest request) { + logger.info(ex.getClass().getName()); + // + final String error = ex.getName() + " should be of type " + ex.getRequiredType().getName(); + + final ApiError apiError = new ApiError(HttpStatus.BAD_REQUEST, ex.getLocalizedMessage(), error); + return new ResponseEntity(apiError, new HttpHeaders(), apiError.getStatus()); + } + + @Override + protected ResponseEntity handleMissingServletRequestPart(final MissingServletRequestPartException ex, final HttpHeaders headers, final HttpStatus status, final WebRequest request) { + logger.info(ex.getClass().getName()); + // + final String error = ex.getRequestPartName() + " part is missing"; + final ApiError apiError = new ApiError(HttpStatus.BAD_REQUEST, ex.getLocalizedMessage(), error); + return new ResponseEntity(apiError, new HttpHeaders(), apiError.getStatus()); + } + + @Override + protected ResponseEntity handleMissingServletRequestParameter(final MissingServletRequestParameterException ex, final HttpHeaders headers, final HttpStatus status, final WebRequest request) { + logger.info(ex.getClass().getName()); + // + final String error = ex.getParameterName() + " parameter is missing"; + final ApiError apiError = new ApiError(HttpStatus.BAD_REQUEST, ex.getLocalizedMessage(), error); + return new ResponseEntity(apiError, new HttpHeaders(), apiError.getStatus()); + } + + // 404 + + @Override + protected ResponseEntity handleNoHandlerFoundException(final NoHandlerFoundException ex, final HttpHeaders headers, final HttpStatus status, final WebRequest request) { + logger.info(ex.getClass().getName()); + // + final String error = "No handler found for " + ex.getHttpMethod() + " " + ex.getRequestURL(); + + final ApiError apiError = new ApiError(HttpStatus.NOT_FOUND, ex.getLocalizedMessage(), error); + return new ResponseEntity(apiError, new HttpHeaders(), apiError.getStatus()); + } + + // 405 + + @Override + protected ResponseEntity handleHttpRequestMethodNotSupported(final HttpRequestMethodNotSupportedException ex, final HttpHeaders headers, final HttpStatus status, final WebRequest request) { + logger.info(ex.getClass().getName()); + // + final StringBuilder builder = new StringBuilder(); + builder.append(ex.getMethod()); + builder.append(" method is not supported for this request. Supported methods are "); + ex.getSupportedMethods(); + + final ApiError apiError = new ApiError(HttpStatus.METHOD_NOT_ALLOWED, ex.getLocalizedMessage(), builder.toString()); + return new ResponseEntity(apiError, new HttpHeaders(), apiError.getStatus()); + } + + // 415 + + @Override + protected ResponseEntity handleHttpMediaTypeNotSupported(final HttpMediaTypeNotSupportedException ex, final HttpHeaders headers, final HttpStatus status, final WebRequest request) { + logger.info(ex.getClass().getName()); + // + final StringBuilder builder = new StringBuilder(); + builder.append(ex.getContentType()); + builder.append(" media type is not supported. Supported media types are "); + ex.getSupportedMediaTypes().forEach(t -> builder.append(t + ", ")); + + final ApiError apiError = new ApiError(HttpStatus.UNSUPPORTED_MEDIA_TYPE, ex.getLocalizedMessage(), builder.substring(0, builder.length() - 2)); + return new ResponseEntity(apiError, new HttpHeaders(), apiError.getStatus()); + } + + + // 500 + + @ExceptionHandler({ Exception.class }) + public ResponseEntity handleAll(final Exception ex, final WebRequest request) { + logger.info(ex.getClass().getName()); + // + final ApiError apiError = new ApiError(HttpStatus.INTERNAL_SERVER_ERROR, ex.getLocalizedMessage(), "error occurred"); + return new ResponseEntity(apiError, new HttpHeaders(), apiError.getStatus()); + } + +} + diff --git a/spring-security-rest/src/main/java/org/baeldung/web/controller/FooController.java b/spring-security-rest/src/main/java/org/baeldung/web/controller/FooController.java index cd0153540f..3b9e5d25c0 100644 --- a/spring-security-rest/src/main/java/org/baeldung/web/controller/FooController.java +++ b/spring-security-rest/src/main/java/org/baeldung/web/controller/FooController.java @@ -9,11 +9,14 @@ import javax.servlet.http.HttpServletResponse; import org.baeldung.persistence.model.Foo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationEventPublisher; +import org.springframework.http.HttpStatus; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.util.UriComponentsBuilder; import com.google.common.collect.Lists; @@ -47,4 +50,11 @@ public class FooController { return Lists.newArrayList(new Foo(randomAlphabetic(6))); } + // write - just for test + @RequestMapping(method = RequestMethod.POST) + @ResponseStatus(HttpStatus.CREATED) + @ResponseBody + public Foo create(@RequestBody final Foo foo) { + return foo; + } } diff --git a/spring-security-rest/src/main/webapp/WEB-INF/web.xml b/spring-security-rest/src/main/webapp/WEB-INF/web.xml index 54f95d6227..3af8709dab 100644 --- a/spring-security-rest/src/main/webapp/WEB-INF/web.xml +++ b/spring-security-rest/src/main/webapp/WEB-INF/web.xml @@ -26,8 +26,11 @@ api - org.springframework.web.servlet.DispatcherServlet - 1 + org.springframework.web.servlet.DispatcherServlet + + throwExceptionIfNoHandlerFound + true + api diff --git a/spring-security-rest/src/test/java/org/baeldung/web/FooLiveTest.java b/spring-security-rest/src/test/java/org/baeldung/web/FooLiveTest.java new file mode 100644 index 0000000000..f148d090e2 --- /dev/null +++ b/spring-security-rest/src/test/java/org/baeldung/web/FooLiveTest.java @@ -0,0 +1,65 @@ +package org.baeldung.web; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.http.HttpStatus; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.support.AnnotationConfigContextLoader; + +import com.jayway.restassured.RestAssured; +import com.jayway.restassured.authentication.FormAuthConfig; +import com.jayway.restassured.response.Response; +import com.jayway.restassured.specification.RequestSpecification; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = { TestConfig.class }, loader = AnnotationConfigContextLoader.class) +public class FooLiveTest { + private static final String URL_PREFIX = "http://localhost:8080/spring-security-rest"; + private FormAuthConfig formConfig = new FormAuthConfig(URL_PREFIX + "/login", "username", "password"); + + private RequestSpecification givenAuth() { + return RestAssured.given().auth().form("user", "userPass", formConfig); + } + + @Test + public void whenMethodArgumentMismatch_thenBadRequest() { + final Response response = givenAuth().get(URL_PREFIX + "/api/foos/ccc"); + final ApiError error = response.as(ApiError.class); + assertEquals(HttpStatus.BAD_REQUEST, error.getStatus()); + assertEquals(1, error.getErrors().size()); + assertTrue(error.getErrors().get(0).contains("should be of type")); + + } + + @Test + public void whenNoHandlerForHttpRequest_thenNotFound() { + final Response response = givenAuth().delete(URL_PREFIX + "/api/xx"); + final ApiError error = response.as(ApiError.class); + assertEquals(HttpStatus.NOT_FOUND, error.getStatus()); + assertEquals(1, error.getErrors().size()); + assertTrue(error.getErrors().get(0).contains("No handler found")); + } + + @Test + public void whenHttpRequestMethodNotSupported_thenMethodNotAllowed() { + final Response response = givenAuth().delete(URL_PREFIX + "/api/foos/1"); + final ApiError error = response.as(ApiError.class); + assertEquals(HttpStatus.METHOD_NOT_ALLOWED, error.getStatus()); + assertEquals(1, error.getErrors().size()); + assertTrue(error.getErrors().get(0).contains("Supported methods are")); + } + + @Test + public void whenSendInvalidHttpMediaType_thenUnsupportedMediaType() { + final Response response = givenAuth().body("").post(URL_PREFIX + "/api/foos"); + final ApiError error = response.as(ApiError.class); + assertEquals(HttpStatus.UNSUPPORTED_MEDIA_TYPE, error.getStatus()); + assertEquals(1, error.getErrors().size()); + assertTrue(error.getErrors().get(0).contains("media type is not supported")); + } + +} diff --git a/spring-security-rest/src/test/java/org/baeldung/web/TestConfig.java b/spring-security-rest/src/test/java/org/baeldung/web/TestConfig.java new file mode 100644 index 0000000000..9ee3df73f1 --- /dev/null +++ b/spring-security-rest/src/test/java/org/baeldung/web/TestConfig.java @@ -0,0 +1,11 @@ +package org.baeldung.web; + +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ComponentScan({ "org.baeldung.web" }) +public class TestConfig { + + +} \ No newline at end of file