7

I'm using XStream and I have an XML sample:

<person>
    <firstname>Joe</firstname>
    <lastname>Walnes</lastname>
    <phone value="1234-456" />
    <fax value="9999-999" />
</person>

and I whant to map it to the class

public class Person {

    private String firstname;
    private String lastname;
    private String phone;
    private String fax;

}

So the idea is to map attribute of nested element to the current object. I tried to find any ready-to-use converter with no success. I believe that's possible by implementing new converter but may be someone already did this. Or there's a solution I haven't found.

Updated:

The idea I'm trying to implement is omitting unnecessary entities of being created and mapped. I don't need Phone and Fax entities at all, I need only their attributes in my model. The XML schema I'm trying to parse is thirdparty for me and I can't change it.

Viktor Stolbin
  • 2,899
  • 4
  • 32
  • 53
  • Well spotted! I've fixed this. Was just written from scratch. – Viktor Stolbin Sep 19 '12 at 10:59
  • Could you clarify what you are seeking a little bit more? You say "I don't need Phone and Fax entities...", well you don't have them - they are just Strings in your model, not separate entities.... Do you want only the first two attributes mapped or all four? – JoeG Sep 19 '12 at 13:14
  • The issue is how to map the XML sample to the model. Without any extra effort XStream supposed XML elements to be interpreted as model members anyway (even they are explicitly omitted). Having a rich XML schema you would probably like to simplify the model to escape redundant 'holders' of meaningful data. Implicit collections are good but not enough. – Viktor Stolbin Sep 19 '12 at 13:36

3 Answers3

5

I don't know of a ready-to-use converter that will do it, but it's pretty trivial to write one

import com.thoughtworks.xstream.converters.*;
import com.thoughtworks.xstream.io.*;

public class ValueAttributeConverter implements Converter {
  public boolean canConvert(Class cls) {
    return (cls == String.class);
  }

  public void marshal(Object source, HierarchicalStreamWriter w, MarshallingContext ctx) {
    w.addAttribute("value", (String)source);
  }

  public Object unmarshal(HierarchicalStreamReader r, UnmarshallingContext ctx) {
    return r.getAttribute("value");
  }
}

You can attach the converter to the relevant fields using annotations

import com.thoughtworks.xstream.annotations.*;

@XStreamAlias("person")
public class Person {

    private String firstname;
    private String lastname;

    @XStreamConverter(ValueAttributeConverter.class)
    private String phone;

    @XStreamConverter(ValueAttributeConverter.class)
    private String fax;

    // add appropriate constructor(s)

    /** For testing purposes - not required by XStream itself */
    public String toString() {
      return "fn: " + firstname + ", ln: " + lastname +
             ", p: " + phone + ", f: " + fax;
    }
}

To make this work, all you need to do is instruct XStream to read the annotations:

XStream xs = new XStream();
xs.processAnnotations(Person.class);
Person p = (Person)xs.fromXML(
  "<person>\n" +
  "  <firstname>Joe</firstname>\n" +
  "  <lastname>Walnes</lastname>\n" +
  "  <phone value='1234-456' />\n" +
  "  <fax value='9999-999' />\n" +
  "</person>");
System.out.println(p);
// prints fn: Joe, ln: Walnes, p: 1234-456, f: 9999-999
Ian Roberts
  • 120,891
  • 16
  • 170
  • 183
  • Thanks, I'll check it. Not sure I understand how it works there. Possibly due the lack of understanding converters at all. – Viktor Stolbin Sep 19 '12 at 11:02
  • 1
    When XStream is reading XML that represents a bean type class it reads the element name to work out which field it's dealing with, then delegates to the relevant converter. The converter's `unmarshal` method is passed a `HierarchicalStreamReader` that's pointing at the opening tag of the (in this case) `phone` element. The normal single value converters operate on the text content of the element, but my custom converter instead looks at the attribute named `value` - it doesn't need to care what the element name was. – Ian Roberts Sep 19 '12 at 11:14
4

Note: I'm the EclipseLink JAXB (MOXy) lead and a member of the JAXB (JSR-222) expert group.

If you are open to using a library other than XStream, below is how you could leverage the @XmlPath extension in EclipseLink JAXB (MOXy).

Person

The @XmlPath annotation allows you to map your field/property to a location in the XML document by an XPath (see: http://blog.bdoughan.com/2010/07/xpath-based-mapping.html).

package forum12425401;

import javax.xml.bind.annotation.*;
import org.eclipse.persistence.oxm.annotations.XmlPath;

@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class Person {

    private String firstname;
    private String lastname;

    @XmlPath("phone/@value")
    private String phone;

    @XmlPath("fax/@value")
    private String fax;

}

jaxb.properties

To specify MOXy as your JAXB provider you need to include a file called jaxb.properties in the same package as your domain model with the following entry (see: http://blog.bdoughan.com/2011/05/specifying-eclipselink-moxy-as-your.html):

javax.xml.bind.context.factory=org.eclipse.persistence.jaxb.JAXBContextFactory

Demo

The code below will convert the XML into your domain model, and then write the domain model back to XML.

package forum12425401;

import java.io.File;
import javax.xml.bind.*;

public class Demo {

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

        Unmarshaller unmarshaller = jc.createUnmarshaller();
        File xml = new File("src/forum12425401/input.xml");
        Person person = (Person) unmarshaller.unmarshal(xml);

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

}

input.xml/Output

<?xml version="1.0" encoding="UTF-8"?>
<person>
   <firstname>Joe</firstname>
   <lastname>Walnes</lastname>
   <phone value="1234-456"/>
   <fax value="9999-999"/>
</person>
bdoughan
  • 147,609
  • 23
  • 300
  • 400
  • Thanks a lot for such a detailed answer. I found JAXB investigating this issue and @XmlPath was the first thing I was concerning. Could you please give brief explanation on now it works, I mean does it require any additional elements to be pulled out? Performance and data efficiency are strongest requirements for me for now. – Viktor Stolbin Sep 21 '12 at 06:25
  • 1
    @ViktorStolbin - `@XmlPath` is actually an extension that is provided in the EclipseLink MOXy implementation of the JAXB (JSR-222) specification. MOXy supports a subset of XPath that can be process during single pass depth first traversal of an XML document. It is a very efficient process. – bdoughan Sep 21 '12 at 14:44
0

See "Attribute aliasing" section of the XStream Alias tutorial: http://x-stream.github.io/alias-tutorial.html .

facundofarias
  • 2,973
  • 28
  • 27
Olaf
  • 6,249
  • 1
  • 19
  • 37