prev table of contents next

2.2.15 Referring to Another XML Element

Among the data types of the XML Schema language there is an inseparable pair of types operating complementary to each other: xsd:ID and xsd:IDREF. They let you represent references to XML elements. This can be put to good use in several circumstances, i.e., whenever you need linkage in addition to the natural parent-to-child relationship.

Our first example uses references for factoring out frequently occuring elements. Below is a schema snippet defining XML elements dealing with airports and flights, which are scheduled from one airport to another one. A naive schema definition would simply have two AirportType sub-elements for each flight:

<xsd:complexType name="AirportType">
  <xsd:attribute name="LocId" type="xsd:string" use="required"/>
  <xsd:attribute name="Name"  type="xsd:string" use="required"/>
</xsd:complexType>

<xsd:complexType name="FlightType">
  <xsd:all>
    <xsd:element name="From"    type="AirportType"/>
    <xsd:element name="To"      type="AirportType"/>
    <xsd:element name="Carrier" type="xsd:string"/>
    <xsd:element name="DepTime" type="xsd:time"/>
    <xsd:element name="ArrTime" type="xsd:time"/>
  </xsd:all>
  <xsd:attribute name="Number" type="xsd:int" use="required"/>
</xsd:complexType>
Instead of copying an element of class AirportType (biggish, aren't they) into all places where it is used, we'll now change our schema to employ element linkage. An AirportType element is presented only once, in full, and a reference is inserted for the From and To sub-elements of FlightType where the original element was. Our document type should be defined along the lines of TimetableType, bundling the actual AirportType elements and the list of flights.

<xsd:complexType name="AirportType">
  <xsd:attribute name="LocId" type="xsd:ID"     use="required"/>
  <xsd:attribute name="Name"  type="xsd:string" use="required"/>
</xsd:complexType>

<xsd:complexType name="FlightType">
  <xsd:all>
    <xsd:element name="From"    type="xsd:IDREF"/>
    <xsd:element name="To"      type="xsd:IDREF"/>
    <xsd:element name="Carrier" type="xsd:string"/>
    <xsd:element name="DepTime" type="xsd:time"/>
    <xsd:element name="ArrTime" type="xsd:time"/>
  </xsd:all>
  <xsd:attribute name="Number" type="xsd:int" use="required"/>
</xsd:complexType>

<xsd:complexType name="TimetableType">
  <xsd:sequence>
    <xsd:element name="Airports" type="AirportType" maxOccurs="unbounded"/>
    <xsd:element name="Flight"   type="FlightType"  maxOccurs="unbounded"/>
  </xsd:sequence>
</xsd:complexType>
The only change required within AirportType is the definition of the attribute Id as xsd:ID, and FlightType has xsd:IDREF as the type for From and To. We'll continue to use the IATA Location Identifier, which we would have used as an airport identification anyway, as the string implementing the XML link. (If there is no attribute that could serve as an identification right away, it may very well be possible to compose one from the element's other attributes. Otherwise you'll just have to use some synthetic key.) The resulting Java code for AirportType has the usual JavaBean layout. More interesting is the code for the class FlightType:
public class FlightType {

    protected Object from;
    protected Object to;
    protected String carrier;
    protected XMLGregorianCalendar depTime;
    protected XMLGregorianCalendar arrTime;
    protected int number;

    public Object getFrom() {
        return from;
    }

    public void setFrom(Object value) {
        this.from = value;
    }

    public Object getTo() {
        return to;
    }

    public void setTo(Object value) {
        this.to = value;
    }

    // ...(more getters and setters)
}
The elements From and To are now represented as Object references. So, to retrieve the origin of some flight, all you'll have to code is
(AirportType)flight.getFrom()
The destination's IATA Location Identifier is obtained by
((AirportType)flight.getTo()).getLocId()
Don't blame JAXB for not making the From and To sub-elements AirportType references. The XML type xsd:ID is a universal reference type, and xsd:IDREF is the union of all reference values. Nevertheless, unmarshalling will automatically create FlightType objects that are actually linked to their airports, saving you the hassle of establishing this linkage yourself.

Notice that by dropping the necessity to have a full-blown XML element for From and To, we could now define this value pair as a couple of attributes.

Keep in mind that XML requires the values of xsd:ID (or, same thing, the ones of xsd:IDREF) to be unique across all of your XML document. As soon as we begin to think about adding bookings to our XML data, flight identifiers would have to make their appearance as another class of values for xsd:ID. There is a good chance that location identifiers and flight identifiers (a concatenation of the carrier id and some number) don't clash, but you'd better research this thoroughly before you commit yourself.

Another excellent reason for using references is the representation of linked data structures such as trees, lists or, in general, graphs. The example given below is for railway buffs. It demonstrates how a track layout can be represented by linking elements such as sets of points (or switches) and tracks to each other, creating the graph that represents the topology of a shunting yard. (Signals are omitted for brevity's sake.)

<xsd:simpleType name="GroupType">
  <xsd:restriction base="xsd:string">
    <xsd:enumeration value="SWITCH"/>
    <xsd:enumeration value="TRACK"/>
  </xsd:restriction>
</xsd:simpleType>

<xsd:complexType name="ElementType">
  <xsd:attribute name="Id"     type="xsd:ID"     use="required"/>
  <xsd:attribute name="Group"  type="GroupType"  use="required"/>
  <xsd:attribute name="Number" type="xsd:int"    use="required"/>
  <xsd:attribute name="Name"   type="xsd:string" use="optional" default=""/>
</xsd:complexType>

<xsd:complexType name="PointLeftRightType">
  <xsd:complexContent>
    <xsd:extension base="ElementType">
     <xsd:attribute name="point" type="xsd:IDREF"/>
     <xsd:attribute name="left"  type="xsd:IDREF"/>
     <xsd:attribute name="right" type="xsd:IDREF"/>
    </xsd:extension>
  </xsd:complexContent>
</xsd:complexType>

<xsd:complexType name="EastWestType">
  <xsd:complexContent>
    <xsd:extension base="ElementType">
     <xsd:attribute name="east" type="xsd:IDREF"/>
     <xsd:attribute name="west" type="xsd:IDREF"/>
    </xsd:extension>
  </xsd:complexContent>
</xsd:complexType>

<xsd:complexType name="ShuntingYardType">
  <xsd:choice maxOccurs="unbounded">
    <xsd:element name="Switch" type="PointLeftRightType"/>
    <xsd:element name="Track"  type="EastWestType"/>
  </xsd:choice>
</xsd:complexType>
From the generated classes, we select EastWestType for a closer inspection. Again, there is Object as the type used in the getters and setters for the attributes linking to the neighbours.
public class EastWestType extends ElementType {
    protected Object east;
    protected Object west;

    public Object getEast() {
        return east;
    }
    public void setEast(Object value) {
        this.east = value;
    }

    public Object getWest() {
        return west;
    }
    public void setWest(Object value) {
        this.west = value;
    }
}

But wait - what do we do with dead-end tracks? Or with tracks that lead out of the shunting yard, i.e., line tracks? We'll need some sort of replacement for Java's null. Simply using "null" or "" as an IDREF value will cause problems as soon as the XML is validated, because, by definition, there must be some element where that string is an id value. One solution would be to define all the link attributes as optional, but this makes it difficult to discriminate an inadvertently dropped element from an intentionally omitted one. One solution that avoids this ambiguity is to use a single artificial element, perhaps in a category of its own, as the "null" element. We extend ShuntingYardType accordingly:

<xsd:complexType name="NullType">
  <xsd:attribute name="Id" type="xsd:ID" use="required"/>
</xsd:complexType>

<xsd:complexType name="ShuntingYardType">
  <xsd:choice maxOccurs="unbounded">
    <xsd:element name="Switch" type="PointLeftRightType"/>
    <xsd:element name="Track"  type="EastWestType"/>
    <xsd:element name="Null"   type="NullType"/>
  </xsd:choice>
</xsd:complexType>
This will give you a class NullType, and one element of that type with an arbitrary Id value - perhaps Id="null" - provides a null element that is comfortably distinguishable from all the actual trackside equipment by class as well as by its Id value. Here is the Java code JAXB generates for it:
public class NullType extends ElementType {
}
No, this is no typesetting accident: this class is indeed empty because it doesn't need any additions to its base class. Its name is distinction enough.

In the XML file you would have one extra element, e.g., the Null element as shown below.

<Track Id="TRACK_168" Group="TRACK" Number="168" Name="b101"
       east="SWITCH_42" west="null"/>
<Null Id="null" Group="NULL" Number="0"/>
The generic element attributes Group and Number could be set to arbitrary values, but we might just as well use some null values there, too.


prev table of contents next