4

I'm generating a plethora of Java files from http://www.ncpdp.org's XSD files (only available to members). After generating them, I'd like to use them in my Spring Controllers, but I'm having problems getting responses converted to XML.

I've tried returning the element itself, as well as JAXBElement<T>, but neither seems to work. The test below fails:

java.lang.AssertionError: Status 
Expected :200
Actual   :406

@Test
public void testHelloWorld() throws Exception {
    mockMvc.perform(get("/api/message")
            .accept(MediaType.APPLICATION_XML)
            .contentType(MediaType.APPLICATION_XML))
            .andExpect(status().isOk())
            .andExpect(content().contentType(MediaType.APPLICATION_XML));
}

Here's my Controller:

import org.ncpdp.schema.transport.MessageType;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloWorldController {

    @RequestMapping(value = "/api/message", method = RequestMethod.GET)
    public MessageType messageType() {
        return new MessageType();
    }
}

I've tried creating a MvcConfig to override Spring Boot's MVC config, but it doesn't seem to be working.

@Configuration
@EnableWebMvc
public class MvcConfig extends WebMvcConfigurerAdapter {

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.add(marshallingHttpMessageConverter());
    }

    @Bean
    public MarshallingHttpMessageConverter marshallingHttpMessageConverter() {
        Jaxb2Marshaller jaxb2Marshaller = new Jaxb2Marshaller();
        jaxb2Marshaller.setPackagesToScan(new String[]{"com.ncpdb.schema.transport"});

        MarshallingHttpMessageConverter converter = new MarshallingHttpMessageConverter();
        converter.setMarshaller(jaxb2Marshaller);
        converter.setUnmarshaller(jaxb2Marshaller);
        converter.setSupportedMediaTypes(Arrays.asList(MediaType.APPLICATION_XML));

        return converter;
    }
}

What do I need to do to get Spring MVC to marshall my generated JAXB objects as XML?

Matt Raible
  • 5,905
  • 8
  • 47
  • 98
  • Question: Does the generated `MessageType` have an `@XmlRootElement` annotation on it. It looks like the default xml converter `Jaxb2RootElementHttpMessageConverter` requires this annotation to consider an instance of `MessageType` to be supported for marshalling – Biju Kunjummen Sep 27 '14 at 02:55
  • @BijuKunjummen is right! Check out https://github.com/spring-projects/spring-boot/issues/407 – geoand Sep 27 '14 at 05:49
  • Can a GET really have a content-type of application/xml? Maybe it's the request that can't be converted? – Dave Syer Sep 27 '14 at 07:21
  • Biju - it does not, and if I try to add it, I get an error. It does have the following annotations: @XmlAccessorType(XmlAccessType.FIELD) @XmlType(name = "MessageType", propOrder = { "header", "body" }) public class MessageType. If I try to add it, there's a compiler error. FWIW, the XSD -> Java creates 350+ classes and I'd prefer not to touch them. – Matt Raible Sep 27 '14 at 16:15
  • @DaveSyer - if I remove the contentType line, I get the same error. I found I needed that when doing a post. – Matt Raible Sep 27 '14 at 16:18
  • I assume there is some way to configure the class generation so that the class you need gets `@XmlRootElement`. Seems like this is really a JAX-B question though so I'm signing off. – Dave Syer Sep 27 '14 at 16:33
  • The XmlRootElement isn't always generated by JAXB, as noted in http://stackoverflow.com/questions/819720/no-xmlrootelement-generated-by-jaxb. AFAICT, it should be possible to tell Spring to use the generated ObjectFactory instead. This is what I was hoping to do with the Jaxb2Marshaller configuration. – Matt Raible Sep 27 '14 at 19:04
  • Strange when returning a `JAXBElement` the `Jaxb2RootElementHttpMessageConverter` should be able to do its work as it will simply delegate to a normal JAXB `Marshaller`, which in turn should use a `ObjectFactory` to do it's work. You have tried the default and returning a `JAXBElement`? – M. Deinum Sep 29 '14 at 05:49
  • @M.Deinum - Yes, I've tried this. It results in the same error. https://gist.github.com/mraible/f409e6715ed44dd721c2 – Matt Raible Sep 29 '14 at 13:11
  • I actually meant without the `MvcConfig` just letting Spring Boot do its thing. Which should register the `Jaxb2RootElementHttpMessageConverter`. – M. Deinum Sep 29 '14 at 13:18
  • If I remove the MvcConfig, the error is the same. – Matt Raible Sep 29 '14 at 14:04

1 Answers1

3

I was able to solve this by creating a bindings.xjb file in the same directory as my schemas. This causes JAXB to generate @XmlRootElement on classes.

<?xml version="1.0"?>
<jxb:bindings version="1.0"
              xmlns:xsd="http://www.w3.org/2001/XMLSchema"
              xmlns:jxb="http://java.sun.com/xml/ns/jaxb"
              xmlns:xjc="http://java.sun.com/xml/ns/jaxb/xjc"
              xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
              xsi:schemaLocation="http://java.sun.com/xml/ns/jaxb http://java.sun.com/xml/ns/jaxb/bindingschema_2_0.xsd">

    <jxb:bindings schemaLocation="transport.xsd" node="/xsd:schema">
        <jxb:globalBindings>
            <xjc:simple/>
        </jxb:globalBindings>
    </jxb:bindings>
</jxb:bindings>

To add namespaces prefixes to the returned XML, I had to modify the maven-jaxb2-plugin to add a couple arguments.

<arg>-extension</arg>
<arg>-Xnamespace-prefix</arg>

And add a dependency:

<dependencies>
    <dependency>
        <groupId>org.jvnet.jaxb2_commons</groupId>
        <artifactId>jaxb2-namespace-prefix</artifactId>
        <version>1.1</version>
    </dependency>
</dependencies>

Then modify my bindings.xjb to include this:

<?xml version="1.0"?>
<jxb:bindings version="1.0"
              xmlns:xsd="http://www.w3.org/2001/XMLSchema"
              xmlns:jxb="http://java.sun.com/xml/ns/jaxb"
              xmlns:xjc="http://java.sun.com/xml/ns/jaxb/xjc"
              xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
              xmlns:namespace="http://jaxb2-commons.dev.java.net/namespace-prefix"
              xsi:schemaLocation="http://java.sun.com/xml/ns/jaxb http://java.sun.com/xml/ns/jaxb/bindingschema_2_0.xsd
              http://jaxb2-commons.dev.java.net/namespace-prefix http://java.net/projects/jaxb2-commons/sources/svn/content/namespace-prefix/trunk/src/main/resources/prefix-namespace-schema.xsd">

    <jxb:bindings schemaLocation="transport.xsd" node="/xsd:schema">
        <jxb:globalBindings>
            <xjc:simple/>
        </jxb:globalBindings>

        <jxb:schemaBindings>
            <jxb:package name="org.ncpdp.schema.transport"/>
        </jxb:schemaBindings>
        <jxb:bindings>
            <namespace:prefix name="transport"/>
        </jxb:bindings>
    </jxb:bindings>
</jxb:bindings>

I learned how to do this from https://java.net/projects/jaxb2-commons/pages/Namespace-prefix. I also found http://blog.frankel.ch/customize-your-jaxb-bindings to be a good resource on how to customize JAXB bindings.

Matt Raible
  • 5,905
  • 8
  • 47
  • 98