Search code examples
javaxmljaxbmarshallingxmlbeans

JAXB: marshal/unmarshal different XML elements to/from one class


Let's say, I have the following class:

@XmlRootElement
class Payment {
    @XmlElement
    int amount;
    @XmlElement
    Currency currency;

    @XmlElement
    IResponse response;
}

If response==null - the element is a "request", otherwise - the element is a "response". When a request, the element should be (un)marshaled to (from) the root element called PaymentRequest, when a response - to (from) PaymentResponse.

How can I configure such marshaling algorithm? If JAXB can't do it, maybe some other engine can?


Solution

  • I've finally implemented the unmarshaling by intercepting StAX events. Here is the code:

    JAXBContext jc = JAXBContext.newInstance(RootElement.class, A.class, B.class, C.class, D.class, E.class);
    Unmarshaller unmarsh = jc.createUnmarshaller();
    XMLStreamReader xs = XMLInputFactory.newInstance().createXMLStreamReader(new StringReader(etalonRs));
    XMLStreamReader xd = new StreamReaderDelegate(xs) {
          public static final String ROOT_ELEMENT = "TestRoot";
          public static final int REPLACEABLE_LEVEL = 2;
          public final Collection<String> sufficesToDelete = Arrays.asList("Rq", "Rs");
    
          protected Stack<String> elementNamesStack = new Stack<>();
          protected Set<String> replaceableNames = new HashSet<>();
    
          @Override
          public String getLocalName() {
              String realName = super.getLocalName();
              if (replaceableNames.contains(realName) && elementNamesStack.size() == REPLACEABLE_LEVEL) {
                  for (String suffix : sufficesToDelete) {
                      if (realName.endsWith(suffix)) {
                          return realName.substring(0, realName.lastIndexOf(suffix));
                      }
                  }
              }
              return realName;
          }
    
          @Override
          public int next() throws XMLStreamException {
              final int eventCode = super.next();
             processLevel(eventCode);
              return eventCode;
          }
    
          @Override
          public int nextTag() throws XMLStreamException {
              final int eventCode = super.nextTag();
              processLevel(eventCode);
              return eventCode;
          }
    
          private void processLevel(int eventCode) {
              switch (eventCode) {
                  case XMLStreamReader.START_ELEMENT:
                      final String origElementName = super.getLocalName();
                      if ((elementNamesStack.size() + 1) == REPLACEABLE_LEVEL && elementNamesStack.peek().equals(ROOT_ELEMENT))
                          replaceableNames.add(origElementName);
                      elementNamesStack.push(origElementName);
                      break;
                  case XMLStreamReader.END_ELEMENT: 
                      assert(elementNamesStack.pop().equals(super.getLocalName()));
                      break;
    
              }
          }
      };
    
    Object o = unmarsh.unmarshal(xd);
    

    Here are my test classes. Yes, the real structure in production is more complicated - there are different "payments" and their elements are not in the root, so I've had to use the @XmlAnyElement annotation:

    @XmlAccessorType(XmlAccessType.FIELD)
    @XmlRootElement(name = "TestRoot")
    public static class RootElement {
        @XmlElement(name = "SomeDate")
        private Date dt = new Date();
    
        @XmlAnyElement(lax=true)
        private A a = new C();
    }
    
    @XmlAccessorType(XmlAccessType.FIELD)
    @XmlRootElement(name = "TestA")
    @XmlType
    public static abstract class A {
        private int fld1 = 1;
    
        @XmlAnyElement(lax=true)
        @XmlElementWrapper
        protected List<Object> list = new ArrayList<>();
    }
    
    @XmlAccessorType(XmlAccessType.FIELD)
    @XmlRootElement(name = "TestC")
    public static class C extends A {
        private int fld2  = 3;
    }
    

    Marshaling can be implemented in the same manner, but you have to write your "StreamWriterDelegate" from scratch.