Excelsior! XML Marshaller for Cocoa
Version 2.0

by Jim Rankin
June 28, 2004

This is the README file for the Excelsior! project. Click here to download Excelsior! now.

QUICKSTART:

If you just want to get started, skip to "IV. HOWTO" below.

0. WHAT'S NEW

These features are new in Excelsior! If you are new to Excelsior!, start with section I. OVERVIEW, and come back here when you have read the rest of the document.

1. Reusable Mapping Types

One of the most requested features for Excelsior! was the ability to refer to a single mapping multiple times in a mapping file. Reusing mappings would make mapping files more elegant, less redundant, smaller, easier to understand, and easier to maintain. Reusable mappings are part of Excelsior! 2.

Actually, it is more accurate to speak of reusable mapping types than reusable mappings. This is because when a mapping is referenced multiple times, the referenced mapping does not contain all of the required information. For example, xmlPath and objectPath attributes are not included in referenced mappings. The information that can be multiply referenced we will refer to as a mapping type. A fully qualified mapping, including xml and object paths, we will continue to call a mapping. (This should become clear in the examples below.)

The mapping file poMapping.xml demonstrates how to reuse mappings. The first difference you will notice is that the root element of this document is a MappingList, not a Mapping. The scope of a mapping type is its enclosing MappingList.

Now, consider the first Mapping child of MappingList.

<Mapping type="date" format="%Y-%m-%d" name="poDate"/>

Notice the name attribute. This allows other mappings to refer to this as a mapping type. For example

<Mapping ref="poDate" xmlPath="@orderDate" objectPath="orderDate"/>

and

<Mapping ref="poDate" xmlPath="shipDate" objectPath="shipDate"/>

both reference this mapping type (ref="poDate"). This means that both of these mappings will be of type date with format "%Y-%m-%d". But the poDate mapping type is only defined once. Without mapping type references, these mappings would look like

<Mapping type="date" format="%Y-%m-%d" xmlPath="@orderDate" objectPath="orderDate"/>
<Mapping type="date" format="%Y-%m-%d" xmlPath="shipDate" objectPath="shipDate"/>

Less elegant, more redundant, and harder to maintain. Object mapping types can also be reused. Consider

    <Mapping type="object" class="JMRAddress" name="address">
        <Mapping type="string" xmlPath="@country" objectPath="country"/>
        <Mapping type="string" xmlPath="name" objectPath="name"/>
        <Mapping type="string" xmlPath="street" objectPath="street"/>
        <Mapping type="string" xmlPath="city" objectPath="city"/>
        <Mapping type="string" xmlPath="state" objectPath="state"/>
        <Mapping type="string" xmlPath="zip" objectPath="zip"/>
    </Mapping>

This mapping type is referenced twice in the mapping list

    <Mapping ref="address" xmlPath="shipTo" objectPath="shipTo"/>
    ...
    <Mapping ref="address" xmlPath="billTo" objectPath="billTo"/>

Without mapping types, this would look like

    <Mapping type="object" class="JMRAddress" xmlPath="shipTo" objectPath="shipTo">
        <Mapping type="string" xmlPath="@country" objectPath="country"/>
        <Mapping type="string" xmlPath="name" objectPath="name"/>
        <Mapping type="string" xmlPath="street" objectPath="street"/>
        <Mapping type="string" xmlPath="city" objectPath="city"/>
        <Mapping type="string" xmlPath="state" objectPath="state"/>
        <Mapping type="string" xmlPath="zip" objectPath="zip"/>
    </Mapping>
    <Mapping type="object" class="JMRAddress" xmlPath="billTo" objectPath="billTo">
        <Mapping type="string" xmlPath="@country" objectPath="country"/>
        <Mapping type="string" xmlPath="name" objectPath="name"/>
        <Mapping type="string" xmlPath="street" objectPath="street"/>
        <Mapping type="string" xmlPath="city" objectPath="city"/>
        <Mapping type="string" xmlPath="state" objectPath="state"/>
        <Mapping type="string" xmlPath="zip" objectPath="zip"/>
    </Mapping>

Much less elegant, much more redundant and much harder to maintain.

Here is an example of how to create a JMRMappingList from a mapping list file and reference a JMRObjectMapping from it by name. The object mapping must be a direct child of the mapping list.

    url = [NSURL fileURLWithPath:[projectDir stringByAppendingString:@"/poMapping.xml"]];
    mappingList = [JMRMappingList mappingListWithDataFromURL:url];
    poMapping = [mappingList mappingWithRootName:@"purchaseOrder"];

At this point, poMapping can be used the same as any other JMRObjectMapping. This example is from the main.m test file.

In summary

Mapping files created with previous versions of Excelsior should continue to work unchanged.

2. Switch Mappings

Here's another mapping type introduced in Excelsior! 2. I found this mapping type essential in a project that uses Excelsior!, and thought others might have a use for it as well. Scan the next few paragraphs. If switch mappings don't appear to solve any particular problem you're having right now, please feel free to ignore them.

A switch mapping is used when you want to map a bunch of child elements, with different tag names, into a single to-many relationship on the object side. For an example, look at the testSwitch test in TestMarshalling.m. It uses the mapping file switchMapping.xml, which contains the mapping

`
    <Mapping type="object" rootName="a" class="A">
        <Mapping type="switch" isList="YES" xmlPath="." objectPath="bcs">
            <Mapping ref="b" xmlPath="b"/>
            <Mapping ref="c" xmlPath="c"/>
            <Mapping type="object" xmlPath="d" class="D">
                <Mapping type="switch" xmlPath="." objectPath="bc">
                    <Mapping ref="b" xmlPath="b"/>
                    <Mapping ref="c" xmlPath="c"/>
                </Mapping>
            </Mapping>
        </Mapping>
    </Mapping>

Notice the first child of this mapping, with type "switch". It maps the immediate children of an element "a" to the "bcs" property of an object instance of class "A". isList="YES" indicates that "bcs" is a to-many property. Also Note that the children mappings of the switch mapping lack an objectPath mapping. This is because the objectPath mapping of the switch mapping applies to all of its children.

When Excelsior! is processing the children of an element with a switch mapping, it uses the child mapping whose xmlPath attribute matches the child element's tag name. So given

<a>
    <b name="1"/>
    <c name="2">value1</c>
    <b name="3"/>
    <b name="4"/>
    <c name="5">value2</c>
    <d><b name="6"/></d>
    <d><c name="7">value3</c></d>
</a>

<b name="1"/>, <b name="3"/>, and <b name="4"/> will be processed with mapping <Mapping ref="b" xmlPath="b"/>. <c name="2">value1</c> and <c name="5">value2</c> will be processed with mapping <Mapping ref="c" xmlPath="c"/>.

3. Pretty Print

XMLKeyValueElement instances can now be rendered as a neatly indented NSString. description still generates XML with no extra whitespace, but the new method formattedString on JMRXMLTree (XMLKeyValueElement's superclass) will produce the indented version. formattedString is now used in the main.m testing file to output XML.

formattedString relies on the class JMROutputStream, which wraps the NSOutputStream class. Because NSOutputStream was introduced in Panther (OS X 10.3), formattedString only works on OS X 10.3 or higher.

4. Mapping Types Design

Implementing reusable mapping types required a significant change to Excelsior!'s design. Now every JMRMapping object refers to a JMRMappingType object. The JMRMappingType encapsulates much of the behavior formerly specified in the JMRMapping subclasses.

JMRMappingType is somewhat of an abstract class, as it implements no functionality on its own. Its two subclasses are JMRObjectMapping and JMRScalarMappingType. (Why not JMRObjectMappingType? Because the JMRObjectMapping class already existed and played pretty much the same role it does now. It was made a subclass of JMRMappingType to conform to the new design.)

The various subclasses of JMRScalarMappingType (JMRBooleanMappingType, JMRDateMappingType, JMRNumberMappingType, JMRStringMappingType, JMRConstantMappingType) encapsulate the behavior formerly contained in the various JMRMapping subclasses with corresponding names. For example, now a JMRBooleanMapping simply always returns a JMRBooleanMappingType as its mapping type, which now encapsulates much of the logic formerly encapsulated in JMRBooleanMapping itself.

This made reusing mapping types through references relatively straightforward.


I. OVERVIEW

Excelsior! is an XML marshaller for Cocoa/Objective C. What this means is that Objective C object instances can be generated from XML data with just a few messages. Inversely, XML can be generated from object instances. An external file defines how the marshalling takes place.

Castor is an example of an XML marshaller for Java and WebObjects has similar functionality. But as far as I know, this is the only XML marshaller for Objective C.

Other features of Excelsior! are

You can learn more about these features in the following sections.

II. DESIGN

Excelsior! builds on the Core Foundation XML functions and data structures in OS X. There are 3 levels of abstraction built on top of CFXML.

1. Objective C wrappers for CFXML data structures

JMRTree wraps a CFTreeRef. JMRXMLTree extends JMRTree and wraps functions for handling XML. JMRXMLNode wraps a CFXMLNodeRef.

2. XML key value coding

XMLKeyValueElement extends JMRXMLTree and facilitates getting and setting XML data using key paths, similar to the key-value coding defined in the NSKeyValueCoding protocol.

For example, given elt, an XMLKeyValueElement representing the following XML,

<root>
    <tag1>
        <tag2 attr="attrText">tag2 text</tag2>
        <multiple>first multiple text</multiple>
        <multiple>second multiple text</multiple>
    </tag1>
</root>
[elt elementForKeyPath:@"tag1/tag2"]
will return @"tag2 text" and
[elt elementForKeyPath:@"tag1/tag2@attr"]
will return @"attrText". Also, multiple nodes with the same tag name can be distinguished by appending an index to the keypath. So
[elt elementForKeyPath:@"tag1/multiple[0]"]
will return @"first multiple text" and
[elt elementForKeyPath:@"tag1/multiple[1]"]
will return @"second multiple text". Note that "root" is NOT included in the paths. Paths are relative to the root. Also note that indexes start at 0.

3. Marshalling

This has two parts.
  1. JMRMapping and it's subclasses specify how to map data between an XML element and an object field. A JMRObjectMapping has an NSArray of JMRMapping's and completely specifies how to marshal an XML element into an object instance.
  2. JMRFactoryMapping takes an XML file specifying an object mapping and constructs a JMRObjectMapping instance. The JMRObjectMapping is handed to a JMRMarshaller, which can then marshal objects into XML and unmarshal objects from XML.

III. TEMPLATES

In addition to creating XML data from scratch, Excelsior! can marshal object instances into an XML or XHTML template. This can be used to view your object's data in a web browser, for example.

Two features that facilitate template marshalling are

  1. Selecting the nth instance of a tag name (see II.2. above). This is very useful for XHTML where there are lots of repeated sibling nodes, like <tr>, <td>, <a>, etc.
  2. Replicating XML nodes for marshalling a list of objects. This is especially useful for marshalling a list of objects into <tr>'s in an XHTML <table>, for example.
Of course, HTML templates must be valid XML (XHTML). Otherwise, Excelsior! won't be able to parse the template.

IV. HOWTO

1. Targets

This project has two targets: ExcelsiorFramework and MarshallingTest. You must build ExcelsiorFramework before you can build and run MarshallingTest.

Prebinding a library or framework allows applications built with the framework to launch faster. To enable prebinding, ExcelsiorFramework is built with a preferred first segment load address of 0x1a000000. This should avoid conflict with the load address of any application using it (0x00000000). If this conflicts with the memory space of any other library used by your application, change this value to something that does not conflict before building the framework. For more information about prebinding see this note.

To understand how to marshal and unmarshal, look at the file main.m and 2. below. To understand how to create a mapping file, look at PersonMapping.xml and read 3. below. To understand how to create a template mapping file, look at PersonHTMLMapping.xml and Person.html and read 3. below.

2. Marshalling

MarshallingTest's main.m file demonstrates marhalling and unmarshalling. The steps are as follows.
  1. Generate a JMRObjectMapping for marshalling between "VCARD" XML data and JMRPerson objects.
  2. Create a JMRMarshaller with this object mapping.
  3. Create an XMLKeyValueElement from the file vcard.xml.
  4. Unmarshal this element into a JMRPerson object.
  5. Append " SUCKS!" to the JMRPerson's name field.
  6. Marshal the object back into XML data and write it to the file vcard_sux.xml.
Then the following steps demonstrate marhsalling into a template.
  1. Create a JMRMarshaller for the PersonHTMLMapping.xml mapping file.
  2. Read Person.html into an XMLKeyValueElement to use for a template.
  3. Marshal the object into the HTML template and write it to the file Person_sux.html.
Note that the <tr> element for phone numbers is replicated for each of the JMRPerson's phone numbers in Person_sux.html.

3. Creating and editing mapping files

There are two new kinds of mappings in Excelsior! 2, reusable mappings and switch mappings. They are described in the What's New section above.

The files PersonMapping.xml and PersonHTMLMapping.xml illustrate most of the kinds of mappings you can specify.

  1. All tags are "Mapping" tags. Every Mapping tag must have a "type" attribute.
  2. Object mappings are Mappings with type "object". Object mappings must specify the name of an Objective C class and an XML root tag. So
    <Mapping type="object" class="JMRPerson" rootName="VCARD">
    means that a VCARD XML is unmarshalled into a JMRPerson object. The root element of a mapping file must be an object mapping.
  3. Mappings that are not the root mapping must have xmlPath and objectPath attributes. xmlPath indicates where to find the data in the XML, and the object path specifies the field in the object. xmlPath must follow the format described for XMLKeyValueElement paths in section II.2 above. objectPath must follow the format used for the valueForKeyPath: message in the NSKeyValueCoding protocol.
  4. An object mapping's children specify how the root XML element's child nodes are mapped to the object's fields. Object mappings can have other object mappings as children. In PersonMapping.xml,
    <Mapping type="object" isList="YES" class="JMRPhoneNumber" xmlPath="TEL" objectPath="numbers">
    indicates that a VCARD's "TEL" children are unmarshalled into an NSArray of JMRPhoneNumber's and assigned to the JMRPerson's "numbers" field.
  5. Scalar mappings indicate how to map a text node onto an object field. If a Mapping is not an object mapping, it is a scalar mapping. A scalar mapping has no children. The following Mapping types are scalar mappings.
    1. string
      map XML text directly to an NSString.
    2. constant
      used only in marshalling; set the text for xmlPath to the Mapping's "default" attribute.
    3. date
      map XML text to an NSDate; the "format" attribute must follow the format used by NSDateFormatter's initWithDateFormat:allowNaturalLanguage: message.
    4. number
      map XML text to an NSNumber; the "format" attribute must follow the format used by NSNumberFormatter's setFormat: message.

    So

    <Mapping type="string" xmlPath="NAME" objectPath="name"/>
    means the text in the element with tag "NAME" is mapped to the "name" field of the JMRPerson, which is an NSString. See the Application Kit API reference for more information on format strings.

  6. A list mapping specifies that you want to map children with the same tag name to an NSArray of objects. Indicate that a mapping is a list mapping by setting the isList attribute to "YES". Both scalar mappings and object mappings can be list mappings. The JMRPhoneNumber mapping in PersonMapping.xml is an example of this.
  7. Order is meaningful. When you marshal an object into XML, the child nodes will appear in the same order as the order of the object mapping's child mappings.
  8. Use the tagName[n] syntax to marshal into the nth XML element with tag tagName. For example in PersonHTMLMapping.xml, the xmlPath in
    <Mapping type="string" xmlPath="body/table/tr[3]/td" objectPath="url"/>
    references the first <td> of the fourth <tr> contained by the element specified by "body/table" (see II.2 above for more examples).
  9. For templates, XML tags, attribtes and text not referenced by any mappings remain unchanged in the marshalled XML.
  10. When marshalling an object list into a template, the element referenced by xmlPath will be replicated once for each object in the list.

V. LICENSE

This work is licensed under the Creative Commons Attribution License. To view a copy of this license, visit http://creativecommons.org/licenses/by/1.0/ or send a letter to Creative Commons, 559 Nathan Abbott Way, Stanford, California 94305, USA.

A copy of this license is also included in the Documentation folder of this project, in the file AttributionLicense.html.

In summary, this license stipulates that anyone who distributes this work or a derivative of this work must credit the original author (Jim Rankin), in a reasonable fashion. One way to satisfy this requirement would be to include the following in a text or HTML file distributed with the derivative work.

"This program uses the Excelsior! class library created by Jim Rankin. For more information about Excelsior, email jimbokun@mac.com."

VI. REQUIREMENTS

This project was developed and tested on a 550MHz PowerBook G4 with 768MB RAM running Mac OS X version 10.2.6. I would like to hear about issues with other hardware/software combinations.

VII. ACKNOWLEDGEMENTS

The following people have contributed bug fixes or other enhancements.

Mark Smith
Don Briggs

Thanks!


VIX. CONTACT ME

Send comments, complaints, suggestions, fixes and praise to jimbokun@mac.com

free hit counter