Using JAXB to pass subclass instances as superclass

14,269

Have you tried adding the following to your message class? The @XmlSeeAlso annotation will let the JAXBContext know about the subclasses.

import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlSeeAlso;

@XmlRootElement
@XmlSeeAlso(RegMessage.class)
public abstract class Message {

    Integer id;

}

Alternate Strategy:

Here is a link to a strategy I have helped people use:

Essentially you have one message object, and multiple individual message payloads. The relationship between the message and payload is handled through a @XmlAnyElement annotation.

Note on Transaction Handling

I noticed that you are handling your own transactions. Have you considered implementing your JAX-RS service as a session bean and leverage JTA for your transaction handling? For an example see:

Share:
14,269
Bret
Author by

Bret

BY DAY: Sys Admin / Code Monkey / Business owner. BY NIGHT: Father, husband, inventor. FOR FUN: Family board gamer, musician.

Updated on July 20, 2022

Comments

  • Bret
    Bret almost 2 years

    What I have is a set of Java classes (close to 25) representing message types. They all inherit from a Message class which I'd like to be abstract. Each message type adds a few additional fields to the set provided by the Message superclass.

    I'm implementing some RESTful web services using RESTeasy and would like to have methods like this:

    public Response persist(Message msg) {
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        try {
            em.persist(msg);
        } catch (Exception e) {
            e.printStackTrace();
        }
        tx.commit();
        em.close();
        return Response.created(URI.create("/message/" + msg.getId())).build();
    }
    

    instead of having 25 separate persist methods, each tailored to a particular message type.

    Currently, I've annotated my Message class like this:

    @MappedSuperclass
    @XmlRootElement(name = "message")
    public abstract class Message implements Serializable {
    
        @Id
        @GeneratedValue(strategy = GenerationType.AUTO)
        Integer id;
        @Embedded
        Header header;
        @Embedded
        SubHeader subHeader;
    

    My subclass then looks like this:

    @Entity
    @XmlRootElement(name="regmessage")
    @XmlAccessorType(XmlAccessType.FIELD)
    public class REGMessage extends Message {
    
        @XmlElement(required = true)
        int statusUpdateRate;
        @XmlElement(required = true)
        int networkRegistrationFlag;
    

    This creates a schema which looks like it should work, but all that's seen on the server side during a persist operation is a Message object (the subtype is completely lost, or at least it isn't marshalled back into its proper subtype). On the client side, to invoke the method I do this:

    REGMessage msg = new REGMessage();
    // populate its fields
    Response r = client.createMessage(msg);
    

    Is what I'm attempting possible? What JAXB magic do I need to use to make the translations happen the way they should -- ie, to treat everything in Java as if it's a Message to keep the number of methods down yet still preserve all the subtype-specific information?


    Thanks to Blaise's blog pointers, this now looks like it's on the road to working fully. Here's what I've got, and it does work:

    //JAXB annotations
    @XmlRootElement(name="message")
    @XmlAccessorType(XmlAccessType.FIELD)
    @XmlSeeAlso(REGMessage.class)
    //JPA annotations
    @MappedSuperclass
    public class Message {
    
        @Id
        @GeneratedValue(strategy = GenerationType.AUTO)
        @XmlAttribute
        private Integer id;
    
        private JICDHeader header;
        private int subheader;
    
        @XmlAnyElement
        @Transient
        private Object body;
    

    One of the problems I encountered this morning was a cryptic error from Hibernate about the number of columns being mismatched. Once I realized that "body" was being mapped into the table, I marked it transient and voila!

    @XmlRootElement(name="regmessage")
    @XmlAccessorType(XmlAccessType.FIELD)
    @Entity
    public class REGMessage extends Message {
    
        private int field1;
        private int field2;
    

    The only table generated from this code now is the regmessage table. On the RESTeasy side:

    @Path("/messages")
    public class MessageResource implements IMessageResource {
    
        private EntityManagerFactory emf;
        private EntityManager em;
    
        Logger logger = LoggerFactory.getLogger(MessageResource.class);
    
        public MessageResource() {
            try {
                emf = Persistence.createEntityManagerFactory("shepherd");
                em = emf.createEntityManager();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        @Override
        @POST
        @Consumes("application/xml")
        public Response saveMessage(Message msg) {
    
            System.out.println(msg.toString());
    
            logger.info("starting saveMessage");
            EntityTransaction tx = em.getTransaction();
            tx.begin();
    
            try {
                em.persist(msg);
            } catch (Exception e) {
                e.printStackTrace();
            }
    
            tx.commit();
            em.close();
            logger.info("ending saveMessage");
    
            return Response.created(URI.create("/message/" + msg.getId())).build();
        }
    }
    

    This implements an interface:

    @Path("/messages")
    public interface IMessageResource {
    
        @GET
        @Produces("application/xml")
        @Path("{id}")
        public Message getMessage(@PathParam("id") int id);
    
        @POST
        @Consumes("application/xml")
        public Response saveMessage(Message msg) throws URISyntaxException;
    
    }
    

    Marshalling & unmarshalling work as expected, and persistence is to the subclass's table (and there is no superclass table at all).

    I did see Blaise's note about JTA, which I may attempt to bring into this mix after I finish fleshing the Message & REGMessage classes back out fully.

  • Bret
    Bret over 13 years
    That looks perfect -- I'll give it a go. Thanks!
  • Bret
    Bret over 13 years
    How important is it to have the java members in the superclass be defined as attributes? Mine are complex types and I'm going to try to leave them as such.
  • Bret
    Bret over 13 years
    It appears to be rather important. Just converting this pretty simply and re-annotating my existing classes results in objects that, upon unmarshalling, aren't getting the body part. Those fields are all 0 or null and my type-specific fields are also getting lost.
  • bdoughan
    bdoughan over 13 years
    The approach from my blog involves having a generic Message object and specialized payloads. The message object is always an instance of the same class, and the payload is a different object outside the Message hierarchy identified by the root element.
  • Bret
    Bret over 13 years
    I also noticed that in your original example, the classes don't extend the Message class, where mine do. I'm now getting an error: "org.hibernate.MappingException: property mapping has wrong number of columns: com.techma.shepherd.messages.REGMessage.body type: object" which I presume comes from the fact that my REGMessage class has inherited the body attribute from Message. Digging back into my books again.
  • bdoughan
    bdoughan over 13 years
    Did you see my update about @XmlSee also, it may help your original setup.
  • Bret
    Bret over 13 years
    I did. What I'm planning to do now is start a new project and build this up from a basic class structure skeleton to get the XML working first. Then add the rest of the data members I need, and finally add the JPA annotations. Not that you'll be having sleepless nights, but I'll post back here with a final solution after I work it out. And yes, I did see the @XmlSeeAlso -- it had an effect but it didn't solve the whole problem. Time to clear the board & start over.
  • user3157855
    user3157855 almost 4 years
    One problem with @XmlSeeAlso is that the specified subclass has to be on the classpath during Java classes generation, a pretty huge problem if you want to split things into different projects/jars