Thursday, April 20, 2006

Ajax: HTML vs XML responses

A friend of mine has a discussion on his blog regarding returning straight HTML or XML in your AJAX responses (see What type of data should an Ajax call return?)

I voiced a comment regarding my appreciation for the direction used by Rico where XML is always returned, though it may contain an HTML snippet such as:


<ajax-response>
<response type="element" id="personInfo">
[valid XHTML here]
</response>
</ajax-response>


where the type="element" id="personInfo" indicates to Rico to replace the innerHTML of the element with id personInfo with the contents of the response.

Or alternatively:


<ajax-response>
<response type="object" id="formLetterUpdater">
[valid XML data here]
</response>
</ajax-response>


The type="object" id="formLetterUpdater" calls a JavaScript object registered under the name matching the id of formLetterUpdater. That Javascript would then process the contents of the response as XML to extract whatever data is necessary.

For either mechanism, multiple "response" clauses can be returned in a single "ajax-response" allowing multiple sections of a web page to be updated from a single AJAX action if necessary.

My apprehension about this method is that the server side processing has too much knowledge about how the data will be used (by dictating the ids). Of course, that might be able to be handled by having the AJAX response send which ids it wants data for.

Of these two mechanisms from Rico, I think the XML data mechanism has the most flexibility, as it describes the data in XML and the UI can extract what it needs and format it correctly whether by coding manual JavaScript DOM processing as indicated with the formLetterUpdater above, or by utilizing XSLT on the browser to format it as desired. Using XSLT provides multiple advantages for formatting and re-formatting the data for the user without the need to communicate with the server over and over to format the HTML. For example, switching themes, languages, or switching from a table to a graph.

Tuesday, April 18, 2006

Wrapping long strings with XSL 1.0

For my recent work with XML and XSL, I needed to wrap strings when generating a text report. I found examples online to do it, but they depended on XSL 2.0 functions such as the string tokenizer. I needed a way to do it with plain old XSL 1.0, so the solution I came up with is below. This solution is a bit more complex than necessary since I needed the ability to add markup around the wrapped lines (the prefix and suffix). Additionally, I needed the ability to add different markup before and after wrapped lines as well (the hangingPrefix and hangingSuffix). I could have taken it out to simplify this example, however, I thought some people might find it useful. Additionally, it's designed for all the prefixes and suffixes to be optional anyway. Here's my solution (the second template called splitString is the entry point):


<!-- recursive section of splitString... don't call directly -->
<xsl:template name="firstString">
<xsl:param name="string" />
<xsl:param name="length" />
<xsl:param name="currentpos" />
<xsl:param name="prefix" />
<xsl:param name="hangingPrefix" />
<xsl:param name="suffix" />
<xsl:param name="hangingSuffix" />
<xsl:param name="padding" />

<xsl:choose>
<xsl:when test="substring($string, $currentpos, 1) = ' '">
<xsl:value-of select="substring($string, 1, $currentpos - 1)" />
<xsl:value-of select="$suffix" />
<xsl:value-of select="$newline" />

<xsl:call-template name="splitString">
<xsl:with-param name="string" select="substring($string, $currentpos + 1)" />
<xsl:with-param name="length" select="$length" />
<xsl:with-param name="prefix" select="$hangingPrefix" />
<xsl:with-param name="hangingPrefix" select="$hangingPrefix" />
<xsl:with-param name="suffix" select="$hangingSuffix" />
<xsl:with-param name="hangingSuffix" select="$hangingSuffix" />
<xsl:with-param name="padding" select="$padding" />
</xsl:call-template>
</xsl:when>
<xsl:when test="substring($string, $currentpos, 1) = '/'">
<xsl:value-of select="substring($string, 1, $currentpos)" />
<xsl:value-of select="$suffix" />
<xsl:value-of select="$newline" />

<xsl:call-template name="splitString">
<xsl:with-param name="string" select="substring($string, $currentpos + 1)" />
<xsl:with-param name="length" select="$length" />
<xsl:with-param name="prefix" select="$hangingPrefix" />
<xsl:with-param name="hangingPrefix" select="$hangingPrefix" />
<xsl:with-param name="suffix" select="$hangingSuffix" />
<xsl:with-param name="hangingSuffix" select="$hangingSuffix" />
<xsl:with-param name="padding" select="$padding" />
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:call-template name="firstString">
<xsl:with-param name="string" select="$string" />
<xsl:with-param name="length" select="$length" />
<xsl:with-param name="currentpos" select="$currentpos - 1" />
<xsl:with-param name="prefix" select="$prefix" />
<xsl:with-param name="hangingPrefix" select="$hangingPrefix" />
<xsl:with-param name="suffix" select="$suffix" />
<xsl:with-param name="hangingSuffix" select="$hangingSuffix" />
<xsl:with-param name="padding" select="$padding" />
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:template>

<!-- split a string to a specified size.
Note: there is no newline at the end of the last line to allow additions to the end of the line

string: string to split
length: max length string can be per line
prefix: optional string to prepend to the first line
hangingPrefix: optional string to prepend subsequent lines with
suffix: optional string to append to the end of the first line
hangingSuffix: optional string to append to subsequent lines
padding: optional string to pad the last line of a splt string with to allow extra text to be added after it
-->
<xsl:template name="splitString">
<xsl:param name="string" />
<xsl:param name="length" select="$maxLineLength" />
<xsl:param name="prefix" />
<xsl:param name="hangingPrefix" select="$prefix" />
<xsl:param name="suffix" />
<xsl:param name="hangingSuffix" select="$suffix" />
<xsl:param name="padding" />

<xsl:choose>
<xsl:when test="(string-length($prefix) +
string-length($string) +
string-length($suffix)) > $length">
<xsl:variable name="currentpos" select="$length - string-length($suffix)" />
<xsl:choose>
<xsl:when test="contains(substring($string, 1, $currentpos), ' ') or
contains(substring($string, 1, $currentpos), '/')">
<xsl:call-template name="firstString">
<xsl:with-param name="string" select="concat($prefix, $string)" />
<xsl:with-param name="length" select="$length" />
<xsl:with-param name="currentpos" select="$currentpos" />
<xsl:with-param name="prefix" select="$prefix" />
<xsl:with-param name="hangingPrefix" select="$hangingPrefix" />
<xsl:with-param name="suffix" select="$suffix" />
<xsl:with-param name="hangingSuffix" select="$hangingSuffix" />
<xsl:with-param name="padding" select="$padding" />
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<!-- no line split character within maximum line length -->
<xsl:value-of select="$prefix" />
<xsl:value-of select="substring($string, 1, $currentpos - string-length($prefix))" />
<xsl:value-of select="$suffix" />
<xsl:value-of select="$newline" />

<xsl:call-template name="splitString">
<xsl:with-param name="string" select="substring($string, $currentpos + 1 - string-length($prefix))" />
<xsl:with-param name="length" select="$length" />
<xsl:with-param name="prefix" select="$hangingPrefix" />
<xsl:with-param name="hangingPrefix" select="$hangingPrefix" />
<xsl:with-param name="suffix" select="$hangingSuffix" />
<xsl:with-param name="hangingSuffix" select="$hangingSuffix" />
<xsl:with-param name="padding" select="$padding" />
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:when>

<!-- less than $length chars left, just write them -->
<xsl:otherwise>
<xsl:value-of select="$prefix" />
<xsl:value-of select="$string" />
<xsl:value-of select="$suffix" />
<xsl:value-of select="substring($padding, 1, ($length - string-length($prefix) - string-length($string) - string-length($suffix))) " />
</xsl:otherwise>
</xsl:choose>
</xsl:template>

Sorting groups in XSL

For some of my recent work I had to generate reports from XML using XSL. One of the techniques I had to use frequently was grouping data using the Muenchian Method.

The referenced web page shows an example of getting a group of contacts and sorting it by surname. That's all well and good if your only computing the group once and processing it straight-forward as shown. But what if the XML data is large so calculating the group is costly (in terms of memory and CPU) and you need to use it multiple times and the order is important because you're using it within the context of processing some larger processing loop (ex. 10 at a time associated with another value)?

For an example, let's just say you need to generate a report that has a cover sheet listing all the unique surnames in your file, then separate statistics for each surname such as how many people share that surname. This is a very contrived example, but at least it will demonstrate what I mean.

Using the example in the referenced web page, we could build a list of unique sorted surnames to use in various places in our XSL via:


<xsl:key name="contacts-by-surname" match="contact" use="surname"/>
<xsl:variable name="surnames" select="contact[count(. | key('contacts-by-surname', surname)[1]) = 1]/surname"/>

<xsl:template match="/">
<xsl:call-template name="sortList">
<xsl:with-param name="list" select="$surnames"/>
</xsl:call-template>

<!-- list is now sorted to use whenever necessary -->
<xsl:for-each select="$surnames">
[process sorted surname]
</xsl:for-each>
</xsl:template>


<!-- sort list by the value of its current node -->
<xsl:template name="sortList">
<xsl:param name="list"/>
<xsl:for-each select="$list">
<xsl:sort select="." />
</xsl:for-each>
</xsl:template>

The call to sortList will sort it for all subsequent uses.

So, to see it in action, here is our sample data:

<records>
<contact id="0001">
<title>Mr</title>
<forename>John</forename>
<surname>Smith</surname>
</contact>
<contact id="0002">
<title>Dr</title>
<forename>Amy</forename>
<surname>Jones</surname>
</contact>
<contact id="0003">
<title>Mr</title>
<forename>John</forename>
<surname>Doe</surname>
</contact>
<contact id="0004">
<title>Mr</title>
<forename>Jack</forename>
<surname>Smith</surname>
</contact>
<contact id="0005">
<title>Mr</title>
<forename>Mike</forename>
<surname>Jordan</surname>
</contact>
<contact id="0006">
<title>Mr</title>
<forename>Jim</forename>
<surname>Johnson</surname>
</contact>
<contact id="0007">
<title>Mr</title>
<forename>Randy</forename>
<surname>Jones</surname>
</contact>
<contact id="0008">
<title>Mr</title>
<forename>Bobby</forename>
<surname>Jones</surname>
</contact>
<contact id="0009">
<title>Mr</title>
<forename>Glenn</forename>
<surname>Smith</surname>
</contact>
<contact id="0010">
<title>Mr</title>
<forename>Bill</forename>
<surname>McDonnald</surname>
</contact>

</records>


Here is the complete style sheet:

<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" indent="no" omit-xml-declaration="yes" />

<xsl:variable name="newline">
<!-- Hex 10 is a line feed, and hex 13 is a carriage return -->
<xsl:value-of select="'&#13;'" />
</xsl:variable>

<!-- Remove all whitespace from the source XML so that it is not echoed into the new report -->
<xsl:strip-space elements="//*" />

<xsl:key name="contacts-by-surname" match="contact" use="surname" />
<xsl:variable name="surnames" select="//contact[count(. | key('contacts-by-surname', surname)[1]) = 1]/surname" />

<xsl:template match="/">
<xsl:text>Full list of surnames:</xsl:text>
<xsl:value-of select="$newline" />
<xsl:for-each select="//surname">
<xsl:value-of select="." />
<xsl:value-of select="' '" />
</xsl:for-each>
<xsl:value-of select="$newline" />
<xsl:value-of select="$newline" />

<xsl:text>List of unique surnames:</xsl:text>
<xsl:value-of select="$newline" />
<xsl:for-each select="$surnames">
<xsl:value-of select="." />
<xsl:value-of select="' '" />
</xsl:for-each>
<xsl:value-of select="$newline" />
<xsl:value-of select="$newline" />

<xsl:call-template name="sortList">
<xsl:with-param name="list" select="$surnames" />
</xsl:call-template>

<!-- list is now sorted to use whenever necessary -->
<xsl:text>List of unique surnames (sorted):</xsl:text>
<xsl:value-of select="$newline" />
<xsl:for-each select="$surnames">
<xsl:value-of select="." />
<xsl:value-of select="' '" />
</xsl:for-each>
<xsl:value-of select="$newline" />
<xsl:value-of select="$newline" />

<xsl:text>People per surname:</xsl:text>
<xsl:value-of select="$newline" />
<xsl:for-each select="$surnames">
<xsl:variable name="surname" select="."/>
<xsl:value-of select="$surname" />
<xsl:value-of select="' ('" />
<xsl:value-of select="count(//contact[surname = $surname])" />
<xsl:value-of select="'):'" />
<xsl:value-of select="$newline" />
<xsl:for-each select="//contact[surname = $surname]">
<xsl:sort select="forname"/>
<xsl:value-of select="' '" />
<xsl:value-of select="forename" />
<xsl:value-of select="$newline" />
</xsl:for-each>
<xsl:value-of select="$newline" />
</xsl:for-each>


</xsl:template>

<!-- sort list by the value of its current node -->
<xsl:template name="sortList">
<xsl:param name="list" />
<xsl:for-each select="$list">
<xsl:sort select="." />
</xsl:for-each>
</xsl:template>

</xsl:stylesheet>


And here is the report:

Full list of surnames:
Smith Jones Doe Smith Jordan Johnson Jones Jones Smith McDonnald

List of unique surnames:
Smith Jones Doe Jordan Johnson McDonnald

List of unique surnames (sorted):
Doe Johnson Jones Jordan McDonnald Smith

People per surname:
Doe (1):
John

Johnson (1):
Jim

Jones (3):
Amy
Randy
Bobby

Jordan (1):
Mike

McDonnald (1):
Bill

Smith (3):
John
Jack
Glenn