3
votes

I am posting a JAXB object to a REST service. The generated class does not have an XMLRootElement, therefore I am creating it using the Object Factory createXMl method. When I manually add the XMLRootElement it works, but that is just a workaround as the JAXB classes are always generated without the XMLRootElement. There seems to be some problem with the XMl that is marshalled while posting the request.

    HttpHeaders headers = new HttpHeaders();
    headers.setContentType(MediaType.APPLICATION_XML);
    String userAndPass = "Test:Test123";
    headers.add("Authorization", "Basic " + Base64Utility.encode(userAndPass.getBytes()));

    JAXBElement<DocumentDef> documentDef = PrintFactory.createPrintObjects();

    HttpEntity<JAXBElement<DocumentDef>> request = new HttpEntity<>(documentDef, headers);

    MultiValueMap<String, String> map = new LinkedMultiValueMap<String, String>();
    map.add("lang", "2");

    ResponseEntity<String> result = restTemplate.exchange(url, HttpMethod.POST, request, String.class, map);

PrintFactory.java

      public JAXBElement<DocumentDef> createPrintObjects() {

       DocumentDef documentDef = new DocumentDef();
       JAXBElement<DocumentDef> documentDefJAXBElement = factory.createXml(documentDef);
       return documentDefJAXBElement;
       }

ObjectFactory.java

        /**
 * Create an instance of {@link JAXBElement }{@code <}{@link DocumentDef }{@code >}}
 * 
 */
@XmlElementDecl(namespace = "http://www.example.com/testservice", name = "xml")
public JAXBElement<DocumentDef> createXml(DocumentDef value) {
    return new JAXBElement<DocumentDef>(_Xml_QNAME, DocumentDef.class, null, value);
}

Error:

org.springframework.web.client.RestClientException: Could not write request: no suitable HttpMessageConverter found for request type [javax.xml.bind.JAXBElement] and content type [application/xml]
at org.springframework.web.client.RestTemplate$HttpEntityRequestCallback.doWithRequest(RestTemplate.java:859)
at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:617)
at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:588)
at org.springframework.web.client.RestTemplate.exchange(RestTemplate.java:507)

The below HttpMessageConverters are already registered :

org.springframework.http.converter.ByteArrayHttpMessageConverter@68022358, org.springframework.http.converter.StringHttpMessageConverter@7b3a8b9f, org.springframework.http.converter.StringHttpMessageConverter@645e9bc0, org.springframework.http.converter.ResourceHttpMessageConverter@7f438dba, org.springframework.http.converter.xml.SourceHttpMessageConverter@2c0def9c, org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter@46ee015c, org.springframework.http.converter.json.MappingJackson2HttpMessageConverter@2c833e50, org.springframework.http.converter.json.MappingJackson2HttpMessageConverter@339b6365, org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter@1e9a965b
4

4 Answers

0
votes

Well, that's true that it doesn't work for you because of:

JAXBElement<DocumentDef> documentDef = PrintFactory.createPrintObjects();
HttpEntity<JAXBElement<DocumentDef>> request = new HttpEntity<>(documentDef, headers);

meanwhile Jaxb2RootElementHttpMessageConverter is for this:

* <p>This converter can read classes annotated with {@link XmlRootElement} and
* {@link XmlType}, and write classes annotated with {@link XmlRootElement},
* or subclasses thereof.

and

public boolean canWrite(Class<?> clazz, MediaType mediaType) {
    return (AnnotationUtils.findAnnotation(clazz, XmlRootElement.class) != null && canWrite(mediaType));
}

when your JAXBElement is exactly without @XmlRootElement.

Try to figure out the solution without JAXBElement wrapper.

0
votes

I have fixed this issue by creating a .xjb file which automatically appends the XMLRootElement annotation to the parent java class while generating as below:

<?xml version="1.0" encoding="UTF-8"?>
<jxb:bindings xmlns:jxb="http://java.sun.com/xml/ns/jaxb" xmlns:xjc="http://java.sun.com/xml/ns/jaxb/xjc"
          xmlns:xs="http://www.w3.org/2001/XMLSchema"
          jxb:extensionBindingPrefixes="xjc" version="1.0">
<jxb:bindings schemaLocation="mySchema.xsd" node="/xs:schema">
    <jxb:globalBindings>
        <xjc:simple/>
    </jxb:globalBindings>
</jxb:bindings>

0
votes

If you need to post a jaxb object that doesn't have a XMLRootElement annotation using the RestTemplate, a solution is to use a custom HttpMessageConverter :

// jaxb context specific to your xml elements
JAXBContext jaxbContext =
    JAXBContext.newInstance(ObjectFactory.class.getPackage().getName());

// custom converter (see declaration below)
JaxbElementHttpMessageConverter converter = new JaxbElementHttpMessageConverter();
converter.setJaxbContext(jaxbContext);

RestTemplate template = new RestTemplate();
template.getMessageConverters().add(converter); // <--- here

// your object needs to be wrapped around a JAXBElement
YourRequestObject yourObject = ...;
JAXBElement<YourRequestObject> element =
    new ObjectFactory().createYourRequestObject(request);

HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_XML);

ResponseEntity<YourResponseObject> response = 
    template.postForEntity("/your/url", 
                           HttpEntity<>(element, headers), 
                           YourResponseObject.class, 
                           params);

Note that this converter will try to convert any object wrapped into a JAXBElement, so you need :

  1. to make sure any element you through at it can be marshalled/unmarshalled by the jaxb context you specified ;
  2. add it at the end of the message converters list so more specific message converters are tried first.

The JaxbElementHttpMessageConverter is defined as follow :

/**
* Implementation of {@link org.springframework.http.converter.HttpMessageConverter HttpMessageConverter} that can read and write XML using JAXB2.
*
* <p>This converter can read and write classes not annotated with {@link XmlRootElement} as long as they are wrapped into a {@link JAXBElement} and that a specific Jaxb context is provided.
*/
public class JaxbElementHttpMessageConverter extends AbstractJaxb2HttpMessageConverter<Object> {

        private JAXBContext jaxbContext;
        private EntityResolver entityResolver = NO_OP_ENTITY_RESOLVER;

        public void setJaxbContext(JAXBContext jaxbContext) {
                this.jaxbContext = jaxbContext;
        }

        public void setEntityResolver(EntityResolver entityResolver) {
                this.entityResolver = entityResolver;
        }

        @Override
        public boolean canRead(Class<?> clazz, MediaType mediaType) {
                return clazz.isAssignableFrom(JAXBElement.class) && canRead(mediaType);
        }

        @Override
        public boolean canWrite(Class<?> clazz, MediaType mediaType) {
                return clazz.isAssignableFrom(JAXBElement.class) && canWrite(mediaType);
        }

        @Override
        protected boolean supports(Class<?> clazz) {
                // should not be called, since we override canRead/Write
                throw new UnsupportedOperationException();
        }

        @Override
        protected Object readFromSource(Class<?> clazz, HttpHeaders headers, Source source) {
                try {
                        source = processSource(source);
                        Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
                        JAXBElement<?> jaxbElement = unmarshaller.unmarshal(source, clazz);
                        return jaxbElement.getValue();
                }
                catch (UnmarshalException ex) {
                        throw new HttpMessageNotReadableException("Could not unmarshal to [" + clazz + "]: " + ex.getMessage(), ex);

                }
                catch (JAXBException ex) {
                        throw new HttpMessageConversionException("Could not instantiate JAXBContext: " + ex.getMessage(), ex);
                }
        }

        protected Source processSource(Source source) {
                if (source instanceof StreamSource) {
                        StreamSource streamSource = (StreamSource) source;
                        InputSource inputSource = new InputSource(streamSource.getInputStream());
                        try {
                                XMLReader xmlReader = XMLReaderFactory.createXMLReader();
                                xmlReader.setEntityResolver(entityResolver);
                                return new SAXSource(xmlReader, inputSource);
                        }
                        catch (SAXException ex) {
                                logger.warn("Processing of external entities could not be disabled", ex);
                                return source;
                        }
                }
                else {
                        return source;
                }
        }

        @Override
        protected void writeToResult(Object o, HttpHeaders headers, Result result) {
                try {
                        Marshaller marshaller = jaxbContext.createMarshaller();
                        setCharset(headers.getContentType(), marshaller);
                        marshaller.marshal(o, result);
                }
                catch (MarshalException ex) {
                        throw new HttpMessageNotWritableException("Could not marshal [" + o + "]: " + ex.getMessage(), ex);
                }
                catch (JAXBException ex) {
                        throw new HttpMessageConversionException("Could not instantiate JAXBContext: " + ex.getMessage(), ex);
                }
        }

        private void setCharset(MediaType contentType, Marshaller marshaller) throws PropertyException {
                if (contentType != null && contentType.getCharset() != null) {
                        marshaller.setProperty(Marshaller.JAXB_ENCODING, contentType.getCharset().name());
                }
        }

        private static final EntityResolver NO_OP_ENTITY_RESOLVER = new EntityResolver() {
                @Override
                public InputSource resolveEntity(String publicId, String systemId) {
                        return new InputSource(new StringReader(""));
                }
        };

}
0
votes

The issue is related to canWrite and canRead methods from MarshallingHttpMessageConverter class. The methods checks if the marshaller and unmarshaller supports a class. In my case enabling jaxb element support helped.


MarshallingHttpMessageConverter converter = new   MarshallingHttpMessageConverter();
Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
marshaller.setSupportJaxbElementClass(true);
marshaller.setContextPath(ObjectFactory.class.getPackage().getName());
converter.setMarshaller(marshaller);

RestTemplate restTemplate = new RestTemplate();
restTemplate.getMessageConverters().add(converter);