Related
I have a long XML file from which I ned to pull out book titles and other information, then sort it alphabetically, with a separator for each letter. I also need a section for items that don't begin with a letter, say a number or symbol. Something like:
#
1494 - hardcover, $9.99
A
After the Sands - paperback, $24.95
Arctic Spirit - hardcover, $65.00
B
Back to the Front - paperback, $18.95
…
I also need to create a separate list of authors, created from the same data but showing different kinds of information.
How I'm currently doing it
This is simplified, but I basically have this same code twice, once for titles and once for authors. The author version of the template works with different elements and does different things with the data, so I can't use the same template.
<xsl:call-template name="BIP-letter">
<xsl:with-param name="letter" select="'#'" />
</xsl:call-template>
<xsl:call-template name="BIP-letter">
<xsl:with-param name="letter" select="'A'" />
</xsl:call-template>
…
<xsl:call-template name="BIP-letter">
<xsl:with-param name="letter" select="'Z'" />
</xsl:call-template>
<xsl:template name="BIP-letter">
<xsl:param name="letter" />
<xsl:choose>
<xsl:when test="$letter = '#'">
<xsl:text>#</xsl:text>
<xsl:for-each select="//Book[
not(substring(Title,1,1) = 'A') and
not(substring(Title,1,1) = 'B') and
…
not(substring(Title/,1,1) = 'Z')
]">
<xsl:sort select="Title" />
<xsl:appy-templates select="Title" />
<!-- Add other relevant data here -->
</xsl:for-each>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$letter" />
<xsl:for-each select="//Book[substring(Title,1,1) = $letter]">
<xsl:sort select="Title" />
<xsl:appy-templates select="Title" />
<!-- Add other relevant data here -->
</xsl:for-each>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
My questions
The code above works just fine, but:
Manually cycling through each letter gets very long, especially having to do it twice. Is there a way to simplify that? Something like a <xsl:for-each select="[A-Z]"> that I could use to set the parameter when calling the template?
Is there a simpler way to select all titles that don't begin with a letter? Something like //Book[not(substring(Title,1,1) = [A-Z])?
There may be cases where the title or author name starts with a lowercase letter. In the code above, they would get grouped with under the # heading, rather than with the actual letter. The only way I can think to accommodate that—doing it manually—would significantly bloat up the code.
This solution answers all questions asked:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:strip-space elements="*"/>
<xsl:variable name="vLowercase" select="'abcdefghijklmnopqrstuvuxyz'"/>
<xsl:variable name="vUppercase" select="'ABCDEFGHIJKLMNOPQRSTUVWXYZ'"/>
<xsl:variable name="vDigits" select="'0123456789'"/>
<xsl:key name="kBookBy1stChar" match="Book"
use="translate(substring(Title, 1, 1),
'abcdefghijklmnopqrstuvuxyz0123456789',
'ABCDEFGHIJKLMNOPQRSTUVWXYZ##########'
)"/>
<xsl:template match="/*">
<xsl:apply-templates mode="firstInGroup" select=
"Book[generate-id()
= generate-id(key('kBookBy1stChar',
translate(substring(Title, 1, 1),
concat($vLowercase, $vDigits),
concat($vUppercase, '##########')
)
)[1]
)
]">
<xsl:sort select="translate(substring(Title, 1, 1),
concat($vLowercase, $vDigits),
concat($vUppercase, '##########')
)"/>
</xsl:apply-templates>
</xsl:template>
<xsl:template match="Book" mode="firstInGroup">
<xsl:value-of select="'
'"/>
<xsl:value-of select="translate(substring(Title, 1, 1),
concat($vLowercase, $vDigits),
concat($vUppercase, '##########')
)"/>
<xsl:apply-templates select=
"key('kBookBy1stChar',
translate(substring(Title, 1, 1),
concat($vLowercase, $vDigits),
concat($vUppercase, '##########')
)
)">
<xsl:sort select="Title"/>
</xsl:apply-templates>
</xsl:template>
<xsl:template match="Book">
<xsl:value-of select="'
'"/>
<xsl:value-of select="concat(Title, ' - ', Binding, ', $', price)"/>
</xsl:template>
</xsl:stylesheet>
When this transformation is applied on the following xml document (none provided in the question!):
<Books>
<Book>
<Title>After the Sands</Title>
<Binding>paperback</Binding>
<price>24.95</price>
</Book>
<Book>
<Title>Cats Galore: A Compendium of Cultured Cats</Title>
<Binding>hardcover</Binding>
<price>5.00</price>
</Book>
<Book>
<Title>Arctic Spirit</Title>
<Binding>hardcover</Binding>
<price>65.00</price>
</Book>
<Book>
<Title>1494</Title>
<Binding>hardcover</Binding>
<price>9.99</price>
</Book>
<Book>
<Title>Back to the Front</Title>
<Binding>paperback</Binding>
<price>18.95</price>
</Book>
</Books>
the wanted, correct result is produced:
#
1494 - hardcover, $9.99
A
After the Sands - paperback, $24.95
Arctic Spirit - hardcover, $65.00
B
Back to the Front - paperback, $18.95
C
Cats Galore: A Compendium of Cultured Cats - hardcover, $5.00
Explanation:
Use of the Muenchian method for grouping
Use of the standard XPath translate() function
Using mode to process the first book in a group of books starting with the same (case-insensitive) character
Using <xsl:sort> to sort the books in alphabetical orser
The most problematic part is this:
I also need a section for items that don't begin with a letter, say a number or symbol.
If you have a list of all possible symbols that an item can begin with, then you can simply use translate() to convert them all to the # character. Otherwise it gets more complicated. I would try something like:
XSLT 1.0 (+ EXSLT node-set())
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:exsl="http://exslt.org/common"
extension-element-prefixes="exsl">
<xsl:output method="text" encoding="UTF-8"/>
<xsl:key name="book" match="Book" use="index" />
<xsl:template match="/Books">
<!-- first-pass: add index char -->
<xsl:variable name="books-rtf">
<xsl:for-each select="Book">
<xsl:copy>
<xsl:copy-of select="*"/>
<index>
<xsl:variable name="index" select="translate(substring(Title, 1, 1), 'abcdefghijklmnopqrstuvwxyz', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ')" />
<xsl:choose>
<xsl:when test="contains('ABCDEFGHIJKLMNOPQRSTUVWXYZ', $index)">
<xsl:value-of select="$index"/>
</xsl:when>
<xsl:otherwise>#</xsl:otherwise>
</xsl:choose>
</index>
</xsl:copy>
</xsl:for-each>
</xsl:variable>
<xsl:variable name="books" select="exsl:node-set($books-rtf)/Book" />
<!-- group by index char -->
<xsl:for-each select="$books[count(. | key('book', index)[1]) = 1]">
<xsl:sort select="index"/>
<xsl:value-of select="index"/>
<xsl:text>
</xsl:text>
<!-- list books -->
<xsl:for-each select="key('book', index)">
<xsl:sort select="Title"/>
<xsl:value-of select="Title"/>
<xsl:text> - </xsl:text>
<xsl:value-of select="Binding"/>
<xsl:text>, </xsl:text>
<xsl:value-of select="Price"/>
<xsl:text>
</xsl:text>
</xsl:for-each>
<xsl:text>
</xsl:text>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
However, this still leaves the problem of items that begin with a diacritic, e.g. "Österreich" or say a Greek letter. Under this method they too will be clumped under #.
Unfortunately, the only good solution for this is to move to XSLT 2.0.
Demo: https://xsltfiddle.liberty-development.net/jyRYYjj/2
I'm looking for the other references regarding the transformation of XML file to Flat File format and I have seen many of them. I've tried some of the codes that I saw over the internet and it helps a lot. I tried to do my own XSLT file and I can't get what I want in my output. Also, I need to minimize my coding in the XSLT since I have a lot of coding and condition to applied from Header Record, Detail/Contra Record and Trailer. The value of the header record is correct, however, 2nd and 3rd row of the current output is incorrect. I need to populate for every Transaction there should have 1 detail and 1 Contra. The output should look like what's on the expected output.
Thank you.
SAMPLE XML FILE
<SyncCreditTransfer xmlns="http://schema.infor.com/InforOAGIS/2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" releaseID="9.2" versionID="2.12.3" xsi:schemaLocation="http://schema.infor.com/InforOAGIS/2 http://schema.infor.com/2.12.x/InforOAGIS/BODs/SyncCreditTransfer.xsd">
<Application>
<Sender>
<LogicalID>company department</LogicalID>
</Sender>
<CreationDateTime>2016-07-01T05:50:16.208Z</CreationDateTime>
</Application>
<Data>
<Sync>
<ID>1122EDF6394</ID>
<EntityID>SampleFiele</EntityID>
</Sync>
<Record>
<Header>
<DateTime>2016-07-01T05:51:16</DateTime>
</Header>
<Payment>
<DisplayID>Payment1: 09459732</DisplayID>
<DebtorParty>
<FinancialAccount>
<ID>11111</ID>
</FinancialAccount>
</DebtorParty>
<Transaction sequence="1">
<TransactionID>BOA-t-121212</TransactionID>
<InstructedAmount currencyID="EUR">123.43</InstructedAmount>
<CreditorParty>
<FinancialAccount>
<ID>AAAAA</ID>
</FinancialAccount>
</CreditorParty>
</Transaction>
<Transaction sequence="1">
<TransactionID>BOA-t-343434</TransactionID>
<InstructedAmount currencyID="GBP">123.43</InstructedAmount>
<CreditorParty>
<FinancialAccount>
<ID>BBBBB</ID>
</FinancialAccount>
</CreditorParty>
</Transaction>
</Payment>
<Payment>
<DisplayID>Payment2: 12435435</DisplayID>
<DebtorParty>
<FinancialAccount>
<ID>22222</ID>
</FinancialAccount>
</DebtorParty>
<Transaction sequence="1">
<TransactionID>BOA-t-090909</TransactionID>
<InstructedAmount currencyID="EUR">123.43</InstructedAmount>
<CreditorParty>
<FinancialAccount>
<ID>AAAAA</ID>
</FinancialAccount>
</CreditorParty>
</Transaction>
<Transaction sequence="1">
<TransactionID>BOA-t-878787</TransactionID>
<InstructedAmount currencyID="GBP">123.43</InstructedAmount>
<CreditorParty>
<FinancialAccount>
<ID>BBBBB</ID>
</FinancialAccount>
</CreditorParty>
</Transaction>
</Payment>
</Record>
</Data>
XSLT FILE
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:func="myfunc">
<xsl:output method="text" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:function name="func:trunc">
<xsl:param name="str"/>
<xsl:param name="len"/>
<xsl:value-of select="substring($str,1,$len)"/>
</xsl:function>
<xsl:template match="/">
<!-- Start of Header Record -->
<xsl:element name="UserHeadLabel">
<xsl:text>UHL</xsl:text>
</xsl:element>
<xsl:element name="Constant01">
<xsl:text>1</xsl:text>
</xsl:element>
<xsl:element name="Filler01">
<xsl:text> </xsl:text>
</xsl:element>
<xsl:element name="PaymentDate">
<xsl:if test="//*:Header/*:DateTime[normalize-space()]!=''">
<xsl:value-of select="func:trunc(//*:Header/*:DateTime,5)"/>
</xsl:if>
</xsl:element>
<xsl:element name="Constant02">
<xsl:text>999999</xsl:text>
</xsl:element>
<xsl:element name="Filler02">
<xsl:text> </xsl:text>
</xsl:element>
<xsl:element name="CurrencyCode">
<xsl:choose>
<xsl:when test="//*:Payment/*:Transaction/*:InstructedAmount/#currencyID[normalize-space()]!='' and //*:Payment/*:Transaction/*:InstructedAmount/#currencyID='EUR'">
<xsl:text>01</xsl:text>
</xsl:when>
<xsl:otherwise>
<xsl:text>00</xsl:text>
</xsl:otherwise>
</xsl:choose>
</xsl:element>
<xsl:element name="Constant03">
<xsl:text>000000</xsl:text>
</xsl:element>
<xsl:element name="Constant04">
<xsl:text>1 DAILY </xsl:text>
</xsl:element>
<xsl:element name="FileNumber">
<xsl:text>001</xsl:text>
</xsl:element>
<xsl:element name="Filler03">
<xsl:text> </xsl:text>
</xsl:element>
<xsl:element name="Optional01">
<xsl:text> </xsl:text>
</xsl:element>
<xsl:element name="Optional02">
<xsl:text> </xsl:text>
</xsl:element>
<xsl:element name="UserOptional">
<xsl:text>000000000000</xsl:text>
</xsl:element>
<xsl:text>
</xsl:text>
<!-- End of Header Record -->
<!-- Start of Detail Record -->
<xsl:element name="DestinationSortCodeNo">
<xsl:if test="//*:Payment/*:Transaction/*:CreditorParty/*:FinancialAccount/*:ID[normalize-space()]!=''">
<xsl:value-of select="//*:Payment/*:Transaction/*:CreditorParty/*:FinancialAccount/*:ID"/>
</xsl:if>
</xsl:element>
<xsl:element name="DestinationAccountNo">
<xsl:if test="//*:Payment/*:Transaction/*:TransactionID[normalize-space()]!=''">
<xsl:value-of select="//*:Payment/*:Transaction/*:TransactionID"/>
</xsl:if>
</xsl:element>
<xsl:element name="Zero01">
<xsl:text>0</xsl:text>
</xsl:element>
<xsl:element name="TransactionCode">
<xsl:text>99</xsl:text>
</xsl:element>
<xsl:text>
</xsl:text>
<!-- End of Detail Record -->
<!-- Start of Contra Record -->
<xsl:element name="UserSortCodeNo1">
<xsl:if test="//*:Payment/*:DebtorParty/*:FinancialAccount/*:ID[normalize-space()]!=''">
<xsl:value-of select="//*:Payment/*:DebtorParty/*:FinancialAccount/*:ID"/>
</xsl:if>
</xsl:element>
<xsl:element name="UserAccountNo1">
<xsl:if test="//*:Payment/*:DisplayID[normalize-space()]!=''">
<xsl:value-of select="//*:Payment/*:DisplayID"/>
</xsl:if>
</xsl:element>
<xsl:element name="Zero01">
<xsl:text>0</xsl:text>
</xsl:element>
<xsl:element name="TransactionCode">
<xsl:text>17</xsl:text>
</xsl:element>
<xsl:text>
</xsl:text>
<!-- End of Contra Record -->
</xsl:template>
CURRENT OUTPUT
UHL1 2016-999999 010000001 DAILY 001 000000000000
AAAAA BBBBB CCCCC DDDDDBOA-t-121212 BOA-t-343434 BOA-t-090909 BOA-t-878787099
11111 22222Payment1: 09459732 Payment2: 12435435017
EXPECTED OUTPUT
UHL1 2016-999999 010000001 DAILY 001 000000000000
AAAAAABOA-t-12099
11111MPayment1017
BBBBBMBOA-t-34099
11111MPayment1017
CCCCCMBOA-t-09099
22222MPayment2017
DDDDDMBOA-t-87099
22222MPayment2017
Explanation: The value AAAAAA comes from the Payment/Transaction/CreditorParty/FinancialAccount/ID and should only have 6 characters. The BOA-t-12 comes from the Payment/Transaction/TransactionID and this field should only have 8characters. 0 is the hardcoded value, as well as, the value 99. On the next line, the 11111M comes from the Payment/DebtorParty/FinancialAccount/ID, Payment1 is from the Payment/DisplayID and 0 and 17 are the hardcoded value. From the next line and soon, it will only repeat the process and this time the value will be get from the next occurrence of Payment/Transaction.
For every occurrence of Payment/Transaction, it will create 1 Detail record and 1 Contra record. In my example, I have 4 Transaction, and the output should have:
Detail - 1st occurrence of Transaction
Contra - 1st occurrence of Transaction
Detail - 2nd occurrence
Contra - 2nd occurrence
Detail - 3rd occurrence
Contra - 3rd occurrence
Detail - 4th occurrence
Contra - 4th occurrence
This is a fixed-length format.
Try this as your starting point:
XSLT 2.0
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xpath-default-namespace="http://schema.infor.com/InforOAGIS/2">
<xsl:output method="text" encoding="UTF-8"/>
<xsl:template match="/SyncCreditTransfer">
<!-- Start of Header Record -->
<!-- skipped for the purpose of this example -->
<!-- End of Header Record -->
<!-- Records -->
<xsl:for-each select="Data/Record/Payment/Transaction">
<!-- Start of Detail Record -->
<xsl:value-of select="substring(CreditorParty/FinancialAccount/ID, 1 , 6)"/>
<xsl:value-of select="substring(TransactionID, 1 , 8)"/>
<xsl:text>099
</xsl:text>
<!-- End of Detail Record -->
<!-- Start of Contra Record -->
<xsl:value-of select="../DebtorParty/FinancialAccount/ID"/>
<xsl:value-of select="../DisplayID"/>
<xsl:text>017
</xsl:text>
<!-- End of Contra Record -->
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
This still needs more work on the "contra" record, but you have not explained that part.
Note:
the use of xpath-default-namespace to handle the namespace used by your input;
the use of xsl:for-each to create a record for each transaction;
Note also that when the output method is text, using xsl:element makes no sense.
I have gathered bits and pieces of this XSLT from these forums. I'm trying to put them altogether to create a single, generic XSLT that can be used to convert XML to CSV by specifying the path to the nodes that should be included in the CSV file.
I have three things that I still can't figure out after about 10 hours of messing with it.
I want to iterate over each column named in csv:columns. During each iteration, I need to extract and store the text() of the column. I think this is the way to iterate, but want to make sure:
<xsl:for-each select="document('')/*/csv:columns/*">
Once I have the text() from the column, I need to put that into the columnname variable in such a way that it works when it is used with getNodeValue.
I was unable to set columnname using variable. If I didn't hard-code the value (surrounded by apostrophes), I could not get it to work. This is why I have the following line in the code:
<xsl:variable name="columnname" select="'location/city'" />
I want to pass the result of getNodeValue into quotevalue so that the result is properly quoted.
The XSLT:
<?xml version="1.0"?>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:csv="csv:csv" xpath-default-namespace="http://nowhere/" >
<xsl:output method="text" encoding="utf-8" />
<xsl:strip-space elements="*" />
<xsl:variable name="delimiter" select="','" />
<csv:columns>
<column>title</column>
<column>location/city</column>
</csv:columns>
<xsl:template match="job">
<xsl:value-of select="concat(#id, ',')"/>
<!-- #1 I WANT TO LOOP THROUGH ALL OF THE CSV COLUMNS HERE -->
<!-- #2 How do I put the text into the variable 'columnname' variable so that it works with getNodeValue? -->
<xsl:variable name="columnname" select="'location/city'" />
<xsl:variable name="vXpathExpression" select="$columnname"/>
<xsl:call-template name="getNodeValue">
<xsl:with-param name="pExpression" select="$vXpathExpression"/>
</xsl:call-template>
<!-- #3 After getNodeValue gets the value, I want to send that value into 'quotevalue' -->
<xsl:text>
</xsl:text>
</xsl:template>
<xsl:template name="getNodeValue">
<xsl:param name="pExpression"/>
<xsl:param name="pCurrentNode" select="."/>
<xsl:choose>
<xsl:when test="not(contains($pExpression, '/'))">
<xsl:value-of select="$pCurrentNode/*[name()=$pExpression]"/>
</xsl:when>
<xsl:otherwise>
<xsl:call-template name="getNodeValue">
<xsl:with-param name="pExpression"
select="substring-after($pExpression, '/')"/>
<xsl:with-param name="pCurrentNode" select=
"$pCurrentNode/*[name()=substring-before($pExpression, '/')]"/>
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template name="quotevalue">
<xsl:param name="value"/>
<xsl:choose>
<!-- Quote the value if required -->
<xsl:when test="contains($value, '"')">
<xsl:variable name="x" select="replace($value, '"', '""')"/>
<xsl:value-of select="concat('"', $x, '"')"/>
</xsl:when>
<xsl:when test="contains($value, $delimiter)">
<xsl:value-of select="concat('"', $value, '"')"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$value"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
Sample XML
<?xml version="1.0" encoding="utf-8"?>
<positionfeed
xmlns="http://nowhere/"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
version="2006-04">
<job id="2830302">
<employer>Acme</employer>
<title>Manager</title>
<description>Full time</description>
<postingdate>2016-09-15T23:12:13Z</postingdate>
<location>
<city>Los Angeles</city>
<state>California</state>
</location>
</job>
<job id="2830303">
<employer>Acme</employer>
<title>Clerk, evenings</title>
<description>Part time</description>
<postingdate>2016-09-15T23:12:13Z</postingdate>
<location>
<city>Albany</city>
<state>New York</state>
</location>
</job>
</positionfeed>
The current output using the XSLT I provided
2830302,Los Angeles
2830303,Albany
The output if the XSLT works as desired
2830302,Manager,Los Angeles
2830303,"Clerk, evenings",Albany
Solution (many thanks to Tim's help below)
<?xml version="1.0"?>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:csv="csv:csv" xpath-default-namespace="http://www.job-search-engine.com/add-jobs/positionfeed-namespace/" >
<xsl:output method="text" encoding="utf-8" />
<xsl:strip-space elements="*" />
<!-- Set the value of the delimiter character -->
<xsl:variable name="delimiter" select="','" />
<!-- The name of the node that contains the column values -->
<xsl:param name="containerNodeName" select="'job'"/>
<!-- All nodes that should be ignored during processing -->
<xsl:template match="source|feeddate"/>
<!-- The names of the nodes to be included in the CSV file -->
<xsl:variable name="columns" as="element()*">
<column header="Title">title</column>
<column header="Category">category</column>
<column header="Description">description</column>
<column header="PostingDate">postingdate</column>
<column header="URL">joburl</column>
<column header="City">location/city</column>
<column header="State">location/state</column>
</xsl:variable>
<!-- ************** DO NOT TOUCH BELOW **************** -->
<!-- ************** DO NOT TOUCH BELOW **************** -->
<!-- ************** DO NOT TOUCH BELOW **************** -->
<!-- ************** DO NOT TOUCH BELOW **************** -->
<!-- ************** DO NOT TOUCH BELOW **************** -->
<!-- Warn about unmatched nodes -->
<xsl:template match="*">
<xsl:message terminate="no">
<xsl:text>WARNING: Unmatched element: </xsl:text>
<xsl:value-of select="name()"/>
</xsl:message>
<xsl:apply-templates/>
</xsl:template>
<!-- Generate the column headers -->
<xsl:template match="//*[*[local-name()=$containerNodeName]]">
<xsl:value-of select="'Id'"/>
<xsl:value-of select="$delimiter"/>
<xsl:for-each select="$columns/#header">
<xsl:variable name="colname" select="." />
<xsl:value-of select="$colname"/>
<xsl:if test="position() != last()">
<xsl:value-of select="$delimiter"/>
</xsl:if>
</xsl:for-each>
<xsl:text>
</xsl:text>
<xsl:apply-templates />
</xsl:template>
<!-- Generate the rows of column data -->
<xsl:template match="//*[local-name()=$containerNodeName]">
<!-- TODO: Handle attributes generically -->
<xsl:value-of select="#id"/>
<xsl:variable name="container" select="." />
<xsl:for-each select="$columns">
<xsl:value-of select="$delimiter"/>
<xsl:variable name="vXpathExpression" select="."/>
<xsl:call-template name="getQuotedNodeValue">
<xsl:with-param name="pCurrentNode" select="$container"/>
<xsl:with-param name="pExpression" select="$vXpathExpression"/>
</xsl:call-template>
</xsl:for-each>
<xsl:text>
</xsl:text>
</xsl:template>
<xsl:template name="getQuotedNodeValue">
<xsl:param name="pExpression"/>
<xsl:param name="pCurrentNode" select="."/>
<xsl:choose>
<xsl:when test="not(contains($pExpression, '/'))">
<xsl:variable name="result" select="$pCurrentNode/*[name()=$pExpression]"/>
<xsl:call-template name="quotevalue">
<xsl:with-param name="value" select="$result"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:call-template name="getQuotedNodeValue">
<xsl:with-param name="pExpression" select="substring-after($pExpression, '/')"/>
<xsl:with-param name="pCurrentNode" select= "$pCurrentNode/*[name()=substring-before($pExpression, '/')]"/>
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template name="quotevalue">
<xsl:param name="value"/>
<xsl:choose>
<xsl:when test="contains($value, '"')">
<!-- Quote the value and escape the double-quotes -->
<xsl:variable name="x" select="replace($value, '"', '""')"/>
<xsl:value-of select="concat('"', $x, '"')"/>
</xsl:when>
<xsl:otherwise>
<!-- Quote the value -->
<xsl:value-of select="concat('"', $value, '"')"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
Sample data to demonstrate solution
<?xml version="1.0" encoding="utf-8"?>
<positionfeed
xmlns="http://www.job-search-engine.com/add-jobs/positionfeed-namespace/"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.job-search-engine.com/add-jobs/positionfeed-namespace/ http://www.job-search-engine.com/add-jobs/positionfeed.xsd"
version="2006-04">
<source>Casting360</source>
<feeddate>2016-11-11T21:48:34Z</feeddate><job id="1363612">
<employer>Casting360</employer>
<title>The Robert Irvine Show Is Seeking Guests</title>
<category>Reality TV</category>
<description>TV personality ROBERT IRVINE (Restaurant Impossible) is seeking guests looking for solutions to their unique problems to share their stories on his show!
Our next show is Thursday, September 22nd in LA. If you're not in LA we will provide your airfare, hotel, car service, and per diem.
Please note: WE ARE NOT LOOKING FOR RESUMES; THIS IS NOT AN ACTING GIG. We are looking for real people to share their stories!
*appearance fee (TBD)
If you or someone you know has a conflict that they need help resolving, WE WANT TO HEAR FROM YOU.
Please email tvgal.ri#gmail.com the following information:
Name
Phone number
Your story in 2-3 paragraphs
1-3 photos of yourself.</description>
<postingdate>2016-09-15T23:12:13Z</postingdate>
<joburl>http://casting360.com/lgj/8886644624?jobid=1363612&city=Los+Angeles&state=CA</joburl>
<location>
<nation>USA</nation>
<city>Los Angeles</city>
<state>California</state>
</location>
<jobsource>Casting360</jobsource>
</job><job id="1370302">
<employer>Casting360</employer>
<title>Photoshoot for Publication</title>
<category>Modeling</category>
<description>6 FEMALE Models are wanted for publication photoshoot.
If you're not in the NYC Vicinity (NY, Pa, Ct,) DO NOT REPLY because your response will be summarily ignored.
Chosen models will be given a 5 look photo shoot. The shoot will occur on location (outdoors) in highly public locations chosen both for it's convenience and scenery.
The 5 looks (outfits) will be pre-determined by our staff of items most outfits within a model's wardrobe.
THIS IS A TF (UNPAID) SHOOT. After the release of the magazine, the photos agreed upon from the shoot shall be given to the model (in digital format) for her to build her portfolio.
Chosen models will receive a 5 outfit photo shoot at no cost to them by a NY Fashion Photographer.As a result, chosen models not only receive a free photo shoot, but also become PUBLISHED MODELS featured in a magazine.
The model (Janeykay) centered in the photo attached (Please look at the attached photo) is a Casting360 member who not only received her photo shoot, not only is being featured in a magazine, but also made the cover becoming a Cover Model from her shoot with us.</description>
<postingdate>2016-10-03T00:34:43Z</postingdate>
<joburl>http://casting360.com/lgj/8886644624?jobid=1370302&city=New+York&state=NY</joburl>
<location>
<nation>USA</nation>
<city>New York</city>
<state>New York</state>
</location>
<jobsource>Casting360</jobsource>
</job><job id="1370962">
<employer>Casting360</employer>
<title>Actresses Needed for "Red Shore", Action Film</title>
<category>Acting</category>
<description>CASTING (non-union)
We are a New Independent company looking to shoot our first feature. We are currently looking to fill two Major roles.
Female/African American, Hispanic, Asian, Pacific Islander/ 5'5-5'10/ Age Late 30's-Early 40's.
Project description: A long standing feud between two best friends turned enemies escalates over a valuable Diamond on display in a New York City Museum. With the stakes high they each seek the help of both friends and strangers to settle their feud once and for all.
Please note this is a non-paid project.
Fight training will be provided for free.
Please email including age and height in your e-mail.
Those selected will be invited to our audition.</description>
<postingdate>2016-10-03T14:18:20Z</postingdate>
<joburl>http://casting360.com/lgj/8886644624?jobid=1370962&city=New+York&state=NY</joburl>
<location>
<nation>USA</nation>
<city>New York</city>
<state>New York</state>
</location>
<jobsource>Casting360</jobsource>
</job>
</positionfeed>
As you are using XSLT 2.0, you could define your columns in a variable like so:
<xsl:variable name="columns" as="element()*">
<column>title</column>
<column>location/city</column>
</xsl:variable>
Then you can just iterate over them with a simple statement
<xsl:for-each select="$columns">
But the problem you may be having is that within this xsl:for-each you have changed context. You are no longer positioned on a job element, but the column element, and you don't want your expression to be relative to that. You really need to swap back to being on the job element, which you can do simply by setting a variable reference to the job element before the xsl:for-each and then using that as a parameter to the named template:
<xsl:template match="job">
<xsl:value-of select="#id"/>
<xsl:variable name="job" select="." />
<xsl:for-each select="$columns">
<xsl:value-of select="$delimiter"/>
<xsl:variable name="vXpathExpression" select="."/>
<xsl:call-template name="getNodeValue">
<xsl:with-param name="pCurrentNode" select="$job"/>
<xsl:with-param name="pExpression" select="$vXpathExpression"/>
</xsl:call-template>
</xsl:for-each>
<xsl:text>
</xsl:text>
</xsl:template>
As for quoting the result; instead of doing just xsl:value-of simply call the quote template with the value as a parameter
<xsl:when test="not(contains($pExpression, '/'))">
<xsl:call-template name="quotevalue">
<xsl:with-param name="value" select="$pCurrentNode/*[name()=$pExpression]" />
</xsl:call-template>
</xsl:when>
EDIT: If you want a header row of column names, you would have to match the parent of the job node, and then just output the values of the $column variable
<xsl:template match="*[job]">
<xsl:value-of select="$columns" separator="," />
<xsl:text>
</xsl:text>
<xsl:apply-templates />
</xsl:template>
Or maybe this if you didn't want the full path
<xsl:value-of select="$columns/(tokenize(., '/')[last()])" separator="," />
Or you could extend your columns variable to have the header text
<xsl:variable name="columns" as="element()*">
<column header="Title">title</column>
<column header="City">location/city</column>
</xsl:variable>
Then you would do this...
<xsl:value-of select="$columns/#header" separator="," />
I would like to find some keyWords in Text by using XSLT 1.0
contentText : Marine weather forecasts, warnings, synopsis, and ice conditions. Hundreds of land and buoy station observations across and marine weather forecasts, warnings, synopsis, and ice conditions. Hundreds of land and buoy station observations across.
KeyWords : "Marine weather, marine weather, Marine Weather"
Delimiter : ,
The following code is only one Keyword works...but I would like to find Multiple KeyWords ("Marine weather, marine weather, Marine Weather")
<xsl:choose>
'<xsl:when test="contains($contentText,$keyWordLower)">
<xsl:value-of select="substring-before($contentText,$keyWordLower)" disable-output-escaping="yes"/>
<span class="texthighlight">
<xsl:value-of select="$keyWordLower" disable-output-escaping="yes"/>
</span>
<!--Recursive call to create the string after keyword-->
<xsl:call-template name="ReplaceSections">
<xsl:with-param name="contentText" select="substring-after($contentText,$keyWordLower)"/>
<xsl:with-param name="keyWordLower" select="$keyWordLower"/>
</xsl:call-template>
</xsl:when> <xsl:otherwise>
<xsl:value-of select="$contentText"/>
</xsl:otherwise>
</xsl:choose>
This transformation:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:param name="pKeyWords">
<kw>Marine weather</kw>
<kw>marine weather</kw>
<kw>Marine Weather</kw>
</xsl:param>
<xsl:variable name="vKeyWords" select=
"document('')/*/xsl:param[#name='pKeyWords']/*"/>
<xsl:template match="/*">
<t><xsl:apply-templates/></t>
</xsl:template>
<xsl:template match="text()" name="highlightKWs">
<xsl:param name="pText" select="."/>
<xsl:if test="not($vKeyWords[contains($pText,.)])">
<xsl:value-of select="$pText"/>
</xsl:if>
<xsl:apply-templates select="$vKeyWords[contains($pText,.)]">
<xsl:sort select="string-length(substring-before($pText,.))"
data-type="number"/>
<xsl:with-param name="pText" select="$pText"/>
</xsl:apply-templates>
</xsl:template>
<xsl:template match="kw">
<xsl:param name="pText"/>
<xsl:if test="position()=1">
<xsl:value-of select="substring-before($pText, .)"/>
<span class="texthighlight">
<xsl:value-of select="."/>
</span>
<xsl:call-template name="highlightKWs">
<xsl:with-param name="pText" select="substring-after($pText, .)"/>
</xsl:call-template>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
when applied on the following XML document:
<t>Marine weather forecasts,
warnings, synopsis, and ice conditions.
Hundreds of land and buoy station observations
across and marine weather forecasts, warnings,
synopsis, and ice conditions. Hundreds of land
and buoy station observations across.</t>
produces the wanted, correct result:
<t>
<span class="texthighlight">Marine weather</span> forecasts,
warnings, synopsis, and ice conditions.
Hundreds of land and buoy station observations
across and <span class="texthighlight">marine weather</span> forecasts, warnings,
synopsis, and ice conditions. Hundreds of land
and buoy station observations across.</t>
I have an XML file that looks like the following...
<states>
<state>
<name>North Carolina</name>
<city>Charlotte</city>
</state>
<state>
<name>Alaska</name>
<city>Fairbanks</city>
</state>
<state>
<name>Virginia</name>
<city>Leesburg</city>
</state>
<state>
<name>Alaska</name>
<city>Coldfoot</city>
</state>
<state>
<name>North Carolina</name>
<city>Harrisburg</city>
</state>
<state>
<name>Virginia</name>
<city>Ashburn</city>
</state>
</states>
I need to produce a report that lists each state, is alphabetical order with each city following.... such as ..
Alaska - Fairbanks, Coldfoot
North Carolina - Charlotte, Harrisburg
Virginia - Leesburg, Ashburn
(the cities do not have to be in alpha order, just the states)
I tried to solve this by doing a for-each on states/state, sorting it by name and processing it. Like this....
<xsl:for-each select="states/state">
<xsl:sort select="name" data-type="text" order="ascending"/>
<xsl:value-of select="name"/>-<xsl:value-of select="city"/>
</xsl:for-each>
This gave me....
Alaska - Fairbanks
Alaska - Coldfoot
North Carolina - Charlotte
North Carolina - Harrisburg
Virginia - Leesburg
Virginia - Ashburn
The sorting worked, now I want to group. The only thing I could think to do was to compare to the previous state, since it is sorted, it should recognize if the state value has not changed. Like this...
<xsl:for-each select="states/state">
<xsl:sort select="name" data-type="text" order="ascending"/>
<xsl:variable name="name"><xsl:value-of select="name">
<xsl:variable name="previous-name"><xsl:value-of select="(preceding-sibling::state)/name">
<xsl:if test="$name != $previous-name">
<br/><xsl:value-of select="name"/>-
</xsl:if>
<xsl:value-of select="city"/>
</xsl:for-each>
Sadly, it appears that the preceding-sibling feature does not work well with the sort, so, the first time through (on the first Alaska) it saw the first North Carolina as a preceding sibling. This causes some weird results, which were not at all to my liking.
So, I am using XSLT1.0... Any thoughts/suggestions?
Thanks
This stylesheet:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:key name="kStateByName" match="state" use="name"/>
<xsl:output method="text"/>
<xsl:template match="/">
<xsl:apply-templates
select="/*/state[count(.|key('kStateByName',name)[1])=1]">
<xsl:sort select="name"/>
</xsl:apply-templates>
</xsl:template>
<xsl:template match="state">
<xsl:value-of select="concat(name,' - ')"/>
<xsl:apply-templates select="key('kStateByName',name)/city"/>
</xsl:template>
<xsl:template match="city">
<xsl:value-of select="concat(.,substring(', ',
1 div (position()!=last())),
substring('
',
1 div (position()=last())))"/>
</xsl:template>
</xsl:stylesheet>
Output:
Alaska - Fairbanks, Coldfoot
North Carolina - Charlotte, Harrisburg
Virginia - Leesburg, Ashburn
Note: Grouping by State's name. Separator substring expression only works with a pull style (applying templates to city)
An XSLT 2.0 solution:
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:template match="states">
<xsl:for-each-group select="state" group-by="name">
<xsl:sort select="name"/>
<xsl:value-of select="concat(name,
' - ',
string-join(current-group()/city,', '),
'
')"/>
</xsl:for-each-group>
</xsl:template>
</xsl:stylesheet>
Just for fun, this XPath 2.0 expression:
string-join(for $state in distinct-values(/*/*/name)
return concat($state,
' - ',
string-join(/*/*[name=$state]/city,
', ')),
'
')
For grouping in XSLT 1.0 you are probably going to have to use the Muenchian Method. It can be hard to understand but once you get it working you should be good to go.
This will return a distinct list of states:
<xsl:for-each select="states/state">
<xsl:sort select="name" />
<xsl:if test="not(name = preceding-sibling::state/name)" >
<xsl:value-of select="name" />
</xsl:if>
</xsl:for-each>
I used your example XML, built a little style sheet with the above, ran it through Xalan-j, and it returns:
Alaska
North Carolina
Virginia
So from there you should be able to apply a template or another for-each loop to pull the list of cities for each distinct state.
Chris