├── .editorconfig ├── .gitignore ├── .maven-bintray.xml ├── .travis.yml ├── CHANGELOG.adoc ├── LICENSE ├── README.adoc ├── pom.xml ├── script └── travis-deploy └── src ├── it ├── groovy │ └── cz │ │ └── jirutka │ │ └── spring │ │ └── exhandler │ │ ├── AbstractConfigurationIT.groovy │ │ ├── JavaConfigurationIT.groovy │ │ ├── XmlConfigurationIT.groovy │ │ └── fixtures │ │ ├── SampleController.groovy │ │ ├── ZuulException.groovy │ │ └── ZuulExceptionHandler.groovy └── resources │ ├── testContext.xml │ └── testMessages.properties ├── main ├── java │ └── cz │ │ └── jirutka │ │ └── spring │ │ └── exhandler │ │ ├── MapUtils.java │ │ ├── RestHandlerExceptionResolver.java │ │ ├── RestHandlerExceptionResolverBuilder.java │ │ ├── RestHandlerExceptionResolverFactoryBean.java │ │ ├── handlers │ │ ├── AbstractRestExceptionHandler.java │ │ ├── ConstraintViolationExceptionHandler.java │ │ ├── ErrorMessageRestExceptionHandler.java │ │ ├── HttpMediaTypeNotSupportedExceptionHandler.java │ │ ├── HttpRequestMethodNotSupportedExceptionHandler.java │ │ ├── MethodArgumentNotValidExceptionHandler.java │ │ ├── NoSuchRequestHandlingMethodExceptionHandler.java │ │ ├── ResponseStatusRestExceptionHandler.java │ │ └── RestExceptionHandler.java │ │ ├── interpolators │ │ ├── MessageInterpolator.java │ │ ├── MessageInterpolatorAware.java │ │ ├── NoOpMessageInterpolator.java │ │ └── SpelMessageInterpolator.java │ │ ├── messages │ │ ├── ErrorMessage.java │ │ └── ValidationErrorMessage.java │ │ └── support │ │ └── HttpMessageConverterUtils.java └── resources │ └── cz │ └── jirutka │ └── spring │ └── exhandler │ └── messages.properties └── test ├── groovy └── cz │ └── jirutka │ └── spring │ └── exhandler │ ├── RestHandlerExceptionResolverBuilderTest.groovy │ ├── RestHandlerExceptionResolverFactoryBeanTest.groovy │ ├── RestHandlerExceptionResolverTest.groovy │ ├── handlers │ ├── AbstractRestExceptionHandlerTest.groovy │ ├── ConstraintViolationExceptionHandlerTest.groovy │ ├── ErrorMessageRestExceptionHandlerTest.groovy │ ├── HttpMediaTypeNotSupportedExceptionHandlerTest.groovy │ ├── HttpRequestMethodNotSupportedExceptionHandlerTest.groovy │ ├── MethodArgumentNotValidExceptionHandlerTest.groovy │ ├── NoSuchRequestHandlingMethodExceptionHandlerTest.groovy │ └── ResponseStatusRestExceptionHandlerTest.groovy │ ├── interpolators │ ├── NoOpMessageInterpolatorTest.groovy │ └── SpelMessageInterpolatorTest.groovy │ ├── messages │ └── ErrorMessageTest.groovy │ └── test │ └── BindingResultBuilder.groovy └── resources └── logback-test.xml /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 4 8 | indent_style = space 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.{adoc,yml}] 13 | indent_size = 2 14 | 15 | [script/*] 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | .idea 3 | *.iml 4 | -------------------------------------------------------------------------------- /.maven-bintray.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | jfrog-oss-snapshot-local 9 | jirutka 10 | ${env.BINTRAY_API_KEY} 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: java 3 | jdk: 4 | - openjdk7 5 | - oraclejdk8 6 | env: 7 | global: 8 | secure: "DiliRHV1VROi28XVoFVupupLMVJ0IqyuJsBR2NyfjhAB4eOvwyRY78H2iNzZ5TPkgQ2x5roCE9vDF4WRlRGd/rTgs2YvRjCL2tOpjxYXgyjuFKq0HtnG6Jpr45LV7dmTxiAUXjiNUa9SSr30CXAOEM2dcPYjci3Ze7HV/5BtkSU=" # BINTRAY_API_KEY 9 | matrix: 10 | - SPRING_VERSION=3.2.17.RELEASE 11 | - SPRING_VERSION=4.0.9.RELEASE 12 | - SPRING_VERSION=4.1.9.RELEASE 13 | - SPRING_VERSION=4.2.6.RELEASE 14 | 15 | # Cache local Maven repository. 16 | cache: 17 | directories: 18 | - $HOME/.m2 19 | before_cache: 20 | - rm -Rf $HOME/.m2/repository/cz/jirutka/spring/spring-rest-exception-handler 21 | 22 | install: 23 | - mvn install -Dspring.version=$SPRING_VERSION -DskipTests=true --batch-mode 24 | script: 25 | - mvn verify -Dspring.version=$SPRING_VERSION --batch-mode 26 | after_success: 27 | - mvn jacoco:report coveralls:report 28 | - script/travis-deploy 29 | -------------------------------------------------------------------------------- /CHANGELOG.adoc: -------------------------------------------------------------------------------- 1 | = Changelog 2 | :repo-uri: https://github.com/jirutka/spring-rest-exception-handler 3 | :issue-uri: {repo-uri}/issues 4 | 5 | == 1.2.0 (2015-05-16) 6 | 7 | * Modify `ErrorMessageRestExceptionHandler` to log missing message on the DEBUG level instead of INFO. 8 | * Inherit versions of dependencies from Spring’s platform-bom. 9 | * Deploy snapshots to JFrog OSS repository. 10 | 11 | == 1.1.1 (2015-08-02) 12 | 13 | * Fix compatibility with Spring 4.2.0. 14 | 15 | == 1.1.0 (2015-06-25) 16 | 17 | * Include fields from `ErrorMessage` in `ValidationErrorMessage#toString()`. 18 | * Get locale from `LocaleContextHolder` instead of `HttpServletRequest` (thanks to @lukasz-kusek). [{issue-uri}/7[#7]] 19 | 20 | == 1.0.3 (2015-03-31) 21 | 22 | * Fix error when deserializing `ErrorMessage` from JSON using Jackson 2 due to multiple setters for the status property (thanks to @lukasniemeier-zalando). [{issue-uri}/6[#6]] 23 | 24 | == 1.0.2 (2015-02-07) 25 | 26 | * Modify `ErrorMessageRestExceptionHandler` to log missing message on the level INFO instead of WARN. [{issue-uri}/3[#3]] 27 | * Fix compile error when Jackson 2 is not on the classpath. 28 | * Fix problem with missing `MappingJacksonHttpMessageConverter` on Spring 4.1.0 and greater. 29 | 30 | == 1.0.1 (2014-06-19) 31 | 32 | * Add exception handler for `ConstraintViolationException` from the Bean Validation (JSR 303/349). 33 | * Fix message key of detail for `MethodArgumentNotValidException`. 34 | * Fix content negotiation to prefer the specified default content type when client doesn’t provide the Accept header. [{issue-uri}/2[#2]] 35 | * Improve integration tests. 36 | 37 | == 1.0 (2014-04-29) 38 | 39 | First stable release. 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | = Spring REST Exception handler 2 | :source-language: java 3 | // Project meta 4 | :name: spring-rest-exception-handler 5 | :version: 1.2.0 6 | :group-id: cz.jirutka.spring 7 | :artifact-id: {name} 8 | :gh-name: jirutka/{name} 9 | :gh-branch: master 10 | :codacy-id: ca5dbac87d564725b6640a67b2b7ea35 11 | // URIs 12 | :src-base: link:src/main/java/cz/jirutka/spring/exhandler 13 | :spring-jdoc-uri: https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework 14 | 15 | ifdef::env-github[] 16 | image:https://travis-ci.org/{gh-name}.svg?branch={gh-branch}["Build Status", link="https://travis-ci.org/{gh-name}"] 17 | image:https://coveralls.io/repos/github/{gh-name}/badge.svg?branch={gh-branch}[Coverage Status, link="https://coveralls.io/github/{gh-name}"] 18 | image:https://api.codacy.com/project/badge/grade/{codacy-id}["Codacy code quality", link="https://www.codacy.com/app/{gh-name}"] 19 | image:https://maven-badges.herokuapp.com/maven-central/{group-id}/{artifact-id}/badge.svg[Maven Central, link="https://maven-badges.herokuapp.com/maven-central/{group-id}/{artifact-id}"] 20 | endif::env-github[] 21 | 22 | 23 | The aim of this project is to provide a convenient exception handler (resolver) for RESTful APIs that meets a best-practices for error responses without repeating yourself. 24 | It’s very easy to handle custom exceptions, customize error responses and even localize them. 25 | Also solves some pitfalls footnote:[Nothing terrible, Spring MVC is still a far better then JAX-RS for RESTful APIs! ;)] in Spring MVC with a content negotiation when producing error responses. 26 | 27 | 28 | == Error message 29 | 30 | Error messages generated by `ErrorMessageRestExceptionHandler` follows the http://tools.ietf.org/html/draft-nottingham-http-problem-06[Problem Details for HTTP APIs] specification. 31 | 32 | For example, the following error message describes a validation exception. 33 | 34 | *In JSON format:* 35 | 36 | [source, json] 37 | ---- 38 | { 39 | "type": "http://example.org/errors/validation-failed", 40 | "title": "Validation Failed", 41 | "status": 422, 42 | "detail": "The content you've send contains 2 validation errors.", 43 | "errors": [{ 44 | "field": "title", 45 | "message": "must not be empty" 46 | }, { 47 | "field": "quantity", 48 | "rejected": -5, 49 | "message": "must be greater than zero" 50 | }] 51 | } 52 | ---- 53 | 54 | *… or in XML:* 55 | 56 | [source,xml] 57 | ---- 58 | 59 | http://example.org/errors/validation-failed 60 | Validation Failed 61 | 422 62 | The content you've send contains 2 validation errors. 63 | 64 | 65 | title 66 | must not be empty 67 | 68 | 69 | quantity 70 | -5 71 | must be greater than zero 72 | 73 | 74 | 75 | ---- 76 | 77 | 78 | == How does it work? 79 | 80 | === RestHandlerExceptionResolver 81 | 82 | The core class of this library that resolves all exceptions is {src-base}/RestHandlerExceptionResolver.java[RestHandlerExceptionResolver]. 83 | It holds a registry of `RestExceptionHandlers`. 84 | 85 | When your controller throws an exception, the `RestHandlerExceptionResolver` will: 86 | 87 | . Find an exception handler by the thrown exception type (or its supertype, supertype of the supertype… up to the `Exception` class if no more specific handler is found) and invoke it. 88 | . Find the best matching media type to produce (using {spring-jdoc-uri}/web/accept/ContentNegotiationManager.html[ContentNegotiationManager], utilises _Accept_ header by default). When the requested media type is not supported, then fallback to the configured default media type. 89 | . Write the response. 90 | 91 | 92 | === RestExceptionHandler 93 | 94 | Implementations of the {src-base}/handlers/RestExceptionHandler.java[RestExceptionHandler] interface are responsible for converting the exception into Spring’s {spring-jdoc-uri}/http/ResponseEntity.html[ResponseEntity] instance that contains a body, headers and a HTTP status code. 95 | 96 | The main implementation is {src-base}/handlers/ErrorMessageRestExceptionHandler.java[ErrorMessageRestExceptionHandler] that produces the `ErrorMessage` body (see above for example). 97 | All the attributes (besides status) are loaded from a properties file (see the section <>). 98 | This class also logs the exception (see the <> section). 99 | 100 | 101 | == Configuration 102 | 103 | === Java-based configuration 104 | 105 | [source] 106 | ---- 107 | @EnableWebMvc 108 | @Configuration 109 | public class RestContextConfig extends WebMvcConfigurerAdapter { 110 | 111 | @Override 112 | public void configureHandlerExceptionResolvers(List resolvers) { 113 | resolvers.add( exceptionHandlerExceptionResolver() ); // resolves @ExceptionHandler 114 | resolvers.add( restExceptionResolver() ); 115 | } 116 | 117 | @Bean 118 | public RestHandlerExceptionResolver restExceptionResolver() { 119 | return RestHandlerExceptionResolver.builder() 120 | .messageSource( httpErrorMessageSource() ) 121 | .defaultContentType(MediaType.APPLICATION_JSON) 122 | .addErrorMessageHandler(EmptyResultDataAccessException.class, HttpStatus.NOT_FOUND) 123 | .addHandler(MyException.class, new MyExceptionHandler()) 124 | .build(); 125 | } 126 | 127 | @Bean 128 | public MessageSource httpErrorMessageSource() { 129 | ReloadableResourceBundleMessageSource m = new ReloadableResourceBundleMessageSource(); 130 | m.setBasename("classpath:/org/example/messages"); 131 | m.setDefaultEncoding("UTF-8"); 132 | return m; 133 | } 134 | 135 | @Bean 136 | public ExceptionHandlerExceptionResolver exceptionHandlerExceptionResolver() { 137 | ExceptionHandlerExceptionResolver resolver = new ExceptionHandlerExceptionResolver(); 138 | resolver.setMessageConverters(HttpMessageConverterUtils.getDefaultHttpMessageConverters()); 139 | return resolver; 140 | } 141 | } 142 | ---- 143 | 144 | 145 | === XML-based configuration 146 | 147 | [source, xml] 148 | ---- 149 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 176 | 177 | 181 | ---- 182 | 183 | 184 | === Another resolvers 185 | 186 | The {spring-jdoc-uri}/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.html[ExceptionHandlerExceptionResolver] is used to resolve exceptions through {spring-jdoc-uri}/web/bind/annotation/ExceptionHandler.html[@ExceptionHandler] methods. 187 | It must be registered _before_ the RestHandlerExceptionResolver. 188 | If you don’t have any `@ExceptionHandler` methods, then you can omit the `exceptionHandlerExceptionResolver` bean declaration. 189 | 190 | 191 | === Default handlers 192 | 193 | Builder and FactoryBean registers a set of the default handlers by default. 194 | This can be disabled by setting `withDefaultHandlers` to false. 195 | 196 | 197 | === Localizable error messages 198 | 199 | Message values are read from a _properties_ file through the provided {spring-jdoc-uri}/context/MessageSource.html[MessageSource], so it can be simply customized and localized. 200 | Library contains a default link:src/main/resources/cz/jirutka/spring/exhandler/messages.properties[messages.properties] file that is implicitly set as a parent (i.e. fallback) of the provided message source. 201 | This can be disabled by setting `withDefaultMessageSource` to false (on a builder or factory bean). 202 | 203 | The key name is prefixed with a fully qualified class name of the Java exception, or `default` for the default value; this is used when no value for a particular exception class exists (even in the parent message source). 204 | 205 | Value is a message template that may contain https://docs.spring.io/spring/docs/current/spring-framework-reference/html/expressions.html[SpEL] expressions delimited by `#{` and `}`. 206 | Inside an expression, you can access the exception being handled and the current request (instance of http://docs.oracle.com/javaee/7/api/javax/servlet/http/HttpServletRequest.html[HttpServletRequest]) under the `ex`, resp. `req` variables. 207 | 208 | *For example:* 209 | 210 | [source, properties] 211 | ---- 212 | org.springframework.web.HttpMediaTypeNotAcceptableException.type=http://httpstatus.es/406 213 | org.springframework.web.HttpMediaTypeNotAcceptableException.title=Not Acceptable 214 | org.springframework.web.HttpMediaTypeNotAcceptableException.detail=\ 215 | This resource provides #{ex.supportedMediaTypes}, but you've requested #{req.getHeader('Accept')}. 216 | ---- 217 | 218 | 219 | === Exception logging 220 | 221 | Exceptions handled with status code 5×× are logged on ERROR level (incl. stack trace), other exceptions are logged on INFO level without a stack trace, or on DEBUG level with a stack trace if enabled. 222 | The logger name is `cz.jirutka.spring.exhandler.handlers.RestExceptionHandler` and a Marker is set to the exception’s full qualified name. 223 | 224 | 225 | === Why is 404 bypassing exception handler? 226 | 227 | When the {spring-jdoc-uri}/web/servlet/DispatcherServlet.html[DispatcherServlet] is unable to determine a corresponding handler for an incoming HTTP request, it sends 404 directly without bothering to call an exception handler (see http://stackoverflow.com/a/22751886/2217862[on StackOverflow]). 228 | This behaviour can be changed, *since Spring 4.0.0*, using `throwExceptionIfNoHandlerFound` init parameter. 229 | You should set this to true for a consistent error responses. 230 | 231 | *When using WebApplicationInitializer:* 232 | 233 | [source] 234 | ---- 235 | public class AppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { 236 | 237 | protected void customizeRegistration(ServletRegistration.Dynamic reg) { 238 | reg.setInitParameter("throwExceptionIfNoHandlerFound", "true"); 239 | } 240 | ... 241 | } 242 | ---- 243 | 244 | *…or classic web.xml:* 245 | 246 | [source, xml] 247 | ---- 248 | 249 | rest-dispatcher 250 | org.springframework.web.servlet.DispatcherServlet 251 | 252 | throwExceptionIfNoHandlerFound 253 | true 254 | 255 | ... 256 | 257 | ---- 258 | 259 | 260 | == How to get it? 261 | 262 | Released versions are available in jCenter and the Central Repository. 263 | Just add this artifact to your project: 264 | 265 | ._Maven_ 266 | [source, xml, subs="verbatim, attributes"] 267 | ---- 268 | 269 | {group-id} 270 | {artifact-id} 271 | {version} 272 | 273 | ---- 274 | 275 | ._Gradle_ 276 | [source, groovy, subs="verbatim, attributes"] 277 | compile '{group-id}:{artifact-id}:{version}' 278 | 279 | However if you want to use the last snapshot version, you have to add the JFrog OSS repository: 280 | 281 | ._Maven_ 282 | [source, xml] 283 | ---- 284 | 285 | jfrog-oss-snapshot-local 286 | JFrog OSS repository for snapshots 287 | https://oss.jfrog.org/oss-snapshot-local 288 | 289 | true 290 | 291 | 292 | ---- 293 | 294 | ._Gradle_ 295 | [source, groovy] 296 | ---- 297 | repositories { 298 | maven { 299 | url 'https://oss.jfrog.org/oss-snapshot-local' 300 | } 301 | } 302 | ---- 303 | 304 | 305 | == Requirements 306 | 307 | * Spring 3.2.0.RELEASE and newer is supported, but 4.× is highly recommended. 308 | * Jackson 1.× and 2.× are both supported and optional. 309 | 310 | 311 | == References 312 | 313 | * http://www.jayway.com/2012/09/23/improve-your-spring-rest-api-part-ii[Improve Your Spring REST API by M. Severson] 314 | * https://stormpath.com/blog/spring-mvc-rest-exception-handling-best-practices-part-1/[Spring MVC REST Exception Handling Best Practices by L. Hazlewood] 315 | * http://spring.io/blog/2013/11/01/exception-handling-in-spring-mvc[Exception Handling in Spring MVC by P. Chapman] 316 | * http://tools.ietf.org/html/draft-nottingham-http-problem-06[IETF draft Problem Details for HTTP APIs by M. Nottingham] 317 | 318 | 319 | == License 320 | 321 | This project is licensed under http://www.apache.org/licenses/LICENSE-2.0.html[Apache License 2.0]. 322 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 4.0.0 6 | 7 | 8 | cz.jirutka.maven 9 | groovy-lombok-parent 10 | 1.3.2 11 | 12 | 13 | 14 | 15 | 16 | cz.jirutka.spring 17 | spring-rest-exception-handler 18 | 1.2.0 19 | jar 20 | 21 | Spring REST Exception Handler 22 | A pluggable exception handler for RESTful APIs. 23 | http://github.com/jirutka/spring-rest-exception-handler 24 | 2014 25 | 26 | 27 | 28 | Jakub Jirutka 29 | jakub@jirutka.cz 30 | CTU in Prague 31 | http://www.cvut.cz 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | Apache 2.0 41 | http://www.apache.org/licenses/LICENSE-2.0.html 42 | 43 | 44 | 45 | 46 | http://github.com/jirutka/spring-rest-exception-handler 47 | scm:git:git@github.com:jirutka/spring-rest-exception-handler.git 48 | 49 | 50 | 51 | github 52 | http://github.com/jirutka/spring-rest-exception-handler/issues 53 | 54 | 55 | 56 | travis 57 | https://travis-ci.org/jirutka/spring-rest-exception-handler/ 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | io.spring.platform 67 | platform-bom 68 | 2.0.5.RELEASE 69 | pom 70 | import 71 | 72 | 73 | 74 | 75 | 76 | 80 | 81 | com.fasterxml.jackson.core 82 | jackson-annotations 83 | 84 | 85 | 86 | org.codehaus.jackson 87 | jackson-mapper-asl 88 | true 89 | 90 | 91 | 92 | org.springframework 93 | spring-core 94 | provided 95 | 96 | 97 | 98 | org.springframework 99 | spring-beans 100 | provided 101 | 102 | 103 | 104 | org.springframework 105 | spring-expression 106 | provided 107 | 108 | 109 | 110 | org.springframework 111 | spring-web 112 | provided 113 | 114 | 115 | 116 | org.springframework 117 | spring-webmvc 118 | provided 119 | 120 | 121 | 122 | javax.servlet 123 | javax.servlet-api 124 | provided 125 | 126 | 127 | 128 | 129 | javax.validation 130 | validation-api 131 | true 132 | 133 | 134 | 135 | 136 | 137 | org.codehaus.groovy 138 | groovy-json 139 | ${groovy.version} 140 | test 141 | 142 | 143 | 144 | org.codehaus.groovy 145 | groovy-xml 146 | ${groovy.version} 147 | test 148 | 149 | 150 | 151 | org.spockframework 152 | spock-core 153 | test 154 | 155 | 156 | 157 | org.spockframework 158 | spock-spring 159 | ${spock.version} 160 | 161 | 162 | org.codehaus.groovy 163 | groovy-all 164 | 165 | 166 | test 167 | 168 | 169 | 170 | 171 | cglib 172 | cglib-nodep 173 | 3.2.0 174 | test 175 | 176 | 177 | 178 | org.springframework 179 | spring-test 180 | test 181 | 182 | 183 | 184 | com.fasterxml.jackson.core 185 | jackson-databind 186 | test 187 | 188 | 189 | 190 | org.slf4j 191 | jcl-over-slf4j 192 | ${slf4j.version} 193 | test 194 | 195 | 196 | 197 | ch.qos.logback 198 | logback-classic 199 | test 200 | 201 | 202 | 203 | org.hibernate 204 | hibernate-validator 205 | test 206 | 207 | 208 | 209 | -------------------------------------------------------------------------------- /script/travis-deploy: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # vim: set ts=4: 3 | set -o errexit 4 | 5 | cd "$(dirname "$0")/.." 6 | 7 | if [ "$TRAVIS_PULL_REQUEST" != 'false' ]; then 8 | echo 'This is a pull request, skipping deploy.'; exit 0 9 | fi 10 | 11 | if [ -z "$BINTRAY_API_KEY" ]; then 12 | echo '$BINTRAY_API_KEY is not set, skipping deploy.'; exit 0 13 | fi 14 | 15 | if [ "$TRAVIS_BRANCH" != 'master' ]; then 16 | echo 'This is not the master branch, skipping deploy.'; exit 0 17 | fi 18 | 19 | if [ "${TRAVIS_BUILD_NUMBER}.8" != "$TRAVIS_JOB_NUMBER" ]; then 20 | echo 'This is not the build job we are looking for, skipping deploy.'; exit 0 21 | fi 22 | 23 | echo '==> Deploying artifact to JFrog OSS Maven repository' 24 | mvn deploy --settings .maven-bintray.xml -Dgpg.skip=true -DskipTests=true 25 | -------------------------------------------------------------------------------- /src/it/groovy/cz/jirutka/spring/exhandler/AbstractConfigurationIT.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.exhandler 17 | 18 | import groovy.json.JsonSlurper 19 | import org.springframework.beans.factory.annotation.Autowired 20 | import org.springframework.http.MediaType 21 | import org.springframework.mock.web.MockHttpServletResponse 22 | import org.springframework.test.context.web.WebAppConfiguration 23 | import org.springframework.test.web.servlet.MockMvc 24 | import org.springframework.test.web.servlet.request.MockMvcRequestBuilders 25 | import org.springframework.test.web.servlet.setup.MockMvcBuilders 26 | import org.springframework.web.context.WebApplicationContext 27 | import spock.lang.Issue 28 | import spock.lang.Specification 29 | 30 | import static org.springframework.http.MediaType.APPLICATION_XML 31 | import static org.springframework.http.MediaType.TEXT_PLAIN 32 | import static org.springframework.http.MediaType.APPLICATION_JSON 33 | 34 | @WebAppConfiguration 35 | abstract class AbstractConfigurationIT extends Specification { 36 | 37 | static final JSON_UTF8 = 'application/json;charset=UTF-8' 38 | 39 | static { 40 | MockHttpServletResponse.metaClass.getContentAsJson = { 41 | new JsonSlurper().parseText(delegate.contentAsString) 42 | } 43 | MockHttpServletResponse.metaClass.getContentAsXml = { 44 | new XmlSlurper().parseText(delegate.contentAsString) 45 | } 46 | } 47 | 48 | @Autowired WebApplicationContext context 49 | 50 | MockMvc mockMvc 51 | MockHttpServletResponse response 52 | 53 | static GET = MockMvcRequestBuilders.&get 54 | static POST = MockMvcRequestBuilders.&post 55 | 56 | void setup() { 57 | mockMvc = MockMvcBuilders.webAppContextSetup(context).build() 58 | } 59 | 60 | 61 | def 'Perform request that results in success response'() { 62 | when: 63 | perform GET('/ping').with { 64 | accept TEXT_PLAIN 65 | } 66 | then: 'no error here' 67 | response.status == 200 68 | } 69 | 70 | def 'Perform request that causes built-in exception handled by default handler'() { 71 | 72 | when: 'require content type not supported by the resource' 73 | perform GET('/ping').with { 74 | accept APPLICATION_JSON 75 | } 76 | 77 | then: 'we got Not Acceptable error' 78 | response.status == 406 79 | 80 | and: 'Content-Type corresponds to the requested type' 81 | response.contentType == JSON_UTF8 82 | 83 | and: 'body contains expected message in correct type' 84 | with (response.contentAsJson) { 85 | type == 'http://httpstatus.es/406' 86 | title == 'This sucks!' 87 | status == 406 88 | detail == "This resource provides only text/plain, but you've sent request with Accept application/json." 89 | } 90 | } 91 | 92 | def 'Perform request that causes user-defined exception with custom exception handler'() { 93 | 94 | when: 'perform request on resource that throws ZuulException' 95 | perform GET('/dana').with { 96 | accept APPLICATION_JSON 97 | } 98 | 99 | then: 'we got error response with status specified in ZuulExceptionHandler' 100 | response.status == 404 101 | 102 | and: 'Content-Type corresponds to the requested type' 103 | response.contentType == JSON_UTF8 104 | 105 | and: 'body is generated by ZuulExceptionHandler in correct type' 106 | response.contentAsJson.title == "There's no Dana, only Zuul!" 107 | } 108 | 109 | def 'Perform request with Accept different from defaultContentType'() { 110 | 111 | when: 'perform request on resource that throws ZuulException and require non-default content type' 112 | perform GET('/dana').with { 113 | accept APPLICATION_XML 114 | } 115 | 116 | then: 'we got error response with status specified in ZuulExceptionHandler' 117 | response.status == 404 118 | 119 | and: 'Content-Type corresponds to the requested type' 120 | response.contentType == 'application/xml' 121 | 122 | and: 'body is generated by ZuulExceptionHandler in correct type' 123 | response.contentAsXml.title == "There's no Dana, only Zuul!" 124 | } 125 | 126 | @Issue('#2') 127 | def 'Perform request without Accept that causes handled exception'() { 128 | 129 | when: "perform request on resource that throws ZuulException and don't specify Accept" 130 | perform GET('/dana') 131 | 132 | then: 'we get error response with the status specified in ZuulExceptionHandler' 133 | response.status == 404 134 | 135 | and: 'Content-Type corresponds to the configured defaultContentType' 136 | response.contentType == JSON_UTF8 137 | response.contentAsJson.title == "There's no Dana, only Zuul!" 138 | } 139 | 140 | def 'Perform request with Accept that is not supported by resource and exception handler'() { 141 | when: 142 | perform GET('/ping').with { 143 | accept MediaType.valueOf('image/png') 144 | } 145 | then: 'we got Not Acceptable error' 146 | response.status == 406 147 | 148 | and: 'Content-Type corresponds to the configured default (fallback) type' 149 | response.contentType == JSON_UTF8 150 | } 151 | 152 | def 'Perform request that causes built-in exception handled by default handler remapped to different status'() { 153 | 154 | when: 'use method not supported by the resource' 155 | perform POST('/ping') 156 | 157 | then: 'we got 418 instead of 405 that is default for this error' 158 | response.status == 418 159 | and: 160 | response.contentType == JSON_UTF8 161 | response.contentAsJson.title == 'Method Not Allowed' 162 | } 163 | 164 | def 'Perform request that causes user-defined exception handled by @ExceptionHandler method'() { 165 | 166 | when: 'perform request on resource that throws exception' 167 | perform POST('/teapot').with { 168 | accept APPLICATION_JSON 169 | } 170 | 171 | then: 'we got error response with status specified in @ResponseStatus on @ExceptionHandler method' 172 | response.status == 418 173 | 174 | and: 'body is generated by @ExceptionHandler method' 175 | response.contentType == JSON_UTF8 176 | response.contentAsJson.title == 'Bazinga!' 177 | } 178 | 179 | 180 | def perform(builder) { 181 | response = mockMvc.perform(builder).andReturn().response 182 | } 183 | 184 | def parseJson(string) { 185 | new JsonSlurper().parseText(string) 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/it/groovy/cz/jirutka/spring/exhandler/JavaConfigurationIT.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.exhandler 17 | 18 | import cz.jirutka.spring.exhandler.fixtures.SampleController 19 | import cz.jirutka.spring.exhandler.fixtures.ZuulException 20 | import cz.jirutka.spring.exhandler.fixtures.ZuulExceptionHandler 21 | import cz.jirutka.spring.exhandler.support.HttpMessageConverterUtils 22 | import org.springframework.context.annotation.Bean 23 | import org.springframework.context.annotation.Configuration 24 | import org.springframework.context.support.ReloadableResourceBundleMessageSource 25 | import org.springframework.test.context.ContextConfiguration 26 | import org.springframework.web.HttpRequestMethodNotSupportedException 27 | import org.springframework.web.servlet.HandlerExceptionResolver 28 | import org.springframework.web.servlet.config.annotation.EnableWebMvc 29 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter 30 | import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver 31 | 32 | import static org.springframework.http.HttpStatus.I_AM_A_TEAPOT 33 | import static org.springframework.http.MediaType.APPLICATION_JSON 34 | 35 | @ContextConfiguration(classes=ContextConfig) 36 | class JavaConfigurationIT extends AbstractConfigurationIT { 37 | 38 | @EnableWebMvc 39 | @Configuration 40 | static class ContextConfig extends WebMvcConfigurerAdapter { 41 | 42 | void configureHandlerExceptionResolvers(List resolvers) { 43 | resolvers.add( exceptionHandlerExceptionResolver() ) 44 | resolvers.add( restExceptionResolver() ) 45 | } 46 | 47 | @Bean restExceptionResolver() { 48 | RestHandlerExceptionResolver.builder() 49 | .messageSource( httpErrorMessageSource() ) 50 | .defaultContentType(APPLICATION_JSON) 51 | .addErrorMessageHandler(HttpRequestMethodNotSupportedException, I_AM_A_TEAPOT) 52 | .addHandler(ZuulException, new ZuulExceptionHandler()) 53 | .build() 54 | } 55 | 56 | @Bean httpErrorMessageSource() { 57 | new ReloadableResourceBundleMessageSource ( 58 | basename: 'classpath:/testMessages', 59 | defaultEncoding: 'UTF-8' 60 | ) 61 | } 62 | 63 | @Bean exceptionHandlerExceptionResolver() { 64 | new ExceptionHandlerExceptionResolver ( 65 | messageConverters: HttpMessageConverterUtils.getDefaultHttpMessageConverters() 66 | ) 67 | } 68 | 69 | @Bean SampleController sampleController() { 70 | new SampleController() 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/it/groovy/cz/jirutka/spring/exhandler/XmlConfigurationIT.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.exhandler 17 | 18 | import org.springframework.test.context.ContextConfiguration 19 | 20 | @ContextConfiguration('/testContext.xml') 21 | class XmlConfigurationIT extends AbstractConfigurationIT {} 22 | -------------------------------------------------------------------------------- /src/it/groovy/cz/jirutka/spring/exhandler/fixtures/SampleController.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.exhandler.fixtures 17 | 18 | import org.springframework.stereotype.Controller 19 | import org.springframework.web.bind.annotation.ExceptionHandler 20 | import org.springframework.web.bind.annotation.RequestMapping 21 | import org.springframework.web.bind.annotation.ResponseBody 22 | import org.springframework.web.bind.annotation.ResponseStatus 23 | 24 | import static org.springframework.http.HttpStatus.I_AM_A_TEAPOT 25 | import static org.springframework.web.bind.annotation.RequestMethod.GET 26 | import static org.springframework.web.bind.annotation.RequestMethod.POST 27 | 28 | @Controller 29 | @RequestMapping('/') 30 | class SampleController { 31 | 32 | @ResponseBody 33 | @RequestMapping(value='/ping', method=GET, produces='text/plain') 34 | String getPing() { 35 | 'pong!' 36 | } 37 | 38 | @ResponseBody 39 | @RequestMapping(value='/dana', method=GET) 40 | String getDana() { 41 | throw new ZuulException() 42 | } 43 | 44 | @RequestMapping(value='/teapot', method=POST) 45 | void postFail() { 46 | throw new TeapotException() 47 | } 48 | 49 | 50 | @ResponseBody 51 | @ResponseStatus(I_AM_A_TEAPOT) 52 | @ExceptionHandler(TeapotException) 53 | TeapotMessage handleException() { 54 | new TeapotMessage(title: 'Bazinga!') 55 | } 56 | 57 | 58 | static class TeapotException extends RuntimeException { } 59 | 60 | static class TeapotMessage { 61 | String title 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/it/groovy/cz/jirutka/spring/exhandler/fixtures/ZuulException.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.exhandler.fixtures 17 | 18 | class ZuulException extends RuntimeException { 19 | } 20 | -------------------------------------------------------------------------------- /src/it/groovy/cz/jirutka/spring/exhandler/fixtures/ZuulExceptionHandler.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.exhandler.fixtures 17 | 18 | import cz.jirutka.spring.exhandler.handlers.RestExceptionHandler 19 | import cz.jirutka.spring.exhandler.messages.ErrorMessage 20 | import org.springframework.http.HttpStatus 21 | import org.springframework.http.ResponseEntity 22 | 23 | import javax.servlet.http.HttpServletRequest 24 | 25 | class ZuulExceptionHandler implements RestExceptionHandler { 26 | 27 | ResponseEntity handleException(ZuulException exception, HttpServletRequest request) { 28 | def body = new ErrorMessage(title: "There's no Dana, only Zuul!") 29 | new ResponseEntity<>(body, HttpStatus.NOT_FOUND) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/it/resources/testContext.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/it/resources/testMessages.properties: -------------------------------------------------------------------------------- 1 | org.springframework.web.HttpMediaTypeNotAcceptableException.title=This sucks! 2 | org.springframework.web.HttpMediaTypeNotAcceptableException.detail=This resource provides only #{ex.supportedMediaTypes}, but you've sent request with Accept #{req.getHeader('Accept')}. 3 | -------------------------------------------------------------------------------- /src/main/java/cz/jirutka/spring/exhandler/MapUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2016 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.exhandler; 17 | 18 | import java.util.Map; 19 | 20 | final class MapUtils { 21 | 22 | private MapUtils() {} 23 | 24 | /** 25 | * Puts entries from the {@code source} map into the {@code target} map, but without overriding 26 | * any existing entry in {@code target} map, i.e. put only if the key does not exist in the 27 | * {@code target} map. 28 | * 29 | * @param target The target map where to put new entries. 30 | * @param source The source map from which read the entries. 31 | */ 32 | static void putAllIfAbsent(Map target, Map source) { 33 | 34 | for (Map.Entry entry : source.entrySet()) { 35 | if (!target.containsKey(entry.getKey())) { 36 | target.put(entry.getKey(), entry.getValue()); 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/cz/jirutka/spring/exhandler/RestHandlerExceptionResolver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2015 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.exhandler; 17 | 18 | import cz.jirutka.spring.exhandler.handlers.RestExceptionHandler; 19 | import org.slf4j.Logger; 20 | import org.slf4j.LoggerFactory; 21 | import org.springframework.beans.factory.InitializingBean; 22 | import org.springframework.core.MethodParameter; 23 | import org.springframework.http.MediaType; 24 | import org.springframework.http.ResponseEntity; 25 | import org.springframework.http.converter.HttpMessageConverter; 26 | import org.springframework.util.Assert; 27 | import org.springframework.util.ClassUtils; 28 | import org.springframework.web.HttpMediaTypeNotAcceptableException; 29 | import org.springframework.web.accept.ContentNegotiationManager; 30 | import org.springframework.web.accept.FixedContentNegotiationStrategy; 31 | import org.springframework.web.accept.HeaderContentNegotiationStrategy; 32 | import org.springframework.web.context.request.NativeWebRequest; 33 | import org.springframework.web.context.request.ServletWebRequest; 34 | import org.springframework.web.method.support.HandlerMethodReturnValueHandler; 35 | import org.springframework.web.method.support.ModelAndViewContainer; 36 | import org.springframework.web.servlet.ModelAndView; 37 | import org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver; 38 | import org.springframework.web.servlet.mvc.method.annotation.HttpEntityMethodProcessor; 39 | 40 | import javax.servlet.http.HttpServletRequest; 41 | import javax.servlet.http.HttpServletResponse; 42 | import java.lang.reflect.Method; 43 | import java.util.LinkedHashMap; 44 | import java.util.List; 45 | import java.util.Map; 46 | 47 | import static cz.jirutka.spring.exhandler.support.HttpMessageConverterUtils.getDefaultHttpMessageConverters; 48 | import static org.springframework.http.MediaType.APPLICATION_XML; 49 | import static org.springframework.web.servlet.HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE; 50 | 51 | /** 52 | * A {@link org.springframework.web.servlet.HandlerExceptionResolver HandlerExceptionResolver} 53 | * for RESTful APIs that resolves exceptions through the provided {@link RestExceptionHandler 54 | * RestExceptionHandlers}. 55 | * 56 | * @see #builder() 57 | * @see RestHandlerExceptionResolverBuilder 58 | * @see RestHandlerExceptionResolverFactoryBean 59 | */ 60 | public class RestHandlerExceptionResolver extends AbstractHandlerExceptionResolver implements InitializingBean { 61 | 62 | private static final Logger LOG = LoggerFactory.getLogger(RestHandlerExceptionResolver.class); 63 | 64 | private final MethodParameter returnTypeMethodParam; 65 | 66 | private List> messageConverters = getDefaultHttpMessageConverters(); 67 | 68 | private Map, RestExceptionHandler> handlers = new LinkedHashMap<>(); 69 | 70 | private MediaType defaultContentType = APPLICATION_XML; 71 | 72 | private ContentNegotiationManager contentNegotiationManager; 73 | 74 | // package visibility for tests 75 | HandlerMethodReturnValueHandler responseProcessor; 76 | 77 | // package visibility for tests 78 | HandlerMethodReturnValueHandler fallbackResponseProcessor; 79 | 80 | 81 | /** 82 | * Returns a builder to build and configure instance of {@code RestHandlerExceptionResolver}. 83 | */ 84 | public static RestHandlerExceptionResolverBuilder builder() { 85 | return new RestHandlerExceptionResolverBuilder(); 86 | } 87 | 88 | 89 | public RestHandlerExceptionResolver() { 90 | 91 | Method method = ClassUtils.getMethod( 92 | RestExceptionHandler.class, "handleException", Exception.class, HttpServletRequest.class); 93 | 94 | returnTypeMethodParam = new MethodParameter(method, -1); 95 | // This method caches the resolved value, so it's convenient to initialize it 96 | // only once here. 97 | returnTypeMethodParam.getGenericParameterType(); 98 | } 99 | 100 | 101 | @Override 102 | public void afterPropertiesSet() { 103 | if (contentNegotiationManager == null) { 104 | contentNegotiationManager = new ContentNegotiationManager( 105 | new HeaderContentNegotiationStrategy(), new FixedContentNegotiationStrategy(defaultContentType)); 106 | } 107 | responseProcessor = new HttpEntityMethodProcessor(messageConverters, contentNegotiationManager); 108 | fallbackResponseProcessor = new HttpEntityMethodProcessor(messageConverters, 109 | new ContentNegotiationManager(new FixedContentNegotiationStrategy(defaultContentType))); 110 | } 111 | 112 | @Override 113 | protected ModelAndView doResolveException( 114 | HttpServletRequest request, HttpServletResponse response, Object handler, Exception exception) { 115 | 116 | ResponseEntity entity; 117 | try { 118 | entity = handleException(exception, request); 119 | } catch (NoExceptionHandlerFoundException ex) { 120 | LOG.warn("No exception handler found to handle exception: {}", exception.getClass().getName()); 121 | return null; 122 | } 123 | try { 124 | processResponse(entity, new ServletWebRequest(request, response)); 125 | } catch (Exception ex) { 126 | LOG.error("Failed to process error response: {}", entity, ex); 127 | return null; 128 | } 129 | return new ModelAndView(); 130 | } 131 | 132 | protected ResponseEntity handleException(Exception exception, HttpServletRequest request) { 133 | // See http://stackoverflow.com/a/12979543/2217862 134 | // This attribute is never set in MockMvc, so it's not covered in integration test. 135 | request.removeAttribute(PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE); 136 | 137 | RestExceptionHandler handler = resolveExceptionHandler(exception.getClass()); 138 | 139 | LOG.debug("Handling exception {} with response factory: {}", exception.getClass().getName(), handler); 140 | return handler.handleException(exception, request); 141 | } 142 | 143 | @SuppressWarnings("unchecked") 144 | protected RestExceptionHandler resolveExceptionHandler(Class exceptionClass) { 145 | 146 | for (Class clazz = exceptionClass; clazz != Throwable.class; clazz = clazz.getSuperclass()) { 147 | if (handlers.containsKey(clazz)) { 148 | return handlers.get(clazz); 149 | } 150 | } 151 | throw new NoExceptionHandlerFoundException(); 152 | } 153 | 154 | protected void processResponse(ResponseEntity entity, NativeWebRequest webRequest) throws Exception { 155 | 156 | // XXX: Create MethodParameter from the actually used subclass of RestExceptionHandler? 157 | MethodParameter methodParameter = new MethodParameter(returnTypeMethodParam); 158 | ModelAndViewContainer mavContainer = new ModelAndViewContainer(); 159 | 160 | try { 161 | responseProcessor.handleReturnValue(entity, methodParameter, mavContainer, webRequest); 162 | 163 | } catch (HttpMediaTypeNotAcceptableException ex) { 164 | LOG.debug("Requested media type is not supported, falling back to default one"); 165 | fallbackResponseProcessor.handleReturnValue(entity, methodParameter, mavContainer, webRequest); 166 | } 167 | } 168 | 169 | 170 | //////// Accessors //////// 171 | 172 | // Note: We're not using Lombok in this class to make it clear for debugging. 173 | 174 | public List> getMessageConverters() { 175 | return messageConverters; 176 | } 177 | 178 | public void setMessageConverters(List> messageConverters) { 179 | Assert.notNull(messageConverters, "messageConverters must not be null"); 180 | this.messageConverters = messageConverters; 181 | } 182 | 183 | public ContentNegotiationManager getContentNegotiationManager() { 184 | return this.contentNegotiationManager; 185 | } 186 | 187 | public void setContentNegotiationManager(ContentNegotiationManager contentNegotiationManager) { 188 | this.contentNegotiationManager = contentNegotiationManager != null 189 | ? contentNegotiationManager : new ContentNegotiationManager(); 190 | } 191 | 192 | public MediaType getDefaultContentType() { 193 | return defaultContentType; 194 | } 195 | 196 | public void setDefaultContentType(MediaType defaultContentType) { 197 | this.defaultContentType = defaultContentType; 198 | } 199 | 200 | public Map, RestExceptionHandler> getExceptionHandlers() { 201 | return handlers; 202 | } 203 | 204 | public void setExceptionHandlers(Map, RestExceptionHandler> handlers) { 205 | this.handlers = handlers; 206 | } 207 | 208 | 209 | //////// Inner classes //////// 210 | 211 | public static class NoExceptionHandlerFoundException extends RuntimeException {} 212 | } 213 | -------------------------------------------------------------------------------- /src/main/java/cz/jirutka/spring/exhandler/RestHandlerExceptionResolverBuilder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.exhandler; 17 | 18 | import cz.jirutka.spring.exhandler.handlers.*; 19 | import cz.jirutka.spring.exhandler.interpolators.MessageInterpolator; 20 | import cz.jirutka.spring.exhandler.interpolators.MessageInterpolatorAware; 21 | import lombok.Setter; 22 | import lombok.experimental.Accessors; 23 | import org.springframework.beans.ConversionNotSupportedException; 24 | import org.springframework.beans.TypeMismatchException; 25 | import org.springframework.context.HierarchicalMessageSource; 26 | import org.springframework.context.MessageSource; 27 | import org.springframework.context.MessageSourceAware; 28 | import org.springframework.context.support.ReloadableResourceBundleMessageSource; 29 | import org.springframework.http.HttpStatus; 30 | import org.springframework.http.MediaType; 31 | import org.springframework.http.converter.HttpMessageConverter; 32 | import org.springframework.http.converter.HttpMessageNotReadableException; 33 | import org.springframework.http.converter.HttpMessageNotWritableException; 34 | import org.springframework.util.ClassUtils; 35 | import org.springframework.web.HttpMediaTypeNotAcceptableException; 36 | import org.springframework.web.HttpMediaTypeNotSupportedException; 37 | import org.springframework.web.HttpRequestMethodNotSupportedException; 38 | import org.springframework.web.accept.ContentNegotiationManager; 39 | import org.springframework.web.bind.MethodArgumentNotValidException; 40 | import org.springframework.web.bind.MissingServletRequestParameterException; 41 | import org.springframework.web.bind.ServletRequestBindingException; 42 | import org.springframework.web.multipart.support.MissingServletRequestPartException; 43 | import org.springframework.web.servlet.mvc.multiaction.NoSuchRequestHandlingMethodException; 44 | 45 | import javax.validation.ConstraintViolationException; 46 | import java.util.HashMap; 47 | import java.util.List; 48 | import java.util.Map; 49 | 50 | import static cz.jirutka.spring.exhandler.MapUtils.putAllIfAbsent; 51 | import static lombok.AccessLevel.NONE; 52 | import static org.springframework.http.HttpStatus.*; 53 | import static org.springframework.util.StringUtils.hasText; 54 | 55 | @Setter 56 | @Accessors(fluent=true) 57 | @SuppressWarnings("unchecked") 58 | public class RestHandlerExceptionResolverBuilder { 59 | 60 | public static final String DEFAULT_MESSAGES_BASENAME = "classpath:/cz/jirutka/spring/exhandler/messages"; 61 | 62 | private final Map exceptionHandlers = new HashMap<>(); 63 | 64 | @Setter(NONE) // to not conflict with overloaded setter 65 | private MediaType defaultContentType; 66 | 67 | /** 68 | * The {@link ContentNegotiationManager} to use to resolve acceptable media types. 69 | * If not provided, the default instance of {@code ContentNegotiationManager} with 70 | * {@link org.springframework.web.accept.HeaderContentNegotiationStrategy HeaderContentNegotiationStrategy} 71 | * and {@link org.springframework.web.accept.FixedContentNegotiationStrategy FixedContentNegotiationStrategy} 72 | * (with {@link #defaultContentType(MediaType) defaultContentType}) will be used. 73 | */ 74 | private ContentNegotiationManager contentNegotiationManager; 75 | 76 | /** 77 | * The message body converters to use for converting an error message into HTTP response body. 78 | * If not provided, the default converters will be used (see 79 | * {@link cz.jirutka.spring.exhandler.support.HttpMessageConverterUtils#getDefaultHttpMessageConverters() 80 | * getDefaultHttpMessageConverters()}). 81 | */ 82 | private List> httpMessageConverters; 83 | 84 | /** 85 | * The message interpolator to set into all exception handlers implementing 86 | * {@link cz.jirutka.spring.exhandler.interpolators.MessageInterpolatorAware} 87 | * interface, e.g. {@link ErrorMessageRestExceptionHandler}. Built-in exception handlers uses 88 | * {@link cz.jirutka.spring.exhandler.interpolators.SpelMessageInterpolator 89 | * SpelMessageInterpolator} by default. 90 | */ 91 | private MessageInterpolator messageInterpolator; 92 | 93 | /** 94 | * The message source to set into all exception handlers implementing 95 | * {@link org.springframework.context.MessageSourceAware MessageSourceAware} interface, e.g. 96 | * {@link ErrorMessageRestExceptionHandler}. Required for built-in exception handlers. 97 | */ 98 | private MessageSource messageSource; 99 | 100 | /** 101 | * Whether to register default exception handlers for Spring exceptions. These are registered 102 | * before the provided exception handlers, so you can overwrite any of the default 103 | * mappings. Default is true. 104 | */ 105 | private boolean withDefaultHandlers = true; 106 | 107 | /** 108 | * Whether to use the default (built-in) message source as a fallback to resolve messages that 109 | * the provided message source can't resolve. In other words, it sets the default message 110 | * source as a parent of the provided message source. Default is true. 111 | */ 112 | private boolean withDefaultMessageSource = true; 113 | 114 | 115 | public RestHandlerExceptionResolver build() { 116 | 117 | if (withDefaultMessageSource) { 118 | if (messageSource != null) { 119 | // set default message source as top parent 120 | HierarchicalMessageSource messages = resolveRootMessageSource(messageSource); 121 | if (messages != null) { 122 | messages.setParentMessageSource(createDefaultMessageSource()); 123 | } 124 | } else { 125 | messageSource = createDefaultMessageSource(); 126 | } 127 | } 128 | 129 | if (withDefaultHandlers) { 130 | // add default handlers 131 | putAllIfAbsent(exceptionHandlers, getDefaultHandlers()); 132 | } 133 | 134 | // initialize handlers 135 | for (RestExceptionHandler handler : exceptionHandlers.values()) { 136 | if (messageSource != null && handler instanceof MessageSourceAware) { 137 | ((MessageSourceAware) handler).setMessageSource(messageSource); 138 | } 139 | if (messageInterpolator != null && handler instanceof MessageInterpolatorAware) { 140 | ((MessageInterpolatorAware) handler).setMessageInterpolator(messageInterpolator); 141 | } 142 | } 143 | 144 | RestHandlerExceptionResolver resolver = new RestHandlerExceptionResolver(); 145 | resolver.setExceptionHandlers((Map) exceptionHandlers); 146 | 147 | if (httpMessageConverters != null) { 148 | resolver.setMessageConverters(httpMessageConverters); 149 | } 150 | if (contentNegotiationManager != null) { 151 | resolver.setContentNegotiationManager(contentNegotiationManager); 152 | } 153 | if (defaultContentType != null) { 154 | resolver.setDefaultContentType(defaultContentType); 155 | } 156 | resolver.afterPropertiesSet(); 157 | 158 | return resolver; 159 | } 160 | 161 | /** 162 | * The default content type that will be used as a fallback when the requested content type is 163 | * not supported. 164 | */ 165 | public RestHandlerExceptionResolverBuilder defaultContentType(MediaType mediaType) { 166 | this.defaultContentType = mediaType; 167 | return this; 168 | } 169 | 170 | /** 171 | * The default content type that will be used as a fallback when the requested content type is 172 | * not supported. 173 | */ 174 | public RestHandlerExceptionResolverBuilder defaultContentType(String mediaType) { 175 | defaultContentType( hasText(mediaType) ? MediaType.parseMediaType(mediaType) : null ); 176 | return this; 177 | } 178 | 179 | /** 180 | * Registers the given exception handler for the specified exception type. This handler will be 181 | * also used for all the exception subtypes, when no more specific mapping is found. 182 | * 183 | * @param exceptionClass The exception type handled by the given handler. 184 | * @param exceptionHandler An instance of the exception handler for the specified exception 185 | * type or its subtypes. 186 | */ 187 | public RestHandlerExceptionResolverBuilder addHandler( 188 | Class exceptionClass, RestExceptionHandler exceptionHandler) { 189 | 190 | exceptionHandlers.put(exceptionClass, exceptionHandler); 191 | return this; 192 | } 193 | 194 | /** 195 | * Same as {@link #addHandler(Class, RestExceptionHandler)}, but the exception type is 196 | * determined from the handler. 197 | */ 198 | public 199 | RestHandlerExceptionResolverBuilder addHandler(AbstractRestExceptionHandler exceptionHandler) { 200 | 201 | return addHandler(exceptionHandler.getExceptionClass(), exceptionHandler); 202 | } 203 | 204 | /** 205 | * Registers {@link ErrorMessageRestExceptionHandler} for the specified exception type. 206 | * This handler will be also used for all the exception subtypes, when no more specific mapping 207 | * is found. 208 | * 209 | * @param exceptionClass The exception type to handle. 210 | * @param status The HTTP status to map the specified exception to. 211 | */ 212 | public RestHandlerExceptionResolverBuilder addErrorMessageHandler( 213 | Class exceptionClass, HttpStatus status) { 214 | 215 | return addHandler(new ErrorMessageRestExceptionHandler<>(exceptionClass, status)); 216 | } 217 | 218 | 219 | HierarchicalMessageSource resolveRootMessageSource(MessageSource messageSource) { 220 | 221 | if (messageSource instanceof HierarchicalMessageSource) { 222 | MessageSource parent = ((HierarchicalMessageSource) messageSource).getParentMessageSource(); 223 | 224 | return parent != null ? resolveRootMessageSource(parent) : (HierarchicalMessageSource) messageSource; 225 | 226 | } else { 227 | return null; 228 | } 229 | } 230 | 231 | private Map getDefaultHandlers() { 232 | 233 | Map map = new HashMap<>(); 234 | 235 | // this class does not exist in Spring 5 236 | if (ClassUtils.isPresent( 237 | "org.springframework.web.servlet.mvc.multiaction.NoSuchRequestHandlingMethodException", 238 | getClass().getClassLoader()) 239 | ) { 240 | map.put( NoSuchRequestHandlingMethodException.class, new NoSuchRequestHandlingMethodExceptionHandler() ); 241 | } 242 | map.put( HttpRequestMethodNotSupportedException.class, new HttpRequestMethodNotSupportedExceptionHandler() ); 243 | map.put( HttpMediaTypeNotSupportedException.class, new HttpMediaTypeNotSupportedExceptionHandler() ); 244 | map.put( MethodArgumentNotValidException.class, new MethodArgumentNotValidExceptionHandler() ); 245 | 246 | if (ClassUtils.isPresent("javax.validation.ConstraintViolationException", getClass().getClassLoader())) { 247 | map.put( ConstraintViolationException.class, new ConstraintViolationExceptionHandler() ); 248 | } 249 | 250 | addHandlerTo( map, HttpMediaTypeNotAcceptableException.class, NOT_ACCEPTABLE ); 251 | addHandlerTo( map, MissingServletRequestParameterException.class, BAD_REQUEST ); 252 | addHandlerTo( map, ServletRequestBindingException.class, BAD_REQUEST ); 253 | addHandlerTo( map, ConversionNotSupportedException.class, INTERNAL_SERVER_ERROR ); 254 | addHandlerTo( map, TypeMismatchException.class, BAD_REQUEST ); 255 | addHandlerTo( map, HttpMessageNotReadableException.class, UNPROCESSABLE_ENTITY ); 256 | addHandlerTo( map, HttpMessageNotWritableException.class, INTERNAL_SERVER_ERROR ); 257 | addHandlerTo( map, MissingServletRequestPartException.class, BAD_REQUEST ); 258 | addHandlerTo(map, Exception.class, INTERNAL_SERVER_ERROR); 259 | 260 | // this class didn't exist before Spring 4.0 261 | try { 262 | Class clazz = Class.forName("org.springframework.web.servlet.NoHandlerFoundException"); 263 | addHandlerTo(map, clazz, NOT_FOUND); 264 | } catch (ClassNotFoundException ex) { 265 | // ignore 266 | } 267 | return map; 268 | } 269 | 270 | private void addHandlerTo(Map map, Class exceptionClass, HttpStatus status) { 271 | map.put(exceptionClass, new ErrorMessageRestExceptionHandler(exceptionClass, status)); 272 | } 273 | 274 | private MessageSource createDefaultMessageSource() { 275 | 276 | ReloadableResourceBundleMessageSource messages = new ReloadableResourceBundleMessageSource(); 277 | messages.setBasename(DEFAULT_MESSAGES_BASENAME); 278 | messages.setDefaultEncoding("UTF-8"); 279 | messages.setFallbackToSystemLocale(false); 280 | 281 | return messages; 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /src/main/java/cz/jirutka/spring/exhandler/RestHandlerExceptionResolverFactoryBean.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.exhandler; 17 | 18 | import cz.jirutka.spring.exhandler.handlers.RestExceptionHandler; 19 | import cz.jirutka.spring.exhandler.interpolators.MessageInterpolator; 20 | import lombok.Setter; 21 | import org.springframework.beans.factory.FactoryBean; 22 | import org.springframework.context.MessageSource; 23 | import org.springframework.http.HttpStatus; 24 | import org.springframework.http.converter.HttpMessageConverter; 25 | import org.springframework.util.Assert; 26 | import org.springframework.web.accept.ContentNegotiationManager; 27 | 28 | import java.util.List; 29 | import java.util.Map; 30 | 31 | import static java.util.Collections.emptyMap; 32 | 33 | @Setter 34 | public class RestHandlerExceptionResolverFactoryBean implements FactoryBean { 35 | 36 | /** 37 | * The {@link ContentNegotiationManager} to use to resolve acceptable media types. 38 | * If not provided, the default instance of {@code ContentNegotiationManager} with 39 | * {@link org.springframework.web.accept.HeaderContentNegotiationStrategy HeaderContentNegotiationStrategy} 40 | * and {@link org.springframework.web.accept.FixedContentNegotiationStrategy FixedContentNegotiationStrategy} 41 | * (with {@link #setDefaultContentType(String) defaultContentType}) will be used. 42 | */ 43 | private ContentNegotiationManager contentNegotiationManager; 44 | 45 | /** 46 | * The default content type that will be used as a fallback when the requested content type is 47 | * not supported. 48 | */ 49 | private String defaultContentType; 50 | 51 | /** 52 | * Mapping of exception handlers where the key is an exception type to handle and the value is 53 | * either a HTTP status (this will register 54 | * {@link cz.jirutka.spring.exhandler.handlers.ErrorMessageRestExceptionHandler 55 | * ErrorMessageRestExceptionHandler}) and/or an instance of the {@link RestExceptionHandler}. 56 | * 57 | *

Each handler is also used for all the exception subtypes, when no more specific mapping 58 | * is found.

59 | * 60 | *

Example: 61 | *

{@code
 62 |      * 
 63 |      *     
 64 |      *         
 65 |      *         
 66 |      *             
 67 |      *         
 68 |      *     
 69 |      * 
 70 |      * }
71 | */ 72 | private Map, ?> exceptionHandlers = emptyMap(); 73 | 74 | /** 75 | * The message body converters to use for converting an error message into HTTP response body. 76 | * If not provided, the default converters will be used (see 77 | * {@link cz.jirutka.spring.exhandler.support.HttpMessageConverterUtils#getDefaultHttpMessageConverters() 78 | * getDefaultHttpMessageConverters()}). 79 | */ 80 | private List> httpMessageConverters; 81 | 82 | /** 83 | * The message interpolator to set into all exception handlers implementing 84 | * {@link cz.jirutka.spring.exhandler.interpolators.MessageInterpolatorAware} 85 | * interface, e.g. {@link cz.jirutka.spring.exhandler.handlers.ErrorMessageRestExceptionHandler}. 86 | * Built-in exception handlers uses {@link cz.jirutka.spring.exhandler.interpolators.SpelMessageInterpolator 87 | * SpelMessageInterpolator} by default. 88 | */ 89 | private MessageInterpolator messageInterpolator; 90 | 91 | /** 92 | * The message source to set into all exception handlers implementing 93 | * {@link org.springframework.context.MessageSourceAware MessageSourceAware} interface, e.g. 94 | * {@link cz.jirutka.spring.exhandler.handlers.ErrorMessageRestExceptionHandler}. 95 | * Required for built-in exception handlers. 96 | */ 97 | private MessageSource messageSource; 98 | 99 | /** 100 | * Whether to register default exception handlers for Spring exceptions. These are registered 101 | * before the provided exception handlers, so you can overwrite any of the default 102 | * mappings. Default is true. 103 | */ 104 | private boolean withDefaultHandlers = true; 105 | 106 | /** 107 | * Whether to use the default (built-in) message source as a fallback to resolve messages that 108 | * the provided message source can't resolve. In other words, it sets the default message 109 | * source as a parent of the provided message source. Default is true. 110 | */ 111 | private boolean withDefaultMessageSource = true; 112 | 113 | 114 | @SuppressWarnings("unchecked") 115 | public RestHandlerExceptionResolver getObject() { 116 | 117 | RestHandlerExceptionResolverBuilder builder = createBuilder() 118 | .messageSource(messageSource) 119 | .messageInterpolator(messageInterpolator) 120 | .httpMessageConverters(httpMessageConverters) 121 | .contentNegotiationManager(contentNegotiationManager) 122 | .defaultContentType(defaultContentType) 123 | .withDefaultHandlers(withDefaultHandlers) 124 | .withDefaultMessageSource(withDefaultMessageSource); 125 | 126 | for (Map.Entry, ?> entry : exceptionHandlers.entrySet()) { 127 | Class exceptionClass = entry.getKey(); 128 | Object value = entry.getValue(); 129 | 130 | if (value instanceof RestExceptionHandler) { 131 | builder.addHandler(exceptionClass, (RestExceptionHandler) value); 132 | 133 | } else { 134 | builder.addErrorMessageHandler(exceptionClass, parseHttpStatus(value)); 135 | } 136 | } 137 | 138 | return builder.build(); 139 | } 140 | 141 | public Class getObjectType() { 142 | return RestHandlerExceptionResolver.class; 143 | } 144 | 145 | public boolean isSingleton() { 146 | return false; 147 | } 148 | 149 | 150 | RestHandlerExceptionResolverBuilder createBuilder() { 151 | return RestHandlerExceptionResolver.builder(); 152 | } 153 | 154 | HttpStatus parseHttpStatus(Object value) { 155 | Assert.notNull(value, "Values of the exceptionHandlers map must not be null"); 156 | 157 | if (value instanceof HttpStatus) { 158 | return (HttpStatus) value; 159 | 160 | } else if (value instanceof Integer) { 161 | return HttpStatus.valueOf((int) value); 162 | 163 | } else if (value instanceof String) { 164 | return HttpStatus.valueOf(Integer.valueOf((String) value)); 165 | 166 | } else { 167 | throw new IllegalArgumentException(String.format( 168 | "Values of the exceptionHandlers maps must be instance of ErrorResponseFactory, HttpStatus, " + 169 | "String, or int, but %s given", value.getClass())); 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/main/java/cz/jirutka/spring/exhandler/handlers/AbstractRestExceptionHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.exhandler.handlers; 17 | 18 | import org.slf4j.Logger; 19 | import org.slf4j.LoggerFactory; 20 | import org.slf4j.Marker; 21 | import org.slf4j.MarkerFactory; 22 | import org.springframework.http.HttpHeaders; 23 | import org.springframework.http.HttpStatus; 24 | import org.springframework.http.ResponseEntity; 25 | 26 | import javax.servlet.http.HttpServletRequest; 27 | 28 | import static org.springframework.core.GenericTypeResolver.resolveTypeArguments; 29 | 30 | /** 31 | * The base implementation of the {@link RestExceptionHandler} interface. 32 | */ 33 | public abstract class AbstractRestExceptionHandler implements RestExceptionHandler { 34 | 35 | private static final Logger LOG = LoggerFactory.getLogger(RestExceptionHandler.class); 36 | 37 | private final Class exceptionClass; 38 | private final HttpStatus status; 39 | 40 | 41 | /** 42 | * This constructor determines the exception class from the generic class parameter {@code E}. 43 | * 44 | * @param status HTTP status 45 | */ 46 | protected AbstractRestExceptionHandler(HttpStatus status) { 47 | this.exceptionClass = determineTargetType(); 48 | this.status = status; 49 | LOG.trace("Determined generic exception type: {}", exceptionClass.getName()); 50 | } 51 | 52 | protected AbstractRestExceptionHandler(Class exceptionClass, HttpStatus status) { 53 | this.exceptionClass = exceptionClass; 54 | this.status = status; 55 | } 56 | 57 | 58 | ////// Abstract methods ////// 59 | 60 | public abstract T createBody(E ex, HttpServletRequest req); 61 | 62 | 63 | ////// Template methods ////// 64 | 65 | public ResponseEntity handleException(E ex, HttpServletRequest req) { 66 | 67 | logException(ex, req); 68 | 69 | T body = createBody(ex, req); 70 | HttpHeaders headers = createHeaders(ex, req); 71 | 72 | return new ResponseEntity<>(body, headers, getStatus()); 73 | } 74 | 75 | public Class getExceptionClass() { 76 | return exceptionClass; 77 | } 78 | 79 | public HttpStatus getStatus() { 80 | return status; 81 | } 82 | 83 | 84 | protected HttpHeaders createHeaders(E ex, HttpServletRequest req) { 85 | return new HttpHeaders(); 86 | } 87 | 88 | /** 89 | * Logs the exception; on ERROR level when status is 5xx, otherwise on INFO level without stack 90 | * trace, or DEBUG level with stack trace. The logger name is 91 | * {@code cz.jirutka.spring.exhandler.handlers.RestExceptionHandler}. 92 | * 93 | * @param ex The exception to log. 94 | * @param req The current web request. 95 | */ 96 | protected void logException(E ex, HttpServletRequest req) { 97 | 98 | if (LOG.isErrorEnabled() && getStatus().value() >= 500 || LOG.isInfoEnabled()) { 99 | Marker marker = MarkerFactory.getMarker(ex.getClass().getName()); 100 | 101 | String uri = req.getRequestURI(); 102 | if (req.getQueryString() != null) { 103 | uri += '?' + req.getQueryString(); 104 | } 105 | String msg = String.format("%s %s ~> %s", req.getMethod(), uri, getStatus()); 106 | 107 | if (getStatus().value() >= 500) { 108 | LOG.error(marker, msg, ex); 109 | 110 | } else if (LOG.isDebugEnabled()) { 111 | LOG.debug(marker, msg, ex); 112 | 113 | } else { 114 | LOG.info(marker, msg); 115 | } 116 | } 117 | } 118 | 119 | @SuppressWarnings("unchecked") 120 | private Class determineTargetType() { 121 | return (Class) resolveTypeArguments(getClass(), AbstractRestExceptionHandler.class)[0]; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/main/java/cz/jirutka/spring/exhandler/handlers/ConstraintViolationExceptionHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.exhandler.handlers; 17 | 18 | import cz.jirutka.spring.exhandler.messages.ErrorMessage; 19 | import cz.jirutka.spring.exhandler.messages.ValidationErrorMessage; 20 | import org.springframework.beans.factory.annotation.Autowired; 21 | import org.springframework.core.convert.ConversionException; 22 | import org.springframework.core.convert.ConversionService; 23 | import org.springframework.core.convert.support.DefaultConversionService; 24 | import org.springframework.util.Assert; 25 | 26 | import javax.servlet.http.HttpServletRequest; 27 | import javax.validation.ConstraintViolation; 28 | import javax.validation.ConstraintViolationException; 29 | import javax.validation.ElementKind; 30 | import javax.validation.Path; 31 | import javax.validation.Path.Node; 32 | import java.util.ArrayList; 33 | import java.util.Collections; 34 | import java.util.Iterator; 35 | import java.util.List; 36 | 37 | import static org.springframework.http.HttpStatus.UNPROCESSABLE_ENTITY; 38 | import static org.springframework.util.StringUtils.isEmpty; 39 | 40 | public class ConstraintViolationExceptionHandler extends ErrorMessageRestExceptionHandler { 41 | 42 | private ConversionService conversionService = new DefaultConversionService(); 43 | 44 | 45 | public ConstraintViolationExceptionHandler() { 46 | super(UNPROCESSABLE_ENTITY); 47 | } 48 | 49 | @Override 50 | public ValidationErrorMessage createBody(ConstraintViolationException ex, HttpServletRequest req) { 51 | 52 | ErrorMessage tmpl = super.createBody(ex, req); 53 | ValidationErrorMessage msg = new ValidationErrorMessage(tmpl); 54 | 55 | for (ConstraintViolation violation : ex.getConstraintViolations()) { 56 | Node pathNode = findLastNonEmptyPathNode(violation.getPropertyPath()); 57 | 58 | // path is probably useful only for properties (fields) 59 | if (pathNode != null && pathNode.getKind() == ElementKind.PROPERTY) { 60 | msg.addError(pathNode.getName(), convertToString(violation.getInvalidValue()), violation.getMessage()); 61 | 62 | // type level constraints etc. 63 | } else { 64 | msg.addError(violation.getMessage()); 65 | } 66 | } 67 | return msg; 68 | } 69 | 70 | /** 71 | * Conversion service used for converting an invalid value to String. 72 | * When no service provided, the {@link DefaultConversionService} is used. 73 | * 74 | * @param conversionService must not be null. 75 | */ 76 | @Autowired(required=false) 77 | public void setConversionService(ConversionService conversionService) { 78 | Assert.notNull(conversionService, "conversionService must not be null"); 79 | this.conversionService = conversionService; 80 | } 81 | 82 | 83 | private Node findLastNonEmptyPathNode(Path path) { 84 | 85 | List list = new ArrayList<>(); 86 | for (Iterator it = path.iterator(); it.hasNext(); ) { 87 | list.add(it.next()); 88 | } 89 | Collections.reverse(list); 90 | for (Node node : list) { 91 | if (!isEmpty(node.getName())) { 92 | return node; 93 | } 94 | } 95 | return null; 96 | } 97 | 98 | private String convertToString(Object invalidValue) { 99 | 100 | if (invalidValue == null) { 101 | return null; 102 | } 103 | try { 104 | return conversionService.convert(invalidValue, String.class); 105 | 106 | } catch (ConversionException ex) { 107 | return invalidValue.toString(); 108 | } 109 | } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /src/main/java/cz/jirutka/spring/exhandler/handlers/ErrorMessageRestExceptionHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2016 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.exhandler.handlers; 17 | 18 | import cz.jirutka.spring.exhandler.interpolators.MessageInterpolator; 19 | import cz.jirutka.spring.exhandler.interpolators.MessageInterpolatorAware; 20 | import cz.jirutka.spring.exhandler.interpolators.NoOpMessageInterpolator; 21 | import cz.jirutka.spring.exhandler.interpolators.SpelMessageInterpolator; 22 | import cz.jirutka.spring.exhandler.messages.ErrorMessage; 23 | import org.slf4j.Logger; 24 | import org.slf4j.LoggerFactory; 25 | import org.springframework.context.MessageSource; 26 | import org.springframework.context.MessageSourceAware; 27 | import org.springframework.context.i18n.LocaleContextHolder; 28 | import org.springframework.http.HttpStatus; 29 | import org.springframework.util.Assert; 30 | 31 | import javax.servlet.http.HttpServletRequest; 32 | import java.net.URI; 33 | import java.util.HashMap; 34 | import java.util.Locale; 35 | import java.util.Map; 36 | 37 | /** 38 | * {@link RestExceptionHandler} that produces {@link ErrorMessage}. 39 | * 40 | * @param Type of the handled exception. 41 | */ 42 | public class ErrorMessageRestExceptionHandler 43 | extends AbstractRestExceptionHandler implements MessageSourceAware, MessageInterpolatorAware { 44 | 45 | private static final Logger LOG = LoggerFactory.getLogger(ErrorMessageRestExceptionHandler.class); 46 | 47 | protected static final String 48 | DEFAULT_PREFIX = "default", 49 | TYPE_KEY = "type", 50 | TITLE_KEY = "title", 51 | DETAIL_KEY = "detail", 52 | INSTANCE_KEY = "instance"; 53 | 54 | private MessageSource messageSource; 55 | 56 | private MessageInterpolator interpolator = new SpelMessageInterpolator(); 57 | 58 | 59 | /** 60 | * @param exceptionClass Type of the handled exceptions; it's used as a prefix of key to 61 | * resolve messages (via MessageSource). 62 | * @param status HTTP status that will be sent to client. 63 | */ 64 | public ErrorMessageRestExceptionHandler(Class exceptionClass, HttpStatus status) { 65 | super(exceptionClass, status); 66 | } 67 | 68 | /** 69 | * @see AbstractRestExceptionHandler#AbstractRestExceptionHandler(HttpStatus) AbstractRestExceptionHandler 70 | */ 71 | protected ErrorMessageRestExceptionHandler(HttpStatus status) { 72 | super(status); 73 | } 74 | 75 | 76 | public ErrorMessage createBody(E ex, HttpServletRequest req) { 77 | 78 | ErrorMessage m = new ErrorMessage(); 79 | m.setType(URI.create(resolveMessage(TYPE_KEY, ex, req))); 80 | m.setTitle(resolveMessage(TITLE_KEY, ex, req)); 81 | m.setStatus(getStatus()); 82 | m.setDetail(resolveMessage(DETAIL_KEY, ex, req)); 83 | m.setInstance(URI.create(resolveMessage(INSTANCE_KEY, ex, req))); 84 | 85 | return m; 86 | } 87 | 88 | 89 | protected String resolveMessage(String key, E exception, HttpServletRequest request) { 90 | 91 | String template = getMessage(key, LocaleContextHolder.getLocale()); 92 | 93 | Map vars = new HashMap<>(2); 94 | vars.put("ex", exception); 95 | vars.put("req", request); 96 | 97 | return interpolateMessage(template, vars); 98 | } 99 | 100 | protected String interpolateMessage(String messageTemplate, Map variables) { 101 | 102 | LOG.trace("Interpolating message '{}' with variables: {}", messageTemplate, variables); 103 | return interpolator.interpolate(messageTemplate, variables); 104 | } 105 | 106 | protected String getMessage(String key, Locale locale) { 107 | 108 | String prefix = getExceptionClass().getName(); 109 | 110 | String message = messageSource.getMessage(prefix + "." + key, null, null, locale); 111 | if (message == null) { 112 | message = messageSource.getMessage(DEFAULT_PREFIX + "." + key, null, null, locale); 113 | } 114 | if (message == null) { 115 | message = ""; 116 | LOG.debug("No message found for {}.{}, nor {}.{}", prefix, key, DEFAULT_PREFIX, key); 117 | } 118 | return message; 119 | } 120 | 121 | 122 | ////// Accessors ////// 123 | 124 | public void setMessageSource(MessageSource messageSource) { 125 | Assert.notNull(messageSource, "messageSource must not be null"); 126 | this.messageSource = messageSource; 127 | } 128 | 129 | public void setMessageInterpolator(MessageInterpolator interpolator) { 130 | this.interpolator = interpolator != null ? interpolator : new NoOpMessageInterpolator(); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/main/java/cz/jirutka/spring/exhandler/handlers/HttpMediaTypeNotSupportedExceptionHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.exhandler.handlers; 17 | 18 | import org.springframework.http.HttpHeaders; 19 | import org.springframework.http.MediaType; 20 | import org.springframework.web.HttpMediaTypeNotSupportedException; 21 | 22 | import javax.servlet.http.HttpServletRequest; 23 | import java.util.List; 24 | 25 | import static org.springframework.http.HttpStatus.UNSUPPORTED_MEDIA_TYPE; 26 | import static org.springframework.util.CollectionUtils.isEmpty; 27 | 28 | public class HttpMediaTypeNotSupportedExceptionHandler extends ErrorMessageRestExceptionHandler { 29 | 30 | 31 | public HttpMediaTypeNotSupportedExceptionHandler() { 32 | super(UNSUPPORTED_MEDIA_TYPE); 33 | } 34 | 35 | @Override 36 | protected HttpHeaders createHeaders(HttpMediaTypeNotSupportedException ex, HttpServletRequest req) { 37 | 38 | HttpHeaders headers = super.createHeaders(ex, req); 39 | List mediaTypes = ex.getSupportedMediaTypes(); 40 | 41 | if (!isEmpty(mediaTypes)) { 42 | headers.setAccept(mediaTypes); 43 | } 44 | return headers; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/cz/jirutka/spring/exhandler/handlers/HttpRequestMethodNotSupportedExceptionHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.exhandler.handlers; 17 | 18 | import cz.jirutka.spring.exhandler.messages.ErrorMessage; 19 | import org.slf4j.Logger; 20 | import org.slf4j.LoggerFactory; 21 | import org.springframework.http.HttpHeaders; 22 | import org.springframework.http.ResponseEntity; 23 | import org.springframework.web.HttpRequestMethodNotSupportedException; 24 | import org.springframework.web.servlet.DispatcherServlet; 25 | 26 | import javax.servlet.http.HttpServletRequest; 27 | 28 | import static org.springframework.http.HttpStatus.METHOD_NOT_ALLOWED; 29 | import static org.springframework.util.ObjectUtils.isEmpty; 30 | 31 | public class HttpRequestMethodNotSupportedExceptionHandler 32 | extends ErrorMessageRestExceptionHandler { 33 | 34 | private static final Logger LOG = LoggerFactory.getLogger(DispatcherServlet.PAGE_NOT_FOUND_LOG_CATEGORY); 35 | 36 | 37 | public HttpRequestMethodNotSupportedExceptionHandler() { 38 | super(METHOD_NOT_ALLOWED); 39 | } 40 | 41 | @Override 42 | public ResponseEntity handleException(HttpRequestMethodNotSupportedException ex, HttpServletRequest req) { 43 | LOG.warn(ex.getMessage()); 44 | 45 | return super.handleException(ex, req); 46 | } 47 | 48 | @Override 49 | protected HttpHeaders createHeaders(HttpRequestMethodNotSupportedException ex, HttpServletRequest req) { 50 | 51 | HttpHeaders headers = super.createHeaders(ex, req); 52 | 53 | if (!isEmpty(ex.getSupportedMethods())) { 54 | headers.setAllow(ex.getSupportedHttpMethods()); 55 | } 56 | return headers; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/cz/jirutka/spring/exhandler/handlers/MethodArgumentNotValidExceptionHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.exhandler.handlers; 17 | 18 | import cz.jirutka.spring.exhandler.messages.ErrorMessage; 19 | import cz.jirutka.spring.exhandler.messages.ValidationErrorMessage; 20 | import org.springframework.validation.BindingResult; 21 | import org.springframework.validation.FieldError; 22 | import org.springframework.validation.ObjectError; 23 | import org.springframework.web.bind.MethodArgumentNotValidException; 24 | 25 | import javax.servlet.http.HttpServletRequest; 26 | 27 | import static org.springframework.http.HttpStatus.UNPROCESSABLE_ENTITY; 28 | 29 | public class MethodArgumentNotValidExceptionHandler extends ErrorMessageRestExceptionHandler { 30 | 31 | 32 | public MethodArgumentNotValidExceptionHandler() { 33 | super(UNPROCESSABLE_ENTITY); 34 | } 35 | 36 | @Override 37 | public ValidationErrorMessage createBody(MethodArgumentNotValidException ex, HttpServletRequest req) { 38 | 39 | ErrorMessage tmpl = super.createBody(ex, req); 40 | ValidationErrorMessage msg = new ValidationErrorMessage(tmpl); 41 | 42 | BindingResult result = ex.getBindingResult(); 43 | 44 | for (ObjectError err : result.getGlobalErrors()) { 45 | msg.addError(err.getDefaultMessage()); 46 | } 47 | for (FieldError err : result.getFieldErrors()) { 48 | msg.addError(err.getField(), err.getRejectedValue(), err.getDefaultMessage()); 49 | } 50 | return msg; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/cz/jirutka/spring/exhandler/handlers/NoSuchRequestHandlingMethodExceptionHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package cz.jirutka.spring.exhandler.handlers; 18 | 19 | import cz.jirutka.spring.exhandler.messages.ErrorMessage; 20 | import org.slf4j.Logger; 21 | import org.slf4j.LoggerFactory; 22 | import org.springframework.http.ResponseEntity; 23 | import org.springframework.web.servlet.DispatcherServlet; 24 | import org.springframework.web.servlet.mvc.multiaction.NoSuchRequestHandlingMethodException; 25 | 26 | import javax.servlet.http.HttpServletRequest; 27 | 28 | import static org.springframework.http.HttpStatus.NOT_FOUND; 29 | 30 | public class NoSuchRequestHandlingMethodExceptionHandler 31 | extends ErrorMessageRestExceptionHandler { 32 | 33 | private static final Logger LOG = LoggerFactory.getLogger(DispatcherServlet.PAGE_NOT_FOUND_LOG_CATEGORY); 34 | 35 | 36 | public NoSuchRequestHandlingMethodExceptionHandler() { 37 | super(NOT_FOUND); 38 | } 39 | 40 | @Override 41 | public ResponseEntity handleException(NoSuchRequestHandlingMethodException ex, HttpServletRequest req) { 42 | 43 | LOG.warn(ex.getMessage()); 44 | return super.handleException(ex, req); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/cz/jirutka/spring/exhandler/handlers/ResponseStatusRestExceptionHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.exhandler.handlers; 17 | 18 | import org.springframework.http.HttpStatus; 19 | import org.springframework.http.ResponseEntity; 20 | 21 | import javax.servlet.http.HttpServletRequest; 22 | 23 | /** 24 | * Simple {@link RestExceptionHandler} that just returns response with the specified status code 25 | * and no content. 26 | */ 27 | public class ResponseStatusRestExceptionHandler implements RestExceptionHandler { 28 | 29 | private final HttpStatus status; 30 | 31 | 32 | public ResponseStatusRestExceptionHandler(HttpStatus status) { 33 | this.status = status; 34 | } 35 | 36 | public ResponseEntity handleException(Exception ex, HttpServletRequest request) { 37 | return new ResponseEntity<>(status); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/cz/jirutka/spring/exhandler/handlers/RestExceptionHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.exhandler.handlers; 17 | 18 | import org.springframework.http.ResponseEntity; 19 | 20 | import javax.servlet.http.HttpServletRequest; 21 | 22 | /** 23 | * Contract for classes generating a {@link ResponseEntity} for an instance of the specified 24 | * Exception type, used in {@link cz.jirutka.spring.exhandler.RestHandlerExceptionResolver 25 | * RestHandlerExceptionResolver}. 26 | * 27 | * @param Type of the handled exception. 28 | * @param Type of the response message (entity body). 29 | */ 30 | public interface RestExceptionHandler { 31 | 32 | /** 33 | * Handles exception and generates {@link ResponseEntity}. 34 | * 35 | * @param exception The exception to handle and get data from. 36 | * @param request The current request. 37 | * @return A response entity. 38 | */ 39 | ResponseEntity handleException(E exception, HttpServletRequest request); 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/cz/jirutka/spring/exhandler/interpolators/MessageInterpolator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.exhandler.interpolators; 17 | 18 | import java.util.Map; 19 | 20 | public interface MessageInterpolator { 21 | 22 | /** 23 | * Interpolates the message template using the given variables. 24 | * 25 | * @param messageTemplate The message to interpolate. 26 | * @param variables Map of variables that will be accessible for the template. 27 | * @return An interpolated message. 28 | */ 29 | String interpolate(String messageTemplate, Map variables); 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/cz/jirutka/spring/exhandler/interpolators/MessageInterpolatorAware.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.exhandler.interpolators; 17 | 18 | import org.springframework.beans.factory.Aware; 19 | 20 | /** 21 | * Interface to be implemented by any object that wishes to be notified 22 | * of the {@link MessageInterpolator} to use. 23 | */ 24 | public interface MessageInterpolatorAware extends Aware { 25 | 26 | void setMessageInterpolator(MessageInterpolator messageInterpolator); 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/cz/jirutka/spring/exhandler/interpolators/NoOpMessageInterpolator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.exhandler.interpolators; 17 | 18 | import java.util.Map; 19 | 20 | /** 21 | * Implementation of the {@link MessageInterpolator} that does nothing, just returns the given 22 | * message template as-is. 23 | */ 24 | public class NoOpMessageInterpolator implements MessageInterpolator { 25 | 26 | public String interpolate(String messageTemplate, Map variables) { 27 | return messageTemplate; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/cz/jirutka/spring/exhandler/interpolators/SpelMessageInterpolator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.exhandler.interpolators; 17 | 18 | import org.slf4j.Logger; 19 | import org.slf4j.LoggerFactory; 20 | import org.springframework.context.expression.MapAccessor; 21 | import org.springframework.expression.EvaluationContext; 22 | import org.springframework.expression.Expression; 23 | import org.springframework.expression.ExpressionException; 24 | import org.springframework.expression.ExpressionParser; 25 | import org.springframework.expression.common.TemplateParserContext; 26 | import org.springframework.expression.spel.standard.SpelExpressionParser; 27 | import org.springframework.expression.spel.support.StandardEvaluationContext; 28 | import org.springframework.util.Assert; 29 | 30 | import java.util.Map; 31 | 32 | /** 33 | * Implementation of the {@link MessageInterpolator} that uses the Spring Expression Language 34 | * (SpEL) to evaluate expressions inside a template message. 35 | * 36 | *

SpEL expressions are delimited by {@code #{} and {@code }}. The provided variables are 37 | * accessible directly by name.

38 | */ 39 | public class SpelMessageInterpolator implements MessageInterpolator { 40 | 41 | private static final Logger LOG = LoggerFactory.getLogger(SpelMessageInterpolator.class); 42 | 43 | private final EvaluationContext evalContext; 44 | 45 | 46 | /** 47 | * Creates a new instance with a custom {@link EvaluationContext}. 48 | */ 49 | public SpelMessageInterpolator(EvaluationContext evalContext) { 50 | Assert.notNull(evalContext, "EvaluationContext must not be null"); 51 | this.evalContext = evalContext; 52 | } 53 | 54 | /** 55 | * Creates a new instance with {@link StandardEvaluationContext} including 56 | * {@link org.springframework.expression.spel.support.ReflectivePropertyAccessor ReflectivePropertyAccessor} 57 | * and {@link MapAccessor}. 58 | */ 59 | public SpelMessageInterpolator() { 60 | StandardEvaluationContext ctx = new StandardEvaluationContext(); 61 | ctx.addPropertyAccessor(new MapAccessor()); 62 | this.evalContext = ctx; 63 | } 64 | 65 | 66 | public String interpolate(String messageTemplate, Map variables) { 67 | Assert.notNull(messageTemplate, "messageTemplate must not be null"); 68 | 69 | try { 70 | Expression expression = parser().parseExpression(messageTemplate, new TemplateParserContext()); 71 | 72 | return expression.getValue(evalContext, variables, String.class); 73 | 74 | } catch (ExpressionException ex) { 75 | LOG.error("Failed to interpolate message template: {}", messageTemplate, ex); 76 | return ""; 77 | } 78 | } 79 | 80 | ExpressionParser parser() { 81 | return new SpelExpressionParser(); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/java/cz/jirutka/spring/exhandler/messages/ErrorMessage.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2015 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.exhandler.messages; 17 | 18 | import com.fasterxml.jackson.annotation.JsonIgnore; 19 | import com.fasterxml.jackson.annotation.JsonInclude; 20 | import com.fasterxml.jackson.annotation.JsonInclude.Include; 21 | import com.fasterxml.jackson.annotation.JsonProperty; 22 | import lombok.Data; 23 | import lombok.NoArgsConstructor; 24 | import lombok.ToString; 25 | import org.codehaus.jackson.map.annotate.JsonSerialize; 26 | import org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion; 27 | import org.springframework.http.HttpStatus; 28 | 29 | import javax.xml.bind.annotation.XmlRootElement; 30 | import java.io.Serializable; 31 | import java.net.URI; 32 | 33 | /** 34 | * @see draft-nottingham-http-problem-06 35 | */ 36 | @Data 37 | @NoArgsConstructor 38 | @ToString(exclude="detail") 39 | @JsonInclude(Include.NON_EMPTY) //for Jackson 2.x 40 | @JsonSerialize(include=Inclusion.NON_EMPTY) //for Jackson 1.x 41 | @XmlRootElement(name="problem") //for JAXB 42 | public class ErrorMessage implements Serializable { 43 | 44 | private static final long serialVersionUID = 1L; 45 | 46 | /** 47 | * An absolute URI that identifies the problem type. When dereferenced, it 48 | * SHOULD provide human-readable documentation for the problem type (e.g., 49 | * using HTML). When this member is not present, its value is assumed to 50 | * be "about:blank". 51 | */ 52 | private URI type; 53 | 54 | /** 55 | * A short, human-readable summary of the problem type. It SHOULD NOT 56 | * change from occurrence to occurrence of the problem, except for purposes 57 | * of localization. 58 | */ 59 | private String title; 60 | 61 | /** 62 | * The HTTP status code generated by the origin server for this occurrence 63 | * of the problem. 64 | */ 65 | private Integer status; 66 | 67 | /** 68 | * An human readable explanation specific to this occurrence of the 69 | * problem. 70 | */ 71 | private String detail; 72 | 73 | /** 74 | * An absolute URI that identifies the specific occurrence of the problem. 75 | * It may or may not yield further information if dereferenced. 76 | */ 77 | private URI instance; 78 | 79 | 80 | public ErrorMessage(ErrorMessage orig) { 81 | this.type = orig.getType(); 82 | this.title = orig.getTitle(); 83 | this.status = orig.getStatus(); 84 | this.detail = orig.getDetail(); 85 | this.instance = orig.getInstance(); 86 | } 87 | 88 | 89 | @JsonProperty 90 | public void setStatus(Integer status) { 91 | this.status = status; 92 | } 93 | 94 | @JsonIgnore 95 | public void setStatus(HttpStatus status) { 96 | this.status = status.value(); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/main/java/cz/jirutka/spring/exhandler/messages/ValidationErrorMessage.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2016 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.exhandler.messages; 17 | 18 | import com.fasterxml.jackson.annotation.JsonInclude; 19 | import lombok.Data; 20 | import lombok.EqualsAndHashCode; 21 | import lombok.NoArgsConstructor; 22 | import lombok.ToString; 23 | import org.codehaus.jackson.map.annotate.JsonSerialize; 24 | import org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion; 25 | 26 | import javax.xml.bind.annotation.XmlRootElement; 27 | import java.util.ArrayList; 28 | import java.util.List; 29 | 30 | import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_EMPTY; 31 | 32 | @Data 33 | @NoArgsConstructor 34 | @ToString(callSuper=true) 35 | @EqualsAndHashCode(callSuper=true) 36 | @JsonInclude(NON_EMPTY) //for Jackson 2.x 37 | @JsonSerialize(include=Inclusion.NON_EMPTY) //for Jackson 1.x 38 | @XmlRootElement(name="problem") //for JAXB 39 | public class ValidationErrorMessage extends ErrorMessage { 40 | 41 | private static final long serialVersionUID = 1L; 42 | 43 | private List errors = new ArrayList<>(6); 44 | 45 | 46 | public ValidationErrorMessage(ErrorMessage orig) { 47 | super(orig); 48 | } 49 | 50 | public ValidationErrorMessage addError(String field, Object rejectedValue, String message) { 51 | errors.add(new Error(field, rejectedValue, message)); 52 | return this; 53 | } 54 | 55 | public ValidationErrorMessage addError(String message) { 56 | errors.add(new Error(null, null, message)); 57 | return this; 58 | } 59 | 60 | 61 | @Data 62 | @JsonInclude(NON_EMPTY) 63 | public static class Error { 64 | private final String field; 65 | private final Object rejected; 66 | private final String message; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/cz/jirutka/spring/exhandler/support/HttpMessageConverterUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2016 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.exhandler.support; 17 | 18 | import org.springframework.http.converter.ByteArrayHttpMessageConverter; 19 | import org.springframework.http.converter.HttpMessageConverter; 20 | import org.springframework.http.converter.ResourceHttpMessageConverter; 21 | import org.springframework.http.converter.StringHttpMessageConverter; 22 | import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; 23 | import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter; 24 | import org.springframework.util.ClassUtils; 25 | 26 | import java.nio.charset.Charset; 27 | import java.util.ArrayList; 28 | import java.util.List; 29 | 30 | public final class HttpMessageConverterUtils { 31 | 32 | private static final ClassLoader CLASSLOADER = HttpMessageConverterUtils.class.getClassLoader(); 33 | 34 | private HttpMessageConverterUtils() {} 35 | 36 | /** 37 | * Determine whether a JAXB binder is present on the classpath and can be loaded. Will return 38 | * false if either the {@link javax.xml.bind.Binder} or one of its dependencies is not 39 | * present or cannot be loaded. 40 | */ 41 | public static boolean isJaxb2Present() { 42 | return ClassUtils.isPresent("javax.xml.bind.Binder", CLASSLOADER); 43 | } 44 | 45 | /** 46 | * Determine whether Jackson 2.x is present on the classpath and can be loaded. Will return 47 | * false if either the {@code com.fasterxml.jackson.databind.ObjectMapper}, 48 | * {@code com.fasterxml.jackson.core.JsonGenerator} or one of its dependencies is not present 49 | * or cannot be loaded. 50 | */ 51 | public static boolean isJackson2Present() { 52 | return ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", CLASSLOADER) && 53 | ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", CLASSLOADER); 54 | } 55 | 56 | /** 57 | * Determine whether Jackson 1.x is present on the classpath and can be loaded. Will return 58 | * false if either the {@code org.codehaus.jackson.map.ObjectMapper}, 59 | * {@code org.codehaus.jackson.JsonGenerator} or one of its dependencies is not present or 60 | * cannot be loaded. 61 | */ 62 | @Deprecated 63 | public static boolean isJacksonPresent() { 64 | return ClassUtils.isPresent("org.codehaus.jackson.map.ObjectMapper", CLASSLOADER) && 65 | ClassUtils.isPresent("org.codehaus.jackson.JsonGenerator", CLASSLOADER); 66 | } 67 | 68 | /** 69 | * Returns default {@link HttpMessageConverter} instances, i.e.: 70 | * 71 | *
    72 | *
  • {@linkplain ByteArrayHttpMessageConverter}
  • 73 | *
  • {@linkplain StringHttpMessageConverter}
  • 74 | *
  • {@linkplain ResourceHttpMessageConverter}
  • 75 | *
  • {@linkplain Jaxb2RootElementHttpMessageConverter} (when JAXB is present)
  • 76 | *
  • {@linkplain MappingJackson2HttpMessageConverter} (when Jackson 2.x is present)
  • 77 | *
  • {@linkplain org.springframework.http.converter.json.MappingJacksonHttpMessageConverter} 78 | * (when Jackson 1.x is present and 2.x not)
  • 79 | *
80 | * 81 | *

Note: It does not return all of the default converters defined in Spring, but just thus 82 | * usable for exception responses.

83 | */ 84 | @SuppressWarnings("deprecation") 85 | public static List> getDefaultHttpMessageConverters() { 86 | 87 | List> converters = new ArrayList<>(); 88 | 89 | StringHttpMessageConverter stringConverter = new StringHttpMessageConverter(Charset.forName("UTF-8")); 90 | stringConverter.setWriteAcceptCharset(false); // See SPR-7316 91 | 92 | converters.add(new ByteArrayHttpMessageConverter()); 93 | converters.add(stringConverter); 94 | converters.add(new ResourceHttpMessageConverter()); 95 | 96 | if (isJaxb2Present()) { 97 | converters.add(new Jaxb2RootElementHttpMessageConverter()); 98 | } 99 | if (isJackson2Present()) { 100 | converters.add(new MappingJackson2HttpMessageConverter()); 101 | 102 | } else if (isJacksonPresent()) { 103 | try { 104 | Class clazz = Class.forName("org.springframework.http.converter.json.MappingJacksonHttpMessageConverter"); 105 | converters.add((HttpMessageConverter) clazz.newInstance()); 106 | 107 | } catch (ClassNotFoundException ex) { 108 | // Ignore it, this class is not available since Spring 4.1.0. 109 | } catch (InstantiationException | IllegalAccessException ex) { 110 | throw new IllegalStateException(ex); 111 | } 112 | } 113 | return converters; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/main/resources/cz/jirutka/spring/exhandler/messages.properties: -------------------------------------------------------------------------------- 1 | org.springframework.web.bind.MissingServletRequestParameterException.title=Missing Query Parameter 2 | org.springframework.web.bind.MissingServletRequestParameterException.detail=This resource requires the query parameter #{ex.parameterName} to be set. 3 | 4 | org.springframework.web.multipart.support.MissingServletRequestPartException.title=Multipart Malformed 5 | 6 | org.springframework.web.servlet.NoHandlerFoundException.type=http://httpstatus.es/404 7 | org.springframework.web.servlet.NoHandlerFoundException.title=Not Found 8 | org.springframework.web.servlet.NoHandlerFoundException.detail=Unable to determine a corresponding handler for your request. 9 | 10 | org.springframework.web.HttpRequestMethodNotSupportedException.type=http://httpstatus.es/405 11 | org.springframework.web.HttpRequestMethodNotSupportedException.title=Method Not Allowed 12 | org.springframework.web.HttpRequestMethodNotSupportedException.detail=This resource supports only #{ex.supportedMethods}, but you've sent #{ex.method}. 13 | 14 | org.springframework.web.HttpMediaTypeNotAcceptableException.type=http://httpstatus.es/406 15 | org.springframework.web.HttpMediaTypeNotAcceptableException.title=Not Acceptable 16 | org.springframework.web.HttpMediaTypeNotAcceptableException.detail=This resource provides only #{ex.supportedMediaTypes}, but you've sent request with Accept #{req.getHeader('Accept')}. 17 | 18 | org.springframework.web.HttpMediaTypeNotSupportedException.type=http://httpstatus.es/415 19 | org.springframework.web.HttpMediaTypeNotSupportedException.title=Unsupported Media Type 20 | org.springframework.web.HttpMediaTypeNotSupportedException.detail=This resource supports only #{ex.supportedMediaTypes}, but you've sent request with Content-Type #{ex.contentType}. 21 | 22 | org.springframework.web.bind.MethodArgumentNotValidException.title=Validation Failed 23 | org.springframework.web.bind.MethodArgumentNotValidException.detail=The content you've sent contains #{ex.bindingResult.errorCount} validation errors. 24 | 25 | javax.validation.ConstraintViolationException.title=Validation Failed 26 | javax.validation.ConstraintViolationException.detail=The content you've sent contains #{ex.constraintViolations.size()} validation errors. 27 | 28 | org.springframework.http.converter.HttpMessageNotReadableException.title=Conversion Failed 29 | org.springframework.http.converter.HttpMessageNotReadableException.detail=The content you've sent is probably malformed. 30 | 31 | # TODO 32 | #org.springframework.web.bind.ServletRequestBindingException 33 | #org.springframework.beans.TypeMismatchException 34 | #org.springframework.beans.ConversionNotSupportedException 35 | #org.springframework.http.converter.HttpMessageNotWritableException 36 | -------------------------------------------------------------------------------- /src/test/groovy/cz/jirutka/spring/exhandler/RestHandlerExceptionResolverBuilderTest.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.exhandler 17 | 18 | import spock.lang.Specification 19 | 20 | // TODO 21 | class RestHandlerExceptionResolverBuilderTest extends Specification { 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/test/groovy/cz/jirutka/spring/exhandler/RestHandlerExceptionResolverFactoryBeanTest.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.exhandler 17 | 18 | import cz.jirutka.spring.exhandler.handlers.ErrorMessageRestExceptionHandler 19 | import cz.jirutka.spring.exhandler.interpolators.MessageInterpolator 20 | import org.springframework.beans.factory.NoSuchBeanDefinitionException 21 | import org.springframework.context.MessageSource 22 | import org.springframework.web.accept.ContentNegotiationManager 23 | import spock.lang.Specification 24 | import spock.lang.Unroll 25 | 26 | import static org.springframework.http.HttpStatus.* 27 | 28 | class RestHandlerExceptionResolverFactoryBeanTest extends Specification { 29 | 30 | def factory = new RestHandlerExceptionResolverFactoryBean() 31 | 32 | def builder = Spy(RestHandlerExceptionResolverBuilder) 33 | def hackedFactory = new RestHandlerExceptionResolverFactoryBean() { 34 | def RestHandlerExceptionResolverBuilder createBuilder() { builder } 35 | } 36 | 37 | 38 | def 'ensure that factory produces RestHandlerExceptionResolver'() { 39 | expect: 40 | factory.objectType == RestHandlerExceptionResolver 41 | factory.getObject() instanceof RestHandlerExceptionResolver 42 | } 43 | 44 | @Unroll 45 | def 'parse HttpStatus from type: #type'() { 46 | expect: 47 | factory.parseHttpStatus(value) == expected 48 | where: 49 | value | expected 50 | BAD_REQUEST | BAD_REQUEST 51 | 404 | NOT_FOUND 52 | '409' | CONFLICT 53 | 54 | type = value.getClass().getSimpleName() 55 | } 56 | 57 | @Unroll 58 | def 'fail to parse HttpStatus when given: #value'() { 59 | when: 60 | factory.parseHttpStatus(value) 61 | then: 62 | thrown IllegalArgumentException 63 | where: 64 | value << [null, '999', 3.14] 65 | } 66 | 67 | def 'process exception handlers map and add handlers to builder'() { 68 | setup: 69 | def malformedUrlHandler = new ErrorMessageRestExceptionHandler(MalformedURLException, UNPROCESSABLE_ENTITY) 70 | and: 71 | hackedFactory.exceptionHandlers = [ 72 | (NoSuchBeanDefinitionException): '500', 73 | (MalformedURLException): malformedUrlHandler 74 | ] 75 | when: 76 | hackedFactory.getObject() 77 | then: 78 | 1 * builder.addErrorMessageHandler(NoSuchBeanDefinitionException, INTERNAL_SERVER_ERROR) 79 | 1 * builder.addHandler(MalformedURLException, malformedUrlHandler) 80 | } 81 | 82 | def 'set properties on builder'() { 83 | setup: 84 | hackedFactory.with { 85 | messageSource = Stub(MessageSource) 86 | messageInterpolator = Stub(MessageInterpolator) 87 | contentNegotiationManager = Stub(ContentNegotiationManager) 88 | defaultContentType = 'application/json' 89 | withDefaultHandlers = false 90 | withDefaultMessageSource = true 91 | } 92 | when: 93 | hackedFactory.getObject() 94 | then: 95 | 1 * builder.messageSource(_ as MessageSource) 96 | 1 * builder.messageInterpolator(_ as MessageInterpolator) 97 | 1 * builder.contentNegotiationManager(_ as ContentNegotiationManager) 98 | 1 * builder.defaultContentType(_ as String) 99 | 1 * builder.withDefaultHandlers(false) 100 | 1 * builder.withDefaultMessageSource(true) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/test/groovy/cz/jirutka/spring/exhandler/RestHandlerExceptionResolverTest.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.exhandler 17 | 18 | import cz.jirutka.spring.exhandler.handlers.RestExceptionHandler 19 | import org.springframework.http.ResponseEntity 20 | import org.springframework.mock.web.MockHttpServletRequest 21 | import org.springframework.mock.web.MockHttpServletResponse 22 | import org.springframework.web.HttpMediaTypeNotAcceptableException 23 | import org.springframework.web.bind.ServletRequestBindingException 24 | import org.springframework.web.method.support.HandlerMethodReturnValueHandler 25 | import org.springframework.web.method.support.ModelAndViewContainer 26 | import org.springframework.web.servlet.mvc.method.annotation.HttpEntityMethodProcessor 27 | import spock.lang.Specification 28 | 29 | import javax.servlet.http.HttpServletRequest 30 | import java.security.InvalidParameterException 31 | 32 | import static org.springframework.http.HttpStatus.BAD_REQUEST 33 | import static org.springframework.http.MediaType.APPLICATION_JSON 34 | import static org.springframework.web.servlet.HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE 35 | 36 | class RestHandlerExceptionResolverTest extends Specification { 37 | 38 | def resolver = new RestHandlerExceptionResolver() 39 | def request = new MockHttpServletRequest() 40 | def response = new MockHttpServletResponse() 41 | def respEntity = new ResponseEntity(BAD_REQUEST) 42 | 43 | def responseProc = Mock(HandlerMethodReturnValueHandler) 44 | def fallbackResponseProc = Mock(HandlerMethodReturnValueHandler) 45 | def responseFactory = Mock(RestExceptionHandler) 46 | 47 | void setup() { 48 | resolver.responseProcessor = responseProc 49 | resolver.fallbackResponseProcessor = fallbackResponseProc 50 | resolver.exceptionHandlers[Exception] = responseFactory 51 | } 52 | 53 | 54 | def 'initialize responseProcessor and fallbackResponseProcessor after properties set'() { 55 | setup: 56 | def newResolver = new RestHandlerExceptionResolver() 57 | when: 58 | newResolver.afterPropertiesSet() 59 | then: 60 | newResolver.responseProcessor instanceof HttpEntityMethodProcessor 61 | newResolver.fallbackResponseProcessor instanceof HttpEntityMethodProcessor 62 | } 63 | 64 | def 'resolve exception and process error response'() { 65 | setup: 66 | def exception = new ServletRequestBindingException('') 67 | when: 68 | resolver.doResolveException(request, response, null, exception) 69 | then: 70 | 1 * responseFactory.handleException(exception, request) >> respEntity 71 | 1 * responseProc.handleReturnValue(respEntity, _, _ as ModelAndViewContainer, { req -> 72 | req.request == request && req.response == response 73 | }) 74 | } 75 | 76 | def 'resolve exception handler when multiple are available'() { 77 | setup: 78 | def factories = new RestExceptionHandler[3].collect{ Mock(RestExceptionHandler) } 79 | and: 80 | resolver.exceptionHandlers = [ 81 | (NumberFormatException): factories[2], 82 | (IllegalArgumentException): factories[1], 83 | (Exception): factories[0] 84 | ] 85 | when: 86 | resolver.doResolveException(request, response, null, exception) 87 | then: 88 | 1 * factories[factoryNum].handleException(exception, _ as HttpServletRequest) >> respEntity 89 | where: 90 | exception | factoryNum 91 | new NumberFormatException() | 2 92 | new InvalidParameterException() | 1 93 | new FileNotFoundException() | 0 94 | } 95 | 96 | def 'fallback to default media type when requested media type is not supported'() { 97 | setup: 98 | resolver.defaultContentType = APPLICATION_JSON 99 | and: 100 | responseFactory.handleException(*_) >> respEntity 101 | responseProc.handleReturnValue(*_) >> { throw new HttpMediaTypeNotAcceptableException([]) } 102 | when: 103 | resolver.doResolveException(request, response, null, new Exception()) 104 | then: 105 | 1 * fallbackResponseProc.handleReturnValue(respEntity, _, _ as ModelAndViewContainer, { req -> 106 | req.request == request && req.response == response 107 | }) 108 | } 109 | 110 | def 'return null when no exception handler is found'() { 111 | setup: 112 | resolver.exceptionHandlers = [:] 113 | expect: 114 | resolver.doResolveException(request, response, null, new IOException()) == null 115 | } 116 | 117 | def 'return null when response processor throws an exception'() { 118 | setup: 119 | responseProc.handleReturnValue(*_) >> { throw new IllegalStateException() } 120 | expect: 121 | resolver.doResolveException(request, response, null, new IOException()) == null 122 | } 123 | 124 | def 'remove PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE from the request'() { 125 | setup: 126 | resolver.exceptionHandlers[Exception] = responseFactory 127 | request.setAttribute(PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE, ['image/png']) 128 | when: 129 | resolver.doResolveException(request, response, null, new Exception()) 130 | then: 131 | ! request.getAttribute(PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE) 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/test/groovy/cz/jirutka/spring/exhandler/handlers/AbstractRestExceptionHandlerTest.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.exhandler.handlers 17 | 18 | import ch.qos.logback.classic.Logger 19 | import ch.qos.logback.classic.spi.LoggingEvent 20 | import ch.qos.logback.core.Appender 21 | import cz.jirutka.spring.exhandler.messages.ErrorMessage 22 | import org.slf4j.LoggerFactory 23 | import org.springframework.http.HttpHeaders 24 | import org.springframework.http.HttpStatus 25 | import org.springframework.http.ResponseEntity 26 | import org.springframework.mock.web.MockHttpServletRequest 27 | import spock.lang.Specification 28 | import spock.lang.Unroll 29 | 30 | import javax.servlet.http.HttpServletRequest 31 | 32 | import static ch.qos.logback.classic.Level.* 33 | import static org.springframework.http.HttpStatus.BAD_REQUEST 34 | 35 | class AbstractRestExceptionHandlerTest extends Specification { 36 | 37 | def 'determine exception class from generic type'() { 38 | given: 39 | def factory = new AbstractRestExceptionHandler(BAD_REQUEST) { 40 | ErrorMessage createBody(IOException ex, HttpServletRequest req) { null} 41 | } 42 | expect: 43 | factory.exceptionClass == IOException 44 | } 45 | 46 | def 'handle exception using createHeaders, createBody and getStatus methods'() { 47 | setup: 48 | def ex = new IOException() 49 | def req = new MockHttpServletRequest() 50 | def expected = new ResponseEntity(new ErrorMessage(), new HttpHeaders(date: 123), BAD_REQUEST) 51 | and: 52 | def factory = Spy(AbstractRestExceptionHandler, constructorArgs: [IOException, expected.statusCode]) { 53 | createHeaders(ex, req) >> expected.headers 54 | createBody(ex, req) >> expected.body 55 | } 56 | when: 57 | def actual = factory.handleException(ex, req) 58 | then: 59 | actual == expected 60 | } 61 | 62 | @Unroll 63 | def 'log exception with status #status on level #expectedLevel #stackTrace'() { 64 | setup: 65 | def factory = new AbstractRestExceptionHandler(HttpStatus.valueOf(status)) { 66 | ErrorMessage createBody(Exception ex, HttpServletRequest req) { null } 67 | } 68 | def exception = new IOException() 69 | def logAppender = Mock(Appender) 70 | LoggingEvent actual = null 71 | and: 72 | (LoggerFactory.getLogger(RestExceptionHandler) as Logger).with { 73 | level = loggerLevel 74 | addAppender(logAppender) 75 | } 76 | when: 77 | factory.handleException(exception, new MockHttpServletRequest()) 78 | then: 79 | 1 * logAppender.doAppend({ actual = it }) 80 | actual.level == expectedLevel 81 | actual.marker.name == exception.class.name 82 | actual.throwableProxy == null ^ hasThrowable 83 | where: 84 | status | loggerLevel | expectedLevel | hasThrowable 85 | 503 | INFO | ERROR | true 86 | 404 | INFO | INFO | false 87 | 404 | TRACE | DEBUG | true 88 | 89 | stackTrace = "${hasThrowable ? 'with' : 'without'} stack trace" 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/test/groovy/cz/jirutka/spring/exhandler/handlers/ConstraintViolationExceptionHandlerTest.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2016 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.exhandler.handlers 17 | 18 | import cz.jirutka.spring.exhandler.messages.ValidationErrorMessage 19 | import org.hibernate.validator.internal.engine.ConstraintViolationImpl 20 | import org.hibernate.validator.internal.engine.path.PathImpl 21 | import org.springframework.core.convert.ConversionService 22 | import org.springframework.core.convert.ConverterNotFoundException 23 | import org.springframework.mock.web.MockHttpServletRequest 24 | import spock.lang.Specification 25 | 26 | import javax.validation.ConstraintViolation 27 | import javax.validation.ConstraintViolationException 28 | 29 | import static cz.jirutka.spring.exhandler.handlers.ConstraintViolationExceptionHandlerTest.PathBuilder.propertyPath 30 | import static java.lang.annotation.ElementType.FIELD 31 | import static java.lang.annotation.ElementType.TYPE 32 | import static org.hibernate.validator.internal.engine.path.NodeImpl.* 33 | 34 | class ConstraintViolationExceptionHandlerTest extends Specification { 35 | 36 | def handler = Spy(ConstraintViolationExceptionHandler) { 37 | resolveMessage(*_) >> '' 38 | } 39 | def conversionService = Mock(ConversionService) 40 | def request = new MockHttpServletRequest() 41 | 42 | def service = new DummyService() 43 | def bean = new DummyBean() 44 | 45 | 46 | def 'create Error for violations on fields of simple type'() { 47 | setup: 48 | def errorMessage = new ValidationErrorMessage() 49 | .addError('number', '42', 'less than 10') 50 | .addError('text', null, 'not empty') 51 | and: 52 | def violation1 = ConstraintViolationImpl.forBeanValidation( 53 | 'less than {value}', [:], 'less than 10', bean.class, 54 | bean, bean, 42, propertyPath('number'), null, FIELD) 55 | 56 | def violation2 = ConstraintViolationImpl.forBeanValidation( 57 | 'not empty', [:], 'not empty', bean.class, bean, bean, 58 | null, propertyPath('text'), null, FIELD) 59 | expect: 60 | assertError([violation1, violation2], errorMessage) 61 | } 62 | 63 | def 'create Error for violation on field of simple types collection'() { 64 | setup: 65 | def errorMessage = new ValidationErrorMessage() 66 | .addError('list', 'foo', 'message') 67 | and: 68 | def path = new PathBuilder().addPropertyNode('list', 2).addBeanNode().build() 69 | def violation = ConstraintViolationImpl.forBeanValidation( 70 | 'message', [:], 'message', bean.class, bean, bean, ['foo'], path, null, FIELD) 71 | expect: 72 | assertError violation, errorMessage 73 | } 74 | 75 | def 'create Error for violation on type level'() { 76 | setup: 77 | def errorMessage = new ValidationErrorMessage() 78 | .addError('bean invalid') 79 | and: 80 | def path = new PathBuilder().build() 81 | def violation = ConstraintViolationImpl.forBeanValidation( 82 | 'bean invalid', [:], 'bean invalid', bean.class, bean, bean, bean, path, null, TYPE) 83 | expect: 84 | assertError violation, errorMessage 85 | } 86 | 87 | def 'create Error for method violation on object with field of simple type'() { 88 | setup: 89 | def errorMessage = new ValidationErrorMessage() 90 | .addError('text', null, 'not null') 91 | and: 92 | def path = new PathBuilder() 93 | .addMethodNode('myMethod', [bean.class]) 94 | .addParameterNode('arg0', 0) 95 | .addPropertyNode('text') 96 | .build() 97 | def violation = ConstraintViolationImpl.forBeanValidation( 98 | 'not null', [:], 'not null', service.class, service, bean, null, path, null, FIELD) 99 | expect: 100 | assertError violation, errorMessage 101 | } 102 | 103 | 104 | def 'convert invalid value using given Conversion Service'() { 105 | setup: 106 | handler.conversionService = conversionService 107 | and: 108 | def invalidValue = [42] 109 | def exception = buildSimpleViolationException(invalidValue) 110 | when: 111 | def body = handler.createBody(exception, request) as ValidationErrorMessage 112 | then: 113 | 1 * conversionService.convert(invalidValue, String) >> '42' 114 | and: 115 | body.errors[0].rejected == '42' 116 | } 117 | 118 | def 'convert invalid value using its toString() when Conversion Service throws exception'() { 119 | setup: 120 | handler.conversionService = conversionService 121 | and: 122 | def invalidValue = new DummyBean() 123 | def exception = buildSimpleViolationException(invalidValue) 124 | when: 125 | def body = handler.createBody(exception, request) as ValidationErrorMessage 126 | then: 127 | 1 * conversionService.convert(invalidValue, String) >> { 128 | throw new ConverterNotFoundException(null, null) 129 | } 130 | and: 131 | body.errors[0].rejected == 'dummy' 132 | } 133 | 134 | 135 | 136 | void assertError(ConstraintViolation violation, ValidationErrorMessage expected) { 137 | assertError([violation], expected) 138 | } 139 | 140 | void assertError(Collection violations, ValidationErrorMessage expected) { 141 | def exception = new ConstraintViolationException(violations as Set) 142 | def actual = handler.createBody(exception, request) as ValidationErrorMessage 143 | 144 | assert actual.errors as Set == expected.errors as Set 145 | } 146 | 147 | def buildSimpleViolationException(invalidValue) { 148 | def violation = ConstraintViolationImpl.forBeanValidation( 149 | 'foo', [:], 'bar', bean.class, bean, bean, invalidValue, propertyPath('text'), null, FIELD) 150 | 151 | new ConstraintViolationException([violation] as Set) 152 | } 153 | 154 | 155 | // Fields and methods in these dummy classes aren't actually used, it's 156 | // just for a reference. 157 | @SuppressWarnings('GroovyUnusedDeclaration') 158 | static class DummyService { 159 | def myMethod(DummyBean arg0) { 160 | } 161 | } 162 | @SuppressWarnings('GroovyUnusedDeclaration') 163 | static class DummyBean { 164 | int number = 42 165 | String text = null 166 | List list = [] 167 | 168 | String toString() { 'dummy' } 169 | } 170 | 171 | static class PathBuilder { 172 | private nodes = [ createBeanNode(null) ] 173 | 174 | static propertyPath(String name) { 175 | new PathBuilder().addPropertyNode(name).build() 176 | } 177 | 178 | def addMethodNode(String name, List types) { 179 | nodes << createMethodNode(name, nodes.last(), types as Class[]) 180 | this 181 | } 182 | 183 | def addParameterNode(String name, int index) { 184 | nodes << createParameterNode(name, nodes.last(), index) 185 | this 186 | } 187 | 188 | def addPropertyNode(String name, int index = -1) { 189 | def node = createPropertyNode(name, nodes.last()) 190 | if (index >= 0) node = setIndex(node, index) 191 | nodes << node 192 | this 193 | } 194 | 195 | def addBeanNode() { 196 | nodes << createBeanNode(nodes.last()) 197 | this 198 | } 199 | 200 | def build() { 201 | // Don't care that the constructor is private, I'm not gonna 202 | // commit a harakiri just to create fixtures I need in a "clean way"... 203 | new PathImpl(nodes) 204 | } 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/test/groovy/cz/jirutka/spring/exhandler/handlers/ErrorMessageRestExceptionHandlerTest.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.exhandler.handlers 17 | 18 | import cz.jirutka.spring.exhandler.interpolators.MessageInterpolator 19 | import cz.jirutka.spring.exhandler.messages.ErrorMessage 20 | import org.springframework.beans.TypeMismatchException 21 | import org.springframework.context.MessageSource 22 | import org.springframework.context.i18n.LocaleContextHolder 23 | import org.springframework.mock.web.MockHttpServletRequest 24 | import spock.lang.Shared 25 | import spock.lang.Specification 26 | 27 | import static ErrorMessageRestExceptionHandler.DEFAULT_PREFIX 28 | import static java.util.Locale.ENGLISH 29 | import static java.util.Locale.JAPANESE 30 | import static org.springframework.http.HttpStatus.BAD_REQUEST 31 | 32 | class ErrorMessageRestExceptionHandlerTest extends Specification { 33 | 34 | @Shared exceptionClass = TypeMismatchException 35 | 36 | def messageSource = Mock(MessageSource) 37 | def interpolator = Mock(MessageInterpolator) 38 | def request = new MockHttpServletRequest() 39 | 40 | def handler = Spy(ErrorMessageRestExceptionHandler, constructorArgs: [exceptionClass, BAD_REQUEST]) 41 | 42 | void setup() { 43 | handler.messageSource = messageSource 44 | handler.messageInterpolator = interpolator 45 | } 46 | 47 | 48 | def 'createBody: create ErrorMessage using resolveMessage'() { 49 | setup: 50 | def exception = new TypeMismatchException(1, String) 51 | def expected = new ErrorMessage( 52 | type: new URI('http://httpstatus.es/400'), 53 | title: 'Type Mismatch', 54 | status: 400, 55 | detail: "You're screwed!", 56 | instance: new URI('http://example.org/type-mismatch')) 57 | when: 58 | def actual = handler.createBody(exception, request) 59 | then: 60 | ['type', 'title', 'detail', 'instance'].each { key -> 61 | 1 * handler.resolveMessage(key, exception, request) >> expected.properties[key].toString() 62 | } 63 | and: 64 | actual == expected 65 | } 66 | 67 | def 'resolveMessage: obtain message using getMessage() and interpolate it'() { 68 | setup: 69 | def ex = new TypeMismatchException(1, String) 70 | def msgTemplate = 'Type mismatch on value: #{value}' 71 | def msg = 'Type mismatch on value: 1' 72 | LocaleContextHolder.locale = JAPANESE 73 | when: 74 | def result = handler.resolveMessage('detail', ex, request) 75 | then: 76 | 1 * handler.getMessage('detail', JAPANESE) >> msgTemplate 77 | then: 78 | 1 * interpolator.interpolate(msgTemplate, [ex: ex, req: request]) >> msg 79 | and: 80 | result == msg 81 | } 82 | 83 | def 'getMessage: find message for this exception class'() { 84 | setup: 85 | def expected = 'Chunky bacon' 86 | when: 87 | def actual = handler.getMessage('title', ENGLISH) 88 | then: 89 | 1 * messageSource.getMessage("${exceptionClass.name}.title", null, _, ENGLISH) >> expected 90 | and: 91 | actual == expected 92 | } 93 | 94 | def 'getMessage: find default message when no message for the exception class is found'() { 95 | given: 96 | def expected = 'Chunky bacon' 97 | when: 98 | def actual = handler.getMessage('type', ENGLISH) 99 | then: 100 | 1 * messageSource.getMessage("${exceptionClass.name}.type", null, _, ENGLISH) >> null 101 | then: 102 | 1 * messageSource.getMessage("${DEFAULT_PREFIX}.type", null, _, ENGLISH) >> expected 103 | and: 104 | actual == expected 105 | } 106 | 107 | def 'getMessage: return empty string if no message is found'() { 108 | setup: 109 | messageSource._ >> null 110 | expect: 111 | handler.getMessage('type', ENGLISH) == '' 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /src/test/groovy/cz/jirutka/spring/exhandler/handlers/HttpMediaTypeNotSupportedExceptionHandlerTest.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.exhandler.handlers 17 | 18 | import org.springframework.web.HttpMediaTypeNotSupportedException 19 | import spock.lang.Specification 20 | 21 | import static org.springframework.http.MediaType.* 22 | 23 | class HttpMediaTypeNotSupportedExceptionHandlerTest extends Specification { 24 | 25 | def handler = new HttpMediaTypeNotSupportedExceptionHandler() 26 | 27 | 28 | def 'create headers with "Accept" when supported media types are specified'() { 29 | given: 30 | def exception = new HttpMediaTypeNotSupportedException(IMAGE_GIF, [IMAGE_PNG, IMAGE_JPEG]) 31 | when: 32 | def headers = handler.createHeaders(exception, null) 33 | then: 34 | headers.getAccept() == [IMAGE_PNG, IMAGE_JPEG] 35 | } 36 | 37 | def 'create headers without "Accept" when supported media types are not specified'() { 38 | given: 39 | def exception = new HttpMediaTypeNotSupportedException('foo') 40 | when: 41 | def result = handler.createHeaders(exception, null) 42 | then: 43 | ! result.get('Accept') 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/test/groovy/cz/jirutka/spring/exhandler/handlers/HttpRequestMethodNotSupportedExceptionHandlerTest.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.exhandler.handlers 17 | 18 | import ch.qos.logback.classic.Logger 19 | import ch.qos.logback.classic.spi.LoggingEvent 20 | import ch.qos.logback.core.Appender 21 | import org.slf4j.LoggerFactory 22 | import org.springframework.mock.web.MockHttpServletRequest 23 | import org.springframework.web.HttpRequestMethodNotSupportedException 24 | import org.springframework.web.servlet.DispatcherServlet 25 | import spock.lang.Specification 26 | 27 | import static org.springframework.http.HttpMethod.POST 28 | import static org.springframework.http.HttpMethod.PUT 29 | 30 | class HttpRequestMethodNotSupportedExceptionHandlerTest extends Specification { 31 | 32 | def handler = new HttpRequestMethodNotSupportedExceptionHandler() 33 | def request = new MockHttpServletRequest() 34 | 35 | 36 | def 'create headers with "Allow" when supported methods are specified'() { 37 | given: 38 | def exception = new HttpRequestMethodNotSupportedException('PATCH', ['PUT', 'POST']) 39 | when: 40 | def headers = handler.createHeaders(exception, request) 41 | then: 42 | headers.getAllow() == [PUT, POST] as Set 43 | } 44 | 45 | def 'create headers without "Allow" when supported methods are not specified'() { 46 | given: 47 | def exception = new HttpRequestMethodNotSupportedException('PATCH') 48 | when: 49 | def result = handler.createHeaders(exception, request) 50 | then: 51 | ! result.get('Allow') 52 | } 53 | 54 | def 'log exception message in PageNotFound logger'() { 55 | setup: 56 | def logAppender = Mock(Appender) 57 | (LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME) as Logger).addAppender(logAppender) 58 | and: 59 | def spiedHandler = Spy(HttpRequestMethodNotSupportedExceptionHandler) { 60 | createBody(_, _) >> null 61 | } 62 | def exception = new HttpRequestMethodNotSupportedException('PUT') 63 | when: 64 | spiedHandler.handleException(exception, request) 65 | then: 66 | 1 * logAppender.doAppend({ LoggingEvent it -> 67 | it.message == exception.message && 68 | it.loggerName == DispatcherServlet.PAGE_NOT_FOUND_LOG_CATEGORY 69 | }) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/test/groovy/cz/jirutka/spring/exhandler/handlers/MethodArgumentNotValidExceptionHandlerTest.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.exhandler.handlers 17 | 18 | import cz.jirutka.spring.exhandler.messages.ValidationErrorMessage 19 | import org.springframework.mock.web.MockHttpServletRequest 20 | import org.springframework.web.bind.MethodArgumentNotValidException 21 | import spock.lang.Specification 22 | 23 | import static cz.jirutka.spring.exhandler.test.BindingResultBuilder.createBindingResult 24 | 25 | class MethodArgumentNotValidExceptionHandlerTest extends Specification { 26 | 27 | def handler = Spy(MethodArgumentNotValidExceptionHandler) { 28 | resolveMessage(*_) >> '' 29 | } 30 | def request = new MockHttpServletRequest() 31 | 32 | 33 | def 'create ValidationProblem with validation errors'() { 34 | given: 35 | def expected = new ValidationErrorMessage() 36 | .addError('Houston, we have a problem!') 37 | .addError('fillet', 666, 'This value is wrong!') 38 | def error0 = expected.errors[0] 39 | def error1 = expected.errors[1] 40 | and: 41 | def bindingResult = createBindingResult() 42 | .addObjectError(error0.message, 'Test') 43 | .addFieldError(error1.message, 'Test', error1.field, error1.rejected) 44 | .build() 45 | def exception = new MethodArgumentNotValidException(null, bindingResult) 46 | when: 47 | def actual = handler.createBody(exception, request) as ValidationErrorMessage 48 | then: 49 | actual.errors == expected.errors 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/test/groovy/cz/jirutka/spring/exhandler/handlers/NoSuchRequestHandlingMethodExceptionHandlerTest.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.exhandler.handlers 17 | 18 | import ch.qos.logback.classic.Logger 19 | import ch.qos.logback.classic.spi.LoggingEvent 20 | import ch.qos.logback.core.Appender 21 | import org.slf4j.LoggerFactory 22 | import org.springframework.mock.web.MockHttpServletRequest 23 | import org.springframework.web.servlet.DispatcherServlet 24 | import org.springframework.web.servlet.mvc.multiaction.NoSuchRequestHandlingMethodException 25 | import spock.lang.Specification 26 | 27 | class NoSuchRequestHandlingMethodExceptionHandlerTest extends Specification { 28 | 29 | 30 | def 'log exception in PageNotFound logger'() { 31 | setup: 32 | def logAppender = Mock(Appender) 33 | (LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME) as Logger).addAppender(logAppender) 34 | and: 35 | def handler = Spy(NoSuchRequestHandlingMethodExceptionHandler) { 36 | createBody(_, _) >> null 37 | } 38 | def exception = new NoSuchRequestHandlingMethodException(new MockHttpServletRequest()) 39 | when: 40 | handler.handleException(exception, new MockHttpServletRequest()) 41 | then: 42 | 1 * logAppender.doAppend({ LoggingEvent it -> 43 | it.message == exception.message && 44 | it.loggerName == DispatcherServlet.PAGE_NOT_FOUND_LOG_CATEGORY 45 | }) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/test/groovy/cz/jirutka/spring/exhandler/handlers/ResponseStatusRestExceptionHandlerTest.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.exhandler.handlers 17 | 18 | import org.springframework.http.HttpStatus 19 | import org.springframework.http.ResponseEntity 20 | import spock.lang.Specification 21 | 22 | class ResponseStatusRestExceptionHandlerTest extends Specification { 23 | 24 | def 'return ResponseEntity with defined status'() { 25 | setup: 26 | def expected = HttpStatus.I_AM_A_TEAPOT 27 | def factory = new ResponseStatusRestExceptionHandler(expected) 28 | expect: 29 | factory.handleException(null, null) == new ResponseEntity(expected) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/test/groovy/cz/jirutka/spring/exhandler/interpolators/NoOpMessageInterpolatorTest.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.exhandler.interpolators 17 | 18 | import spock.lang.Specification 19 | 20 | class NoOpMessageInterpolatorTest extends Specification { 21 | 22 | def interpolator = new NoOpMessageInterpolator() 23 | 24 | 25 | def 'just return given template as-is'() { 26 | given: 27 | def expected = 'Allons-y!' 28 | expect: 29 | interpolator.interpolate(expected, [:]) is expected 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/test/groovy/cz/jirutka/spring/exhandler/interpolators/SpelMessageInterpolatorTest.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.exhandler.interpolators 17 | 18 | import org.springframework.context.expression.MapAccessor 19 | import org.springframework.expression.EvaluationContext 20 | import org.springframework.expression.Expression 21 | import org.springframework.expression.ExpressionException 22 | import org.springframework.expression.ExpressionParser 23 | import org.springframework.expression.common.TemplateParserContext 24 | import org.springframework.expression.spel.standard.SpelExpressionParser 25 | import org.springframework.expression.spel.support.ReflectivePropertyAccessor 26 | import org.springframework.expression.spel.support.StandardEvaluationContext 27 | import spock.lang.Specification 28 | 29 | class SpelMessageInterpolatorTest extends Specification { 30 | 31 | def evalContext = Mock(EvaluationContext) 32 | def parser = Mock(ExpressionParser) 33 | def expression = Mock(Expression) 34 | 35 | 36 | def 'create with default evaluation context'() { 37 | when: 38 | def mi = new SpelMessageInterpolator() 39 | then: 40 | mi.evalContext instanceof StandardEvaluationContext 41 | mi.evalContext.propertyAccessors*.class.containsAll(MapAccessor, ReflectivePropertyAccessor) 42 | } 43 | 44 | def 'interpolate message using ExpressionParser'() { 45 | given: 46 | def interpolator = new SpelMessageInterpolator(evalContext) { 47 | ExpressionParser parser() { parser } 48 | } 49 | def msgTemplate = 'Allons-y, #{name}!' 50 | def vars = [name: 'Alonso'] 51 | def interpolated = 'Allons-y, Alonso!' 52 | when: 53 | def result = interpolator.interpolate(msgTemplate, vars) 54 | then: 55 | 1 * parser.parseExpression(msgTemplate, _ as TemplateParserContext) >> expression 56 | 1 * expression.getValue(evalContext, vars, String) >> interpolated 57 | and: 58 | result == interpolated 59 | } 60 | 61 | def 'return empty string when parser or evaluator throws exception'() { 62 | given: 63 | def interpolator = new SpelMessageInterpolator(evalContext) { 64 | ExpressionParser parser() { parser } 65 | } 66 | parser.parseExpression(*_) >> { throw new ExpressionException('fail') } 67 | expect: 68 | interpolator.interpolate('fail', [:]) == '' 69 | } 70 | 71 | def 'created parser should be SpelExpressionParser'() { 72 | given: 73 | def interpolator = new SpelMessageInterpolator() 74 | expect: 75 | interpolator.parser() instanceof SpelExpressionParser 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/test/groovy/cz/jirutka/spring/exhandler/messages/ErrorMessageTest.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2015 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.exhandler.messages 17 | 18 | import com.fasterxml.jackson.databind.ObjectMapper 19 | import groovy.json.JsonSlurper 20 | import spock.lang.Specification 21 | 22 | class ErrorMessageTest extends Specification { 23 | 24 | def jackson = new ObjectMapper() 25 | def jsonParser = new JsonSlurper() 26 | 27 | 28 | def 'convert to JSON using Jackson2 and ignore empty fields'() { 29 | given: 30 | def object = new ErrorMessage( 31 | type: new URI('http://httpstatus.es/400'), 32 | title: 'Type Mismatch', 33 | status: 400, 34 | detail: '') 35 | when: 36 | def result = jackson.writeValueAsString(object) 37 | then: 38 | with (jsonParser.parseText(result)) { 39 | type == object.type.toString() 40 | title == object.title 41 | status == object.status 42 | ! detail 43 | ! instance 44 | } 45 | } 46 | 47 | def 'convert from JSON using Jackson2'() { 48 | given: 49 | def json = ''' 50 | { 51 | "type": "http://httpstatus.es/500", 52 | "title": "Internal Server Error", 53 | "status": 500, 54 | "detail": "Some detail", 55 | "instance": "http://example.org/exceptions/123456" 56 | }''' 57 | when: 58 | def result = jackson.readValue(json, ErrorMessage) 59 | then: 60 | jsonParser.parseText(json).each { attr, val -> 61 | assert result[attr].toString() == val.toString() 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/test/groovy/cz/jirutka/spring/exhandler/test/BindingResultBuilder.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Jakub Jirutka . 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cz.jirutka.spring.exhandler.test 17 | 18 | import org.springframework.validation.BeanPropertyBindingResult 19 | import org.springframework.validation.BindingResult 20 | import org.springframework.validation.FieldError 21 | import org.springframework.validation.ObjectError 22 | 23 | class BindingResultBuilder { 24 | 25 | final BindingResult bindingResult 26 | 27 | 28 | private BindingResultBuilder(Object target, String objectName) { 29 | this.bindingResult = new BeanPropertyBindingResult(target, objectName) 30 | } 31 | 32 | static BindingResultBuilder createBindingResult(Object target = null, String objectName = 'Test') { 33 | new BindingResultBuilder(target, objectName) 34 | } 35 | 36 | 37 | BindingResultBuilder addFieldError( 38 | String defaultMessage, String objectName, String field, Object rejectedValue = null, 39 | boolean bindingFailure = false, String[] codes = [], Object[] arguments = []) { 40 | 41 | bindingResult.addError( 42 | new FieldError(objectName, field, rejectedValue, bindingFailure, codes, arguments, defaultMessage)) 43 | this 44 | } 45 | 46 | BindingResultBuilder addObjectError( 47 | String defaultMessage, String objectName, String[] codes = [], Object[] arguments = []) { 48 | 49 | bindingResult.addError(new ObjectError(objectName, codes, arguments, defaultMessage)) 50 | this 51 | } 52 | 53 | BindingResult build() { 54 | bindingResult 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | spring-rest-exception-handler 5 | 6 | 7 | 8 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | --------------------------------------------------------------------------------