2

I want to create REST Server which accepts XML requests and plain text into different controllers. I tried to implement this:

@SpringBootApplication
public class Application extends SpringBootServletInitializer implements WebMvcConfigurer {

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
        return application.sources(Application.class);
    }
    ..............

    private BasicAuthenticationInterceptor basicAuthenticationInterceptor;

    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.removeIf(converter -> converter instanceof MappingJackson2XmlHttpMessageConverter);
        converters.removeIf(converter -> converter instanceof MappingJackson2HttpMessageConverter);
        converters.add(new MappingJackson2XmlHttpMessageConverter(
                ((XmlMapper) createObjectMapper(Jackson2ObjectMapperBuilder.xml()))
                        .enable(ToXmlGenerator.Feature.WRITE_XML_DECLARATION)));
        converters.add(new MappingJackson2HttpMessageConverter(createObjectMapper(Jackson2ObjectMapperBuilder.json())));
    }

    private ObjectMapper createObjectMapper(Jackson2ObjectMapperBuilder builder) {
        builder.indentOutput(true);
        builder.modules(new JaxbAnnotationModule());
        builder.serializationInclusion(JsonInclude.Include.NON_NULL);
        builder.defaultUseWrapper(false);
        return builder.build();
    }

    @Autowired
    public void setBasicAuthenticationInterceptor(BasicAuthenticationInterceptor basicAuthenticationInterceptor) {
        this.basicAuthenticationInterceptor = basicAuthenticationInterceptor;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(basicAuthenticationInterceptor);
    }
}

Check for XML proper formatting:

@ControllerAdvice
public class RestExceptionHandler extends ResponseEntityExceptionHandler {

    @Override
    protected ResponseEntity<Object> handleHttpMessageNotReadable(HttpMessageNotReadableException ex,
                                                                  HttpHeaders headers, HttpStatus status, WebRequest request) {
        PaymentTransaction response;
        if (ex.getMessage().contains("Required request body")) {
            response = new PaymentTransaction(PaymentTransaction.Response.failed_response, 350,
                    "Invalid XML message: No XML data received", "XML request parsing failed!");
        } else {
            response = new PaymentTransaction(PaymentTransaction.Response.failed_response, 351,
                    "Invalid XML message format", null);
        }
        return ResponseEntity.badRequest().body(response);
    }
}

Controller Class:

@RestController()
public class HomeController {

    @Autowired
    public HomeController(Map<String, MessageProcessor> processors, Map<String, ReconcileProcessor> reconcileProcessors,
            @Qualifier("defaultProcessor") MessageProcessor defaultProcessor,
            AuthenticationService authenticationService, ClientRepository repository,
            @Value("${request.limit}") int requestLimit) {
        // Here I receive XML 
    }

    @GetMapping(value = "/v1/*")
    public String message() {
        return "REST server";
    }

    @PostMapping(value = "/v1/{token}", consumes = { MediaType.APPLICATION_XML_VALUE,
            MediaType.APPLICATION_JSON_VALUE }, produces = { MediaType.APPLICATION_XML_VALUE,
                    MediaType.APPLICATION_JSON_VALUE })
    public PaymentResponse handleMessage(@PathVariable("token") String token,
            @RequestBody PaymentTransaction transaction, HttpServletRequest request) throws Exception {
        // Here I receive XML 
    }

    @PostMapping(value = "/v1/notification")
    public ResponseEntity<String> handleNotifications(@RequestBody Map<String, String> keyValuePairs) {
         // Here I receive key and value in request body
    }

    @PostMapping(value = "/v1/summary/by_date/{token}", consumes = { MediaType.APPLICATION_XML_VALUE,
            MediaType.APPLICATION_JSON_VALUE }, produces = { MediaType.APPLICATION_XML_VALUE,
                    MediaType.APPLICATION_JSON_VALUE })
    public PaymentResponses handleReconcile(@PathVariable("token") String token, @RequestBody Reconcile reconcile,
            HttpServletRequest request) throws Exception {
         // Here I receive XML 
    }

    @ResponseStatus(value = HttpStatus.UNAUTHORIZED)
    public static class UnauthorizedException extends RuntimeException {
        UnauthorizedException(String message) {
            super(message);
        }
    }
}

As you can see in some methods I receive XML and in other I receive String in form of key=value&.....

How I configure Spring to accept both types? Also should I split the Rest controller into different files?

EDIT:

Sample XML request:

<?xml version="1.0" encoding="UTF-8"?>
<payment_transaction>
  <transaction_type>authorize</transaction_type>
  <transaction_id>2aeke4geaclv7ml80</transaction_id>
  <amount>1000</amount>
  <currency>USD</currency>
  <card_number>22</card_number>
  <shipping_address>
    <first_name>Name</first_name>    
  </shipping_address>
</payment_transaction>

Sample XML response:

<?xml version="1.0" encoding="UTF-8"?>
<payment_response>
    <transaction_type>authorize</transaction_type>
    <status>approved</status>
    <unique_id>5f7edd36689f03324f3ef531beacfaae</unique_id>
    <transaction_id>asdsdlddea4sdaasdsdsa4dadasda</transaction_id>
    <code>500</code>
    <amount>101</amount>
    <currency>EUR</currency>
</payment_response>

Sample Notification request:

uniqueid=23434&type=sale&status=33

Sample Notification response: It should return only HTTP status OK.

I use:

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.4.RELEASE</version>
        <relativePath />
    </parent>

Java version: "10.0.2" 2018-07-17

About the XML generation I use:

@XmlRootElement(name = "payment_transaction")
public class PaymentTransaction {

    public enum Response {
        failed_response, successful_response
    }

    @XmlElement(name = "transaction_type")
    public String transactionType;
    @XmlElement(name = "transaction_id")
    public String transactionId;
    @XmlElement(name = "usage")

POM Configuration: https://pastebin.com/zXqYhDH3

Peter Penzov
  • 1,126
  • 134
  • 430
  • 808
  • What do you mean by accepting both formats? – akortex Sep 02 '18 at 17:15
  • I mean this: The implemented Rest server should accept XML requests and Simple keys and values into requests body. – Peter Penzov Sep 02 '18 at 17:50
  • What about implementing [request interceptor](https://stackoverflow.com/questions/31082981/spring-boot-adding-http-request-interceptors), that could manipulate the request before it arrives your controller(s) or a [RequestBodyAdvice](http://deventh.blogspot.com/2017/07/spring-mvc-requestbodyadvice-or-how-to.html)? In this way you can have only one controller that handles XML input, in the interceptor/advice you can manipulate the request body to be able to forward the expected XML. – m4gic Sep 03 '18 at 09:10
  • @m4gic Can you paste official answer with working example, please? – Peter Penzov Sep 03 '18 at 09:15
  • If you would like me to do that, please provide sample requests of the querystring version and XML version. The request parameters and the request body are important too. Please show an expected output (e.g. an XML, that this should be parsed from the request). I would like to know the spring-boot version and the JDK version too. However I can create an example only in the evening. – m4gic Sep 03 '18 at 09:19
  • Maybe I will be able to create something soon. – m4gic Sep 03 '18 at 09:26
  • Another question about PaymentTransaction: it is [jaxb-generated](https://stackoverflow.com/questions/26070566/returning-jaxb-generated-elements-from-spring-boot-controller) or just annotated with @JacksonXmlProperty/@JacksonXmlRootElement/etc.? (maybe bad question, from the applied messageconverters, I would deduce to the latter) – m4gic Sep 03 '18 at 09:56

3 Answers3

2

This is an another solution (it worked well for me) with less Spring magic and using the good old way of HttpServletRequestWrapper.

In the WebMvcConfigurerAdapter class, now we don't need the MessageConverter:

@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
    //MyRequestBodyHttpMessageConverter converter = new MyRequestBodyHttpMessageConverter();
    //FormHttpMessageConverter converter = new FormHttpMessageConverter();
    //MediaType utf8FormEncoded = new MediaType("application","x-www-form-urlencoded", Charset.forName("UTF-8"));
    //MediaType mediaType = MediaType.APPLICATION_FORM_URLENCODED; maybe UTF-8 is not needed
    //converter.setSupportedMediaTypes(Arrays.asList(MediaType.APPLICATION_FORM_URLENCODED));
    //converter.setSupportedMediaTypes(Arrays.asList(utf8FormEncoded));
    //converters.add(converter);
    converters.add(new MappingJackson2HttpMessageConverter());
    converters.add(new MappingJackson2XmlHttpMessageConverter());
    super.configureMessageConverters(converters);
}

And everything else happens in this (servlet) Filter implementation:

@WebFilter("/v1/notification")
public class MyRequestBodyFilter implements Filter {

    private static class MyServletInputStream extends ServletInputStream {

        private ByteArrayInputStream buffer;

        public MyServletInputStream(byte[] contents) {
            this.buffer = new ByteArrayInputStream(contents);
        }

        @Override
        public int read() throws IOException {
            return buffer.read();
        }

        @Override
        public boolean isFinished() {
            return buffer.available() == 0;
        }

        @Override
        public boolean isReady() {
            return true;
        }

        @Override
        public void setReadListener(ReadListener listener) {
            throw new RuntimeException("Not implemented");
        }
    }

    private class MyHttpServletRequestWrapper extends HttpServletRequestWrapper{

        MyHttpServletRequestWrapper(HttpServletRequest request) {
            super(request);
        }

        @Override
        public ServletInputStream getInputStream() throws IOException {
            // converting the request parameters to the pojo and serialize it to XML
            // the drawback of this way that the xml will be parsed again somewhere later
            long id = Long.parseLong(getRequest().getParameter("id"));
            String name = getRequest().getParameter("name");
            MyRequestBody body = new MyRequestBody();
            body.setId(id);
            body.setName(name);
            return new MyServletInputStream(new XmlMapper().writeValueAsBytes(body));
        }
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        chain.doFilter(new MyHttpServletRequestWrapper(httpRequest), response);
    }

    @Override
    public void destroy() {

    }
}

I have changed nothing in my test controller, so the signature of the methods remained the same:

@PostMapping(value = "/v1/{token}",
        consumes =  MediaType.APPLICATION_XML_VALUE,
        produces = MediaType.APPLICATION_JSON_VALUE)
public @ResponseBody  MyResponseBody handleMessage(@PathVariable("token") String token, @RequestBody MyRequestBody transaction, HttpServletRequest request) throws     Exception {
           MyResponseBody body = new MyResponseBody();
           body.setId(transaction.getId());
           body.setName("received " + transaction.getName());
           return body;
}

@PostMapping(consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, value = "/v1/notification")
public ResponseEntity<String> handleNotifications(@ModelAttribute MyRequestBody transaction) {
       return new ResponseEntity<String>(HttpStatus.OK);
}
m4gic
  • 1,461
  • 12
  • 19
  • I had to keep the APPLICATION_FORM_URLENCODED_VALUE and ModelAttribute on handleNotifications, probably if I modified the Content-type of the request to 'application/xml' in the filter, it would have worked on the same way as handleMessage (with RequestBody and with consumes = MediaType.APPLICATION_XML_VALUE). – m4gic Sep 03 '18 at 15:31
  • Thank you very much for the effort. In long term which solution would you recommend? Please check the updated post for your questions above. – Peter Penzov Sep 04 '18 at 06:06
  • I would go with the more Spring friendly approach, because it should be faster (you don't have to create the xml that will be parsed later). I would use the second approach as a backup plan if the Spring friendly version does not work for some reason (classpath, jaxb/marhalling problems). In the servlet wrapper I used XmlMapper that - I think - would work with Jackson related xml annotations well, so maybe there you should change the serialization if you use that. One advantage for the latter that is more generic and does not need to modify your DTOs (no need to extend with an interface). – m4gic Sep 04 '18 at 07:30
  • You mean the first approach? – Peter Penzov Sep 04 '18 at 07:37
  • Yes, I would go with the custom MessageConverter. In order to get it work, you will need to add a mail-api jar to the project dependencies (your spring-boot version uses the version 1.6.1). – m4gic Sep 04 '18 at 07:45
  • Just in case to know is there any solution with much less code? – Peter Penzov Sep 04 '18 at 07:47
  • IMHO there is not. If you think it over, you have a querystring that you would like to map to an XML (what is complex data structure with trees, structured lists, etc.). You have to specify how to map the data of your querystring to this data structure. This can be done with almost zero code, if your request contains your data in a parseable serialized form (Json and XML) but this case is different unfortunately. – m4gic Sep 04 '18 at 07:51
  • However if you can modify your client what will send to the controller, you can get rid of the querystring processing part, and you will have a cleaner controller, that accepts only XML (one can assemble XMLs at client side using javascript that can be added to request body too, not necessary posting the form data is the only option.) – m4gic Sep 04 '18 at 09:09
  • I can show you the project in Bitbucket if you need further info. Do you have e-mail account and Java 10 installed? – Peter Penzov Sep 04 '18 at 10:05
  • If won't work by copy-paste. You have to replace the MyRequestBody with your PaymentTransaction class. This class must implement RequestParamSupport (see my MyRequestBody) and this class must provide a setter (PaymentTransaction instance).setRequestParams(valueMap). I don't have Java 10 but I think it should work with smaller version too. If you write me a message I can give you my email account. – m4gic Sep 04 '18 at 10:15
  • I think I got it. Your problems because you are using version 2.0.4.RELEASE but I used version 1.x. Unfortunately at now I don't have so much free time to implement the same for 2.0.4, I can do it in the evening only :( – m4gic Sep 04 '18 at 10:19
  • Ok, I will wait. Thanks you! – Peter Penzov Sep 04 '18 at 10:33
2

For Spring boot 2.0.4-RELEASE, it seems you don't have to do a lot.

I made this configuration:

@Configuration
public class WebConfiguration implements WebMvcConfigurer {

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        //MyRequestBodyHttpMessageConverter converter = new MyRequestBodyHttpMessageConverter();
        FormHttpMessageConverter converter = new FormHttpMessageConverter();
        //MediaType utf8FormEncoded = new MediaType("application","x-www-form-urlencoded", Charset.forName("UTF-8"));
        //MediaType mediaType = MediaType.APPLICATION_FORM_URLENCODED; maybe UTF-8 is not needed
        converter.setSupportedMediaTypes(Arrays.asList(MediaType.APPLICATION_FORM_URLENCODED));
        //converter.setSupportedMediaTypes(Arrays.asList(utf8FormEncoded));
        converters.add(converter);
        MappingJackson2HttpMessageConverter conv1 = new MappingJackson2HttpMessageConverter();
        conv1.getObjectMapper().registerModule(new JaxbAnnotationModule());
        converters.add(conv1);

        MappingJackson2XmlHttpMessageConverter conv = new MappingJackson2XmlHttpMessageConverter();
        // required by jaxb annotations
        conv.getObjectMapper().registerModule(new JaxbAnnotationModule());
        converters.add(conv);
    }
}

I used about your DTO:

@XmlRootElement(name = "payment_transaction")
public class PaymentTransaction {

    @XmlElement(name = "transaction_type")
    public String transactionType;
    @XmlElement(name = "transaction_id")
    public String transactionId;

    public String getTransactionType() {
        return transactionType;
    }
    public void setTransactionType(String transactionType) {
        this.transactionType = transactionType;
    }
    public String getTransactionId() {
        return transactionId;
    }
    public void setTransactionId(String transactionId) {
        this.transactionId = transactionId;
    }
    @Override
    public String toString() {
        return "PaymentTransaction [transactionType=" + transactionType
                + ", transactionId=" + transactionId + "]";
    }
}

The controller:

@RestController
public class MyController {

    /**
     * https://stackoverflow.com/questions/34782025/http-post-request-with-content-type-application-x-www-form-urlencoded-not-workin/38252762#38252762
    */

    @PostMapping(value = "/v1/{token}",
            consumes =  MediaType.APPLICATION_XML_VALUE,
            produces = MediaType.APPLICATION_JSON_VALUE)
    public @ResponseBody  PaymentTransaction handleMessage(@PathVariable("token") String token,
            @RequestBody PaymentTransaction transaction, HttpServletRequest request) throws Exception {
           System.out.println("handleXmlMessage");
           System.out.println(transaction);
           PaymentTransaction body = new PaymentTransaction();
           body.setTransactionId(transaction.getTransactionId());
           body.setTransactionType("received: " + transaction.getTransactionType());
           return body;
    }

    @PostMapping(consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, value = "/v1/notification")
    public ResponseEntity<String> handleNotifications(@ModelAttribute PaymentTransaction transaction) {
           System.out.println("handleFormMessage");
           System.out.println(transaction);
           return new ResponseEntity<String>(HttpStatus.OK);
    }
 }

The only main thing to remember that it seems the filling of the DTO with the parsed data happens by reflection:

For your input

<payment_transaction>
  <transaction_id>1</transaction_id>
  <transaction_type>name</transaction_type>
</payment_transaction>

I got this response (see my controller):

{
"transactionType": "received: null",
"transactionId": null
}

But when I changed to the name of the fields of the DTO, it started to work (the root element did not matter, interesting):

<payment_transaction>
  <transactionId>1</transactionId>
  <transactionType>name</transactionType>
</payment_transaction>

result:

{
"transactionType": "received: name",
"transactionId": "1"
}

The same is true for the querystring. I don't know what to change to get spring to parse the xmls using the defined names in @XmlRootElement/@XmlElement.

m4gic
  • 1,461
  • 12
  • 19
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/179413/discussion-between-m4gic-and-peter-penzov). – m4gic Sep 04 '18 at 20:14
1

Update this solution works for pre-2.x Spring-boot versions. Another thing to consider that during my tests I used Jackson's XML annotations on my DTOs (JacksonXmlRootElement, JacksonXmlProperty) and maybe FormHttpMessageConverter can handle DTOs with standard JAXB annotations (see my answer for Spring 2.0.4-RELEASE) - so may you'd better to go to that direction if you can (or at least give it a try before you apply the sketched solution).

This is my solution. I dropped the RequestIntereptor (because that is rather for inspect the request not for modifying it) and the RequestBodyAdvice too (because it turned out that there is a better way.

If you have a look for the available MessageConverters you can see that the only MessageConverter that reads the posted form data is the FormHttpMessageConverter. The problem with this class is the return type, which is Multivaluemap

But, using this class as a base, I have created an abstract class that reads the form data to this Multivaluemap, and have only one abstract funtion that you have to implement in the subclass: that will create an object from the values stored in the multivaluemap.

Unfortunately I had to introduce an interface (because I kept the original implementation of the writing part just adopt it) on the DTO you would like to read.

All in all, my working solution:

In the WebMvcConfigurerAdapter class, I have this config:

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        MyRequestBodyHttpMessageConverter converter = new MyRequestBodyHttpMessageConverter();
        //FormHttpMessageConverter converter = new FormHttpMessageConverter();
        MediaType utf8FormEncoded = new MediaType("application","x-www-form-urlencoded", Charset.forName("UTF-8"));
        //MediaType mediaType = MediaType.APPLICATION_FORM_URLENCODED; maybe UTF-8 is not needed
        //converter.setSupportedMediaTypes(Arrays.asList(MediaType.APPLICATION_FORM_URLENCODED));
        converter.setSupportedMediaTypes(Arrays.asList(utf8FormEncoded));
        converters.add(converter);
        converters.add(new MappingJackson2HttpMessageConverter());
        converters.add(new MappingJackson2XmlHttpMessageConverter());
        super.configureMessageConverters(converters);
    }

I modified a bit your controller functions:

    @PostMapping(value = "/v1/{token}",
        consumes =  MediaType.APPLICATION_XML_VALUE,
        produces = MediaType.APPLICATION_JSON_VALUE)
    public @ResponseBody  MyResponseBody handleMessage(@PathVariable("token") String token, @RequestBody MyRequestBody transaction, HttpServletRequest request) throws  Exception {
       MyResponseBody body = new MyResponseBody();
       body.setId(transaction.getId());
       body.setName("received " + transaction.getName());
       return body;
     }

// check @ModelAttribute workaround https://stackoverflow.com/questions/4339207/http-post-with-request-content-type-form-not-working-in-spring-mvc-3

    @PostMapping(consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, value = "/v1/notification")
    public ResponseEntity<String> handleNotifications(@ModelAttribute MyRequestBody transaction) {
       return new ResponseEntity<String>(HttpStatus.OK);
    }

(in the next part the import packages are meaningful, some mail api classes can be found somewhere else)

import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.mail.internet.MimeUtility;

import org.springframework.core.io.Resource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.StreamingHttpOutputMessage;
import org.springframework.http.converter.AbstractHttpMessageConverter;
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.http.converter.ResourceHttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MimeTypeUtils;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StreamUtils;
import org.springframework.util.StringUtils;


/**
 * based on {@link org.springframework.http.converter.FormHttpMessageConverter
 *
 * it uses the readed MultiValueMap to build up the DTO we would like to get from the request body.
 */

public abstract class AbstractRequestBodyFormHttpMessageConverter<T extends RequestParamSupport> implements HttpMessageConverter<T> {

    /**
    * This is the only method you have to implement for your DTO class
    * the class must implement RequestParamSupport
    */    
    protected abstract T buildObject(MultiValueMap<String, Object> valueMap);

    public interface RequestParamSupport{
        MultiValueMap<String, Object> getRequestParams();
    }


    public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");

    private List<MediaType> supportedMediaTypes = new ArrayList<MediaType>();

    private List<HttpMessageConverter<?>> partConverters = new ArrayList<HttpMessageConverter<?>>();

    private Charset charset = DEFAULT_CHARSET;

    private Charset multipartCharset;

    private Class<T> bodyClass;

    public AbstractRequestBodyFormHttpMessageConverter(Class<T> bodyClass) {
        this.bodyClass = bodyClass;
        this.supportedMediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED);
        this.supportedMediaTypes.add(MediaType.MULTIPART_FORM_DATA);

        StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter();
        stringHttpMessageConverter.setWriteAcceptCharset(false);  // see SPR-7316

        this.partConverters.add(new ByteArrayHttpMessageConverter());
        this.partConverters.add(stringHttpMessageConverter);
        this.partConverters.add(new ResourceHttpMessageConverter());

        applyDefaultCharset();
    }

    /**
     * Set the character set to use when writing multipart data to encode file
     * names. Encoding is based on the encoded-word syntax defined in RFC 2047
     * and relies on {@code MimeUtility} from "javax.mail".
     * <p>If not set file names will be encoded as US-ASCII.
     * @since 4.1.1
     * @see <a href="http://en.wikipedia.org/wiki/MIME#Encoded-Word">Encoded-Word</a>
     */
    public void setMultipartCharset(Charset charset) {
        this.multipartCharset = charset;
    }

    /**
     * Apply the configured charset as a default to registered part converters.
     */
    private void applyDefaultCharset() {
        for (HttpMessageConverter<?> candidate : this.partConverters) {
            if (candidate instanceof AbstractHttpMessageConverter) {
                AbstractHttpMessageConverter<?> converter = (AbstractHttpMessageConverter<?>) candidate;
                // Only override default charset if the converter operates with a charset to begin with...
                if (converter.getDefaultCharset() != null) {
                    converter.setDefaultCharset(this.charset);
                }
            }
        }
    }


    @Override
    public boolean canRead(Class<?> clazz, MediaType mediaType) {
        if (!bodyClass.isAssignableFrom(clazz)) {
            return false;
        }
        if (mediaType == null) {
            return true;
        }
        for (MediaType supportedMediaType : getSupportedMediaTypes()) {
            // We can't read multipart....
            if (!supportedMediaType.equals(MediaType.MULTIPART_FORM_DATA) && supportedMediaType.includes(mediaType)) {
                return true;
            }
        }
        return false;
    }

    @Override
    public boolean canWrite(Class<?> clazz, MediaType mediaType) {
        if (!bodyClass.isAssignableFrom(clazz)) {
            return false;
        }
        if (mediaType == null || MediaType.ALL.equals(mediaType)) {
            return true;
        }
        for (MediaType supportedMediaType : getSupportedMediaTypes()) {
            if (supportedMediaType.isCompatibleWith(mediaType)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Set the list of {@link MediaType} objects supported by this converter.
     */
    public void setSupportedMediaTypes(List<MediaType> supportedMediaTypes) {
        this.supportedMediaTypes = supportedMediaTypes;
    }

    @Override
    public List<MediaType> getSupportedMediaTypes() {
        return Collections.unmodifiableList(this.supportedMediaTypes);
    }

    @Override
    public T read(Class<? extends T> clazz, HttpInputMessage inputMessage)
            throws IOException, HttpMessageNotReadableException {
        MediaType contentType = inputMessage.getHeaders().getContentType();
        Charset charset = (contentType.getCharset() != null ? contentType.getCharset() : this.charset);
        String body = StreamUtils.copyToString(inputMessage.getBody(), charset);

        String[] pairs = StringUtils.tokenizeToStringArray(body, "&");
        MultiValueMap<String, Object> result = new LinkedMultiValueMap<String, Object>(pairs.length);
        for (String pair : pairs) {
            int idx = pair.indexOf('=');
            if (idx == -1) {
                result.add(URLDecoder.decode(pair, charset.name()), null);
            }
            else {
                String name = URLDecoder.decode(pair.substring(0, idx), charset.name());
                String value = URLDecoder.decode(pair.substring(idx + 1), charset.name());
                result.add(name, value);
            }
        }
        return buildObject(result);
    }

    @Override
    public void write(T object, MediaType contentType,
            HttpOutputMessage outputMessage) throws IOException,
            HttpMessageNotWritableException {
        if (!isMultipart(object, contentType)) {
            writeForm(object.getRequestParams(), contentType, outputMessage);
        }
        else {
            writeMultipart(object.getRequestParams(), outputMessage);
        }
    }

    private boolean isMultipart(RequestParamSupport object, MediaType contentType) {
        if (contentType != null) {
            return MediaType.MULTIPART_FORM_DATA.includes(contentType);
        }
        MultiValueMap<String, Object> map = object.getRequestParams();
        for (String name : map.keySet()) {
            for (Object value : map.get(name)) {
                if (value != null && !(value instanceof String)) {
                    return true;
                }
            }
        }
        return false;
    }

    private void writeForm(MultiValueMap<String, Object> form, MediaType contentType,
            HttpOutputMessage outputMessage) throws IOException {

        Charset charset;
        if (contentType != null) {
            outputMessage.getHeaders().setContentType(contentType);
            charset = (contentType.getCharset() != null ? contentType.getCharset() : this.charset);
        }
        else {
            outputMessage.getHeaders().setContentType(MediaType.APPLICATION_FORM_URLENCODED);
            charset = this.charset;
        }
        StringBuilder builder = new StringBuilder();
        for (Iterator<String> nameIterator = form.keySet().iterator(); nameIterator.hasNext();) {
            String name = nameIterator.next();
            for (Iterator<Object> valueIterator = form.get(name).iterator(); valueIterator.hasNext();) {
                String value = (String) valueIterator.next();
                builder.append(URLEncoder.encode(name, charset.name()));
                if (value != null) {
                    builder.append('=');
                    builder.append(URLEncoder.encode(value, charset.name()));
                    if (valueIterator.hasNext()) {
                        builder.append('&');
                    }
                }
            }
            if (nameIterator.hasNext()) {
                builder.append('&');
            }
        }
        final byte[] bytes = builder.toString().getBytes(charset.name());
        outputMessage.getHeaders().setContentLength(bytes.length);

        if (outputMessage instanceof StreamingHttpOutputMessage) {
            StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) outputMessage;
            streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() {
                @Override
                public void writeTo(OutputStream outputStream) throws IOException {
                    StreamUtils.copy(bytes, outputStream);
                }
            });
        }
        else {
            StreamUtils.copy(bytes, outputMessage.getBody());
        }
    }

    private void writeMultipart(final MultiValueMap<String, Object> parts, HttpOutputMessage outputMessage) throws IOException {
        final byte[] boundary = generateMultipartBoundary();
        Map<String, String> parameters = Collections.singletonMap("boundary", new String(boundary, "US-ASCII"));

        MediaType contentType = new MediaType(MediaType.MULTIPART_FORM_DATA, parameters);
        HttpHeaders headers = outputMessage.getHeaders();
        headers.setContentType(contentType);

        if (outputMessage instanceof StreamingHttpOutputMessage) {
            StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) outputMessage;
            streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() {
                @Override
                public void writeTo(OutputStream outputStream) throws IOException {
                    writeParts(outputStream, parts, boundary);
                    writeEnd(outputStream, boundary);
                }
            });
        }
        else {
            writeParts(outputMessage.getBody(), parts, boundary);
            writeEnd(outputMessage.getBody(), boundary);
        }
    }

    private void writeParts(OutputStream os, MultiValueMap<String, Object> parts, byte[] boundary) throws IOException {
        for (Map.Entry<String, List<Object>> entry : parts.entrySet()) {
            String name = entry.getKey();
            for (Object part : entry.getValue()) {
                if (part != null) {
                    writeBoundary(os, boundary);
                    writePart(name, getHttpEntity(part), os);
                    writeNewLine(os);
                }
            }
        }
    }

    @SuppressWarnings("unchecked")
    private void writePart(String name, HttpEntity<?> partEntity, OutputStream os) throws IOException {
        Object partBody = partEntity.getBody();
        Class<?> partType = partBody.getClass();
        HttpHeaders partHeaders = partEntity.getHeaders();
        MediaType partContentType = partHeaders.getContentType();
        for (HttpMessageConverter<?> messageConverter : this.partConverters) {
            if (messageConverter.canWrite(partType, partContentType)) {
                HttpOutputMessage multipartMessage = new MultipartHttpOutputMessage(os);
                multipartMessage.getHeaders().setContentDispositionFormData(name, getFilename(partBody));
                if (!partHeaders.isEmpty()) {
                    multipartMessage.getHeaders().putAll(partHeaders);
                }
                ((HttpMessageConverter<Object>) messageConverter).write(partBody, partContentType, multipartMessage);
                return;
            }
        }
        throw new HttpMessageNotWritableException("Could not write request: no suitable HttpMessageConverter " +
                "found for request type [" + partType.getName() + "]");
    }


    /**
     * Generate a multipart boundary.
     * <p>This implementation delegates to
     * {@link MimeTypeUtils#generateMultipartBoundary()}.
     */
    protected byte[] generateMultipartBoundary() {
        return MimeTypeUtils.generateMultipartBoundary();
    }

    /**
     * Return an {@link HttpEntity} for the given part Object.
     * @param part the part to return an {@link HttpEntity} for
     * @return the part Object itself it is an {@link HttpEntity},
     * or a newly built {@link HttpEntity} wrapper for that part
     */
    protected HttpEntity<?> getHttpEntity(Object part) {
        return (part instanceof HttpEntity ? (HttpEntity<?>) part : new HttpEntity<Object>(part));
    }

    /**
     * Return the filename of the given multipart part. This value will be used for the
     * {@code Content-Disposition} header.
     * <p>The default implementation returns {@link Resource#getFilename()} if the part is a
     * {@code Resource}, and {@code null} in other cases. Can be overridden in subclasses.
     * @param part the part to determine the file name for
     * @return the filename, or {@code null} if not known
     */
    protected String getFilename(Object part) {
        if (part instanceof Resource) {
            Resource resource = (Resource) part;
            String filename = resource.getFilename();
            if (filename != null && this.multipartCharset != null) {
                filename = MimeDelegate.encode(filename, this.multipartCharset.name());
            }
            return filename;
        }
        else {
            return null;
        }
    }


    private void writeBoundary(OutputStream os, byte[] boundary) throws IOException {
        os.write('-');
        os.write('-');
        os.write(boundary);
        writeNewLine(os);
    }

    private static void writeEnd(OutputStream os, byte[] boundary) throws IOException {
        os.write('-');
        os.write('-');
        os.write(boundary);
        os.write('-');
        os.write('-');
        writeNewLine(os);
    }

    private static void writeNewLine(OutputStream os) throws IOException {
        os.write('\r');
        os.write('\n');
    }


    /**
     * Implementation of {@link org.springframework.http.HttpOutputMessage} used
     * to write a MIME multipart.
     */
    private static class MultipartHttpOutputMessage implements HttpOutputMessage {

        private final OutputStream outputStream;

        private final HttpHeaders headers = new HttpHeaders();

        private boolean headersWritten = false;

        public MultipartHttpOutputMessage(OutputStream outputStream) {
            this.outputStream = outputStream;
        }

        @Override
        public HttpHeaders getHeaders() {
            return (this.headersWritten ? HttpHeaders.readOnlyHttpHeaders(this.headers) : this.headers);
        }

        @Override
        public OutputStream getBody() throws IOException {
            writeHeaders();
            return this.outputStream;
        }

        private void writeHeaders() throws IOException {
            if (!this.headersWritten) {
                for (Map.Entry<String, List<String>> entry : this.headers.entrySet()) {
                    byte[] headerName = getAsciiBytes(entry.getKey());
                    for (String headerValueString : entry.getValue()) {
                        byte[] headerValue = getAsciiBytes(headerValueString);
                        this.outputStream.write(headerName);
                        this.outputStream.write(':');
                        this.outputStream.write(' ');
                        this.outputStream.write(headerValue);
                        writeNewLine(this.outputStream);
                    }
                }
                writeNewLine(this.outputStream);
                this.headersWritten = true;
            }
        }

        private byte[] getAsciiBytes(String name) {
            try {
                return name.getBytes("US-ASCII");
            }
            catch (UnsupportedEncodingException ex) {
                // Should not happen - US-ASCII is always supported.
                throw new IllegalStateException(ex);
            }
        }
    }


    /**
     * Inner class to avoid a hard dependency on the JavaMail API.
     */
    private static class MimeDelegate {

        public static String encode(String value, String charset) {
            try {
                return MimeUtility.encodeText(value, charset, null);
            }
            catch (UnsupportedEncodingException ex) {
                throw new IllegalStateException(ex);
            }
        }
    }
}

The bean converter implementation

public class MyRequestBodyHttpMessageConverter extends
        AbstractRequestBodyFormHttpMessageConverter<MyRequestBody> {

    public MyRequestBodyHttpMessageConverter() {
        super(MyRequestBody.class);
    }

    @Override
    protected MyRequestBody buildObject(MultiValueMap<String, Object> valueMap) {
        MyRequestBody parsed = new MyRequestBody();
        parsed.setId(Long.valueOf((String)valueMap.get("id").get(0)));
        parsed.setName((String)valueMap.get("name").get(0));
        parsed.setRequestParams(valueMap);
        return parsed;
    }
}

And finally the MyRequestBody DTO (the MyRequestBody was the same just with different name)

@JacksonXmlRootElement
public class MyRequestBody implements RequestParamSupport, Serializable {

    @JsonIgnore
    private transient MultiValueMap<String, Object> requestParams;

    @JacksonXmlProperty
    private Long id;
    @JacksonXmlProperty
    private String name;

    //empty constructor, getters, setters, tostring, etc

    @Override
    public MultiValueMap<String, Object> getRequestParams() {
        return requestParams;
    }
}

** Finally my answers: **

How I configure Spring to accept both types?

As you can see, you have to have your own form-data to your bean converter. (Do not forget that you have to use @ModelAttribute when you are mapping from form data and not @RequestBody.)

Also should I split the Rest controller into different files?

No, that is not necessary, just register your converter.

m4gic
  • 1,461
  • 12
  • 19
  • The only problem with this, that this is not happen "under the hood", you have to implement your mapping in the AbstractRequestBodyFormHttpMessageConverter subclass and you have to register it in order to use. But at least it works. – m4gic Sep 03 '18 at 13:46
  • In addition I have to tell you that I tested only with the application/x-www-form-urlencoded header, and not with multipart/form-data. – m4gic Sep 03 '18 at 13:49