Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JAXB unmarshalling tree structure

I have XML input which is essentially a tree structure. The goal is to (un)marshall the code into Java classes.

<config>

<key-value-pair>
   <key>Key1</key>
   <value>Value1</value>
</key-value-pair>

<key-value-pair>
   <key>Key2</key>
   <value>
      <key-value-pair>
         <key>Subkey2</key>
         <value>Value999</value>
      </key-value-pair>
   </value>
</key-value-pair>

</config>

The XML contains typical Key/Value pairs. And each Value can contain either another Key/Value pair, or List Key/Value pairs or just a single String value.

@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
static class KeyValuePair {

    @XmlElement(name="key")
    private String key;

    @XmlElement(name="value")
    private String value;   // here I don't know how to reflect
                            // the choice of String or another
                            // list of KeyValuePair objects
    @XmlElement(name="value")
    private List<KeyValuePair> valuePairs;

    // getters/setters here
}

Then I have just another wrapper Class

@XmlRootElement(name="config")
@XmlAccessorType(XmlAccessType.FIELD)
static class Structure {

    @XmlElement(name="key-value-pair")
    private List<KeyValuePair> keyValuePair;

    // getters/setters
 }

And here is the logic I am trying to use for (un)marshaling.

 Unmarshaller jaxbUnmarshaller = jc.createUnmarshaller();
 StringReader reader = new StringReader(input);
 Structure struct = (Structure) jaxbUnmarshaller.unmarshal(reader);
 // struct doesn't get the data correctly ...

This is what I got so far. It doesn't work the way I have it, but I am hoping I got myself clear what the end goal should look like.

I want to take the XML at the beginning and put in the instance of Structure class.

like image 269
Albrecht Avatar asked Nov 09 '22 05:11

Albrecht


1 Answers

You may well have already thought of this... If you start by modeling it at schema level like this;

<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:complexType name="key-value-pair">
    <xs:sequence>
     <xs:element name="key" type="xs:string"/>
     <xs:element name="value">
        <xs:complexType mixed="true">
            <xs:choice>
                <xs:element name="key-value-pair" type="key-value-pair" minOccurs="0"/>
            </xs:choice>
        </xs:complexType>
     </xs:element>
    </xs:sequence>
  </xs:complexType>
  <xs:element name="config">
    <xs:complexType>
        <xs:sequence>
            <xs:element name="key-value-pair" type="key-value-pair" minOccurs="1" maxOccurs="unbounded"/>
        </xs:sequence>  
    </xs:complexType>
  </xs:element>
</xs:schema>

and then use XJC Ant Task or call it programmatically or whatever to generate the JAXB classes you end up with

package uk.co.his.test.model;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementRef;
import javax.xml.bind.annotation.XmlMixed;
import javax.xml.bind.annotation.XmlType;


/**
 * <p>Java class for key-value-pair complex type.
 * 
 * <p>The following schema fragment specifies the expected content contained within this class.
 * 
 * <pre>
 * &lt;complexType name="key-value-pair"&gt;
 *   &lt;complexContent&gt;
 *     &lt;restriction base="{http://www.w3.org/2001/XMLSchema}anyType"&gt;
 *       &lt;sequence&gt;
 *         &lt;element name="key" type="{http://www.w3.org/2001/XMLSchema}string"/&gt;
 *         &lt;element name="value"&gt;
 *           &lt;complexType&gt;
 *             &lt;complexContent&gt;
 *               &lt;restriction base="{http://www.w3.org/2001/XMLSchema}anyType"&gt;
 *                 &lt;choice&gt;
 *                   &lt;element name="key-value-pair" type="{}key-value-pair" minOccurs="0"/&gt;
 *                 &lt;/choice&gt;
 *               &lt;/restriction&gt;
 *             &lt;/complexContent&gt;
 *           &lt;/complexType&gt;
 *         &lt;/element&gt;
 *       &lt;/sequence&gt;
 *     &lt;/restriction&gt;
 *   &lt;/complexContent&gt;
 * &lt;/complexType&gt;
 * </pre>
 * 
 * 
 */
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "key-value-pair", propOrder = {
    "key",
    "value"
})
public class KeyValuePair {

    @XmlElement(required = true)
    protected String key;
    @XmlElement(required = true)
    protected KeyValuePair.Value value;

    /**
     * Gets the value of the key property.
     * 
     * @return
     *     possible object is
     *     {@link String }
     *     
     */
    public String getKey() {
        return key;
    }

    /**
     * Sets the value of the key property.
     * 
     * @param value
     *     allowed object is
     *     {@link String }
     *     
     */
    public void setKey(String value) {
        this.key = value;
    }

    /**
     * Gets the value of the value property.
     * 
     * @return
     *     possible object is
     *     {@link KeyValuePair.Value }
     *     
     */
    public KeyValuePair.Value getValue() {
        return value;
    }

    /**
     * Sets the value of the value property.
     * 
     * @param value
     *     allowed object is
     *     {@link KeyValuePair.Value }
     *     
     */
    public void setValue(KeyValuePair.Value value) {
        this.value = value;
    }


    /**
     * <p>Java class for anonymous complex type.
     * 
     * <p>The following schema fragment specifies the expected content contained within this class.
     * 
     * <pre>
     * &lt;complexType&gt;
     *   &lt;complexContent&gt;
     *     &lt;restriction base="{http://www.w3.org/2001/XMLSchema}anyType"&gt;
     *       &lt;choice&gt;
     *         &lt;element name="key-value-pair" type="{}key-value-pair" minOccurs="0"/&gt;
     *       &lt;/choice&gt;
     *     &lt;/restriction&gt;
     *   &lt;/complexContent&gt;
     * &lt;/complexType&gt;
     * </pre>
     * 
     * 
     */
    @XmlAccessorType(XmlAccessType.FIELD)
    @XmlType(name = "", propOrder = {
        "content"
    })
    public static class Value {
        @XmlElementRef(name = "key-value-pair", type = JAXBElement.class, required = false)
        @XmlMixed
        protected List<Serializable> content;

        /**
         * Gets the value of the content property.
         * 
         * <p>
         * This accessor method returns a reference to the live list,
         * not a snapshot. Therefore any modification you make to the
         * returned list will be present inside the JAXB object.
         * This is why there is not a <CODE>set</CODE> method for the content property.
         * 
         * <p>
         * For example, to add a new item, do as follows:
         * <pre>
         *    getContent().add(newItem);
         * </pre>
         * 
         * 
         * <p>
         * Objects of the following type(s) are allowed in the list
         * {@link String }
         * {@link JAXBElement }{@code <}{@link KeyValuePair }{@code >}
         * 
         * 
         */
        public List<Serializable> getContent() {
            if (content == null) {
                content = new ArrayList<Serializable>();
            }
            return this.content;
        }

    }

}

Plus an ObjectFactory and a Config class (which is essentially your Structure class). The ObjectFactory gives us some 'sugar' to help with the horrible JAXBElement dance.

As Andreas said that will still allow String and nested KeyValuePair(s). You would then have to wrap the Config class with a validator to check that wasn't happening; for example;

package uk.co.his.test;

import java.io.Serializable;
import java.util.List;

import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;

import uk.co.his.test.model.Config;
import uk.co.his.test.model.KeyValuePair;

public class Validate {


    public void validate(Config c) throws JAXBException
    {
        for(KeyValuePair kvp: c.getKeyValuePair())
        {
            validate(kvp);  
        }
    }

    public void validate(KeyValuePair kv) throws JAXBException
    {
        List<Serializable> mixed = kv.getValue().getContent();
        boolean nonWhitespaceStringFound = false;
        boolean kvpFound = false;
        for(Serializable c: mixed)
        {
            if(c instanceof String)
            {
                String s  = (String) c;
                if(s.trim().length()>0) {
                    nonWhitespaceStringFound = true;
                }
            }
            else
            {
                @SuppressWarnings("unchecked")
                JAXBElement<KeyValuePair> t = (JAXBElement<KeyValuePair>) c;
                KeyValuePair child = t.getValue();
                kvpFound = true;
                validate(child);
            }
            if(kvpFound && nonWhitespaceStringFound) {
                throw new JAXBException("KeyValuePair "+kv.getKey()+" value element contained String data and nested KeyValuePair(s)");
            }
        }
    }
}

To create a config you have to do the JAXBElement dance;

private static final File Test1Out = new File("files/test1.xml");
@Test
public void test1() throws JAXBException
{
    ObjectFactory of = new ObjectFactory();
    Config c = new Config();
    c.getKeyValuePair().add(createKVPair(of, 2, 2, "a", "one"));
    c.getKeyValuePair().add(createKVPair(of, 2, 1, "b", "two"));
    c.getKeyValuePair().add(createKVPair(of, 0, 0, "c", "three"));
    JAXBContext jbc = JAXBContext.newInstance("uk.co.his.test.model");
    Marshaller m = jbc.createMarshaller();
    m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
    m.marshal(c, Test1Out);

    Unmarshaller u = jbc.createUnmarshaller();
    Config c2 = (Config) u.unmarshal(Test1Out);
    Assert.assertTrue("Round trip produces different things", c2.getKeyValuePair().size() ==3);
}

private KeyValuePair createKVPair(ObjectFactory of, int depth, int length, String initialKey, String value) {
    KeyValuePair kv = new KeyValuePair();
    kv.setKey(initialKey);
    Value v = new Value();
    kv.setValue(v);
    if(depth==0)
    {
        v.getContent().add(value);
    }
    else 
    {
        int newdepth = --depth;
        for(int i = 0; i < length; i++)
        {
            v.getContent().add(of.createKeyValuePairValueKeyValuePair(createKVPair(of, newdepth, length, initialKey+depth, value+i)));
        }
    }
    return kv;
}

Well, it's a start...

like image 129
JFK Avatar answered Nov 14 '22 21:11

JFK