I am writing a SOAP based web service and a client application that calls the web service with import data read from an XML file. I used wsimport to generate the necessary classes for the client from a WSDL file.
The import file "Books.xml" must look like:
<?xml version="1.0" encoding="UTF-8"?>
<books>
<book isbn="aaa" title="Aaa" subtitle="aaa" description="aaa" pages="25" language="german">
<publisher name="a" postcode="a" countrycode="a" />
<authors>
<author firstname="a" lastname="a" birthday="1999-01-01" />
</authors>
</book>
</books>
I use a self-created wrapper class Books.java as an @XmlRootElement
to unmarshal the list of Books. When unmarshalling the XML file, I am unable to obtain the children of the <authors> element, so the Book object created by the unmarshaller never has any Authors.
This is the code used for unmarshalling the XML file and after that marshalling the resulting Books object:
JAXBContext jaxbContext = JAXBContext.newInstance(Books.class);
Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
Source source = new StreamSource(new File(xmlPath));
JAXBElement<Books> jaxbBooks = unmarshaller.unmarshal(source, Books.class);
Books books = jaxbBooks.getValue();
Marshaller marshaller = jaxbContext.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_ENCODING, "ISO-8859-1");
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
marshaller.marshal(books, System.out); // just for debug purposes
which produces the following result:
<books xmlns:ns2="http://technikumwien.at/">
<book isbn="aaa" title="Aaa" subtitle="aaa" description="aaa" pages="25" language="german">
<ns2:publisher publisherId="0" name="a" postcode="a" countrycode="a"/>
<ns2:authors/>
</book>
</books>
In my server's Book.java, I used a @XmlElementWrapper
to wrap the list of Authors:
@XmlElementWrapper(name="authors")
@XmlElement(name="author")
private List<Author> authors;
I took a look at the client's Book.java generated by wsimport - the list of Authors has been transformed into a nested static class:
/**
* <p>Java-Klasse für anonymous complex type.
*
* <p>Das folgende Schemafragment gibt den erwarteten Content an, der in dieser Klasse enthalten ist.
*
* <pre>
* <complexType>
* <complexContent>
* <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
* <sequence>
* <element ref="{http://technikumwien.at/}author" maxOccurs="unbounded" minOccurs="0"/>
* </sequence>
* </restriction>
* </complexContent>
* </complexType>
* </pre>
*
*
*/
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "", propOrder = {
"author"
})
public static class Authors {
protected List<Author> author;
/**
* Gets the value of the author property.
*
* <p>
* This accessor method returns a reference to the live list,
* not a snapshot. Therefore any modification you make to the
* returned list will be present inside the JAXB object.
* This is why there is not a <CODE>set</CODE> method for the author property.
*
* <p>
* For example, to add a new item, do as follows:
* <pre>
* getAuthor().add(newItem);
* </pre>
*
*
* <p>
* Objects of the following type(s) are allowed in the list
* {@link Author }
*
*
*/
public List<Author> getAuthor() {
if (author == null) {
author = new ArrayList<Author>();
}
return this.author;
}
}
I noticed that the name attribute of the @XmlType
annotation has been created empty, if I replace @XmlType(name="")
with @XmlType(name="authors")
the unmarshalling succeeds at capturing a Book's Author children elements:
<?xml version="1.0" encoding="ISO-8859-1" standalone="yes"?>
<books xmlns:ns2="http://technikumwien.at/">
<book isbn="aaa" title="Aaa" subtitle="aaa" description="aaa" pages="25" language="german">
<ns2:publisher publisherId="0" name="a" postcode="a" countrycode="a"/>
<ns2:authors>
<ns2:author authorId="0" firstname="a" lastname="a" birthday="1999-01-01"/>
</ns2:authors>
</book>
</books>
HOWEVER, I DO NOT WANT TO MODIFY THE CLASSES GENERATED BY WSIMPORT!
So I solved the problem by parsing the XML into a DOM tree, traversing the DOM tree using TreeWalker. For each Node, if the Node was a <Book> element I unmarshalled this "Book node" and pushed it on a seperate list of Books. If the traversed Node was a <Author> element, I unmarshalled the "Author Node" and added the Author object to the last Book object in the list of Books:
private static final List<Book> traverseLevel(TreeWalker walker, Unmarshaller unmarshaller, List<Book> bookList) throws JAXBException {
Node parent = walker.getCurrentNode();
if (parent.getNodeName().equals("book"))
{
JAXBElement<Book> bookElem = unmarshaller.unmarshal(parent, Book.class);
Book book = bookElem.getValue();
bookList.add(book);
}
if (parent.getNodeName().equals("author"))
{
JAXBElement<Author> authorElem = unmarshaller.unmarshal(parent, Author.class);
Author author = authorElem.getValue();
bookList.get(bookList.size() - 1).getAuthors().getAuthor().add(author);
}
for (Node n = walker.firstChild(); n != null; n = walker.nextSibling()) {
traverseLevel(walker, unmarshaller, bookList);
}
walker.setCurrentNode(parent);
return bookList;
}
This solution works, however I find it terribly unflexible and I think there must be an easier way to sucessfully unmarshal the Books' nested Author elements without manually traversing the DOM tree.
I also had to use the following fix https://stackoverflow.com/a/7736235/4716861 to get rid of a namespace prefix problem so in my server's package-info.java I wrote:
@XmlSchema(
namespace = "http://technikumwien.at/",
elementFormDefault = XmlNsForm.QUALIFIED,
xmlns = {
@XmlNs(prefix="", namespaceURI="http://technikumwien.at/")
}
)
@XmlJavaTypeAdapters({
@XmlJavaTypeAdapter(type=LocalDate.class, value=LocalDateXmlAdapter.class)
})
package at.technikumwien;
import javax.xml.bind.annotation.*;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapters;
import java.time.LocalDate;
Part of Wsdl file (autogenerated when starting WildFly):
<xs:complexType name="book">
<xs:sequence>
<xs:element minOccurs="0" ref="publisher"></xs:element>
<xs:element minOccurs="0" name="authors">
<xs:complexType>
<xs:sequence>
<xs:element maxOccurs="unbounded" minOccurs="0" ref="author"></xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
<xs:attribute name="bookId" type="xs:long"></xs:attribute>
<xs:attribute name="isbn" type="xs:string"></xs:attribute>
<xs:attribute name="title" type="xs:string"></xs:attribute>
<xs:attribute name="subtitle" type="xs:string"></xs:attribute>
<xs:attribute name="description" type="xs:string"></xs:attribute>
<xs:attribute name="pages" type="xs:int" use="required"></xs:attribute>
<xs:attribute name="language" type="xs:string"></xs:attribute>
</xs:complexType>
<xs:complexType name="publisher">
<xs:sequence></xs:sequence>
<xs:attribute name="publisherId" type="xs:long" use="required"></xs:attribute>
<xs:attribute name="name" type="xs:string"></xs:attribute>
<xs:attribute name="postcode" type="xs:string"></xs:attribute>
<xs:attribute name="countrycode" type="xs:string"></xs:attribute>
</xs:complexType>
<xs:complexType name="author">
<xs:sequence></xs:sequence>
<xs:attribute name="authorId" type="xs:long" use="required"></xs:attribute>
<xs:attribute name="firstname" type="xs:string"></xs:attribute>
<xs:attribute name="lastname" type="xs:string"></xs:attribute>
<xs:attribute name="birthday" type="xs:string"></xs:attribute>
</xs:complexType>
server's Book.java
@Entity
@Table(name="book")
@NamedQueries({
@NamedQuery(name="Book.selectAll", query="SELECT b FROM Book b"),
@NamedQuery(name="Book.selectByTitle", query="SELECT b FROM Book b WHERE b.title like CONCAT('%', :title ,'%')")
})
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@XmlAttribute(name="bookId")
private Long bookId;
@XmlAttribute(name="isbn")
private String isbn;
@XmlAttribute(name="title")
private String title;
@XmlAttribute(name="subtitle")
private String subtitle;
@XmlAttribute(name="description")
private String description;
@XmlAttribute(name="pages")
private int pages;
@XmlAttribute(name="language")
private String language;
@ManyToOne(optional = false)
@NotNull
@XmlElement(name="publisher")
private Publisher publisher;
@ManyToMany(mappedBy = "books", fetch = FetchType.EAGER)
@NotEmpty
@XmlElementWrapper(name="authors")
@XmlElement(name="author")
private List<Author> authors;
server's Author.java
@Entity
@Table(name="author")
@NamedQueries({
@NamedQuery(name="Author.selectAll", query="SELECT a FROM Author a"),
@NamedQuery(name="Author.checkExists", query="SELECT a FROM Author a WHERE a.firstname = :firstname AND a.lastname = :lastname AND a.birthday = :birthday")
})
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class Author {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@XmlAttribute(name="authorId")
private long authorId;
@XmlAttribute(name="firstname")
private String firstname;
@XmlAttribute(name="lastname")
private String lastname;
@XmlAttribute(name="birthday")
@Convert(converter = LocalDatePersistenceConverter.class)
private LocalDate birthday;
@ManyToMany
@XmlTransient
private List<Book> books = new ArrayList<Book>();
Is the problem I am experiencing expected behaviour or am I missing something? Shouldn't the unmarshaller be able to capture all children of the XML hierarchy using the wsimport - generated Classes by default?
Sry for posting so much code, but maybe it can be of relevance to solve the problem. Hope anybody can hint me into the right direction. Thank you!