Hot questions for Spring OXM

Question:

I have a spring boot project. I have a few xsds in my project. I have generated the classes using maven-jaxb2-plugin. I have used this tutorial to get a sample spring boot application running.

import org.kaushik.xsds.XOBJECT;

@SpringBootApplication
public class JaxbExample2Application {

public static void main(String[] args) {
    //SpringApplication.run(JaxbExample2Application.class, args);
    XOBJECT xObject = new XOBJECT('a',1,2);

    try {
        JAXBContext jc = JAXBContext.newInstance(User.class);

        Marshaller marshaller = jc.createMarshaller();
        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
        marshaller.marshal(xObject, System.out);

    } catch (PropertyException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    } catch (JAXBException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
 }
}

But my concern is that I need to have all the jaxb classes of the schema mapped. Also is there something in Spring that I can use to make my task easier. I have looked at the Spring OXM project but it had application context configured in xml. Does spring boot have anything that I can use out of the box. Any examples will be helpful.

Edit

I tried xerx593's answer and I ran a simple test using main method

    JaxbHelper jaxbHelper = new JaxbHelper();
    Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
    marshaller.setClassesToBeBound(XOBJECT.class);
    jaxbHelper.setMarshaller(marshaller);
    XOBJECT xOBJECT= (PurchaseOrder)jaxbHelper.load(new StreamSource(new FileInputStream("src/main/resources/PurchaseOrder.xml")));
    System.out.println(xOBJECT.getShipTo().getName());

It ran perfectly fine. Now I just need to plug it in using spring boot.


Answer:

OXM is definitely the right for you!

A simple java configuration of a Jaxb2Marshaller would look like:

//...
import java.util.HashMap;
import org.springframework.oxm.jaxb.Jaxb2Marshaller;
//...

@Configuration
public class MyConfigClass {
    @Bean
    public Jaxb2Marshaller jaxb2Marshaller() {
        Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
        marshaller.setClassesToBeBound(new Class[]{
           //all the classes the context needs to know about
           org.kaushik.xsds.All.class,
           org.kaushik.xsds.Of.class,
           org.kaushik.xsds.Your.class,
           org.kaushik.xsds.Classes.class
        });
        // "alternative/additiona - ly":
          // marshaller.setContextPath(<jaxb.context-file>)
          // marshaller.setPackagesToScan({"com.foo", "com.baz", "com.bar"});

        marshaller.setMarshallerProperties(new HashMap<String, Object>() {{
          put(javax.xml.bind.Marshaller.JAXB_FORMATTED_OUTPUT, true);
          // set more properties here...
        }});

        return marshaller;
    }
}

In your Application/Service class you could approach like this:

import java.io.InputStream;
import java.io.StringWriter;
import javax.xml.bind.JAXBException;
import javax.xml.transform.Result;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import org.springframework.oxm.jaxb.Jaxb2Marshaller;  

@Component
public class MyMarshallerWrapper {
   // you would rather:
   @Autowired
   private Jaxb2Marshaller  marshaller;
   // than:
   // JAXBContext jc = JAXBContext.newInstance(User.class);
   // Marshaller marshaller = jc.createMarshaller();

   // marshalls one object (of your bound classes) into a String.
   public <T> String marshallXml(final T obj) throws JAXBException {
      StringWriter sw = new StringWriter();
      Result result = new StreamResult(sw);
      marshaller.marshal(obj, result);
      return sw.toString();
   }

   // (tries to) unmarshall(s) an InputStream to the desired object.
   @SuppressWarnings("unchecked")
   public <T> T unmarshallXml(final InputStream xml) throws JAXBException {
      return (T) marshaller.unmarshal(new StreamSource(xml));
   }
}

See Jaxb2Marshaller-javadoc, and a related Answer

Question:

When trying to unmarshall some XML into a POJO using EclipseLink MOXy I'm getting a FileNotFoundException where it's looking for the document's DTD as a relative path.

Exception in thread "main" org.springframework.oxm.UnmarshallingFailureException: JAXB unmarshalling exception; nested exception is javax.xml.bind.UnmarshalException
 - with linked exception:
[java.io.FileNotFoundException: C:\Users\deejay\Documents\workspace-sts-3.0.0.RELEASE\moxy-test\ieee_idams_exchange.dtd (The system cannot find the file specified)]
    at org.springframework.oxm.jaxb.Jaxb2Marshaller.convertJaxbException(Jaxb2Marshaller.java:761)
    at org.springframework.oxm.jaxb.Jaxb2Marshaller.unmarshal(Jaxb2Marshaller.java:682)
    at org.springframework.oxm.jaxb.Jaxb2Marshaller.unmarshal(Jaxb2Marshaller.java:665)
    at com.mendeley.services.utility.EclipseLinkMarshaller.load(EclipseLinkMarshaller.java:29)
    at com.mendeley.MoxyTest.main(MoxyTest.java:31)

I'm providing "externalized metadata" as per this example, so I've no idea why it even needs a DTD. If I could get it to ignore the DTD, or not try and resolve it, that'd be great.


Answer:

You could unmarshal from an XMLStreamReader that has DTD support disabled:

import javax.xml.bind.*;
import javax.xml.stream.*;
import javax.xml.transform.stream.StreamSource;

public class Demo {

    public static void main(String[] args) throws Exception {
        JAXBContext jc = JAXBContext.newInstance(Customer.class);

        XMLInputFactory xif = XMLInputFactory.newFactory();
        xif.setProperty(XMLInputFactory.SUPPORT_DTD, false);
        XMLStreamReader xsr = xif.createXMLStreamReader(new StreamSource("input.xml"));

        Unmarshaller unmarshaller = jc.createUnmarshaller();
        Customer customer = (Customer) unmarshaller.unmarshal(xsr);
    }

}

Extra

If you want to write the DTD declaration you could do the following:

    XMLOutputFactory xof = XMLOutputFactory.newFactory();
    XMLStreamWriter xsw = xof.createXMLStreamWriter(System.out);
    xsw.writeDTD("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" \"http://www.w3.org/TR/html4/loose.dtd\">");

    Marshaller marshaller = jc.createMarshaller();
    marshaller.setProperty(Marshaller.JAXB_FRAGMENT, true);
    marshaller.marshal(html, xsw);
    xsw.close();

Question:

I am using the spring framework 3.1 (with hibernate) and I am trying to produce an XML representation like:

<user>
<iduser>1</iduser>
<email>bla@hello.com</email>
<firstName>bob</firstName>
</user>

from this java class:

@Entity
public class User {

    @GenericGenerator(name = "table-hilo-generator", strategy = "org.hibernate.id.IncrementGenerator")
    @GeneratedValue(generator = "table-hilo-generator")
    @Id
    @Column(name = "iduser", unique = true, nullable = false)
    private int iduser;

    @NotBlank
    @NotNull
    @NotEmpty
    @Length(max = EMAIL_MAX_SIZE)
    @Column(name = "email", nullable = false)
    private String email;

    @NotBlank
    @NotNull
    @NotEmpty
    @Length(max = FIRST_NAME_MAX_SIZE)
    @Column(name = "firstName", nullable = false)
    private String firstName;
}

my servlet-conf.xml contains this view in a ContentNegotiatingViewResolver:

<!-- XML View -->
<bean class="org.springframework.web.servlet.view.xml.MarshallingView">
  <constructor-arg>
    <bean class="org.springframework.oxm.xstream.XStreamMarshaller">
        <property name="aliases">
        <map>
                <entry key="user123" value="com.....entities.User" />
        </map>
        </property>
    </bean>
   </constructor-arg>
</bean>

But I don't understand why the result is a strange xml with hundreds of elements such that:

<org.springframework.validation.BeanPropertyBindingResult>
<nestedPath/>
<nestedPathStack serialization="custom">
<unserializable-parents/>
<vector>
<default>
<capacityIncrement>0</capacityIncrement>
<elementCount>0</elementCount>
<elementData>
<null/>
<null/>
<null/>
<null/>
<null/>
<null/>
<null/>
<null/>
<null/>
<null/>
</elementData>
</default>
</vector>
</nestedPathStack>
<objectName>user</objectName>
<messageCodesResolver class="org.springframework.validation.DefaultMessageCodesResolver">
<prefix/>

1-Probably, the marshaller is playing too much with the reflection, how can I obtain the expected result that I want? ( 2-I am interested also in producing a XML file with a list of Users) How can I do that?


Answer:

Like you have noted, what is happening is since you have not specified the explicit model key that needs to be serialized, it is serializing the first non-null valued model object, which in this happens be BindingResult(used for keeping the binding/validation errors in your model). There are a few fixes that you can make:

a. Specify the exact modelKey for your marshalling view, this should work and set the model to the specific model key:

<bean class="org.springframework.web.servlet.view.xml.MarshallingView">
<property name="marshaller">
...
</property>
<property name="modelKey" value="command"/>
</bean>

model.addAttribute("command", mymodel);

b. A better fix, IMHO could be to use the http converters in Spring, this way you can return your object from a request mapped method, annotate it with @ResponseBody and Spring will take care of converting the object to a wire reprsentation(xml or json etc), you will just have to register the correct converter:

@RequestMapping(...)
public @ResponseBody User myMethod(Model model){
    return user;
}

<mvc:annotation-driven conversion-service="conversionService"> 
   <mvc:message-converters register-defaults="false"> <!-- you may have to explicitly register other converters though-->
       <bean class="org.springframework.http.converter.xml.MarshallingHttpMessageConverter">
           <property name="marshaller">
               <bean class="org.springframework.oxm.xstream.XStreamMarshaller"/>
           </property>
       </bean>
   </mvc:message-converters>

Question:

Im trying to understand this particular issue i'm having. Using Spring OXM here to implement a Soap WS Consumer.

I'm only attaching the relevant information to explain the problem.

LoginWsConfiguration,

@Configuration
public class LoginWsConfiguration {

    @Inject
    private OnboardingProperties onboardingProperties;

    @Bean
    Jaxb2Marshaller jaxb2Marshaller() {
        Jaxb2Marshaller jaxb2Marshaller = new Jaxb2Marshaller();
        jaxb2Marshaller
            .setPackagesToScan("org.openiam.ws.login.api_3_2_6");
        try {
            jaxb2Marshaller.afterPropertiesSet();
        }
        catch (Exception ex) {
            throw new BeanCreationException(ex.getMessage(), ex);
        }
        return jaxb2Marshaller;
    }

    @Bean
    public WebServiceTemplate webServiceTemplate(Jaxb2Marshaller jaxb2Marshaller) {
        WebServiceTemplate webServiceTemplate = new WebServiceTemplate();
        webServiceTemplate.setMarshaller(jaxb2Marshaller);
        webServiceTemplate.setUnmarshaller(jaxb2Marshaller);
        webServiceTemplate.setDefaultUri(onboardingProperties.getIAM().getEndpoint().getLogin());

        return webServiceTemplate;
    }

And the LoginClient client is implemented as,

@Component
public class LoginClient {

    private static final Logger log = LoggerFactory.getLogger(IAMLoginClient.class);

    @Autowired
    private LoginMapper loginMapper;

    @Inject
    private OnboardingProperties onboardingProperties;

    private WebServiceTemplate webServiceTemplate;

    @Autowired
    public LoginClient(WebServiceTemplate webServiceTemplate) {
        this.webServiceTemplate = webServiceTemplate;
    }

    public LoginClient() {
    }

    public Optional getLoginByUserId(final String userId) {
        ObjectFactory factory = new ObjectFactory();
        GetLoginByUser req = factory.createGetLoginByUser();
        req.setUserId(userId);
        GetLoginByUserResponse resp = (GetLoginByUserResponse)webServiceTemplate.marshalSendAndReceive(req);
        if (resp != null && resp.getReturn() != null && resp.getReturn().getStatus() == org.openiam.ws.login.api_3_2_6.ResponseStatus.SUCCESS) {
            List<Login> foundLogins = resp.getReturn().getPrincipalLists();
            Login foundLogin = foundLogins.stream()
                .filter(login -> login.getManagedSysId().equals(Constants.IAM_DEFAULT_MANAGED_SYS_ID))
                .findFirst()
                .orElse(null);
            return Optional.ofNullable((foundLogin != null) ? this.loginMapper.mapIAMLogin(foundLogin) : null);
        }
        return Optional.empty();
    }

For completeness, GetLoginByUser,

@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "getLoginByUser", propOrder = {
    "userId"
})
@XmlRootElement(name = "getLoginByUser")
public class GetLoginByUser {

    protected String userId;

    /**
     * Gets the value of the userId property.
     * 
     * @return
     *     possible object is
     *     {@link String }
     *     
     */
    public String getUserId() {
        return userId;
    }

    /**
     * Sets the value of the userId property.
     * 
     * @param value
     *     allowed object is
     *     {@link String }
     *     
     */
    public void setUserId(String value) {
        this.userId = value;
    }

}

GetLoginByUserResponse,

@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "getLoginByUserResponse", propOrder = {
    "_return"
})
@XmlRootElement(name = "getLoginByUserResponse")
public class GetLoginByUserResponse {

    @XmlElement(name = "return")
    protected LoginListResponse _return;

    /**
     * Gets the value of the return property.
     * 
     * @return
     *     possible object is
     *     {@link LoginListResponse }
     *     
     */
    public LoginListResponse getReturn() {
        return _return;
    }

    /**
     * Sets the value of the return property.
     * 
     * @param value
     *     allowed object is
     *     {@link LoginListResponse }
     *     
     */
    public void setReturn(LoginListResponse value) {
        this._return = value;
    }

}

However, when I try to,

@Autowired
private UserClient iamUserClient;

@Autowired
private LoginClient iamLoginClient;

Optional<IAMUser> userFromIAM = iamUserClient.getUserByPrincipal(lowercaseLogin);
IAMUser userIAM = userFromIAM.
        orElseThrow(() -> new UsernameNotFoundException("User " + lowercaseLogin + " was not found in IAM"));
    if (userIAM.getStatus() != IAMUserStatus.ACTIVE || userIAM.getAccountStatus() != null) {
        throw new UserNotActivatedException("User " + lowercaseLogin + " is not active");
    }
Optional<IAMLogin> loginFromIAM = iamLoginClient.getLoginByUserId(userIAM.getId());

I get the error,

org.springframework.oxm.UncategorizedMappingException: Unknown JAXB exception; nested exception is javax.xml.bind.JAXBException: class org.openiam.ws.login.api_3_2_6.GetLoginByUser nor any of its super class is known to this context.
    at org.springframework.oxm.jaxb.Jaxb2Marshaller.convertJaxbException(Jaxb2Marshaller.java:915)
    at org.springframework.oxm.jaxb.Jaxb2Marshaller.marshal(Jaxb2Marshaller.java:684)
    at org.springframework.ws.support.MarshallingUtils.marshal(MarshallingUtils.java:81)
    at org.springframework.ws.client.core.WebServiceTemplate$2.doWithMessage(WebServiceTemplate.java:399)
    at org.springframework.ws.client.core.WebServiceTemplate.doSendAndReceive(WebServiceTemplate.java:590)
    at org.springframework.ws.client.core.WebServiceTemplate.sendAndReceive(WebServiceTemplate.java:555)
    at org.springframework.ws.client.core.WebServiceTemplate.marshalSendAndReceive(WebServiceTemplate.java:390)
    at org.springframework.ws.client.core.WebServiceTemplate.marshalSendAndReceive(WebServiceTemplate.java:383)
    at org.springframework.ws.client.core.WebServiceTemplate.marshalSendAndReceive(WebServiceTemplate.java:373)
    at com.orchestral.dsm.onboarding.iam.ws.login.LoginClient.getLoginByUserId(LoginClient.java:77)
    at com.orchestral.dsm.onboarding.security.IAMUserDetailsService.loadUserByUsername(IAMUserDetailsService.java:45)

From what I can see it doesn't fail when marshalling in getUserByPrincipal (UserClient) but fails on getLoginByUserId (LoginClient).


Answer:

This was due to the fact that both,

@Autowired
private UserClient iamUserClient;

@Autowired
private LoginClient iamLoginClient;

were conflicting with each other. Having individual jaxb2Marshaller and passing that to WebServiceTemplate fixed the issue.

So LoginWsConfiguration updated to,

@Configuration
public class LoginWsConfiguration {

    @Bean
    Jaxb2Marshaller jaxb2LoginMarshaller() {
        Jaxb2Marshaller jaxb2Marshaller = new Jaxb2Marshaller();
        jaxb2Marshaller.setPackagesToScan("org.openiam.ws.login.api_3_2_6");
        return jaxb2Marshaller;
    }

    @Bean
    public LoginClient loginClient(Jaxb2Marshaller jaxb2LoginMarshaller) {
        LoginClient client = new LoginClient();
        client.setMarshaller(jaxb2LoginMarshaller);
        client.setUnmarshaller(jaxb2LoginMarshaller);
        client.setDefaultUri("uri");

        return client;
    }
}