Spring MVC - #ExceptionHandler based on Accept header - java

I have a HandlerInterceptorAdapter that intercepts all requests and performs user authorization checks. Very basically:
#Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
User user = ... // get user
checkIfAuthorized(user); // throws AuthorizationException
return true;
}
I then have an #ExceptionHandler for that AuthorizationException.
#ExceptionHandler(value = AuthorizationException.class)
public ResponseEntity<String> handleNotAuthorized(AuthorizationException e) {
// TODO Custom EXCEPTION HANDLER for json/jsp/xml/other types, based on content type
ResponseEntity<String> responseEntity = new ResponseEntity<>("You are not authorized to access that page.", HttpStatus.UNAUTHORIZED);
return responseEntity;
}
This is fine if the (unauthorized) request accepts text/plain (and can be easily changed for json).
How can I make different #ExceptionHandlers for specific Accept headers?
#RequestMapping has produces(). Is there something similar for #ExceptionHandler?

I know this comes late but I've been looking up a solution to this, came across this question and found what I think to be a better solution. You can return "forward:/error" in your #ExceptionHandler (returning a String) to forward the request to a
#RequestMapping("/error")
ErrorController {...}
and use
#RequestMapping(produces = "text/html")
ModelAndView errorPage() {...}
on one method of that ErrorController,
#RequestMapping(produces = "application/json") // or no 'produces' attribute for a default
MyJsonObject errorJson() {...} on another.
I think this is a pretty neat way to do it, it's probably already out there but I didn't find it when trying to look it up.
So basically the #ExceptionHandler is the same for all, but forwards to a controller that can do the usual stuff

I think of two approaches:
Manually
public ResponseEntity<String> handleNotAuthorized(AuthorizationException e, HttpServletRequest request) {
// TODO Custom EXCEPTION HANDLER for json/jsp/xml/other types, based on content type
if (/*read header accept from request and build appropiate response*/) {}
ResponseEntity<String> responseEntity = new ResponseEntity<>("You are not authorized to access that page.", HttpStatus.UNAUTHORIZED);
return responseEntity;
Automatically
#ResponseBody
public SomeObject handleNotAuthorized(AuthorizationException e, HttpServletRequest request) {
// TODO Custom EXCEPTION HANDLER for json/jsp/xml/other types, based on content type
/* Construct someObject and let Spring MessageConverters transform it to JSON or XML. I don't remember what happens in case of HTML (it should go to a view)*/
return someObject;
Don't forget to set the Response's Status code.

Not exactly the same use case, but the same requirement. I solve it with a custom HttpMessageConverter implementation.
#RestController
#RequestMapping("/foo")
public class MyResource {
#GetMapping(path = "/{id}", produces = "application/json")
public ResponseEntity<MyDto> get (#PathVariable(ID) long id)
throws IOException {
throw new MyCustomException();
}
#GetMapping(path = "/{id}/export", produces = "application/zip")
public ResponseEntity<byte[]> export (#PathVariable(ID) long id)
throws IOException {
throw new MyCustomException();
}
}
...
#ControllerAdvice
public class MyCustomExceptionHandler {
#ResponseBody
#ExceptionHandler
#ResponseStatus(BAD_REQUEST)
public JsonAPIErrorDocument handleException (MyCustomException e) {
return ....;
}
}
...
public class JsonAPIErrorDocumentToByteArrayMessageConverter extends AbstractHttpMessageConverter {
public ErrorDocumentToByteArrayMessageConverter () {
super(new MediaType("application", "zip"), MediaType.ALL);
}
#Override
protected boolean supports (Class clazz) {
return JsonAPIErrorDocument.class == clazz;
}
#Override
protected Object readInternal (Class clazz, HttpInputMessage inputMessage)
throws IOException,
HttpMessageNotReadableException {
return new byte[0];
}
#Override
protected void writeInternal (Object t, HttpOutputMessage outputMessage)
throws IOException,
HttpMessageNotWritableException {
}
}
...
#EnableWebMvc
#Configuration
#ComponentScan({ "com.foo" })
public class ApplicationConfig implements WebMvcConfigurer {
...
#Override
public void configureMessageConverters (List<HttpMessageConverter<?>> converters) {
converters.add(new MappingJackson2HttpMessageConverter(objectMapper));
converters.add(new ByteArrayHttpMessageConverter());
converters.add(new JsonAPIErrorDocumentToByteArrayMessageConverter());
}
...
}

Related

How to throw NoHandlerFoundException if path param is not Long in springboot

Currently, there is a GetMapping like follows
#GetMapping(value = "/{id}")
public ResponseEntity<Dog> getTrainById(#PathVariable Long id) {
Dog dog= animalService.getAnimalById(id);
return new ResponseEntity<>(Dog , HttpStatus.OK);
}
now if someone accesses http://localhost:8080/api/animal/1, it returns the animal.
But I need to throw NoHandlerFoundException if someone accesses this endpoint without a Long variable as a path parameter, that means like this http://localhost:8080/api/animal/asdsad
IF anyone can tell me way to achieve this, that would be much appreciated
Also I have global exception handling like follows
#ControllerAdvice
public class DemoExceptionHandler extends ResponseEntityExceptionHandler {
#ExceptionHandler(EntityNotFoundException.class)
public ResponseEntity<GenericResponse> customHandleNotFound(Exception ex, WebRequest request)
{
return new ResponseEntity<>(new GenericResponse(ex.getMessage(), null), HttpStatus.NOT_FOUND);
}
#Override
protected ResponseEntity<Object> handleNoHandlerFoundException(NoHandlerFoundException ex,
HttpHeaders headers, HttpStatus status, WebRequest request) {
return new ResponseEntity<>(new GenericResponse("invalid endpoint", null), HttpStatus.METHOD_NOT_ALLOWED);
}
}
In this case which the request cannot be resolved to the parameter type of a controller method , it will throw MethodArgumentTypeMismatchException.
So the most effective way to solve the problem is to think about how to handle MethodArgumentTypeMismatchException directly but not think how to make it re-throw NoHandlerFoundException. So you can simply create a #ControllerAdvice to handle MethodArgumentTypeMismatchException :
#ControllerAdvice
public class DemoExceptionHandler {
#ExceptionHandler(MethodArgumentTypeMismatchException.class)
public ResponseEntity<Object> handle(MethodArgumentTypeMismatchException ex) {
return new ResponseEntity<>( GenericResponse("invalid endpoint", null), HttpStatus.METHOD_NOT_ALLOWED);
}
}
It will be applied to all controllers that throw this type of exception . If you only want it to apply for a particular controller but not globally , you can do :
#RestController
#RequestMapping("/foo")
public class FooController {
#GetMapping(value = "/{id}")
public ResponseEntity<Dog> getTrainById(#PathVariable Long id) {
}
#ExceptionHandler(MethodArgumentTypeMismatchException.class)
public ResponseEntity<Object> handleMethodArgumentTypeMismatchException() {
return new ResponseEntity<>( GenericResponse("invalid endpoint", null), HttpStatus.METHOD_NOT_ALLOWED);
}
}

Validation of JWT token along with requestBody parameter in springBoot

I have an reactive spring boot application where I am having different controllers and all the controller having get, post, put, delete methods
GET and DELETE method URI format => /{userName}/{others}
and it's ensured that put and post method must have a field userid in their request body.
Also All the request having an authorization header.
And I already have a method called validate that accepts 2 parameters authorizationHeader and userName and returns true if this mapping exists false if not.
I am trying to write generic filter can filter incoming request and validate before going to controller.
How can I write this generic webfilter especially how to extract body from post request and validate requests.
I tried writing this
#Component
#Slf4j
public class ExampleWebFilter implements WebFilter {
#Override
public Mono<Void> filter(ServerWebExchange serverWebExchange, WebFilterChain webFilterChain) {
ObjectMapper mapper = new ObjectMapper();
return serverWebExchange
.getRequest()
.getBody()
.next()
.flatMap(body -> {
try {
return validate(body, serverWebExchange
.geHeaders().get(0))
} catch (IOException e) {
return Mono.error(e);
}
})
.flatMap((boolean s) -> {
return webFilterChain.filter(serverWebExchange);
});
}
Mono<Boolean> validate(DataBuffer body, String Header){
//my logic to validate
}
}
But it seems it's hanging after this filter method executed. so my question is
How can I write webfilter which will read body and validate?
Is there any other generic solution available for this type of problem in spring-boot?
I think you should use Interceptors. You can intercept the http call, and make your validations on the request. You can do this as global or you can do this for specific endpoints/paths. Here is a example for your case.
#Component
public class ProductServiceInterceptor implements HandlerInterceptor {
#Override
public boolean preHandle(
HttpServletRequest request, HttpServletResponse response, Object handler) throws
Exception {
return true;
}
#Override
public void postHandle(
HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
//make validations
}
#Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception exception) throws Exception {
//make validations
}
}
After this you need to register your interceptor like below.
#Component
public class ProductServiceInterceptorAppConfig extends WebMvcConfigurerAdapter {
#Autowired
ProductServiceInterceptor productServiceInterceptor;
#Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(productServiceInterceptor);
}
}
For more depth information you can visit the links below.
https://www.youtube.com/watch?v=agBadIAx0Wc
https://www.tutorialspoint.com/spring_boot/spring_boot_interceptor.htm

Unable to intercept and manipulate HttpServletResponse in Spring Boot

I have a requirement to Base64 decode every JSON request payload that my Spring Boot service receives. The JSON payload would have been Base64 encoded at the client before posting using the HTTP POST method. Further, I also need to Base64 encode the JSON response before presenting to the calling client application.
I am required to reduce boilerplate code by using handler interceptors.
I have already achieved the request/incoming leg of the operation by the use of interceptors but is yet to achieve this for the response leg.
I have posted the code snippets below. The code to intercept the response and base64 encode it is in the postHandle method of the interceptor class.
What am I doing wrong here?
Interceptor Class:
public class Base64ResponseEncodingInterceptor implements HandlerInterceptor {
private static final String DECODED_REQUEST_ATTRIB = "decodedRequest";
private static final String POST = "POST";
#Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView arg3) throws Exception {
try {
if (POST.equalsIgnoreCase(request.getMethod())) {
CharResponseWrapper res = new CharResponseWrapper(response);
res.getWriter();
byte[] encoded = Base64.encodeBase64(res.toString().getBytes());
byte[] encoded = Base64.encodeBase64(response.getHeader(ENCODED_RESPONSE_ATTRIB).getBytes());
response.getWriter().write(new String(encoded));
}
} catch (Exception e) {
throw new Exception(e.getMessage());
}
}
// preHandle and afterCompletion methods
// Omitted
}
The CharResponseWrapper Class used above:
public class CharResponseWrapper extends HttpServletResponseWrapper {
protected CharArrayWriter charWriter;
protected PrintWriter writer;
protected boolean getOutputStreamCalled;
protected boolean getWriterCalled;
public CharResponseWrapper(HttpServletResponse response) {
super(response);
charWriter = new CharArrayWriter();
}
#Override
public ServletOutputStream getOutputStream() throws IOException {
if (getWriterCalled) {
throw new IllegalStateException("getWriter already called");
}
getOutputStreamCalled = true;
return super.getOutputStream();
}
#Override
public PrintWriter getWriter() throws IOException {
if (writer != null) {
return writer;
}
if (getOutputStreamCalled) {
throw new IllegalStateException("getOutputStream already called");
}
getWriterCalled = true;
writer = new PrintWriter(charWriter);
return writer;
}
#Override
public String toString() {
String s = null;
if (writer != null) {
s = charWriter.toString();
}
return s;
}
}
JavaConfig Class where Interceptor is registered:
#Configuration
#EnableJpaRepositories(repositoryBaseClass = BaseRepositoryBean.class, basePackages = "")
#EntityScan(basePackages = { "com.companyname", "com.companyname.productname"})
public class RestConfig extends WebMvcConfigurerAdapter {
#Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new Base64ResponseEncodingInterceptor());
}
}
The Controller Class, where the Interceptor is used (Only the working request leg is shown here):
#Autowired
HttpServletRequest request;
String decodedRequest = null;
#ModelAttribute("decodedRequest")
public void getDecodedParam(){
decodedRequest = (String) request.getAttribute("decodedRequest");
}
The code in the postHandle method does not work. It is either the HttpServletResponse is null or I get an exception message:
getOutputStream already called
Update: Work around solution to reading the response directly in the ResponseBodyAdvice
In the Controller Class, I added the following:
#RestController
#RequestMapping("/api/ipmanager")
public class IPProfileRestController extends AbstractRestController {
#Autowired
HttpServletResponse response;
String encodedResponse = null;
#ModelAttribute("encodedResponse")
public void getEncodedResponse(){
response.setHeader("encodedResponse", StringUtils.EMPTY);
}
#RequestMapping(value = "/time", method = { RequestMethod.POST }, produces = { MediaType.APPLICATION_JSON_VALUE, MediaType.TEXT_PLAIN_VALUE }, consumes = {
MediaType.APPLICATION_JSON_VALUE })
public #ResponseBody String saveAccessClientTime(#RequestBody String ecodedRequest) {
// Some code here
String controllerResponse = prettyJson(iPProfileResponse);
response.setHeader("encodedResponse", controllerResponse);
return controllerResponse;
}
}
I have the following in the ResponseBodyAdvice
#ControllerAdvice
public class Base64EncodedResponseBodyAdvice implements ResponseBodyAdvice<Object> {
#Override
public boolean supports(MethodParameter returnType,
Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}
#Override
public Object beforeBodyWrite(Object body,
MethodParameter returnType,
MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> converterType,
ServerHttpRequest request,
ServerHttpResponse response) {
String body1 = StringUtils.EMPTY;
// Encode the response and return
List<String> listOfHeaderValues = response.getHeaders().get("encodedResponse");
body1=new String(Base64.encodeBase64(listOfHeaderValues.get(0).getBytes()));
return body1;
}
}
As the Spring MVC documentation states:
the postHandle method of HandlerInterceptor is not always ideally
suited for use with #ResponseBody and ResponseEntity methods. In such
cases an HttpMessageConverter writes to and commits the response
before postHandle is called which makes it impossible to change the
response, for example to add a header. Instead an application can
implement ResponseBodyAdvice and either declare it as an
#ControllerAdvice bean or configure it directly on
RequestMappingHandlerAdapter.
With that being said:
What am I doing wrong here?
Since the response has been already committed, you can't change it. In order to change the response you should register a ResponseBodyAdvice<T> and put your response encoding logic there:
#ControllerAdvice
public class Base64EncodedResponseBodyAdvice implements ResponseBodyAdvice<Object> {
#Override
public boolean supports(MethodParameter returnType,
Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}
#Override
public Object beforeBodyWrite(Object body,
MethodParameter returnType,
MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> converterType,
ServerHttpRequest request,
ServerHttpResponse response) {
// Encode the response and return
}
}
If your method is returning ResponseEntity<T> then write this code in postHandle() method of your HandlerInterceptor:
eg. response.addHeader("jwt_token", "kajdlakjd");
it will work!!

How to add HttpServletRequest param to complex RestController bean?

I have a complex bean holding the rest parameters, eg:
public class MyRestParams {
private HttpServletRequest req;
private #NotBlank String name;
//getter, setter
}
Usage:
#RestController
#RequestMapping("/xml")
public class MyServlet {
#RequestMapping(value = "/")
public void getTest(#Valid MyRestParams p) {
Sysout(p.getName()); //works when invoked with /xml?name=test
Sysout(p.getReq()); //always null
}
}
Problem: the HttpServletRequest is always null. Isn't it possible to add this parameter within the bean itself?
You can provide an implementation for HandlerMethodArgumentResolver to resolve your MyRestParams:
public class MyRestParamsArgumentResolver implements HandlerMethodArgumentResolver {
#Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterType().equals(MyRestParams.class);
}
#Override
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
MyRestParams restParam = new MyRestParams();
restParam.setReq((HttpServletRequest) webRequest.getNativeRequest());
return restParam;
}
}
Then register it in your WebMvcConfigurerAdapter:
#EnableWebMvc
#Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
#Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new MyRestParamsArgumentResolver());
}
}
When using that form of method signature Spring will use your bean as a model attribute. Spring will bind your request parameters to bean properties of matching names using a WebDataBinder e.g ServletRequestDataBinder.
Since there is no request parameter which matches your bean property req the field will never be set. Even if the request parameter with name req existed in your request it wont be convertible to a HttpServletRequest.
To receive the actual request add a parameter of type HttpServletRequest to your handler method
#RequestMapping(value = "/")
public void getTest(#Valid MyRestParams p , HttpServletRequest request) {
Sysout(p.getName()); //works when invoked with /xml?name=test
Sysout(request); //always null
}
Or a parameter of type WebRequest if you dont want to tie yourself to the Servlet API.

How do I design a generic Response builder / RESTful Web Service using Spring MVC?

Trying to build a RESTful web service using Spring MVC.
The controller should return specific Java types, but the response body must be a generic envelope. How can this be done?
The following sections of code are what I have so far:
Controller method:
#Controller
#RequestMapping(value = "/mycontroller")
public class MyController {
public ServiceDetails getServiceDetails() {
return new ServiceDetails("MyService");
}
}
Response envelope:
public class Response<T> {
private String message;
private T responseBody;
}
ServiceDetails code:
public class ServiceDetails {
private String serviceName;
public ServiceDetails(String serviceName) {
this.serviceName = serviceName;
}
}
Intended final response to clients should appear as:
{
"message" : "Operation OK"
"responseBody" : {
"serviceName" : "MyService"
}
}
What you can do is having a MyRestController just wrapping the result in a Response like this:
#Controller
#RequestMapping(value = "/mycontroller")
public class MyRestController {
#Autowired
private MyController myController;
#RequestMapping(value = "/details")
public #ResponseBody Response<ServiceDetails> getServiceDetails() {
return new Response(myController.getServiceDetails(),"Operation OK");
}
}
This solution keep your original MyController independant from your REST code. It seems you need to include Jackson in your classpath so that Spring will auto-magically serialize to JSON (see this for details)
EDIT
It seems you need something more generic... so here is a suggestion.
#Controller
#RequestMapping(value = "/mycontroller")
public class MyGenericRestController {
#Autowired
private MyController myController;
//this will match all "/myController/*"
#RequestMapping(value = "/{operation}")
public #ResponseBody Response getGenericOperation(String #PathVariable operation) {
Method operationToInvoke = findMethodWithRequestMapping(operation);
Object responseBody = null;
try{
responseBody = operationToInvoke.invoke(myController);
}catch(Exception e){
e.printStackTrace();
return new Response(null,"operation failed");
}
return new Response(responseBody ,"Operation OK");
}
private Method findMethodWithRequestMapping(String operation){
//TODO
//This method will use reflection to find a method annotated
//#RequestMapping(value=<operation>)
//in myController
return ...
}
}
And keep your original "myController" almost as it was:
#Controller
public class MyController {
//this method is not expected to be called directly by spring MVC
#RequestMapping(value = "/details")
public ServiceDetails getServiceDetails() {
return new ServiceDetails("MyService");
}
}
Major issue with this : the #RequestMapping in MyController need probably to be replaced by some custom annotation (and adapt findMethodWithRequestMapping to perform introspection on this custom annotation).
By default, Spring MVC uses org.springframework.http.converter.json.MappingJacksonHttpMessageConverter to serialize/deserialize JSON through Jackson.
I'm not sure if it's a great idea, but one way of solving your problem is to extend this class, and override the writeInternal method:
public class CustomMappingJacksonHttpMessageConverter extends MappingJacksonHttpMessageConverter {
#Override
protected void writeInternal(Object object, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
super.writeInternal(new Response(object, "Operation OK"), outputMessage);
}
}
If you're using XML configuration, you could enable the custom converter like this:
<mvc:annotation-driven>
<mvc:message-converters>
<bean class="path.to.CustomMappingJacksonHttpMessageConverter">
</mvc:message-converters>
</mvc:annotation-driven>
Try the below solution.
Create a separate class such ResponseEnvelop. It must implement ResponseBodyAdvice interface.
Annotate the above class with #ControllerAdvice
Autowire HttpServletRequest
Override methods according to your requirement. Take reference from below.
#Override
public boolean supports(
MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
if (httpServletRequest.getRequestURI().startsWith("/api")) {
return true;
}
return false;
}
#Override
public Object beforeBodyWrite(
Object body,
MethodParameter returnType,
MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> converterType,
ServerHttpRequest request,
ServerHttpResponse response) {
if (((ServletServerHttpResponse) response).getServletResponse().getStatus()
== HttpStatus.OK.value()
|| ((ServletServerHttpResponse) response).getServletResponse().getStatus()
== HttpStatus.CREATED.value()) {
return new EntityResponse(Constants.SUCCESS, body);
}
return body;
}

Categories

Resources