Systems built with XML and XSLT can often provide much more flexibility and cross-platform functionality than other approaches.

Michiel shows us how to build a shopping cart application that's simple, yet highly extensible, and in the process teaches us a few practical uses for these exciting technologies.

Many websites these days have some kind of shop, and thus a shopping cart mechanism. As business through the Internet becomes bigger, the need for a good and flexible shopping cart increases. One approach you can take is building (almost) the entire cart in XML and XSLT, which has some important advantages over other approaches. The most important issues are cross-platform use, flexibility and extensibility.

Cross-platform use

Providing we write most of the processing in XSLT, we can easily port our shopping cart to other platforms that have XML support. The only code we need to write on the other platform is the code that loads the XML and XSLT into the XML parser and transforms the XML. This is more or less basic functionality when it comes to using XML and XML parsers, and this code is usually not more than a few lines.

Another point to make here is that for most of the processing it doesn't matter if it happens on the server or on the client, as long as an XML processor is available. Currently only Internet Explorer 5 and up, and Netscape 6 support XML on the client. However these implementations still lack major XSLT functionality, if implemented at all. Therefore we will stick to doing the processing on the server rather than the client.

However, in XML you don't rely on tables, but on elements in a tree. The elements need not necessarily be the same.

Another cross-platform issue is the type of client that is used. A full-fledged browser has many more capabilities than that of a handheld or a WAP enabled phone, so we need to keep this in mind. Because our shopping basket is XML based, we can use different transforms to display the data on different platforms. If we detect the type of client before transforming, we can use the transform most suited for the target platform. The client used may also be a propriety application that just requires the XML to be sent over without any transformation. Note that we can also make use of the same mechanism when we pass the shopping basket on to a payment system, taking care of payment and sending invoices.

Flexibility and extensibility

Flexibility and extensibility are nearly synonymous when it comes to a shopping cart in XML. Basically, we want to be able to change two things: the processing mechanism and what is being processed (the data representing products). With a database or table-based approach, this is very hard to achieve. If the table structure changes, you have to change the code as well. However, in XML you don't rely on tables, but on elements in a tree. The elements need not necessarily be the same. So as long as the changes to the structure of the product data do not interfere with the mathematical processing of the shopping basket, this data may change and may even differ for different products.

Because XSLT is based on patterns, adding attributes will have no effect to other attributes and elements whatsoever. In addition we can use wildcard patterns to deal with added attributes, so they will be copied into the shopping basket as well. This means we don't have to add code for the added attributes. Another advantage of patterns and templates is that we can change the processing of a certain pattern without affecting the rest of the processing.

Design

The basic design of the shopping basket is very simple. There are two XML files: the shopping basket (which is unique for each user) and the product data file. These files can be displayed to the user with XSLT. Because XSLT is pattern-based, we can use the same XSLT for the two files.

There are four types of requests: show products, show basket, add product and update basket. The first two display the contents, as discussed previously. The other two request types use the same XSLT to change the contents of the basket, each with a different set of parameters. Then, the shopping basket is displayed as before. Theoretically, we don't need a separate request for adding a product, but the processing rules are easier when we do have one. Figure 1 shows a flowchart for the system we have designed.

Figure 1 - A partial logic flowchart illustrates the various types of requests and the resulting flow and transformation of data.
Figure 1 - A partial logic flowchart illustrates the various types of requests and the resulting flow and transformation of data.

Implementation

Now that we know what the overall structure of our implementation will look like, we can move on to the implementation details. Key to our implementation are the products and the product data. Shown below is part of our product data file. As you can see there are different products, some of which have a different set of attributes. The only required attributes are prd:ID, prd:description and prd:price, because they are the basis for our shopping basket. Another thing to note is that prd:ID needs to be unique. We could use a DTD or Schema to force these requirements. As you can see, there are also some products that belong to a certain category. This is not required however. Note that we could also have added the category as an attribute or a child-node of the product element. The obvious advantage to the latter is that we could have products belonging to more than one category.

<?xml version="1.0" encoding="ISO-8859-1"?>
<prd:products xmlns:prd="http://www.aspnl.com/xmlns/products";>
    <prd:category name="Wine">
        <prd:category name="Red">
            <prd:product prd:ID="1" prd:description="Bordeaux" prd:price="19.95" prd:year="1998" prd:country="France" />
            <prd:product prd:ID="2" prd:description="Beaujolais" prd:price="16.95" prd:year="2000" prd:country="France" />
            <prd:product prd:ID="3" prd:description="Cabernet Sauvignon" prd:price="15.95" prd:year="1999" prd:country="France" />
            <prd:product prd:ID="4" prd:description="Shirah" prd:price="13.95" prd:year="1999" prd:country="Argentina" />
        </prd:category>
    </prd:category>
    <prd:category name="Bread">
        <prd:product prd:ID="5" prd:description="Panne" prd:price="1.85" prd:weight="500" />
        <prd:product prd:ID="6" prd:description="Ciabatta" prd:price="2.95" prd:weight="750" />
    </prd:category>
</prd:products>

With the appropriate XSLT we can produce a product list in HTML, with each product having an “add to shopping basket” command, resulting in a request with the following URL:

<a href="http://www.aspnl.com/xmlshop/shop.asp?action=add&;amp">http://www.aspnl.com/xmlshop/shop.asp?action=add&;amp</a>;productID=2

This request would result in a basket looking like the following. Notice that the entire product is copied, including the namespace. Also notice that the quantity attribute is part of the shop namespace. This is done to have a clean data design, but it also simplifies processing.

<shop:basket xmlns:shop="http://www.aspnl.com/xmlns/shop";
             xmlns:prd="http://www.aspnl.com/xmlns/products";>
  <prd:product prd:ID="2" prd:description="Beaujolais" prd:price="16.95" prd:year="2000" prd:country="France" shop:quantity="1" />
</shop:basket>

What happens under the hood is that the request type is determined from the action value in the QueryString. Then, the productID value is stored and used as a parameter when applying the XSLT that updates the basket. Now we come to the guts of the system: the XSLT used to change the shopping basket. Below is the part of the XSLT that adds a product to the shopping basket. The parameter addproductID is set to the value given in the QueryString, and addproductquantity is set to 1.

<xsl:param name="addproductID" />
<xsl:param name="addproductquantity" />

<xsl:template match="/"> 
    <shop:basket>
        <xsl:if test="$addproductID != ''">
            <xsl:call-template name="addproduct" />
        </xsl:if>
        <xsl:call-template name="updateproducts" />
    </shop:basket>
</xsl:template> 

<xsl:template name="addproduct">
    <prd:product>
        <xsl:variable name="data" select="document('productdata.xml')" />
        <xsl:for-each select="$data//prd:product[./@prd:ID = $addproductID]/@*">
            <xsl:copy />
        </xsl:for-each>
        <xsl:attribute name="shop:quantity">
            <xsl:value-of select="$addproductquantity" />
        </xsl:attribute>
    </prd:product>
</xsl:template>

The first thing done in the XSLT is to check if there is a product to add. If addproductID is not empty, this is the case and the addproduct template is called. This template creates a new product element with the elements of the same product in the product data file. For this operation, the product data file is read into the variable data with the document function. This function creates a node tree from the XML read from the file, which can then be accessed through the variable name in which it was read. Note that because we are using patterns and templates, this happens only when there is actually a product that needs to be added. This is because the document function is called only inside the template that adds a product.

Providing we write most of the processing in XSLT, we can easily port our shopping cart to other platforms that have XML support.

From the product data, the right product is selected by comparing the prd:ID attribute with the addproductID parameter. From that selection all the attributes are carried over to the product element created in the shopping basket. Finally the quantity is added as shop:quantity attribute.

In a good shopping basket you can change the quantity of a product in the basket or delete items from the basket. In our sample application, we post these changes back to the server and make it into a simple XML “updategram”, illustrated below. As you can see, there is an update element for each product we update, with the new quantity. Delete elements obviously don't need the quantity.

<updates>
    <delete ID="4" />
    <delete ID="5" />
    <update ID="6" quantity="10" />
    <update ID="2" quantity="2" />
</updates>

Passing a parameter into an XSLT processor is easy, as long as it is a single value. We would like to pass the whole updategram however, which is an XML tree. In XSLT 1.0 this only works if you either use a parser specific function like msxml:node-set() to convert the tree, or pass a DOM object as parameter. The former means that your XSLT is platform dependent, but the latter might not be possible with some of the available parsers. In the sample application we have chosen to go with the latter: loading the updategram into an XML DOM Document object and passing that as a parameter just like you would with a single valued parameter. When XSLT 1.1 becomes the standard, all this is no longer an issue. We will no longer need propriety functions or pass a DOM object to solve this.

Updating the Shopping basket

Let's look at the part of the XSLT that updates the shopping basket with the updates provided as the parameter basketupdates. The fragment loops through the items in the basket and does nothing if the current product has a corresponding delete node in the update-gram. If there is an update node for the current product, then the attributes in the prd namespace are copied as is. The quantity attribute is part of the shop namespace, so it is ignored. However, the next step is updating this value with the value given in the update-gram. Here is the XSLT for all of this:

<xsl:param name="basketupdates" select="empty" />

<xsl:template name="updateproducts">
    <xsl:for-each select="/shop:basket/prd:product">
        <xsl:choose>
            <xsl:when test="@prd:ID = $basketupdates//delete/@ID" />
            <xsl:when test="@prd:ID = $basketupdates//update/@ID">
                <prd:product>
                    <xsl:for-each select="@prd:*">
                        <xsl:copy />
                    </xsl:for-each>
                    <xsl:attribute name="shop:quantity">
                        <xsl:value-of select="$basketupdates//update[@ID = current()/@prd:ID]/@quantity" />
                    </xsl:attribute>
                </prd:product>
            </xsl:when>
            <xsl:otherwise>
                <xsl:copy-of select="." />
            </xsl:otherwise>
        </xsl:choose>
    </xsl:for-each>
</xsl:template>

With the basket entirely updated, we can show it to the user, forward it to payment pages, print an invoice and so on. Because we have included only price and quantity in the basket, it is usable in different processes. We can calculate totals, taxes and any other value using the information in the basket and processing it with an XSLT that adds the appropriate values. Optionally, we could do things like currency conversion. So, the system is very flexible indeed. To give you an idea of what the XSLT would look like to display the basket in a browser, here is a template that can be used to transform single products.

<xsl:template name="basketproduct">
    <xsl:variable name="taxrate" select="0.06" />
    <xsl:variable name="tax" select="@prd:price * $taxrate" />
    <xsl:variable name="producttotal" select="@shop:quantity * (@prd:price + $tax)" />
    <tr>  
        <td>
            <input type="text" size="2" name="update{@prd:ID}" value="{@shop:quantity}" />
        </td>
        <td><xsl:value-of select="@prd:ID" /></td>
        <td><xsl:value-of select="@prd:description" /></td>
        <td><xsl:value-of select="@prd:price" /></td>
        <td><xsl:value-of select="format-number($tax, '#,##0.00')" /></td>
        <td><xsl:value-of select="format-number($producttotal, '#,##0.00')" /></td>
        <td>
            <input type="checkbox" name="delete{@prd:ID}" />
        </td>
    </tr>
</xsl:template>

Conclusion

Although you need to think a little differently, creating a shopping basket in XML and XSLT is quite feasible. In fact, the result is a very flexible and portable piece of code. As long as the processing doesn't change and the product data conforms to the basic rules, the product data can grow and adapt to the products (should the basic rules change, we can probably write a transform that can apply these changes to the product data as a whole). This means that we no longer have to code for every change we make. We may want to change how the products are being displayed, but with a good design this can be done by simply adding a template for a certain type of product or attributes.