Reading and Writing XML

Reading and Writing XML

The FCL s System.Xml namespace offers a variety of classes for reading and writing XML documents. For DOM lovers, there s the XmlDocument class, which looks and feels like MSXML but is simpler to use. If you prefer a stream-based approach to reading XML documents, you can use XmlTextReader or the schema-aware XmlValidatingReader instead. A complementary class named XmlTextWriter simplifies the process of creating XML documents. These classes are the first line of defense when battle plans call for manipulating XML.

The XmlDocument Class

XmlDocument provides a programmatic interface to XML documents that complies with the DOM Level 2 Core specification. It represents a document as an upside-down tree of nodes, with the root element, or document element, at the top. Each node is an instance of XmlNode, which exposes methods and properties for navigating DOM trees, reading and writing node content, adding and removing nodes, and more. XmlDocument derives from XmlNode and adds methods and properties of its own supporting the loading and saving of documents, the creation of new nodes, and other operations.

The following statements create an XmlDocument object and initialize it with the contents of Guitars.xml:

XmlDocument doc = new XmlDocument (); doc.Load ("Guitars.xml");

Load parses the specified XML document and builds an in-memory representation of it. It throws an XmlException if the document isn t well-formed.

A successful call to Load is often followed by reading the XmlDocument s DocumentElement property. DocumentElement returns an XmlNode reference to the document element, which is the starting point for a top-to-bottom navigation of the DOM tree. You can find out whether a given node (including the document node) has children by reading the node s HasChildNodes property. You can enumerate a node s children by reading its ChildNodes property, which returns an XmlNodeList representing a collection of nodes. The combination of HasChildNodes and ChildNodes makes possible a recursive approach to iterating over all the nodes in the tree. The following code loads an XML document and writes a list of its nodes to a console window:

XmlDocument doc = new XmlDocument (); doc.Load ("Guitars.xml"); OutputNode (doc.DocumentElement); . . . void OutputNode (XmlNode node) { Console.WriteLine ("Type={0}\tName={1}\tValue={2}", node.NodeType, node.Name, node.Value); if (node.HasChildNodes) { XmlNodeList children = node.ChildNodes; foreach (XmlNode child in children) OutputNode (child); } }

Run against Guitars.xml in Figure 13-3, it produces the following output:

Type=Element Name=Guitars Value= Type=Element Name=Guitar Value= Type=Element Name=Make Value= Type=Text Name=#text Value=Gibson Type=Element Name=Model Value= Type=Text Name=#text Value=SG Type=Element Name=Year Value= Type=Text Name=#text Value=1977 Type=Element Name=Color Value= Type=Text Name=#text Value=Tobacco Sunburst Type=Element Name=Neck Value= Type=Text Name=#text Value=Rosewood Type=Element Name=Guitar Value= Type=Element Name=Make Value= Type=Text Name=#text Value=Fender Type=Element Name=Model Value= Type=Text Name=#text Value=Stratocaster Type=Element Name=Year Value= Type=Text Name=#text Value=1990 Type=Element Name=Color Value= Type=Text Name=#text Value=Black Type=Element Name=Neck Value= Type=Text Name=#text Value=Maple

Notice the varying node types in the listing s first column. Element nodes represent elements in an XML document, and text nodes represent the text associated with those elements. The following table lists the full range of possible node types, which are represented by members of the XmlNodeType enumeration. Whitespace nodes represent insignificant white space that is, white space that appears between markup elements and therefore contributes nothing to a document s content and aren t counted among a document s nodes unless you set XmlDocument s PreserveWhitespace property, which defaults to false, equal to true before calling Load.

XmlNodeType

Example

Attribute

<Guitar Image="MySG.jpeg">

CDATA

<![CDATA["This is character data"]]>

Comment

<!-- This is a comment -->

Document

<Guitars>

DocumentType

<!DOCTYPE Guitars SYSTEM "Guitars.dtd">

Element

<Guitar>

Entity

<!ENTITY filename "Strats.xml">

EntityReference

&lt;

Notation

<!NOTATION GIF89a SYSTEM "gif">

ProcessingInstruction

<?xml-stylesheet type="text/xsl" href="Guitars.xsl"?>

Text

<Model>Stratocaster</Model>

Whitespace

<Make/>\r\n<Model/>

XmlDeclaration

<?xml version="1.0"?>

Observe that the preceding output contains no attribute nodes even though the input document contained two elements having attributes. That s because attributes get special treatment. A node s ChildNodes property doesn t include attributes, but its Attributes property does. Here s how you d modify the OutputNode method to list attributes as well as other node types:

void OutputNode (XmlNode node) { Console.WriteLine ("Type={0}\tName={1}\tValue={2}", node.NodeType, node.Name, node.Value); if (node.Attributes != null) { foreach (XmlAttribute attr in node.Attributes) Console.WriteLine ("Type={0}\tName={1}\tValue={2}", attr.NodeType, attr.Name, attr.Value); } if (node.HasChildNodes) { foreach (XmlNode child in node.ChildNodes) OutputNode (child); } }

An XmlNode object s NodeType, Name, and Value properties expose the type, name, and value of the corresponding node. For some node types (for example, elements), Name is meaningful and Value is not. For others (text nodes, for instance), Value is meaningful but Name is not. And for still others attributes being a great example both Name and Value are meaningful. Name returns a node s qualified name, which includes a namespace prefix if a prefix is present (for example, win:Guitar). Use the LocalName property to retrieve names without prefixes.

You don t have to iterate through every node in a document to find a specific node or set of nodes. You can use XmlDocument s GetElementsByTagName, SelectNodes, and SelectSingleNode methods to target particular nodes. The sample application in Figure 13-5 uses GetElementsByTagName to quickly create an XmlNodeList targeting all of the document s Guitar nodes. SelectNodes and SelectSingleNode execute XPath expressions. XPath is introduced later in this chapter.

XmlDocument can be used to write XML documents as well as read them. The following code sample opens Guitars.xml, deletes the first Guitar element, adds a new Guitar element, and saves the results back to Guitars.xml:

XmlDocument doc = new XmlDocument (); doc.Load ("Guitars.xml"); // Delete the first Guitar element XmlNode root = doc.DocumentElement; root.RemoveChild (root.FirstChild); // Create element nodes XmlNode guitar = doc.CreateElement ("Guitar"); XmlNode elem1 = doc.CreateElement ("Make"); XmlNode elem2 = doc.CreateElement ("Model"); XmlNode elem3 = doc.CreateElement ("Year"); XmlNode elem4 = doc.CreateElement ("Color"); XmlNode elem5 = doc.CreateElement ("Neck"); // Create text nodes XmlNode text1 = doc.CreateTextNode ("Gibson"); XmlNode text2 = doc.CreateTextNode ("Les Paul"); XmlNode text3 = doc.CreateTextNode ("1959"); XmlNode text4 = doc.CreateTextNode ("Gold"); XmlNode text5 = doc.CreateTextNode ("Rosewood"); // Attach the text nodes to the element nodes elem1.AppendChild (text1); elem2.AppendChild (text2); elem3.AppendChild (text3); elem4.AppendChild (text4); elem5.AppendChild (text5); // Attach the element nodes to the Guitar node guitar.AppendChild (elem1); guitar.AppendChild (elem2); guitar.AppendChild (elem3); guitar.AppendChild (elem4); guitar.AppendChild (elem5); // Attach the Guitar node to the document node root.AppendChild (guitar); // Save the modified document doc.Save ("Guitars.xml");

Other XmlDocument methods that are useful for modifying document content include PrependChild, InsertBefore, InsertAfter, RemoveAll, and ReplaceChild. As an alternative to manually creating text nodes and making them children of element nodes, you can assign text by writing to elements InnerText properties. By the same token, reading an element node s InnerText property is a quick way to retrieve the text associated with an XML element.

XmlDocument is typically used by applications that read XML documents and care about the relationships between nodes. Figure 13-6 shows one such application. Called XmlView, it s a Windows Forms application that reads an XML document and displays it in a tree view control. Each item in the control represents one node in the document. Items are color-coded to reflect node types. Items without colored blocks represent attributes.

Figure 13-6

Windows Forms XML viewer.

XmlView s source code appears in Figure 13-7. Clicking the Load button activates XmlViewForm.OnLoadDocument, which loads an XmlDocument from the specified data source and calls a local method named AddNodeAndChildren to recursively navigate the document tree and populate the tree view control. The end result is a graphic depiction of the document s structure and a handy tool for digging around in XML files to see what they re made of. XmlView is compiled slightly differently than the Windows Forms applications in Chapter 4. Here s the command to compile it:

csc /t:winexe /res:buttons.bmp,Buttons xmlview.cs

The /res switch embeds the contents of Buttons.bmp in XmlView.exe and assigns the resulting resource the name Buttons . Buttons.bmp contains an image depicting the colored blocks used in the tree view control. The statement

NodeImages.Images.AddStrip (new Bitmap (GetType (), "Buttons"));

loads the image and uses it to initialize the ImageList named NodeImages. Packaging the image as an embedded resource makes the resulting executable self-contained.

XmlView.cs

using System; using System.Drawing; using System.Windows.Forms; using System.Xml; class XmlViewForm : Form { GroupBox DocumentGB; TextBox Source; Button LoadButton; ImageList NodeImages; TreeView XmlView; public XmlViewForm () { // Initialize the form's properties Text = "XML Viewer"; ClientSize = new System.Drawing.Size (488, 422); // Instantiate the form's controls DocumentGB = new GroupBox (); Source = new TextBox (); LoadButton = new Button (); XmlView = new TreeView (); // Initialize the controls Source.Anchor = AnchorStyles.Top AnchorStyles.Left AnchorStyles.Right; Source.Location = new System.Drawing.Point (16, 24); Source.Size = new System.Drawing.Size (336, 24); Source.TabIndex = 0; Source.Name = "Source"; LoadButton.Anchor = AnchorStyles.Top AnchorStyles.Right; LoadButton.Location = new System.Drawing.Point (368, 24); LoadButton.Size = new System.Drawing.Size (72, 24); LoadButton.TabIndex = 1; LoadButton.Text = "Load"; LoadButton.Click += new System.EventHandler (OnLoadDocument); DocumentGB.Anchor = AnchorStyles.Top AnchorStyles.Left AnchorStyles.Right; DocumentGB.Location = new Point (16, 16); DocumentGB.Size = new Size (456, 64);

 DocumentGB.Text = "Document"; DocumentGB.Controls.Add (Source); DocumentGB.Controls.Add (LoadButton); NodeImages = new ImageList (); NodeImages.ImageSize = new Size (12, 12); NodeImages.Images.AddStrip (new Bitmap (GetType(), "Buttons")); NodeImages.TransparentColor = Color.White; XmlView.Anchor = AnchorStyles.Top AnchorStyles.Bottom AnchorStyles.Left AnchorStyles.Right; XmlView.Location = new System.Drawing.Point (16, 96); XmlView.Size = new System.Drawing.Size (456, 308); XmlView.ImageList = NodeImages; XmlView.TabIndex = 2; XmlView.Name = "XmlView"; // Add the controls to the form Controls.Add (DocumentGB); Controls.Add (XmlView); } void OnLoadDocument (object sender, EventArgs e) { try { XmlDocument doc = new XmlDocument (); doc.Load (Source.Text); XmlView.Nodes.Clear (); AddNodeAndChildren (doc.DocumentElement, null); } catch (Exception ex) { MessageBox.Show (ex.Message); } } void AddNodeAndChildren (XmlNode xnode, TreeNode tnode) { TreeNode child = AddNode (xnode, tnode); if (xnode.Attributes != null) { foreach (XmlAttribute attribute in xnode.Attributes) AddAttribute (attribute, child); } if (xnode.HasChildNodes) { foreach (XmlNode node in xnode.ChildNodes) AddNodeAndChildren (node, child); } } TreeNode AddNode (XmlNode xnode, TreeNode tnode) { string text = null; TreeNode child = null; TreeNodeCollection tnodes = (tnode == null) ? XmlView.Nodes : tnode.Nodes; switch (xnode.NodeType) { case XmlNodeType.Element: case XmlNodeType.Document: tnodes.Add (child = new TreeNode (xnode.Name, 0, 0)); break; case XmlNodeType.Text: text = xnode.Value; if (text.Length > 128) text = text.Substring (0, 128) + "..."; tnodes.Add (child = new TreeNode (text, 2, 2)); break; case XmlNodeType.CDATA: text = xnode.Value; if (text.Length > 128) text = text.Substring (0, 128) + "..."; text = String.Format ("<![CDATA]{0}]]>", text); tnodes.Add (child = new TreeNode (text, 3, 3)); break; case XmlNodeType.Comment: text = String.Format ("<!--{0}-->", xnode.Value); tnodes.Add (child = new TreeNode (text, 4, 4)); break; case XmlNodeType.XmlDeclaration: case XmlNodeType.ProcessingInstruction: text = String.Format ("<?{0} {1}?>", xnode.Name, xnode.Value); tnodes.Add (child = new TreeNode (text, 5, 5)); break; case XmlNodeType.Entity: text = String.Format ("<!ENTITY {0}>", xnode.Value); tnodes.Add (child = new TreeNode (text, 6, 6)); break; case XmlNodeType.EntityReference: text = String.Format ("&{0};", xnode.Value); tnodes.Add (child = new TreeNode (text, 7, 7)); break; case XmlNodeType.DocumentType: text = String.Format ("<!DOCTYPE {0}>", xnode.Value); tnodes.Add (child = new TreeNode (text, 8, 8)); break; case XmlNodeType.Notation: text = String.Format ("<!NOTATION {0}>", xnode.Value); tnodes.Add (child = new TreeNode (text, 9, 9)); break; default: tnodes.Add (child = new TreeNode (xnode.NodeType.ToString (), 1, 1)); break; } return child; } void AddAttribute (XmlAttribute attribute, TreeNode tnode) { string text = String.Format ("{0}={1}", attribute.Name, attribute.Value); tnode.Nodes.Add (new TreeNode (text, 1, 1)); } static void Main () { Application.Run (new XmlViewForm ()); } }

Figure 13-7

Source code for XmlView.

Incidentally, the FCL includes a class named XmlDataDocument that s closely related to and, in fact, derives from XmlDocument. XmlDataDocument is a mechanism for treating relational data as XML data. You can wrap an XmlDataDocument around a DataSet, as shown here:

DataSet ds = new DataSet (); // TODO: Initialize the DataSet with a database query XmlDataDocument doc = new XmlDataDocument (ds);

This action layers an XML DOM over a DataSet and allows the DataSet s contents to be read and written using XmlDocument semantics.

The XmlTextReader Class

XmlDocument is an efficient and easy-to-use mechanism for reading XML documents. It allows you to move backward, forward, and sideways within a document and even make changes to the document as you go. But if your intent is simply to read XML and you re less interested in the structure of the document than its contents, there s another way to go about it: the FCL s XmlTextReader class. XmlTextReader, which, like XmlDocument, belongs to the System.Xml namespace, provides a fast, forward-only, read-only interface to XML documents. It s stream-based like SAX. It s more memory-efficient than XmlDocument, especially for large documents, because it doesn t read an entire document into memory at once. And it makes it even easier than XmlDocument to read through a document searching for particular elements, attributes, or other content items.

Using XmlTextReader is simplicity itself. The basic idea is to create an XmlTextReader object from a file, URL, or other data source, and to call XmlTextReader.Read repeatedly until you find the content you re looking for or reach the end of the document. Each call to Read advances an imaginary cursor to the next node in the document. XmlTextReader properties such as NodeType, Name, Value, and AttributeCount expose information about the current node. Methods such as GetAttribute, MoveToFirstAttribute, and MoveToNextAttribute let you access the attributes, if any, attached to the current node.

The following code fragment wraps an XmlTextReader around Guitars.xml and reads through the entire file node by node:

XmlTextReader reader = null; try { reader = new XmlTextReader ("Guitars.xml"); reader.WhitespaceHandling = WhitespaceHandling.None; while (reader.Read ()) { Console.WriteLine ("Type={0}\tName={1}\tValue={2}", reader.NodeType, reader.Name, reader.Value); } } finally { if (reader != null) reader.Close (); }

Running it against the XML document in Figure 13-3 produces the following output:

Type=XmlDeclaration Name=xml Value=version="1.0" Type=Element Name=Guitars Value= Type=Element Name=Guitar Value= Type=Element Name=Make Value= Type=Text Name= Value=Gibson Type=EndElement Name=Make Value= Type=Element Name=Model Value= Type=Text Name= Value=SG Type=EndElement Name=Model Value= Type=Element Name=Year Value= Type=Text Name= Value=1977 Type=EndElement Name=Year Value= Type=Element Name=Color Value= Type=Text Name= Value=Tobacco Sunburst Type=EndElement Name=Color Value= Type=Element Name=Neck Value= Type=Text Name= Value=Rosewood Type=EndElement Name=Neck Value= Type=EndElement Name=Guitar Value= Type=Element Name=Guitar Value= Type=Element Name=Make Value= Type=Text Name= Value=Fender Type=EndElement Name=Make Value= Type=Element Name=Model Value= Type=Text Name= Value=Stratocaster Type=EndElement Name=Model Value= Type=Element Name=Year Value= Type=Text Name= Value=1990 Type=EndElement Name=Year Value= Type=Element Name=Color Value= Type=Text Name= Value=Black Type=EndElement Name=Color Value= Type=Element Name=Neck Value= Type=Text Name= Value=Maple Type=EndElement Name=Neck Value= Type=EndElement Name=Guitar Value= Type=EndElement Name=Guitars Value=

Note the EndElement nodes in the output. Unlike XmlDocument, XmlText-Reader counts an element s start and end tags as separate nodes. XmlTextReader also includes whitespace nodes in its output unless told to do otherwise. Setting its WhitespaceHandling property to WhitespaceHandling.None prevents a reader from returning whitespace nodes.

Like XmlDocument, XmlTextReader treats attributes differently than other nodes and doesn t return them as part of the normal iterative process. If you want to enumerate attribute nodes, you have to read them separately. Here s a revised code sample that outputs attribute nodes as well as other nodes:

XmlTextReader reader = null; try { reader = new XmlTextReader ("Guitars.xml"); reader.WhitespaceHandling = WhitespaceHandling.None; while (reader.Read ()) { Console.WriteLine ("Type={0}\tName={1}\tValue={2}", reader.NodeType, reader.Name, reader.Value); if (reader.AttributeCount > 0) { while (reader.MoveToNextAttribute ()) { Console.WriteLine ("Type={0}\tName={1}\tValue={2}", reader.NodeType, reader.Name, reader.Value); } } } } finally { if (reader != null) reader.Close (); }

A common use for XmlTextReader is parsing an XML document and extracting selected node values. The following code sample finds all the Guitar elements that are accompanied by Image attributes and echoes the attribute values to a console window:

XmlTextReader reader = null; try { reader = new XmlTextReader ("Guitars.xml"); reader.WhitespaceHandling = WhitespaceHandling.None; while (reader.Read ()) { if (reader.NodeType == XmlNodeType.Element && reader.Name == "Guitar" && reader.AttributeCount > 0) { while (reader.MoveToNextAttribute ()) { if (reader.Name == "Image") { Console.WriteLine (reader.Value); break; } } } } } finally { if (reader != null) reader.Close (); }

Run against Guitars.xml (Figure 13-3), this sample produces the following output:

MySG.jpeg MyStrat.jpeg

It s important to close an XmlTextReader when you re finished with it so that the reader, in turn, can close the underlying data source. That s why all the samples in this section call Close on their XmlTextReaders and do so in finally blocks.

The XmlValidatingReader Class

XmlValidatingReader is a derivative of XmlTextReader. It adds one important feature that XmlTextReader lacks: the ability to validate XML documents as it reads them. It supports three schema types: DTD, XSD, and XML-Data Reduced (XDR). Its Schemas property holds the schema (or schemas) that a document is validated against, and its ValidationType property specifies the schema type. ValidationType defaults to ValidationType.Auto, which allows XmlValidating Reader to determine the schema type from the schema document provided to it. Setting ValidationType to ValidationType.None creates a nonvalidating reader the equivalent of XmlTextReader.

XmlValidatingReader doesn t accept a file name or URL as input, but you can initialize an XmlTextReader with a file name or URL and wrap an XmlValidatingReader around it. The following statements create an XmlValidating Reader and initialize it with an XML document and a schema document:

XmlTextReader nvr = new XmlTextReader ("Guitars.xml"); XmlValidatingReader reader = new XmlValidatingReader (nvr); reader.Schemas.Add ("", "Guitars.xsd");

The first parameter passed to Add identifies the target namespace, if any, specified in the schema document. An empty string means the schema defines no target namespace.

Validating a document is as simple as iterating through all its nodes with repeated calls to XmlValidatingReader.Read:

while (reader.Read ());

If the reader encounters well-formedness errors as it reads, it throws an XmlException. If it encounter validation errors, it fires ValidationEventHandler events. An application that uses an XmlValidatingReader can trap these events by registering an event handler:

reader.ValidationEventHandler += new ValidationEventHandler (OnValidationError);

The event handler receives a ValidationEventArgs containing information about the validation error, including a textual description of it (in ValidationEventArgs.Message) and an XmlSchemaException (in ValidationEvent Args.Exception). The latter contains additional information about the error such as the position in the source document where the error occurred.

Figure 13-8 lists the source code for a console app named Validate that validates XML documents against XSD schemas. To use it, type the command name followed by the name or URL of an XML document and the name or URL of a schema document, as in

validate guitars.xml guitars.xsd

As a convenience for users, Validate uses an XmlTextReader to parse the schema document for the target namespace that s needed to add the schema to the Schemas collection. (See the GetTargetNamespace method for details.) It takes advantage of the fact that XSDs, unlike DTDs, are XML documents themselves and can therefore be read using XML parsers.

Validate.cs

using System; using System.Xml; using System.Xml.Schema; class MyApp { static void Main (string[] args) { if (args.Length < 2) { Console.WriteLine ("Syntax: VALIDATE xmldoc schemadoc"); return; } XmlValidatingReader reader = null; try { XmlTextReader nvr = new XmlTextReader (args[0]); nvr.WhitespaceHandling = WhitespaceHandling.None; reader = new XmlValidatingReader (nvr); reader.Schemas.Add (GetTargetNamespace (args[1]), args[1]); reader.ValidationEventHandler += new ValidationEventHandler (OnValidationError); while (reader.Read ()); } catch (Exception ex) {

 Console.WriteLine (ex.Message); } finally { if (reader != null) reader.Close (); } } static void OnValidationError (object sender, ValidationEventArgs e) { Console.WriteLine (e.Message); } public static string GetTargetNamespace (string src) { XmlTextReader reader = null; try { reader = new XmlTextReader (src); reader.WhitespaceHandling = WhitespaceHandling.None; while (reader.Read ()) { if (reader.NodeType == XmlNodeType.Element && reader.LocalName == "schema") { while (reader.MoveToNextAttribute ()) { if (reader.Name == "targetNamespace") return reader.Value; } } } return ""; } finally { if (reader != null) reader.Close (); } } }

Figure 13-8

Utility for validating XML documents.

The XmlTextWriter Class

The FCL s XmlDocument class can be used to modify existing XML documents, but it can t be used to generate XML documents from scratch. XmlTextWriter can. It features an assortment of Write methods that emit various types of XML, including elements, attributes, comments, and more. The following example uses some of these methods to create an XML file named Guitars.xml containing a document element named Guitars and a subelement named Guitar:

XmlTextWriter writer = null; try { writer = new XmlTextWriter ("Guitars.xml", System.Text.Encoding.Unicode); writer.Formatting = Formatting.Indented; writer.WriteStartDocument (); writer.WriteStartElement ("Guitars"); writer.WriteStartElement ("Guitar"); writer.WriteAttributeString ("Image", "MySG.jpeg"); writer.WriteElementString ("Make", "Gibson"); writer.WriteElementString ("Model", "SG"); writer.WriteElementString ("Year", "1977"); writer.WriteElementString ("Color", "Tobacco Sunburst"); writer.WriteElementString ("Neck", "Rosewood"); writer.WriteEndElement (); writer.WriteEndElement (); } finally { if (writer != null) writer.Close (); }

Here s what the generated document looks like:

<?xml version="1.0" encoding="utf-16"?> <Guitars> <Guitar Image="MySG.jpeg"> <Make>Gibson</Make> <Model>SG</Model> <Year>1977</Year> <Color>Tobacco Sunburst</Color> <Neck>Rosewood</Neck> </Guitar> </Guitars>

Setting an XmlTextWriter s Formatting property to Formatting.Indented before writing begins produces the indentation seen in the sample. Skipping this step omits the indents and the line breaks too. The default indentation depth is 2, and the default indentation character is the space character. You can change the indentation depth and indentation character using XmlTextWriter s Indentation and IndentChar properties.



Programming Microsoft  .NET
Applied MicrosoftNET Framework Programming in Microsoft Visual BasicNET
ISBN: B000MUD834
EAN: N/A
Year: 2002
Pages: 101

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