Reputation: 998
I have the following class hierarchy:
@XmlRootElement
public abstract class Animal{}
@XmlRootElement
public class Dog extends Animal{}
@XmlRootElement
public class Cat extends Animal{}
@XmlRootElement
public class Lion extends Animal{}
and a class which has an attribute named animal:
@XmlRootElement
public class Owner{
private Animal animal;
}
I would like to allow different XML Schemas as follows and bind the Animal Type in the schema to animal object
in Owner class
<Owner>
<Dog></Dog>
</Owner>
<Owner>
<Cat></Cat>
</Owner>
<Owner>
<Lion></Lion>
</Owner>
The solutions that I have found use XmlElements
which can take multiple XmlElement
fields and creates a collection. However, in my case I don't need a collection but a single attribute.
Does JAXB allow any XmlElement multiple naming convention for this problem? Is there any other annotation which could solve this problem?
Note: I have looked at multiple answers to similar questions in stackoverflow and around but almost all of them create a collection instead of a single object. The closest answer I have found is this : @XmlElement with multiple names
Edit : I think this solution might work. Have to test it out
Upvotes: 2
Views: 10781
Reputation: 156
I want to offer an alternative solution. The previous solution is fine - but you'll notice that the @XmlElements annotation creates strong dependencies between the Owner.class - and the concrete implementations of your animals (Dog.class, Cat.class, Lion.class) This can be source of frustration - causing you to recompile your Owner class every time you add a new implementation of Animal. (We have a microservice architecture and continuous delivery - and couplings of this sort were not ideal for our build process...)
Instead - consider this decoupled solution. New animal implementations can be created and used - without recompiling the Owner class - satisfying the Open Closed principle.
Start with an Owner class that defines an Abstract Animal element.
package com.bjornloftis.domain;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
@XmlRootElement(name = "owner")
public class Owner {
@XmlElement(name = "animal")
@XmlJavaTypeAdapter(AnimalXmlAdapter.class)
private Animal animal;
public Owner() {
}
public Owner(Animal animal) {
this.animal = animal;
}
public Animal getAnimal() {
return animal;
}
}
Now you'll need an abstract class and a interface. This will be important for marshalling and unmarshalling.
package com.bjornloftis.domain;
import javax.xml.bind.annotation.XmlTransient;
@XmlTransient
public abstract class Animal implements AnimalType{
}
The AnimalType interface defines a method that ensures at runtime that JaxB can determine which implementation should be used to unmarshall an XML document. It is used by our XmlAdapter - which you will see shortly. Otherwise - JAXB would not be able to derive the implementing class at runtime.
package com.bjornloftis.domain;
import javax.xml.bind.annotation.XmlAttribute;
public interface AnimalType {
@XmlAttribute(name = "type")
String getAnimalType();
}
Now - you'll have a wrapper for your animal - and the animal implementation itself. This can be compiled separately from the owner. Not coupled at compile time.
package com.bjornloftis.domain;
import javax.xml.bind.annotation.*;
@XmlRootElement(name = "animal")
@XmlAccessorType(XmlAccessType.FIELD)
public class DogWrapper extends Animal {
private Dog dog;
public DogWrapper(){
}
public DogWrapper(Dog dog) {
dog = dog;
}
public Dog getDog() {
return dog;
}
public void setError(Dog dog) {
this.dog = dog;
}
@Override
@XmlAttribute(name = "type")
public String getAnimalType(){
return "dog";
}
}
And the animal itself:
package com.bjornloftis.domain;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "dog")
public class Dog {
@XmlElement(name = "name")
private String name;
public Dog() {
}
}
Finally - to tie it all together - you'll need to implement the XmlAdapter - which will facilitate marshalling and unmarshalling.
package com.bjornloftis.domain;
import javax.xml.bind.Binder;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.annotation.adapters.XmlAdapter;
import org.w3c.dom.Node;
import com.bjornloftis.registry.PropertyRegistryFactory;
public class AnimalXmlAdapter extends XmlAdapter<Object, Animal> {
@Override
public Animal unmarshal(Object elementNSImpl) throws Exception {
Node node = (Node)elementNSImpl;
String simplePayloadType = node.getAttributes().getNamedItem("type").getNodeValue();
Class<?> clazz = PropertyRegistryFactory.getInstance().findClassByPropertyName(simplePayloadType);
JAXBContext jc = JAXBContext.newInstance(clazz);
Binder<Node> binder = jc.createBinder();
JAXBElement<?> jaxBElement = binder.unmarshal(node, clazz);
return (Animal)jaxBElement.getValue();
}
@Override
public Animal marshal(Animal animal) throws Exception {
return animal;
}
}
Finally - we need to associate the type "dog" with the wrapper class DogWrapper.class This is done with a registry that we initialize at runtime in the code that will marshall or unmarshall dogs.
package com.bjornloftis.registry;
import com.bjornloftis.registry.PropertyRegistry;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class PropertyRegistryFactory {
private static final Map<String, Class<?>> DEFAULT_REGISTRY = new ConcurrentHashMap();
public PropertyRegistryFactory() {
}
public static final PropertyRegistry getInstance() {
return new PropertyRegistry(DEFAULT_REGISTRY);
}
public static final void setDefaultRegistry(Map<String, Class<?>> defaultRegistry) {
DEFAULT_REGISTRY.putAll(defaultRegistry);
}
}
This is all extracted from our production code - and somewhat sanitized to remove proprietary IP.
If it is hard to follow - let me know in a comment - and I'll bundle it all up into a working project on github.
Again, understood to be a much more complicated solution - but necessary to avoid coupling our code. An additional benefit is this also works with Jackson's libraries pretty seamlessly for JSON. For JSON marshalling and unmarshalling - we have a similar set of annotations using the TypeIdResolver - which provides a function analogous to the XmlAdapter for JAXB.
The end result is that you can marshall and unmarshall the following - but without the nasty compile time coupling that @XmlElements introduces:
<owner>
<animal type="dog">
<dog>
<name>FIDO</name>
</dog>
</animal>
</owner>
Upvotes: 3
Reputation: 97150
I got it to work using the @XmlElements
annotation, as follows:
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElements;
import javax.xml.bind.annotation.XmlRootElement;
import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
public class Main {
public static void main(String[] args) throws JAXBException {
String xml = "<owner><dog></dog></owner>";
JAXBContext jaxbContext = JAXBContext.newInstance(Owner.class);
Unmarshaller jaxbUnmarshaller = jaxbContext.createUnmarshaller();
Owner owner = (Owner) jaxbUnmarshaller.unmarshal(
new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)));
System.out.println(owner.getAnimal().getClass());
}
}
abstract class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}
class Lion extends Animal {}
@XmlRootElement
class Owner {
@XmlElements({
@XmlElement(name = "dog", type = Dog.class),
@XmlElement(name = "cat", type = Cat.class),
@XmlElement(name = "lion", type = Lion.class)
})
private Animal animal;
public Animal getAnimal() {
return animal;
}
}
Using the default JAXB implementation that ships with the Oracle Java 8 SDK, this prints out:
class Dog
Upvotes: 3