Sdílet prostřednictvím


Understanding XML Namespaces

 

Aaron Skonnard
DevelopMentor

Updated July 2002

"Understanding XML Namespaces," by Aaron Skonnard first appeared in MSDN Magazine, July 2001. This updated version is used with permission. Copyright © 2001 Microsoft Corp. and CMP Media LLC.

Namespaces are the source of much confusion in XML, especially for those new to the technology. Most of the questions that I receive from readers, students, and conference attendees are related to namespaces in one way or another. It's actually kind of ironic since the Namespaces in XML Recommendation is one of the shorter XML specifications, coming in at just under 10 pages, excluding appendices. The confusion, however, is related to namespace semantics as opposed to the syntax outlined by the specification. To fully understand XML namespaces, you must know what a namespace is, how namespaces are defined, and how they are used.

The rest of this column is dedicated to answering these three questions, both syntactically and abstractly. By the time you finish reading this, you'll understand how namespaces affect the family of XML technologies.

What Is a Namespace?

A namespace is a set of names in which all names are unique. For example, the names of my children could be thought of as a namespace, as could the names of California corporations, the names of C++ type identifiers, or the names of Internet domains. Any logically related set of names in which each name must be unique is a namespace.

Namespaces make it easier to come up with unique names. Imagine how hard it would be to name your next child if the name had to be unique across the face of the earth. Restricting uniqueness to a more limited context, like my set of children, simplifies things tremendously. When I name my next child, my only consideration is that I don't use the same name that I already used for one of my other children. Another set of parents can choose the same name I choose for one of their children, but those names will be part of distinct namespaces and, therefore, can be easily distinguished.

Before a new name is added to a namespace, a namespace authority must ensure that the new name doesn't already exist in the namespace. In some scenarios this is trivial as it is in child naming. In others it's quite complex. Today's many Internet naming authorities present a good example. If this step is skipped, however, duplicate names will eventually corrupt the namespace, making it impossible to refer to certain names without ambiguity. When this happens, the set of names is no longer officially considered a namespace—by definition a namespace must ensure the uniqueness of its members.

Namespaces themselves must also be given names in order to be useful. Once a namespace has a name, it's possible to refer to its members. For example, consider the sample namespaces shown in the two boxes in Figure 1. The names of these sample namespaces are Microsoft and AcmeHardware, respectively. Notice that even though both of these namespaces contain some of the same local names, it's possible to refer to them without ambiguity through namespace-qualified names, also shown in Figure 1.

Figure 1. Non-ambiguous namespaces

Of course, this assumes that the namespace names are also unique. If this cannot be guaranteed, then the actual namespace names themselves could also be placed into a namespace of their own. For example, if there are multiple AcmeHardware stores (one in California and one in Utah), placing the name AcmeHardware in two distinct namespaces resolves the conflict, as shown here:

California.AcmeHardware.Paint
Utah.AcmeHardware.Paint

This pattern can be continued as many times as necessary to guarantee the uniqueness of namespace names. This is exactly how the Internet Domain Name System (DNS) works— it's simply one big namespace of namespaces.

Without this type of namespace partitioning, you would be forced to use extremely long (uncommon) names to ensure uniqueness:

MicrosoftWindowsOperatingSystemPaintApplication

Imagine the complexity and the resulting frustration of only having a single global namespace that could not be partitioned. People rely heavily on namespaces in day-to-day social interactions, although in most situations they're not made explicit. To use namespaces in software development, however, they must be made explicit through concrete syntax. Before turning to namespaces in XML, let's look at an example of namespace syntax in one of today's mainstream programming languages.

Namespaces in Programming Languages

To use namespaces in a programming language, you must become familiar with the syntax for defining a namespace and referring to something within a namespace. Many of today's languages including C++, Java, and C# provide support for namespaces. In C++, a namespace is defined through a namespace block, as shown below.

namespace foo1
{
   class bar
   {
      ...
   };
   class baz
   {
      ...
   };
}
namespace foo2
{
   class bar
   {
      ...
   };
   class baz
   {
      ...
   };
}

This example defines two namespaces, foo1 and foo2, each of which contains two names, bar and baz (which are class identifiers in this case).

foo1::bar b1;   // refers to bar class in foo1
foo2::bar b2;   // refers to bar class in foo2

To refer to the bar class of a particular namespace, the bar identifier must be qualified with the given namespace identifier.

It's also possible to declare that you're using a particular namespace in a given source file for convenience. This essentially makes the specified namespace one of the default namespaces for the source file. Then it's no longer necessary to fully qualify the particular namespace members, unless of course it's absolutely required to avoid ambiguity:

using namespace foo1;
bar b1; // refers to bar class in foo1

As you can see, the syntax for defining and using namespaces in C++ is simple and straightforward. C# works very much the same way with a few minor variations. The syntax for namespaces in Java is somewhat different, but the concept is the same.

Namespaces are used in many programming languages to help avoid name collisions. This is exactly the type of solution needed to complete the XML 1.0 specification.

Namespaces in XML

Many developers feel that the XML 1.0 specification was incomplete because it didn't offer namespace support. As a result, all names used in XML documents belonged to one global namespace, making it very difficult to come up with unique names.

Most developers, including the XML 1.0 authors themselves, knew that this would eventually cause too much ambiguity in large XML-based distributed systems. For example, consider the following XML document:

<student>
  <id>3235329</id>
  <name>Jeff Smith</name>
  <language>C#</language>
  <rating>9.5</rating>
</student>

This document uses several names, each of which are quite commonplace. The student element models a student of a software-training course. The id, language, and rating elements model the student's database record number, programming language of preference, and the rating the student gave the course (out of 10). Each of these names will certainly be used in other situations where they don't convey the same meaning.

For example, here is another XML document that uses the same names in a completely different way:

<student>
  <id>534-22-5252</id>
  <name>Jill Smith</name>
  <language>Spanish</language>
  <rating>3.2</rating>
</student>

In this case, the student element models an elementary school student. Now the id, language, and rating elements model the child's Social Security Number, native spoken language, and the student's current grade point average (out of 4), respectively. The authors of these two documents could have used longer, less common names to help ensure uniqueness, but in the end that doesn't guarantee uniqueness anyway, and they're harder to use.

Although humans might be able to look at these two documents and tell the difference, they would look exactly the same to a piece of software. Imagine that you're responsible for building a student-management application that must support many different student-related XML documents, including those just mentioned. As you write the code, how are you going to (programmatically) distinguish between the professional student and the elementary school student, or any other type of student, for that matter? There is no reliable way to make the distinction.

Anytime elements and attributes from distinct XML vocabularies are being used in the same document or application, name collisions arise. Consider XSLT, which is itself an XML vocabulary for defining transformations. In a given transformation it's possible to output user-defined literal elements. Therefore, since the XSLT vocabulary contains an element named template, how would it ever be possible to output a user-defined literal element also named template?

<!-- this is the template element from XSLT -->
<template match="foo">
  <!-- I want to output this template element -->
  <template match="foo"/>
</template>

The potential for name collisions become extremely evident in languages like XSLT and XML Schema that heavily mix XML vocabularies. These problems could easily be avoided, however, if XML provided support for namespaces.

The Namespaces in XML Recommendation is the W3C's solution to the XML 1.0 naming woes. This specification defines how to extend the XML 1.0 concrete syntax to support namespaces. Because most developers consider this addition fundamental and absolutely necessary, it is often given the respect of an official XML 1.0 addendum, even though it isn't one. In fact, today many developers refuse to refer to XML 1.0 alone but rather as "XML 1.0 + Namespaces" for the same reason.

The Namespaces in XML Recommendation defines the syntax for naming XML namespaces as well as the syntax for referring to something in an XML namespace. It doesn't address, however, the syntax for defining what's in the XML namespace. This was left to another specification, namely XML Schema. Each of these areas requires some explanation.

Naming Namespaces

When you define a namespace in a programming language like C++, there are restrictions on the characters that may be used in the name. XML namespace identifiers must also conform to a specific syntax—the syntax for Uniform Resource Identifier (URI) references. This means that XML namespace identifiers must follow the generic syntax for URIs defined by RFC 2396.

A URI is defined as a compact string of characters for identifying an abstract or physical resource. In most situations, URI references are used to identify physical resources (Web pages, files to download, and so on), but in the case of XML namespaces, URI references identify abstract resources, specifically, namespaces.

According to the URI specification, there are two general forms of URI: Uniform Resource Locators (URL) and Uniform Resource Names (URN). Either type of URI may be used as a namespace identifier. Here is an example of two URLs that could be used as a namespace identifiers:

https://www.develop.com/student
https://www.ed.gov/elementary/students

And here are a few examples of URNs that could also be used as namespace identifiers:

urn:www-develop-com:student
urn:www.ed.gov:elementary.students
urn:uuid:E7F73B13-05FE-44ec-81CE-F898C4A6CDB4

The most important attribute of a namespace identifier is that it is unique. Authors can guarantee the uniqueness of a URL by registering a domain name with an Internet naming authority. Then it's the author's responsibility to make sure that all strings used after the domain name are unique.

URNs work the same way. The following is basic URN syntax:

urn:<namespace identifier>:<namespace specific string>

To guarantee the uniqueness of a URN, authors must again register their namespace identifier with an Internet naming authority. The author is then responsible for following a scheme for generating unique namespace-specific strings.

Organizations defining XML namespaces should develop a consistent scheme for creating new namespace names. The W3C, for example, is constantly defining new XML namespaces. They use a fairly intuitive heuristic that uses the current year and the name of the working group. Figure 2 illustrates the pattern used by the W3C.

Figure 2. W3C URI Constructions

By definition, a URI is unique, so there is never a need to layer additional namespaces on top of XML namespace identifiers. As long as the namespace author guarantees the uniqueness of the namespace identifier, it's always possible to uniquely identify something in XML with only a single namespace qualifier. This greatly simplifies the job of working with namespaces in XML.

XML processors treat namespace identifiers as opaque strings and never as resolvable resources. Let me repeat: namespace identifiers are just strings! Two namespace identifiers are considered identical when they are exactly the same, character for character.

In the end, it really doesn't matter which type of URI reference you choose to use. Many developers like to use URLs because they are easier to read and remember, while others prefer URNs because of their flexibility. Whichever type you choose, make sure you know how to ensure uniqueness.

Defining Namespaces

The Namespaces in XML Recommendation did not provide syntax for defining what's in a namespace. In many situations, this type of syntactic definition is not even necessary. Today, most XML namespaces are defined in formal specification documents that describe the names of elements as well as attributes along with their semantics. This is exactly how all of the W3C namespaces are formally defined (see the XSLT 1.0 specification at https://www.w3.org/TR/xslt for an example).

Once a namespace has been defined, software developers implement the namespace as outlined by the specification. For example, MSXML 3.0, Xalan, and Saxon are all implementations of the XSLT 1.0 specification. These implementations are hardcoded to look for elements that belong to the XSLT 1.0 namespace (https://www.w3.org/1999/XSL/Transform). To use these implementations, you need to feed them an XML document that uses the names from the XSLT 1.0 namespace correctly (more on this in the next section). If anything in the XSLT 1.0 namespace were to change, the supporting software would have to be updated.

The XML Schema working group (https://www.w3.org/XML/Schema) has put together a new specification (XML Schema) that defines an XML-based syntax for defining elements, attributes, and types in a namespace. XML Schema finally makes it possible to provide a syntactic definition of a namespace, as illustrated below.

<schema xmlns='https://www.w3.org/2000/10/XMLSchema'
   targetNamespace='https://www.develop.com/student'
   elementFormDefault='qualified'
>
  <element name='student'>
     <complexType>
         <sequence>
            <element name='id' type='long'/>
            <element name='name' type='string'/>
            <element name='language' type='string'/>
            <element name='rating' type='double'/>         
         </sequence>
     </complexType>
   </element>
</schema>

This example defines the namespace https://www.develop.com/student as containing four named elements: student, id, name, language, and rating. Besides just providing a namespace, this schema also provides additional metadata, such as the order of the student child elements as well as their types.

With syntactic namespace definitions like those offered by XML Schema in place, it's possible to build more sophisticated software that leverages the name and type information at runtime. XML Schemas still don't define the semantics of the defined elements and attributes and would therefore still require an accompanying specification. In the future, most XML namespaces will be defined through both specifications and schema definitions.

Using Namespaces

I define using a namespace as the process of using one or more elements or attributes from the given namespace in an XML document. This requires an understanding of the syntax outlined by the Namespaces in XML Recommendation for qualifying element and attribute names with namespace identifiers.

The names of both elements and attributes are really made up of two parts: a namespace name and a local name. Such a two-part name is known as a qualified name or QName.

In an XML document we use a namespace prefix to qualify the local names of both elements and attributes . A prefix is really just an abbreviation for the namespace identifier (URI), which is typically quite long. The prefix is first mapped to a namespace identifier through a namespace declaration. The syntax for a namespace declaration is:

xmlns:<prefix>='<namespace identifier>'

A namespace declaration looks just like an attribute (on an element), but they're not officially considered attributes in terms of the logical document structure (that is, they won't appear in an element's attributes collection when using the DOM).

A namespace prefix is considered in-scope on the declaration element as well as on any of its descendant elements. Once declared, the prefix can be used in front of any element or attribute name separated by a colon (such as s:student). This complete name including the prefix is the lexical form of a qualified name (QName):

QName = <prefix>:<local name>

The prefix associates the element or attribute with the namespace identifier mapped to the prefix currently in scope.

Let's suppose that a developer wants to use the XSLT 1.0 namespace. He would need to provide a namespace declaration that maps an arbitrary prefix to the official XSLT 1.0 namespace identifier (https://www.w3.org/1999/XSL/Transform). Then each element or attribute that the developer wants to use from the XSLT 1.0 namespace simply needs to be prefixed accordingly, as illustrated by the following example:

<x:transform version='1.0'
   xmlns:x='https://www.w3.org/1999/XSL/Transform'
>
   <x:template match='/'>
      <hello_world/>
   </x:template>
</x:transform>

The previous example shows the syntax for referring to elements within a namespace. Every element prefixed by "x" is from the https://www.w3.org/1999/XSL/Transform namespace, while anything that does not have a prefix is from no namespace (for example, hello_world). Processors can now distinguish between XSLT 1.0 programming constructs and literal elements that are meant for output, like hello_world. If the XSLT 1.0 namespace were misspelled by one character, an XSLT 1.0 processor wouldn't be able to recognize the document as a vocabulary it understands.

In essence, each element now has a two-part name, a namespace identifier and a local name. The combination of these two names is often referred to as the namespace name (note: this is different from the QName, which is the combination of the prefix and the local name).

As another example, the following XML document shows how to use the elements from the XML Schema definition shown earlier in this column:

<d:student xmlns:d='https://www.develop.com/student'>
  <d:id>3235329</d:id>
  <d:name>Jeff Smith</d:name>
  <d:language>C#</d:language>
  <d:rating>9.5</d:rating>
</d:student>

Notice that regardless of how namespaces are defined, the syntax for referring to them is the same.

When documents use elements or attributes from more than one namespace, it's common to have multiple namespace declarations on a given element, as illustrated by the following example:

<d:student xmlns:d='https://www.develop.com/student'
  xmlns:i='urn:schemas-develop-com:identifiers'
  xmlns:p='urn:schemas-develop-com:programming-languages'
>
  <i:id>3235329</i:id>
  <name>Jeff Smith</name>
  <p:language>C#</p:language>
  <d:rating>9.5</d:rating>
</d:student>

Here, student and rating are both from the same namespace, while id and language are each from a different namespace, but name doesn't belong to a namespace.

Namespace prefixes can also be overridden by redeclaring the prefix at a nested scope, as shown here:

<d:student xmlns:d='https://www.develop.com/student'>
  <d:id>3235329</d:id>  
  <d:name xmlns:d='urn:names-r-us'>Jeff Smith</d:name>
  <d:language>C#</d:language>
  <d:rating>35</d:rating>
</d:student>

In this example, everything is from the same namespace except for the name element, which is from the urn:names-r-us namespace. While it is possible to re-declare a namespace prefix, it's not possible to undeclare a namespace prefix. For example, the following is illegal:

<d:student xmlns:d='https://www.develop.com/student'>
  <d:id xmlns:d=''>3235329</d:id>  
   ...
</d:student>

The prefix-oriented syntax for referring to things in an XML namespaces is fairly intuitive for most software developers. Had the Namespaces in XML Recommendation stopped here, namespaces would have been much less confusing.

Default Namespaces

There is one more type of namespace declaration that can be used to associate namespace identifiers with element names. This is known as a default namespace declaration, which uses the following syntax:

xmlns='<namespace identifier>'

Notice that there is no prefix. When a default namespace declaration is used on an element, all unqualified element names within its scope are automatically associated with the specified namespace identifier. Default namespace declarations, however, have absolutely no effect on attributes. The only way to associate an attribute with a namespace identifier is through a prefix.

Consider the following example:

<d:student  xmlns:d='https://www.develop.com/student'
     xmlns='urn:foo' id='3235329'
>
  <name>Jeff Smith</name>
  <language xmlns=''>C#</language>
  <rating>35</rating>
</d:student>

Here, "student" is from the https://www.develop.com/student namespace while "name" and "rating" are from the default namespace urn:foo. The id attribute doesn't belong to a namespace since attributes aren't automatically associated with the default namespace identifier.

This example also illustrates that you can undeclare a default namespace by simply setting the default namespace identifier back to the empty string, as shown in the language element (remember you cannot do this with prefix declarations). As a result, the language element also doesn't belong to a namespace.

The syntax for default namespaces was designed for convenience, but they tend to cause more confusion than they're worth. The confusion typically stems from the fact that elements and attributes are treated differently and it's not immediately apparent that nested elements are being assigned the default namespace identifier. Nevertheless, in the end, choosing between prefixes and default namespaces is mostly a matter of style, except when attributes come into play.

Namespace Abstractions

Dealing with namespaces from the abstract view of an XML document is much simpler than dealing with the lexical issues just described. The XML Information Set (Infoset) defines the abstract structure of an XML document, which shields developers from the complexities of the underlying serialization format, like the namespace syntax just described.

According to the Infoset, each element or attribute has two name properties: a namespace identifier and a local name. Figure 3 illustrates the logical structure of an XML document that contains namespace-qualified names. Notice that student, id, and language are all from the same namespace, while ratings is from a different namespace and name belongs to no namespace. This document could be serialized out using either of the techniques described in the previous section.

Figure 3. Namespace-qualified XML Document

Consider how today's mainstream APIs, SAX and DOM, implement this abstract data model. SAX models elements through the startElement/endElement method calls of ContentHandler:

public interface contentHandler
{
...
void startElement(String namespaceURI, String localName, 
   String qName, Attributes atts) throws SAXException;
void endElement(String namespaceURI, String localName, 
   String qName) throws SAXException;
...
}

Notice that elements are identified by the combination of their namespace identifier and local name (and optionally, the QName). Attributes are also identified through a set of namespace-aware methods on the Attributes interface. It's up to the SAX parser (or any other producer application) to provide namespace names as it delivers the document stream. That said, using SAX it's simple to distinguish between different types of student elements programmatically.

...
void startElement(String namespaceURI, String localName, 
   String qName, Attributes atts)
{
    if ( namespaceURI.equals("urn:dm:student") &&
         localName.equals("student") )
       {
        // process Developmentor student element here
    }
    else if ( namespaceURI.equals("urn:www.ed.gov:student") 
         && localName.equals("student") )
       {
        // process elementary school student element here
    }
}
...

Since the namespace name (namespace identifier + local name) is automatically resolved by the SAX parser, it doesn't matter what prefix (if any) was used on a particular element or attribute in the source document—it's mostly a serialization detail. That does not mean, however, that prefixes can be thrown away once parsed. Consider the following XML document:

<student xmlns:xsd='https://www.w3.org/2000/10/XMLSchema'
 xmlns:xsi='https://www.w3.org/2000/10/XMLSchema-instance'
>
  <age xsi:type='xsd:double'>35.0</age>
</student>

Notice that the XML Schema xsi:type attribute on age contains a QName value. Any time a QName is used in element or attribute content, the consuming application is required to deal with it manually. The only way the consuming application can interpret this value correctly is if it knows what namespace identifier "xsd" is bound to. For this reason, the Infoset also maintains the set of in-scope namespace declarations for each element in the document. SAX models this information through the startPrefixMapping and endPrefixMapping method calls.

The DOM API is another implementation of the Infoset. The DOM's Node interface models the basic identity of element/attribute nodes through two name properties: namespaceURI and localName. It also models the node's QName and prefix through the nodeName and prefix properties. The Java language code below illustrates how you could distinguish between two different student elements using the DOM.

void processStudent(Node n)
{
    if ( n.getNamespaceURI().equals("urn:dm:student") &&
         n.getLocalName().equals("student") )
       {
        // process Developmentor student element here
    }
    else if ( 
        n.getNamespaceURI().equals("urn:www.ed.gov:student") 
        && n.getLocalName().equals("student") )
       {
        // process elementary school student element here
    }
}

As with SAX, the XML parser that builds the DOM tree is responsible for populating the namespace properties appropriately. So again, it doesn't matter how the namespaces were declared in the source document once you're working with the logical document structure. If you're creating the document through the DOM API, then you're responsible for supplying the namespace identifiers for each element and attribute upon creation:

void generateStudentDocument(Document doc)
{
   Node docEl = doc.createElementNS("urn:dm:student", "student");
   doc.appendChild(docEl);
   Node n = doc.createElementNS("", "name");
   docEl.appendChild(n);
   ...
}

As you can see, this code allows you to create the logical structure directly. Then it's up to the DOM implementation to figure out how to serialize the namespace declarations into the underlying XML 1.0 document. This particular DOM tree could be serialized as follows:

<student xmlns='urn:dm:student'>
   <name xmlns=''/>
</student>

When you're dealing with the abstract view of an XML document (via the SAX/DOM APIs) it's important to note that there is no notion of a default namespace. In the previously cited example, after the call to createElementNS for "student", urn:dm:student didn't magically become the default namespace. The call to createElementNS for "name" with no namespace assigned an empty namespace identifier to the name element (not urn:dm:student). The same applies to SAX with respect to a sequence of startElement/endElement method calls. Each element/attribute node is always treated independently with respect to name information.

XPath is another XML specification that defines how to identify nodes in the abstract document structure. XPath expressions make it possible to identify elements and attributes by namespace-qualified names. Since XPath name tests are simple string expressions, the only way to associate an XPath name test with a namespace identifier is through a namespace prefix.

You can think of XPath node tests as being of type QName. That means if a node test does not include a prefix, it's like asking for the given name that belongs to "no namespace". For example, take the following XPath expression:

/student/name

This expression identifies all name elements that belong to no namespace that are children of the root student element that belongs to no namespace. To identify the student and name elements that belong to the urn:dm:student namespace, first it's necessary to associate a namespace prefix with urn:dm:student. Then that prefix can be used in the XPath expression.

Assuming that "dm" has been associated with urn:dm:student in the XPath context, the following expression will now identify name elements belonging to the urn:dm:store namespace that are children of the root student element also belonging to the urn:dm:store namespace:

/dm:student/dm:name

If the queried document looked like the code that follows, then the previous expression would identify all three name elements that are children of student, regardless of their prefixes, because they're all from the same namespace in question.

<s:student xmlns:s='urn:dm:student'>
   <s:name/>
   <n:name xmlns:n='urn:dm:student'/>
   <s:name/>
</s:student>

Prefixes are mapped in the XPath context in an implementation-dependent fashion (see the May 2001 column of The XML Files for more information on how this works in MSXML 3.0). One example of this is XSLT, which provides a context for using XPath expressions. To use namespace-qualified XPath expressions in an XSLT document, a standard namespace declaration can be used to map the target namespace identifier to an arbitrary prefix:

<x:transform version='1.0'
   xmlns:x='https://www.w3.org/1999/XSL/Transform'
   xmlns:d='urn:dm:student'
>
   <x:template match='d:student'>
     <!-- transform student here -->
     <x:apply-templates select='d:name'/>
   </x:template>
   ...
</x:transform>

Notice that the first template matches on the student element from the urn:dm:student namespace. If the match value were simply "student", it would only match student elements from no namespace. The apply-templates element then processes all child name elements that are also the urn:dm:student namespace.

As you can see, understanding how namespaces work both lexically and abstractly is crucial to understanding the entire family of XML specifications. You will encounter many situations similar to these scattered throughout the emerging XML specifications.

Recap

A namespace is a set of names in which all names are unique. Namespaces in XML make it possible to give elements and attributes unique names. Although namespaces tend to be the source of much confusion, they're easy to comprehend once you become familiar with how they're defined and used, both syntactically and abstractly. For more information about namespaces, see the Namespaces in XML Recommendation and XML Namespaces by Example.

Aaron Skonnard is an instructor and researcher at DevelopMentor, where he develops the XML curriculum. Aaron coauthored Essential XML (Addison-Wesley Longman, 2000) and wrote Essential WinInet (Addison-Wesley Longman, 1998). Get in touch with Aaron at https://staff.develop.com/aarons.