James Murphy
James Murphy

Reputation: 798

Jackson XML Bind Element based on value of attributes

I have the following XML structure:

<participants>
    <participant side="AWAY">
        <team id="18591" name="Orlando Apollos" />
    </participant>
    <participant side="HOME">
        <team id="18594" name="Memphis Express" />
    </participant>
</participants>

If I am using the FasterXML Jackson library with JAXB annotations how can I bind the participants fields to two different Participant objects participantHome and participantAway using the side property of AWAY and HOME to bind the fields.

Using the following object won't work obviously because there are duplicate fields:

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

@XmlRootElement(name = "participants")
public class Participants {

    @XmlElement(name = "participant")
    Participant participantHome;

    @XmlElement(name = "participant")
    Participant participantAway;
}

How can I dynamically bind those elements using JAXB annotations or a custom JAXB implementation?

Upvotes: 1

Views: 2993

Answers (3)

James Murphy
James Murphy

Reputation: 798

Couple of great answers and alternatives here but I've decided to go with a hybrid of binding with a list and returning the correct home or away team by implementing getter methods that return the correct home or away team to essentially flatten the List. This will reduce the amount of computation when processing the lists throughout the application.

I added the following code to my parent class (to each home/away participant):

Participant getHome() {
    return (Participant) participants.stream()
            .filter(p -> p.getSide().equalsIgnoreCase("home"));
}

Participant getAway() {
    return (Participant) participants.stream()
            .filter(p -> p.getSide().equalsIgnoreCase("away"));
}

Thanks for the help!

Upvotes: 0

Michał Ziober
Michał Ziober

Reputation: 38625

You need to write custom deserialiser because there is no annotation which allow to bind list item to given property in object. If you already use Jackson try to implement custom JsonDeserializer instead of custom XmlAdapter. We can simplify our custom deserialiser by deserialising internal Participant objects to Map. Simple example:

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.type.MapType;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

public class XmlMapperApp {

    public static void main(String[] args) throws Exception {
        File xmlFile = new File("./resource/test.xml").getAbsoluteFile();

        XmlMapper xmlMapper = new XmlMapper();

        Participants result = xmlMapper.readValue(xmlFile, Participants.class);
        System.out.println(result);
    }
}

class ParticipantsXmlAdapter extends JsonDeserializer<Participants> {

    @Override
    public Participants deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
        List<Map<String, Object>> participants = readParticipantsMap(p, ctxt);

        Participants result = new Participants();
        for (Map<String, Object> participantMap : participants) {
            Object side = participantMap.get("side").toString();
            if ("AWAY".equals(side)) {
                result.setParticipantAway(convert((Map<String, Object>) participantMap.get("team")));
            } else if ("HOME".equals(side)) {
                result.setParticipantHome(convert((Map<String, Object>) participantMap.get("team")));
            }
        }

        return result;
    }

    private List<Map<String, Object>> readParticipantsMap(JsonParser p, DeserializationContext ctxt) throws IOException {
        MapType mapType = ctxt.getTypeFactory().constructMapType(Map.class, String.class, Object.class);
        JsonDeserializer<Object> mapDeserializer = ctxt.findRootValueDeserializer(mapType);
        List<Map<String, Object>> participants = new ArrayList<>();
        p.nextToken(); // skip Start of Participants object
        while (p.currentToken() == JsonToken.FIELD_NAME) {
            p.nextToken(); // skip start of Participant
            Object participant = mapDeserializer.deserialize(p, ctxt);
            participants.add((Map<String, Object>) participant);
            p.nextToken(); // skip end of Participant
        }

        return participants;
    }

    private Participant convert(Map<String, Object> map) {
        Participant participant = new Participant();
        participant.setId(Integer.parseInt(map.get("id").toString()));
        participant.setName(map.get("name").toString());

        return participant;
    }
}

@JsonDeserialize(using = ParticipantsXmlAdapter.class)
class Participants {

    private Participant participantHome;
    private Participant participantAway;

    // getters, setters, toString
}

class Participant {
    private int id;
    private String name;

    // getters, setters, toString
}

prints:

Participants{participantHome=Participant{id=18594, name='Memphis Express'}, participantAway=Participant{id=18591, name='Orlando Apollos'}}

Upvotes: 1

Anurag Dwivedi
Anurag Dwivedi

Reputation: 174

You can use List of Participant in place of two different participant. Annotate side with @XmlAttribute(name = "side", required = true). Then create two different Participant objects and add them to list.

Upvotes: 1

Related Questions