Complex XSLT with parameters passed from the URL

I have an xml file of the following structure

<document type="x">
    <title>Doc 1</title>
    <author>Joe Bloggs</author>
</document>

<document type="y">
    <title>Who Cares</title>
    <author>No one</author>
</document>

<category category="cat1">
    <document type="x">
        <title>Doc 2</title>
        <author>John Doe</author>
    </document>

    <category category="cat2">
         <document type="x">
            <title>More Docs</title>
            <author>A N Other</author>
         </document>
    </category>

    <document type="x">
        <title>Doc 3</title>
        <author>Jane Doe</author>
    </document>

    <document type="y">
        <title>Who Cares Vol 2</title>
        <author>No One</author>
    </document>
</category>

I want to use XSLT to convert this into a page which will display documents of type 'z' where the URL is www.blah.com/documents.xml?type=z&category=.
Furthermore I wish to list the documents in a definition list of the form
<dl>
    <dt>Doc 1</dt>
    <dd>Joe Bloggs</dd>
    <a href="www.blah.com/documents.xml?type=z&category=cat1">cat1</a> &gt;
    <dl>
        <dt>Doc 2</dt>
        <dd>John Doe</dd>
        <dt>Doc 3</dt>
        <dd>Jane Doe</dd>
        <a href="www.blah.com/documents.xml?type=z&category=cat1">cat1</a> &gt; <a href="www.blah.com/documents.xml?type=z&category=cat2">cat2</a> &gt;
        <dl>
            <dt>More docs</dt>
            <dd>A N Other</dd>
        </dl>
    </dl>
</dl>

It is important that categories can have unlimited levels of subcategory and a clickable breadcrumb is displayed as shown below to allow users to drill down to only display the data they require.


Doc 1
Joe Bloggs

cat1 >
     Doc 2
     John Doe

     Doc 3
     Jane Doe
cat1 > cat2 >
          More Docs
          A N Other
LVL 4
Neil_SimpsonAsked:
Who is Participating?
 
conorjConnect With a Mentor Commented:
Neil,

have you tried changing

<xsl:apply-templates select="//category[@category = $category]" />

to

<xsl:apply-templates select="//category[@category = $category][document/@type = $type]" />

rgds,
Conor.
0
 
conorjCommented:
Neil,

The following stylesheet should get you on your way:

.................
<xsl:param name="type" select="'x'" />
<xsl:param name="category" select="'cat1'" />
<!--      Top Level Variables      -->

<!--      Template Matches      -->
<xsl:template match="/">
    <html>
        <head></head>
        <body>
            <dl>
                <xsl:apply-templates select="root/document[@type = $type]" />
            </dl>
        </body>
    </html>
</xsl:template>

<xsl:template match="document">
    <dt><xsl:value-of select="title" /></dt>
    <dd><xsl:value-of select="author" /></dd>
    <xsl:variable name="isCatParam" select="($category = '') or (($category != '') and (..//category/@category = $category))" />
    <xsl:if test="(position() = last()) and $isCatParam">
        <xsl:apply-templates select="../category[document/@type = $type]" />
    </xsl:if>
</xsl:template>

<xsl:template match="category">
    <xsl:for-each select="ancestor::category">
        <a href="http://www.blah.com/documents.xml?type={$type}&amp;category={@category}"><xsl:value-of select="@category" /></a> >
    </xsl:for-each>
    <a href="http://www.blah.com/documents.xml?type={$type}&amp;category={@category}"><xsl:value-of select="@category" /></a> >
    <xsl:variable name="subdocs" select="document[@type = $type]" />
    <xsl:if test="$subdocs">
        <dl>
            <xsl:apply-templates select="$subdocs" />
        </dl>
    </xsl:if>
</xsl:template>

.................

HTH.

rgds,
Conor.
0
 
Neil_SimpsonAuthor Commented:
Thanks, this works well to create the list of previous categories. There are two problems which I have found so far.

Firstly, in <xsl:template match="/"> I cannot use <xsl:apply-templates select="root/document[@type = $type]" /> as this prevents any output from being displayed. This was fixed by changing <xsl:template match="document"> to <xsl:template match="document[@type = $type]">.

Secondly, if I change the category variable to a different category it still displays all of the other information. If for example <xsl:param name="category" select="'cat 2'" /> then all of the documents without a category AND the documents in 'cat 1' are still displayed.

If this can be solved then this solution solves the first part. I still need to know how to set the variables $category and $type from those passed in on the URL.
0
Cloud Class® Course: CompTIA Cloud+

The CompTIA Cloud+ Basic training course will teach you about cloud concepts and models, data storage, networking, and network infrastructure.

 
Yury_DelendikCommented:
Second part of solution depend on where transformation will occur on server or client and what web server (or client browser) will perform this
0
 
Neil_SimpsonAuthor Commented:
This in an in-house issue so the target browser is Mozilla 1.4 and the server (from the returned HTTP header) is "Server: Netscape-Enterprise/6.0". I do not mind whether the transformation occurs server side or client side. I know that I could use javascript and DHTML to display only approopriate <div>s  based upon a full listing of the documents but I would rather use XML/XSLT if/where possible so that I get some exposure to this as the site will make greater use of this as time goes on and new sections/applications are developed.
0
 
conorjCommented:
Neil,

the reason I put root into the XPath expression is that your XML is not well formed as it does not have a root element. So i wrapped your XML inside an element named "root" for testing purposes.

the following XSLT should do what you want. I have used a key to test if there is a category document with the right document type.
...................
<xsl:key match="category" name="cats" use="@category" />

<xsl:param name="type" select="'x'" />
<xsl:param name="category" select="'cat2'" />
<!--     Top Level Variables     -->

<!--     Template Matches     -->
<xsl:template match="/">
    <html>
        <head></head>
        <body>
            <dl>
                <xsl:choose>
                    <xsl:when test="$category = ''">
                        <xsl:apply-templates select="root/document[@type = $type]" />
                    </xsl:when>
                    <xsl:otherwise>
                        <xsl:if test="key('cats', $category)/document[@type = $type]">
                            <xsl:apply-templates select="root/document[@type = $type]" />
                        </xsl:if>
                    </xsl:otherwise>
                </xsl:choose>
            </dl>
        </body>
    </html>
</xsl:template>

<xsl:template match="document">
    <dt><xsl:value-of select="title" /></dt>
    <dd><xsl:value-of select="author" /></dd>
    <xsl:variable name="isCatParam" select="($category = '') or (($category != '') and (..//category/@category = $category))" />
    <xsl:if test="(position() = last()) and $isCatParam">
        <xsl:apply-templates select="../category[document/@type = $type]" />
    </xsl:if>
</xsl:template>

<xsl:template match="category">
    <xsl:for-each select="ancestor::category">
        <a href="http://www.blah.com/documents.xml?type={$type}&amp;category={@category}"><xsl:value-of select="@category" /></a> >
    </xsl:for-each>
    <a href="http://www.blah.com/documents.xml?type={$type}&amp;category={@category}"><xsl:value-of select="@category" /></a> >
    <xsl:variable name="subdocs" select="document[@type = $type]" />
    <xsl:variable name="selDocs" select="($category = '') or (@category = $category)" />
    <xsl:choose>
        <xsl:when test="$subdocs and $selDocs">
            <dl>
                <xsl:apply-templates select="$subdocs" />
            </dl>
        </xsl:when>
        <xsl:when test="$category != ''">
            <dl>
                <xsl:apply-templates select="category[document/@type = $type]" />
            </dl>
        </xsl:when>
    </xsl:choose>
</xsl:template>
.......................

As Yury said the method of passing parameters to the stylesheet depends upon the transformation environment.

HTH.

rgds,
Conor.
0
 
Neil_SimpsonAuthor Commented:
Hi Conor,

Thanks for your help. This solution appeared to add some unnnecessary complexity (although I won't pretend to fully understand your use of <xsl:key>). I found that by adding
<xsl:choose>
      <xsl:when test="($category = '')">
            <xsl:apply-templates select="documents/document[@type = $type]" />
      </xsl:when>
      <xsl:otherwise>
            <xsl:apply-templates select="//category[@category = $category]" />
      </xsl:otherwise>
</xsl:choose>
to the <xsl:template match="/"> and
<xsl:apply-templates select="child::category" />
to the <xsl:template match="category"> that it worked correctly.

Can anyone suggest a way to pass the parameters in from the URL based upon the fact that I am just using mozilla to display the XML file so the transformation is taking place within the browser I'd assume? I'm incredibly close to getting this working successfully it's just this that is holding me up. Another problem I can forsee is the inclusion of the SSIs which I use for the site menus and layout. Is it possible to dump the contents of an external file into the output during the transformation?

PS: I did actually have a root node called documents but I forgot to add it in the example, whoops. :o)

Anyway, thanks again, if I can just get this last
0
 
Yury_DelendikConnect With a Mentor Commented:
what programming language you are using?
0
 
Neil_SimpsonAuthor Commented:
I'm now using an HTML file with a Javascript to apply the variables I require and it all works fantastically well. This avoids the SSI problem I would ave encountered as I can just have this in the HTML wrapper file. I have included the output below. Just before I award the points there is one remaining niggle in the XSLT; it prints out category names even if no documents within the category match the specified type? Any final nuggets of XSLT wizardry to prevent me from using my own brain to think about this?

<script>
var xslStylesheet;
var xsltProcessor = new XSLTProcessor();
var myDOM;

var xmlDoc;

function Init() {
      // load the xslt file, example1.xsl
      var myXMLHTTPRequest = new XMLHttpRequest();
      myXMLHTTPRequest.open("GET", "1pager_styles.xsl", false);
      myXMLHTTPRequest.send(null);

      xslStylesheet = myXMLHTTPRequest.responseXML;
      xsltProcessor.importStylesheet(xslStylesheet);

      // load the xml file, example1.xml
      myXMLHTTPRequest = new XMLHttpRequest();
      myXMLHTTPRequest.open("GET", "documents.xml", false);
      myXMLHTTPRequest.send(null);

      xmlDoc = myXMLHTTPRequest.responseXML;
      //Get URL parameters
      var x = 0
      URLParams = location.search.substr(1).split("&");
      for (x=0;x<=URLParams.length-1;x++) {
            singleParam = URLParams[x].split("=");
            xsltProcessor.setParameter(null, singleParam[0], unescape(singleParam[1]));
      }
      var fragment = xsltProcessor.transformToFragment(xmlDoc, document);      

      document.getElementById("example").innerHTML = "";

      myDOM = fragment;
      document.getElementById("example").appendChild(fragment)        
}

</script>  
0
 
Neil_SimpsonAuthor Commented:
Well, I think I've almost cracked it. All I need to know is:
1) how to display sub-categories when they have a file of @type=$type but their parent category does not.
2) an elegant way to select all categories or types when a value is not specified, I had wondered if it was possible to use a wildcard as the variable value but this did not appear to work (although it may well have been my implementation of this that didn't work). I know it is possible to have a choose and then 4 options so all possibilities of $category and/or $type = '' but this needs to be repeated frequently throughout the sheet and is obviously ineffiecient. Also, this would not scale well at all if I were to allow any other parameter to be passed on the URL.
0
 
Neil_SimpsonAuthor Commented:
Anybody?
0
 
Yury_DelendikCommented:
0
 
Neil_SimpsonAuthor Commented:
Thanks to a seperate question I've managed to solve problem 2) above. I shall investigate problem 1 now.
0
 
Yury_DelendikCommented:
<xsl:for-each select="//category[ document[@type = $type] and not(ancestor::category[document[@type = $type]]) ]">
0
 
Yury_DelendikCommented:
To do both $category and $type and they may be empty (or null, nothing):

"//category[ not(@category != $category) and document[not(@type != $type)] ][not(ancestor::category[ not(@category != $category) and document[not(@type != $type)] ]) ]"
0
 
Neil_SimpsonAuthor Commented:
I have changed category to refer to an id attribute and based upon your answer to the other question I have the select as:

"//category[ (@id = $id) and document[not($type) or @type = $type] ][not(ancestor::category[ (@id = $id) and document[not($type) or @type = $type] ])]"

The thing is, if I say type=other and do not specify an ID (the default for the id is the ID of the root category) then I don not see files of type other which are in a sub-category when the parent category does not contain a document of type=other.
0
 
Neil_SimpsonAuthor Commented:
It looks like this does the trick:

"//category[(document[not($type) or @type = $type]) and (@id = $id)]|//category[@id = $id]//category[document[not($type) or @type = $type]]"

Thanks for your help everyone. I'll split the point between Conor and Yury
0
Question has a verified solution.

Are you are experiencing a similar issue? Get a personalized answer when you ask a related question.

Have a better answer? Share it in a comment.

All Courses

From novice to tech pro — start learning today.