Next up is the Zeus data binding framework. This is a project over at Enhydra (http://www.enhydra.org), and is based on a series of articles I originally wrote for the folks at IBM's Developer Works (http://www.ibm.com/developerWorks). It was the direct result of my need for a simple solution for data binding (at the time, Castor seemed overly complex for a fairly straightforward data binding problem). Since then, it's developed into a complete project at Enhydra, and several other folks are working on it. You can get the complete story by checking out http://zeus.enhydra.org. I'll touch on the same things I dealt with in Castor, so you can use either competently in your applications.
Zeus is still in the fairly early stages of functional development (it's been around a bit, but some serious architecture is still going on). As a result, I'd recommend you get the latest version from CVS instead of downloading a binary. That way, you'll be sure to get the latest and greatest in features. You can find complete instructions on getting Zeus from CVS at the web site, but in short, you'll need to get CVS (from http://www.cvshome.org, for example). Simply use the following command:
cvs -d :pserver:anoncvs@enhydra.org:/u/cvs login
Enter "anoncvs" as the password. Then:
cvs -d :pserver:anoncvs@enhydra.org:/u/cvs co toolsTech/Zeus
You'll get everything you need. Change into the Zeus directory (which is created by the CVS download), and build the binaries:
bmclaugh@GANDALF ~/dev/Zeus $ ./build.sh
Or, on Windows:
c:\dev\Zeus> build.bat
You should also build the samples, which you can do by appending "samples" as an argument to the build command:
bmclaugh@GANDALF ~/dev/Zeus $ ./build.sh samples
At that point, you'll end up with zeus.jar in the build/ directory. Add this to your classpath, as well as jdom.jar and xerces.jar, both of which are in the lib directory. Finally, if you're going to use DTD generation, you should also add dtdparser113.jar to the classpath. And, to use the samples, add the build/classes directory itself, just to make things really easy. So, my classpath looks like this:
bmclaugh@GANDALF ~/dev/Zeus $ echo $CLASSPATH /dev/Zeus/lib/jdom.jar:/dev/Zeus/lib/xerces.jar:/dev/Zeus/lib/dtdparser113.jar: /dev/Zeus/build/zeus.jar:/dev/Zeus/build/classes
Or, on Windows:
c:\dev\Zeus> echo %CLASSPATH% c:\dev\Zeus\lib\jdom.jar;c:\dev\Zeus\lib\xerces.jar; c:\dev\Zeus\lib\dtdparser113.jar;c:\dev\Zeus\build\zeus.jar; c:\dev\Zeus\build\classes
That's it. With your classpath set, and the XML from the earlier portions of the chapter, you're ready to go.
The primary difference between Zeus and frameworks like Castor and even JAXB, Sun's offering, is the way that class generation is handled. Remember from Figure 15-1 that the standard means of handling class generation is taking a set of constraints, reading through them, and streaming out Java source code. While this is handy, it makes it difficult to add support for other types of constraints, like DTDs or newer schema-alternatives such as Relaxx and TREX. To accommodate this, Zeus adds an intermediary step. What occurs is that a set of constraints (in schema form, DTD form, etc.) is converted into a set of Zeus bindings. These bindings are not tied to any specific means of constraint representation. In other words, if you modeled a set of constraints within a DTD, and then modeled the exact set of constraints again within an XML Schema, Zeus would convert both into an identical set of bindings. These bindings are used to generate Java source code. The result is a process like that modeled in Figure 15-3.
What's nice about this, and the reasoning behind it, is that adding support for a new method of constraints like TREX becomes a very simple process. You could write a class, implementing the org.enhydra.zeus.Binder interface, that takes in a set of constraints and creates Zeus bindings from them. Class generation is already handled, so you don't have to deal with hundreds of annoying out.write( ) type statements. Zeus comes prepackaged with two binders, DTDBinder and SchemaBinder, both in the org.enhydra.zeus.binder package.
Other than that architectural change (which doesn't affect you when using existing binders), Castor and Zeus operate similarly in generating classes. Example 15-5 shows a DTD that constrains the original XML document from Example 15-1. I'll demonstrate using class generation from DTDs with this example.
<!ELEMENT catalog (item+)> <!ATTLIST catalog xmlns CDATA #FIXED "http://www.homespuntapes.com" > <!ELEMENT item (title, teacher, guest*, description)> <!ATTLIST item id CDATA #REQUIRED level CDATA #REQUIRED > <!ELEMENT title (#PCDATA)> <!ELEMENT teacher (#PCDATA)> <!ELEMENT guest (#PCDATA)> <!ELEMENT description (#PCDATA)>
Zeus is set up to allow applications to call the source generator, and therefore does not provide a static entry point for source generation within the API. Because that's a common task, though, a class within the samples (remember running the build program with the "samples" argument?) does allow easy class generation. If your classpath is set up like I showed you previously, all you need to do is create a new directory called "output". The samples program puts generated classes in there by default (programmatically, you can change that option). So, with an output directory in place, run the following command:
C:\javaxml2\build>java samples.TestDTDBinder -file=c:\javaxml2\ch14\xml\zeus\catalog.dtd -package=javaxml2.zeus -quiet=true
I have spaced things out for clarity. I've specified the file to generate source from, the package to put the source within, and that generation should occur quietly (without spitting out any debugging information). Generation from a schema is very similar. Since Zeus uses the final version of the XML Schema recommendation, you need to use a newer version of the XML Schema shown back in the Castor section; Example 15-6 updates that schema to a current version.
<?xml version="1.0"?> <schema xmlns="http://www.w3.org/2001/XMLSchema" targetNamespace="http://www.homespuntapes.com"> <element name="catalog"> <complexType> <sequence> <element ref="item" minOccurs="1" maxOccurs="unbounded" /> </sequence> </complexType> </element> <element name="item"> <complexType> <sequence> <element name="title" type="string" /> <element name="teacher" type="string" /> <element name="guest" type="string" minOccurs="0" maxOccurs="unbounded" /> <element name="description" type="string" /> </sequence> <attribute name="id" type="string" /> <attribute name="level"> <simpleType> <restriction base="integer"> <enumeration value="1" /> <enumeration value="2" /> <enumeration value="3" /> <enumeration value="4" /> <enumeration value="5" /> </restriction> </simpleType> </attribute> </complexType> </element> </schema>
Once you've got that, use the following command for class generation:
C:\javaxml2\build>java samples.TestSchemaBinder -file=c:\javaxml2\ch14\xml\zeus\catalog.xsd -package=javaxml2.zeus -quiet=true
You'll get identical output using either a DTD or a schema.
WARNING: This is a slight overstatement. Actually, you will get identical output if you can represent the constraints in the DTD and XML Schema identically. In this example, the schema defines "level" as an integer, while the DTD must define it as PCDATA. The result is that using a schema, you get a typed level (a Java int), and using a DTD, you get a String. I've used the DTD version throughout this section, to show you that option.
Within the output directory, you'll find a javaxml2 directory, and then a zeus directory (matching up to the package hierarchy). You can take a look at the source files generated within.
Right off, you should notice that Zeus produces an interface (for example, Catalog.java) and a default implementation of that interface (for example, CatalogImpl.java). The reasoning behind this, as opposed to the concrete classes that Castor generates, is that you may want to have your own custom classes, perhaps already in existence, that implement the Catalog interface. You can therefore use your own implementation, but get the benefits of class generation. I commonly see developers using Zeus to generate classes and then creating their own implementation of the generated interfaces as Enterprise JavaBeans (EJBs). These EJBs are then persisted using Zeus, which is a piece of cake (as I'll show you in the next section). Without providing for user-defined implementations, this behavior would require modifying the generated source code, which could break marshalling and unmarshalling.
Another notable difference you should be aware of is the method names. For example, here's the Item interface (trimmed down to its simplest form):
package javaxml2.zeus; public interface Item { public Title getTitle( ); public void setTitle(Title title); public Teacher getTeacher( ); public void setTeacher(Teacher teacher); public java.util.List getGuestList( ); public void setGuestList(java.util.List guestList); public void addGuest(Guest guest); public void removeGuest(Guest guest); public Description getDescription( ); public void setDescription(Description description); public String getLevel( ); public void setLevel(String level); public String getId( ); public void setId(String id); }
Notice that the JavaBeans naming is used, and elements that can appear multiple times are returned as Java 2 Collections classes. In any case, take a look at this source, and then compile it:
javac -d . output/javaxml2/zeus/*.java
Now you're ready to look at marshalling and unmarshalling using Zeus.
The process of marshalling and unmarshalling in Zeus is very similar to that in Castor. At the heart of the operations are two classes, org.enhydra.zeus.Marshaller and org.enhydra.zeus.Unmarshaller. Like Castor, the methods you will be interested in are marshal( ) and unmarshal( ), on their respective classes. However, there are some differences. First, the Marshaller and Unmarshaller classes in Zeus don't provide static entry points. This is intentional, reminding the user to set options like the package to unmarshal XML to, and whether or not there are methods that should be ignored in marshalling. Additionally, Zeus uses the JAXP style of input and output, providing data binding flavors of the Source and Result classes I talked about in Chapter 9, "JAXP". Like JAXP, Zeus provides some default flavors of these in the org.enhydra.zeus.source and org.enhydra.zeus.result packages. Even with these changes, the process turns out to be very similar in both frameworks:
File catalogFile = new File("catalog.xml"); FileReader reader = new FileReader(catalogFile); FileWriter writer = new FileWriter(catalogFile); // Castor: Unmarshalling Catalog catalog = (Catalog)org.exolab.castor.xml.Unmarshaller.unmarshal(Catalog.class, reader); // Zeus: Unmarshalling StreamSource source = new StreamSource(reader); org.enhydra.zeus.Unmarshaller unmarshaller = new org.enhydra.zeus.Unmarshaller( ); Catalog catalog = (Catalog)unmarshaller.unmarshal(source); // Castor: Marshalling org.exolab.castorMarshaller.marshal(catalog, writer); // Zeus: Marshalling StreamResult result = new StreamResult(writer); org.enhydra.zeus.Marshaller marshaller = new org.enhydra.zeus.Marshaller( ); marshaller.marshal(catalog, result);
While there's a bit more typing involved with using Zeus, the methodology should seem a lot more familiar to JAXP users, which was the goal. It also allows input and output of SAX streams and DOM trees using the appropriate Source and Result implementations. As an example of Zeus in action, check out Example 15-7. This program reads in the catalog supplied as the first argument on the command line, prints out the item names and IDs, modifies an existing item, and then marshals the catalog back to XML. There's very little user interaction here, but you should be able to see how Zeus works.
package javaxml2; import java.io.File; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.util.Iterator; import java.util.List; // Zeus classes import org.enhydra.zeus.Marshaller; import org.enhydra.zeus.Unmarshaller; import org.enhydra.zeus.source.StreamSource; import org.enhydra.zeus.result.StreamResult; // Zeus generated classes import javaxml2.zeus.Catalog; import javaxml2.zeus.Guest; import javaxml2.zeus.GuestImpl; import javaxml2.zeus.Item; import javaxml2.zeus.ItemImpl; public class CatalogViewer { public void view(File catalogFile) throws IOException { FileReader reader = new FileReader(catalogFile); StreamSource source = new StreamSource(reader); // Convert from XML to Java Unmarshaller unmarshaller = new Unmarshaller( ); unmarshaller.setJavaPackage("javaxml2.zeus"); Catalog catalog = (Catalog)unmarshaller.unmarshal(source); List items = catalog.getItemList( ); for (Iterator i = items.iterator(); i.hasNext( ); ) { Item item = (Item)i.next( ); String id = item.getId( ); System.out.println("Item ID: " + id); String title = item.getTitle().getValue( ); System.out.println("Item Title: " + title); // Modify an item if (id.equals("CDZ-SM01")) { item.getTitle( ).setValue("Sam Bush Teaches Mandolin " + "Repertoire and Technique, 2nd edition"); Guest guest = new GuestImpl( ); guest.setValue("Bela Fleck"); item.addGuest(guest); } } // Write back out FileWriter writer = new FileWriter(new File("newCatalog.xml")); StreamResult result = new StreamResult(writer); Marshaller marshaller = new Marshaller( ); marshaller.marshal(catalog, result); } public static void main(String[] args) { try { if (args.length != 1) { System.out.println("Usage: java javaxml2.CatalogViewer " + "[XML Catalog Filename]"); return; } // Get access to XML catalog File catalogFile = new File(args[0]); CatalogViewer viewer = new CatalogViewer( ); viewer.view(catalogFile); } catch (Exception e) { e.printStackTrace( ); } } }
This reads in the file specified, and then spits out the ID and title of each item in the catalog. It also checks for a certain ID ("CDZ-SM01"), and modifies that item to reflect a second edition (and all of us around here love second editions!). Finally, it writes the catalog back out to a new file, with the modified information. Try it out; just be sure that you already have the compiled Zeus-generated classes in your classpath.
Once you've learned how basic data binding works, you aren't in for too many surprises. You'll just need to find the framework that is most suitable for your specific application's functional needs. One thing that Zeus has going for it is the generation of interfaces and implementation classes, which allows you to use your existing classes with little modification. Additionally, Zeus provides for using DTDs, which many of you with hundreds of DTDs in older XML-based applications will love. On the other hand, Zeus is newer than Castor, and has less momentum and a smaller developer community at this point. So, check it out online, and use it if it helps you out.
Copyright © 2002 O'Reilly & Associates. All rights reserved.