Chapter 13. Vertical XSLT Application Recipes

A newcomer wonders if there’s a secret handshake or code required to delve into its riddles . . . such as knowing what to bring to a potluck.

From a book review of Potluck: Stories That Taste Like Hawaii

Introduction

This chapter differs from the others because the examples represent mini-XSLT applications covering a diverse set of domains (a potluck, if you will). Many examples relate to the use of specific commercial software. As software vendors embrace XML, they provide opportunities for their products to be used in ways they never imagined (or did not get around to implementing).

Microsoft is one vendor that has jumped on the XML bandwagon. The latest versions of Microsoft Visio (Version 10.0) and Excel (Office XP Version 10.0) both support XML output. Visio is a proprietary vector drawing package, and Visio’s XML output (called Visio VDX) is also Visio-specific. John Breen has done an admirable job converting this output to Scalable Vector Graphics (SVG). His code is featured in Recipe 13.1.

Microsoft Excel allows spreadsheets to be saved in XML. Unfortunately, the XML directly models the structure of an Excel spreadsheet. Recipe 13.2 shows how to convert them to a more usable form.

Topic Maps are an up-and-coming XML technology for modeling knowledge in a way that makes information published on the Web more useful to both people and machines. XTM is an open standard for representing topic maps in XML. By analogy, software developers model knowledge about systems by using the Unified Modeling Language (UML). UML has its own standard XML representation known as XML Metadata Interchange (XMI). UML and Topic Maps do not serve the same audience; however, UML is rich enough to capture the concepts addressed by Topic Maps if you follow certain conventions. Since UML has been around longer than Topic Maps, the software tools are more mature. Recipe 13.3 shows how the XMI output of a popular UML authoring tool (Rational Rose) can be converted into XTM Topic Maps.[1]

One of XTM’s most useful features is its ability to generate web sites. Recipe 13.4 addresses this Topic Map application. Nikita Ogievetsky contributed this recipe based on his work on the Cogitative Topic Maps Web Site (CTW) framework

Finally, we clean up the chapter with some SOAP, the W3C’s XML format for implementing web services. The Simple Object Access Protocol is a way for software systems to communicate via standardized XML messages. This section addresses a SOAP-related draft specification called Web Service Definition Language (WSDL). As its name implies, WSDL is an XML specification for documenting a SOAP service. This discussion shows how to convert WSDL into human-readable documentation.

The examples in this chapter are long, but you can find the full source code at http://www.oreilly.com/catalog/xsltckbk/.

13.1. Converting Visio VDX Documents to SVG

Problem

You want to convert Microsoft Visio XML files (VDX) into more portable SVG files.

Solution

John Breen implemented the following solution. He maps the major Visio elements to SVG as shown in Table 13-1.

Table 13-1. Visio-to-SVG mappings

Visio element

SVG element

VisioDocument/Colors/ColorEntry

color value

VisioDocument/Stylesheets/StyleSheet

CSS Style

VisioDocument/Pages/Page
Svg
Page/Shapes/Shape
G
Shapes/Shape/@NameU
@id
Shapes/Shape/XForm
Transform
XForm/PinX and XForm/PinX
translate()
Shapes/Shape/Fill/FillForegnd
@fill (with lookup)
Shapes/Shape/Geom
Path
Shape/Geom/MoveTo
@d "M"
Shape/Geom/LineTo
@d "L"
Shape/Geom/NoFill(0)
@d "z"

This section goes over only the main stylesheet and select portions of the included ones. The entire source code with examples is available at http://sourceforge.net/projects/vdxtosvg/:

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:v="urn:schemas-microsoft-com:office:visio"
  xmlns:xlink="http://www.w3.org/1999/xlink"
  xmlns:math="java.lang.Math"
  xmlns:jDouble="java.lang.Double"
  xmlns:saxon="http://icl.com/saxon"
  exclude-result-prefixes="v math saxon jDouble"
  xmlns="http://www.w3.org/2000/svg"
  version="1.0">
   
  <xsl:output method="xml"
    version="1.0"
    omit-xml-declaration="no"
    media-type="image/svg+xml"
    encoding="iso-8859-1"
    indent="yes"
    cdata-section-elements="style"
    doctype-public="-//W3C//DTD SVG 1.0//EN"
    doctype-system="http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"
    />

The stylesheet uses a parameter pageNumber for specifying what page should be extracted from the VDX, and the parameter userScale specifies by which amount to scale Visio units to user units:

  <xsl:param name="pageNumber" select="1"/>
  <xsl:param name="userScale"  select="100"/>
   
  <!-- =  =  =  =  Variables (ie, Constants) =  =  =  =  =  =  =  =  =  =  =  =  =  = -->
  <!-- Color map -->
  <xsl:variable name="Colors"
                select="//v:Colors[position()=1]/v:ColorEntry"/>
   
  <!-- Page being processed -->
  <xsl:variable name="Page"
            select="/v:VisioDocument/v:Pages/v:Page[number($pageNumber)]"/>
   
  <!-- Template Masters -->
  <xsl:variable name="Masters"
                select="//v:Masters[position()=1]/v:Master"/>
   
  <!-- viewBox Master -->
  <xsl:variable name="viewBoxMaster"
                select="$Masters[@NameU='viewBox']"/>
   
  <!-- Ratio of font height to width (fudge factor) -->
  <xsl:variable name="fontRatio"
                select="2"/>
   
  <!-- Pi (SVG uses degrees, Visio uses radians) -->
  <xsl:variable name="pi" select="3.14159265358979323846264338327"/>

The stylesheet is decomposed into several components that are included here. Portions of these modules are discussed later in this section. The stylesheet implements some extensions in JavaScript; however, if your XSLT processor does not support JavaScript, you can still use this code. Some text might not format nicely, however:

  <!-- Included files -->
  <xsl:include href="visio-style.xsl"/>
  <xsl:include href="visio-text.xsl"/>
  <xsl:include href="visio-masters.xsl"/>
  <xsl:include href="visio-nurbs.xsl"/>
   
   <!-- Scripts -->
  <xsl:template name="required-scripts">
    <script xlink:href="wordwrap.js" type="text/ecmascript"/>
  </xsl:template>
   
  <xsl:template match="/v:VisioDocument">
    <xsl:apply-templates
      select="$Page"/>
  </xsl:template>

A Visio page is mapped onto an SVG graphic. Information from the Visio document determines how best to lay out the graphic in a view box:

  <!-- =  =  =  =  =  =  =  = Page =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  = = =-->
  <xsl:template match="v:Page">
    <xsl:message>
      <xsl:value-of select="@NameU"/>
    </xsl:message>
    <svg id="{@NameU}">
      <xsl:attribute name="xml:space">
        <xsl:value-of select="'preserve'"/>
      </xsl:attribute>
      <xsl:choose>
        <!-- Use viewBox with name 'default' if present -->
        <xsl:when test="//v:Shape[@Master=$viewBoxMaster/@ID
                        and @NameU='default'][1]">
          <xsl:for-each
            select="//v:Shape[@Master=$viewBoxMaster/@ID
                    and @NameU='default']">
            <xsl:attribute name="viewBox">
              <xsl:value-of select="concat(
                                    v:XForm/v:PinX*$userScale, ' ',
                                    -v:XForm/v:PinY*$userScale, ' ',
                                    v:XForm/v:Width*$userScale, ' ',
                                    v:XForm/v:Height*$userScale)"/>
            </xsl:attribute>
          </xsl:for-each>
        </xsl:when>
        <!-- Otherwise, center on sheet -->
        <xsl:otherwise>
          <xsl:attribute name="viewBox">
            <xsl:value-of select="concat('0 ', 
                                  -v:PageSheet/v:PageProps/v:PageHeight
                                    *$userScale, ' ', 
                                  v:PageSheet/v:PageProps/v:PageWidth
                                    *$userScale, ' ',
                                  v:PageSheet/v:PageProps/v:PageHeight
                                    *$userScale)"/>
          </xsl:attribute>
        </xsl:otherwise>
      </xsl:choose>
      <xsl:call-template name="required-scripts"/>
      <xsl:call-template name="predefined-pattern-fgnds"/>
      <xsl:call-template name="predefined-markers"/>

The real meat of the conversion begins here. Start by processing Visio stylesheet elements to convert them into equivalent Cascading Style Sheet directives. Then convert all shapes into their SVG representation:

      <xsl:apply-templates select="../../v:StyleSheets"/>
      <xsl:apply-templates select="v:Shapes/v:Shape"/>
    </svg>
  </xsl:template>
   
  <!-- =  =  =  =  =  =  =  = StyleSheets =  =  =  =  =  =  =  =  = -->
  <xsl:template match="v:StyleSheets">
    <defs>
      <xsl:for-each select="v:StyleSheet">
        <!-- Line style -->
        <style id="ss-line-{@ID}" type="text/css">
          <xsl:text>*.ss-line-</xsl:text><xsl:value-of select="@ID"/>
          <xsl:text> { </xsl:text>
          <xsl:call-template name="recursive-line-style">
            <xsl:with-param name="ss" select="."/>
          </xsl:call-template>
          <xsl:text> }</xsl:text>
        </style>
        <!-- Fill style -->
        <style id="ss-fill-{@ID}" type="text/css">
          <xsl:text>*.ss-fill-</xsl:text><xsl:value-of select="@ID"/>
          <xsl:text> { </xsl:text>
          <xsl:call-template name="recursive-fill-style">
            <xsl:with-param name="ss" select="."/>
          </xsl:call-template>
          <xsl:text> }</xsl:text>
        </style>
        <!-- Text style -->
        <style id="ss-text-{@ID}" type="text/css">
          <xsl:text>*.ss-text-</xsl:text><xsl:value-of select="@ID"/>
          <xsl:text> { </xsl:text>
          <xsl:call-template name="recursive-text-style">
            <xsl:with-param name="ss" select="."/>
          </xsl:call-template>
          <xsl:text> } </xsl:text>
        </style>
      </xsl:for-each>
    </defs>
  </xsl:template>
   
  <!-- Recurse through StyleSheet inheritance -->
  <xsl:template name="recursive-line-style">
    <xsl:param name="ss"/>
    <xsl:if test="$ss/@LineStyle">
      <xsl:call-template name="recursive-line-style">
        <xsl:with-param name="ss"
          select="$ss/../v:StyleSheet[@ID=$ss/@LineStyle]"/>
      </xsl:call-template>
    </xsl:if>
    <xsl:apply-templates select="$ss/v:Line" mode="style"/>
  </xsl:template>
   
  <xsl:template name="recursive-fill-style">
    <xsl:param name="ss"/>
    <xsl:if test="$ss/@FillStyle">
      <xsl:call-template name="recursive-fill-style">
        <xsl:with-param name="ss"
          select="$ss/../v:StyleSheet[@ID=$ss/@FillStyle]"/>
      </xsl:call-template>
    </xsl:if>
    <xsl:apply-templates select="$ss/v:Fill" mode="style"/>
  </xsl:template>
   
  <xsl:template name="recursive-text-style">
    <xsl:param name="ss"/>
    <xsl:if test="$ss/@TextStyle">
      <xsl:call-template name="recursive-text-style">
        <xsl:with-param name="ss"
          select="$ss/../v:StyleSheet[@ID=$ss/@TextStyle]"/>
      </xsl:call-template>
    </xsl:if>
    <xsl:apply-templates select="$ss/v:Char|$ss/v:Para" mode="style"/>
  </xsl:template>
   
  <!-- This template returns a string for the line style -->
  <xsl:template match="v:Line" mode="style">
    <xsl:for-each select="v:LineWeight">
      <xsl:text>stroke-width:</xsl:text>
      <xsl:value-of select=". * $userScale"/><xsl:text>;</xsl:text>
    </xsl:for-each>
    <xsl:for-each select="v:LineColor">
      <xsl:choose>
        <xsl:when test="../v:LinePattern > 0">
          <xsl:text>stroke:</xsl:text>
          <xsl:call-template name="lookup-color">
            <xsl:with-param name="c_el" select="."/>
          </xsl:call-template>
        </xsl:when>
        <xsl:when test="../v:LinePattern = 0">
          <xsl:text>stroke:none</xsl:text>
        </xsl:when>
      </xsl:choose>
      <xsl:text>;</xsl:text>
    </xsl:for-each>
    <xsl:for-each select="v:EndArrow">
      <xsl:choose>
        <xsl:when test=". = 0">
          <xsl:value-of select="string('marker-end:none;')"/>
        </xsl:when>
        <xsl:otherwise>
          <xsl:value-of select="concat('marker-end:url(#EndArrow-', .,
                                '-', ../v:EndArrowSize, '),')"/>
        </xsl:otherwise>
      </xsl:choose>
    </xsl:for-each>
    <xsl:apply-templates select="v:LinePattern[. &gt; 1]" mode="style"/>
  </xsl:template>
   
  <!-- This template returns a string for the fill style -->
  <xsl:template match="v:Fill" mode="style">
    <xsl:for-each select="v:FillForegnd">
      <xsl:choose>
        <xsl:when test="../v:FillPattern = 1">
          <xsl:text>fill:</xsl:text>
          <xsl:call-template name="lookup-color">
            <xsl:with-param name="c_el" select="."/>
          </xsl:call-template>
        </xsl:when>
        <xsl:when test="../v:FillPattern = 0">
          <xsl:text>fill:none</xsl:text>
        </xsl:when>
        <xsl:otherwise>
          <xsl:text>fill:url(#</xsl:text>
          <xsl:value-of select="generate-id(../..)"/>
          <xsl:text>-pat)</xsl:text>
        </xsl:otherwise>
      </xsl:choose>
      <xsl:text>;</xsl:text>
    </xsl:for-each>
  </xsl:template>
   
  <!-- This template returns a string for the text style -->
  <xsl:template match="v:Char|v:Para" mode="style">
    <xsl:for-each select="v:Color">
      <!-- I don't think Visio handles filled characters -->
      <xsl:text>stroke:none</xsl:text>
      <xsl:text>;fill:</xsl:text>
      <xsl:call-template name="lookup-color">
        <xsl:with-param name="c_el" select="."/>
      </xsl:call-template>
      <xsl:text>;</xsl:text>
    </xsl:for-each>
    <xsl:for-each select="v:Size">
      <xsl:text>font-size:</xsl:text>
      <xsl:value-of select=". * $userScale"/><xsl:text>;</xsl:text>
    </xsl:for-each>
    <xsl:for-each select="v:HorzAlign">
      <xsl:text>text-anchor:</xsl:text>
      <xsl:choose>
        <xsl:when test="(. = 0) or (. = 3)">
          <xsl:text>start</xsl:text>
        </xsl:when>
        <xsl:when test=". = 1">
          <xsl:text>middle</xsl:text>
        </xsl:when>
        <xsl:when test=". = 2">
          <xsl:text>end</xsl:text>
        </xsl:when>
      </xsl:choose>
      <xsl:text>;</xsl:text>
    </xsl:for-each>
  </xsl:template>
   
  <!-- Ignore all other StyleSheet elements -->
  <xsl:template match="*[parent::v:StyleSheet]" priority="-100"/>

Here is where shapes are mapped onto an SVG equivalent. Notice how shapes can be associated with masters in a Visio document. Think of a master as a template of a shape from which a shape on a page can inherit attributes and behavior.

Each Visio shape is, by default , translated as a <g> element since a Visio shape can contain both graphics and text. Recall that the <g> element is SVG’s way of specifying a group of graphical elements that can share stylistic traits.

SvgElement is a Visio property that the user can attach to a Visio shape to specify special handling by this translator. For example, svgElement can be set to any other SVG container element, such as defs. This feature keeps certain shapes from being rendered, such as paths for animateMotion elements. This way, the path can be referenced in the SVG file, but it will not be displayed.

One of the main reasons for using svgElement is to indicate special shapes that are translated to elements that have no correspondence in Visio, such as animate, animateMotion, and viewBox. visio-master.xsl, discussed later, handles these elements:

  <!-- =  =  =  =  =  =  = Shape =  =  =  =  =  =  =  =  =  =  =  =  = -->
  <xsl:template match="v:Shape">
   
    <xsl:variable name="master"
                  select="/v:VisioDocument//v:Masters[1]/
                           v:Master[@ID=current()/@Master]"/>
   
    <xsl:variable name="svgElement">
      <xsl:choose>
         <!-- Check for special svgElement property in shape ... -->
        <xsl:when test="./v:Prop/v:Label[.='svgElement']">
          <xsl:value-of
            select="./v:Prop/v:Label[.='svgElement']/../v:Value"/>
        </xsl:when>
         <!-- ... and in master -->
        <xsl:when test="@Master and 
                        $master//v:Prop/v:Label[.='svgElement']">
          <xsl:value-of
            select="$master//v:Prop/v:Label[.='svgElement']/../v:Value"/>
        </xsl:when>
   
         <!-- The simple case maps a shape onto a svg (g)roup -->     
        <xsl:otherwise>
          <xsl:value-of select="'g'"/>
        </xsl:otherwise>
      </xsl:choose>
    </xsl:variable>
   
    <xsl:choose>
   
      <xsl:when test="@Master and string($svgElement)
                      and contains($specialMasters, $svgElement)">
        <xsl:call-template name="choose-special-master">
          <xsl:with-param name="master" select="$master"/>
          <xsl:with-param name="masterElement" select="$svgElement"/>
        </xsl:call-template>
      </xsl:when>
   
      <xsl:when test="($svgElement = 'defs') or ($svgElement = 'g') or
                      ($svgElement = 'symbol')">
        <xsl:choose>
          <xsl:when test="v:Hyperlink">
            <!-- Surround shape with 'a' element -->
            <!-- This is a minimal implementation.  It doesn't support
                 multiple links, subaddress, etc. -->
            <a xlink:title="{v:Hyperlink/v:Description}"
              xlink:href="{v:Hyperlink/v:Address}">
              <xsl:if test="v:Hyperlink/v:NewWindow">
                <xsl:attribute name="show">
                  <xsl:value-of select="new"/>
                </xsl:attribute>
              </xsl:if>
              <xsl:element name="{$svgElement}">
                <xsl:call-template name="userShape"/>
              </xsl:element>
            </a>
          </xsl:when>
          <xsl:otherwise>
            <xsl:element name="{$svgElement}">
              <xsl:call-template name="userShape"/>
            </xsl:element>
          </xsl:otherwise>
        </xsl:choose>
      </xsl:when>
    </xsl:choose>
  </xsl:template>

Here the normal shapes created by the user are mapped to SVG, as specified in Table 13-1:

  <!-- This does the processing for normal 'user' shapes -->
  <xsl:template name="userShape">
    <xsl:variable name="master"
      select="/v:VisioDocument/v:Masters
              /v:Master[(@ID=current()/@Master)
                        and (current()/@Type != 'Group')]
              /v:Shapes/v:Shape |
              /v:VisioDocument/v:Masters
              /v:Master[@ID=current()
              /ancestor::v:Shape[@Master]/@Master]
              //v:Shape[@ID=current()/@MasterShape] | ."/>
   
    <xsl:call-template name="setIdAttribute"/>
   
    <xsl:attribute name="class">
      <xsl:for-each select="($master[@LineStyle])[last()]">
        <xsl:text> ss-line-</xsl:text>
        <xsl:value-of select="@LineStyle"/>
      </xsl:for-each>
      <xsl:for-each select="($master[@FillStyle])[last()]">
        <xsl:text> ss-fill-</xsl:text>
        <xsl:value-of select="@FillStyle"/>
      </xsl:for-each>
    </xsl:attribute>
    <xsl:attribute name="style">
      <xsl:for-each select="$master">
        <xsl:apply-templates select="./v:Line" mode="style"/>
        <xsl:apply-templates select="./v:Fill" mode="style"/>
      </xsl:for-each>
    </xsl:attribute>
    <xsl:for-each select="v:XForm">
      <xsl:call-template name="transformAttribute">
      </xsl:call-template>
    </xsl:for-each>
    <!-- This is to create the custom pattern -->
    <xsl:apply-templates select="v:Fill" mode="Shape"/>
    <xsl:for-each select="v:Geom">
      <xsl:apply-templates select="v:Ellipse"/>
      <xsl:if test="v:MoveTo or v:LineTo">
        <xsl:call-template name="pathElement"/>
      </xsl:if>
    </xsl:for-each>
    <xsl:for-each select="($master/v:Text)[last()]">
      <xsl:apply-templates select="."/>
    </xsl:for-each>
   
    <xsl:apply-templates select="v:Shapes/v:Shape"/>
   
    <!-- Add elements from properties -->
    <xsl:for-each select="v:Prop">
      <xsl:choose>
        <xsl:when test="starts-with(v:Label, 'svg-element')">
          <!-- This is sort of ugly - it may disappear some day -->
          <xsl:value-of disable-output-escaping="yes" select="v:Value"/>
        </xsl:when>
      </xsl:choose>
    </xsl:for-each>
  </xsl:template>
   
  <xsl:template match="v:Ellipse">
    <!-- This is a somewhat limited translation.  It assumes that the
         axes are parallel to the x & y axes, and the lower-left corner
         of the bounding box is at the origin (which appears to be the
         way Visio draws them by default). -->
    <ellipse id="ellipse-{generate-id(ancestor::v:Shape[1])}"
      cx="{v:X*$userScale}" cy="{-v:Y*$userScale}"
      rx="{v:X*$userScale}" ry="{v:Y*$userScale}"/>
  </xsl:template>
   
  <!-- =  =  =  =  =  =  =  = Utility templates =  =  =  =  =  =  =  =  =  =  =  =  =  = -->
   
  <!-- Lookup color value in Colors element -->
  <xsl:template name="lookup-color">
    <xsl:param name="c_el"/>
    <xsl:choose>
      <xsl:when test="starts-with($c_el, '#')">
        <xsl:value-of select="$c_el"/>
      </xsl:when>
      <xsl:otherwise>
        <xsl:value-of select="$Colors[@IX=string($c_el)]/@RGB"/>
      </xsl:otherwise>
    </xsl:choose>
  </xsl:template>

If a Visio element has a name, use it as the shape ID; otherwise, use generate-id():

  <xsl:template name="setIdAttribute">
    <xsl:attribute name="id">
      <xsl:choose>
        <xsl:when test="@NameU">
          <xsl:value-of select="@NameU"/>
        </xsl:when>
        <xsl:otherwise>
          <xsl:value-of select="generate-id(.)"/>
        </xsl:otherwise>
      </xsl:choose>
    </xsl:attribute>
  </xsl:template>
   
  <!-- Translate XForm element into transform attribute -->
  <xsl:template name="transformAttribute">
    <xsl:attribute name="transform">
      <xsl:text>translate(</xsl:text>
      <xsl:value-of select="concat((v:PinX - v:LocPinX)*$userScale,
                            ',', -(v:PinY - v:LocPinY)*$userScale)"/>
      <xsl:if test="v:Angle != 0">
        <xsl:text>) rotate(</xsl:text>
        <xsl:value-of select="-v:Angle*180 div $pi"/>
        <xsl:value-of select="concat(',', v:LocPinX*$userScale,
                              ',', -v:LocPinY*$userScale)"/>
      </xsl:if>
      <xsl:text>)</xsl:text>
    </xsl:attribute>
  </xsl:template>

Visio Geom elements are translated to paths. Most of the mapping is straightforward, except for the handling of Non-Uniform Rational B-Splines (NURBS), which is delegated to a special set of templates in visio-nurbs.xsl, which you can peruse in the full distribution:

  <!-- Translate Geom element into path element -->
  <xsl:template name="pathElement">
    <xsl:variable name="pathID">
      <xsl:text>path-</xsl:text>
      <xsl:choose>
        <xsl:when test="ancestor::v:Shape[1]/@NameU">
          <xsl:value-of select="ancestor::v:Shape[1]/@NameU"/>
        </xsl:when>
        <xsl:otherwise>
          <xsl:value-of select="generate-id(ancestor::v:Shape[1])"/>
        </xsl:otherwise>
      </xsl:choose>
    </xsl:variable>
    <path id="{$pathID}">
      <xsl:attribute name="d">
        <xsl:for-each select="v:*">
          <xsl:choose>
            <xsl:when test="name() = 'MoveTo'">
              <xsl:value-of select="concat('M', v:X*$userScale,
                                    ',', -v:Y*$userScale, ' ')"/>
            </xsl:when>
            <xsl:when test="name() = 'LineTo'">
              <xsl:value-of select="concat('L', v:X*$userScale,
                                    ',', -v:Y*$userScale, ' ')"/>
            </xsl:when>
            <xsl:when test="name() = 'EllipticalArcTo'">
              <!-- If we don't have access to trig functions, the
                   arc will just be represented by two line segments-->
              <xsl:choose>
                <xsl:when test="function-available('math:atan2')">
                  <xsl:call-template name="ellipticalArcPath"/>
                </xsl:when>
                <xsl:otherwise>
                  <xsl:value-of select="concat('L', v:A*$userScale,
                                        ',', -v:B*$userScale,
                                        ' L', v:X*$userScale,
                                        ',', -v:Y*$userScale, ' ')"/>
                </xsl:otherwise>
              </xsl:choose>
            </xsl:when>
            <xsl:when test="(name() = 'NoFill') or (name() = 'NoLine') or
                            (name() = 'NoShow') or (name() = 'NoSnap')">
              <!-- Ignore these -->
            </xsl:when>
            <xsl:when test="name() = 'NURBSTo'">
              <xsl:call-template name="NURBSPath"/>
            </xsl:when>
            <xsl:otherwise>
              <xsl:message>
                <xsl:text>Warning: unsupported path command found:</xsl:text>
                <xsl:value-of select="name()"/>
                <xsl:text>; replacing with LineTo</xsl:text>
              </xsl:message>
              <xsl:value-of select="concat('L', v:X*$userScale,
                                    ',', -v:Y*$userScale, ' ')"/>
            </xsl:otherwise>
          </xsl:choose>
        </xsl:for-each>
      </xsl:attribute>
      <xsl:if test="v:NoFill = 1">
        <xsl:attribute name="fill"><xsl:text>none</xsl:text></xsl:attribute>
      </xsl:if>
    </path>
  </xsl:template>
   
  <!-- This template calculates the path string for an elliptical arc -->
   
  <xsl:template name="ellipticalArcPath">
   
  <!-- Figure sweep based on angle from current point -->
  <!-- to (X,Y) and (A,B) -->
   
  <!-- TODO: figure a better way to make sure the preceding
       sibling is a drawing element -->
   
    <xsl:variable name="lastX"
      select="preceding-sibling::*[1]/v:X"/>
    <xsl:variable name="lastY"
      select="preceding-sibling::*[1]/v:Y"/>
    <xsl:variable name="angle"
      select="math:atan2(v:Y - $lastY, v:X - $lastX)
              - math:atan2(v:B - $lastY, v:A - $lastX)"/>
    <xsl:variable name="sweep">
      <xsl:choose>
        <xsl:when test="$angle &gt; 0
                        and math:abs($angle) &lt; 180">
          <xsl:value-of select='0'/>
        </xsl:when>
        <xsl:when test="$angle &lt; 0
                        and math:abs($angle) &gt; 180">
          <xsl:value-of select='0'/>
        </xsl:when>
        <xsl:otherwise>
          <xsl:value-of select='1'/>
        </xsl:otherwise>
      </xsl:choose>
    </xsl:variable>
    <xsl:value-of select="concat('A',
                   (preceding-sibling::*[1]/v:X - v:X)*$userScale, ',',
                   (preceding-sibling::*[1]/v:Y - v:Y)*$userScale, ' ',
                   v:C,  ' 0,', $sweep, ' ', v:X*$userScale, ',',
                   -v:Y*$userScale, ' ')"/>
  </xsl:template>
   
</xsl:stylesheet>

Discussion

The latest version of Visio now supports SVG output. However, this recipe is still useful if you have an older Visio or you want to tweak the conversion produced by the current version. Needless to say, SVG is not powerful enough to represent every Visio construct accurately, but it can come close. Simple Visio diagrams can be translated to SVG almost exactly. More complex SVG may need some touch up in a native SVG editor. The release notes that come with the full distribution identify missing features. Figure 13-1 is a sample of the SVG generated from a Visio file and rendered almost flawlessly.

Basic SVG shapes and text converted from Visio
Figure 13-1. Basic SVG shapes and text converted from Visio

The important lesson you should learn from this example goes beyond the details of Visio VDX , SVG, or any special tricks or techniques the author uses. It is simply the fact that this complex transformation was the author’s first experience with XSLT. This says something very positive about the power of XSLT’s transformational paradigm.

See Also

The source code and some demonstrations are available on Source Forge at http://sourceforge.net/projects/vdxtosvg/.

13.2. Working with Excel XML Spreadsheets

Problem

You want to export data from Excel to XML, but not in the native format supported by Microsoft.

Solution

XSLT 1.0

If you have an Excel spreadsheet that looks like this:

Date

Price

Volume

20010817

61.88

260163

20010820

62.7

241859

20010821

60.78

233989

20010822

60.66

387444

Then the Excel (XP or 2003) XML format looks like this:

<?xml version="1.0"?>
<Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet"
 xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-
microsoft-com:office:excel" 
xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet" xmlns:html=
"http://www.w3.org/TR/REC-html40">
  <DocumentProperties xmlns="urn:schemas-microsoft-com:office:office">
    <Author>Salvatore R. Mangano</Author>
    <LastAuthor>Salvatore R. Mangano</LastAuthor>
    <Created>2002-08-18T00:43:49Z</Created>
    <LastSaved>2002-08-18T02:19:21Z</LastSaved>
    <Company>Descriptix</Company>
    <Version>10.3501</Version>
  </DocumentProperties>
  <OfficeDocumentSettings xmlns="urn:schemas-microsoft-com:office:office">
    <DownloadComponents/>
    <LocationOfComponents HRef="/"/>
  </OfficeDocumentSettings>
  <ExcelWorkbook xmlns="urn:schemas-microsoft-com:office:excel">
    <WindowHeight>9915</WindowHeight>
    <WindowWidth>10140</WindowWidth>
    <WindowTopX>240</WindowTopX>
    <WindowTopY>255</WindowTopY>
    <ProtectStructure>False</ProtectStructure>
    <ProtectWindows>False</ProtectWindows>
  </ExcelWorkbook>
  <Styles>
    <Style ss:ID="Default" ss:Name="Normal">
      <Alignment ss:Vertical="Bottom"/>
      <Borders/>
      <Font/>
      <Interior/>
      <NumberFormat/>
      <Protection/>
    </Style>
  </Styles>
  <Worksheet ss:Name="msft">
    <Table ss:ExpandedColumnCount="3" ss:ExpandedRowCount="5" x:FullColumns="1" 
    x:FullRows="1">
      <Row>
        <Cell>
          <Data ss:Type="String">Date</Data>
        </Cell>
        <Cell>
          <Data ss:Type="String">Price</Data>
        </Cell>
        <Cell>
          <Data ss:Type="String">Volume</Data>
        </Cell>
      </Row>
      <Row>
        <Cell>
          <Data ss:Type="Number">20010817</Data>
        </Cell>
        <Cell>
          <Data ss:Type="Number">61.88</Data>
        </Cell>
        <Cell>
          <Data ss:Type="Number">260163</Data>
        </Cell>
      </Row>
      <Row>
        <Cell>
          <Data ss:Type="Number">20010820</Data>
        </Cell>
        <Cell>
          <Data ss:Type="Number">62.7</Data>
        </Cell>
        <Cell>
          <Data ss:Type="Number">241859</Data>
        </Cell>
      </Row>
      <Row>
        <Cell>
          <Data ss:Type="Number">20010821</Data>
        </Cell>
        <Cell>
          <Data ss:Type="Number">60.78</Data>
        </Cell>
        <Cell>
          <Data ss:Type="Number">233989</Data>
        </Cell>
      </Row>
      <Row>
        <Cell>
          <Data ss:Type="Number">20010822</Data>
        </Cell>
        <Cell>
          <Data ss:Type="Number">60.66</Data>
        </Cell>
        <Cell>
          <Data ss:Type="Number">387444</Data>
        </Cell>
      </Row>
    </Table>
    <WorksheetOptions xmlns="urn:schemas-microsoft-com:office:excel">
      <Selected/>
      <Panes>
        <Pane>
          <Number>3</Number>
          <ActiveRow>11</ActiveRow>
          <ActiveCol>5</ActiveCol>
        </Pane>
      </Panes>
      <ProtectObjects>False</ProtectObjects>
      <ProtectScenarios>False</ProtectScenarios>
    </WorksheetOptions>
  </Worksheet>
</Workbook>

which is probably not what you had in mind!

This example conveniently maps an Excel XML file to a simpler XML file. Many spreadsheets created in Excel have a structure in which the first row contains column names and subsequent rows contain data for those columns.

One obvious mapping would convert the column names into element names and the remaining cells into element content. The only missing pieces of information are the names of the top-level element and the element containing each row. This stylesheet takes these names as parameters with some obvious defaults. It converts some of the useful metadata into comments and throws away the Excel-specific stuff. This section provides several other parameters that increase the generality of the conversion, such as which row contains the column names, where the data starts, and what to do about empty cells:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" 
                xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
                xmlns:o="urn:schemas-microsoft-com:office:office" 
                xmlns:x="urn:schemas-microsoft-com:office:excel" 
                xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet">
     
  <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
  
  <!-- The name of the top-level element -->
  <xsl:param name="topLevelName" select=" 'Table' "/>
  <!-- The name of each row -->
  <xsl:param name="rowName" select=" 'Row' "/>
  <!-- The namespace to use -->
  <xsl:param name="namespace"/>
  <!-- The namespace prefix to use -->
  <xsl:param name="namespacePrefix"/>
  <!-- The character to use if column names contain whitespace -->
  <xsl:param name="wsSub" select="'_'"/>
  <!--Determines which row contains the col names-->
  <xsl:param name="colNamesRow" select="1"/>
  <!--Determines which row the data begins -->
  <xsl:param name="dataRowStart" select="2"/>
  <!-- If false then cells with null or whitespace-only content -->
  <!-- will be skipped -->
  <xsl:param name="includeEmpty" select="true()"/>
  <!-- If false then author and creation metadata will not be put -->
  <!-- into a comment-->
  <xsl:param name="includeComment" select="true()"/>
  
  <!--Normalize the namespacePrefix -->
  <xsl:variable name="nsp">
    <xsl:if test="$namespace">
      <!-- Only use prefix if namespace is specified -->
      <xsl:choose>
        <xsl:when test="contains($namespacePrefix,':')">
          <xsl:value-of 
               select="concat(translate(substring-before(
                                            $namespacePrefix,
                                            ':'),' ',''),':')"/>
        </xsl:when>
        <xsl:when test="translate($namespacePrefix,' ','')">
          <xsl:value-of 
               select="concat(translate($namespacePrefix,' ',''),':')"/>
        </xsl:when>
        <xsl:otherwise/>
      </xsl:choose>
    </xsl:if>
  </xsl:variable>
  
  <!--Get the names of all the columns with whitespace replaced by  -->
  <xsl:variable name="COLS" select="/*/*/*/ss:Row[$colNamesRow]/ss:Cell"/>
  
  <xsl:template match="o:DocumentProperties">
    <xsl:if test="$includeComment">
      <xsl:text>&#xa;</xsl:text>
      <xsl:comment>
       <xsl:text>&#xa;</xsl:text>
        <xsl:if test="normalize-space(o:Company)">
          <xsl:text>Company: </xsl:text>
          <xsl:value-of select="o:Company"/>
          <xsl:text>&#xa;</xsl:text>
        </xsl:if>
        <xsl:text>Author: </xsl:text>
        <xsl:value-of select="o:Author"/>
        <xsl:text>&#xa;</xsl:text>
        <xsl:text>Created on: </xsl:text>
        <xsl:value-of select="translate(o:Created,'TZ',' ')"/>
        <xsl:text>&#xa;</xsl:text>
        <xsl:text>Last Author: </xsl:text>
        <xsl:value-of select="o:LastAuthor"/>
        <xsl:text>&#xa;</xsl:text>
        <xsl:text>Saved on:</xsl:text>
        <xsl:value-of select="translate(o:LastSaved,'TZ',' ')"/>
        <xsl:text>&#xa;</xsl:text>
      </xsl:comment>
    </xsl:if>
  </xsl:template>
  
  <xsl:template match="ss:Table">
    <xsl:element 
         name="{concat($nsp,translate($topLevelName,
                       '&#x20;&#x9;&#xA;',$wsSub))}" 
         namespace="{$namespace}">
      <xsl:apply-templates select="ss:Row[position() >= $dataRowStart]"/>
    </xsl:element>
  </xsl:template>
  
  <xsl:template match="ss:Row">
    <xsl:element
        name="{concat($nsp,translate($rowName,
                      '&#x20;&#x9;&#xA;',$wsSub))}" 
        namespace="{$namespace}">
      <xsl:for-each select="ss:Cell">
        <xsl:variable name="pos" select="position()"/>
   
       <!-- Get the correct column name even if there were empty -->
       <!-- cols in original spreadsheet -->     
        <xsl:variable name="colName">
          <xsl:choose>
            <xsl:when test="@ss:Index and 
                            $COLS[@ss:Index = current()/@ss:Index]">
              <xsl:value-of 
                  select="$COLS[@ss:Index = current()/@ss:Index]/ss:Data"/>
            </xsl:when>
            <xsl:when test="@ss:Index">
              <xsl:value-of 
                    select="$COLS[number(current()/@ss:Index)]/ss:Data"/>
            </xsl:when>
            <xsl:otherwise>
              <xsl:value-of select="$COLS[$pos]/ss:Data"/>
            </xsl:otherwise>
          </xsl:choose>
        </xsl:variable>
        
        <xsl:if test="$includeEmpty or 
                      translate(ss:Data,'&#x20;&#x9;&#xA;','')">
          <xsl:element
               name="{concat($nsp,translate($colName,
                                      '&#x20;&#x9;&#xA;',$wsSub))}" 
               namespace="{$namespace}">
            <xsl:value-of select="ss:Data"/>
          </xsl:element>
        </xsl:if>
        
      </xsl:for-each>
    </xsl:element>
  </xsl:template>
  
  <xsl:template match="text()"/>
  
</xsl:stylesheet>

The result of the transformation, with default parameter values, is the much more direct XML representation that follows:

<Table>
  <Row>
    <Date>20010817</Date>
    <Price>61.88</Price>
    <Volume>260163</Volume>
  </Row>
  <Row>
    <Date>20010820</Date>
    <Price>62.7</Price>
    <Volume>241859</Volume>
  </Row>
  <Row>
    <Date>20010821</Date>
    <Price>60.78</Price>
    <Volume>233989</Volume>
  </Row>
  <Row>
    <Date>20010822</Date>
    <Price>60.66</Price>
    <Volume>387444</Volume>
  </Row>
</Table>

XSLT 2.0

The main improvements of using XSLT 2.0 is the ability to introduce some helper functions to remove redundant code and the use of more succinct XPath 2.0 syntax.

<xsl:stylesheet version="2.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
 xmlns:xs="http://www.w3.org/2001/XMLSchema" 
 xmlns:fn="http://www.w3.org/2005/02/xpath-functions" 
 xmlns:o="urn:schemas-microsoft-com:office:office" 
 xmlns:x="urn:schemas-microsoft-com:office:excel" 
 xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet"
 xmlns:ckbk="http://www.oreilly.com/xsltckbk">
 
  <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
  
  <!-- The name of the top-level element -->
  <xsl:param name="topLevelName" select=" 'Table' " as="xs:string"/>
  <!-- The name of each row -->
  <xsl:param name="rowName" select=" 'Row' " as="xs:string"/>
  <!-- The namespace to use -->
  <xsl:param name="namespace" select=" '' " as="xs:string"/>
  <!-- The namespace prefix to use -->
  <xsl:param name="namespacePrefix" select=" '' " as="xs:string" />
  <!-- The character to use if column names contain whitespace -->
  <xsl:param name="wsSub" select="'_'" as="xs:string"/>
  <!--Determines which row contains the col names-->
  <xsl:param name="colNamesRow" select="1" as="xs:integer"/>
  <!--Determines which row the data begins -->
  <xsl:param name="dataRowStart" select="2" as="xs:integer"/>
  <!-- If false then cells with null or whitespace-only content -->
  <!-- will be skipped -->
  <xsl:param name="includeEmpty" select="true()" as="xs:boolean"/>
  <!-- If false then author and creation metadata will not be put -->
  <!-- into a comment-->
  <xsl:param name="includeComment" select="true()" as="xs:boolean"/>
  
  <!--Normalize the namespacePrefix -->
  <xsl:variable name="nsp" as="xs:string" 
       select="if (contains($namespacePrefix,':')) 
         then concat(translate(substring-before($namespacePrefix,':'),' ',''),':')
         else
         if (matches($namespacePrefix,'W'))
         then concat(translate($namespacePrefix,' ',''),':') 
         else '' "/>
   
  <!--Get the names of all the columns-->
  <xsl:variable name="COLS" select="/*/*/*/ss:Row[$colNamesRow]/ss:Cell"/>
  
  <xsl:template match="o:DocumentProperties">
    <xsl:if test="$includeComment">
      <xsl:text>&#xa;</xsl:text>
      <xsl:comment select="concat('&#xa;',
                                  ckbk:comment(o:Company), 
                                  ckbk:comment(o:Author),
                                  ckbk:comment(o:Created,'Created on'),
                                  ckbk:comment(o:LastAuthor,'Last Author'),
                                  ckbk:comment(o:LastSaved,'Saved on'))"/>
    </xsl:if>
    <xsl:text>&#xa;</xsl:text>
  </xsl:template>
  
  <xsl:template match="ss:Table">
    <xsl:element
        name="{ckbk:makeName($nsp,$topLevelName,$wsSub)}" 
         namespace="{$namespace}">
      <xsl:apply-templates select="ss:Row[position() ge $dataRowStart]"/>
    </xsl:element>
  </xsl:template>
  
  <xsl:template match="ss:Row">
    <xsl:element
        name="{ckbk:makeName($nsp,$rowName,$wsSub)}" 
        namespace="{$namespace}">
      <xsl:for-each select="ss:Cell">
        <xsl:variable name="pos" select="position()"/>
   
       <!-- Get the correct column name even if there were empty -->
       <!-- cols in original spreadsheet -->     
        <xsl:variable name="colName" as="xs:string" 
               select="if (@ss:Index and $COLS[@ss:Index = current()/@ss:Index]) 
                       then $COLS[@ss:Index = current()/@ss:Index]/ss:Datae
                       else
                       if (@ss:Index)
                       then $COLS[number(current()/@ss:Index)]/ss:Data
                       else $COLS[$pos]/ss:Data"/>
        
        <xsl:if test="$includeEmpty or 
                      translate(ss:Data,'&#x20;&#x9;&#xA;','')">
          <xsl:element
               name="{ckbk:makeName($nsp,$colName,$wsSub)}" 
               namespace="{$namespace}">
            <xsl:value-of select="ss:Data"/>
          </xsl:element>
        </xsl:if>
      </xsl:for-each>
    </xsl:element>
  </xsl:template>
  
  <xsl:template match="text()"/>

 <xsl:function name="ckbk:makeName" as="xs:string">
  <xsl:param name="nsp" as="xs:string"/>
  <xsl:param name="name" as="xs:string"/>
  <xsl:param name="wsSub" as="xs:string"/>
  <xsl:sequence select="concat($nsp,translate($name,
                                      '&#x20;&#x9;&#xA;',$wsSub))"/>
 </xsl:function>
 
 <xsl:function name="ckbk:comment" as="xs:string">
  <xsl:param name="elem"/>
  <xsl:sequence select="ckbk:comment($elem, local-name($elem))"/>
 </xsl:function>

 <xsl:function name="ckbk:comment" as="xs:string">
  <xsl:param name="elem"/>
  <xsl:param name="label" as="xs:string"/>
  <xsl:sequence select="if (normalize-space($elem)) 
                                       then concat($label,': ',$elem,'&#xa;')
                                       else '' "/>
 </xsl:function>
  
</xsl:stylesheet>

Discussion

I almost did not include this recipe in the book because it initially seemed trivial. However, I realized that a robust solution needs to handle many special cases, and many implementations (including my first) would miss them. For example, spreadsheets often contain empty columns used as spacers. You need to know how to handle them by looking for the @ss:Index attribute. This book’s initial version also hardcoded many of the choices this version exposes as parameters.

At least one obvious additional extension could be made to this stylesheet: the handling of multiple ss:Worksheet elements. This handling could be done by specifying the worksheet number as a parameter:

  <xsl:param name="WSNum" select="1"/>
   
  <xsl:variable name="COLS" 
         select="/*/ss:Worksheet[$WSNum]/*/ss:Row[$colNamesRow]/ss:Cell"/>
   
  <xsl:template match="ss:Workbook">
    <xsl:element name="{concat($nsp,translate($topLevelName,
                      '&#x20;&#x9;&#xA;',$wsSub))}" 
                 namespace="{$namespace}">
      <xsl:apply-templates select="ss:Worksheet[number($WSNum)]/ss:Table"/>
    </xsl:element>
  </xsl:template>

A more ambitious solution handles each Worksheet in a multiple Worksheet document as a separate element in the resulting document. This setup means that the column names can no longer be handled as a global variable:

  <xsl:template match="ss:Workbook">
    <xsl:element name="{concat($nsp,translate($topLevelName,
                      '&#x20;&#x9;&#xA;',$wsSub))}" 
                 namespace="{$namespace}">
      <xsl:choose>
        <xsl:when test="number($WSNum) > 0">
          <xsl:apply-templates 
               select="ss:Worksheet[number($WSNum)]/ss:Table">
            <xsl:with-param name="COLS" 
                 select="ss:Worksheet[number($WSNum)]
                                         /*/ss:Row[$colNamesRow]/ss:Cell"/>
          </xsl:apply-templates>
        </xsl:when>
        <xsl:otherwise>
          <xsl:for-each select="ss:Worksheet">
            <xsl:element 
                 name="{concat($nsp,translate(@ss:Name,
                        '&#x20;&#x9;&#xA;',$wsSub))}"
                 namespace="{$namespace}">
              <xsl:apply-templates select="ss:Table">
                <xsl:with-param name="COLS" 
                                select="*/ss:Row[$colNamesRow]/ss:Cell"/>
              </xsl:apply-templates>
            </xsl:element>
          </xsl:for-each>
        </xsl:otherwise>
      </xsl:choose>
    </xsl:element>
  </xsl:template>
  
  <xsl:template match="ss:Table">
    <xsl:param name="COLS"/>
      <xsl:apply-templates select="ss:Row[position() >= $dataRowStart]">
        <xsl:with-param name="COLS" select="$COLS"/>
      </xsl:apply-templates>
  </xsl:template>
  
  <xsl:template match="ss:Row">
    <xsl:param name="COLS"/>
   
     <!-- The rest is the same as original ... -->
     
  </xsl:template>

The only trouble with this solution is that it assumes that the column names have to be in the same row in each worksheet.

13.3. Generating XTM Topic Maps from UML Modelsvia XMI

Problem

You want to use your favorite XMI-enabled UML modeling tool to author XTM Topic Maps.

Solution

Readers not already familiar with Topic Maps might want to read the “Discussion” section before reviewing this solution.

Since UML was not explicitly designed to represent topic maps, this example works only if certain conventions are adopted. Most conventions revolve around the use of specific UML stereotypes . A stereotype is a UML extension mechanism that lets you associate a symbol with any UML classifier that designates that classifier as having user-defined semantics. To implement Topic Maps in UML, refer to the stereotypes listed in Table 13-2.

Table 13-2. Conventions for topic mapping in UML

Stereotype

UML context

Meaning

Topic

Class

Represents a topic. It is the default role of a class in our conventions, so this stereotype is optional.

Subject

Class

Designates that the class models a subject indicator. Designate the class as a subject when it is the target of a subjectIndentityRef. Specifically, this stereotype disambiguates the subjectIndicator that references a published subject indicator rather than a topic or a resource. See http://www.topicmaps.org/xtm/index.html#elt-subjectIdentity.

Resource

Class

Designates that the class represents a resource used as the target of a topic-occurrence association.

Base Name

Class

Designates that the class models an alternate topic base name and therefore implies a scope. The scope elements are associated with the baseName class via associations with stereotype scope.

Occurrence

Association

Designates that the association points to a resource that is an occurrence of the topic.

Scope

Association

Indicates the association that specifies the scope of the topic map characteristic. The nature of the scope depends on the stereotype of the target class (topicRef, resourceRef, or subjectIndicatorRef).

Variant

Generalization Association and Class

Connects to the topic of which it is a variant via a generalization association with stereotype variant. The parameters controlling the variant’s applicability are encoded into the class’s attributes. The class itself is also given the stereotype, variant to distinguish it from a topic.

In addition to these stereotypes, the following UML-to-Topic Map mappings are used:

UML class

Models Topic Map topic, unless a non-topic stereotype is present. The class name is used as the topic base name. If the class has an attribute called ID, its default value is used as the topic ID. If no such ID is present, then the class name is also used as the topic ID.

UML association

Models a Topic Map association unless a stereotype specifies otherwise. The UML association name is the Topic Map association name. The UML role specifiers are the Topic Map role specifiers.

UML instantiates association

Models the Topic Map instanceOf relationship.

Unadorned UML generalization (inheritance) association

Used as a shortcut to specify the canonical superclass-subclass association where the ends are assigned the roles superclass and subclass automatically. This same relationship links classes with the baseName stereotype to the topic classes for which they represent an alternate scoped name. Generalization with the stereotype variant also specifies topic variants.

Tip

If you use Rational Rose and it cannot save models as XMI, you should download the XMI or RoseXMI add-in at http://www.rational.com/support/downloadcenter/addins/rose/index.jsp. I only used Rose to test this example, but other UML tools such as TogetherSoft’s Together Control Center also support XMI, and this example will probably work with these tools as well.

The stylesheet that transforms XMI to XTM is shown here. Use several entities to make the long element names of XMI manageable:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE xslt [
  <!--=  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =-->
  <!-- XMI's high-level organization constructs    -->
  <!--=  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =-->
  <!ENTITY FC "Foundation.Core">
  <!ENTITY FX "Foundation.Extension_Mechanisms">
  <!ENTITY FD "Foundation.Data_Types">
  <!ENTITY MM "Model_Management.Model">
  <!ENTITY ME "&FC;.ModelElement">
   
  <!--=  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =-->
  <!-- Abbreviations for the basic elements of a XMI -->
  <!-- file that are of most interest to this        -->
  <!-- stylesheet.                                   -->
  <!--=  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =-->
  <!--Some generic kind of UML element -->
  <!ENTITY ELEM "&FC;.Namespace.ownedElement">
  <!--The association as a whole -->
  <!ENTITY ASSOC "&FC;.Association">
  <!--The connection part of the association-->
  <!ENTITY CONN "&ASSOC;.connection">
  <!--The ends of an association. -->
  <!ENTITY END "&ASSOC;End">
  <!ENTITY CONNEND "&CONN;/&END;">
  <!ENTITY ENDTYPE "&END;.type">
  <!-- A UML class -->
  <!ENTITY CLASS "&FC;.Class">
  <!--The name of some UML entity -->
  <!ENTITY NAME "&FC;.ModelElement.name">
  <!--A UML sterotype -->
  <!ENTITY STEREOTYPE "&ME;.stereotype/&FX;.Stereotype">
  <!--The place where UML documentation is stored in XMI. -->
  <!-- We use for resource data -->
  <!ENTITY TAGGEDVALUE 
     "&ME;.taggedValue/&FX;.TaggedValue/&FX;.TaggedValue.value">
  <!-- A Supertype relation (inheritance) -->
  <!ENTITY SUPERTYPE "&FC;.Generalization.supertype">
  <!ENTITY SUBTYPE "&FC;.Generalization.subtype">
  <!ENTITY SUPPLIER "&FC;.Dependency.supplier">
  <!ENTITY CLIENT "&FC;.Dependency.client">
  <!ENTITY DEPENDENCY 
      "/XMI/XMI.content/&MM;/&ELEM;/&FC;.Dependency">
  <!ENTITY EXPRBODY 
       "&FC;.Attribute.initialValue/&FD;.Expression/&FD;.Expression.body">
  <!ENTITY ATTR "&CLASS;ifier.feature/&FC;.Attribute">
  <!--Used for pointing at standard XTM PSIs -->
  <!ENTITY TM.ORG "http://www.topicmaps.org/xtm/index.html">
]>
   
<xsl:stylesheet version="1.0"  
                xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
                xmlns:xtm="http://www.topicmaps.org/xtm/1.0" 
                xmlns:xlink="http://www.w3.org/1999/xlink">
   
 <xsl:param name="termOnErr" select="true()"/>
 
  <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>

Use keys to simplify the identification of stereotypes and traverse UML associations, which use cross references between xmi.id and xmi.idref attributes:

  <!--Index classes by their name -->
  <xsl:key name="classKey" match="&CLASS;" use="@xmi.id"/>
  <!-- Index stereotypes by both name and xmi.id -->
  <xsl:key name="stereotypeKey" 
                 match="&FX;.Stereotype" use="@xmi.id"/>
  <xsl:key name="stereotypeKey"
                 match="&FX;.Stereotype" use="&NAME;"/>
  
  <!-- The xmi ids of stereotypes used to encode topic maps in UML  -->
  <!-- We use these as an efficient means for checking if a sterotype-->
  <!--  is attached to an element.                                   -->
   
  <xsl:variable name="OCCURRENCE_ID" 
                select="key('stereotypeKey','occurrence')/@xmi.id"/>
  <xsl:variable name="RESOURCE_ID"
                select="key('stereotypeKey','resource')/@xmi.id"/>
  <xsl:variable name="TOPIC_ID"
                select="key('stereotypeKey','topic')/@xmi.id"/>
  <xsl:variable name="SUBJECT_ID"
                select="key('stereotypeKey','subject')/@xmi.id"/>
  <xsl:variable name="BASENAME_ID"
                select="key('stereotypeKey','baseName')/@xmi.id"/>
  <xsl:variable name="SCOPE_ID"
                select="key('stereotypeKey','scope')/@xmi.id"/>
  <xsl:variable name="VARIANT_ID"
                select="key('stereotypeKey','variant')/@xmi.id"/>

You can convert a XMI UML model to a topic map in two passes. First, import topics from classes, and then import XTM associations from UML associations:

  <xsl:template match="/">
    <xtm:topicMap>
      <xsl:apply-templates mode="topics"/>
      <xsl:apply-templates mode="associations"/>
    </xtm:topicMap>
  </xsl:template>

The only classes that should be translated into topic are the ones without a stereotype or with a topic stereotype. The other classes in the model are representations to which a topic can refer, such as subject indicators and resources:

  <!--=  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =-->
  <!-- UML Classes to TOPICS Translation  -->
  <!--=  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =-->
  
  <xsl:template match="&ELEM;/&CLASS;" mode="topics">
    <!-- Topics are modeled as classes whose    -->
    <!-- stereotype is either empty or 'topic'  -->
    <xsl:if test="not(&STEREOTYPE;/@xmi.idref) or 
                        &STEREOTYPE;/@xmi.idref = $TOPIC_ID">
      <xsl:variable name="topicId">
        <xsl:call-template name="getTopicId">
          <xsl:with-param name="class" select="."/>
          <xsl:with-param name="prefix" select="''"/>
        </xsl:call-template>
      </xsl:variable>
      <xtm:topic id="{$topicId}">
        <!--This for-each is solely to change context to the optional -->
        <!-- Core.Attribute attribute named 'subjectIdentityid' -->
        <xsl:for-each select="&ATTR;[&NAME; = 'subjectIdentity']">
          <xtm:subjectIdentity>
            <xtm:subjectIndicatorRef xlink:href="{&EXPRBODY;}"/>
          </xtm:subjectIdentity>
        </xsl:for-each>
        <xtm:baseName>
          <xtm:baseNameString>
            <xsl:value-of select="&NAME;"/>
          </xtm:baseNameString>
        </xtm:baseName>
        <xsl:apply-templates select="." mode="getAlternateBaseNames"/>
        <xsl:apply-templates select="." mode="getVariants"/>
        <xsl:apply-templates select="." mode="getInstanceOf">
          <xsl:with-param name="classId" select="@xmi.id"/>
        </xsl:apply-templates>
        <xsl:apply-templates select="." mode="getOccurrences"/>
      </xtm:topic>
    </xsl:if>
  </xsl:template>
  
  <!-- Return the topic id of a topic class which is its id -->
  <!-- attribute value or its name -->
  <xsl:template name="getTopicId">
    <xsl:param name="class"/>
    <xsl:param name="prefix" select="'#'"/>
    <xsl:for-each select="$class">
      <xsl:choose>
        <xsl:when test="&ATTR;/&NAME; = 'id' ">
          <xsl:value-of select="&ATTR;[&NAME; = 'id']/&EXPRBODY;"/>
        </xsl:when>
        <xsl:otherwise>
          <xsl:value-of select="concat($prefix,&NAME;)"/>
        </xsl:otherwise>
      </xsl:choose>
    </xsl:for-each>
  </xsl:template>
  
  <!-- Return the subject identity of a subject class which -->
  <!-- is its subjectIdentity attribute value or its name -->
  <xsl:template name="getSubjectIdentity">
    <xsl:param name="class"/>
    <xsl:for-each select="$class">
      <xsl:choose>
        <xsl:when test="&ATTR;/&NAME; = 'subjectIdentity' ">
          <xsl:value-of select="&ATTR;[&NAME; =
                                'subjectIdentity']&EXPRBODY;"/>
        </xsl:when>
        <xsl:otherwise>
          <xsl:value-of select="concat('#',&NAME;)"/>
        </xsl:otherwise>
      </xsl:choose>
    </xsl:for-each>
  </xsl:template>
   
  <!-- Return the resource identity of a resource class which -->
  <!-- is either its resourceName attribute or its name -->
  <xsl:template name="getResourceIdentity">
    <xsl:param name="class"/>
    <xsl:for-each select="$class">
      <xsl:choose>
        <xsl:when test="&ATTR;/&NAME; = 'resourceName' ">
          <xsl:value-of select="&ATTR;[&NAME; =
                                 'resourceName']/&EXPRBODY;"/>
        </xsl:when>
        <xsl:otherwise>
          <xsl:value-of select="concat('#',&NAME;)"/>
        </xsl:otherwise>
      </xsl:choose>
    </xsl:for-each>
  </xsl:template>

You can model alternate base names and variants as specializations of the base topic class through the UML generalization association. Depending on your point of view, this may seem natural or an abuse of the concept. Nevertheless, it is effective and allows a visual cue in the UML diagram, rather than relying solely on stereotype tags:

<!-- Alternate base names are found by traversing UML -->
  <!-- generalization relationships and looking for baseName -->
  <!-- stereotypes -->
  
  <xsl:template match="&ELEM;/&CLASS;" mode="getAlternateBaseNames">
    <xsl:variable name="xmiId" select="@xmi.id"/>
    <xsl:for-each select="../&FC;.Generalization
                          [&SUPERTYPE;/&CLASS;/@xmi.idref = $xmiId]">
      <xsl:variable name="subtypeXmiId"
                    select="&FC;.Generalization.subtype/&CLASS;/@xmi.idref"/>
      <xsl:variable name="class" select="key('classKey',$subtypeXmiId)"/>
      <xsl:if test="$class/&STEREOTYPE;/@xmi.idref = $BASENAME_ID">
        <xsl:variable name="name" select="$class/&NAME;"/>
        <xtm:baseName>
          <xsl:call-template name="getScope">
            <xsl:with-param name="class" select="$class"/>
          </xsl:call-template>
          <xtm:baseNameString>
            <xsl:value-of select="substring-after($name,'::')"/>
          </xtm:baseNameString>
        </xtm:baseName>
      </xsl:if>
    </xsl:for-each>
  </xsl:template>
   
  <!-- Variants are found by traversing UML -->
  <!-- generalization relationships and looking for baseName -->
  <!-- stereotypes -->
   
  <xsl:template match="&ELEM;/&CLASS;" mode="getVariants">
    <xsl:variable name="xmiId" select="@xmi.id"/>
    <xsl:for-each select="../&FC;.Generalization
                           [&SUPERTYPE;/&CLASS;/@xmi.idref = $xmiId]">
      <xsl:variable name="subtypeXmiId"
             select="&FC;.Generalization.subtype/&CLASS;/@xmi.idref"/>
      <xsl:variable name="variantClass"
                    select="key('classKey',$subtypeXmiId)"/>
      <xsl:if test="$variantClass/&STEREOTYPE;/@xmi.idref = $VARIANT_ID">
        <xsl:variable name="name" select="$variantClass/&NAME;"/>
        <xtm:variant>
          <xtm:variantName>
            <xsl:call-template name="resourceRep">
              <xsl:with-param name="class" select="$variantClass"/>
            </xsl:call-template>
          </xtm:variantName>
          <xtm:parameters>
            <xsl:call-template name="getVariantParams">
              <xsl:with-param name="class" select="$variantClass"/>
            </xsl:call-template>
          </xtm:parameters>
          <!-- Change context to this variant to get nested variants, -->
          <!-- if any. -->
          <xsl:apply-templates select="$variantClass" mode="getVariants"/>
        </xtm:variant>
      </xsl:if>
    </xsl:for-each>
  </xsl:template>
   
  <!-- Gets a variant's parameters from   -->
  <!-- the attributes of the variant class -->
  <xsl:template name="getVariantParams">
    <xsl:param name="class"/>
    <xsl:if test="not($class/&ATTR;)">
      <xsl:message terminate="{$termOnErr}">
      A variant must have at least one parameter.
      </xsl:message>
    </xsl:if>
    <xsl:for-each select="$class/&ATTR;">
      <!-- A parameter is either modeled as a subject indicator  --> 
      <!-- or topic ref                                         -->
      <xsl:choose>
        <xsl:when test="&STEREOTYPE;/@xmi.idref = $SUBJECT_ID">
            <xtm:subjectIndicatorRef xlink:href="{&EXPRBODY;}"/>
        </xsl:when>
        <xsl:otherwise>
            <xtm:topicRef  xlink:href="{&EXPRBODY;}"/>
        </xsl:otherwise>
      </xsl:choose>
    </xsl:for-each>
  </xsl:template>

Topic Map occurrences are modeled as associations to classes containing resource references or data. Since inline resource data can be too large to fit nicely as an attribute value, this example allows the attribute description to be used as an alternate container of resource data:

<!-- Topic map occurrences are modeled as associations to -->
  <!-- classes containing resource references or data -->
  <xsl:template match="&ELEM;/&CLASS;" mode="getOccurrences">
    <xsl:variable name="xmiId" select="@xmi.id"/>
    <!--Search over the associations this class participates-->
    <xsl:for-each 
            select="../&ASSOC;
                        [&CONN;/*/&ENDTYPE;/&CLASS;/@xmi.idref = $xmiId]">
       <!-- Test for the presence of the occurrence stereotype -->                     
      <xsl:if test="&STEREOTYPE;/@xmi.idref = $OCCURRENCE_ID">
        <!--Get the id of the resource by looking at the other end -->
        <!-- of the occurrence association -->
        <xsl:variable name="resourceId" 
                      select="&CONN;/*/&ENDTYPE;/&CLASS;
                                [@xmi.idref != $xmiId]/@xmi.idref"/>
        <!-- Get the class representing the resource -->
        <xsl:variable name="resourceClass" 
                      select="key('classKey',$resourceId)"/>
        <xtm:occurrence>
          <xsl:apply-templates select="." mode="getInstanceOf">
            <xsl:with-param name="classId" select="$resourceId"/>
          </xsl:apply-templates>
          <!--TODO: Can't model this yet!
            <xsl:call-template name="getScope">
              <xsl:with-param name="class"/>
            </xsl:call-template>
          -->
          <!-- We either have a resource ref or resource data. -->
          <!-- If the class has a resourceData attribute it   -->
          <!-- is the later.                                   -->
          <xsl:call-template name="resourceRep">
            <xsl:with-param name="class" select="$resourceClass"/>
          </xsl:call-template>
        </xtm:occurrence>
      </xsl:if>
    </xsl:for-each>
  </xsl:template>
   
  <!-- This template determines how the resource is represented -->
  <xsl:template name="resourceRep">
      <xsl:param name="class" />
      <xsl:variable name="resourceData">
        <!--for-each to change context -->
        <xsl:for-each select="$class/&ATTR;[&NAME; = 'resourceData']">
          <xsl:choose>
            <!--The resource data was encoded in the UML attr -->
            <!--documentation                                 --> 
            <xsl:when test="&TAGGEDVALUE;">
              <xsl:value-of select="&TAGGEDVALUE;"/>
            </xsl:when>
            <!--The resource data was encoded in the UML attr value -->
            <xsl:otherwise>
              <xsl:value-of select="&EXPRBODY;"/>
            </xsl:otherwise>
          </xsl:choose>
        </xsl:for-each>
      </xsl:variable>
      <!-- If we found some resource data then use it. -->
      <!-- Otherwise assume the user meant this to be a reference -->
      <xsl:choose>
        <xsl:when test="string($resourceData)">
          <xtm:resourceData>
            <xsl:value-of select="$resourceData"/>
          </xtm:resourceData>
        </xsl:when>
        <xsl:otherwise>
          <xsl:variable name="resource">
            <xsl:call-template name="getResourceIdentity">
              <xsl:with-param name="class" select="$class"/>
            </xsl:call-template>
          </xsl:variable>
          <xtm:resourceRef xlink:href="{$resource}"/>
        </xsl:otherwise>
      </xsl:choose>
  </xsl:template>

XTM instanceOf relationships are modeled as UML dependency associations, also called instantiates. This representation of instanceOf is quite natural:

  <!-- This template finds if a topic class has any instanceOf -->
  <!-- associations. -->
  <xsl:template match="&ELEM;/&CLASS;" mode="getInstanceOf">
    <xsl:param name="classId"/>
    <!-- We loop of dependency relations and determine  -->
    <!-- how the instance is represented-->
    <xsl:for-each 
            select="&DEPENDENCY;[&CLIENT;/&CLASS;/@xmi.idref = $classId]">
      <xtm:instanceOf>
        <xsl:variable name="instanceClass"
             select="key('classKey',&SUPPLIER;/&CLASS;/@xmi.idref)"/>
        <!-- Figure out if instance is modeled as a subject or a topic -->
        <xsl:variable name="sterotypeId"
                      select="$instanceClass/&STEREOTYPE;/@xmi.idref"/>
        <xsl:choose>
          <!-- This is the case of a subject indicator -->
          <xsl:when test="$sterotypeId = $SUBJECT_ID">
            <xsl:variable name="subjectIdentity">
              <xsl:call-template name="getSubjectIdentity">
                <xsl:with-param name="class" select="$instanceClass"/>
              </xsl:call-template>
            </xsl:variable>
            <xsl:if test="not(normalize-space($subjectIdentity))">
              <xsl:message terminate="{$termOnErr}">
              Subject with no identity!
              </xsl:message>
            </xsl:if>
            <xtm:subjectIndicatorRef xlink:href="{$subjectIdentity}"/>
          </xsl:when>
          <!-- Otherwise the instance is represented by a topic -->
          <xsl:when test="not($sterotypeId) or $sterotypeId = $TOPIC_ID">
            <xsl:variable name="topicId">
              <xsl:call-template name="getTopicId">
                <xsl:with-param name="class" select="$instanceClass"/>
              </xsl:call-template>
            </xsl:variable>
            <xsl:if test="not(normalize-space($topicId))">
              <xsl:message terminate="{$termOnErr}">
              Topic with no id!
              </xsl:message>
            </xsl:if>

            <topicRef xlink:href="{$topicId}"/>
          </xsl:when>
          <xsl:otherwise>
            <xsl:message terminate="{$termOnErr}">
              <xsl:text>instanceOf must point to a topic or a subject. </xsl:text>
              <xsl:value-of select="$instanceClass/&NAME;"/>
              <xsl:text> is a </xsl:text>
              <xsl:value-of
                   select="key('stereotypeKey',$sterotypeId)/&NAME;"/>
              <xsl:text>.&#xa;</xsl:text>
            </xsl:message>
          </xsl:otherwise>
        </xsl:choose>
      </xtm:instanceOf>
    </xsl:for-each>
  </xsl:template>
  
  <xsl:template name="getScope">
    <xsl:param name="class"/>
    <xsl:variable name="classesAssociations"
                  select="/*/XMI.content/*/&ELEM;
                          /&ASSOC;
                          [&CONN;/*/
                          &FC;.AssociationEnd.type/
                          &CLASS;/@xmi.idref = $class/@xmi.id]"/>
    <xsl:variable name="scopeAssociations" 
                  select="$classesAssociations[
                          &FC;.ModelElement.stereotype/
                          &FX;.Stereotype/
                          @xmi.idref = $SCOPE_ID]"/>
    <xsl:if test="$scopeAssociations">
      <xtm:scope>
        <xsl:for-each select="$scopeAssociations">
          <xsl:variable name="targetClassId"
               select="&CONN;/*/&ENDTYPE;/&CLASS;
                              [@xmi.idref != $class/@xmi.id]/@xmi.idref"/>
          <xsl:variable name="targetClass" 
                        select="key('classKey',$targetClassId)"/>
          <xsl:call-template name="getScopeRef">
            <xsl:with-param name="class" select="$targetClass"/>
          </xsl:call-template>
        </xsl:for-each>
      </xtm:scope>
    </xsl:if>
  </xsl:template>
  
  <xsl:template name="getScopeRef">
    <xsl:param name="class"/>
    <xsl:variable name="stereotypeId" 
                  select="$class/&FC;.ModelElement.stereotype/
                          &FX;.Stereotype/
                          @xmi.idref"/>
    <xsl:choose>
      <xsl:when test="not($stereotypeId) or $stereotypeId = $TOPIC_ID">
        <xsl:variable name="topidId">
          <xsl:call-template name="getTopicId">
            <xsl:with-param name="class" select="$class"/>
          </xsl:call-template>
        </xsl:variable>
        <xtm:topicRef xlink:href="{$topidId}"/>
      </xsl:when>
      <xsl:when test="$stereotypeId = $SUBJECT_ID">
        <xsl:variable name="subjectId">
          <xsl:call-template name="getSubjectIdentity">
            <xsl:with-param name="class" select="$class"/>
          </xsl:call-template>
        </xsl:variable>
        <xtm:subjectIndicatorRef xlink:href="{$subjectId}"/>
      </xsl:when>
      <xsl:when test="$stereotypeId = $RESOURCE_ID">
        <xsl:variable name="resourceId">
          <xsl:call-template name="getResourceIdentity">
            <xsl:with-param name="class" select="$class"/>
          </xsl:call-template>
        </xsl:variable>
        <xtm:resourceRef xlink:href="{$resourceId}"/>
      </xsl:when>
      <xsl:otherwise>
        <xsl:message terminate="{$termOnErr}">
        A Scope must be either a topicRef, subjectRef, or resourceRef!
        </xsl:message>
      </xsl:otherwise>
    </xsl:choose>
  </xsl:template>
  
  <xsl:template match="text()" mode="topics"/>
  
  <!--=  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =-->
  <!-- UML ASSOCIATION TO TOPIC ASSOCIATIONS -->
  <!--=  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =-->
  <xsl:template match="&ASSOC;" mode="associations">
    <!-- Only named UML associations are topic map associations -->
    <xsl:if test="normalize-space(&NAME;)">
      <xtm:association id="{&NAME;}">
        <xtm:instanceOf>
          <topicRef
            xlink:href="{key('stereotypeKey',
                             &STEREOTYPE;/@xmi.idref)/&NAME;}"/>
        </xtm:instanceOf>
        <xsl:for-each select="&CONNEND;">
          <xtm:member>
            <xtm:roleSpec>
              <xtm:topicRef xlink:href="{&NAME;}"/>
            </xtm:roleSpec>
            <xsl:variable name="topicId">
              <xsl:call-template name="getTopicId">
                <xsl:with-param name="class"
                        select="key('classKey',
                                    &ENDTYPE;/&CLASS;/@xmi.idref)"/>
              </xsl:call-template>
            </xsl:variable>
            <xtm:topicRef xlink:href="{$topicId}"/>
          </xtm:member>
        </xsl:for-each>
      </xtm:association>
    </xsl:if>
  </xsl:template>
   
  <xsl:template match="&ELEM;/&FC;.Generalization"
                mode="associations">
   
    <xsl:variable name="subClassId"
                  select="&SUBTYPE;/&CLASS;/@xmi.idref"/>
    <xsl:variable name="subClass"
                  select="key('classKey',$subClassId)"/>
    <xsl:variable name="superClassId"
                  select="&SUPERTYPE;/&CLASS;/@xmi.idref"/>
    <xsl:variable name="superClass"
                  select="key('classKey',$superClassId)"/>
    
    <!-- If a generalization relation exists from a topic to a -->
    <!-- topic, we use this as an indication of a canonical     -->    
    <!-- superclass-subclass relation, Ideally we would use an --> 
    <!-- absence of a stereotype on the generalization, but the -->
    <!-- version of XMI I am using is not storing stereotype   -->
    <!-- info for generalizations                              -->
    <xsl:if test="(not($subClass/&STEREOTYPE;/@xmi.idref) or 
                        $subClass/&STEREOTYPE;/@xmi.idref = $TOPIC_ID) and
                        (not($superClass/&STEREOTYPE;/@xmi.idref) or 
                        $superClass/&STEREOTYPE;/@xmi.idref = $TOPIC_ID)">
                        
      <xtm:association>
        <xsl:variable name="id">
          <xsl:choose>
            <xsl:when test="normalize-space(&NAME;)">
              <xsl:value-of select="&NAME;"/>
            </xsl:when>
            <xsl:otherwise>
              <xsl:value-of select="@xmi.id"/>
            </xsl:otherwise>
          </xsl:choose>
        </xsl:variable>
        
        <xsl:attribute name="id">
          <xsl:value-of select="$id"/>
        </xsl:attribute>
        
        <xtm:instanceOf>
          <subjectIndicatorRef 
             xlink:href="&TM.ORG;#psi-superclass-subclass"/>
        </xtm:instanceOf>
        
        <xtm:member>
        
          <xtm:roleSpec>
            <xtm:subjectIndicatorRef 
                                xlink:href="&TM.ORG;#psi-superclass"/>
          </xtm:roleSpec>
          
          <xsl:variable name="superClassTopicId">
            <xsl:call-template name="getTopicId">
              <xsl:with-param name="class" select="$superClass"/>
            </xsl:call-template>
          </xsl:variable>
          <xtm:topicRef xlink:href="{$superClassTopicId}"/>
          
        </xtm:member>
        
        <xtm:member>
          <xtm:roleSpec>
            <xtm:subjectIndicatorRef xlink:href="&TM.ORG;#psi-subclass"/>
          </xtm:roleSpec>
          
          <xsl:variable name="subClassTopicId">
            <xsl:call-template name="getTopicId">
              <xsl:with-param name="class" select="$subClass"/>
            </xsl:call-template>
          </xsl:variable>
   
          <xtm:topicRef xlink:href="{$subClassTopicId}"/>
        </xtm:member>
        
      </xtm:association>
    </xsl:if>
  </xsl:template>
  
  <xsl:template match="text()" mode="associations"/>
  
</xsl:stylesheet>

These templates are part of the second pass in which UML associations not already handled due to special stereotypes are converted into topic map associations. Here, the stereotype of an association is the topicRef that determines what kind of association is modeled. The stereotype entry was abused in this way largely because UML provided no other natural home for this information. Scoped associations are a problem I chose to ignore. In all other respects, a UML association matches the topic map concept well:

  <!--=  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =-->
  <!-- UML ASSOCIATION TO TOPIC ASSOCIATIONS -->
  <!--=  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =  =-->
  <xsl:template match="&ASSOC;" mode="associations">
    <!-- Only named UML associations are topic map associations -->
    <xsl:if test="normalize-space(&NAME;)">
      <xtm:association id="{&NAME;}">
        <xtm:instanceOf>
          <topicRef
            xlink:href="{key('stereotypeKey',
                             &STEREOTYPE;/@xmi.idref)/&NAME;}"/>
        </xtm:instanceOf>
        <xsl:for-each select="&CONNEND;">
          <xtm:member>
            <xtm:roleSpec>
              <xtm:topicRef xlink:href="{&NAME;}"/>
            </xtm:roleSpec>
            <xsl:variable name="topicId">
              <xsl:call-template name="getTopicId">
                <xsl:with-param name="class"
                        select="key('classKey',
                                    &ENDTYPE;/&CLASS;/@xmi.idref)"/>
              </xsl:call-template>
            </xsl:variable>
            <xtm:topicRef xlink:href="{$topicId}"/>
          </xtm:member>
        </xsl:for-each>
      </xtm:association>
    </xsl:if>
  </xsl:template>
   
  <xsl:template match="&ELEM;/&FC;.Generalization"
                mode="associations">
   
    <xsl:variable name="subClassId"
                  select="&SUBTYPE;/&CLASS;/@xmi.idref"/>
    <xsl:variable name="subClass"
                  select="key('classKey',$subClassId)"/>
    <xsl:variable name="superClassId"
                  select="&SUPERTYPE;/&CLASS;/@xmi.idref"/>
    <xsl:variable name="superClass"
                  select="key('classKey',$superClassId)"/>

If a generalization relation exists from topic to topic, use it as an indication of a canonical superclass-subclass relation. The XTM specification provides explicit support for this important relationship via published subject indicators (PSI). Ideally, you would use an absence of a stereotype on the generalization, but the version of XMI I use does not store stereotype information for generalizations:

    <xsl:if test="(not($subClass/&STEREOTYPE;/@xmi.idref) or 
                   $subClass/&STEREOTYPE;/@xmi.idref = $TOPIC_ID) and
                        (not($superClass/&STEREOTYPE;/@xmi.idref) or 
                        $superClass/&STEREOTYPE;/@xmi.idref = $TOPIC_ID)">
                        
      <xtm:association>
        <xsl:variable name="id">
          <xsl:choose>
            <xsl:when test="normalize-space(&NAME;)">
              <xsl:value-of select="&NAME;"/>
            </xsl:when>
            <xsl:otherwise>
              <xsl:value-of select="@xmi.id"/>
            </xsl:otherwise>
          </xsl:choose>
        </xsl:variable>
        
        <xsl:attribute name="id">
          <xsl:value-of select="$id"/>
        </xsl:attribute>
        
        <xtm:instanceOf>
          <subjectIndicatorRef 
             xlink:href="&TM.ORG;#psi-superclass-subclass"/>
        </xtm:instanceOf>
        
        <xtm:member>
        
          <xtm:roleSpec>
            <xtm:subjectIndicatorRef xlink:href="&TM.ORG;#psi-superclass"/>
          </xtm:roleSpec>
          
          <xsl:variable name="superClassTopicId">
            <xsl:call-template name="getTopicId">
              <xsl:with-param name="class" select="$superClass"/>
            </xsl:call-template>
          </xsl:variable>
          <xtm:topicRef xlink:href="{$superClassTopicId}"/>
          
        </xtm:member>
        
        <xtm:member>
          <xtm:roleSpec>
            <xtm:subjectIndicatorRef xlink:href="&TM.ORG;#psi-subclass"/>
          </xtm:roleSpec>
          
          <xsl:variable name="subClassTopicId">
            <xsl:call-template name="getTopicId">
              <xsl:with-param name="class" select="$subClass"/>
            </xsl:call-template>
          </xsl:variable>
   
          <xtm:topicRef xlink:href="{$subClassTopicId}"/>
        </xtm:member>
        
      </xtm:association>
    </xsl:if>
  </xsl:template>
  
  <xsl:template match="text()" mode="associations"/>
  
</xsl:stylesheet>

Discussion

Topic Maps represent knowledge about real-world subjects. These techniques enable computers and people to find relevant information faster and with greater precision. Topic Maps were first discussed in 1993, when the ideas were first expressed as a Davenport Group working document.[2] The paradigm was extended in the context of the GCA Research Institute (now called the IDEAlliance) in relation to applications of HyTime (http://www.hytime.org/papers/htguide.html). The XTM specification was an offshoot of this work, which was organized under the control of an independent organization called TopicMaps.org.

A topic is an electronic proxy to a real-world subject. Ozzy Osborne is a real-world subject; however, since you can’t store the real Ozzy in a computer, you create an Ozzy topic as a surrogate. Topics have names called base names. A topic can have one universal name (properly called unconstrained ) and several other names that are specific to a scope. A scope is a context in which a topic map characteristic is valid. In the case of a topic name, a scope can indicate that the topic Ozzy Osborne is also John Michael Osbourne in the legal scope.[3]

A topic can point to an occurrence , which is a resource that supplies information relevant to a topic. A resource might refer to an addressable content (resourceRef) or the content itself (resourceData).

A topic can participate in a special association called instanceOf that declares this topic to be a specific instance of a more general class of objects. The class can be designated by a reference to another topic or to a subject indicator. Subject indicators are an interesting topic map feature. They facilitate a method that specifies the nature of a subject by associating it with a standard published address, such as one maintained by a government standards body.

Topics can be related by associations. An association is a named relationship between two or more topics in which each topic plays a specified role in the association.

Several other interesting topic map facilities model knowledge about subjects. Readers interested in topic maps are encouraged to read the specification of XTM at http://www.TopicMaps.org. Compared to other specifications, this one is especially friendly to the uninitiated. Almost all of XTM’s functionality is mapped on to some construct in UML (as explained in the “Solution” section). Dealing with the Topic Map notion of scope is this mapping’s main difficulty. In the topic map paradigm, a scope is a method specifying that a topic characteristic is only valid in a particular setting. Scope applies to base names, associations, and occurrences. Although these conventions can deal with scope for base names, they cannot currently handle scopes for associations and occurrences. This is because these conventions are modeled as UML associations, and in UML an association is not normally context sensitive. You can model this feature via UML constraints. Alas, the version of XMI that is available for Rational Rose does not capture information on constraints. In practice, scope is an advanced topic map function that many users will not need, so this problem might not be a major liability, especially for novice topic mappers.

Figure 13-2 is an example of a topic map represented in UML. This topic models information about a tomato in the context of a meal. Although the example is somewhat whimsical, I used it because Sam Hunting used the same example in XML Topic Maps: Creating and Using Topic Maps for the Web (Addison Wesley, 2002). In this example, he exercised many of the topic map facilities, which allows you to check the resulting XMI’s accuracy.

UML diagram of a tomato topic map
Figure 13-2. UML diagram of a tomato topic map

A portion of the resulting XTM file follows:

<xtm:topicMap xmlns:xtm="http://www.topicmaps.org/xtm/1.0" xmlns:xlink="http://www.
w3.org/1999/xlink">
   <xtm:topic id="EN">
      <xtm:subjectIdentity>
         <xtm:subjectIndicatorRef 
             xlink:href="http://www.topicmaps.org/xtm/1.0/language.xtm#en"/>
      </xtm:subjectIdentity>
      <xtm:baseName>
         <xtm:baseNameString>EN</xtm:baseNameString>
      </xtm:baseName>
   </xtm:topic>
   <xtm:topic id="FR">
      <xtm:subjectIdentity>
         <xtm:subjectIndicatorRef 
             xlink:href="http://www.topicmaps.org/xtm/1.0/language.xtm#fr"/>
      </xtm:subjectIdentity>
      <xtm:baseName>
         <xtm:baseNameString>FR</xtm:baseNameString>
      </xtm:baseName>
   </xtm:topic>
   <xtm:topic id="myTomato">
      <xtm:subjectIdentity>
         <xtm:subjectIndicatorRef 
             xlink:href="http://www.fed.gov/usda/doc/tomato.htm#gradeA"/>
      </xtm:subjectIdentity>
      <xtm:baseName>
         <xtm:baseNameString>tomato</xtm:baseNameString>
      </xtm:baseName>
      <xtm:baseName>
         <xtm:scope>
            <xtm:topicRef xlink:href="#EN"/>
         </xtm:scope>
         <xtm:baseNameString>tomato</xtm:baseNameString>
      </xtm:baseName>
      <xtm:baseName>
         <xtm:scope>
            <xtm:topicRef xlink:href="#FR"/>
         </xtm:scope>
         <xtm:baseNameString>tomato</xtm:baseNameString>
      </xtm:baseName>
      <xtm:variant>
         <xtm:variantName>
            <xtm:resourceData>TMT</xtm:resourceData>
         </xtm:variantName>
         <xtm:parameters>
            <xtm:topicRef xlink:href="cell_phone"/>
            <xtm:topicRef xlink:href="TMT"/>
         </xtm:parameters>
      </xtm:variant>
      <xtm:occurrence>
         <xtm:resourceRef xlink:href="#tomato.gif"/>
      </xtm:occurrence>
   </xtm:topic>
   <xtm:topic id="myConfite">
      <xtm:baseName>
         <xtm:baseNameString>tomato confite farcie aux douze saveurs
         </xtm:baseNameString>
      </xtm:baseName>
      <xtm:instanceOf>
         <topicRef xlink:href="#desert"/>
      </xtm:instanceOf>
   </xtm:topic>
   
<!-- Elided -->
   
   <xtm:association id="tomato_confite_association">
      <xtm:instanceOf>
         <topicRef xlink:href="ingredient_of"/>
      </xtm:instanceOf>
      <xtm:member>
         <xtm:roleSpec>
            <xtm:topicRef xlink:href="anIngredient"/>
         </xtm:roleSpec>
         <xtm:topicRef xlink:href="myTomato"/>
      </xtm:member>
      <xtm:member>
         <xtm:roleSpec>
            <xtm:topicRef xlink:href="aDish"/>
         </xtm:roleSpec>
         <xtm:topicRef xlink:href="myConfite"/>
      </xtm:member>
   </xtm:association>
   <xtm:association id="caramels_confite">
      <xtm:instanceOf>
         <topicRef xlink:href="ingredient_of"/>
      </xtm:instanceOf>
      <xtm:member>
         <xtm:roleSpec>
            <xtm:topicRef xlink:href="anIngredient"/>
         </xtm:roleSpec>
         <xtm:topicRef xlink:href="myCarmel"/>
      </xtm:member>
      <xtm:member>
         <xtm:roleSpec>
            <xtm:topicRef xlink:href="aDish"/>
         </xtm:roleSpec>
         <xtm:topicRef xlink:href="myConfite"/>
      </xtm:member>
   </xtm:association>
<!-- Elided -->
</xtm:topicMap>

See Also

UML and XMI are standards of the Object Management Group (OMG). More information is available at http://www.omg.org/uml/ and http://www.omg.org/technology/xml/index.htm.

TopicMaps.org (http://www.topicmaps.org) is the official site for Topic Map- and XTM-related information.

Recipe 13.4 shows how topic maps generate web sites.

13.4. Generating Web Sites from XTM Topic Maps

Problem

You want to capture knowledge about a subject in a Topic Map. You want to do so in a way that facilitates generation of a web site from the Topic Map using XSLT.

Solution

The solution is based on the Cogitative Topic Maps Web Site (CTW) framework introduced to readers in XML Topic Maps, edited by Jack Park (Addison Wesley, 2002). Extreme Markup Languages initially presented this work in 2000.

The CTW uses the following mapping from topic map elements to HTML:

Topic map element

HTML rendering

Topic map

Web site

Topic

Web page

Topic associations

Site map

Topic occurrences

Images, graphics, text, HTML fragments, etc.

Topic names

Page headers, titles, lists, and hyperlink titles

The Topic Map we create covers the subject of algorithms and, specifically, sorting algorithms. Knowledge represented in this topic map was aggregated from information resources gathered on the Internet and organized as class-subclass associations between algorithms and occurrences of types descriptions, demonstrations, and code examples in several programming languages.

Once the subject and content of the web site were decided, the ontology of the web site subjects and objects followed quite naturally. The CTW ontology layer consists of two main parts: classification of web site’s topic subjects and classification of topic characteristics that provide web-page content.

Both classifications play a very important role in controlling a web site’s look and feel. Topic types control web-page layouts, and types of topic characteristics control the styling of web-page elements and building blocks. The results are depicted in Figure 13-3.

The look and feel of a generated site
Figure 13-3. The look and feel of a generated site

The following subsections describe the subjects of the Sorting Algorithms web site.

Sorting algorithms

The main subjects of our web site are the sorting algorithm’s various subclasses:

<topic id="sort">
     ...
</topic>
<association>
     <instanceOf>
          <topicRef xlink:href="#_class-subclass"/>
     </instanceOf>
     <member>
          <roleSpec>
               <topicRef xlink:href="#_superclass"/>
          </roleSpec>
          <topicRef xlink:href="#sort"/>
     </member>
     <member>
          <roleSpec>
               <topicRef xlink:href="#_subclass"/>
          </roleSpec>
          <topicRef xlink:href="#simplesort"/>
          <topicRef xlink:href="#in-place sort"/>
          <topicRef xlink:href="#heapsort"/>
          <topicRef xlink:href="#adaptivesort"/>
          <topicRef xlink:href="#distributionsort"/>
          <topicRef xlink:href="#mergesort"/>
     </member>
</association>

Tip

The full version of the topic map and XSLT scripts is available online at http://www.cogx.com/ctw. This section provides only some fragments to illustrate example instructions.

Each sorting algorithm can have its own subclasses—for example, you can count four variations of in-place-sort, each of which may in turn have its own subclasses:

<association>
     <instanceOf>
          <topicRef xlink:href="#_class-subclass"/>
     </instanceOf>
     <member>
          <roleSpec>
               <topicRef xlink:href="#_superclass"/>
          </roleSpec>
          <topicRef xlink:href="#in-place sort"/>
     </member>
     <member>
          <roleSpec>
               <topicRef xlink:href="#_subclass"/>
          </roleSpec>
          <topicRef xlink:href="#quicksort"/>
          <topicRef xlink:href="#insertionsort"/>
          <topicRef xlink:href="#selsort"/>
          <topicRef xlink:href="#dimincrsort"/>
     </member>
</association>

The National Institute of Standards and Technology (NIST) maintains an excellent web site on algorithms (http://www.nist.gov/dads/) and collects information about various computational algorithms. It maintains a web page devoted to each algorithm. This topic map uses URLs of those pages as algorithm subject identifiers of algorithms:

<topic id="insertionsort">
     <subjectIdentity>
          <subjectIndicatorRef 
              xlink:href="http://www.nist.gov/dads/HTML/insertsrt.html"/>
     </subjectIdentity>

Besides playing roles in class-subclass associations with other algorithms, sorting algorithms have other topic characteristics such as base names and occurrences.

In this topic map, sorting algorithms have names under which they are commonly recognized:

     <baseName>
          <baseNameString>insertion sort</baseNameString>
     </baseName>

Sometimes they also have alternative names represented as base names in the also-known-as scope:

     <baseName>
          <scope>
               <topicRef xlink:href="#also-known-as"/>
          </scope>
          <baseNameString>linear insertion sort</baseNameString>
     </baseName>

The algorithm’s description is represented as a topic occurrence of type description in the scope of the description’s source. The following code is a citation from the National Institute of Standards and Technology web site (thus specified in the scope of the nist topic) that can also read, “In the context of NIST, insertion sort is described as . . . "

     <occurrence>
          <instanceOf>
               <topicRef xlink:href="#description"/>
          </instanceOf>
          <scope>
               <topicRef xlink:href="#nist"/>
          </scope>
          <resourceData>Sort by repeatedly taking the next item and inserting it into
          the final data structure in its proper order with respect to items already
          inserted. </resourceData>
     </occurrence>

Links to algorithm demonstrations such as applets and animations are represented as topic occurrences of type demonstration:

     <occurrence>
          <instanceOf>
               <topicRef xlink:href="#demo"/>
          </instanceOf>
          <resourceRef xlink:href=
          "http://www.cosc.canterbury.ac.nz/people/mukundan/dsal/ISort.html"/>
     </occurrence>

You can also record links to sorting algorithms implementations and represent them in your topic map as occurrences of type code sample specified in the scope of a programming language in which they are implemented. Programming languages is the other class of topics represented on your web site that constitute an orthogonal navigational dimension:

     <occurrence>
          <instanceOf>
               <topicRef xlink:href="#code"/>
          </instanceOf>
          <scope>
               <topicRef xlink:href="#fortran"/>
          </scope>
          <resourceRef 
          xlink:href="http://gams.nist.gov/serve.cgi/Module/TOMS/505/8547"/>
     </occurrence>
     <occurrence>
          <instanceOf>
               <topicRef xlink:href="#code"/>
          </instanceOf>
          <scope>
               <topicRef xlink:href="#java"/>
          </scope>
          <resourceRef xlink:href=
          "http://www.cs.ubc.ca/spider/harrison/Java/InsertionSortAlgorithm.java"/>
     </occurrence>
</topic>

That is all the information about algorithms that we chose to represent. You will use it to build page headers, links to related algorithms, descriptions, links to pages on the Web defining that algorithm, links to algorithm demonstrations, and code samples with cross links to programming languages in which these examples are implemented.

Programming languages

You only need be interested in the fact that program languages are, in their names and definitions, instances of the programming language class:

<topic id="java">
     <subjectIdentity>
          <subjectIndicatorRef 
          xlink:href="http://foldoc.doc.ic.ac.uk/foldoc/foldoc.cgi?query=java"/>
     </subjectIdentity>
     <instanceOf>
          <topicRef xlink:href="#plang"/>
     </instanceOf>
     <baseName>
          <baseNameString>Java</baseNameString>
     </baseName>
     <occurrence>
          <instanceOf>
               <topicRef xlink:href="#definition"/>
          </instanceOf>
          <scope>
               <topicRef xlink:href="#cnet"/>
          </scope>
          <resourceData>Sun Microsystems' Java is a programming language for adding 
animation and other action to Web sites. The small applications (called applets) that 
Java creates can play back on any graphical system that's Web-ready, but your Web 
browser has to be Java-capable for you to see it. According to Sun's description, 
Java is a "simple, object-oriented, distributed, interpreted, robust, secure, 
architecture-neutral, portable, high-performance, multithreaded, dynamic, buzzword-
compliant, general-purpose programming language." </resourceData>
     </occurrence>
</topic>

The programming language page shown in Figure 13-4 gathers links to code samples implemented in that language and cross links to the implemented algorithms.

Programming languages page
Figure 13-4. Programming languages page

Root topic

In the CTW framework, root is the topic whose subject is indicated by the topic map document itself. In topic map terms, root topic reifies the topic map document to which it belongs:

<topic id="default">
     <subjectIdentity>
          <subjectIndicatorRef xlink:href="#map"/>
     </subjectIdentity>

When topic maps merge in CTW, this topic is added to the scopes of all topic characteristic assignments. More importantly, this topic corresponds to the home or default page of the CTW web site.

This example displays its hyperlinked name in the upper-left corner of all pages on your web site:

     <baseName>
          <baseNameString>Sort algorithms home</baseNameString>
     </baseName>

This is the place to store topic map annotations and assertions about the topic map. This example is limited to the description of project and copyright metadata:

     <occurrence>
          <instanceOf>
               <topicRef xlink:href="#definition"/>
          </instanceOf>
          <resourceData><![CDATA[ This web site covers the subject of 
          algorithms and specifically sorting algorithms.<br><br>
          It was created for the purposes of a CTW recipe for the 
          O'Reilly XSLT Cookbook.]]> </resourceData>
     </occurrence>
</topic>

The root page shown in Figure 13-5 shows only the project’s description as an introduction to the web site.

Root page of the sort algorithms web site
Figure 13-5. Root page of the sort algorithms web site

Page elements and layout

First, define the root variable that represents the root topic: as described earlier, this topic is indicated by the containing topic map document:

<xsl:variable name="root"
   select="//topic[subjectIdentity/subjectIndicatorRef/@xlink:href
                                          = concat('#',/topicMap/@id)]"/>

The same node could be matched using the subjectIndicator key that matches topics using addresses of resources that indicate them:

<xsl:key
  name = "subjectIndicator" 
  match = "topic" 
  use = "subjectIdentity/subjectIndicatorRef/@xlink:href" />
   
<xsl:variable name="root"
             select="key('subjectIndicator',concat('#',/topicMap/@id))"/>

First, generate the default page by calling the root-page layout template for the root topic:

<xsl:template match="/">
     <xsl:call-template name="root-page">
          <xsl:with-param name="this" select="$root"/>
     </xsl:call-template>

Next, the code generates web pages for every subclass of the sorting algorithm. Here you call the algorithm-page layout template with basic sorting algorithm as this parameter. The template recursively calls itself to iterate over subclasses of all subclasses:

     <xsl:call-template name="algorithm-page">
          <xsl:with-param name="this" select="key('topicByID','#sort')"/>
     </xsl:call-template>

The stylesheet generates pages for every instance of a programming language by calling the plang-page layout template:

     <xsl:for-each select="key('instanceOf','#plang')">
          <xsl:call-template name="plang-page"/>
     </xsl:for-each>
</xsl:template>

The instanceOf key returns all topic instances of a class based on the class topic’s hashed IDs:

<xsl:key
  name = "instanceOf" 
  match = "topic" 
  use = "instanceOf/topicRef/@xlink:href" />

The topicByID key returns all topic elements based on a given topic’s hashed IDs:

<xsl:key
  name = "instanceOf" 
  match = "topic" 
  use = "instanceOf/topicRef/@xlink:href" />

As you might have noticed in the screenshots, all three layout templates have a common subdivision into four square areas. The common main page-building template controls this. First, instruct the processor to create an output file relative to the output folder specified in the $out-dir parameter, and create the TITLE header with the current topic’s base name in the unconstrained scope. The latter is achieved by instantiating template matching topic elements in the label mode. Then start the page’s subdivision into four parts. In the upper-right corner, create a hyperlink to the home page whose label is the name of root topic in the unconstraint scope. The topic-matching template in the link mode accomplishes this task. In the upper-right part of the page, you’ll have a selection of links to programming languages pages. Iterating over all instances of programming-language topic creates this. In the lower-left quarter of the page under the home page link, print the part of the site map corresponding to the sorting algorithms’ classification. Finally, in the main part of the page in the lower-right quarter, output the main-page content pertinent to the current topic submitted to the page template as content parameter:

<xsl:template name="page">
  <xsl:param name="this"/>
  <xsl:param name="content"/>
  <redirect:write select="concat($out-dir,$this/@id,'.html')">
  <HTML>
    <head>
      <title>
        <xsl:apply-templates select="$this" mode="label"/>
      </title>
    </head>
    <body>
    <table width="1000" height="100%" cellspacing="0" cellpadding="10">
    <tr>
      <td width="250" height="20" bgcolor="#ffddbb" align="center">
        <xsl:apply-templates select="$root" mode="link"/>
      </td>
      <td width="750" height="20" valign="top" bgcolor="#eeeeee">
        <table cellspacing="10">
          <tr>
            <xsl:for-each select="key('instanceOf','#plang')">
               <td background="grey">
                <xsl:apply-templates select="." mode="link"/>
              </td>  
            </xsl:for-each>
          </tr>
        </table>  
      </td>
    </tr>
    <tr>
      <td valign="top" bgcolor="#eeeeee">
        <xsl:call-template name="sitemap">
          <xsl:with-param name="classRef">#sort</xsl:with-param>
          <xsl:with-param name="current" select="$this/@id"/>
        </xsl:call-template>
      </td>
      <td valign="top" bgcolor="#ffeedd" >
       <xsl:copy-of select="$content"/>
      </td>
    </tr></table>
    </body>
  </HTML>
  </redirect:write>
</xsl:template>

The previous template uses the baseName-matching template in the label mode:

<xsl:template match="topic" mode="label">
     <xsl:value-of select="baseName[not(scope)]/baseNameString"/>
</xsl:template>

The baseName matching template in the link mode creates a hyperlink to the topic’s web page:

<xsl:template match="topic" mode="link">
  <a href="{@id}.html">
    <xsl:value-of select="baseName[not(scope)]/baseNameString"/>
  </a>
</xsl:template>

Later in the code, you will use the baseName-matching template in the subject-indicator mode. This template creates a hyperlink to a resource indicating the matched topic:

<xsl:template match="topic" mode="indicator">
  <a href="{subjectIdentity/subjectIndicatorRef/@xlink:href}">
    <xsl:value-of select="baseName[not(scope)]/baseNameString"/>
  </a>
</xsl:template>

The sitemap template iterates over all subclasses of the sort topic, creates hyperlinks to pages corresponding to derived algorithms, and recursively calls itself to iterate over subclasses of the subclasses:

<xsl:template name="sitemap">
  <xsl:param name="classRef"/>
  <xsl:param name="current"/>
  <xsl:variable name="topic" select="key('topicByID',$classRef)"/>
  <xsl:choose>
    <xsl:when test="$topic/@id=$current">
      <span class="A">
       <xsl:apply-templates select="$topic" mode="label"/>
      </span>
    </xsl:when>
    <xsl:otherwise>
      <xsl:apply-templates select="$topic" mode="link"/>
    </xsl:otherwise>
  </xsl:choose>
  <xsl:variable name="aref" select="key('classAssoc',$classRef)"/>
  <xsl:if test="$aref">
    <ul>
      <xsl:for-each 
           select="$aref/member
                    [roleSpec/topicRef/@xlink:href=
                     '#_subclass']/topicRef">
        <li>
        <xsl:call-template name="sitemap">
          <xsl:with-param name="classRef" select="@xlink:href"/>
          <xsl:with-param name="current" select="$current"/>
        </xsl:call-template>
        </li>
      </xsl:for-each>
    </ul>
  </xsl:if>
</xsl:template>

The classRef key used by the site map template uses the ID of a given topic to match superclass-subclass associations for which the topic plays super-class role:

<xsl:key
  name = "classAssoc" 
  match = "association[instanceOf/topicRef/@xlink:href=
                       '#_class-subclass']" 
  use = "member[roleSpec/topicRef/@xlink:href=
                '#_superclass']/topicRef/@xlink:href" />

Later in the code, you will use a subClassRef key that also matches superclass-subclass associations, but this time using member where the topic plays sub-class role:

<xsl:key
  name = "subClassAssoc" 
  match = "association[instanceOf/topicRef/@xlink:href=
                        '#_class-subclass']" 
  use = "member[roleSpec/topicRef/@xlink:href=
                '#_subclass']/topicRef/@xlink:href" />

Now you are ready to consider the three layout templates.

The root-page layout template is very simple. Call the page template described earlier and send it the generated HTML code for occurrences of type description in the content parameter:

<xsl:template name="root-page">
  <xsl:param name="this"/>
  <xsl:call-template name="page">
    <xsl:with-param name="this" select="$this"/>
    <xsl:with-param name="content">
      <font size="+1">
        <xsl:apply-templates 
             select="$this/occurrence
                       [instanceOf/topicRef/@xlink:href='#description']"/>
      </font>
    </xsl:with-param>
  </xsl:call-template>
</xsl:template>

The plang-page layout template is a bit more involved. It has a title composed of the name of the current topic followed by the name of topic’s type. Then a subject identity line points to a place elsewhere on the Web where the topic’s subject is defined. Following it are topic descriptions, if any. Iterate and output a link for each code occurrence implemented in the current language. In square brackets following the resource, place a link to the sorting algorithm implemented in this resource:

<xsl:template name="plang-page">
  <xsl:param name="this" select="."/>
  <xsl:call-template name="page">
    <xsl:with-param name="this" select="$this"/>
    <xsl:with-param name="content">
      <font size="+2">
        <xsl:apply-templates select="$this" mode="label"/>, a
        <xsl:apply-templates mode="label"
       select="key('topicByID',$this/instanceOf/topicRef/@xlink:href)" />.
      </font>
      <br/><br/>
      <xsl:apply-templates select="$this/subjectIdentity"/>
      <xsl:apply-templates
       select="$this/occurrence
                      [instanceOf/topicRef/@xlink:href='#description']"/>
      <xsl:variable name="codes" 
         select="key('plang-codes',concat('#',$this/@id))"/>
      <xsl:if test="$codes">
        <span>Sorting algorithms implemented in 
          <xsl:apply-templates select="$this" mode="label"/>:</span>
        <ul>
          <xsl:for-each select="$codes">
            <li>
              <a href="{resourceRef/@xlink:href}">
                 <xsl:value-of select="resourceRef/@xlink:href"/>
              </a>
              [<xsl:apply-templates select=".." mode="link"/>]<br/>
            </li>
          </xsl:for-each>
        </ul>
        <br/><br/>
      </xsl:if>
    </xsl:with-param>
  </xsl:call-template>
</xsl:template>

The plang-codes key used earlier matches all occurrences in the topic map by any of their scope themes:

<xsl:key 
 name="plang-codes" 
 match="occurrence" 
 use="scope/topicRef/@xlink:href"/>

The algorithm-page layout template comes last. Its title is composed of the current topic’s name followed by also-known-as names in square brackets. The subject identity line is followed by the list of superclasses, if any, from which the current sorting algorithm inherits. Then you’ll find one or more sorting algorithm descriptions, followed by the list of links to algorithm demonstration occurrences and the list of code samples with cross links to implementing programming languages in square brackets. On the bottom of the page, you’ll list subclasses or variations, if any, of the current sorting algorithm. Finally, the algorithm-page layout template calls itself recursively to output pages for subclasses of all its subclasses:

<xsl:template name="algorithm-page">
  <xsl:param name="this"/>
  <xsl:call-template name="page">
    <xsl:with-param name="this" select="$this"/>
    <xsl:with-param name="content">
      <font size="+2"><xsl:apply-templates select="$this" mode="label"/>
        <xsl:if test="$this/baseName
                           [scope/topicRef/@xlink:href='#also-known-as']">
          [<xsl:value-of select="$this/baseName
            [scope/topicRef/@xlink:href='#also-known-as']
           /baseNameString"/>]
        </xsl:if>
      </font>
      <br/><br/>
      <xsl:apply-templates select="$this/subjectIdentity"/>
      <xsl:variable name="superclasses"
        select="key('subClassAssoc',concat('#',$this/@id))
        member[roleSpec/topicRef/@xlink:href='#_superclass']/topicRef"/>
      <xsl:if test="$superclasses">
        Inherits from 
        <xsl:for-each select="$superclasses">
          <xsl:apply-templates 
            select="key('topicByID',@xlink:href)" mode="link"/>
            <xsl:if test="position() != last()">, </xsl:if>
        </xsl:for-each>
        <br/><br/>
      </xsl:if>
      <xsl:apply-templates
        select="$this/occurrence
                       [instanceOf/topicRef/@xlink:href='#description']"/>
      <xsl:variable name="demos" 
         select="$this/occurrence
                       [instanceOf/topicRef/@xlink:href='#demo']"/>
      <xsl:if test="$demos">
        <span>Demonstrations: </span>
        <ul>
          <xsl:for-each select="$demos">
            <li>
              <a href="{resourceRef/@xlink:href}"><
               xsl:value-of select="resourceRef/@xlink:href"/>
              </a><br/>
            </li>
          </xsl:for-each>
        </ul>
        <br/>
      </xsl:if>
      <xsl:variable name="codes" 
          select="$this/occurrence[instanceOf/topicRef/@xlink:href='#code']"/>
      <xsl:if test="$codes">
        <span>Implementations and sample code: </span>
        <ul>
          <xsl:for-each select="$codes">
            <li>
              <a href="{resourceRef/@xlink:href}">
                <xsl:value-of select="resourceRef/@xlink:href"/>
              </a>
              [<xsl:apply-templates mode="link"
               select="key('topicByID',scope/topicRef/@xlink:href)"/>]
            </li>
          </xsl:for-each>
        </ul>
        <br/>
      </xsl:if>
      <xsl:variable name="subclasses" 
          select="key('classAssoc',
                      concat('#',$this/@id))/member
                                           [roleSpec/topicRef/@xlink:href=
                                            '#_subclass']/topicRef"/>
      <xsl:if test="$subclasses">
        See also 
        <xsl:value-of select="$this/baseName[not(scope)]/baseNameString"/>
        variants: 
        <xsl:for-each select="$subclasses">
          <xsl:apply-templates 
               select="key('topicByID',@xlink:href)" mode="link"/>
          <xsl:if test="position() != last()">, </xsl:if>
        </xsl:for-each>
      </xsl:if>
    </xsl:with-param>
  </xsl:call-template>
  <xsl:variable name="aref"
                select="key('classAssoc',concat('#',$this/@id))"/>
  <xsl:for-each
    select="$aref/member
             [roleSpec/topicRef/@xlink:href='#_subclass']/topicRef">
    <xsl:call-template name="algorithm-page">
      <xsl:with-param name="this" select="key('topicByID',@xlink:href)"/>
    </xsl:call-template>
  </xsl:for-each>
</xsl:template>

Discussion

Topic Maps is a technology that accumulates and manages knowledge about real-world domains. In this case, you represented relationships between algorithms and other objects and resources. If you are careful to follow the conventions of CTW, you’ll be able to drive the creation of a web site from the topic map source.

In this solution, you were limited to just a few types of objects and relationships between them. Real-life applications are much more complicated. The main idea was to demonstrate the power and tremendous opportunities provided by the CTW framework.

In the CTW framework, a single topic map document controls both the content and structure of an entire web site. Proper CTW topic map architecture provides robust and intuitive maintenance of links between web pages, web-page content, and metadata. Web sites built according to CTW are easily merged and immune to dead links. XSLT offers a consistent look and feel, platform independence, and reusability.

You do have to pay to get all these benefits: the CTW framework requires you to think of your content in terms of topics, topic characteristics, and associations. This approach limits you to creating content only in the limits of the chosen ontology, but it helps keep the knowledge represented on the web site well organized and navigable.

XSLT enables the CTW because it provides a modular and maintainable means to transform and stylize the knowledge contained in the topic map. Dynamic CTW solutions created with XSLT scale up to several thousand topics, and static solutions are limited only by the available disk space.

See Also

XML Topic Maps: Creating and Using Topic Maps for the Web, edited by Jack Park, (Addison Wesley, 2002) is an excellent book that covers both the theory and application of topic maps in a very accessible manner.

You can find a good collection of links to resources about topic maps at http://www.dmoz.org/Computers/Artificial_Intelligence/Knowledge_Representation/Topic_Maps/.

The author maintains a site at http://www.cogx.com/ctw that further discusses topic maps and the cogitative topic maps framework. Slides from the Extreme Markup Languages presentation of CTW are available at http://www.cogx.com/Extreme2000/.

13.5. Serving SOAP Documentation from WSDL

Problem

You are using SOAP and WSDL to build a service-based enterprise architecture. You want developers to be able to find complete and pertinent information on available services.

Solution

This solution constructs a WSDL-based service documentation server: a service that provides information about services.[4] This example will use a Perl-based CGI that invokes XSLT processing on a single WSDL file containing or including information about all services available in an enterprise. Here the CGI invokes the XSLT processor in a system call. This clumsy setup is good for prototyping but not production use. A better solution would use the Perl modules XML::LibXML and XML::LibXSLT. An even better architecture would use a more sophisticated server-side XSLT-enabled solution such as Cocoon. To focus on the XSLT and WSDL aspects of this example, and not the CGI architecture, we took a simplistic approach.

The site’s main page is generated by a CGI that shows the user available services and ports. See the discussion for explanations of services and ports. It uses the following Perl CGI front end to Saxon:

#!c:/perl/bin/perl 
print "Content-type: text/html

" ;
system "saxon StockServices.wsdl wsdlServiceList.xslt" ;

The transformation builds a form containing all available services, ports, bindings, and port types:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet 
     version="1.0"
     xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
    xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
    xmlns:http="http://schemas.xmlsoap.org/wsdl/http/"
    xmlns:mime="http://schemas.xmlsoap.org/wsdl/mime/"
    exclude-result-prefixes="wsdl soap http mime">
   
  <xsl:output method="html"/>
  
  <xsl:template match="/">
      <html>
      <head>
        <title>Available Services at ACME Web Services, Inc.</title>
        <xsl:comment>WSDL Documentation: Generated by wsdlServiceList.xslt
        </xsl:comment>

      </head>
      <body>
        <xsl:apply-templates/> 
      </body>
    </html>
   
  </xsl:template>
   
  <xsl:template match="wsdl:definitions">
    <h1>Available Services at ACME Web Services, Inc.</h1>
    <br/>
    <form name="QueryServiceForm" method="post" action="QueryService.pl">
    <table>
      <tbody>
        <tr>
          <td>Services</td>
          <td>
            <select name="service">
              <option value="ALL">ALL</option>
              <xsl:apply-templates select="wsdl:service"/>
            </select>   
          </td>
        </tr>
         <tr>
          <td>Ports</td>
          <td>
            <select name="port">
              <option value="ALL">ALL</option>
              <xsl:apply-templates select="wsdl:service/wsdl:port"/>
            </select>   
          </td>
        </tr>
         <tr>
          <td>Bindings</td>
          <td>
            <select name="binding">
              <option value="ALL">ALL</option>
              <xsl:apply-templates select="wsdl:binding"/>
            </select>   
          </td>
        </tr>
         <tr>
          <td>Port Types</td>
          <td>
            <select name="portType">
              <option value="ALL">ALL</option>
              <xsl:apply-templates select="wsdl:portType"/>
            </select>   
          </td>
        </tr>
      </tbody>
    </table>
    <br/>
    <button type="submit" name="submit">Query Services</button> 
    </form>
  </xsl:template>
   
<xsl:template match="wsdl:service | wsdl:port | wsdl:binding | wsdl:portType">
  <option value="{@name}"><xsl:value-of select="@name"/></option>
</xsl:template>      
   
</xsl:stylesheet>

Users can select a combination of service, port, bind, and port type on which they want more detailed information. When submitted, another small Perl CGI extracts the input and invokes another XSLT transform:

#!/perl/bin/perl
   
use warnings;
   
use strict;
use CGI qw(:standard);
   
my $query = new CGI ;
   
my $service = $query->param('service'),
my $port = $query->param('port'),
my $binding = $query->param('binding'),
my $portType = $query->param('portType'),
   
print $query->header('text/html'),
   
system "saxon StockServices.wsdl QueryService.xslt service=$service port=$port
binding=$binding portType=$portType"

This transformation extracts the services that match the user’s criteria and displays detailed information about the service. The first part of the stylesheet normalizes some parameters. It does so because an attribute reference in a WSDL file could contain a namespace prefix, but the @name attribute of the element it references does not. The same reasoning applies to key construction:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE xslt [
  <!ENTITY TNSPREFIX "'acme:'">
]>
<xsl:stylesheet 
     version="1.0"
     xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
     xmlns:xsd="http://www.w3.org/2000/10/XMLSchema"
     xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
     xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
     xmlns:http="http://schemas.xmlsoap.org/wsdl/http/"
     xmlns:mime="http://schemas.xmlsoap.org/wsdl/mime/">
   
<!--Query parameters -->
<xsl:param name="service" select="'ALL'"/>
<xsl:param name="port" select="'ALL'"/>
<xsl:param name="binding" select="'ALL'"/>
<xsl:param name="portType" select="'ALL'"/>
   
<!-- A technique (or a hack) to make variables empty   -->
<!-- if the corrsponding parameter is ALL or otherwise -->
<!-- the concatenation of &TNSPREFIX and the parameter  -->
<!-- For example, number($service = 'ALL') * 99999 + 1 -->
<!-- will be 1 if $service is not equal to 'ALL' but   -->
<!-- 100000 if it is and hence beyond the length of the--> 
<!-- string, which causes substring to return empty.   -->
<!-- This is all to simplify cross-referencing.        -->
<xsl:variable name="serviceRef" 
              select="substring(concat(&TNSPREFIX;,$service),
                                number($service = 'ALL') * 99999 + 1)"/>
<xsl:variable name="portRef"
              select="substring(concat(&TNSPREFIX;,$port),
                                number($port = 'ALL') * 99999 + 1)"/>
<xsl:variable name="bindingRef"
              select="substring(concat(&TNSPREFIX;,$binding),
                                number($binding = 'ALL') * 99999 + 1)"/>
<xsl:variable name="portTypeRef"
              select="substring(concat(&TNSPREFIX;,$portType),
                                number($portType = 'ALL') * 99999 + 1)"/>
   
<!-- These keys simplify and speed up querying -->              
<xsl:key name="bindings_key" 
         match="wsdl:binding" use="concat(&TNSPREFIX;,@name)"/>
<xsl:key name="portType_key"
         match="wsdl:portType" use="concat(&TNSPREFIX;,@name)"/>
   
<xsl:output method="html"/>
   
  <xsl:template match="/">
      <html>
      <head>
        <title>ACME Web Services, Inc. Query Result</title>
        <xsl:comment>WSDL Documentation: Generated by wsdlServiceList.xslt
        </xsl:comment>
      </head>
      <body>
        <xsl:apply-templates select="wsdl:definitions"/> 
      </body>
    </html>
</xsl:template>

Here you simplify the problem of determining whether the query found matching services by capturing the result in a variable and testing its normalized value. If it is empty, then you know the query failed, and it displays an appropriate message. The query is a little tricky because the service’s portType can be determined only by making two hops from the service to its binding and then to the binding’s port type:

<xsl:template match="wsdl:definitions">
   <xsl:variable name="result">
        <!-- Query services that match the query parameters             -->
        <!-- The portType match is the only complicated part            -->
        <!-- We need to traverse from the service's port to the binding -->
        <!-- and then to the binding's type to do the match. Hence      -->
        <!-- the nested key() calls                                     -->
        <xsl:apply-templates 
             select="wsdl:service[
                    (not($serviceRef)  or @name = $service) and
                    (not($portRef)     or wsdl:port/@name = $port) and
                    (not($bindingRef)  or wsdl:port/@binding = $bindingRef) 
                    and
                    (not($portTypeRef) or key('portType_key',
                                               key('bindings_key',
                                                 wsdl:port/@binding)/@type)
                                                    /@name = $portType)]"/>
  </xsl:variable>
  <xsl:choose>
    <xsl:when test="normalize-space($result)">
      <xsl:copy-of select="$result"/>
    </xsl:when>
    <xsl:otherwise>
      <p><b>No Matching Services Found</b></p>
    </xsl:otherwise>
  </xsl:choose>
</xsl:template>

The rest of the stylesheet is mostly logic that renders the WSDL as HTML table entries. A service may contain more than one port, so be sure to select ports based on the query parameters once more:

<xsl:template match="wsdl:service" mode="display">
<h1><xsl:value-of select="@name"/></h1>
<p><xsl:value-of select="wsdl:documentation"/></p>
<table border="1" cellpadding="5" cellspacing="0"  width="600">
  <tbody>
    <xsl:apply-templates 
         select="wsdl:port[(not($portRef) or @name = $port) and
                           (not($bindingRef) or @binding = $bindingRef)]" 
         mode="display"/>
  </tbody>
</table>
</xsl:template>

Use these keys to traverse (do a join) between port and binding, as well as between binding and port type:

<xsl:template match="wsdl:port" mode="display">
    <tr>
      <td style="font-weight:bold" colspan="2" align="center">Port</td>
    </tr>
    <tr>
      <td colspan="2"><h3><xsl:value-of select="@name"/></h3></td>
    </tr>
    <tr>
      <td style="font-weight:bold" width="50">Binding</td>
      <td><xsl:value-of select="substring-after(@binding,':')"/></td>
    </tr>
    <tr>
      <td style="font-weight:bold" width="50">Address</td>
      <td><xsl:value-of select="soap:address/@location"/></td>
    </tr>
    <tr>
      <th colspan="2" align="center">Operations</th>
    </tr>
    <xsl:apply-templates select="key('bindings_key',@binding)" mode="display"/>
</xsl:template>
   
<xsl:template match="wsdl:binding" mode="display">
  <xsl:apply-templates select="key('portType_key',@type)" mode="display">
    <xsl:with-param name="operation" select="wsdl:operation/@name"/>
  </xsl:apply-templates>
</xsl:template>
   
<xsl:template match="wsdl:portType" mode="display">
  <xsl:param name="operation"/>
  <xsl:for-each select="wsdl:operation[@name = $operation]">
    <tr>
      <td colspan="2"><h3><xsl:value-of select="@name"/></h3></td>
    </tr>
    <xsl:if test="wsdl:input">
      <tr>
        <td style="font-weight:bold" width="50">Input</td>
        <td><xsl:value-of select="substring-after(wsdl:input/@message,':')"/></td>
      </tr>
      <xsl:variable name="msgName" 
          select="substring-after(wsdl:input/@message,':')"/>
      <xsl:apply-templates 
          select="/*/wsdl:message[@name = $msgName]" mode="display"/>
    </xsl:if>
    <xsl:if test="wsdl:output">
      <tr>
        <td style="font-weight:bold" width="50">Output</td>
        <td><xsl:value-of select="substring-after(wsdl:output/@message,':')"/></td>
      </tr>
      <xsl:variable name="msgName" 
          select="substring-after(wsdl:output/@message,':')"/>
      <xsl:apply-templates 
          select="/*/wsdl:message[@name = $msgName]" mode="display"/>
   </xsl:if>
  </xsl:for-each>
</xsl:template>
   
<xsl:template match="wsdl:message" mode="display">
  <xsl:variable name="dataType" 
      select="substring-after(wsdl:part[@name='body']/@element,':')"/>
  <tr>
    <td colspan="2">
      <xsl:apply-templates  
          select="/*/wsdl:types/*/*[@name=$dataType]" mode="display">
        <xsl:with-param name="initial-newline" select="false()"/>
      </xsl:apply-templates>
    </td>
  </tr>
</xsl:template>

This code renders the messages’ XSD schema as XML within HTML. The actual schema is probably the most informative piece of information you can display in order to document the data types associated with the service input and output messages:

<xsl:template match="*" mode="display">
  <xsl:param name="initial-newline" select="true()"/>
  
  <xsl:if test="$initial-newline">
    <xsl:call-template name="newline"/>
  </xsl:if>
  <!-- open tag -->
  <a>&lt;</a>
  <a><xsl:value-of select="name(.)" /> </a>
  
  <!-- Output attributes -->
  <xsl:for-each select="@*">
    <a><xsl:text> </xsl:text><xsl:value-of select="name(.)" />  </a>
    <a>=&quot;</a>
    <xsl:value-of select="." />
    <a>&quot;</a>
  </xsl:for-each>
  
  <xsl:choose>
       <xsl:when test="child::node()">
        <!-- close start tag -->
        <a>&gt;</a>
        <xsl:apply-templates  mode="display"/>
        <xsl:call-template name="newline"/>
        <!-- closing tag -->
        <a>&lt;</a>
        <a>/<xsl:value-of select="name(.)" /></a>
       </xsl:when>
       <xsl:otherwise>
            <a>/</a>
       </xsl:otherwise>
  </xsl:choose>
  <a>&gt;</a>
</xsl:template>
   
<!-- Add a newline and then indent based on depth within the schema -->
<xsl:template name="newline">
     <br/>
     <xsl:for-each select="ancestor::xsd:*[not(self::xsd:schema)]">
          <xsl:text>&#160;&#160;&#160;&#160;</xsl:text>
     </xsl:for-each>
</xsl:template>
   
</xsl:stylesheet>

Discussion

Chapter 10 spoke of a message repository that can capture metadata about messages exchanged in a client-server architecture. WSDL is an example of a kind of message-repository syntax for a web-service-based architecture. Here you can also use the same WSDL to generate code and documentation. This example focuses on serving up documentation.

WSDL describes web services in terms of associations between services and ports. In WSDL terms, a port is a single end point defined as a combination of a binding and a network address. A binding is a concrete protocol and data-format specification for a particular port type. A port type defines an abstract set of operations supported by one or more end points. An operation is an action that a service can perform, and it is defined in terms of a message with a corresponding data payload. The data payload is usually described in terms of an XSD schema. The following code is a partial example of a simple WSDL describing a set of services offered within the Acme Corporation:

<definitions name="StockServices" targetNamespace="http://acme.com/services.wsdl" 
xmlns:acme="http://acme.com/services.wsdl" xmlns:xsd1="http://acme.com/stockquote.
xsd" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns="http://schemas.
xmlsoap.org/wsdl/">

WSDL uses XSD schema as the default method of specifying the message data types exchanged in a service architecture:

  <types>
    <schema targetNamespace="http://acme.com/services.xsd" 
      xmlns="http://www.w3.org/2000/10/XMLSchema">
      <element name="Ticker">
        <complexType>
          <all>
            <element name="tickerSymbol" type="string"/>
          </all>
        </complexType>
      </element>
      <element name="TradePrice">
        <complexType>
          <all>
            <element name="price" type="float"/>
          </all>
        </complexType>
      </element>
      <element name="CompanyName">
        <complexType>
          <all>
            <element name="company" type="string"/>
          </all>
        </complexType>
      </element>
      <element name="ServicesInfo">
        <complexType>
          <sequence>
            <element name="Services">
              <complexType>
                <sequence>
                  <element name="Service" type="string" minOccurs="0" 
                           maxOccurs="unbounded"/>
                </sequence>
              </complexType>
            </element>
            <element name="Ports">
              <complexType>
                <sequence>
                  <element name="Port" type="string" minOccurs="0" 
                           maxOccurs="unbounded"/>
                </sequence>
              </complexType>
            </element>
            <element name="Bindings">
              <complexType>
                <sequence>
                  <element name="Binding" type="string" minOccurs="0" 
                           maxOccurs="unbounded"/>
                </sequence>
              </complexType>
            </element>
            <element name="PortTypes">
              <complexType>
                <sequence>
                  <element name="Port" type="string" minOccurs="0" 
                           maxOccurs="unbounded"/>
                </sequence>
              </complexType>
            </element>
          </sequence>
        </complexType>
      </element>
      <element name="ServicesQuery">
        <complexType>
          <all>
            <element name="Service" type="string"/>
            <element name="Port" type="string"/>
            <element name="Binding" type="string"/>
            <element name="PortType" type="string"/>
          </all>
        </complexType>
      </element>
      <element name="ServicesResponse">
        <complexType>
          <sequence>
            <any/>
          </sequence>
        </complexType>
      </element>
    </schema>
  </types>

Messages bind names to data types and are used to specify what is communicated within the service architecture:

  <message name="GetLastTradePriceInput">
    <part name="body" element="xsd1:Ticker"/>
  </message>
  <message name="GetLastTradePriceOutput">
    <part name="body" element="xsd1:TradePrice"/>
  </message>
  <message name="GetCompanyInput">
    <part name="body" element="xsd1:Ticker"/>
  </message>
  <message name="GetCompanyOutput">
    <part name="body" element="xsd1:CompanyName"/>
  </message>
  <message name="GetTickerInput">
    <part name="body" element="xsd1:CompanyName"/>
  </message>
  <message name="GetTickerOutput">
    <part name="body" element="xsd1:Ticker"/>
  </message>
  <message name="GetServicesInput"/>
  <message name="GetServicesOutput">
    <part name="body" element="xsd1:ServicesInfo"/>
  </message>
  <message name="QueryServicesInput">
    <part name="body" element="xsd1:ServicesQuery"/>
  </message>
  <message name="QueryServicesOutput">
    <part name="body" element="xsd1:ServicesRespose"/>
  </message>

The port types specify which operations are available at a particular service end point (port). This section describes two such port types: one that gives stock quote information and another that describes a service discovery service. The service discovery service is the service you implement in this example, except that it would be used by a program other than a browser:

  <portType name="StockPortType">
    <operation name="GetLastTradePrice">
      <input message="acme:GetLastTradePriceInput"/>
      <output message="acme:GetLastTradePriceOutput"/>
    </operation>
    <operation name="GetTickerFromCompany">
      <input message="acme:GetTickerInput"/>
      <output message="acme:GetCompanyOutput"/>
    </operation>
    <operation name="GetCompanyFromTicker">
      <input message="acme:GetCompanyInput"/>
      <output message="acme:GetTickerOutput"/>
    </operation>
  </portType>
  <portType name="ServicePortType">
    <operation name="GetServices">
      <input message="acme:GetServicesInput"/>
      <output message="acme:GetServicesOutput"/>
    </operation>
    <operation name="QueryServices">
      <input message="acme:QueryServicesInput"/>
      <output message="acme:QueryServicesOutput"/>
    </operation>
  </portType>

The bindings tie operations to specific protocols. Here you declare that operations are bound to the SOAP protocol:

  <binding name="StockQuoteSoapBinding" type="acme:StockPortType">
    <soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
    <operation name="GetLastTradePrice">
      <soap:operation soapAction="http://acme.com/GetLastTradePrice"/>
      <input>
        <soap:body use="literal"/>
      </input>
      <output>
        <soap:body use="literal"/>
      </output>
    </operation>
  </binding>
  <binding name="StockTickerSoapBinding" type="acme:StockPortType">
    <soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
    <operation name="GetTickerFromCompany">
      <soap:operation soapAction="http://acme.com/GetTickerSymbol"/>
      <input>
        <soap:body use="literal"/>
      </input>
      <output>
        <soap:body use="literal"/>
      </output>
    </operation>
  </binding>
  <binding name="StockNameSoapBinding" type="acme:StockPortType">
    <soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
    <operation name="GetCompanyFromTicker">
      <soap:operation soapAction="http://acme.com/GetCompanyName"/>
      <input>
        <soap:body use="literal"/>
      </input>
      <output>
        <soap:body use="literal"/>
      </output>
    </operation>
  </binding>
  <binding name="ServiceListSoapBinding" type="acme:ServicePortType">
    <soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
    <operation name="GetServices">
      <soap:operation soapAction="http://acme.com/GetServices"/>
      <input>
        <soap:body use="literal"/>
      </input>
      <output>
        <soap:body use="literal"/>
      </output>
    </operation>
  </binding>

Two services are advertised here. One is related to stock quotes, and the other to service discovery. Notice how the services organize a set of ports to specify the functionality available though the service:

  <service name="StockInfoService">
    <documentation>Provides information about stocks.</documentation>
    <port name="StockQuotePort" binding="acme:StockQuoteSoapBinding">
      <soap:address location="http://acme.com/stockquote"/>
    </port>
    <port name="StockTickerToNamePort" binding="acme:StockTickerSoapBinding">
      <soap:address location="http://acme.com/tickertoname"/>
    </port>
    <port name="StockNameToTickerPort" binding="acme:StockNameSoapBinding">
      <soap:address location="http://acme.com/nametoticker"/>
    </port>
  </service>
  <service name="ServiceInfoService">
    <documentation>Provides information about avaialable services.</documentation>
    <port name="ServiceListPort" binding="acme:ServiceListSoapBinding">
      <soap:address location="http://acme.com/stockquote"/>
    </port>
    <port name="ServiceQueryPort" binding="acme:ServiceQuerySoapBinding">
      <soap:address location="http://acme.com/tickertoname"/>
    </port>
  </service>
   
</definitions>

Using your CGI-based server, you get a service-query screen. You can select the parameters of the query, as shown in Figure 13-6.

WSDL query screen
Figure 13-6. WSDL query screen

When you submit the query, you obtain the result shown in Figure 13-7.

WSDL result screen
Figure 13-7. WSDL result screen

See Also

The specifications for WSDL are available at http://www.w3.org/TR/wsdl. WSDL is not yet an official W3C recommendation; however, it is quickly gaining recognition and support in the industry. An official Web Services Description Working Group (http://www.w3.org/2002/ws/desc/) is working on what will eventually be a W3C-sanctioned recommendation.

See Recipe 14.16 for ways of embedding XSLT processing into Perl rather than forking off a separate process.

Uche Ogbuji writes about generating documentation and RDF from WSDL in his article “WSDL processing with XSLT” at http://www-106.ibm.com/developerworks/library/ws-trans/?dwzone=ws.



[1] You might call this recipe Acronym Conversion Software (ACS).

[2] The Davenport Group was founded by a Unix System vendor and others, including O’Reilly & Associates.

[3] Unless John Michael legally changed his name to Ozzy, in which case his mom might be the only one who cares about this scope.

[4] A metaservice, if you will.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset