Incorrect behaviour of xsl:for-each in Java - java

I'm seeing inconsistent behaviour when applying my XSLT with a Java program compared do testing with XML Spy. Specifically, when using Java, the xsl:for-each directive does not seem to iterate over the entire list. This is not the case when testing with XML Spy.
My XML document:
<metadata xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:fn="http://www.w3.org/2005/xpath-functions">
<maps>
<geographicCoverages>
<coverage key="ON">Ontario</coverage>
<coverage key="MB">Manitoba</coverage>
<coverage key="SK">Saskatchewan</coverage>
<coverage key="AB">Alberta</coverage>
<coverage key="BC">British Columbia</coverage>
<coverage key="YT">Yukon</coverage>
<coverage key="NU">Nunavut</coverage>
</geographicCoverages>
</maps>
<entry>
<string>GEO_COVERAGE</string>
<list>
<string>BC</string>
<string>ON</string>
</list>
</entry>
...
</metadata>
My XSLT:
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:fn="http://www.w3.org/2005/xpath-functions">
<xsl:output method="text" version="1.0" indent="yes"/>
<xsl:key name="geoCoverageKey" match="metadata/maps/geographicCoverages/coverage" use="#key" />
<xsl:template match="/metadata">
<xsl:for-each select="entry[string='GEO_COVERAGE']/list">
<xsl:value-of select="key('geoCoverageKey', string)" separator=", " />
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
When I test the above XSLT with XMLSpy, I get my expected output:
Ontario, British Columbia
My Java test program:
public static void main(String[] args) throws TransformerException {
String xsltPath = "test_migration.xslt";
String xmlPath = "test-migration.xml";
TransformerFactory tFactory = TransformerFactory.newInstance();
Transformer transformer = tFactory.newTransformer(new StreamSource(xsltPath));
StreamResult result = new StreamResult( new StringWriter() );
transformer.transform(new StreamSource(xmlPath), result );
System.out.println( result.getWriter().toString() );
}
However, when I run the above Java test program, the output I get back consists of one value only; Ontario. I'm at a loss as to why I'm getting this difference.

The default processor in the JDK is Xalan. Xalan is an XSLT 1.0 processor. When an XSLT 1.0 processor is given a stylesheet that specifies version="2.0", it is required (under the rules for XSLT 1.0 processors) to ignore attributes that are not defined in XSLT 1.0. The "separator" attribute falls into this category. In XSLT 1.0, xsl:value-of outputs the first node in a node-set and ignores the rest.
The answer is to install Saxon, which provides XSLT 2.0 processing in Java.

Related

Convert XML having multiple for-each case to CSV

I have written a Java Program that converts XML to CSV. But currently it is converting partial only.
FileUtils.writeByteArrayToFile(new File("src/main/resources/excel/Data.xml"), inputFile);
File stylesheet = new File("src/main/resources/excel/Data.xsl");
File xmlSource = new File("src/main/resources/excel/Data.xml");
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document document = builder.parse(xmlSource);
StreamSource stylesource = new StreamSource(stylesheet);
Transformer transformer = TransformerFactory.newInstance().newTransformer(stylesource);
Source source = new DOMSource(document);
String path = "src/main/resources/excel/validatedXmlToCSV.csv";
Result outputTarget = new StreamResult(new File(path));
transformer.transform(source, outputTarget);
This is the Java code that takes XML and XSL and converts it to the CSV.
XML file that I am trying to convert it to CSV is:
<root>
<row>
<TECHNICIANID>AA5263</TECHNICIANID>
<CUID>AA5263</CUID>
<TURFS>
<TURF>
<AREANAME>CA_MILPITAS_ABEL_A</AREANAME>
<DEFAULT>Y</DEFAULT>
<ALTERNATE>Y</ALTERNATE>
</TURF>
<TURF>
<AREANAME>CA_SNJS_WHITE_RD_A</AREANAME>
<DEFAULT>Y</DEFAULT>
<ALTERNATE>Y</ALTERNATE>
</TURF>
</TURFS>
</row>
<row>
<TECHNICIANID>AC1964</TECHNICIANID>
<CUID>AC1964</CUID>
<TURFS>
<TURF>
<AREANAME>CA_MILPITAS_ABEL_A</AREANAME>
<DEFAULT>Y</DEFAULT>
<ALTERNATE>Y</ALTERNATE>
</TURF>
<TURF>
<AREANAME>CA_SNJS_WHITE_RD_A</AREANAME>
<DEFAULT>Y</DEFAULT>
<ALTERNATE>Y</ALTERNATE>
</TURF>
</TURFS>
</row>
</root>
Let me show you how I wrote XSL file that it not working properly:
<?xml version="1.0"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:fo="http://www.w3.org/1999/XSL/Format" >
<xsl:output method="text" omit-xml-declaration="yes" indent="no"/>
<xsl:template match="/">TECHNICIANID,CUID,TURFS/TURF/0/AREANAME,TURFS/TURF/0/DEFAULT,TURFS/TURF/0/ALTERNATE,TURFS/TURF/1/AREANAME,TURFS/TURF/1/DEFAULT,TURFS/TURF/1/ALTERNATE
<xsl:for-each select="/root/row">
<xsl:value-of select="concat(TECHNICIANID,',',CUID,'
')"/>
<xsl:for-each select="/TURFS/TURF">
<xsl:value-of select="concat(AREANAME,',',DEAFAULT,',',ALTERNATE,',
')"/>
</xsl:for-each>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
Currently while converting it is printing row TECHNICIANID and CUID only. Other it is not printing.
Expected Output:
TECHNICIANID,CUID,TURFS/TURF/0/AREANAME,TURFS/TURF/0/DEFAULT,TURFS/TURF/0/ALTERNATE,TURFS/TURF/1/AREANAME,TURFS/TURF/1/DEFAULT,TURFS/TURF/1/ALTERNATE
AA5263,AA5263,CA_MILPITAS_ABEL_A,Y,Y,CA_SNJS_WHITE_RD_A,Y,Y
AC1964,AC1964,CA_MILPITAS_ABEL_A,Y,Y,CA_SNJS_WHITE_RD_A,Y,Y
I guess the inner <xsl:for-each select="/TURFS/TURF"> should be <xsl:for-each select="TURFS/TURF">. And concat(AREANAME,',',DEAFAULT should be concat(AREANAME,',',DEFAULT.
I believe you could do simply:
XSLT 2.0
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" encoding="UTF-8"/>
<xsl:template match="/root">
<!-- header -->
<xsl:text>TECHNICIANID,CUID,TURFS/TURF/0/AREANAME,TURFS/TURF/0/DEFAULT,TURFS/TURF/0/ALTERNATE,TURFS/TURF/1/AREANAME,TURFS/TURF/1/DEFAULT,TURFS/TURF/1/ALTERNATE
</xsl:text>
<!-- data -->
<xsl:for-each select="row">
<xsl:value-of select=".//*[not(*)]" separator=","/>
<xsl:text>
</xsl:text>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
Demo: https://xsltfiddle.liberty-development.net/bF2MmXG/1
If you want to pick the values explicitly, then change:
<xsl:value-of select=".//*[not(*)]" separator=","/>
to:
<xsl:value-of select="TECHNICIANID, CUID, TURFS/TURF[1]/(AREANAME, DEFAULT, TALTERNATE), TURFS/TURF[2]/(AREANAME, DEFAULT, TALTERNATE)" separator=","/>
Your problem involves extracting data from a multilevel XML file and convert it to a CSV file. The Java code for this is complicated.
It is convenient to perform the conversion using SPL, the open-source Java package. You just need several lines of code:
A
1
=xml(file("Data.xml").read(),"root/row").conj(TECHNICIANID|CUID|TURFS.TURF.conj(~.array()))
2
=create(TECHNICIANID,CUID,TURFS/TURF/0/AREANAME,TURFS/TURF/0/DEFAULT,TURFS/TURF/0/ALTERNATE,TURFS/TURF/1/AREANAME,TURFS/TURF/1/DEFAULT,TURFS/TURF/1/ALTERNATE).record(A1)
3
=file("validatedXmlToCSV.csv").export#ct(A2)
SPL offers JDBC driver to be invoked by Java. Just store the above SPL script as xml2csv.splx and invoke it in Java as you call a stored procedure:
…
Class.forName("com.esproc.jdbc.InternalDriver");
con= DriverManager.getConnection("jdbc:esproc:local://");
st=con.prepareCall("call xml2csv()");
st.execute();
…

Java XML and XSLT transformation error: Namespace for prefix 'm' has not been declared

I'm trying to transform an XML document with the javax.xml.transform.Transformer and XSLT, and am having trouble with a namespace prefix that is not recognized when I call the transform method.
Here is the XML document which has the "m" namespace prefix defined in it:
<?xml version="1.0" encoding="UTF-8"?>
<edmx:Edmx xmlns:edmx="http://schemas.microsoft.com/ado/2007/06/edmx" Version="1.0">
<edmx:DataServices xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata" m:DataServiceVersion="1.0">
<Schema xmlns="http://schemas.microsoft.com/ado/2008/09/edm" Namespace="SITUATION">
</Schema>
</edmx:DataServices>
</edmx:Edmx>
Here is the XSL that currently just copies the whole XML document (later I'd like to extend it to merge in another XML document, similar to what is described here: https://stackoverflow.com/a/5706319/208011):
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<!-- Copy everything including attributes as default action -->
<xsl:template match="*">
<xsl:element name="{name()}">
<xsl:apply-templates select="#*" />
<xsl:apply-templates />
</xsl:element>
</xsl:template>
<xsl:template match="#*">
<xsl:attribute name="{name()}"><xsl:value-of select="." /></xsl:attribute>
</xsl:template>
</xsl:stylesheet>
Here is the java code:
Source xsltSource = new StreamSource(new File(getClass().getClassLoader().getResource("merge-metadata.xsl").getFile()));
transformer = transFact.newTransformer(xsltSource);
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware(true);
Document input = dbf.newDocumentBuilder().parse(new InputSource(new StringReader(s1)));
StringWriter out = new StringWriter();
transformer.transform(new DOMSource(input), new StreamResult(out));
Here's the stack trace:
Caused by: java.lang.RuntimeException: Namespace for prefix 'm' has not been declared.
at com.sun.org.apache.xml.internal.serializer.SerializerBase.getNamespaceURI(SerializerBase.java:915)
at com.sun.org.apache.xml.internal.serializer.SerializerBase.addAttribute(SerializerBase.java:431)
at com.sun.org.apache.xml.internal.serializer.ToUnknownStream.addAttribute(ToUnknownStream.java:316)
Eventually I want to merge this XML document with another, but I can't get past the namespace issue to just copy this one.
If I don't use the xsl stylesheet in the transformer factory newTransformer() method then I don't get the namespace error and the output of the transformation is exactly the same as the original XML document.
If I set the DocumentBuilderFactory to not be namespace aware then I don't get the exception, but the output of the transformation is missing the namespaces.
Thanks for your help.
Your hand-crafted, pseudo-identity transform is losing the namespace declarations and failing when it tries to name an element with an undeclared namespace prefix:
<xsl:element name="{name()}">
Use the standard identity transformation instead:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>

Exclude processing instruction from XSLT

I have a simple plain-text-generating XSLT that I am applying like this (using the reference implementation):
StreamSource schemasource = new StreamSource(getClass().getResourceAsStream("source.xml"));
StreamSource stylesource = new StreamSource(getClass().getResourceAsStream("transform.xslt"));
Transformer transformer = TransformerFactory.newInstance().newTransformer(stylesource);
StringWriter writer = new StringWriter();
transformer.transform(schemasource, new StreamResult(domWriter));
System.out.println("XML after transform:");
System.out.println(writer.toString());
The "transform" call always inserts an processing instruction in the output. Is there any way to configure it not to do so?
(For example for a very simple node identity transform
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xsl:template match="*">
<xsl:copy/>
<xsl:apply-templates/>
</xsl:template>
</xsl:stylesheet>
when applied to
<hello/>
I get
<?xml version="1.0" encoding="UTF-8"?>
<hello/>
)
Many thanks, Andy
Strictly speaking the <?xml declaration is not a processing instruction. However, you should be able to use the xsl:output command here to specify that no xml declaration should be output:
<xsl:output method="xml" omit-xml-declaration="yes" indent="yes" />
This should be placed as a direct child of the xsl:stylesheet element.

xmlns attribute not getting set in xslt transformation

I am passing a stylesheet and an input stream to Transformer class to get output XML.
The relative variable passed to xmlns is not getting replaced in the result.
Input Stream:
"http://www.abc.com/foo/bar"
+"EventCommon"
+"application,xsd:string"
+"companyId,xsd:string"
+"operator,xsd:string"
+"today,xsd:date"
+"transactionStage,xsd:string"
Stylesheet:
<xsl:stylesheet
version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
>
<xsl:output method="xml" encoding="UTF-8" indent="yes" />
<xsl:template match="/">
<xsl:variable name="var_main_namespace" select="array/data[#attribute=1][#value=1][#subvalue=1]" />
<xsd:schema xmlns="($var_main_namespace)" targetNamespace="{$var_main_namespace}" elementFormDefault="qualified" />
</xsl:template>
</xsl:stylesheet>
Output XML:
<xsd:schema
xmlns="($var_main_namespace)"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://www.abc.com/foo/bar"
elementFormDefault="qualified"
/>
The code snippet:
TransformerFactory tFactory = TransformerFactory.newInstance();
StreamSource sc = new StreamSource(new ByteArrayInputStream(new String(c).getBytes()));
Transformer transformer = tFactory.newTransformer(sc);
ByteArrayOutputStream outputxml = new ByteArrayOutputStream();
transformer.transform(
new StreamSource(
new ByteArrayInputStream(new String(c2).getBytes())
),
new StreamResult(outputxml)
);
Any help is appreciated
Leaving aside the ($...) vs. {$...} typo, this won't work because xmlns is not an attribute, it's a namespace declaration.
Not being attributes, namespace declarations in literal result elements are not treated as attribute value templates and you can't create them using <xsl:attribute>. But as you're apparently using XSLT 2.0 you can create them using <xsl:namespace>
<xsl:template match="/">
<xsl:variable name="var_main_namespace" select="array/data[#attribute=1][#value=1][#subvalue=1]" />
<xsd:schema targetNamespace="{$var_main_namespace}" elementFormDefault="qualified">
<xsl:namespace name="" select="$var_main_namespace" />
</xsd:schema>
</xsl:template>
For this to work with the Java code you've given your default TransformerFactory needs to be one that supports XSLT 2.0, such as Saxon 9.

Create xmlns attribute in the XML using XSLT Transformation

I am trying to add the xmlns attribute to the resulting XML with a value passed by parameter during XSLT transformation using JDK Transformer (Oracle XML v2 Parser or JAXP) but it always defaults to http://www.w3.org/2000/xmlns/
My source XML
<test/>
My XSLT
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns="http://example.com">
<xsl:param name="myNameSpace" select="'http://neilghosh.com'"/>
<xsl:template match="/">
<process>
<xsl:attribute name="xmlns:neil">
<xsl:value-of select="$myNameSpace"/>
</xsl:attribute>
</process>
</xsl:template>
</xsl:stylesheet>
My Result
<?xml version="1.0"?>
<process xmlns="http://www.w3.org/2000/xmlns/" xmlns:neil="neilghosh.com">
</process>
My Desired Result
<?xml version="1.0"?>
<process xmlns="http://example.com" xmlns:neil="neilghosh.com">
</process>
Firstly, in the XSLT data model, you don't want to create an attribute node, you want to create a namespace node.
Namespace nodes are usually created automatically: if you create an element or attribute in a particular namespace, the requisite namespace node (and hence, when serialized, the namespace declaration) are added automatically by the processor.
If you want to create a namespace node that isn't necessary (because it's not used in the name of any element or attribute) then in XSLT 2.0 you can use xsl:namespace. If you're stuck with XSLT 1.0 then there's a workaround, that involves creating an element in the relevant namespace and then copying its namespace node:
<xsl:variable name="ns">
<xsl:element name="neil:dummy" namespace="{$param}"/>
</xsl:variable>
<process>
<xsl:copy-of select="$ns/*/namespace::neil"/>
</process>
Michael Kay provided you with the correct answer, but based on your comments, you aren't sure how to use it in your transformation.
Here is a complete transformation:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:ext="http://exslt.org/common" exclude-result-prefixes="ext">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:param name="pNamespace" select="'neilghosh.com'"/>
<xsl:variable name="vDummy">
<xsl:element name="neil:x" namespace="{$pNamespace}"/>
</xsl:variable>
<xsl:template match="/*">
<xsl:element name="process" namespace="http://example.com">
<xsl:copy-of select="namespace::*"/>
<xsl:copy-of select="ext:node-set($vDummy)/*/namespace::*[.=$pNamespace]"/>
</xsl:element>
</xsl:template>
</xsl:stylesheet>
When this transformation is applied on the provided XML document:
<test/>
the wanted, correct result is produced:
<process xmlns="http://example.com" xmlns:neil="neilghosh.com" />
Namespace declarations in XML are not attributes even though they look like attributes. In XSLT 2.0 you can use <xsl:namespace name="neil" select="$myNameSpace" /> to add a namespace declaration to the result tree dynamically but that feature is not available in XSLT 1.0.
Don't try to create "xmlns" attributes yourself. Create the namespaces in the XSLT and they will be done automatically.
This XSLT works (tested with Saxon 9.4):
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:neil="neilghosh.com"
xpath-default-namespace="http://example.com"
xmlns="http://example.com" version="2.0">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:param name="myDynamicNamespace" select="'http://neilghosh.com'"/>
<xsl:template match="/">
<xsl:element name="process">
<xsl:namespace name="neil" select="$myDynamicNamespace"/>
</xsl:element>
</xsl:template>
</xsl:stylesheet>
And gives the following output:
<?xml version="1.0" encoding="UTF-8"?>
<process xmlns="http://example.com" xmlns:neil="http://neilghosh.com"/>
Finally got an workaround which worked with my XSLT Processor (Oracle XML V2 Parser)
I had to transform it to a DOM Document and then persist that DOM to filesystem instead of outputting directly to StreamResult
I used DOMResult in the transform method
Following XSLT fragment worked but there was an extra xmlns:xmlns="http://www.w3.org/2000/xmlns/" which was probably absorbed by Document and did not appear in the final output when I persisted to file system.
<process>
<xsl:attribute name="xmlns">
<xsl:value-of select="'http://example.com'"/>
</xsl:attribute>
<process>
I know this is not the best way to do but given the parse constraint this is the only choice I have now.

Categories