XML Serialization

I l @ ve RuBoard

Previous chapters have described the rationale behind XML and explained its benefits, such as portability, interoperability, and so on. Although version 1.1.4 of the JDK predates much of the XML support now available for Java, many recent packages developed by the Java Community Process program provide mechanisms for applications written in Java to convert objects into XML representations and back again. Perhaps the best known of these is the Java Architecture for XML Binding (JAXB), which is intended as a mechanism for serializing Java objects into an XML format. Related specifications include the Java API for XML-Based RPC (JAX-RPC) and the Java API for XML Messaging (JAXM). Both of these allow applications written in Java to send and receive XML messages using SOAP, either RPC-style or document-oriented . As of this writing, JAXB and JAX-RPC are works in progress, but they should be released by the end of 2002; JAXM 1.0 was finalized in the fall of 2001.

The .NET Framework Class Library has built-in support for converting objects to and from an XML format through the XmlSerializer class of the System.Xml.Serialization namespace. The XmlSerializer allows you to serialize and deserialize objects into XML documents while providing you with a fine degree of control over the shape of the output. Next, we'll look at how to use the XmlSerializer class and the supporting classes in the System.Xml.Serialization namespace.

XML Formatting

Chapter 5 showed you how to manually convert objects into XML and back again using Microsoft's implementation of DOM. The XmlSerializer class can automate much of this work, generating a default XML representation of an object or a graph of objects. The XMLSerialization class (in file XMLSerialization.jsl in the XMLSerialization project) shows a simple example of using an XmlSerializer object to serialize the CakeInfo.Cake class used earlier in this chapter to an XML file called CakeInfo.xml. By the way, the XmlSerializer class does not require that the object being serialized implement ISerializable or that it be tagged with SerializableAttribute (although you won't get an error if it is).

The two key lines in the XMLSerialization class are the statements that instantiate the XmlSerializer and serialize the object. The XmlSerializer class has several constructors, the simplest of which takes a System.Type . You can then use the XmlSerializer object to create XML documents that comprise serialized instances of this type. (The constructor uses reflection to determine the constitution of the type and initialize some internal data structures.) There are several ways to obtain type information from an object; the example uses the GetType method, which is inherited from System.Object . The class being serialized must provide a default (no parameters) constructor:

 Cakecake=newCake(); XmlSerializerserializer=newXmlSerializer(cake.GetType()); 

You can then use the Serialize method of the XmlSerializer class to send an XML representation of an object to the appointed stream, much as you would the BinaryFormatter class:

 Streamstream=newFileStream(...); serializer.Serialize(stream,cake); 

The Serialize method is overloaded, and you can serialize to a TextWriter or an XmlWriter as well as to a generic Stream .

If you're thinking that this all looks just a bit too easy, your suspicions might be justified. If you build and run the XMLSerialization project and then examine the results in the CakeInfo.xml file, you'll see the following:

 <?xmlversion="1.0"?> <Cakexmlns:xsd="http://www.w3.org/2001/XMLSchema"  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" /> 

The serializer has written a perfectly valid XML document, but all it contains is the XML header and the namespace declaration. So what went wrong? The problem is that the Cake class contains no public instance member variables ”XML serialization will output only public accessible instance data. (Static member variables are not considered part of the object's data and are omitted even if they're public, as they are with binary serialization.) As you'll recall, this is what the Cake class looks like:

 publicclassCake { //Cakedata privateStringmessage; privateshortfilling; privateshortsize; privateshortshape; } 

Although the XMLSerializer class will not read private data, it will make use of properties. If you edit the Cake class and uncomment the @property directives (the XMLSerialization project contains a copy of the Cake class that you can edit), rebuild the Cake project, and execute the XMLSerialization class again, you should notice the difference in the CakeInfo.xml file that is produced:

 <?xmlversion="1.0"?> <Cakexmlns:xsd="http://www.w3.org/2001/XMLSchema"  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <Filling>1</Filling> <Message>BonVoyage</Message> <Shape>1</Shape> <Size>12</Size> </Cake> 

The key point to learn from this little lesson is that XML serialization will serialize public member variables and data that's accessible through properties, but it will not serialize data that can be read only by using regular methods . The XML deserialization process is similar ”it will reconstitute an object only through its public member variables and properties. What happens if a property is read-only? XML serialization will allow data to be serialized, but it cannot be used by XML deserialization when populating an object.

Tip

Don't expose a public field as a property or it will be serialized twice!


Serializing Graphs as XML

You can also serialize graphs of multiple objects, such as the list of Cakes and Bakers discussed earlier, using the XmlSerializer class. The contents of a container object, such as a collection (an object that implements the ICollection interface), will be serialized automatically as long as some conditions are met; the Add method must take a single parameter, and the Item method must take a single integer parameter. This is true for many of the collection types, but not for the Hashtable class because the Add method takes multiple parameters and the Item method takes an Object rather than an integer. This means you cannot initialize an XmlSerializer using a Hashtable . (You'll get an InvalidOperationException when the XmlSerializer class attempts to reflect the Hashtable object.) Instead, you can use an ArrayList , which is less functional but allows you to at least collect objects of different types together.

In addition to selecting an appropriate collection type, you must of course ensure that the contents of the collection themselves meet the XML serialization requirements described in the previous section. Each class must supply a default constructor , and you should provide a means of accessing the contents of the class through publicly available members or properties. You can control the format of the output and the namespaces used by applying the various Xml Attribute classes to your code. Some of these will be described later in this chapter.

To serialize a collection or array, you must also supply information to the XmlSerializer object about the type of the collection and the type or types of the contents. For example, consider the situation where the object being serialized is an ArrayList and the contents are Cake and Baker objects. The XmlSerializer class supplies a constructor for just such an eventuality ”it expects the type of the containing class and an array of types describing the classes comprising the contents:

 Type[]extraTypes=newType[2]; extraTypes[0]=Type.GetType("Baker"); extraTypes[1]=Type.GetType("Cake"); XmlSerializerserializer=new XmlSerializer(Type.GetType("System.Collections.ArrayList"),extraTypes); 

You can then create a stream and serialize to that stream, much as before.

Namespace and Type Handling

The default namespaces added to the XML generated by the XmlSerializer are xmlns:xsd (http://www.w3.org/2001/XMLSchema) and xmlns:xsi (http://www.w3.org/2001/XMLSchema-instance). You can override this behavior by creating an XmlSerializerNamespaces object and populating it with a list of namespaces and aliases. To attach the namespace http://www.fourthcoffee.com/xmlcakes to the XML output and give it the alias fc , you would create the following System.Xml.Serialization.XmlSerializerNamespace object:

 XmlSerializerNamespacesnameSpaces=newXmlSerializerNamespaces(); nameSpaces.Add("fc", "http://www.fourthcoffee.com/xmlcakes"); 

An XmlSerializerNamespace can contain a list of several namespaces. The Add method simply appends a namespace to the list it holds. To emit the namespace information when the XML is generated, you must invoke a variant of the Serialize method:

 serializer.Serialize(stream,cake,nameSpaces); 

The third parameter is the list of namespaces. The result of this command is an XML file containing the following:

 <?xmlversion="1.0"?> <Cakexmlns:fc="http://www.fourthcoffee.com/xmlcakes"> <Filling>1</Filling> <Message>BonVoyage</Message> <Shape>1</Shape> <Size>12</Size> </Cake> 

For this effort to be totally meaningful, you must also tag the elements of the XML document with the namespace prefix. You can do this using a System.Xml.Serialization.XmlTypeAttribute applied to the Cake class:

 /**@attributeXmlTypeAttribute(Namespace= "http://www.fourthcoffee.com/xmlcakes") */ publicclassCake { } 

The Namespace parameter should be the same as one of the namespaces added to the XmlSerializerNamespace object in order for the appropriate alias to be applied. Otherwise, the full namespace specified in the XmlTypeAttribute will be output. The result should look like this:

 <?xmlversion="1.0"?> <Cakexmlns:fc="http://www.fourthcoffee.com/xmlcakes"> <fc:Filling>1</fc:Filling> <fc:Message>BonVoyage</fc:Message> <fc:Shape>1</fc:Shape> <fc:Size>12</fc:Size> </Cake> 

You can also change the name of the XML type itself. This is especially useful if the class name is not a legal XML identifier, as defined by W3C. If you want the type to be named FondantArt rather than Cake , you can use the TypeName parameter of the XmlTypeAttribute :

 /**@attributeXmlTypeAttribute(TypeName="FondantArt", Namespace="http://www.fourthcoffee.com/xmlcakes")*/ publicclassCake { } 

As a further step, you can override the name, namespace, and other information for an individual element in a class using a System.Xml.Serialization.XmlElementAttribute . You can apply this attribute to public member variables and to property accessor methods. For example, to change the element name and namespace for the Message element of the Cake class, you can use the following code:

 /**@attributeXmlTypeAttribute(TypeName="FondantArt", Namespace="http://www.fourthcoffee.com/xmlcakes")*/ publicclassCake { /**@property*/ /**@attributeXmlElementAttribute(ElementName="Greeting", Namespace="http://www.fourthcoffee.com/greeting")*/ publicStringget_Message() { returnthis.message; } } 

The results of these changes are shown below:

 <?xmlversion="1.0"?> <FondantArtxmlns:fc="http://www.fourthcoffee.com/xmlcakes"> <fc:Filling>1</fc:Filling> <fc:Greeting>BonVoyage</fc:Greeting> <fc:Shape>1</fc:Shape> <fc:Size>12</fc:Size> </FondantArt> 
Controlling Serialization

Besides the XmlTypeAttribute , the System.Xml.Serialization namespace contains a couple of other attributes that you can use to govern the format of the XML generated by the serialization process. The XmlRootAttribute allows you to identify a class as forming the root element of an XML document, which is useful primarily if you're defining classes that contain nonprimitive public members, effectively defining a data hierarchy. The class at the top of the hierarchy can be tagged with XmlRootAttribute , and you can specify a namespace and an element name. (The default is the same as the name of the class.) You can also indicate the XSD data type that should be used when generating an XML document. This set of possibilities is illustrated by the various classes in the XMLGraph project, which serializes a graph of cakes and bakers (like the binary example shown earlier).

The KitchenData class (in KitchenData.jsl) exposes pairs of the Baker and Cake classes through public properties. The Baker class is available in Baker.jsl, and the Cake class is found in Cake.jsl. Both classes have been modified for this project. The KitchenData class is shown here:

 /**@attribute XmlRootAttribute(Namespace="http://www.fourthcoffee.com/xmlcakes",                                               ElementName="Kitchen")*/ publicclassKitchenData { privateBakerbaker; privateCakecake; /**@property*/ publicvoidset_Baker(Bakerbaker) { this.baker=baker; } /**@property*/ publicBakerget_Baker() { returnthis.baker; } /**@property*/ publicvoidset_Cake(Cakecake) { this.cake=cake; } /**@property*/ publicCakeget_Cake() { returnthis.cake; } } 

The following code fragment from the main method of the Kitchen class creates, populates, and serializes a KitchenData object to a file called Kitchen.xml:

 //Createabaker Bakerdiana=newBaker("Diana"); //Getthebakertomakeacake Cakecake1=newCake("Cake1",diana); cake1.ID= "cake1"; cake1.message= "HappyBirthday"; //StorethebakerandcakeinaKitchenDataobject KitchenDatakitchen=newKitchenData(); kitchen.baker=diana; kitchen.cake=cake1; //SerializetheKitchenDataobjecttoafile FileStreamstream=newFileStream("Kitchen.xml",FileMode.Create, FileAccess.Write,FileShare.None); XmlSerializerNamespacesnameSpaces=newXmlSerializerNamespaces(); XmlSerializerserializer=new XmlSerializer(Type.GetType(kitchen.ToString())); nameSpaces.Add("fc", "http://www.fourthcoffee.com/xmlcakes"); serializer.Serialize(stream,kitchen,nameSpaces); stream.Close(); 

The resulting XML file should resemble the following:

 <?xmlversion="1.0" ?> <fc:Kitchenxmlns:fc="http://www.fourthcoffee.com/xmlcakes"> <fc:Baker> <fc:Name>Diana</fc:Name> </fc:Baker> <fc:Cake> <fc:Baker> <fc:Name>Diana</fc:Name> </fc:Baker> <fc:Filling>0</fc:Filling> <fc:Id>cake1</fc:Id> <fc:Message>HappyBirthday</fc:Message> <fc:Shape>0</fc:Shape> <fc:Size>8</fc:Size> </fc:Cake> </fc:Kitchen> 

You can use the XmlIgnoreAttribute to specify that a field or property (the get accessor) in a class should not be included in the serialization stream. For example, the following code will cause the Id property to be omitted when the Cake class is serialized:

 publicclassCake { privateStringid; /**@attributeXmlIgnoreAttribute()*/ /**@property*/ publicStringget_Id() { returnthis.id; } } 

Deserializing an XML Stream

The XmlSerializer class provides a Deserialize method that you can invoke to read an XML stream and use to create and populate objects. You can deserialize from a generic stream, a TextReader , or an XmlReader . To deserialize a KitchenData object containing the nested Baker and Cake objects from the file Kitchen.xml (created in the previous example), you can simply open a file stream, instantiate an XmlSerializer , and call Deserialize :

 FileStreamstream=newFileStream("Kitchen.xml",FileMode.Open,                                                FileAccess.Read,FileShare.Read); XmlSerializerserializer=new XmlSerializer(Type.GetType("XMLGraph.KitchenData")); KitchenDatakitchen=(KitchenData)serializer.Deserialize(stream); stream.Close(); 

When you get it right, XML deserialization is almost trivial, but getting it right relies on you having a valid XML file and making sure that the class or classes you're attempting to deserialize into are compatible with the contents of that file. If you take care of the first requirement (you might have to manually edit the file if it needs fixing!), you can meet the second one by using the XSD.exe command-line tool supplied with the .NET Framework Class Library. You can use the XSD utility to reverse-engineer a schema from an XML file and then create a class definition from the resulting XSD file.

The simplest way to generate an XSD schema is to use Microsoft Visual Studio .NET. You can add the XML file to a project, display it, and choose Create Schema from the XML menu on the toolbar. Figure 10-3 shows the schema created for the Kitchen.xml file (Kitchen.xsd).

Figure 10-3. Kitchen.XSD schema in Visual Studio .NET

Having created an XML Schema, you can use the command following line to generate a class that corresponds to this schema.

 xsdKitchen.xsd/c/l:cs 

If you're programming in Java, you'll face a slight problem ”the XSD utility will output code in a limited choice of languages: Visual Basic .NET, C#, or JScript. Java is not yet an option! Just to prove that XSD works, we generated the file below from Kitchen.xsd using the C# option. (It looks a bit like Java if you squint.)

Kitchen.cs
 //------------------------------------------------------------------------- //<autogenerated> //Thiscodewasgeneratedbyatool. //RuntimeVersion:1.0.3705.209 // //Changestothisfilemaycauseincorrectbehaviorandwillbelost //ifthecodeisregenerated. //</autogenerated> //------------------------------------------------------------------------- // //Thissourcecodewasauto-generatedbyxsd,Version=1.0.3705.209. // usingSystem.Xml.Serialization; ///<remarks/> [System.Xml.Serialization .XmlTypeAttribute(Namespace="http://www.fourthcoffee.com/xmlcakes")] [System.Xml.Serialization. XmlRootAttribute("Kitchen", Namespace="http://www.fourthcoffee.com/xmlcakes",IsNullable=false)] publicclassKitchen{ ///<remarks/> [System.Xml.Serialization.XmlElementAttribute("Cake")] publicKitchenCake[]Items; } ///<remarks/> [System.Xml.Serialization .XmlTypeAttribute(Namespace="http://www.fourthcoffee.com/xmlcakes")] publicclassKitchenCake{ ///<remarks/> publicstringFilling; ///<remarks/> publicstringMessage; ///<remarks/> publicstringShape; ///<remarks/> publicstringSize; ///<remarks/> [System.Xml.Serialization.XmlElementAttribute("Baker")] publicKitchenCakeBaker[]Baker; } ///<remarks/> [System.Xml.Serialization .XmlTypeAttribute(Namespace="http://www.fourthcoffee.com/xmlcakes")] publicclassKitchenCakeBaker{ ///<remarks/> publicstringName; } 

If the input stream does not match what is expected, the deserialization process will attempt to recover as best it can, but as a result one or more objects might be set to null when the procedure has completed. To help you handle these situations, the XmlSerializer class publishes four events that you can trap. These events are raised when certain conditions arise. They are

  • UnknownAttribute

    This occurs if an attribute whose type is not known is presented in the input stream.

  • UnknownElement

    Likewise, this occurs if an unknown element appears in the input stream.

  • UnknownNode

    This occurs if an unexpected node appears.

  • UnreferencedObject

    This applies only to SOAP messages and occurs when the XmlSerializer finds a type definition in the input stream that is not actually used anywhere else in the input stream.

You can catch these events by creating an appropriate delegate and referencing a method to be executed when the event is raised. The System.Xml.Serialization namespace supplies a delegate for each of these events: XmlAttributeEventHandler , XmlElementEventHandler , XmlNodeEventHandler , and UnreferencedObjectEventHandler .

You subscribe to an event by calling the apposite add event method of the XmlSerializer object. The following code shows how to intercept the UknownElement event:

 privatevoidhandleXmlElementEvent(Objectsender, XmlElementEventArgse) { } XmlSerializerserializer=newXmlSerializer(...); serializer.add_UnknownElement(new XmlElementEventHandler(handleXmlElementEvent)); KitchenDatakitchen=(KitchenData)serializer.Deserialize(stream); 

The EventArgs parameter passed to the event handler contains information about the unexpected element and the position in the input stream at which it occurred. You can use this information to take some corrective action or record the fact that some unexpected input was received.

I l @ ve RuBoard


Microsoft Visual J# .NET (Core Reference)
Microsoft Visual J# .NET (Core Reference) (Pro-Developer)
ISBN: 0735615500
EAN: 2147483647
Year: 2002
Pages: 128

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net