Sunday, July 23, 2017

Spring MVC RequestBodyAdvice or how to add a processing step for parameters in Page Controller methods

... There are a few problems when we need to add some additional processing steps for the Spring MVC Page Controller method parameters. Before the Spring 4.2 we needed to add additional Argument Resolver to do that which was a bit problematic if we need to mix our logic with standard Spring MVC existing one like declarative validation using @Valid annotation?

Assume we have the most popular Request Mapping approach (@Controller/@RequestMapping) and want to add some logic deal with with incoming parameters. It could be the problem of HTML-escaping (JavaScript, XML, SQL, whatever) of some fields for incoming POJOs (DTOs).

Here is the Controller code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.example.demo;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonTypeName;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.NotBlank;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestBody;

import javax.validation.Valid;

@Controller
public class ReportController {
    @GetMapping(value = "/content")
    void report(@RequestBody @Valid ContentRequest contentRequest) {
        // process it ...
    }

    @JsonTypeName("content")
     static class ContentRequest {
        @JsonProperty("id")
        long id;

        @JsonProperty("title")
        @Length(max = 10000, message = "{validation.max.length.exceeds}")
        @NotBlank(message = "{validation.not.empty}")
        String content;
    }
}

And we need to add HTML escape for the content field since it could be published somewhere on other pages.

The solution might be to start using RequestBodyAdvice feature available since Spring MVC.

Let's declare our Advice to MVC configuration:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
@Configuration
@EnableWebMvc
class MvcConfig extends WebMvcConfigurationSupport {
 @Resource
 private RequestBodyAdvice escapeRequestBodyAdvice;

 @Override
 protected void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
  // we can avoid adding new custom since we can use RequestBodyAdvices ...
 }

 @Override
 public RequestMappingHandlerAdapter requestMappingHandlerAdapter() {
  RequestMappingHandlerAdapter requestMappingHandlerAdapter = super.requestMappingHandlerAdapter();
  // here is the our custom one: escapeRequestBodyAdvice
  requestMappingHandlerAdapter.setRequestBodyAdvice(Collections.singletonList(escapeRequestBodyAdvice));
  return requestMappingHandlerAdapter;
 }
}

and our RequestBodyAdvice looks like:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@Component
public class EscapeRequestBodyAdvice extends RequestBodyAdviceAdapter {
    @Override
    public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        return true;
    }

    @Override
    public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        EscapeBeanUtil.escape(body);
        return body;
    }
}

where EscapeBeanUtil is the utility to escape the fields. It is not recursive - just for the demo purposes. It searches for @EscapeHTML and escapes the annotated field.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import org.apache.commons.lang3.StringEscapeUtils;
import org.springframework.util.ReflectionUtils;

class EscapeBeanUtil {
  /**
   * Only one level of simple String fields supported
   *
   * @param bean
   */
  static void escape(Object bean) {
    ReflectionUtils.doWithFields(
        bean.getClass(),
        field -> {
          if (!field.isAccessible()) {
            field.setAccessible(true);
          }

          Object value = ReflectionUtils.getField(field, bean);

          if (value != null) {
            String escaped = StringEscapeUtils.escapeHtml4(
                String.valueOf(value)
            );

            ReflectionUtils.setField(field, bean, escaped);
          }
        },
        field -> field.getAnnotationsByType(EscapeHTML.class).length > 0 &&
            String.class.isAssignableFrom(field.getType())
    );
  }
}

Annotation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface EscapeHTML {

}


Now, after these changes we have our POJO/DTO a bit modified (See at @EscapeHTML)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonTypeName;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.NotBlank;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestBody;

import javax.validation.Valid;

@Controller
public class ReportController {
    @GetMapping(value = "/content")
    void report(@RequestBody @Valid ContentRequest contentRequest) {
        // process it
    }

    @JsonTypeName("content")
     static class ContentRequest {
        @JsonProperty("id") // other fields which require validation...
        long id;

        @JsonProperty("title")
        @Length(max = 100000, message = "{validation.max.length.exceeds}")
        @NotBlank(message = "{validation.not.empty}")
        @EscapeHTML
        String content;
    }
}

That's it!

Also please note that now validation is broken a bit: after HTML escaping limits for the content length could not fit since more chars are added. To overcome it you can pre-calculate the field size on UI or relax a bit the size or add some custom validator for that field. But very often for the big text inputs where the escape is required there are no limits


No comments:

Post a Comment

Design Patterns Wrapper revisited

 Now lots of software designed and developed following microservice architecture. Components need to communicate with each other via VO (val...