XML Schemas
As you know, all XML documents must be well formed. For example, tags cannot overlap. They must specify some sort of hierarchy. But often the benefits of producing a well-formed document aren't enough. Saying that the XML elements cannot overlap is not as useful as saying that the XML elements cannot overlap and must follow a specific order or use certain tag names. The XML Schema specification identifies an XML vocabulary that you can use to create other XML vocabularies. In doing so, you tell consumers of your schema how your XML should be constructed to be considered valid by your design.
Understanding XML Schemas
Many times as you develop XML documents, you often need to place constraints on the way data is represented in the document. You might be concerned that a particular set of XML elements follows a specific order, or you might want to identify an XML element as containing text that actually represents a specific datatype, such as a floating point.
To place constraints on data, you must build Document Type Definitions (DTDs) or XML Schemas to provide data about the data, also known as metadata. DTDs were an early XML constraint mechanism. And although DTDs are beneficial to many XML applications, they do not have the characteristics necessary for describing constructs such as inheritance or complex datatypes. To overcome these limitations, a working group was formed to produce XML Schemas based on an original draft from Microsoft. The XML Schema specification is divided into two parts. The first part, XML Schema Part 1: Structures, proposes a way to structure and constrain document content. The second part, XML Schema Part 2: Data Types, provides a way to describe both primitive and complex datatypes within a document.
The XML Schema specification establishes a means by which the XML Schema language describes the structure and content of XML documents. A desirable feature of XML Schemas is the fact that they are represented in XML, so standard XML parsers can be used to navigate them.
At this point, you are already familiar with XML and many of the terms used to identify the concepts behind XML. The XML Schema draft defines several new terms that help describe the semantics of using and understanding schemas.
Instances and Schema
An XML instance document refers to the document element, including elements, attributes, and content contained within the document, that conforms to an XML Schema. Instances in the more general sense may refer to any element (including its attributes and content) that conforms to an XML Schema. An instance that conforms to a schema is considered to be schema-valid.
Schemas can be independent XML documents, or they can be embedded inside other XML with references to the schema. Schemas take this form:
<xsd:schema xmlns:xsd="http://www.w3.org/TR/xmlschema-1/"> <!--type definitions, element declarations, etc. --> </xsd:schema>
Definitions and Declarations
A great advantage of schemas is that they enable you to create simple or complex types for applying classifications to elements. As in most programming languages, this is called type definition and is shown as follows:
<xsd:schema xmlns:xsd="http://www.w3.org/TR/xmlschema-1/"> <xsd:complexType name="Person"> <xsd:element name="FirstName" type="xsd:string" /> <xsd:attribute name="Age" type="xsd:integer" /> </xsd:complexType> </xsd:schema>
The preceding example also shows an element declaration (for the element <FirstName/>) and an attribute declaration (for the attribute Age) that are local to a particular type named Person.
Beyond participating in the type definition, elements also may be declared as top-level elements of a particular type, as shown in the following example, where BaseballPlayer is a type of Person:
<xsd:schema xmlns:xsd="http://www.w3.org/TR/xmlschema-1/"> <!-- ...Type definition... --> <xsd:element name="BaseballPlayer" type="Person" /> </schema>
Attributes, however, can be of only simple types, as defined in XML Schema Part 2: Datatypes, such as string, boolean, and float.
Target Namespace
Because element and attribute declarations are used to validate instances, it is necessary for them to match the namespace characteristic of a particular instance. This implies that declarations have an association with a target namespace URI or no namespace at all, depending on whether the instance has a qualified name. For a schema to specify a target namespace, it must use the targetNamespace attribute, as follows:
<xsd:schema xmlns:xsd="http://www.w3.org/TR/xmlschema-1/" targetNamespace="SomeNamespaceURI"> <xsd:element name="ElementInNS" type="xsd:string" /> <xsd:complexType name="TypeInNS"> <xsd:element name="LocalElementInNS" type="xsd:integer" /> <xsd:attribute name="LocalAttrInNS" type="xsd:string" /> </xsd:complexType> </xsd:schema>
As you can see, all global and local elements are associated with SomeNamespaceURI. Lack of the targetNamespace attribute designates that no namespace is associated.
Datatypes and Schema Constraints
Datatypes consist of a value space, lexical space, and facets. The value space is the datatype's permitted set of values, and it can have various properties associated with it. A set of valid literals for a datatype makes up the lexical space of that datatype. Finally, a facet is a single dimension of a concept that enables you to distinguish among different datatypes. Two kinds of facets are used to describe datatypes, fundamental and constraining.
Fundamental facets enable you to describe the order, bounds, cardinality, exactness, and numeric properties of a given datatype's value space.
Constraining facets enable you to describe the constraints on a datatype's value space. Possible constraints include minimum and maximum length, pattern matching, upper and lower bounds, and enumeration of valid values.
The following is the fragment of a simple type definition:
<xsd:simpleType name="HourType"> <xsd:restriction base="xsd:integer"> <xsd:minInclusive value="1" /> <xsd:maxInclusive value="12" /> </xsd:restriction> </datatype>
In this case, HourType is defined to be of the built-in integer datatype and additionally is constrained to values between 1 and 12. This new type can then be used in other type definitions as in the following Hour attribute:
<xsd:complexType name="Time"> <xsd:attribute name="Hour" type="HourType" /> <xsd:attribute name="Minute" type="MinuteType" /> </xsd:complexType>
The instance for this type might look something like this:
<Time Hour="7" Minute="30" />
That was also an example of a complex type definition. The complex type definition combines one or more simple types to form something new. Here is another complex type example:
<xsd:element name="cars" type="CarsType"/> <xsd:complexType name="CarsType"> <xsd:element name="car" type="CarType" minoccurs="0" maxoccurs="unbounded"/> </xsd:complexType> <xsd:complexType name="CarType"> <xsd:element name="make" type="xsd:string"/> <xsd:element name="model" type="xsd:string"/> </xsd:complexType>
This type can be represented by an instance as follows:
<cars xmlns:xsi="http://www.w3.org/TR/xmlschema-1/" xsi:noNamespaceSchemaLocation="CarSchema.xsd"> <car> <make>Cheverolet</make> <model>Corvette</model> </car> </cars>
minOccurs and maxOccurs
Elements and attributes enable you to specify the minimum and maximum number of times that they may appear in the instance. The following example shows how you can force an attribute to appear one and only one time:
<xsd:element name="Book"> <attribute name="Author" type="A" minOccurs="1" maxOccurs="1" /> <attribute name="Title" type="T" minOccurs="1" maxOccurs="1" /> </xsd:element>
The maxOccurs attribute can also be set to unbounded to denote that the element or attribute can appear many times. You also can prevent a value from appearing by setting the maxOccurs attribute equal to 0.
Deriving Type Definitions
Similar to the way object-oriented programming languages work, schemas enable you to derive types from other types in a controlled way. When defining a new type, you may choose to extend or restrict the other type definition.
When extending another type definition, you can introduce additional elements and attributes, as shown in the following example:
<xsd:complexType name="Book"> <xsd:element name="Title" type="xsd:string" /> <xsd:element name="Author" type="xsd:string" /> </xsd:complexType> <xsd:complexType name="ElectronicBook"> <xsd:complexContent> <xsd:extension base="Book"> <xsd:sequence> <element name="URL" type="xsd:string" /> </xsd:sequence> </xsd:extension> </xsd:complexContent> </xsd:complexType>
Sometimes an instance wants to explicitly indicate its type. To do this, the instance can use the XML Schema instance namespace definition of xsi:type, as follows:
<Car xsi:type="SportsCar"> <Driver>Me</Driver> </Car>
Although this was not an exhaustive coverage of schemas, at least you now should realize the following:
You can constrain your XML documents and their content using XML Schema.
The XML Schema specification provides you with a set of built-in datatypes.
You can use the built-in datatypes or create your own datatypes.
Schemas may be standalone documents or may be combined within other XML documents.
This information is here because XML Schemas are important to .NET. Let's see why.
.NET Web Services and XML Schemas
If you happen to glance at the SOAP specification, you'll find a section that describes how to encode method parameters, Section 5. This section was necessary when the SOAP specification was introduced because there was no way to otherwise describe the SOAP XML. If you couldn't somehow validate the incoming SOAP packets, you could not extract the method's parameter data and actually invoke the method on your local systems.
Section 5 is becoming far less important today because of the Web Service Description Language (WSDL), as you'll see in detail in Chapter 5, "Web Service Description and Discovery." WSDL serves as an interface description document that you can use to determine what XML information the Web Service will accept. In other words, you can change the XML formatting for your Web Service by changing the way you describe the Web Service in its WSDL file. As it happens, there is a schema embedded within the WSDL file.
This actually makes a lot of sense. If you think about it, handing someone an arbitrary XML document and expecting that person to figure out by inspection just what you're asking him to do is a very complex undertaking, if that person even has enough information to make an informed decision. On the other hand, if you hand that same person a document that outlines the datatypes of your method parameters and the order in which they can be found, you've given that person enough information to decipher the XML instance documents that you intend to transmit.
For this reason, you'll find an XML schema embedded within the WSDL document that describes your Web Service. Essentially, with your WSDL document, you're telling the other side what datatypes you expect and how you want them ordered, as well as how they should appear within the SOAP packet. Web Services are now significantly more flexible.
Now that you know how XML documents are formed and how they are validated, how do you get the values associated with the XML elements back out of the XML document? This is the job of XPath.
We will discuss XPath in some detail, mainly because many people who have worked with XML to some degree still might require a little XPath brush-up. And it's XPath that makes your work easier if you need to reach into a SOAP packet and modify what you find, as you might do with a .NET SoapExtension. Why? Because of interoperability, if nothing else, but you might have other reasons as well, depending upon your individual system requirements. For example, you might want to retrieve a SOAP parameter value and encrypt it.
Let's take a more detailed look at XPath to see what it can offer when you're tweaking XML.
XPath Drilldown
Imagine that you have this XML document you created for yourself to remind you how to access a couple of your favorite Web Services:
<?xml version="1.0"?> <Servers> <Server name="Gumby"> <WebService wsdl="?wsdl"> <Family>Calculators</Family> <EndpointURL>http://www.myurl.com/calc</EndpointURL> </WebService> </Server> <Server name="Pokey"> <WebService wsdl=".wsdl"> <Family>Time</Family> <EndpointURL>http://www.myurl.com/time</EndpointURL> </WebService> </Server> </Servers>
This totally fictitious XML document describes two imaginary Web Service servers. The first provides some sort of calculator service, based upon the service's family, and the second gives the time. The calculator service sends its WSDL by adding ?WSDL to the endpoint URL, which is how .NET works. The second does the same by concatenating .WSDL, which is how the SOAP Toolkit works. Of course, only two servers are shown in this case. You could have hundreds or more, so the corresponding XML document could grow to be quite large. How will you find the one particular server's information that is of interest to you?
Now let's say that you want to retrieve all the servers that are part of the Time family. You could use this XPath query string:
/Servers/Server/WebService[./Family="Time"]
The result of this query is a nodeset, which is a set of XML elements that match the query. The query itself can be called a location step, which can be broken into three parts:
The axis
The node test
The predicate
Let's take a closer look at each.
The XPath Axis
The axis is optionalin fact, this particular location step has no axis identified. You use the axis to move through the XML document in some other manner than from the top down. This is because the default location step is child::, so the XPath query returns, by default, a nodeset containing children of the current context node. The context node is the current XML element that you happen to be examining, which, in this case, is the root or document element. Other possible axes include ancestor::, parent::, following::, and a myriad of other possible values, all shown in Table 3.1.
Table 3.1 XPath Axis Values
Axis |
Purpose |
---|---|
ancestor |
Ancestors of the context node, including the root node if the context node is not already the root node |
ancestor-or-self |
Same as ancestor, but includes the context node |
attribute |
Attributes of the context node (context node should be an element) |
child |
All (immediate) children of the context node |
descendant |
All children of the context node, regardless of depth |
descendant-or-self |
Same as the descendant, but includes the context node |
following |
All nodes following the context node, in document order |
following-or-sibling |
Only sibling nodes following the context node, in document order |
namespace |
Namespace of the context node (the context node should be an element) |
parent |
Immediate parent of the context node, if any (that is, not the root node) |
preceding |
Similar to the following, except returns preceding nodes in document order |
preceding-sibling |
Same as preceding, but for sibling nodes only |
self |
The context node itself |
You probably noticed the term document order mixed into Table 3.1. Document order refers not to the ordering and hierarchy of XML elements, but instead to the literal order in which the element is found in the document, whether it is a parent, sibling, or whatever. Essentially, when you access nodes in document order, you're flattening any hierarchy that might be present.
The XPath Node Test
The node test is effectively a road map that shows the element names in progression, from the start of the document to the particular element in question. It's literally a path from the document element (the root XML element) to the data that you're testing for inclusion into the result nodeset. XPath, as usually implemented, is often more efficient if you specify the complete element path. However, you could have written the example location step as this:
//WebService[./Family="Time"]
The initial double slash, //, tells the XPath processor to start at the document element, search recursively for the <WebService/> element, and, after finding it, execute the predicate.
The XPath Predicate
The predicate, sometimes referred to as the filter, is a Boolean test that you apply to make a final decision about the particular XML element that XPath is examining. If the predicate returns a true result, the XML element is added to the result nodeset. If not, the element is discarded from the nodeset. Essentially, you're fine-tuning an XML element filter. For example, given the two servers shown in the example XML document, the initial nodeset returned from the axis yields both servers, as does the result of the node test. That is, both <Server/> elements have children <WebService/> elements. It's the predicate that distinguishes them, in this case, because only the second server, Pokey, exposes a Time family Web Service.
The node test, being a pathway into the XML document, is sensitive to both the alphabetical case and the namespace of the particular XML element shown in the path. That is, imagine that you mistyped the example location step in this manner:
/servers/server/webservice[./family="Time"]
The resulting nodeset would be empty, whereas before it contained the element for the Pokey server. Notice in the second XPath query that all the text is lowercase, which is why it would fail.
To help with XPath query generation, we wrote the application that you see in Figure 3.2. The XPathExerciser is a utility that enables you to load an XML document, display its contents so that you can see what your queries should produce, type in an XPath location step, and display the resulting nodeset using a tree control.
You'll examine the source code for the XPathExerciser when we discuss .NET's XML handling capabilities, starting with the upcoming section ".NET and XPath." This is a tool that you truly will use because you can never be too expert at recording XPath expressions.
Figure 3.2 The XPathExerciser user interface.
XPath Operators
XPath is a language all its own. Like any programming language, XPath has a set of operators. The operators represent intrinsic capabilities that XPath can perform upon requestyou see these listed in Table 3.2.
Table 3.2 XPath Intrinsic Operators
Operator |
Purpose |
---|---|
/ |
Child operator, which selects child nodes or specifies the root node |
// |
Recursive descent, which looks for a specified element at any depth |
. |
Current context node (akin to C++ this or VB me) |
.. |
Shorthand notation for parent of current context node (akin to moving up a file directory) |
* |
Wildcard, which selects all elements regardless of their element name |
: |
Namespace operator (same use as in XML proper) |
@ |
Attribute operator, which prefixes an attribute name |
@ |
Attribute wildcard (when used alone), which is semantically equivalent to * |
+ |
Addition indicator |
- |
Subtraction indicator |
* |
Multiplication indicator |
div |
Floating-point division indicator |
mod |
Modulo (remainder from a truncating division operation) |
() |
Precedence operator |
[] |
Operator that applies a filter (akin to a Boolean test) |
[] |
Set subscript operator (akin to an array index specification) |
Not too much in Table 3.2 should be too surprising. The square brackets, [ and ], indicate either an array or a filter pattern depending upon how you use them. The single period, ., indicates the current context node, much like the same operator does in a Windows file path. The same is true for the dual period, ... The @ indicates an attribute. Otherwise, you have operators that you would expect to see, such as the wildcard operator, *, and mathematical operations.
Returning to the previous example, you could locate all the SOAP Toolkit Web Services stored in the XML document using this XPath location step:
/Servers/Server/WebService[./@wsdl=".wsdl"]
You could accomplish the same task with these two location steps:
//WebService[./@wsdl=".wsdl"] /Servers/*/*[./@wsdl=".wsdl"]
If some of the servers had no wsdl attribute but others did, you could test merely for the presence of the attribute, like so:
/Servers/Server/WebService[./@wsdl]
In this case, the nodeset would contain both servers shown in the example XML document, but this is only because both servers have a wsdl attribute. If one server had no wsdl attribute, the predicate would fail for that particular node, and that XML element would be removed from the result nodeset.
XPath Intrinsic Functions
In addition to operators, XPath has an entire suite of intrinsic functions that it exposes to help with your queries. There are a lot of these, so the more commonly used functions are distilled in Table 3.3.
Table 3.3 Commonly Used XPath Intrinsic Functions
Operator |
Purpose |
---|---|
ceiling() |
Is the smallest integer not less than the argument |
count(nodeset) |
Gives the number of nodes in the nodeset argument |
contains(string,string) |
Returns true if the first argument contains the second |
false() |
Always returns a Boolean false |
floor() |
Is the largest integer not greater than the argument |
last() |
Gives the context size (number of nodes in context node set) |
local-name() |
Returns the local name of the first node (document order) |
name() |
Returns the QName of the first node (document order) |
node() |
Returns true for any type of node |
not() |
Indicates a logical negation |
number(object) |
Converts object to a number |
position() |
Gives the index number of the node within the parent |
starts-with(string,string) |
Returns true if the first argument starts with the second |
string(object) |
Turns object into a string |
sum(nodeset) |
Converts nodeset to numerical values and adds them |
true() |
Always returns a Boolean true |
For a complete list of XPath intrinsic functions, you should refer to a good XPath reference. You'll probably find that these functions will handle most of your XPath needs, however.
Using the Web Service server example, you could identify the first server, or an arbitrary server, using this location step:
/Servers/Server[position()="1"]
Similarly, you find the last server like so:
/Servers/Server[last()]
If you want all the servers but the last one, you query the document in this way:
/Servers/Server[not(position()=last())]
Many people want to locate XML information within a document based upon string values or string searches. Say, for example, that you want all the servers that have names starting with the letter G:
/Servers/Server[starts-with(string(./@name),"G")]
Of course, this will return the Gumby server's XML information.
As you can see, you can produce a wide variety of queries, especially if you combine an axis with a node test and a filter. This becomes important later if you need to crack open a SOAP packet to examine and modify the contents by hand.
SOAP uses another XML technology called XPointer. Let's now turn to that technology and see what it offers the Web Service.