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>
* <complexType name="key-value-pair">
* <complexContent>
* <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
* <sequence>
* <element name="key" type="{http://www.w3.org/2001/XMLSchema}string"/>
* <element name="value">
* <complexType>
* <complexContent>
* <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
* <choice>
* <element name="key-value-pair" type="{}key-value-pair" minOccurs="0"/>
* </choice>
* </restriction>
* </complexContent>
* </complexType>
* </element>
* </sequence>
* </restriction>
* </complexContent>
* </complexType>
* </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>
* <complexType>
* <complexContent>
* <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
* <choice>
* <element name="key-value-pair" type="{}key-value-pair" minOccurs="0"/>
* </choice>
* </restriction>
* </complexContent>
* </complexType>
* </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...