Naruto Uzumaki
Naruto Uzumaki

Reputation: 998

JAXB - XmlElement with multiple names and types

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

Answers (2)

Bjorn Loftis
Bjorn Loftis

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

Robby Cornelissen
Robby Cornelissen

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

Related Questions