2

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>
 * &lt;complexType>
 *   &lt;complexContent>
 *     &lt;restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
 *       &lt;sequence>
 *         &lt;element ref="{http://technikumwien.at/}author" maxOccurs="unbounded" minOccurs="0"/>
 *       &lt;/sequence>
 *     &lt;/restriction>
 *   &lt;/complexContent>
 * &lt;/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!

Community
  • 1
  • 1
Florian
  • 21
  • 2
  • I haven't been able to determine from all that code whether you are using the same class Book in server and client. If you do, there shouldn't be any problem. - Anyway, if you are controlling both sides: for what do you need the wrapper element (``)? It'll work nicely without. – laune Nov 02 '15 at 14:15
  • in server I use a self-written class Book.java which is the second Code-Block from the bottom, in client i use a Book.java which has been generated by wsimport. I need to use the wrapper because I am not allowed to change the format of my import Books.xml. Yes, I have tried without the wrapper and that works, but I am looking for a way to make it work including the wrapper element – Florian Nov 03 '15 at 08:42
  • I think the most elegant solution would be to find a way to wsimport - autogenerate the nested Book.Authors class with the annotation @XmlType(name="authors") instead of @XmlType(name="") but I don't know if that's even possible – Florian Nov 03 '15 at 08:53
  • Indeed, the problem goes away with a @XmlElement attribute (name = "xxxxx"). You can change this attribute during generation using the Maven [jaxb2-annotate-plugin](https://github.com/highsource/jaxb2-annotate-plugin). – serg ogoltsov Sep 16 '16 at 21:00

0 Answers0