tags:

views:

7398

answers:

5

I'm using JAXB to read and write XML. What I want is to use a base JAXB class for marshalling and an inherited JAXB class for unmarshalling. This is to allow a sender Java application to send XML to another receiver Java application. The sender and receiver will share a common JAXB library. I want the receiver to unmarshall the XML into a receiver specific JAXB class which extends the generic JAXB class.

Example:

This is the common JAXB class which is used by the sender.

@XmlRootElement(name="person")
public class Person {
    public String name;
    public int age;
}

This is the receiver specific JAXB class used when unmarshalling the XML. The receiver class has logic specific to the receiver application.

@XmlRootElement(name="person")
public class ReceiverPerson extends Person {
    public doReceiverSpecificStuff() ...
}

Marshalling works as expected. The problem is with unmarshalling, it still unmarshals to Person despite the JAXBContext using the package name of the subclassed ReceiverPerson.

JAXBContext jaxbContext = JAXBContext.newInstance(package name of ReceiverPerson);

What I want is to unmarshall to ReceiverPerson. The only way I've been able to do this is to remove @XmlRootElement from Person. Unfortunately doing this prevents Person from being marshaled. It's as if JAXB starts at the base class and works its way down until it finds the first @XmlRootElement with the appropriate name. I've tried adding a createPerson() method which returns ReceiverPerson to ObjectFactory but that doesn't help.

+2  A: 

I am not sure why you would want to do this... it doesn't seem all that safe to me.

Consider what would happen in ReceiverPerson has additional instance variables... then you would wind up with (I guess) those variables being null, 0, or false... and what if null is not allowed or the number must be greater than 0?

I think what you probably want to do is read in the Person and then construct a new ReceiverPerson from that (probably provide a constructor that takes a Person).

TofuBeer
ReceiverPerson does not have an additional variables. Its purpose is to do receiver specific stuff.
Steve Kuo
You are still going to have to, at some level, create it as a different class... and the normal way of doing that is via constructor that takes the type you want to convert from. Also, just because that is the case now doesn't mean you won't add vars in the future.
TofuBeer
Yikes, that is way to much speculation about what a class might do in the future. If it's good enough for today then it's good enough, and you can always refactor later...
Amber Shah
Consumers of public APIs love when they change... not. Refactoring is fine for internal things, but anything that is public it is not. The whole point of refactoring is to change the way things work behind the scenes without breaking things. If you are changing the public API you are not refactoring, you are re-writing.
TofuBeer
+8  A: 

You're using JAXB 2.0 right? (since JDK6)

There is a class:

javax.xml.bind.annotation.adapters.XmlAdapter<ValueType,BoundType>

which one can subclass, and override following methods:

public abstract BoundType unmarshal(ValueType v) throws Exception;
public abstract ValueType marshal(BoundType v) throws Exception;

Example:

public class YourNiceAdapter
        extends XmlAdapter<ReceiverPerson,Person>{

    @Override public Person unmarshal(ReceiverPerson v){
        return v;
    }
    @Override public ReceiverPerson marshal(Person v){
        return new ReceiverPerson(v); // you must provide such c-tor
    }
}

Usage is done by as following:

@Your_favorite_JAXB_Annotations_Go_Here
class SomeClass{
    @XmlJavaTypeAdapter(YourNiceAdapter.class)
    Person hello; // field to unmarshal
}

I'm pretty sure, by using this concept you can control the marshalling/unmarshalling process by yourself (including the choice the correct [sub|super]type to construct).

ivan_ivanovich_ivanoff
I tried this and it doesn't work. No matter what I try, ObjectFactory or my XmlAdapter never gets called. From what I've read Sun's JAXB resolves through static class references. It appears that Glassfish's implementation has more promise https://jaxb.dev.java.net/guide/Adding_behaviors.html
Steve Kuo
XmlJavaAdapters are suited to make non-JAXB-annotated classes usable with JAXB. Because Person and ReceiverPerson (and/or their superlass, if any) have JAXB annotations, it doesn't work. You need Person and ReceiverPerson without JAXB, plus adapters for both.
ivan_ivanovich_ivanoff
Have you tried swapping types for marshall|unmarshall? [...] extends XmlAdapter<Person,ReceiverPerson> [...] @Override public ReceiverPerson unmarshal(Person v){ return new ReceiverPerson(v); } @Override public Person marshal(ReceiverPerson v){ return v; }
ivan_ivanovich_ivanoff
Please ignore the first comment ;) It MUST work also on JAXB-annotated types, why not? (Well I coudn't find something in the spec).Just replace "extends XmlAdapter<ReceiverPerson,Person>" with "extends XmlAdapter<Person,ReceiverPerson>" and the words marshal<->unmarshall, as said in second comment
ivan_ivanovich_ivanoff
A: 

Since you really have two separate apps, compile them with different versions of the class "Person" - with the receiver app not having @XmlRootElement(name="person") on Person. Not only is this ugly, but it defeats the maintainability you wanted from using the same definition of Person for both sender and receiver. Its one redeeming feature is that it works.

13ren
I want both apps to share the common base JAXB classes.
Steve Kuo
@Steve: but my second ("neat way") solution does share common base JAXB classes... it looks like you only read the first paragraph, and I wasn't clear that I was giving two solutions. I'll edit.
13ren
I separated "neat way" into a new answer.
13ren
+1  A: 

The following snippet is a method of a Junit 4 test with a green light:

@Test
public void testUnmarshallFromParentToChild() throws JAXBException {
  Person person = new Person();
  int age = 30;
  String name = "Foo";
  person.name = name;
  person.age= age;

  // Marshalling
  JAXBContext context = JAXBContext.newInstance(person.getClass());
  Marshaller marshaller = context.createMarshaller();

  StringWriter writer = new StringWriter();
  marshaller.marshal(person, writer);

  String outString = writer.toString();

  assertTrue(outString.contains("</person"));

  // Unmarshalling
  context = JAXBContext.newInstance(Person.class, RecieverPerson.class);
  Unmarshaller unmarshaller = context.createUnmarshaller();
  StringReader reader = new StringReader(outString);
  RecieverPerson reciever = (RecieverPerson)unmarshaller.unmarshal(reader);

  assertEquals(name, reciever.name);
  assertEquals(age, reciever.age);
}

The important part is the use of the JAXBContext.newInstance(Class... classesToBeBound) method for the unmarshalling context:

 context = JAXBContext.newInstance(Person.class, RecieverPerson.class);

With this call, JAXB will compute a reference closure on the classes specified and will recognize RecieverPerson. The test passes. And if you change the parameters order, you'll get a java.lang.ClassCastException (so they must be passed in this order).

Pascal Thivent
A: 

Subclass Person twice, once for receiver and once for sender, and only put the XmlRootElement on these subclassses (leaving the superclass, Person, without an XmlRootElement). Note that sender and receiver both share the same JAXB base classes.

@XmlRootElement(name="person")
public class ReceiverPerson extends Person {
  // receiver specific code
}

@XmlRootElement(name="person")
public class SenderPerson extends Person {
  // sender specific code (if any)
}

// note: no @XmlRootElement here
public class Person {
  // data model + jaxb annotations here
}

[tested and confirmed to work with JAXB]. It circumvents the problem you note, when multiple classes in the inheritance hierarchy have the XmlRootElement annotation.

This is arguably also a neater and more OO approach, because it separates out the common data model, so it's not a "workaround" at all.

13ren