2

I have a root Xml document (name = "Entity") that contains one known Xml element (name = "Header") and another Xml element of unknown name but it is known to have an inner XmlElement(name="label")

Here are possible Xmls:

<Entity>
   <Header>this is a header</Header>
   <a>
     <label>this is element A</label>
     <otherElements/>
   </a>
</Entity>

<Entity>
   <Header>this is a different header</Header>
   <b>
     <label>this is some other element of name b</label>
     <others/>
   </b>
</Entity>

Here are my JAXB annotated classes:

@XmlRootElement(name = "Entity")
@XmlAccessorType(XmlAccessType.NONE)
public class Entity {
    @XmlElement(name = "Header")
    private Header header;

    @XmlElements( {
       @XmlElement(name = "a", type=LabelledElement.A.class), 
       @XmlElement(name = "b", type=LabelledElement.B.class)
    } )
    private LabelledElement labelledElement;

    // constructors, getters, setters...
}

@XmlAccessorType(XmlAccessType.NONE)
public abstract class LabelledElement {
     @XmlElement
     private String label;
     @XmlAnyElement
     private List<Element> otherElements;

     public static class A extends LabelledElement {}
     public static class B extends LabelledElement {}
}

This was working great! But then I noticed that it isn't only <a> and <b>

It could be <c>, <asd> and even <anything>...

So listing the XmlElement(name = "xyz", type = LabelledElement.xyz.class) is obviously not the right solution.

All I care about is Entity#getLabelledElement()#getLabel() no matter what the LabelledElement name is.

Is this even possible with JAXB?

user640853
  • 163
  • 10
  • I am also a bit stuck in a similar issue during the `unmarshalling`. I am trying to `unmarshal` unknown elements into `Map` but it's not working as expected. I have posted the complete question in the below-provided link. If you get a chance can you please have a look at it and provide your suggestion: https://stackoverflow.com/questions/67648941/jaxb-moxy-unmarshalling-assigns-all-field-values-to-mapstring-object-rather-th – BATMAN_2008 May 23 '21 at 18:31

2 Answers2

1

With EclipseLink JAXB Implementation (MOXy), this should work :

@XmlRootElement(name = "Entity")
@XmlSeeAlso({LabelledElement.class}) //Might not be necessary
@XmlAccessorType(XmlAccessType.NONE)
public class Entity {
    @XmlElement(name = "Header")
    private Header header;

    @XmlPath("child::*[position() = 2]")
    @XmlJavaTypeAdapter(MapAdapter.class)
    private Map<String,LabelledElement> labelledElementMap;


    public LabelledElement getLabelledElement(){
         return labelledElementMap.values().get(0);
    }
    // constructors, getters, setters...
}

The MapAdapter class :

public class MapAdapter extends XmlAdapter<MapAdapter.AdaptedMap, Map<String, LabelledElement>> {

    public static class AdaptedMap {

        @XmlVariableNode("key")
        List<LabbeledElement> entries = new ArrayList<LabbeledElement>();

    }

    public static class AdaptedEntry {

        @XmlTransient
        public String key;

        @XmlElement
        public LabelledElement value;

    }

    @Override
    public AdaptedMap marshal(Map<String, LabelledElement> map) throws Exception {
        AdaptedMap adaptedMap = new AdaptedMap();
        for(Entry<String, LabelledElement> entry : map.entrySet()) {
            AdaptedEntry adaptedEntry = new AdaptedEntry();
            adaptedEntry.key = entry.getKey();
            adaptedEntry.value = entry.getValue();
            adaptedMap.entries.add(adaptedEntry);
        }
        return adaptedMap;
    }

    @Override
    public Map<String, LabelledElement> unmarshal(AdaptedMap adaptedMap) throws Exception {
        List<AdaptedEntry> adaptedEntries = adaptedMap.entries;
        Map<String, LabelledElement> map = new HashMap<String, LabelledElement>();
        for(AdaptedEntry adaptedEntry : adaptedEntries) {
            map.put(adaptedEntry.key, adaptedEntry.value);
        }
        return map;
    }

}

For reference, my solution is inspired by this link.

Dimpre Jean-Sébastien
  • 1,067
  • 1
  • 6
  • 14
  • Thanks for your answer, however, this does not help. I am marshalling the Xml into File even though I don't care about the "otherElements" but I need to read the label value. – user640853 Sep 27 '17 at 13:48
  • @user640853 Sorry it didn't help, could you explain a bit more what result you're expecting when you're marshalling ? – Dimpre Jean-Sébastien Sep 27 '17 at 13:54
  • I need to marshall the Xml as is, meaning all the elements and attributes under the `labelledElement` are kept but I need to read the value of label `
    header
    ` With your solution, I would lose the `otherElements` element
    – user640853 Sep 27 '17 at 14:35
  • @user640853 Changed the answer, please check if it works for you. – Dimpre Jean-Sébastien Sep 28 '17 at 04:38
  • Switching to MOXy impl is not feasible; however, this answers my question. Thank you – user640853 Sep 28 '17 at 08:49
  • I am also a bit stuck in a similar issue during the `unmarshalling`. I am trying to `unmarshal` unknown elements into `Map` but it's not working as expected. I have posted the complete question in the below-provided link. If you get a chance can you please have a look at it and provide your suggestion: https://stackoverflow.com/questions/67648941/jaxb-moxy-unmarshalling-assigns-all-field-values-to-mapstring-object-rather-th – BATMAN_2008 May 23 '21 at 18:31
0

Apparently it's possible with EclipseLink JAXB (MOXy) implementation, which allows you annotate an interface variable providing a Factory class and method to be used when binding XML to Java, see this answer.

(edited to provide an example of this approach) For instance, you instead of having an abstract class LabelledElement, have an interface LabelledElement:

public interface LabelledElement {
    String getLabel();
}

and then have classes A and B implement it like this:

import javax.xml.bind.annotation.XmlElement;

public class A implements LabelledElement{

    private String label;

    @Override
    @XmlElement(name="label")
    public String getLabel() {
        return label;
    }
}

and Entity class annotated like this:

@XmlRootElement(name = "Entity")
@XmlAccessorType(XmlAccessType.NONE)
public class Entity {
    @XmlElement(name = "Header")
    private Header header;

    @XmlRootElement
    @XmlType(
    factoryClass=Factory.class, 
    factoryMethod="createLabelledElement")
    private LabelledElement labelledElement;

    // constructors, getters, setters...
}

Then, as the answer I linked to suggests, you need a Factory class like:

import java.lang.reflect.*;
import java.util.*;

public class Factory {

    public A createA() {
        return createInstance(A.class);
    }

    public B createB() {
        return createInstance(B.class);;
    }

    private <T> T createInstance(Class<T> anInterface) {
        return (T) Proxy.newProxyInstance(anInterface.getClassLoader(), new Class[] {anInterface}, new InterfaceInvocationHandler());
    }

    private static class InterfaceInvocationHandler implements InvocationHandler {

        private Map<String, Object> values = new HashMap<String, Object>();

        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            String methodName = method.getName();
            if(methodName.startsWith("get")) {
                return values.get(methodName.substring(3));
            } else {
                values.put(methodName.substring(3), args[0]);
                return null;
            }
        }

    }
}
Guillem
  • 456
  • 4
  • 9
  • Well, not really! The answer shows how to annotate with JAXB for the same element name but with different types which is the opposite of the answer I'm looking for. – user640853 Sep 27 '17 at 10:30
  • @user640853 see my edited reply as an example of how I'd modify it by using an interface, a Factory class, and MOXy JAXB implementation. – Guillem Sep 27 '17 at 13:51