Skip to main content

Use an XForms document as a custom XML editor

Filling in a bracket with XForms

Doug Tidwell (dtidwell@us.ibm.com), Technology Evangelist, IBM
Photo of Doug Tidwell
Doug Tidwell is a technology evangelist for the Software Group Strategy organization at IBM. His current job focuses on emerging technologies, such as Service Component Architecture (SCA), Service Data Objects (SDO), and XForms. He is the author of O'Reilly's XSLT, a second edition of which is available for preorder at your favorite bookseller. A speaker at the first XML conference in 1997, he has been working with markup languages for roughly 20 years, some rougher than others. He is currently writing a book about the inventor of the combo meal, British fast-food magnate William "Add-a-Piece" Thackeray. Doug lives in Chapel Hill, North Carolina, with his wife, daughter, and dog.

Summary:  In a recent article we looked at XSLT 2.0 functions that allowed us to generate an attractive HTML table that represented the results of an XML tournament (a bracket). What we didn't address in that article is how to fill in the winners and losers for that XML tournament. In this article, we'll revisit our XML tournament and create an XForms document that lets us fill in the tournament results without an angle bracket in sight. The result is an attractive editor for our bracket document type, complete with Ajax-like effects. Best of all, our use of XForms means the custom editor is built with declarative markup and is based on the data structures in the XML document itself.

Date:  06 Nov 2007
Level:  Intermediate PDF:  A4 and Letter (558KB | 30 pages)Get Adobe® Reader®
Activity:  2743 views
Comments:  

An XForms tournament

The previous article showed how to use XSLT 2.0 to transform an XML tournament document into an HTML bracket that displayed the tournament results. What we didn't consider in that article was how to capture those results in the first place. The goal of this article is to convince you to use XForms whenever you need an editor for an XML document type. We'll build an XForms document that presents all of the matchups between contestants in all of the rounds, tracks all the results, displays the bracket, and creates an XML document as it goes.

The final XForms document looks like this:


Figure 1. A matchup from round 1 of the Colorado Software Summit tournament
A matchup from round 1 of the Colorado Software Summit tournament

This is a friendly, interactive Web application that fills in the XML document. As a reminder, the XML bracket format looks like this:


Listing 1. The XML document for a tournament
<bracket>
  <title>How I learned to stop worrying and love the data model</title>
   <contestants>
     <contestant seed="1" image="images/HomerSimpson.png">Donuts</contestant>
     <contestant seed="2" image="images/CirqueDuSoleil.jpg">Cirque Du Soleil</contestant>
     <contestant seed="3" image="images/ArtificialIntelligence.jpg">
       AI (artificial intelligence)</contestant>
     <contestant seed="4" image="images/RockyMountains.gif">Rocky Mountains</contestant>
     <contestant seed="5" image="images/WMD.jpg">Weapons of Mass Destruction</contestant>
     <contestant seed="6" image="images/HaroldPie.gif">Pie</contestant>
     <contestant seed="7" image="images/GeorgeWBush.jpg">Misunderestimated</contestant>
     <contestant seed="8" image="images/ColoradoAvalancheHockey.gif">
       Colorado Avalanche (hockey team)</contestant>
     <contestant seed="9" image="images/ColoradoAvalanche.jpg">
       Colorado avalanche (natural disaster)</contestant>
     <contestant seed="10" image="images/SnoopDogg.png">Biz-Implification</contestant>
     <contestant seed="11" image="images/AjaxCan.png">AJAX</contestant>
     <contestant seed="12" image="images/DarthVader.jpg">Darth Vader</contestant>
     <contestant seed="13" image="images/Matterhorn.jpg">Swiss Alps</contestant>
     <contestant seed="14" image="images/AllenIverson.jpg">
       AI (natural disaster)</contestant>
     <contestant seed="15" image="images/CircleK.png">Circle K</contestant>
     <contestant seed="16" image="images/HealthyFood.jpg">Food</contestant>
   </contestants>
   <titles>
      <round>Round</round>
      <matchup>Matchup</matchup>
  </titles>
   <results>
      <result round="1" firstSeed="1" secondSeed="16" winnerSeed="1"/>
      <result round="1" firstSeed="8" secondSeed="9" winnerSeed="9"/>
      <result round="1" firstSeed="5" secondSeed="12" winnerSeed="12"/>
      <result round="1" firstSeed="4" secondSeed="13" winnerSeed="4"/>
      <result round="1" firstSeed="6" secondSeed="11" winnerSeed="6"/>
      <result round="1" firstSeed="3" secondSeed="14" winnerSeed="3"/>
      <result round="1" firstSeed="7" secondSeed="10" winnerSeed="10"/>
      <result round="1" firstSeed="2" secondSeed="15" winnerSeed="2"/>
      <result round="2" firstSeed="1" secondSeed="9" winnerSeed="1"/>
      <result round="2" firstSeed="12" secondSeed="4" winnerSeed="4"/>
      <result round="2" firstSeed="6" secondSeed="3" winnerSeed="3"/>
      <result round="2" firstSeed="10" secondSeed="2" winnerSeed="2"/>
      <result round="3" firstSeed="1" secondSeed="4" winnerSeed="4"/>
      <result round="3" firstSeed="3" secondSeed="2" winnerSeed="3"/>
      <result round="4" firstSeed="4" secondSeed="3" winnerSeed="4"/>
  </results>
</bracket>      

(Note: This document represents the actual results of a tournament held at the Colorado Software Summit conference in October. Thanks to everybody who attended the sessions.)

The results of the tournament are recorded by filling in the attributes of the <result> elements at the bottom of the document. There are several ways to gather this data:

  • You could edit the document in a text editor.
  • You could build a custom application using Swing or some other graphical framework.
  • You could build an HTML page with JavaScript and your favorite Ajax library.
  • You could build an XForms document.

As you would expect, this article covers the last approach. The reasons for rejecting the other approaches are:

  • A text editor is unusable unless you have a complete understanding of the XML document format. For example, the value of the winnerSeed attribute of the first <result> element becomes the value of the firstSeed attribute of the ninth <result> element (the first result for round two). Even if you know the document format, using a text editor is extremely error-prone.
  • Creating a custom application is the most time-consuming approach of the four. Even with a sophisticated graphical development tool, you have to define all of the logic connecting the controls with the appropriate data in the XML document.
  • Creating an HTML page requires you to write lots of JavaScript code behind the input controls. You can use a library such as Dojo to create client-side graphical effects and validation, but you're still writing and maintaining a lot of code. Worst of all, the HTML input controls aren't tied to the XML data model at all. If there's a relationship between two pieces of data, it's up to you to define that. Finally, when it's time to submit the XML document, you have to gather the data from the input fields and construct XML from them.

In contrast, XForms lets you build a declarative model that's based on the XML document. Each input control is linked directly to a particular part of the XML document. That takes care of the data collection; even better, the interface is defined declaratively as well. If we need to replace different parts of the screen with client-side Ajax effects, we can do that without writing code.

Our design goals are:

  • The entire tournament should be displayed and manipulated in a single XForms document.
  • As the results for a given matchup are entered, that information should be reflected throughout the form.
  • In addition to the four rounds of the tournament, the form should also include the bracket.
  • The application should not require any interaction with a server or file system to track results as the user goes through a tournament.

We'll look at several tasks in this article:

  • Defining the layout of the XHTML page
  • Importing the data model (our XML bracket) into the XForms document
  • Defining the panels that display the matchups
  • Defining the panel that displays the bracket
  • Defining the navigation buttons
  • Defining the XForms actions to save and reset the tournament data

Defining the layout of the XHTML page

The layout of the page is straightforward. The entire page is a table with three rows. The first row contains the header for the page and the third row contains the footer. The middle row contains a single table cell that has the majority of the XForms markup; that's where we'll spend most of our time. The footer contains five navigation buttons that let us move among the four rounds of the tournament and the bracket. The Save and Reset buttons are in the footer, as well.

The page layout looks like this:


Figure 2. The layout of the XHTML page
The layout of the XHTML page

The XHTML that defines the cells is straightforward:


Listing 2. Layout of the XHTML page
<html xmlns="http://www.w3.org/1999/xhtml">
  . . .
  <head>
    <title>Bracketology</title>
    . . .
  </head>
  <body>
    <xf:group nodeset="instance('default')">
      <table cellpadding="5" cellspacing="0" width="95%">

        <!-- Header row -->

        <tr style="height: 100px;">
          <td>
            <!-- Conference logo goes here -->
          </td>
          <td>
            <!-- Tournament title goes here -->
          </td>
          <td>
            <img src="images/dwHoF.png" alt="developerWorks logo"/>
          </td>
        </tr>

        <!-- The tournament area: The four rounds and the bracket -->

        <tr>
          <td colspan="3">
            <xf:switch>
              . . .
              <!-- XForms <xf:case> elements here, -->
             <!-- one for each of the 16 panels -->
              . . .
            </xf:switch>
          </td>
        </tr>

        <!-- Footer row -->

        <tr style="height: 100px;">
          <td>
            Bracketology
          </td>
          <td>
            <!-- Round 1, 2, 3 and 4 buttons go here -->
          </td>
          <td>
            <!-- Bracket, Save and Reset buttons go here -->
          </td>
        </tr>

      </table>
    </xf:group>
  </body>
</html>      

The middle row is where we'll put the XForms controls for the tournament data. These controls let users update the tournament by selecting between two contestants and moving on to the next matchup. In the footer, we'll define the buttons (<xf:trigger> elements in XForms) and the actions for each one. The resulting application is a user-friendly editor for a specific XML document type, defined declaratively with XForms.


Importing the data model into the XForms document

With the XHTML page defined, we can move on to importing the data model. For this example, we'll create an XForms data model that contains three things:

  1. An <xf:instance> to load an XML document from a file
  2. An <xf:submission> to write the data out to a file
  3. Some <xf:bind> elements to define the graphics for each contestant.

To start, we'll import an XML document named tourney.xml. Here's the XForms magic that does that:


Listing 3. Creating an instance of the data model
<xf:instance id="default" src="tourney.xml"/> 

In our example, tourney.xml is the file we looked at earlier in this article. With the contents of the file loaded into our document, we can display and modify its data. For example, to get the title of the tournament, we use the XPath expression /bracket/title.

The submission we'll use here simply writes the model's data to a file. We load the data with <xf:instance>, we can modify it however we want, and we can write it back to the file tourney.xml. Here's the XForms submission:


Listing 4. Defining an XForms submission
<xf:submission id="save" action="tourney.xml" method="put"
  indent="true" includenamespaceprefixes="#default"
  omit-xml-declaration="true"/>      

The <xf:submission> element writes the XML data back to the same file. The attributes here ask that the XML file be indented and that the XML declaration be omitted. (An XML declaration is the <?xml version="1.0... that can appear at the top of an XML file.) The XML document we're editing doesn't use namespaces, so the attribute includenamespaceprefixes="#default" doesn't write any namespace prefixes to the XML file.

This submission has the ID save. That allows us to invoke this submission by using the name save. A more complicated XForms document could have multiple submissions to submit the XML data to a URL, write the data to different files, and so forth, but our example here keeps it simple.

The last thing we need in our data model is a series of <xf:bind> elements to define the graphics for each contestant:


Listing 5. Binding image data to xsd:anyURI
<xf:bind type="xsd:anyURI"
  nodeset="instance('default')/contestants/contestant[@seed='1']/@image"/>
<xf:bind type="xsd:anyURI"
  nodeset="instance('default')/contestants/contestant[@seed='2']/@image"/>
. . .      

The <xf:bind> elements associate the values of the image attributes with the XML Schema datatype anyURI. When we display these values using <xf:output mediatype="image/*">, the XForms engine displays the data as an image. When we want to display the image associated with the sixth-seeded contestant, we use the image attribute of the appropriate contestant (contestant[@seed='6']). Without the binding and the mediatype, the bracket displays the values as strings:


Figure 3. Without <xf:bind> and mediatype="image/*", images display as strings
Without <xf:bind> and mediatype="image/*", images display as strings

The complete data model looks like this:


Listing 6. The XForms data model
<xf:model id="theModel">
  <xf:instance id="default" src="tourney.xml"/>

  <xf:submission id="save" action="tourney.xml" method="put"
    indent="true" includenamespaceprefixes="#default"
    omit-xml-declaration="true"/>

  <xf:bind type="xsd:anyURI"
    nodeset="instance('default')/contestants/contestant[@seed='1']/@image"/>
  <xf:bind type="xsd:anyURI"
    nodeset="instance('default')/contestants/contestant[@seed='2']/@image"/>
  . . .
</xf:model>      

With the data model in place, it's time to define XForms controls for the user interface.


Defining the panels that display the matchups

The panels that display the bracket and all of the markup are defined with the XForms <xf:switch> and <xf:case> elements. The <xf:switch> defines a set of panels, while each <xf:case> defines a single panel. We will use XForms events to determine which panel should be visible. The navigation buttons in the footer move among the different rounds and the bracket; we'll also use navigation buttons on each panel to move between the different matchups within each round.

The overall layout of the XForms document looks like this:


Figure 4. Arrangement of the XForms panels (<xf:case>)
Arrangement of the XForms panels (<xf:case>)

The different panels are effectively stacked on top of each other, with only one panel visible at a time. The XForms markup for the different panels looks like this:


Listing 7. XForms markup for the bracket panels
<xf:switch>
  <!-- Round 1, matchup 1 -->
  <xf:case id="r1m1">
    ...
  </xf:case>
  <!-- Round 1, matchup 2 -->
  <xf:case id="r1m2">
    ...
  </xf:case>
  ...
  <!-- Round 2, matchup 1 -->
  <xf:case id="r2m1">
    ...
  </xf:case>
  ...
  <!-- Round 3, matchup 1 -->
  <xf:case id="r3m1">
    ...
  </xf:case>
  ...
  <!-- Round 4 -->
  <xf:case id="r4m1">
    ...
  </xf:case>
  ...
  <!-- The bracket -->
  <xf:case id="bracket">
    ...
  </xf:case>
</xf:switch>      

There are sixteen panels that display the tournament. There are fifteen matchups between contestants: Eight in the first round, four in the second, two in the third and the championship matchup in the fourth. (The sixteenth panel displays the bracket; we'll discuss it in the next section.)

Each matchup panel contains a title followed by a table with a single row of three cells. The title indicates the round and the matchup ("Round 1, Matchup 4," for example). In the table, the left and right cells contain the graphic for each contestant, while the middle cell contains the XForms controls for choosing between the contestants. The middle cell also contains navigation buttons to move to the next or previous matchup in the current round.

For translation purposes, the strings "Round" and "Matchup" are stored in the XML file as children of the <titles> element. The title of each panel uses those strings to create the table heading:


Listing 8. Creating the matchup heading with string data from the XML file
<xf:label>
  <xf:output class="matchupHeading" 
    value="concat(titles/round, ' 1, ', 
                  titles/matchup, ' 1')"/>
</xf:label>      

This markup uses <xf:output> to generate the title "Round 1, Matchup 1." This is a weak attempt at enabling the document for translation. Tasks such as moving all of the strings into the XML document ("Bracket," "Save," "Reset" and other text) and allowing for different ordering of numbers and strings in the panel headings (another language might display this as "1 Round, 1 Matchup") are left as an exercise for the reader.

The cells with the graphics are straightforward. Here's a table cell that displays the graphic for the fourth-seeded contestant, using the mediatype="image/*" attribute:


Listing 9. Displaying a graphic with <xf:output mediatype="image/*">
<td align="center" valign="center" width="35%" height="325px">
  <xf:output mediatype="image/*" 
    ref="/bracket/contestants/contestant[@seed='4']/@image"/>
</td> 

That leaves us with the middle cell of the table. The middle cell contains the seeds and names of the two contestants and navigation buttons that move to the next or previous matchup in the round. The two contestants are displayed with an XForms <xf:select1> element. Here is the markup to select a winner in the first round matchup between the 4 and 13 seeds:


Listing 10. Displaying two choices with <xf:select1>
<xf:select1 appearance="full" 
  ref="/bracket/results/result[4]/@winnerSeed">
  <xf:item>
    <xf:label>
      <xf:output 
        value="concat('[4] ', /bracket/contestants/contestant[@seed='4'])"/>
    </xf:label>
    <xf:value>4</xf:value>
  </xf:item>
  <xf:item>
    <xf:label>
      <xf:output 
        value="concat('[13] ', /bracket/contestants/contestant[@seed='13'])"/>
    </xf:label>
    <xf:value>13</xf:value>
  </xf:item>
</xf:select1>

There are quite a few details here. First of all, appearance="full" causes the choices to appear as radio buttons. (That's how it works in Firefox, at any rate; an XForms engine is free to use whatever controls it chooses.) The important attribute of the <xf:select1> element is ref. The ref uses an XPath expression to map the XForms control to something in the data model. In this case, the value selected by this control becomes the value of the winnerSeed attribute of the fourth <result> element.

Having defined the <xf:select1> element, we need to define the two choices. Those are defined with <xf:item> elements. For each choice, we'll generate a label and a value. The label is the text of the appropriate <contestant> element, while the value is the seed of that contestant. As you would expect, we use <xf:output> to display the text of each contestant.

The matchups for the first round are always the same. The first matchup is always between the 1 seed and the 16 seed, the second matchup is always between the 8 seed and the 9 seed, and so forth. The complication is in the remaining rounds. The first matchup in the second round is the 1 seed or the 16 seed versus the 8 seed or the 9 seed. As we go further into the tournament, finding the contestants for each round gets more complicated. For example, the first seed of the championship match is the 1, 16, 8, 9, 5, 12, 4, or 13 seed. Rather than create complicated XPath predicates ([@seed='1' or @seed='16' or @seed='8' or...]), we use the order of the <result> elements. The winner of the first matchup (result[1]/@winnerSeed) is the first seed of the first matchup of the second round (result[9]/@firstSeed). Arranging the results this way makes the code much simpler.

The relationships between the matchups, the <result> elements and their seeds are as follows:


Table 1. Relationships between matchups, <result> elements and seeds
Panel (panel name)Round - MatchupWinner stored in...First Seed value / stored in...Second Seed value / stored in...
1 (r1m1)1 - 1result[1]/@winnerSeed116
2 (r1m2)1 - 2result[2]/@winnerSeed89
3 (r1m3)1 - 3result[3]/@winnerSeed512
4 (r1m4)1 - 4result[4]/@winnerSeed413
5 (r1m5)1 - 5result[5]/@winnerSeed611
6 (r1m6)1 - 6result[6]/@winnerSeed314
7 (r1m7)1 - 7result[7]/@winnerSeed710
8 (r1m8)1 - 8result[8]/@winnerSeed215
9 (r2m1)2 - 1result[9]/@winnerSeedresult[1]/@winnerSeedresult[2]/@winnerSeed
10 (r2m2)2 - 2result[10]/@winnerSeedresult[3]/@winnerSeedresult[4]/@winnerSeed
11 (r2m3)2 - 3result[11]/@winnerSeedresult[5]/@winnerSeedresult[6]/@winnerSeed
12 (r2m4)2 - 4result[12]/@winnerSeedresult[7]/@winnerSeedresult[8]/@winnerSeed
13 (r3m1)3 - 1result[13]/@winnerSeedresult[9]/@winnerSeedresult[10]/@winnerSeed
14 (r3m2)3 - 2result[14]/@winnerSeedresult[11]/@winnerSeedresult[12]/@winnerSeed
15 (r4m1)4 - 1result[15]/@winnerSeedresult[13]/@winnerSeedresult[14]/@winnerSeed

The seeds for the eight matchups in Round 1 never change, but all the matchups in all the other rounds change based on the winners of earlier matchups. The markup to select the winner of the first matchup in the second round is more complicated because we don't know the seeds of the contestants beforehand. Here's how it looks:


Listing 11. Displaying contestants for a second round matchup
<xf:select1 appearance="full"
  ref="/bracket/results/result[9]/@winnerSeed">
  <xf:item>
    <xf:label>
      <xf:output 
        value="concat('[', /bracket/results/result[9]/@firstSeed, '] ', 
      /bracket/contestants/contestant
        [@seed=/bracket/results/result[9]/@firstSeed])"/>
    </xf:label>
    <xf:value ref="/bracket/results/result[9]/@firstSeed"/>
  </xf:item>
  <xf:item>
    <xf:label>
      <xf:output 
        value="concat('[', /bracket/results/result[9]/@secondSeed, '] ', 
      /bracket/contestants/contestant
        [@seed=/bracket/results/result[9]/@secondSeed])"/>
    </xf:label>
    <xf:value ref="/bracket/results/result[9]/@secondSeed"/>
  </xf:item>
</xf:select1>      

This is the first matchup of the second round, so we store it in the winnerSeed attribute of the ninth <result> element. The XPath expression in the <xf:select1 ref="..."> attribute associates the control with that value. Because we don't know which contestants won in the first round, we have to use more complicated XPath expressions to define the <xf:item>s. The two contestants are identified with the firstSeed and secondSeed attributes of the appropriate <result> element. For the first matchup of the second round, that's the ninth one (result[9]).

The text of the first <xf:item> is from the contestant whose seed attribute matches the firstSeed attribute. The second is based on the secondSeed attribute. The values associated with the two items are the values of the firstSeed and secondSeed attributes themselves.

With the contestant names displayed in the XForms controls, the only thing left in the matchup panel is the navigation buttons. We define those with the <xf:trigger> and <xf:toggle> elements. If we're defining the panel for round 1, matchup 4 (panel r1m4), the Previous button should go to the panel for round 1, matchup 3 (panel r1m3) and the Next button should go to the panel for round 1, matchup 5 (panel r1m5). Here's the markup:


Listing 12. Navigation buttons between three matchups in the same round
<tr style="vertical-align: bottom;">
  <td style="text-align: right;" width="50%">
    <xf:trigger>
      <xf:label class="npButton">&lt; Previous</xf:label>
      <xf:toggle ev:event="DOMActivate" case="r1m3"></xf:toggle>
    </xf:trigger>
  </td>
  <td style="text-align: left;" width="50%">
    <xf:trigger>
      <xf:label class="npButton">Next &gt;</xf:label>
      <xf:toggle ev:event="DOMActivate" case="r1m5"></xf:toggle>
    </xf:trigger>
  </td>
</tr>

The <xf:trigger> element creates a button, and the <xf:label> element is the text of the button. The <xf:toggle> element causes the display to switch to another panel (another <xf:case>). The event we're responding to is DOMActivate; this simply means that we take this action when the user clicks on the button.

The buttons look like this:


Figure 5. Navigation buttons on the matchup panels
Navigation buttons on the matchup panels

Note: The first panel in a round doesn't have a Previous button, and the last panel in a round doesn't have a Next button. The championship is the only matchup in Round 4, so it doesn't have any navigation buttons at all.


Defining the panel that displays the bracket

The bracket panel contains the HTML layout discussed in our previous article:


Figure 6. The bracket
The bracket

The difference between the XForms bracket and the HTML bracket from our previous article is that the results from the matchups are displayed with <xf:output> elements inside the table cells. For example, the results of the matchup between the Rocky Mountains and the Swiss Alps are displayed like this:


Listing 13. Displaying tournament results with <xf:output>
<td>
  <xf:output 
    value="concat('[', 
    /bracket/results/result[4]/@winnerSeed, 
    '] ', 
    /bracket/contestants/contestant
    [@seed=/bracket/results/result[4]
    /@winnerSeed])"/>
</td>

The values passed to the XPath concat() function are a left bracket, the seed of the winner, a right angle bracket, and the name of the winner. Notice that the winner is the <contestant> whose seed attribute is equal to the winnerSeed attribute of the current <result>. This convoluted XPath expression is necessary because we don't know the contents of this table cell until this matchup has a winner. Here are seven table cells from the bracket panel:


Figure 7. Tournament results displayed in the bracket with <xf:output>
Tournament results displayed in the bracket with <xf:output>

The seeds of the four contestants in the leftmost row don't change, so it's easy to display them. For the other columns, we have to use <xf:output> and XPath expressions to display the names and seeds of the contestants.


Defining the navigation buttons

Our next task is to define the navigation buttons to move among the different rounds of the tournament and the bracket panel. Moving from one matchup to another within the same round isn't difficult, but moving from one round to another has some challenges. The problem is that we have to copy the winner from one matchup into the seed of a matchup in the next round. For example, the winner of the first matchup of the first round becomes the firstSeed attribute of the first matchup in the second round.

For the second round, we need to copy the winner of the first matchup into the firstSeed attribute of the ninth <result> element. To keep everything synchronized, we need to copy all of the winnerSeed, firstSeed and secondSeed attributes whenever the user clicks any of the navigation buttons that go to the bracket or to rounds two, three, or four.

We use the <xf:setvalue> element to copy values from one place to another. Here's the markup that copies the winnerSeed attribute to the firstSeed attribute of another <result> element:


Listing 14. Copying a first round winner to a second round matchup
<xf:setvalue ev:event="DOMActivate"
  ref="/bracket/results/result[9]/@firstSeed"
  value="/bracket/results/result[1]/@winnerSeed"/>

We don't keep track of the round the user is moving from, so we use <xf:setvalue> to copy all of the values any time the user clicks one of the Round 1-4 buttons or the Bracket button. Here's how the markup looks for the "Round 2" button:


Listing 15. Copying all the relevant values when switching to Round 2
<xf:trigger>
  <xf:label class="buttonText">Round 2</xf:label>
  
  <!-- First round winners, eight of them in all -->
  <xf:setvalue ev:event="DOMActivate"
    ref="/bracket/results/result[9]/@firstSeed"
    value="/bracket/results/result[1]/@winnerSeed"/>
  <xf:setvalue ev:event="DOMActivate"
    ref="/bracket/results/result[9]/@secondSeed"
    value="/bracket/results/result[2]/@winnerSeed"/>
  
  <xf:setvalue ev:event="DOMActivate"
    ref="/bracket/results/result[10]/@firstSeed"
    value="/bracket/results/result[3]/@winnerSeed"/>
  <xf:setvalue ev:event="DOMActivate"
    ref="/bracket/results/result[10]/@secondSeed"
    value="/bracket/results/result[4]/@winnerSeed"/>

  . . .  

  <!-- Second round winners, the final four -->
  <xf:setvalue ev:event="DOMActivate"
    ref="/bracket/results/result[13]/@firstSeed"
    value="/bracket/results/result[9]/@winnerSeed"/>
  <xf:setvalue ev:event="DOMActivate"
    ref="/bracket/results/result[13]/@secondSeed"
    value="/bracket/results/result[10]/@winnerSeed"/>
  
  . . .
  
  <xf:toggle ev:event="DOMActivate" case="r2m1"/>
</xf:trigger>      

The <xf:trigger> element creates the button. It contains a number of elements, all of which are triggered by the DOMActivate event. In other words, whenever the button is clicked, all of the elements inside <xf:trigger> are processed. The <xf:setvalue> elements copy the values, and the <xf:toggle> element switches the interface to the first panel of the second round.


Defining the XForms actions to save and reset the tournament data

Our final task is to define the actions to save and reset the tournament data. Saving the data uses the <xf:submission> element we defined as part of our data model. Resetting the data uses <xf:setvalue> to set all the winnerSeed attributes to an empty string (winnerSeed="") and to set the firstSeed and secondSeed attributes for rounds two, three, and four to empty strings as well.

Because the user can click the Save button at any time, we'll reuse all the <xf:setvalue> elements we used for the navigation buttons. The main difference here is that the Save button is created with the <xf:submit> element. Here's the markup:


Listing 16. Markup for the Save button
<xf:submit submission="save">
  <xf:label class="buttonText">Save</xf:label>
  <xf:action ev:event="DOMActivate">
    
    <!-- First round winners, eight of them in all -->
    <xf:setvalue ref="/bracket/results/result[9]/@firstSeed"
      value="/bracket/results/result[1]/@winnerSeed"/>
    <xf:setvalue ref="/bracket/results/result[9]/@secondSeed"
      value="/bracket/results/result[2]/@winnerSeed"/>
    . . .
  
    <xf:message level="modal">
      Tournament results saved in tourney.xml.
    </xf:message>

  </xf:action>
</xf:submit>      

Notice that the markup refers to the <xf:submission> element we defined in the data model. Another difference between the Save button and the navigation buttons is that all of the <xf:setvalue> elements are inside an <xf:action> element. The <xf:action> event responds to the DOMActivate event, and everything inside it is executed when the user clicks the button. Finally, the code displays an <xf:message> to tell the user the data has been saved:


Figure 8. A message indicates that the data has been saved
A message indicates that the data has been saved

The Reset button is straightforward; it merely resets all the appropriate values to empty strings and displays a message that the data has been reset. The Reset button exists only to demonstrate how the XForms document updates the display whenever the values in the data model change; it's unlikely you would use this in a real application. The markup looks like this:


Listing 17. Markup for the Reset button
<xf:trigger>
  <xf:label class="buttonText">Reset</xf:label>
  
  <!-- Round 1, reset the winnerSeed attribute only -->
  <xf:setvalue ev:event="DOMActivate" 
    ref="/bracket/results/result[1]/@winnerSeed" value=""/>
  <xf:setvalue ev:event="DOMActivate" 
    ref="/bracket/results/result[2]/@winnerSeed" value=""/>
  . . .

  <!-- Round 2, reset all attributes -->
  <xf:setvalue ev:event="DOMActivate" 
    ref="/bracket/results/result[9]/@firstSeed" value=""/>
  <xf:setvalue ev:event="DOMActivate" 
    ref="/bracket/results/result[9]/@secondSeed" value=""/>
  <xf:setvalue ev:event="DOMActivate" 
    ref="/bracket/results/result[9]/@winnerSeed" value=""/>
  . . .
    
  <xf:message level="modal">
    Tournament results are now reset.
  </xf:message>
</xf:trigger>  

For the <result> elements in the first round, we only reset the winnerSeed attributes; for the second, third, and fourth rounds (<result> elements 9 through 15), we reset the firstSeed and secondSeed attributes as well. Resetting the values in the data model automatically updates the display:


Figure 9. Resetting the values in the data model automatically updates the display
Reset automatically updates the display

If a cell doesn't have a result, its contents are the left and right angle brackets ([]).


Refactoring the code with XSLT

We've built the XForms document, but there's a lot of repetitive code here. As a final step, we'll create an XSLT stylesheet that generates the bracket. The code we have to maintain is much simpler this way. For example, instead of maintaining 15 matchup panels in our XForms document, we'll maintain a single matchup panel in an XSLT stylesheet. This also makes it easier to make changes across the entire XForms document; if we want to change the look of the matchup panels, we change the code one place in the stylesheet and regenerate all the panels.

In moving from XForms source to XSLT, all of the non-repetitive XForms markup appears unchanged in the stylesheet. We'll use XSLT to refactor the tedious markup we don't want to maintain by hand. We'll start with the 16 <xf:bind> elements:


Listing 18. XSLT code to create the XForms bindings
<xsl:for-each select="1 to 16">
  <xf:bind type="xsd:anyURI"
    nodeset="instance('default')/contestants/contestant[@seed='{.}']/@image"/>
</xsl:for-each>

The only thing that changes for each <xf:bind> element is the seed of the contestant. We use the XSLT 2.0 to operator to create a range of integers from 1 to 16. The {curly braces} in the nodeset attribute are an attribute value template. The code inside the braces is evaluated and written to the output document. The dot here represents the current item in the range. The result is a set of 16 <xf:bind> elements that contain [@seed='1'], [@seed='2'], and so on.

Our next major task is to create the 15 matchup panels. We'll use XSLT to create a named template that does this. Invoking the template looks like this:


Listing 19. Invoking the named template that creates the matchup panels
<xsl:for-each select="1 to 15">
  <xsl:call-template name="createMatchupPanel">
    <xsl:with-param name="resultNumber" select="."/>
  </xsl:call-template>
</xsl:for-each>

We're using the to attribute again to create a range of integers; each integer is the number of the matchup. We pass that number to the template that creates the matchup panels. Before we talk about refactoring the code, we'll take a close look at one of the matchup panels:


Listing 20. A typical matchup panel
<xf:case id="r2m2">
  <xf:label>
    <xf:output class="matchupHeading"
      value="concat(titles/round, 
             ' 2 , ', 
             titles/matchup, 
             ' 2')"/>
  </xf:label>
  <table cellpadding="3" cellspacing="0" width="95%">
    <tr>
      <td align="center" valign="center" width="35%" height="325px">
        <xf:output mediatype="image/*"
          ref="/bracket/contestants/
               contestant[@seed=/bracket/results/result[10]
               /@firstSeed]/@image"/>
      </td>
      <td style="line-height: 35px; vertical-align: center;
                 padding: 15px; width: 30%;">
        <table width="100%" height="300px">
          <tr>
            <td colspan="2">
              <xf:select1 ref="results/result[10]/@winnerSeed"
                required="true()" appearance="full" xf:navindex="1">
                <xf:item>
                  <xf:label>
                    <xf:output 
                      value="concat('[',
                             /bracket/results/result[10]/@firstSeed, 
                             '] ',
                             /bracket/contestants/contestant
                             [@seed=/bracket/results/result[10]
                             /@firstSeed])"/>
                  </xf:label>
                  <xf:value ref="/bracket/results/result[10]/@firstSeed"/>
                </xf:item>
                <xf:item>
                  <xf:label>
                    <xf:output 
                      value="concat('[',
                             /bracket/results/result[10]
                             /@secondSeed,  
                             '] ', 
                             /bracket/contestants/contestant
                             [@seed=/bracket/results/result[10]
                             /@secondSeed])"/>
                  </xf:label>
                  <xf:value ref="/bracket/results/result[10]/@secondSeed"/>
                </xf:item>
              </xf:select1>
            </td>
          </tr>
          <tr style="vertical-align: bottom;">
            <td style="text-align: right;" width="50%">
              <xf:trigger>
                <xf:label class="npButton">< Previous</xf:label>
                <xf:toggle ev:event="DOMActivate" case="r2m1"></xf:toggle>
              </xf:trigger>
            </td>
            <td style="text-align: left;" width="50%">
              <xf:trigger>
                <xf:label class="npButton">Next ></xf:label>
                <xf:toggle ev:event="DOMActivate" case="r2m3"></xf:toggle>
              </xf:trigger>
            </td>
          </tr>
        </table>
      </td>
      <td align="center" valign="center" width="35%" height="325px">
        <xf:output mediatype="image/*"
          ref="/bracket/contestants/contestant                    
               [@seed=/bracket/results/result[10]/@secondSeed]/@image"/>
      </td>
    </tr>
  </table>
</xf:case> 

The text in bold above are the only things that change between panels. This panel is the second match in the second round, the tenth overall match in the tournament. Notice that most of the information specific to this panel is the number 10. That means most of this code will be easy to generate; all we have to do is plug in the number of the match and most of the code is done.

There are four other things that change in each matchup panel. The first is the ID of the panel. The second is the title above the panel. Matchup number 10 should have the title "Round 2, Matchup 2." The third and fourth changes are the IDs of the next and previous panels, if any. The tenth matchup is the second matchup in round two, so the ID is r2m2. We know this is matchup number 10, so we need a function to tell us the round and the matchup within the round. We do that with two sequences and two functions:


Listing 21. Determining the round and matchup within that round
<xsl:variable name="rounds" as="xsd:integer*"
  select="1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 4"/>

<xsl:function name="dw:getRound" as="xsd:integer">
  <xsl:param name="resultNumber" as="xsd:integer"/>
  <xsl:value-of 
    select="subsequence($rounds, $resultNumber, 1)"/>
</xsl:function>

<xsl:variable name="matchups" as="xsd:integer*"
  select="1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 1, 2, 1"/>

<xsl:function name="dw:getMatchup" as="xsd:integer">
  <xsl:param name="resultNumber" as="xsd:integer"/>
  <xsl:value-of 
    select="subsequence($matchups, $resultNumber, 1)"/>
</xsl:function>

The sequence $rounds defines the round for each matchup. The first eight matchups are in Round 1, the next four are in Round 2, the next two are in Round 3, and the final matchup is in Round 4. The function dw:getRound takes a matchup number as input and returns the round for that matchup. For our example of the tenth round, the tenth item in the sequence is the number 2. The sequence $matchups and the function dw:getMatchup do the same thing for the number of the matchup within the round. Given the number of the matchup, we can use these two functions to determine everything else we need to know.

The functions we just discussed are defined globally in the stylesheet. When our template to create the matchup panels begins, we start by calculating the round and the matchup within the round:


Listing 22. The start of the template to generate matchup panels
<xsl:template name="createMatchupPanel">
  <xsl:param name="resultNumber" as="xsd:integer" required="yes"/>
  
  <xsl:variable name="round" as="xsd:integer" 
    select="dw:getRound($resultNumber)"/>
  <xsl:variable name="matchup" as="xsd:integer"
    select="dw:getMatchup($resultNumber)"/>

The number of the round is stored in the variable $round, and the number of the matchup within the round is in the variable $matchup. To generate the ID of the panel, we use the code concat('r', $round, 'm', $matchup). We also need to generate the heading "Round 2, Matchup 2." That uses the concat() function as well.

The final things we need to generate are the Next and Previous buttons. The first step is to determine whether this is the first panel in the round or the last panel in the round (for the final round, the one matchup panel is both). If this is the first panel, we don't create a Previous button, and if this is the last panel, we don't create a Next button. We use the XSLT 2.0 <xsl:function> element to determine these properties:


Listing 23. Determining if a match is the first or last in a round
<xsl:function name="dw:isNotFirstMatchup" as="xsd:boolean">
  <xsl:param name="resultNumber" as="xsd:integer"/>
  <xsl:value-of 
    select="not($resultNumber eq  1) and not($resultNumber eq  9) and
            not($resultNumber eq 13) and not($resultNumber eq 15)"/>
</xsl:function>

<xsl:function name="dw:isNotLastMatchup" as="xsd:boolean">
  <xsl:param name="resultNumber" as="xsd:integer"/>
  <xsl:value-of 
    select="not($resultNumber eq  8) and not($resultNumber eq 12) and
            not($resultNumber eq 14) and not($resultNumber eq 15)"/>
</xsl:function>

The first matchups are numbers 1, 9, 13, and 15, while the last matchups are numbers 8, 12, 14, and 15. These simple functions make the XSLT code simpler. Here's how we generate the Next and Previous buttons:


Listing 24. Generating the Next and Previous buttons
<td style="text-align: right;" width="50%">
  <xsl:if test="dw:isNotFirstMatchup($resultNumber)">
    <xf:trigger>
      <xf:label class="npButton">&lt; Previous</xf:label>
      <xf:toggle ev:event="DOMActivate" 
        case="{concat('r', $round, 'm', $matchup - 1)}"/>
    </xf:trigger>
  </xsl:if>
</td>
<td style="text-align: left;" width="50%">
  <xsl:if test="dw:isNotLastMatchup($resultNumber)">
    <xf:trigger>
      <xf:label class="npButton">Next &gt;</xf:label>
      <xf:toggle ev:event="DOMActivate"
        case="{concat('r', $round, 'm', $matchup + 1)}"/>
    </xf:trigger>
  </xsl:if>
</td>

If this is not the first matchup in the round, we create the Previous button; if this is not the last matchup in the round, we create the Next button. The final detail is generating the ID of the next or previous panel. The Next and Previous buttons never go to a different round, so we know round in the panel ID already. For the matchup within the round, $matchup - 1 is the previous panel's position in the current round, and $matchup + 1 is the next panel's position. The two concat() functions highlighted in Listing 24 illustrate this.

One note about this template: When we wrote all of the panels by hand, the XPath expressions for the first eight matchups were simpler. We already know the seeds for the first eight matchups, so we hardcoded them into the XForms document. (Compare Listing 10 to Listing 11.) After refactoring, our template generates the same type of code for 15 matchups. This is slightly less efficient for the first eight matchups. Adding logic to the XSLT template so that panels 1 through 8 have the simpler markup is an exercise for the reader.

Now that we have the template for generating the matchup panels, there are three more areas of repetitive code we want to refactor: The bracket panel, the code to reset the results of the tournament, and the code to copy values from one attribute to another (the <xf:setvalue> elements we mentioned earlier). The code to generate the bracket panel was discussed in an earlier article here on developerWorks, so we won't revisit that code here. The code to reset all of the seeds in the tournament is straightforward:


Listing 25. Resetting the results of the tournament
<xsl:template name="resetData">
  <xsl:for-each select="1 to 8">
    <xf:setvalue ev:event="DOMActivate"
      ref="/bracket/results/result[{.}]/@winnerSeed" value=""/>
  </xsl:for-each>
  
  <xsl:for-each select="9 to 15">
    <xf:setvalue ev:event="DOMActivate"
      ref="/bracket/results/result[{.}]/@firstSeed" value=""/>
    <xf:setvalue ev:event="DOMActivate"
      ref="/bracket/results/result[{.}]/@secondSeed" value=""/>
    <xf:setvalue ev:event="DOMActivate"
      ref="/bracket/results/result[{.}]/@winnerSeed" value=""/>
  </xsl:for-each>
</xsl:template>

For the first eight seeds, we only reset the winnerSeed attribute; for the rest of the results, we reset the firstSeed and secondSeed attributes as well. The attribute value template {.} inserts the number of the result we want to reset.

That leaves us with the task of generating the code to copy attributes from the results of one round to the results of the next round. This repetitive code can be simplified significantly by looking for patterns in the numbers of the <result> elements. Here's a simplified version of the information in Table 1:


Table 2. Relationships between <result> elements
Winner SeedCopied toDifference Between PositionsPosition Copied to
result[1]/@winnerSeedresult[9]/@firstSeed8 (9 - 1) 9
result[2]/@winnerSeedresult[9]/@secondSeed7 (9 - 2)9
result[3]/@winnerSeedresult[10]/@firstSeed7 (10 - 3)10
result[4]/@winnerSeedresult[10]/@secondSeed6 (10 - 4)10
result[5]/@winnerSeedresult[11]/@firstSeed6 (11 - 5)11
result[6]/@winnerSeedresult[11]/@secondSeed5 (11 - 6)11
result[7]/@winnerSeedresult[12]/@firstSeed5 (12 - 7)12
result[8]/@winnerSeedresult[12]/@secondSeed4 (12 - 8)12
result[9]/@winnerSeedresult[13]/@firstSeed4 (13 - 9)13
result[10]/@winnerSeedresult[13]/@secondSeed3 (13 - 10)13
result[11]/@winnerSeedresult[14]/@firstSeed3 (14 - 11)14
result[12]/@winnerSeedresult[14]/@secondSeed2 (14 - 12)14
result[13]/@winnerSeedresult[15]/@firstSeed2 (15 - 13)15
result[14]/@winnerSeedresult[15]/@secondSeed1 (15 - 14)15

We don't need to change the first eight results because the two seeds for those matchups are known in advance. Looking at the seven remaining results, the winner seeds copied to future results appear in order from 1 to 14. Now all we need to do is create a mathematical formula to get from 1 to 9, 2 to 9, 3 to 10, and so forth. Here's the template that does the trick:


Listing 26. The template that generates the <xf:setvalue> elements
<xsl:template name="updateData">
  <xsl:for-each select="1 to 14">
    <xf:setvalue ev:event="DOMActivate"
      ref="{concat('/bracket/results/result[',
           . + (((16 - .) idiv 2) + . mod 2),
           ']',
           if ((. mod 2) eq 1) then '/@firstSeed'
           else '/@secondSeed')}"
      value="/bracket/results/result[{.}]/@winnerSeed"/>
  </xsl:for-each>
</xsl:template> 

That's fairly intimidating code, eh? We'll go through this step by step. First of all, we're creating the XPath expression for the ref attribute. The generated expression is of the form /bracket/results/result[9]/@firstSeed. We have to calculate the position of the result and determine whether we're copying to the firstSeed attribute or the secondSeed attribute.

Looking back at Table 2, the numbers we add to the current item (the numbers in the third and fourth columns) have a pattern. In the calculation in Listing 26, we subtract the current item from 16, then we use the XPath 2.0 idiv (integer division) operator to divide the result by 2. To that we add 1 if the current item is odd (an odd number mod 1 is 1, while an even number mod 1 is zero). Here's a table that illustrates how the calculation works:


Table 3. Calculating the position of <result> elements
Item (.)(16 - .)((16 - .) idiv 2). mod 2(((16 - .) idiv 2) + . mod 2)Item + (((16 - .) idiv 2) + . mod 2)
1157189
2147079
31361710
41260610
51151611
61050511
7941512
8840412
9731413
10630313
11521314
12420214
13311215
14210115

The numbers in the last two columns of Table 3 match the numbers in the last two columns of Table 2. The XPath expressions here are somewhat confusing, but they get the job done. Given the integers 1 through 15, the expression generates the sequence 8, 7, 7, 6, 6, 5, 5, 4, 4, 3, 3, 2, 2, 1; that's the same sequence of numbers in the third column of Table 2.

The only other thing we have to do in this template is figure out whether we're copying a value to the firstSeed attribute or the secondSeed attribute. We can use . mod 2 for this as well. To keep the syntax simple, we can use the XPath 2.0 if operator. The expression if (. mod 2) then '/@firstSeed' else '/@secondSeed' returns the apropriate attribute. The expression uses the fact that a non-zero number is true, while a zero is false. Any odd-numbered items will go to the first seed, while any even-numbered items go to the second.

One final wrinkle: The XForms code for each matchup panel uses the concat() function in three places. That means we need to use the concat() function to generate the concat() function in the XForms document. Here's an example that generates the XPath expression that creates the "Round x, Matchup y" heading:


Listing 27. Using the concat() function to generate the concat() function
<xf:label>
  <xf:output class="matchupHeading"
    value="{concat('concat(titles/round, '' ', $round, 
           ', '', titles/matchup, '' ', $matchup, ''')')}"/>
</xf:label>

This generates the value concat(titles/round, ' 1, ', titles/matchup, ' 1') when the values of $round and $matchup are both 1. Notice that the entire expression is an attribute value template.

To run the stylesheet and generate the XForms document, use the following command:


Listing 28. Generating the XForms document with XSLT
      java net.sf.saxon.Transform -o tournament.xhtml tourney.xml MatchupBuilder.xsl

The refactored XForms code is generated by an XSLT stylesheet. All of the repetitive elements of the XForms document are now generated by XSLT, giving us far less code to write and maintain. Any global changes to the look and feel of the XForms document can be made much easier. The stylesheet that generates the same XForms markup is 70% smaller than the original XForms document. This technique makes it possible to generate a great deal of code with a lower maintenance cost, building on open standards along the way.


Summary

In this article, we've created an attractive, usable XForms interface for manipulating XML data. A user who selects the winners of the 15 matchups automatically creates a complete, valid XML document. In our example here, we simply wrote that document to a file; we could have just as easily submitted the XML document to a Web application. To simplify development and maintenance, we refactored the markup in our XForms document by generating it with an XSLT stylesheet. Best of all, everything in the XForms document is tied directly to the XML data model.

Whenever you need to display, create or manipulate XML data, think of XForms as your preferred technology for building custom user interfaces.

Acknowledgements

The author would like to acknowledge the help, generosity, and XForms expertise of Keith Wells in answering many questions during the creation of the document at the heart of this article.



Download

DescriptionNameSizeDownload method
XML and XHTML samples from this articlexfbracket-samples.zip196KBHTTP

Information about download methods


Resources

Learn

Get products and technologies

Discuss

About the author

Photo of Doug Tidwell

Doug Tidwell is a technology evangelist for the Software Group Strategy organization at IBM. His current job focuses on emerging technologies, such as Service Component Architecture (SCA), Service Data Objects (SDO), and XForms. He is the author of O'Reilly's XSLT, a second edition of which is available for preorder at your favorite bookseller. A speaker at the first XML conference in 1997, he has been working with markup languages for roughly 20 years, some rougher than others. He is currently writing a book about the inventor of the combo meal, British fast-food magnate William "Add-a-Piece" Thackeray. Doug lives in Chapel Hill, North Carolina, with his wife, daughter, and dog.

Comments



Trademarks  |  My developerWorks terms and conditions

Help: Update or add to My dW interests

What's this?

This little timesaver lets you update your My developerWorks profile with just one click! The general subject of this content (AIX and UNIX, Information Management, Lotus, Rational, Tivoli, WebSphere, Java, Linux, Open source, SOA and Web services, Web development, or XML) will be added to the interests section of your profile, if it's not there already. You only need to be logged in to My developerWorks.

And what's the point of adding your interests to your profile? That's how you find other users with the same interests as yours, and see what they're reading and contributing to the community. Your interests also help us recommend relevant developerWorks content to you.

View your My developerWorks profile

Return from help

Help: Remove from My dW interests

What's this?

Removing this interest does not alter your profile, but rather removes this piece of content from a list of all content for which you've indicated interest. In a future enhancement to My developerWorks, you'll be able to see a record of that content.

View your My developerWorks profile

Return from help

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=XML
ArticleID=267299
ArticleTitle=Use an XForms document as a custom XML editor
publish-date=11062007
author1-email=dtidwell@us.ibm.com
author1-email-cc=dwxed@us.ibm.com

My developerWorks community

Tags

Help
Use the search field to find all types of content in My developerWorks with that tag.

Use the slider bar to see more or fewer tags.

Popular tags shows the top tags for this particular content zone (for example, Java technology, Linux, WebSphere).

My tags shows your tags for this particular content zone (for example, Java technology, Linux, WebSphere).

Use the search field to find all types of content in My developerWorks with that tag. Popular tags shows the top tags for this particular content zone (for example, Java technology, Linux, WebSphere). My tags shows your tags for this particular content zone (for example, Java technology, Linux, WebSphere).

Special offers