hippofan
hippofan

Reputation: 3

Marshal/unmarshal XML list containing elements with same name to different specific variables

Using MOXy (or any other java XML framework) is it possible to perform the following marshalling and unmarshalling between this xml and object:

<teacher>
    <field name="Age">30</field>
    <field name="Name">Bob</field>
    <field name="Course">Math</field>
</teacher>
public class Teacher {
   Field age;
   Field name;
   Field course;
}

public class Field {
    String name;
    String value;
}

There are solutions that work for marshalling (annotating all fields with @XmlElement(name= "field")) and there are some solutions that work for unmarshalling (@XmlPath("field[@name='Age']/text()") .

Is there however a solution that works both ways, or an approach that will unmarshal and marshal XML between these two formats?

Upvotes: 0

Views: 74

Answers (3)

hippofan
hippofan

Reputation: 3

This is an solution using MOXy

  @Test
  @SneakyThrows
  void testMarshal() {

    // Marshalling
    Teacher teacher =
        new Teacher(new Field("age", "30"), new Field("name", "Bob"), new Field("course", "Math"));

    JAXBContext context = JAXBContext.newInstance(Teacher.class);
    Marshaller marshaller = context.createMarshaller();
    marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);

    marshaller.marshal(teacher, System.out);

    // Unmarshalling
    String xml =
        """
        <teacher>
            <field name="age">30</field>
            <field name="name">Bob</field>
            <field name="course">Math</field>
        </teacher>""";

    Unmarshaller unmarshaller = context.createUnmarshaller();
    Teacher unmarshalledTeacher = (Teacher) unmarshaller.unmarshal(new StringReader(xml));

    System.out.printf("Unmarshalled: %s, %s, %s%n",
            unmarshalledTeacher.getAge(),
            unmarshalledTeacher.getName(),
            unmarshalledTeacher.getCourse());
  }
}

@XmlRootElement(name = "teacher")
@Getter
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PRIVATE)
class Teacher {
  @XmlPath("field[@name='age']")
  private Field age;

  @XmlPath("field[@name='name']")
  private Field name;

  @XmlPath("field[@name='course']")
  private Field course;
}

@Getter
@ToString
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PRIVATE)
class Field {
  @XmlAttribute(name = "name")
  private String name;

  @XmlValue
  private String value;

  void beforeMarshal(Marshaller m){
        name = null;
      }
}

Output:

<?xml version="1.0" encoding="UTF-8"?>
<teacher>
   <field name="age">30</field>
   <field name="name">Bob</field>
   <field name="course">Math</field>
</teacher>
Unmarshalled: Field(name=age, value=30), Field(name=name, value=Bob), Field(name=course, value=Math)

The beforeMarshal() is a dirty getaround for the fact that attributes are duplicated when marshalling. I made an issue here https://github.com/eclipse-ee4j/eclipselink/issues/2022

Upvotes: 0

Rick O&#39;Sullivan
Rick O&#39;Sullivan

Reputation: 476

Using JAXB's xjc tool to generate JAXB classes from an XML Schema yields:

xjc -no-header teacher.xsd

Teacher.java

package generated;

import java.util.*;
import jakarta.xml.bind.annotation.*;

@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "", propOrder = { "field" })
@XmlRootElement(name = "teacher")
public class Teacher
{
    @XmlElement(required = true)
    protected List<Field> field;
    public List<Field> getField()
    {
        if (field == null)
            field = new ArrayList<>();
        return this.field;
    }
}

Field.java

package generated;

import jakarta.xml.bind.annotation.*;

@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "", propOrder = { "value" })
@XmlRootElement(name = "field")
public class Field
{
    @XmlValue
    protected String value;
    public String getValue() { return value; }
    public void setValue(String value) { this.value = value; }

    @XmlAttribute(name = "name", required = true)
    protected String name;
    public String getName() { return name; }
    public void setName(String value) { this.name = value; }
}

From this XML schema:

teacher.xsd

<?xml version="1.0" encoding="UTF-8"?>
<xs:schema
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    elementFormDefault="qualified"
>

    <xs:element name="teacher">
        <xs:complexType>
            <xs:sequence>
                <xs:element maxOccurs="unbounded" ref="field"/>
            </xs:sequence>
        </xs:complexType>
    </xs:element>

    <xs:element name="field">
        <xs:complexType>
            <xs:simpleContent>
                <xs:extension base="xs:string">
                    <xs:attribute name="name" use="required" type="xs:string"/>
                </xs:extension>
            </xs:simpleContent>
        </xs:complexType>
    </xs:element>

</xs:schema>

To address the comment "I wish to not unmarshal my XML into a list of Field but into their own distinct "age", "name" and "course" variables.", here is a revised XML schema to inject properties for "age", "name" and "course":

teacher.xsd (revised)

<?xml version="1.0" encoding="UTF-8"?>
<xs:schema
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    xmlns:ci="http://jaxb.dev.java.net/plugin/code-injector"
    xmlns:jaxb="https://jakarta.ee/xml/ns/jaxb"
    jaxb:extensionBindingPrefixes="ci"
    elementFormDefault="qualified"
>
    <xs:element name="teacher">
        <xs:complexType>
            <xs:annotation>
                <xs:appinfo>
                    <ci:code>
<![CDATA[
    private java.util.Map<String,String> fieldMap;
    public java.util.Map<String,String> getFieldMap()
    {
        if ( fieldMap == null )
        {
            fieldMap = new java.util.HashMap<>();
            for ( Field field : getField() )
                fieldMap.put(field.getName(), field.getValue());
        }
        return fieldMap;
    }

    public String getAge() { return getFieldMap().get("Age"); }
    public void setAge(String value) { getFieldMap().put("Age", value); }
                    
    public String getName() { return getFieldMap().get("Name"); }
    public void setName(String value) { getFieldMap().put("Name", value); }
                    
    public String getCourse() { return getFieldMap().get("Course"); }
    public void setCourse(String value) { getFieldMap().put("Course", value); }
]]>
                    </ci:code>
                </xs:appinfo>
            </xs:annotation>
            <xs:sequence>
                <xs:element maxOccurs="unbounded" ref="field"/>
            </xs:sequence>
        </xs:complexType>
    </xs:element>
    <xs:element name="field">
        <xs:complexType>
            <xs:simpleContent>
                <xs:extension base="xs:string">
                    <xs:attribute name="name" use="required" type="xs:string"/>
                </xs:extension>
            </xs:simpleContent>
        </xs:complexType>
    </xs:element>
</xs:schema>

To generate the revised Teacher class, use:

xjc -no-header -extension -Xinject-code teacher.xsd

Teacher.java (revised)

package generated;

import java.util.*;
import jakarta.xml.bind.annotation.*;

@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "", propOrder = { "field" })
@XmlRootElement(name = "teacher")
public class Teacher
{
    @XmlElement(required = true)
    protected List<Field> field;
    public List<Field> getField()
    {
        if (field == null)
            field = new ArrayList<>();
        return this.field;
    }

    private java.util.Map<String,String> fieldMap;
    public java.util.Map<String,String> getFieldMap()
    {
        if ( fieldMap == null )
        {
            fieldMap = new java.util.HashMap<>();
            for ( Field field : getField() )
                fieldMap.put(field.getName(), field.getValue());
        }
        return fieldMap;
    }

    public String getAge() { return getFieldMap().get("Age"); }
    public void setAge(String value) { getFieldMap().put("Age", value); }
                    
    public String getName() { return getFieldMap().get("Name"); }
    public void setName(String value) { getFieldMap().put("Name", value); }
                    
    public String getCourse() { return getFieldMap().get("Course"); }
    public void setCourse(String value) { getFieldMap().put("Course", value); }
}

Upvotes: 0

jdweng
jdweng

Reputation: 34421

Using a Powershell script with and Xml Linq

using assembly System.Xml.Linq 

$inputFilename = 'c:\temp\test.xml'
$outputFilename = 'c:\temp\test1.xml'

class Teacher {
   [int]$Age
   [string]$Name
   [string]$Course
}

class Field {
    [string]$Name
    [string]$Value
}

class Content {
   $Teachers = [System.Collections.ArrayList]::new()
   $Fields = [System.Collections.ArrayList]::new()
}

$xDoc = [System.Xml.Linq.XDocument]::Load($inputFilename)

$Contents = [Content]::new()

#Parse Xml to classes
foreach($xTeacher in $xDoc.Descendants('teacher'))
{
    $teacher = [Teacher]::new()
    $Contents.Teachers.Add($teacher) | out-null
    
    foreach($xField in $xTeacher.Elements('field'))
    {
        $name = $xField.Attribute('name').Value
        $value = $xField.Value

        $field = [Field]::new()
        $field.Name = $name 
        $field.Value = $value
        $Contents.Fields.Add($field) | out-null

        $teacher.$name = $value
    }
}

#create new XMl File
$ident = '<teachers></teachers>'
$xDoc = [System.Xml.Linq.XDocument]::Parse($ident)
$xTeachers = $xDoc.Root

foreach($teacher in $Contents.Teachers)
{
   $xTeacher = [System.Xml.Linq.XElement]::new([System.Xml.Linq.XName]::Get('teacher'))
   $xTeachers.Add($xTeacher) | out-null

   $field = [System.Xml.Linq.XElement]::new([System.Xml.Linq.XName]::Get('field'), $teacher.Age)
   $name = [System.Xml.Linq.XAttribute]::new([System.Xml.Linq.Xname]::Get('name'), 'Age')
   $field.Add($name) | out-null
   $xTeacher.Add($field) | out-null

   $field = [System.Xml.Linq.XElement]::new([System.Xml.Linq.XName]::Get('field'), $teacher.Name)
   $name = [System.Xml.Linq.XAttribute]::new([System.Xml.Linq.Xname]::Get('name'), 'Name')
   $field.Add($name) | out-null
   $xTeacher.Add($field) | out-null

   $field = [System.Xml.Linq.XElement]::new([System.Xml.Linq.XName]::Get('field'), $teacher.Course)
   $name = [System.Xml.Linq.XAttribute]::new([System.Xml.Linq.Xname]::Get('name'), 'Course')
   $field.Add($name) | out-null
   $xTeacher.Add($field) | out-null
   
}

$xDoc.Save($outputFilename)

Here is the xml file

<teachers>
   <teacher>
      <field name="Age">30</field>
      <field name="Name">Bob</field>
      <field name="Course">Math</field>
   </teacher>
</teachers>

Upvotes: 0

Related Questions