I have a problem figuring out this grouping in xslt:
The initial information:
<Application>
<ApplicationItem LayoutPath="Attachments.Package.Attachment[bfd0b74d-2888-49d9-a986-df807f08ad8a].UniqueID" Value="bfd0b74d-2888-49d9-a986-df807f08ad8a" />
<ApplicationItem LayoutPath="Attachments.Package.Attachment[bfd0b74d-2888-49d9-a986-df807f08ad8a].Filename" Value="Document 1 Test" />
<ApplicationItem LayoutPath="Attachments.Package.Attachment[bfd0b74d-2888-49d9-a986-df807f08ad8a].URI" Value="https/.test.pdf" />
<ApplicationItem LayoutPath="Attachments.Package.Attachment[bfd0b74d-2888-49d9-a986-df807f08ad8b].UniqueID" Value="bfd0b74d-2888-49d9-a986-df807f08ad8b" />
<ApplicationItem LayoutPath="Attachments.Package.Attachment[bfd0b74d-2888-49d9-a986-df807f08ad8b].Filename" Value="Document 2 Test" />
<ApplicationItem LayoutPath="Attachments.Package.Attachment[bfd0b74d-2888-49d9-a986-df807f08ad8b].URI" Value="google.com" />
</Application>
The expected result:
<Package>
<Attachment UniqueID="bfd0b74d-2888-49d9-a986-df807f08ad8a"
Filename="Document 1 Test"
URI="https/.test.pdf"/>
<Attachment UniqueID="bfd0b74d-2888-49d9-a986-df807f08ad8b"
Filename="Document 2 Test"
URI="google.com"/>
<Package>
My code:
I've done the grouping by using the id from the square brackets.
<xsl:for-each-group select="ApplicationItem[contains(#LayoutPath,'Attachments.Package.Attachment')]" group-by="substring-before(substring-after(#LayoutPath, 'Attachments.Package.Attachment['), ']')">
<Attachment>
<xsl:for-each select="current-group()">
<xsl:attribute name="UniqueID" select="current-grouping-key()"/>
<xsl:attribute name="Filename" select=".[contains(#LayoutPath,'Filename')]/#Value"/>
<xsl:attribute name="URI" select=".[contains(#LayoutPath,'URI')]/#Value"/>
</xsl:for-each>
<Attachment>
</xsl:for-each-group>
My results:
<Package>
<Attachment UniqueID="bfd0b74d-2888-49d9-a986-df807f08ad8a"
Filename=""
URI="https/.test.pdf"/>
<Attachment UniqueID="bfd0b74d-2888-49d9-a986-df807f08ad8b"
Filename=""
URI="google.com"/>
<Package>
What i need to change in code to use the grouping because for now is not working taking only the last ApplicationItem with the unique #LayoutPath.
I think the problem is with the grouping but don't now how to fix it.
Remove the <xsl:for-each select="current-group()"> and change
<xsl:attribute name="Filename" select=".[contains(#LayoutPath,'Filename')]/#Value"/>
<xsl:attribute name="URI" select=".[contains(#LayoutPath,'URI')]/#Value"/>
to
<xsl:attribute name="Filename" select="current-group()[contains(#LayoutPath,'Filename')]/#Value"/>
<xsl:attribute name="URI" select="current-group()[contains(#LayoutPath,'URI')]/#Value"/>
Related
I don't like to ask for help, but this time I'm getting totally stuck with a xpath query.
Please have a look at this XML:
<doc>
<car>
<property id="color">
<attribute id="black" />
<attribute id="white" />
<attribute id="green" />
</property>
<property id="size">
<attribute id="small" />
<attribute id="medium" />
<attribute id="large" />
</property>
</car>
<attributes>
<color>white</color>
<size>small</size>
</attributes>
</doc>
The car/properties should be output according to the attributes nodenames. The desired output is:
<property id="color"><attribute id="white" /></property>
<property id="size"><attribute id="small" /></property>
The xpath
/doc/car/property[#id=name(/doc/attributes/*)]/attribute[#id=/doc/attributes/*/text()]
results only the first node, because the name() function returns only the name of the first element.
Who can help me to find out a working xpath (XSLT 1.0)? Many thanks for your help in advance!
You can achieve this with XSLT-1.0, but not only with XPath-1.0, because in XPath-1.0 you can only return the first item. This is not a problem in XSLT-1.0, because you can use an xsl:for-each loop, like the following:
<xsl:for-each select="/doc/attributes/*">
<property id="{/doc/car/property[#id=current()/local-name()]/#id}"><attribute id="{/doc/car/property[#id=current()/local-name()]/attribute[#id=current()/.]/#id}" /></property>
</xsl:for-each>
This code emits the following XML:
<property id="color"><attribute id="white"/></property>
<property id="size"><attribute id="small"/></property>
As seen, your requirements seem to be a little bit redundant, but I guess that your greater scenario justify the means.
What about these options (it's still unclear to me why you're using name() since I don't see any namespace in your sample data) :
//property|//attribute[#id=//attributes/*]
//attribute[#id=//attributes/*]|//attribute[#id=//attributes/*]/parent::property
//property|//attribute[#id=substring-before(normalize-space(//attributes)," ") or #id=substring-after(normalize-space(//attributes)," ")]
Third option should work even if you have to deal with a namespace for the #id inside the attributes node.
Output :
My working solution:
<xsl:stylesheet version="1.0">
<xsl:template match="/">
<xsl:for-each select="/doc/car/property">
<property id="{#id}">
<xsl:variable name="id" select="#id" />
<xsl:copy-of select="attribute[#id=/doc/attributes/*[name()=$id]/text()]" />
</property>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
Another solution without using a loop:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<xsl:apply-templates select="doc/car/property"/>
</xsl:template>
<xsl:template match="property">
<xsl:copy>
<xsl:copy-of select="#*"/>
<xsl:copy-of select="attribute[#id = /doc/attributes/*[name() = current()/#id]]"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
For each property it copies the element node and its attributes. Then it copies its attribute children having an id matching the respective element below /doc/attributes.
I have a number of vendor records which contain multiple addresses e.g.
<vendor>
<addresses>
<address primary="yes">
<line1 />
<city />
<state />
....
</address>
<address primary="no">
<line1 />
<city />
<state />
....
</address>
</addresses>
</vendor>
Some required elements are missing -- preventing updating of the records. Can xmlstarlet can be used to add an element with a default value if it is missing?
Here's a simple example. I'll use xmllint --auto for the xml source. Then we'll add an <add-me> element as a child of <info> if it doesn't exist using the identity transform pattern.
Source xml:
xmllint --auto
<?xml version="1.0"?>
<info>abc</info>
Add the missing element:
xmllint --auto | xsltproc add-missing.xsl -
<?xml version="1.0"?>
<info><add-me>some stuff</add-me>abc</info>
add-missing.xsl:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="info">
<xsl:copy>
<xsl:if test="not(add-me)">
<add-me>some stuff</add-me>
</xsl:if>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
Another XSLT w/xmlstarlet option is to use a variable that contains the required elements (with or without default values) and treat is as a node set (using the supported exsl:node-set() function).
You can then iterate over the node set to see if an element with the same name already exists. If it does, use it. Otherwise use the default.
Example...
XML Input (input.xml)
<vendor>
<addresses>
<address primary="yes">
<line1>address 1 line1</line1>
<state>address 1 state1</state>
</address>
<address primary="no">
<line1>address 2 line1</line1>
<city>address 2 city</city>
<state>address 2 state</state>
</address>
</addresses>
</vendor>
XSLT 1.0 (so.xsl)
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:exsl="http://exslt.org/common" exclude-result-prefixes="exsl">
<xsl:output indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:variable name="req_elems">
<req>
<line1/>
<city/>
<state/>
<country/>
</req>
</xsl:variable>
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="address">
<xsl:variable name="ctx" select="."/>
<xsl:copy>
<xsl:apply-templates select="#*"/>
<xsl:for-each select="exsl:node-set($req_elems)/req/*">
<xsl:choose>
<xsl:when test="$ctx/*[local-name()=local-name(current())]">
<xsl:apply-templates select="$ctx/*[local-name()=local-name(current())]"/>
</xsl:when>
<xsl:otherwise>
<xsl:apply-templates select="."/>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
XML Output
<vendor>
<addresses>
<address primary="yes">
<line1>address 1 line1</line1>
<city/>
<state>address 1 state1</state>
<country/>
</address>
<address primary="no">
<line1>address 2 line1</line1>
<city>address 2 city</city>
<state>address 2 state</state>
<country/>
</address>
</addresses>
</vendor>
Note: This only works if the only allowed elements in address are the same as $req_elements. For example, if you have an element named "foo" in address, it will be dropped from the output.
XML:
<node>
<node date="01-01-2002">Node</node>
<node date="01-01-2005">Node</node>
<node date="01-01-2001">Node</node>
<node date="01-01-2003">Node</node>
<node date="01-01-2006">Node</node>
<node>
<node date="01-01-2000">Node</node>
<node date="01-01-2007">Node</node>
</node>
<node date="01-01-2004">Node</node>
</node>
Problem:
I need to sort by date AND take a limited number of sorted nodes. Need to be able to traverse any number of levels.
Required result:
<p>01-01-2000</p>
<p>01-01-2001</p>
<p>01-01-2002</p>
<p>01-01-2003</p>
<p>01-01-2004</p>
Assumptions:
For sorting by date I use c# extension method that returns time stamp:
<xsl:sort select="cs:formatDate(#date)" order="ascending" data-type="number" />
Limit to 5 oldest nodes.
Order: ascending
XSLT 1.0
EDIT:
As requested this is where i got so far:
I can do sorting and limiting for not nested nodes:
<xsl:template match="node">
<xsl:apply-templates select="node">
<xsl:sort select="cs:formatDate(#date,'dd-MM-yyyy','timestamp')" order="ascending" data-type="number" />
<xsl:with-param name="limit" select="5"/>
</xsl:apply-templates>
</xsl:template>
<xsl:template match="node[#date]">
<xsl:param name="limit" />
<xsl:if test="position() < $limit+1">
<h5><xsl:value-of select="#date"/></h5>
</xsl:if>
</xsl:template>
Or when I try to apply for nested as below, I get nested nodes sorted in isolation, and I cannot limit them in same way anymore:
<xsl:template match="*">
<xsl:apply-templates select="node[#date]">
<xsl:sort select="cs:formatDate(#date,'dd-MM-yyyy','timestamp')" order="ascending" data-type="number" />
</xsl:apply-templates>
<xsl:apply-templates select="node[not(#date)]">
</xsl:apply-templates>
</xsl:template>
<xsl:template match="node[#date]">
<h5><xsl:value-of select="#date"/></h5>
</xsl:template>
<xsl:template match="node[not(#date)]">
<xsl:apply-templates select="node[#date]">
<xsl:sort select="cs:formatDate(#date,'dd-MM-yyyy','timestamp')" order="ascending" data-type="number" />
</xsl:apply-templates>
<xsl:apply-templates select="node[not(#date)]">
</xsl:apply-templates>
</xsl:template>
EDIT:
I thought it is obvious, but probably not: I need sort to be applied before the limit. E.g: "get oldest five" and NOT:"get first five nodes from xml and then sort them"
<xsl:template match="/">
<xsl:apply-templates select="//node[#date]">
<xsl:sort select="concat(substring-after(substring-after(#date,'-'),'-'),substring-before(substring-after(#date,'-'),'-'),substring-before(#date,'-'))" order="ascending" data-type="number" />
<xsl:with-param name="start" select="1"/>
<xsl:with-param name="end" select="5"/>
</xsl:apply-templates>
</xsl:template>
<xsl:template match="node">
<xsl:param name="start" />
<xsl:param name="end" />
<xsl:if test="position() >= $start and position() <= $end">
<p>
<xsl:value-of select="#date"/>
</p>
</xsl:if>
</xsl:template>
i have this code that purpose of that create dynamic elment name.
<xsl:template name="Band" match="*[contains(name(), 'Band')]">
<xsl:param name="DA" />
<xsl:element name="$DA"> <!--error this-->
</xsl:element>
You need to add curly braces to evaluate the variable:
<xsl:param name="DA" />
<xsl:element name="{$DA}">
...
</xsl:element>
In my CMS it is possible to create a new article, and choose an image to be shown on that article. When an image is chosen, a thumbnail of the image will automatically be created as well.
If the uploaded image is called image.jpg, then the corresponding thumbnail will automatically be named image_thumbnail.jpg.
I would now like to use the thumbnail image, everywhere on the website where the article is mentioned, except in the article itself (where the original big image should be shown).
But how can I do that?
I imagine if I could get the original name of the image, and then split it up before the suffix (.jpg, .png, .jpeg etc.) and hardcode _thumbnail after the name, then that would be sufficient.
In other words, I want to take the complete filename, and cut it into two parts, so that I can insert the string _thumbnail between the two parts.
Maybe that would work, but what if an image called image.2horses.jpg (a file with more than one dot in the filename) is uploaded? A naive cut before the '.' wouldn't work here.
Is there a way to get around this? Perhaps by cutting the filename up before the last 4 (.jpg, .png) or 5 (.jpeg) characters?
Off the top of my head:
<xsl:template name="substring-before-last">
<xsl:param name="string1" select="''" />
<xsl:param name="string2" select="''" />
<xsl:if test="$string1 != '' and $string2 != ''">
<xsl:variable name="head" select="substring-before($string1, $string2)" />
<xsl:variable name="tail" select="substring-after($string1, $string2)" />
<xsl:value-of select="$head" />
<xsl:if test="contains($tail, $string2)">
<xsl:value-of select="$string2" />
<xsl:call-template name="substring-before-last">
<xsl:with-param name="string1" select="$tail" />
<xsl:with-param name="string2" select="$string2" />
</xsl:call-template>
</xsl:if>
</xsl:if>
</xsl:template>
Called as:
<xsl:template match="/">
<xsl:variable name="filename" select="'image.2horses.jpg'" />
<xsl:variable name="basename">
<xsl:call-template name="substring-before-last">
<xsl:with-param name="string1" select="$filename" />
<xsl:with-param name="string2" select="'.'" />
</xsl:call-template>
</xsl:variable>
<xsl:value-of select="$basename" />
</xsl:template>
Yields:
image.2horses
Given the image's filename in $filename,
If you can assume that all images will end in ".jpg" and won't have ".jpg" elsewhere in the filename, then this should work:
<img src="{substring-before($filename, '.jpg')}_thumbnail.jpg" ... />
If you don't know the image type (like, you want to handle gif and png as well), or if you think the extension may occur multiple times in the filename ("image.jpg.jpg"), then you will want a template to help you:
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:template match="/">
<p>
<xsl:call-template name="image_thumbnail">
<xsl:with-param name="filename" select="'image.jpg'"/>
</xsl:call-template>
</p>
<p>
<xsl:call-template name="image_thumbnail">
<xsl:with-param name="filename" select="'image.09.07.11.jpg'"/>
</xsl:call-template>
</p>
<p>
<xsl:call-template name="image_thumbnail">
<xsl:with-param name="filename" select="'image.gif'"/>
</xsl:call-template>
</p>
<p>
<xsl:call-template name="image_thumbnail">
<xsl:with-param name="filename" select="'image with spaces.jpg'"/>
</xsl:call-template>
</p>
<p>
<xsl:call-template name="image_thumbnail">
<xsl:with-param name="filename" select="'image with irregular spaces.jpg'"/>
</xsl:call-template>
</p>
<p>
<xsl:call-template name="image_thumbnail">
<xsl:with-param name="filename" select="'image.jpg.again.jpg'"/>
</xsl:call-template>
</p>
</xsl:template>
<xsl:template name="image_thumbnail">
<xsl:param name="filename"/>
<xsl:choose>
<xsl:when test="contains($filename, '.')">
<xsl:variable name="before" select="substring-before($filename, '.')"/>
<xsl:variable name="after" select="substring-after($filename, '.')"/>
<xsl:choose>
<xsl:when test="contains($after, '.')">
<xsl:variable name="recursive">
<xsl:call-template name="image_thumbnail">
<xsl:with-param name="filename" select="$after"/>
</xsl:call-template>
</xsl:variable>
<xsl:value-of select="concat($before, '.', $recursive)"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="concat($before, '_thumbnail.', $after)"/>
</xsl:otherwise>
</xsl:choose>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$filename"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
A general solution involving only standard XSLT is somewhat hard since you have to search the string from the end. You can split your filename usings two functions, substring-before-last and substring-after-last. Unfortunately, these functions are not part of XSLT. You can Google and try to find implementations. Assuming you have these two functions implemented as XSLT templates you can then use the following template to generate thumbnail names:
<xsl:template name="thumbnail-name">
<xsl:param name="file-name"/>
<xsl:call-template name="substring-before-last">
<xsl:with-param name="text" select="$file-name"/>
<xsl:with-param name="chars" select="'.'"/>
</xsl:call-template>
<xsl:text>_thumbnail.</xsl:text>
<xsl:call-template name="substring-after-last">
<xsl:with-param name="text" select="$file-name"/>
<xsl:with-param name="chars" select="'.'"/>
</xsl:call-template>
</xsl:template>
You can use the template like this (assuming the variable $file-name contains the name of the image):
<img>
<xsl:attribute name="src">
<xsl:call-template name="thumbnail-name">
<xsl:with-param name="file-name" select="$file-name"/>
</xsl:call-template>
</xsl:attribute>
</img>
Have a look at the XPath functions overview at W3Schools, specifically the substring-before method.
I believe XPath functions operating on string might help you. I would try with some simple replace or translate.
XSLT 2 solution using regexp:
replace($filename, '(\.[^\.]*)$', concat('_thumbnail', '$1'))
Original answer (also XSLT 2):
This removes all after the last separator (including the separator). So below the $separatorRegexp could be '\.jpg' or just '\.' and the $separator '.jpg' or '.' in the other case.
string-join(reverse(remove(reverse(tokenize($filename, $separatorRegexp)),1)),$separator)
Eventually the '_thumbnail.jpg' can be appended with concat.