Skip to content
This repository has been archived by the owner on Mar 15, 2022. It is now read-only.
thamtech edited this page Jul 19, 2013 · 3 revisions

XML related features

<wiki:toc max_depth="3" />

Introduction

XML (de)serialization works by selecting a "path" expression. If no XPath expression is given, the fields name is taken as a base for the XPath expression. The XML is selected and converted if necessary to the type of the annotated property.

XML → POJO

Let's assume you want to map the following XML

<book isbn="978-0345417954">
    <pages>432</pages>
    <title>The Hotel New Hampshire</title>
    <author>
        <firstname>John</firstname>
        <surname>Irving</surname>
    </author>
    <reviews>
        <review>
            A hectic gaudy saga with the verve of a Marx Brothers movie.
        </review>
        <review>
            Rejoice! John Irving has written another book according to your world. 
            You must read this book.
        </review>
        <review>
            Spellbinding, intensely human, a high-wire act of 
            dazzling virtuosity.
        </review>
    </reviews>
</book>

Therefore you have the following POJOs in your GWT client

public class Book
{
    String isbn;
    int pages;
    String title;
    Author author;
    List<String> reviews;
}

public class Author
{
    String firstname;
    String surname;
}

To map the XML to your classes, all you have to do to define an interface of type !XmlReader<T> and configure XPath expressions. When using relative XPath expressions, please note that they're relative to the element currently mappped. Thus in the following example "@isbn" and "reviews/review" are relative to the <book> element:

public class Book
{
    interface BookReader extends XmlReader<Book> {}
    public static final BookReader XML = GWT.create(BookReader.class);

    @Path("@isbn") String isbn;
    int pages;
    String title;
    Author author;
    @Path("reviews/review") List<String> reviews;
}

public class Author
{
    interface AuthorReader extends XmlReader<Author> {}
    public static final AuthorReader XML = GWT.create(AuthorReader.class);

    String firstname;
    String surname;
}

Now you can map the XML by calling

Document document = new XmlParser().parse(xmlAsString);
Book book = Book.XML.read(document);

POJO → XML

POJOs can be serialized to XML by using the same annotations which are used for parsing. To generate XML from existings POJO instances you have to define a XmlWriter<T> interface.

public class Book
{
    public interface BookReader extends XmlReader<Book> {}
    public static final BookReader READER = GWT.create(BookReader.class);

    public interface BookWriter extends XmlWriter<Book> {}
    public static final BookWriter WRITER = GWT.create(BookWriter.class);

    @Path("@isbn") String isbn;
    int pages;
    String title;
    Author author;
    @Path("reviews/review") List<String> reviews;
}

public class Author
{
    public interface AuthorReader extends XmlReader<Author> {}
    public static final AuthorReader READER = GWT.create(AuthorReader.class);

    public interface AuthorWriter extends XmlWriter<Author> {}
    public static final AuthorWriter WRITER = GWT.create(AuthorWriter.class);

    String firstname;
    String surname;
}

A typical scenario would be to parse the XML received from the server, make some modifications and send back the modified XML:

String xml = ...; // received from the server
Book book = Book.READER.read(xml);
book.title += " 2nd edition";
xml = Book.WRITER.toXml(book);
// do some REST request

When POJOs are serialized null values will be result in empty elements. In case they're mapped to attributes, the attributes will be skipped:

Book book = new Book();
book.isbn = null;
book.pages = 0;
book.title = "";
book.author = null;
book.reviews = new ArrayList<String>();

String xml = Book.WRITER.toXml(book); 

will result in

<book>
    <pages>0</pages>
    <title/>
    <author/>
    <reviews/>
</book>

Arrays and collections

When mapping arrays and collections the XPath expression should return a list of elements:

<book>
    ...
    <reviews>
        <review>
            A hectic gaudy saga with the verve of a Marx Brothers movie.
        </review>
        <review>
            Rejoice! John Irving has written another book according to your world. 
            You must read this book.
        </review>
        <review>
            Spellbinding, intensely human, a high-wire act of 
            dazzling virtuosity.
        </review>
    </reviews>
    ...
</book>
public class Book
{
    ...
    @Path("reviews/review") List<String> reviews;
    ...
}

In the example above the XPath expression "reviews/review" selects the three reviews which are inserted in a new list and assigned to Book.reviews.

References in XML

Introduction

Often you have references in XML. Piriti can resolve those references if expressed by ID and IDREF. Let's take the following XML document as an example (references in JSON are not supported):

<company>
    <employees>
        <employee id="boss">
            <name>Big Boss</name>
            <team>
                <member ref="seller" />
                <member ref="engineer" />
            </team>
            <department ref="board" />
        </employee>
        <employee id="seller">
            <name>Sally Seller</name>
            <boss ref="boss" />
            <department ref="sales" />
        </employee>
        <employee id="engineer">
            <name>Ed Engineer</name>
            <boss ref="boss" />
            <team>
                <member ref="coder" />
                <member ref="tester" />
            </team>
            <department ref="it" />
        </employee>
        <employee id="coder">
            <name>Carl Coder</name>
            <boss ref="engineer" />
            <department ref="it" />
        </employee>
        <employee id="tester">
            <name>Tom Tester</name>
            <boss ref="engineer" />
            <department ref="it" />
        </employee>
    </employees>
    <departments>
        <department id="board">
            <name>Board</name>
            <employees members="boss" />
        </department>
        <department id="sales">
            <name>Sales</name>
            <employees members="seller" />
        </department>
        <department id="it">
            <name>IT</name>
            <employees members="engineer coder tester" />
        </department>
    </departments>
</company>

The XML document uses references for

  • team members
  • employees of a department and
  • departments itself

Piriti supports the mapping of those references. So you end up with references to other POJOs in the mapped POJO. In the given example the 'boss' POJO has references to the 'seller' and 'engineer' POJO.

Mapping

Let's take a look at the mapping:

public class Employee
{
    public interface EmployeeReader extends XmlReader<Employee> {}
    public static final EmployeeReader XML = GWT.create(EmployeeReader.class);

    @Id @Path("@id") String id;
    String name;
    @IdRef @Path("boss/@ref") Employee boss;
    @IdRef @Path("team/member/@ref") List<Employee> team;
    @IdRef @Path("department/@ref") Department department;
}

public class Department
{
    public interface DepartmentReader extends XmlReader<Department> {}
    public static final DepartmentReader XML = GWT.create(DepartmentReader.class);

    @Id @Path("@id") String id;
    String name;
    @IdRef @Path("employees/@members") Employee[] employees;
}

@Id

Identifiers in the XML document have to be marked with the @Id annotation. If necessary specify an XPath expressions to select the id value. The selected value is mapped to a String member of the POJO. The @Id annotation can be used only once in a POJO. The POJO instance which contains the @Id annotation is put into an internal map using the identifier as key.

@!IdRef

Using the @!IdRef annotation you can define reference to other elements in the XML document. These elements must contain an identifier marked with @Id. Specify an XPath expressions to select one or multiple strings.

  • One string value: The type of the field marked with the @!IdRef annotation has to be a type with a registered reader.
  • Multiple string values: The type of the field has to be an array or collection of types with a registered reader.

Namespaces

Piriti supports namespaces in XML documents and XPath expressions. This is possible because the XML is not parsed with GWTs built-in parser, but with the parser from Totoe which supports namespaces.

When mapping XML documents we have to distinguish two use cases:

  1. XML documents without a default namespace
  2. XML documents with a default namespace

No Default Namespace

That's the easy part, so we start with this. Suppose you have the following XML document

<?xml version="1.0" encoding="UTF-8"?>
<lotteryTicket date="10.10.2010" xmlns:foo="http://code.google.com/p/piriti/foo"
    xmlns:bar="http://code.google.com/p/piriti/bar">

    <foo:player foo:gender="male" age="42">
        <foo:firstname>Homer</foo:firstname>
        <foo:surname>Simpson</foo:surname>
        <bar:address bar:type="home" foo:valid="true">
            24, evergreen terrace, springfield
        </bar:address>
    </foo:player>

    <numbers game="6x49">
        <number>4</number>
        <number>8</number>
        <number>15</number>
        <number>16</number>
        <number>23</number>
        <number>42</number>
    </numbers>
</lotteryTicket>

Now you want to map this XML to the following POJOs

public class LotteryTicket
{
    Date date;
    Player player;
    String game;
    List<Integer> numbers;
}

public class Player
{
    Gender gender;
    int age;
    String firstname;
    String surname;
    String address;
    String addressType;
    boolean validAddress;
}

public enum Gender
{
    FEMALE,
    MALE
}

To do this you need to:

  1. specify the namespaces when parsing the XML and
  2. include the namespace prefix in the annotations

Specify namespaces

When parsing the XML you have to specify the namespaces. This can be done in two ways:

  • By providing a whilespace-seperated list of namespace declarations as those would appear in an XML document:

      String namespaces = "xmlns:foo=\"http://code.google.com/p/piriti/foo\" "
          + "xmlns:bar=\"http://code.google.com/p/piriti/bar\"";
      Document document = new XmlParser().parse(xmlAsString, namespaces);
    
  • By providing a map with the namespace prefix as key and the namespace URI as value:

      Map<String,String> namespaces = new HashMap<String,String>();
      namespaces.put("foo", "http://code.google.com/p/piriti/foo");
      namespaces.put("bar", "http://code.google.com/p/piriti/bar");
      Document document = new XmlParser().parse(xmlAsString, namespaces);
    

Include the namespace prefix in the annotations

In the annotations of your POJOs you have to adjust the XPath expressions to include the namespace prefix:

public class LotteryTicket
{
    public interface LotteryTicketReader extends XmlReader<LotteryTicket> {}
    public static final LotteryTicketReader XML = GWT.create(LotteryTicketReader.class);

    @Path("@date") @Format("dd.MM.yyyy") Date date;
    @Path("foo:player") Player player;
    @Path("numbers/@game") String game;
    @Path("numbers/number") List<Integer> numbers;
}

public class Player
{
    public interface PlayerXmlReader extends XmlReader<Player> {}
    public static final PlayerXmlReader XML = GWT.create(PlayerXmlReader.class);

    @Path("@foo:gender") Gender gender;
    @Path("@age") int age;
    @Path("foo:firstname") String firstname;
    @Path("foo:surname") String surname;
    @Path("bar:address") String address;
    @Path("bar:address/@bar:type") String addressType;
    @Path("bar:address/@foo:valid") boolean validAddress;
}

Default Namespace

Things get a little bit more complicated if your XML uses a default namespace (DNS). When you want to use the DNS in your XPath expression you also have to provide a prefix for that DNS. Let's add a default namespace to our {{{lotteryTicket.xml}}}:

<?xml version="1.0" encoding="UTF-8"?>
<lotteryTicket xmlns="http://code.google.com/p/piriti"
    xmlns:foo="http://code.google.com/p/piriti/foo"
    xmlns:bar="http://code.google.com/p/piriti/bar" date="10.10.2010">

    <foo:player foo:gender="male" age="42">
        <foo:firstname>Homer</foo:firstname>
        <foo:surname>Simpson</foo:surname>
        <bar:address bar:type="home" foo:valid="true">
            24, evergreen terrace, springfield
        </bar:address>
    </foo:player>

    <numbers game="6x49">
        <number>4</number>
        <number>8</number>
        <number>15</number>
        <number>16</number>
        <number>23</number>
        <number>42</number>
    </numbers>
</lotteryTicket>

Now this document uses the default namespace {{{http://code.google.com/p/piriti}}}.

Specify namespaces

When parsing the XML you now have to specify a prefix for the DNS. You can choose whtever you like as long as it is not already used (I will use "dns" in the following examples):

  • Providing a whilespace-seperated list of namespace declarations as those would appear in an XML document (plus the dns prefix):

      String namespaces = "xmlns:dns=\"http://code.google.com/p/piriti\" "
          + "xmlns:foo=\"http://code.google.com/p/piriti/foo\" "
          + "xmlns:bar=\"http://code.google.com/p/piriti/bar\"";
      Document document = new XmlParser().parse(xmlAsString, namespaces);
    
  • By providing a map with the namespace prefix as key and the namespace URI as value (plus the dns prefix):

      Map<String,String> namespaces = new HashMap<String,String>();
      namespaces.put("dns", "http://code.google.com/p/piriti");
      namespaces.put("foo", "http://code.google.com/p/piriti/foo");
      namespaces.put("bar", "http://code.google.com/p/piriti/bar");
      Document document = new XmlParser().parse(xmlAsString, namespaces);
    

Include the namespace prefix in the annotations

Finally the prefix of the DNS has to be added to the annotations in your POJOs:

public class LotteryTicketDns
{
    public interface LotteryTicketReader extends XmlReader<LotteryTicketDns> {}
    public static final LotteryTicketReader XML = GWT.create(LotteryTicketReader.class);

    @Path("@date") @Format("dd.MM.yyyy") Date date;
    @Path("foo:player") Player player;
    @Path("dns:numbers/@game") String game;
    @Path("dns:numbers/dns:number") List<Integer> numbers;
}

As the mapping for {{{Player}}} does not use the DNS the code does not change compared to the first example without a default namespace.

Limitations

Currently Piriti has the following limitations when serializing POJOs to XML

  • Namespaces are not supported
  • ID and IDREF is not supported
  • Only a limited set of XPath expression can be used

Supported XPath expressions

The following list shows the supported XPath expressions when serializing POJOs to XML.

  • a/b/c/.../x/y/z
  • a/b/c/.../x/y/@z or just
  • @z The attribute must be the last part of the XPath expression and cannot be used for collections. All other XPath expressions are skipped! Here are some examples of XPath expressions which are not supported:
  • bookstore//book/excerpt//emph
  • book/*/last-name
  • author[- /bookstore/bookprice>35.00`

According to these rules the following code

public class Person
{
    public interface PersonWriter extends XmlWriter<Person> {}
    public static final PersonWriter WRITER = GWT.create(PersonWriter.class);
    
    @Path("deeply/nested/name/first") String first;
    @Path("deeply/nested/name/middle") String middle;
    @Path("deeply/nested/name/last") String last;
    @Path("@age") int age;
    @Path("//adress/street") String street;
    @Path("//adress/street") String city;
}
...
Person p = new Person();
p.first = "Homer";
p.middle = "Jay";
p.last = "Simpson";
p.age = 36;
p.street = "742 Evergreen Terrace";
p.city = "Springfield";

String xml = Person.WRITER.toXml(p);

will result in this XML

<person age="36">
    <deeply>
        <nested>
            <name>
                <first>Homer</first>
                <middle>Jay</middle>
                <last>Simpson</last>
            </name>
        </nested>
    </deeply>
</person>