Detecting if a node exists? - xslt

I have a set of data called <testData> with many nodes inside.
How do I detect if the node exists or not?
I've tried
<xsl:if test="/testData">
and
<xsl:if test="../testData">
Neither one works. I'm sure this is possible but I'm not sure how. :P
For context the XML file is laid out like this
<overall>
<body/>
<state/>
<data/>(the one I want access to
</overall>
I'm currently in the <body> tag, though I'd like to access it globally. Shouldn't /overall/data work?
Edit 2:
Right now I have an index into data that I need to use at anytime when apply templates to the tags inside of body. How do I tell, while in body, that data exists? Sometimes it does, sometimes it doesn't. Can't really control that. :)

Try count(.//testdata) > 0.
However if your context node is textdata and you want to test whether it has somenode child or not i would write:
<xsl:if test="somenode">
...
</xsl:if>
But I think that's not what you really want. I think you should read on different techniques of writing XSLT stylesheets (push/pull processing, etc.). When applying these, then such expressions are not usually necessary and stylesheets become simplier.

This XSLT:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:template match="text()"/> <!-- for clarity only -->
<xsl:template match="body">
<xsl:if test="following-sibling::data">
<xsl:text>Data occurs</xsl:text>
</xsl:if>
<xsl:if test="not(following-sibling::data)">
<xsl:text>No Data occurs</xsl:text>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
Applied to this sample:
<overall>
<body/>
<state/>
<data/>(the one I want access to
</overall>
Will produce this correct result:
Data occurs
When applied to this sample:
<overall>
<body/>
<state/>
</overall>
Result will be:
No Data occurs

This will work with XSL 1.0 if someone needs...
<xsl:choose>
<xsl:when test="/testdata">node exists</xsl:when>
<xsl:otherwise>node does not exists</xsl:otherwise>
</xsl:choose>

Related

How can I access an xsl:variable from outside the "scope" of an xsl:for-each block

I'm trying to access a variable from within a for-each. I've done a lot of reading today to try to figure this out, but nothing quite fits my scenario. Eventually, I will have multiple series like seen below and I will use the variables that I'm pulling out and make different condition. The for-each that I have below is bringing back data from 400 records. I'm sorry, I cannot provide an XML. I'm not able to expose GUIDs and such.
UPDATE
<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output media-type="xml" indent="yes"/>
<xsl:template match="/">
<Records>
<xsl:for-each select="Records/Record/Record[#levelGuid = 'level1']">
<xsl:variable name="rocName1" select="Field[#guid = '123']"/>
<xsl:variable name ="rocName2" select="substring-before($rocName1, ' - ')"/>
</xsl:for-each>
<xsl:for-each select="Records/Record/Record[#levelGuid = 'levelA']">
<xsl:variable name ="findingName" select="Field[#guid = '123']"/>
<xsl:variable name="findingName1" select="substring-after($findingName, ': ')"/>
<xsl:variable name="findingName2" select="substring-after($findingName1, 'PCIDSSv3.1:')"/>
</xsl:for-each>
<xsl:if test="$findingName1 = $rocName1">
<Records>
<findingID>
<xsl:for-each select="Records/Record/Record[#levelGuid = '123']">
<xsl:value-of select ="Field[#guid = '123']"/>
</xsl:for-each>
</findingID>
</Records>
</xsl:if>
</Records>
</xsl:template>
</xsl:stylesheet>
The desired output is any findingID that has a $findingName1 that equals $rocName1. The GUIDS only appear once, but each level has hundreds of records.
I'm trying to access a variable from within a for-each.
The variable $rocRecord is in scope inside the for-each. You can simply reference it and use it.
But I think you are trying to do something else. I.e., defining a variable inside for-each and wanting to use it outside it.
Variables are scoped within their focus-setting containing block. So the short answer is: you cannot do it. The long answer however...
Use templates. The only reason to do what you seem to want to be doing is to need to access the data elsewhere:
<xsl:template match="/">
<!-- in fact, you don't need for-each at all, but I leave it in for clarity -->
<xsl:for-each select="records/record">
<!--
apply-templates means: call the declared xsl:template that
matches this node, it is somewhat similar to a function call in other
languages, except that it works the other way around, the processor will
magically find the "function" (i.e., template) for you
-->
<xsl:apply-templates select="Field[#guid='123']" />
</xsl:for-each>
</xsl:template>
<xsl:template match="Field">
<!-- the focus here is what is the contents of your variable $rocName1 -->
<rocName>
<xsl:value-of select="substring=-before(., ' - ')" />
</rocName>
</xsl:template>
XSLT is a declarative, template-oriented, functional language with concepts that are quite unique compared to most other languages. It can take a few hours to get used to it.
You said you did a lot of reading, but perhaps it is time to check a little XSLT course? There are a few online, search for "XSLT fundamentals course". It will save you hours / days of frustration.
This is a good, short read to catch up on variables in XSLT.
Update
On second read, I think it looks like you are troubled by the fact that the loop goes on for 400 items and that you only want to output the value of $rocName1. The example I showed above, does exactly that, because apply-templates does nothing if the selection is empty, which is what happens if the guid is not found.
If the guid appears once, the code above will output it once. If it appears multiple times and you only want the first, append [1] to the select statement.
Update #2 (after your update with an example)
You have two loops:
<xsl:for-each select="Records/Record/Record[#levelGuid = 'level1']">
and
<xsl:for-each select="Records/Record/Record[#levelGuid = 'levelA']">
You then want to do something (created a findingId) when a record in the first loop matches a record in the second loop.
While you can solve this using (nested) loops, it is not necessary to do so, in fact, it is discouraged as it will make your code hard to read. As I explained in my original answer, apply-templates is usually the easier way to do get this to work.
Since the Record elements are siblings of one another, I would tackle this as follows:
<xsl:template match="/">
<Records>
<xsl:apply-templates select="Records/Records/Record[#levelGuid = 'level1']" />
</Records>
</xsl:template>
<xsl:template match="Record">
<xsl:variable name="rocName1" select="Field[#guid = '123']"/>
<xsl:variable name ="rocName2" select="substring-before($rocName1, ' - ')"/>
<xsl:variable name="findingNameBase" select="../Record[#levelGuid = 'levelA']" />
<xsl:variable name ="findingName" select="$findingNameBase/Field[#guid = '123']"/>
<xsl:variable name="findingName1" select="substring-after($findingName, ': ')"/>
<xsl:variable name="findingName2" select="substring-after($findingName1, 'PCIDSSv3.1:')"/>
<findingId rocName="{$rocName1}">
<xsl:value-of select="$findingName" />
</findingId>
</xsl:template>
While this can be simplified further, it is a good start to learn about applying templates, which is at the core of anything you do with XSLT. Learn about applying templates, because without it, XSLT will be very hard to understand.

not(#attribute) test not working in JDom XSL transform?

I have a piece of an XSLT stylesheet that works as expected using xsltproc but produces a different output in my actual application, where the transform is applied via org.jdom.transform.XSLTransformer (jdom 1.0), I believe using Xalan.
Stylesheet snippet (this is part of a larger template that starts like this: <xsl:template match="/dspace:dim[#dspaceType='ITEM']">):
<xsl:if test="//dspace:field[#mdschema='dc' and #element='rights']">
<rightsList>
<xsl:if test="//dspace:field[#mdschema='dc' and #element='rights' and not(#qualifier) and #language='*']">
<rights>
<xsl:if test="//dspace:field[#mdschema='dc' and #element='rights' and #qualifier='uri' and #language='*']">
<xsl:attribute name="rightsUri">
<xsl:value-of select="//dspace:field[#mdschema='dc' and #element='rights' and #qualifier='uri' and #language='*']"/>
</xsl:attribute>
</xsl:if>
<xsl:value-of select="//dspace:field[#mdschema='dc' and #element='rights' and not(#qualifier) and #language='*']" />
</rights>
</xsl:if>
<xsl:apply-templates select="//dspace:field[#mdschema='dc' and #element='rights' and not(#language='*')]" />
</rightsList>
</xsl:if>
and
<xsl:template match="//dspace:field[#mdschema='dc' and #element='rights' and not(#language='*')]">
<rights><xsl:value-of select="." /></rights>
</xsl:template>
XML snippet:
<dim:dim dspaceType="ITEM" xmlns:dim="http://www.dspace.org/xmlns/dspace/dim">
<dim:field element="rights" language="en_NZ" mdschema="dc">Actual text redacted</dim:field>
<dim:field element="rights" language="*" mdschema="dc">Attribution 3.0 New Zealand</dim:field>
<dim:field element="rights" qualifier="uri" language="*" mdschema="dc">http://creativecommons.org/licenses/by/3.0/nz/</dim:field>
</dim:dim>
With xsltproc, this produces
<rightsList>
<rights rightsUri="http://creativecommons.org/licenses/by/3.0/nz/">Attribution 3.0 New Zealand</rights>
<rights>Actual text redacted</rights>
</rightsList>
In my application, this produces
<rightsList>
<rights>Actual text redacted</rights>
<rights>Attribution 3.0 New Zealand</rights>
<rights>http://creativecommons.org/licenses/by/3.0/nz/</rights>
</rightsList>
So to me it looks like the not(#qualifier) bit doesn't work using jdom.
I'd appreciate any insight into what's going on here and how I might change the stylesheet to get the same result in my application that I currently get via xsltproc.
Edited to add: just in case it makes any difference, the stylesheet starts out as
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:dspace="http://www.dspace.org/xmlns/dspace/dim"
xmlns:exslt="http://exslt.org/common"
xmlns="http://datacite.org/schema/kernel-3"
extension-element-prefixes="exslt"
exclude-result-prefixes="exslt"
version="1.0">
and also includes this template:
<!-- Don't copy everything by default! -->
<xsl:template match="#* | text()" />
See my answer below the XML structure is actually different from what I thought it was, so the problem wasn't in the XSL after all.
Apart from solving your original problem, let's have a quick look at how to reorganize your code.
You use a lot of //foo expressions. Starting an expression with //foo means "search the whole document, at any level, for the element with the name foo". Apart from this being a potentially expensive operation, this often has unwanted side effects and makes your code hard to read, because it requires you to specify each element uniquely, leading to a lot of duplicated code.
You also use a lot of xsl:if, but in XSLT, it is hardly ever necessary to use if-statements (an exception in XSLT 1.0 and 2.0 being when you deal with something other than nodes). In almost all cases, you can replace an xsl:if with a simple xsl:apply-templates.
That said, let's have a look how we can rewrite your code to get the same effect and have less chance for error:
<xsl:if test="//dspace:field[#mdschema='dc' and #element='rights']">
<rightsList>
.....
Is similar to having a matching template as follows (assuming you have a throw-away template for uninteresting nodes):
<xsl:template match="dspace:dim[dspace:field[#mdschema='dc' and #element='rights']]">
<rightsList>
This says: if you encounter a dim element with any field element that has those properties set, then output <rightsList>.
Then you have:
<xsl:if test="//dspace:field[#mdschema='dc' and #element='rights' and not(#qualifier) and #language='*']">
<rights>
Which is precisely equivalent to the following apply-template expression (assuming a matching template with it):
<xsl:apply-templates select="dspace:field[#mdschema='dc' and #element='rights' and not(#qualifier) and #language='*']" />
Here we find that a little bit below that, we have an almost equivalent expression, this time with not(#language='*'). So let's see if we can get rid of those duplicate expressions altogether.
First, let's go back a bit and have a look at what you were doing:
If anywhere any "dc" and "rights", then create a <rightsList>
If anywhere any of these have do not have a qualifier but have a language "*", create <rights>
Inside this, create an attribute rightsUri if anywhere any qualifier has value "uri" and language "*", set its value to the first such you find
After this <rights> element (there can be at most one of them in your current structure), create a list of <rights> for each field element with language "*"
If this is correct, then this can be rewritten as follows:
<xsl:template match="dspace:dim[dspace:field[#mdschema='dc' and #element='rights']]">
<xsl:variable name="adjusted">
<xsl:copy-of select="dspace:field[#mdschema='dc' and #element='rights']"/>
</xsl:variable>
<rightsList>
<xsl:apply-templates select="exsl:node-set($adjusted)/*[not(#qualifier) and #language='*'][1]" mode="noquali"/>
<xsl:apply-templates select="exsl:node-set($adjusted)/*[not(#language='*')]" />
</rightsList>
</xsl:template>
<xsl:template match="dspace:field" mode="noquali">
<rights>
<xsl:apply-templates select="/dspace:field[#qualifier='uri' and #language='*'][1]" mode="uri"/>
<xsl:value-of select="."/>
</rights>
</xsl:template>
<xsl:template match="dspace:field" mode="uri">
<xsl:attribute name="rightsUri" select="." />
</xsl:template>
<!-- matching anything else -->
<xsl:template match="dspace:field">
<rights><xsl:value-of select="." /></rights>
</xsl:template>
The exsl:node-set function is supported by just about every XSLT 1.0 processor, just add the namespace xmlns:exsl="http://exslt.org/common" to your xsl:stylesheet declaration.
Note that I added a few times [1] to the select-expressions. While you don't do that in your code, your current code has the same effect, but if you use apply-templates, if you encounter multiple matches, you have to specify that you are only interested in the first match.
I think your code can be further simplified, but I wanted to make sure that the logic remains exactly the same. As you can see, the end result is without any //. However, you do see one /, which is now pointing to the root of the node-set, which conveniently only has the nodes you are interested in: the ones with schema "dc" and "rights" element attributes, so we do not have to repeat that expression over and over again.
You may try this rewrite and see if it helps with your current bug, otherwise I'll gladly to help you further.
Edit
After your edit, your original context item will have been dspace:dim already. If you don't mind always outputting <rightsList> (even if it ends up empty), you can simply replace my first template match pattern above with your existing dspace:dim pattern.
Duh. Forest/trees indeed. Even though the language attribute is called "language" pretty much everywhere else in the application (see also, the XML snippet I gave), it is actually called "lang" in the XML that my stylesheet operates on - I finally gave in and used this answer to be sure what the XML structure is. Surprise!
Anyway, I followed the advice Abel gave in his answer in part and simplified the templates for this particular case quite a bit. I now just have
<xsl:if test="dspace:field[#mdschema='dc' and #element='rights']">
<rightsList>
<xsl:apply-templates select="dspace:field[#mdschema='dc' and #element='rights']"/>
</rightsList>
</xsl:if>
in the big template, plus a couple of custom ones:
<xsl:template match="dspace:field[#mdschema='dc' and #element='rights']">
<xsl:choose>
<xsl:when test="#qualifier='uri'"/>
<xsl:otherwise>
<rights>
<xsl:if test="#lang='*'">
<xsl:apply-templates select="//dspace:field[#mdschema='dc' and #element='rights' and #qualifier='uri' and #lang='*'][1]" mode="rightsURI"/>
</xsl:if>
<xsl:value-of select="."/>
</rights>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template match="dspace:field[#mdschema='dc' and #element='rights' and #qualifier='uri' and #lang='*']" mode="rightsURI">
<xsl:attribute name="rightsURI"><xsl:value-of select="."/></xsl:attribute>
</xsl:template>

Xslt - How do you check for a grandchild node with a certain path name. (xpath 1.0)

What I want to do is given an element as context, I want to determine if it has a child with a given name and determine if that child has a node with a given name so I can do operations with it. It is important that I do this in XPath 1.0 syntax.
The code that I've gotten so far is this.
<xsl:for-each select="child::*">
<xsl:if test="contains(name(), 'description')">
<xsl:for-each select="child::*">
<xsl:if test="contains(name(), 'text')">
<xsl:value-of select="node()"/>
</xsl:if>
</xsl:for-each>
</xsl:if>
</xsl:for-each>
It works, but it's big and ugly and I know that there's a way to condense it. The for-eachs there are unnecessary, since I'm only expecting one child node to be named description, and for it to only have one text node.
I feel like this solution should work
<xsl:for-each select="./description/text">
..
</xsl:for-each>
But it isn't, and I'm not really good enough with XPath Syntax to know why.
The reason I'm asking is because though I've found answers that detect whether a child node has a name, and I've found answers that can get to that child node's context, I haven't found an answer that combines the two, though maybe I just haven't been searching hard enough, in which case I apologize.
Edit: Woops, sorry yeah I forgot to mention that the contains() part of the code was also just a hack because I wasn't sure how to compare their values with equality.
Also as long as the answer is there, <xsl:for-each select="description/text"> does not work either.
A sample of the XML in question is this
<leaf>
<description>
<text> Various Words
</text>
</description>
</leaf>
where the context is the leaf and I am trying to get to the text node.
Edit: The Second Coming:
The problem for me was that my XSLT file was using a default namespace (in my case named a). If I had added that then Borodin's answer would have been correct.
To be specific, this is the code which ended up working for me in the end, in case anyone wants to know.
<xsl:for-each select="a:description/a:text>
<xsl:value-of select="node()"/>
</xsl:for-each>
Thanks Guys ^-^
Do you really want to check whether the element names contain those strings? Or, as your narrative says, do you want elements with that exact name?
To do something like what you have already written, use
<xsl:for-each select="*[contains(name(), 'description')]/*[contains(name(), 'text')]">
<xsl:value-of select="node()"/>
</xsl:for-each>
But if you know the complete names it is a lot neater:
<xsl:for-each select="description/text">
<xsl:value-of select="node()"/>
</xsl:for-each>
If that doesn't work then we need to see more of your source XML and your transform.
Update
If I use this XML
<leaf>
<description>
<text>Various Words</text>
</description>
<description>
<text>More Words</text>
</description>
<description>
<text>Other Words</text>
</description>
</leaf>
and apply this stylesheet
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:template match="/leaf">
<xsl:for-each select="description/text">
<xsl:value-of select="."/>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
the output is the expected Various WordsMore WordsOther Words. I don't know how to help you unless you describe your situation better, except to say that transforms should be written with another template rather than for-each wherever possible. Like this variation which produces the same output as above.
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:template match="/leaf">
<xsl:apply-templates select="description/text"/>
</xsl:template>
<xsl:template match="text">
<xsl:value-of select="."/>
</xsl:template>
</xsl:stylesheet>

XSLT: XPath context and document()

I have an XSLT like this:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:transform version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xalan="http://xml.apache.org/xalan">
<xsl:variable name="fooDocument" select="document('fooDocument.xml')"/>
<xsl:template match="/">
<xsl:apply-templates select="$fooDocument//*"/>
</xsl:template>
<xsl:template match="nodeInFooDocument">
<xsl:variable name="valueFromSource" select="//someSourceElement"/>
</xsl:template>
</xsl:transform>
In the second template, which matches nodes in the fooDocument.xml which is loaded with document(), I want to access nodes in the XML source the transformation is executed upon. This does not work with //someSourceElement, because apparently, XPath executes this path in the context of fooDocument.
A first workaround that comes to mind is this:
...
<!-- global variable -->
<xsl:variable name="root" select="/"/>
...
<!-- in the template -->
<xsl:variable name="valueFromSource" select="$root//someSourceElement"/>
...
But I cannot use this workaround, because actually, my variable is selected like this:
<xsl:variable name="valueFromSource" select="xalan:evaluate($someXPathString)"/>
$someXPathString is not crafted in the XSLT file, but loaded from fooDocument (and contains an absolute path like the one used above). Still, I need to somehow change the XPath context back to the XML source. A very hacky workaround I found is this:
<xsl:for-each select="$root[1]">
<xsl:variable name="valueFromSource" select="xalan:evaluate($someXPathString)"/>
</xsl:for-each>
The (useless) for-each loop changes the context back to the main XML source, thus the XPath evaluates correctly. But obviously, this is not an acceptable solution.
Is there a way to do this right, or can someone suggest a better workaround?
Even if you think your attempt with a for-each select="$root" to change the context document is not acceptable this is the right approach. So use that, there is no other way.
Have you considered doing all the computation that constructs $someXPathString using a series of global variables?
<xsl:variable name="fooDocument" select="document('fooDocument.xml')"/>
<xsl:variable name="temp1"
.. some computation using fooDocument ..
</xsl:variable>
<xsl:variable name="temp2"
.. some computation using temp1 ..
</xsl:variable>
<xsl:variable name="someXPathString"
.. some computation using temp2 ..
</xsl:variable>
<xsl:variable name="root" select="xalan:evaluate($someXPathString)"/>

How to comment in XSLT and not HTML

I'm writing XSL and I want to make comments throughout the code that will be stripped when it's processed, like PHP, however I'm not sure how.
I'm aware of the comment object, but it prints out an HTML comment when processed. :\
<xsl:comment>comment</xsl:comment>
You use standard XML comments:
<!-- Comment -->
These are not processed by the XSLT transformer.
Just make sure that you put your <!-- comments --> AFTER the opening XML declaration (if you use one, which you really don't need):
BREAKS:
<!-- a comment -->
<?xml version="1.0"?>
WORKS:
<?xml version="1.0"?>
<!-- a comment -->
I scratched my head on this same issue for a bit while debugging someone else's XSLT... seems obvious, but easily overlooked.
Note that white space on either side of the comments can end up in the output stream, depending on your XSLT processor and its settings for handling white-space. If this is an issue for your output, make sure the comment is bracketed by xslt tags.
EG
<xsl:for-each select="someTag">
<xsl:text>"</xsl:text>
<!-- output the id -->
<xsl:value-of select="#id"/>
<xsl:text>"</xsl:text>
</xsl:for-each>
Will output " someTagID" (the indent tab/spaces in front of the comment tag are output).
To remove, either unindent it flush with left margin, or bracket it like
<xsl:text>"</xsl:text><!-- output the id --><xsl:value-of select="#id"/>
This is the way to do it in order to create a comment node that won't be displayed in html
<xsl:comment>
<!-- Content:template -->
</xsl:comment>
Sure. Read http://www.w3.org/TR/xslt#built-in-rule and then it should be apparent why this simple stylesheet will (well, should) do what you want:
<?xml version="1.0"?>
<xsl:stylesheet xmlns="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:template match="comment()">
<xsl:copy/>
</xsl:template>
<xsl:template match="text()|#*"/>
</xsl:stylesheet>
Try :
<xsl:template match="/">
<xsl:for-each select="//comment()">
<SRC_COMMENT>
<xsl:value-of select="."/>
</SRC_COMMENT>
</xsl:for-each>
</xsl:template>
or use a <xsl:comment ...> instruction for a more literal duplication of the source document content in place of my <SRC_COMMENT> tag.