0

I want to create REST Server which accepts XML requests and plain text from Ruby code 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);
    }
    ..............

    @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();
    }    
}

@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);
    }
}

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 and Wildfly 13.

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
  • Please mention that the target platform/appserver is Wildfly (and the version too). And because of this, you may need to [influence](https://stackoverflow.com/questions/37196082/deploy-spring-boot-to-wildfly-10) Wildfly's classloader to use your bundled libraries instead of Wildfly own modules and jars. – m4gic Sep 05 '18 at 09:30
  • Updated. Any proposal how to implement this feature? – Peter Penzov Sep 05 '18 at 10:51
  • In the evening I will try to reproduce in Wildfly. – m4gic Sep 05 '18 at 10:55

1 Answers1

0

When deployed as a WAR application on WildFly, Spring Boot applications might require some advanced JDK reflection API for proxying, included in the sun.reflect package. For that, your application needs to list jdk.unsupported as a Dependency in its MANIFEST.MF file (see the WildFly wiki about this).

source - so I applied this first.

<plugin>
    <artifactId>maven-war-plugin</artifactId>
    <version>3.2.2</version>
    <configuration>
        <packagingExcludes>WEB-INF/web.xml</packagingExcludes>
        <archive>
            <manifestEntries>
                <Dependencies>jdk.unsupported</Dependencies>
            </manifestEntries>
        </archive>
    </configuration>
</plugin>

Next, I fix the logging: I applied the first comment of the accepted answer here for logging and added a basic logback.xml from here to the src/main/resources.

So my basic jboss-deployment-structure.xml is the following:

<?xml version="1.0" encoding="UTF-8"?>
<jboss-deployment-structure>
    <deployment>
        <exclude-subsystems>
            <subsystem name="logging" />
        </exclude-subsystems>
    </deployment>
</jboss-deployment-structure>

From now, in the Wildfly console you can see what happen.

I tested a POC in two distribution:

Servlet-Only Distribution (wildfly-servlet-13.0.0.Final.zip)

This worked with the basic jboss-deployment-structure.xml.

Application Server Distribution (wildfly-13.0.0.Final.zip)

In this server, I had to add the org.reactivestreams module:

<?xml version="1.0" encoding="UTF-8"?>
<jboss-deployment-structure>
    <deployment>
        <exclude-subsystems>
            <subsystem name="logging" />
        </exclude-subsystems>
        <dependencies>
            <module name="org.reactivestreams"/>
        </dependencies>
    </deployment>
</jboss-deployment-structure>

You have to put this file to the src/main/webapp/WEB-INF directory.

If you need to use support different content-type than application/x-www-form-urlencoded, you can do it by registering a proper MessageConverter to your content-type.

In your case because of the querystring in request body, the FormHttpMessageConverter would be the 'proper', and you have to specify your content-type as MY_OTHER_CONTENT_TYPE constant:

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

However the used converter should be able to handle it, so you have to check the source of the converter (I did, and in case of FormHttpMessageConverter, it should work in this way) I think (or you can try it out, and if it works, works). Sometimes this is not so simple, but I hope that for you it will work in this way.

If you could not solve it, please, give us a full sample of the problematic request and I will try to get it work.

To the testing

Please be really sure that you are using the Content-Type: application/x-www-form-urlencoded header with POST method, and your parameters are travelling in the request body. (I have used Boomerang Chrome plugin for testing and in there when I defined request parameters for a post request, it added to the request URL as get parameters. This obviously leaded to wrong result.)

I hope these will help you out. If not, after you apply the proper logging settings, you will be able to show us why (please update your post with the exception).

m4gic
  • 1,461
  • 12
  • 19
  • Thank you very much. Is there a way to receive the data without `Content-Type: application/x-www-form-urlencoded`? – Peter Penzov Sep 06 '18 at 09:03
  • I have shared my POC application via gdrive share, you can examine its configuration, pom, etc. I used the standard FormHttpMessageConverter I am afraid. – m4gic Sep 06 '18 at 09:07
  • I already did. It's working but only with `Content-Type: application/x-www-form-urlencoded` into the header. – Peter Penzov Sep 06 '18 at 09:10