I want to convert the raw file into the below format using Java -
Raw Input:
state | abc
country | FR-FRA
Output:
<data attr ="StateFr">abc</data>
<data attr ="country">FR-FRA</data>
State attribute should be appended with the country code shown above. Can someone help me regarding this.
Java Stream API can help
String raw ="name1|value1\n" +
"name2|value2";
String template = "<data attribute=\"%s\">%s</data>";
String output = Arrays.stream(raw.split("\n"))
.map(rawPair -> rawPair.split("\\|"))
.map(pair -> String.format(template, pair[0], pair[1]))
.collect(Collectors.joining("\n"));
will output
<data attribute="name1">value1</data>
<data attribute="name2">value2</data>
But having specific business logic requires a bit more movements. Get country code first and then decorate you attribute name on stream processing
BiFunction<String, String, String> decorate = (String name, String code) -> {
if ("state".equals(name)) {
return name + code;
} else {
return name;
}
};
Function<String, String> countryCode = (String source) -> {
String head = "country|";
int start = source.indexOf(head) + head.length();
return source.substring(start, start + 2);
};
String code = countryCode.apply(raw);
...
.map(pair -> String.format(template, decorate.apply(pair[0], code), pair[1]))
...
With new requirements
a raw file is large
a raw file has the country code coming next to state
reading file one by one.
it is also required to output transformed entries in the same order they appear in the raw source.
you should
recognize sate and keep it, not producing next entry yet
recognize subsequent country, update state kept and release both state and contry entries
so here I employ sort of shallow buffer for this role
String raw = "name|value1\n" +
"state|some-state1\n" +
"country|fr-fra\n" +
"name|value2\n" +
"state|some-state2\n" +
"country|en-us\n";
class ShallowBuffer {
private String stateKey = "state";
private String countryKey = "country";
private String[] statePairWaitingForCountryCode = null;
private List<String[]> pump(String[] pair) {
if (stateKey.equals(pair[0])) {
statePairWaitingForCountryCode = pair;
return Collections.emptyList();
}
if (countryKey.equals(pair[0])) {
statePairWaitingForCountryCode[0] = statePairWaitingForCountryCode[0] + pair[1].substring(0, 2);
String[] stateRelease = statePairWaitingForCountryCode;
statePairWaitingForCountryCode = null;
return Arrays.asList(stateRelease, pair);
}
return Collections.singletonList(pair);
}
}
ShallowBuffer patience = new ShallowBuffer();
String template = "<data attribute=\"%s\">%s</data>";
String output = Arrays.stream(raw.split("\n"))
.map(rawPair -> rawPair.split("\\|"))
.map(patience::pump)
.flatMap(Collection::stream)
.map(pair -> String.format(template, pair[0], pair[1]))
.collect(Collectors.joining("\n"));
this will output
<data attribute="name">value1</data>
<data attribute="statefr">some-state1</data>
<data attribute="country">fr-fra</data>
<data attribute="name">value2</data>
<data attribute="stateen">some-state2</data>
<data attribute="country">en-us</data>
Shallow buffer is mutable, so you cannot use parallel methods in your stream-chain.
It also mean marking it accesible out of the scope will require synchronisation work.
And you still need to capitalize the first letter of a country code )
Run the following XSLT 3.0 stylesheet:
<xsl:transform xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="3.0" expand-text="yes" xmlns:f="f">
<xsl:template name="xsl:initial-template">
<root>
<xsl:iterate select="unparsed-text-lines('input.txt')">
<xsl:param name="prev-parts" select="()"/>
<xsl:on-completion>
<attribute name="{$prev-parts[1]}">{$prev-parts[2]}</attribute>
</xsl:on-completion>
<xsl:variable name="parts" select="tokenize(., '\|')"/>
<xsl:choose>
<xsl:when test="$parts[1] = 'country'">
<attribute name="{f:titleCase($prev-parts[1])}{f:titleCase(substring-before($parts[2], '-')}">{$prev-parts[2]}</attribute>
</xsl:when>
<xsl:otherwise>
<attribute name="{$prev-parts[1]}>{$prev-parts[2]}</attribute>
</xsl:otherwise>
</xsl:choose>
<xsl:next-iteration>
<xsl:with-param name="prev-parts" select="$parts"/>
</xsl:next-iteration>
</xsl:iterate>
</root>
</xsl:template>
<xsl:function name="f:titleCase">
<xsl:param name="in"/>
<xsl:sequence select="upper-case(substring($in, 1, 1))||substring($in, 2)"/>
</xsl:function>
</xsl:transform>
Note that unlike other solutions presented here, this one will always produce well-formed XML output. (We see an awful lot of problems on StackOverflow from people receiving so-called XML that has been incorrectly generated because it ignores the problem of escaping special characters.)
Related
How do you edit values on xml that has been appended to a stringbuilder?
We have an xml file looking like the following, which we eventually reads in Java:
<?xml version="1.0" encoding="UTF-8"?>
<urn:receive
xmlns:urn="urn:xxx"
xmlns:ns="xxx"
xmlns:ns1="xxx"
xmlns:urn1="urn:xxx">
<urn:give>
<urn:giveNumber>
<ns1:number>12345678</ns1:number>
</urn:giveNumber>
<urn:giveDates>
<urn1:dateFrom>2021-07-01</urn1:dateFrom>
<urn1:dateTo>2021-09-30</urn1:dateTo>
</urn:giveDates>
</urn:give>
</urn:receive>
The following is a snippet of code that we use to read an xml file by appending to a stringbuilder and eventually saving it to a string with .toString(). Do notice that there is an int for number and string for startDate and for endDate. These values must be inserted into the xml, and replace the number and dates. Keep in mind that we are not allowed to edit the xml file.
public class test {
// Logger to print output in commandprompt
private static final Logger LOGGER = Logger.getLogger(test.class.getName());
public void changeDate() {
number = 44444444;
startDate = "2021-01-01";
endDate = "2021-03-31";
try {
// the XML file for this example
File xmlFile = new File("requests/dates.xml");
Reader fileReader = new FileReader(xmlFile);
BufferedReader bufReader = new BufferedReader(fileReader);
StringBuilder sb = new StringBuilder();
String line = bufReader.readLine();
while( line != null ) {
sb.append(line).append("\n");
line = bufReader.readLine();
}
String request = sb.toString();
LOGGER.info("Request" + request);
} catch (Exception e) {
e.printStackTrace();
}
}
}
How do we replace the number and dates in the xml with number, startDate and endDate, but without editing the xml file?
LOGGER.info("Request" + request); should print the following:
<?xml version="1.0" encoding="UTF-8"?>
<urn:receive
xmlns:urn="urn:xxx"
xmlns:ns="xxx"
xmlns:ns1="xxx"
xmlns:urn1="urn:xxx">
<urn:give>
<urn:giveNumber>
<ns1:number>44444444</ns1:number>
</urn:giveNumber>
<urn:giveDates>
<urn1:dateFrom>2021-01-01</urn1:dateFrom>
<urn1:dateTo>2021-03-31</urn1:dateTo>
</urn:giveDates>
</urn:give>
</urn:receive>
Simple answer: you don't.
You need to parse the XML, and parsing the XML can be done perfectly easily by supplying the parser with the file name; reading the XML into a StringBuilder first is pointless effort.
The easiest way to make a small change to an XML document is to use XSLT, which can be easily invoked from Java. Java comes with an XSLT 1.0 processor built in. XSLT 1.0 is getting rather ancient and you might prefer to use XSLT 3.0 which is much more powerful but requires a third-party library; but for a simple job like this, 1.0 is quite adequate. The stylesheet needed consists of a general rule that copies things unchanged:
<xsl:template match="*">
<xsl:copy><xsl:apply-templates/></xsl:copy>
</xsl:template>
and then a couple of rules for changing the things you want to change:
<xsl:param name="number"/>
<xsl:param name="startDate"/>
<xsl:param name="endDate"/>
<xsl:template match="ns1:giveNumber/text()" xmlns:ns1="xxx">
<xsl:value-of select="$number"/>
</xsl:template>
<xsl:template match="urn1:dateFrom/text()" xmlns:urn1="urn:xxx">
<xsl:value-of select="$dateFrom"/>
</xsl:template>
<xsl:template match="urn1:dateTo/text()" xmlns:urn1="urn:xxx">
<xsl:value-of select="$dateTo"/>
</xsl:template>
and then you just run the transformation from Java as described at https://docs.oracle.com/javase/tutorial/jaxp/xslt/transformingXML.html, supplying values for the parameters.
I have a program to perform XML mapping using XSLT. I'm using Saxon-HE-9.7 library for this. I'm also using reflexive extension functions in XSLT.
The XSLT calls a java function that returns ArrayList<HashMap<String, String>>
<?xml version="1.0"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="2.0" xmlns:SQLService="com.db.SQLService" xmlns:ArrayList="java:java.util.ArrayList" xmlns:HashMap="java.util.HashMap" >
<xsl:output method="xml" indent="yes" />
<xsl:variable name="city">Texas</xsl:variable>
<xsl:variable name="query" select="'Select name, emp_id from employee where city = ?'" />
<xsl:variable name="list" select="SQLService:executeQueryMultiResult($query, $city)" />
<xsl:template match="/">
<test>
<xsl:for-each select="abc/company[#type='product']">
<employee>
<xsl:attribute name="details">
<xsl:value-of select="$list" />
</xsl:attribute>
</employee>
</xsl:for-each>
</test>
</xsl:template>
</xsl:stylesheet>
I'm getting only sinlge record in the list which is the last record of the list returned by executeQueryMultiResult.
I want to store and iterate all the elements of the list?
Firstly, I'm a bit surprised that when you iterate over abc/company[#type='product'], the body of the xsl:for-each doesn't depend in any way on the current selected company. This means that each iteration of this loop will produce exactly the same output.
Under the default Java-to-XPath conversions, the ArrayList should be converted to an XPath sequence, but the java Maps will not be converted to XPath maps; they need to be accessed as external objects.
See what count($list) returns and check that it matches your expectations.
LATER
I am unable to reproduce the problem. I tested it like this:
public void testListOfMaps() {
try {
Processor p = new Processor(true);
XsltCompiler c = p.newXsltCompiler();
String s = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
"<xsl:stylesheet version=\"3.0\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\"\n" +
" xmlns:jf=\"java:s9apitest.TestReflexionHelper\">\n" +
" <xsl:output method=\"text\" />\n" +
" <xsl:template name='main'>\n" +
" <xsl:variable name=\"theList\" select=\"jf:getListOfMaps('Weds', 'Wednesday')\" />\n" +
" <xsl:value-of select=\"count($theList)\" />\n" +
" <xsl:value-of select=\"Q{java:java.util.Map}get($theList[1], 'Weds')\" />\n" +
" </xsl:template>\n" +
"</xsl:stylesheet>";
XsltTransformer t = c.compile(new StreamSource(new StringReader(s))).load();
StringWriter out = new StringWriter();
Serializer ser = p.newSerializer(out);
t.setDestination(ser);
t.setInitialTemplate(new QName("main"));
t.transform();
assertTrue(out.toString().equals("2Wednesday"));
} catch (SaxonApiException e1) {
fail(e1.getMessage());
}
}
where the extension function jf:getListOfMaps() is:
public static List<Map<String, String>> getListOfMaps(String x, String y) {
Map<String, String> m = new HashMap<>();
m.put("Mon", "Monday");
m.put("Tues", "Tuesday");
m.put(x, y);
Map<String, String> n = new HashMap<>();
m.put("Jan", "January");
m.put("Feb", "February");
List<Map<String, String>> list = new ArrayList<>();
list.add(m);
list.add(n);
return list;
}
The test demonstrates that Saxon is behaving according to the spec: the Java List of Maps is converted to an XPath sequence of external objects, where the external object is a wrapper around the Java Map that allows use of the underlying Java methods.
I ran this on Saxon 9.9 (9.7 is no longer supported).
I suggest you try and produce a repro that simplifies the problem by replacing your extension function with a dummy stub with the same signature that anyone can run for testing.
I also suggest you tell us exactly what your environment is. I'm a bit puzzled that you say you are using Saxon-HE, because Saxon-HE doesn't support reflexive extension functions.
I would like to create an enum class using a file.
I hope to make maintenance easier.
txt example:
//name of the enum instance and devided by a '-' are the parameter values:
JOHN-23
ANNA-19
xml example:
<friends>
<friend name="JOHN">
<age>23</age>
</friend>
<friend name="ANNA">
<age>19</age>
</friend>
</friends>
I would like to have an enum akting like this one:
enum Friends {
JOHN(23),
ANNA(19);
private int age;
Friends(int age) {
this.age = age;
}
}
You can do it with an XSLT transformation and call out to SAXON via a task in your build system.
e.g. applying this to your example XML will result in your example enum code
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="2.0">
<xsl:output method="text" indent="no"/>
<xsl:variable name="classname"><xsl:sequence select="concat(upper-case(substring(/*/local-name(),1,1)), substring(/*/local-name(), 2), ' '[not(last())])"/> </xsl:variable>
<xsl:template match="/*">
enum <xsl:value-of select="$classname"/>
{<xsl:for-each select="*"><xsl:if test="position()!=1">,</xsl:if><xsl:text>
</xsl:text><xsl:value-of select="#name"/>(<xsl:for-each select="*"><xsl:if test="position()!=1">, </xsl:if><xsl:value-of select="text()"/></xsl:for-each>)</xsl:for-each>;
<xsl:for-each select="*[1]/*"> private int <xsl:value-of select="local-name()"/>;
</xsl:for-each><xsl:text>
</xsl:text><xsl:value-of select="$classname"/>(<xsl:for-each select="*[1]/*"><xsl:if test="position()!=1">, </xsl:if>int <xsl:value-of select="local-name()"/></xsl:for-each>)
{
<xsl:for-each select="*[1]/*"> this.<xsl:value-of select="local-name()"/> = <xsl:value-of select="local-name()"/>;
</xsl:for-each> }
}
</xsl:template>
</xsl:stylesheet>
However,
it would break if the XML didn't have the same number of parameters for each enum value.
your input is encoding type names and field names as element names, whereas it's easier for metamodels to encode them as attributes
it's easier to write transforms for explicit rather than implicit information (i.e. say that you have an int age parameter rather than just happening to have age elements whose content is a string of decimal digits)
if you move on to anything a bit more complicated, such as generating hierarchies of classes, the queries to resolve overloads and inheritance rapidly go past simple XSLT
Here an example how you can generate the enum with StAX in java:
package codegen;
import java.io.FileWriter;
import java.io.IOException;
import java.net.URL;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
public class GenCode {
public static void main(String[] args) throws XMLStreamException, IOException {
URL xmlFile = GenCode.class.getResource("Friends.xml");
XMLInputFactory inFactory = XMLInputFactory.newFactory();
XMLStreamReader reader = inFactory.createXMLStreamReader(xmlFile.openStream());
try (FileWriter out = new FileWriter("generated/codegen/Friends.java")) {
out.write("package codegen;\n");
out.write("\n");
out.write("public enum Friends {\n");
String friendName = null;
boolean inAge = false;
String sep = "\t";
while (reader.hasNext()) {
switch (reader.next()) {
case XMLStreamReader.START_ELEMENT:
if (reader.getLocalName().equals("friend"))
friendName = reader.getAttributeValue(null, "name");
if (reader.getLocalName().equals("age"))
inAge = true;
break;
case XMLStreamReader.CHARACTERS:
if (inAge) {
out.write(sep + friendName + "_" + reader.getText());
sep = ",\n\t";
}
break;
case XMLStreamReader.END_ELEMENT:
if (reader.getLocalName().equals("age"))
inAge = false;
break;
}
}
out.write("\n}");
}
}
}
You might need to change some paths.
You have to compile this file, invoke it with java, which will create the Friends.java with the enum and then compile the rest.
In MS-Word 2010 there is an Option under File -> Information to check the document for problems before sharing it. This makes it possible to handle track changes (to new newest version) and remove all comments and annotations from the document at once.
Is this possibility available in docx4j as well or do I need to investiagte the corresponding JAXB-Objects and write a traverse finder?
Doing that manually could be a lot of work since I would have to add the RunIns (w:ins) to the R (w:r) and remove the RunDel (w:del). I also saw a w:del once inside a w:ins. In this case I don't know if this also appears vice versa or in deeper nestings.
Further research brought this XSLT up:
https://github.com/plutext/docx4all/blob/master/docx4all/src/main/java/org/docx4all/util/ApplyRemoteChanges.xslt
I was not able to run this within docx4j but by manually unzipping the docx and extracting the document.xml. After applying the xslt on the plain document.xml I wrapped it in the docx container again to open it with MS-Word. The result was not the same as it would be by accepting the revision with MS-Word itself. More concrete: The XSLT removed the deleted marked text (in a Table), but not a listing dot before the text. This appears quite often in my document.
If this request is not posible to solve in an easy manner, I will change the constraints. It is sufficent for me to have a method for getting all text of a ContentAccessor, as a String. The ContentAccessor could be a P or Tc. The String shall be inside a R there or inside a RunIns (with R inside of that) For this I have a half solution below. The intersting part starts in the line of else if (child instanceof RunIns) {. But as mentioned above I'm not sure how nested del/ins Statements might appear and if this will handle them well. And the results are still not the same as if I would prepare the document with MS-Word before.
//Similar to:
//http://www.docx4java.org/forums/docx-java-f6/how-to-get-all-text-element-of-a-paragraph-with-docx4j-t2028.html
private String getAllTextfromParagraph(ContentAccessor ca) {
String result = "";
List<Object> children = ca.getContent();
for (Object child : children) {
child = XmlUtils.unwrap(child);
if (child instanceof Text) {
Text text = (Text) child;
result += text.getValue();
} else if (child instanceof R) {
R run = (R) child;
result += getTextFromRun(run);
}
else if (child instanceof RunIns) {
RunIns ins = (RunIns) child;
for (Object obj : ins.getCustomXmlOrSmartTagOrSdt()) {
if (obj instanceof R) {
result += getTextFromRun((R) obj);
}
}
}
}
return result.trim();
}
private String getTextFromRun(R run) {
String result = "";
for (Object o : run.getContent()) {
o = XmlUtils.unwrap(o);
if (o instanceof R.Tab) {
Text text = new Text();
text.setValue("\t");
result += text.getValue();
}
if (o instanceof R.SoftHyphen) {
Text text = new Text();
text.setValue("\u00AD");
result += text.getValue();
}
if (o instanceof Br) {
Text text = new Text();
text.setValue(" ");
result += text.getValue();
}
if (o instanceof Text) {
result += ((Text) o).getValue();
}
}
return result;
}
https://github.com/plutext/docx4j/commit/309a8e4008553452ebe675e81def30aab97542a2?w=1 adds a method for transforming just one Part, and sample code to use it to accept changes.
The XSLT is just what you found (relicensed as Apache 2):
<?xml version="1.0" encoding="UTF-8" ?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
xmlns:o="urn:schemas-microsoft-com:office:office"
xmlns:v="urn:schemas-microsoft-com:vml"
xmlns:WX="http://schemas.microsoft.com/office/word/2003/auxHint"
xmlns:aml="http://schemas.microsoft.com/aml/2001/core"
xmlns:w10="urn:schemas-microsoft-com:office:word"
xmlns:pkg="http://schemas.microsoft.com/office/2006/xmlPackage"
xmlns:msxsl="urn:schemas-microsoft-com:xslt"
xmlns:ext="http://www.xmllab.net/wordml2html/ext"
xmlns:java="http://xml.apache.org/xalan/java"
xmlns:xml="http://www.w3.org/XML/1998/namespace"
version="1.0"
exclude-result-prefixes="java msxsl ext o v WX aml w10">
<xsl:output method="xml" encoding="utf-8" omit-xml-declaration="no" indent="yes" />
<xsl:template match="/ | #*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="w:del" />
<xsl:template match="w:ins" >
<xsl:apply-templates select="*"/>
</xsl:template>
</xsl:stylesheet>
You'll need to add support for the other elements identified in the MSDN link. If you do that, I'd be happy to get a pull request
I want to be able to generate a complete XML file, given a set of XPath mappings.
The input could specified in two mappings: (1) One which lists the XPath expressions and values; and (2) the other which defines the appropriate namespaces.
/create/article[1]/id => 1
/create/article[1]/description => bar
/create/article[1]/name[1] => foo
/create/article[1]/price[1]/amount => 00.00
/create/article[1]/price[1]/currency => USD
/create/article[2]/id => 2
/create/article[2]/description => some name
/create/article[2]/name[1] => some description
/create/article[2]/price[1]/amount => 00.01
/create/article[2]/price[1]/currency => USD
For namespaces:
/create => xmlns:ns1='http://predic8.com/wsdl/material/ArticleService/1/
/create/article => xmlns:ns1='http://predic8.com/material/1/‘
/create/article/price => xmlns:ns1='http://predic8.com/common/1/‘
/create/article/id => xmlns:ns1='http://predic8.com/material/1/'
Note also, that it is important that I also deal with XPath Attributes expressions as well. For example: I should also be able to handle attributes, such as:
/create/article/#type => richtext
The final output should then look something like:
<ns1:create xmlns:ns1='http://predic8.com/wsdl/material/ArticleService/1/'>
<ns1:article xmlns:ns1='http://predic8.com/material/1/‘ type='richtext'>
<name>foo</name>
<description>bar</description>
<ns1:price xmlns:ns1='http://predic8.com/common/1/'>
<amount>00.00</amount>
<currency>USD</currency>
</ns1:price>
<ns1:id xmlns:ns1='http://predic8.com/material/1/'>1</ns1:id>
</ns1:article>
<ns1:article xmlns:ns1='http://predic8.com/material/2/‘ type='richtext'>
<name>some name</name>
<description>some description</description>
<ns1:price xmlns:ns1='http://predic8.com/common/2/'>
<amount>00.01</amount>
<currency>USD</currency>
</ns1:price>
<ns1:id xmlns:ns1='http://predic8.com/material/2/'>2</ns1:id>
</ns1:article>
</ns1:create>
PS: This is a more detailed question to a previous question asked, although due to a series of further requirements and clarifications, I was recommended to ask a more broader question in order to address my needs.
Note also, I am implementing this in Java. So either a Java-based or XSLT-based solution would both be perfectly acceptable. thnx.
Further note: I am really looking for a generic solution. The XML shown above is just an example.
This problem has an easy solution if one builds upon the solution of the previous problem:
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:my="my:my">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:key name="kNSFor" match="namespace" use="#of"/>
<xsl:variable name="vStylesheet" select="document('')"/>
<xsl:variable name="vPop" as="element()*">
<item path="/create/article/#type">richtext</item>
<item path="/create/article/#lang">en-us</item>
<item path="/create/article[1]/id">1</item>
<item path="/create/article[1]/description">bar</item>
<item path="/create/article[1]/name[1]">foo</item>
<item path="/create/article[1]/price[1]/amount">00.00</item>
<item path="/create/article[1]/price[1]/currency">USD</item>
<item path="/create/article[1]/price[2]/amount">11.11</item>
<item path="/create/article[1]/price[2]/currency">AUD</item>
<item path="/create/article[2]/id">2</item>
<item path="/create/article[2]/description">some name</item>
<item path="/create/article[2]/name[1]">some description</item>
<item path="/create/article[2]/price[1]/amount">00.01</item>
<item path="/create/article[2]/price[1]/currency">USD</item>
<namespace of="create" prefix="ns1:"
url="http://predic8.com/wsdl/material/ArticleService/1/"/>
<namespace of="article" prefix="ns1:"
url="xmlns:ns1='http://predic8.com/material/1/"/>
<namespace of="#lang" prefix="xml:"
url="http://www.w3.org/XML/1998/namespace"/>
<namespace of="price" prefix="ns1:"
url="xmlns:ns1='http://predic8.com/material/1/"/>
<namespace of="id" prefix="ns1:"
url="xmlns:ns1='http://predic8.com/material/1/"/>
</xsl:variable>
<xsl:template match="/">
<xsl:sequence select="my:subTree($vPop/#path/concat(.,'/',string(..)))"/>
</xsl:template>
<xsl:function name="my:subTree" as="node()*">
<xsl:param name="pPaths" as="xs:string*"/>
<xsl:for-each-group select="$pPaths" group-adjacent=
"substring-before(substring-after(concat(., '/'), '/'), '/')">
<xsl:if test="current-grouping-key()">
<xsl:choose>
<xsl:when test=
"substring-after(current-group()[1], current-grouping-key())">
<xsl:variable name="vLocal-name" select=
"substring-before(concat(current-grouping-key(), '['), '[')"/>
<xsl:variable name="vNamespace"
select="key('kNSFor', $vLocal-name, $vStylesheet)"/>
<xsl:choose>
<xsl:when test="starts-with($vLocal-name, '#')">
<xsl:attribute name=
"{$vNamespace/#prefix}{substring($vLocal-name,2)}"
namespace="{$vNamespace/#url}">
<xsl:value-of select=
"substring(
substring-after(current-group(), current-grouping-key()),
2
)"/>
</xsl:attribute>
</xsl:when>
<xsl:otherwise>
<xsl:element name="{$vNamespace/#prefix}{$vLocal-name}"
namespace="{$vNamespace/#url}">
<xsl:sequence select=
"my:subTree(for $s in current-group()
return
concat('/',substring-after(substring($s, 2),'/'))
)
"/>
</xsl:element>
</xsl:otherwise>
</xsl:choose>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="current-grouping-key()"/>
</xsl:otherwise>
</xsl:choose>
</xsl:if>
</xsl:for-each-group>
</xsl:function>
</xsl:stylesheet>
When this transformation is applied on any XML document (not used), the wanted, correct result is produced:
<ns1:create xmlns:ns1="http://predic8.com/wsdl/material/ArticleService/1/">
<ns1:article xmlns:ns1="xmlns:ns1='http://predic8.com/material/1/" type="richtext"
xml:lang="en-us"/>
<ns1:article xmlns:ns1="xmlns:ns1='http://predic8.com/material/1/">
<ns1:id>1</ns1:id>
<description>bar</description>
<name>foo</name>
<ns1:price>
<amount>00.00</amount>
<currency>USD</currency>
</ns1:price>
<ns1:price>
<amount>11.11</amount>
<currency>AUD</currency>
</ns1:price>
</ns1:article>
<ns1:article xmlns:ns1="xmlns:ns1='http://predic8.com/material/1/">
<ns1:id>2</ns1:id>
<description>some name</description>
<name>some description</name>
<ns1:price>
<amount>00.01</amount>
<currency>USD</currency>
</ns1:price>
</ns1:article>
</ns1:create>
Explanation:
A reasonable assumption is made that throughout the generated document any two elements with the same local-name() belong to the same namespace -- this covers the predominant majority of real-world XML documents.
The namespace specifications follow the path specifications. A nsmespace specification has the form: <namespace of="target element's local-name" prefix="wanted prefix" url="namespace-uri"/>
Before generating an element with xsl:element, the appropriate namespace specification is selected using an index created by an xsl:key. From this namespace specification the values of its prefix and url attributes are used in specifying in the xsl:element instruction the values of the full element name and the element's namespace-uri.
Interesting question. Let's assume that your input set of XPath expressions satisfies some reasonsable constraints, for example if there is an X/article[2] then there also (preceding it) an X/article[1]. And let's put the namespace part of the problem to one side for the moment.
Let's go for an XSLT 2.0 solution: we'll start with the input in the form
<paths>
<path value="1">/create/article[1]/id</path>
<path value="bar">/create/article[1]/description</path>
</paths>
and then we'll turn this into
<paths>
<path value="1"><step>create</step><step>article[1]</step><step>id</step></path>
...
</paths>
Now we'll call a function which does a grouping on the first step, and calls itself recursively to do grouping on the next step:
<xsl:function name="f:group">
<xsl:param name="paths" as="element(path)*"/>
<xsl:param name="step" as="xs:integer"/>
<xsl:for-each-group select="$paths" group-by="step[$step]">
<xsl:element name="{replace(current-grouping-key(), '\[.*', '')}">
<xsl:choose>
<xsl:when test="count(current-group) gt 1">
<xsl:sequence select="f:group(current-group(), $step+1)"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="current-group()[1]/#value"/>
</xsl:otherwise>
</xsl:choose>
</xsl:element>
</xsl:for-each-group>
</xsl:function>
That's untested, and there may well be details you have to adjust to get it working. But I think the basic approach should work.
The namespace part of the problem is perhaps best tackled by preprocessing the list of paths to add a namespace attribute to each step element; this can then be used in the xsl:element instruction to put the element in the right namespace.
i came across a similar situation where i had to convert Set of XPath/FQN - value mappings to XML. A generic simple solution can be using the following code, which can be enhanced to specific requirements.
public class XMLUtils {
static public String transformToXML(Map<String, String> pathValueMap, String delimiter)
throws ParserConfigurationException, TransformerException {
DocumentBuilderFactory documentFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder documentBuilder = documentFactory.newDocumentBuilder();
Document document = documentBuilder.newDocument();
Element rootElement = null;
Iterator<Entry<String, String>> it = pathValueMap.entrySet().iterator();
while (it.hasNext()) {
Entry<String, String> pair = it.next();
if (pair.getKey() != null && pair.getKey() != "" && rootElement == null) {
String[] pathValuesplit = pair.getKey().split(delimiter);
rootElement = document.createElement(pathValuesplit[0]);
break;
}
}
document.appendChild(rootElement);
Element rootNode = rootElement;
Iterator<Entry<String, String>> iterator = pathValueMap.entrySet().iterator();
while (iterator.hasNext()) {
Entry<String, String> pair = iterator.next();
if (pair.getKey() != null && pair.getKey() != "" && rootElement != null) {
String[] pathValuesplit = pair.getKey().split(delimiter);
if (pathValuesplit[0].equals(rootElement.getNodeName())) {
int i = pathValuesplit.length;
Element parentNode = rootNode;
int j = 1;
while (j < i) {
Element child = null;
NodeList childNodes = parentNode.getChildNodes();
for (int k = 0; k < childNodes.getLength(); k++) {
if (childNodes.item(k).getNodeName().equals(pathValuesplit[j])
&& childNodes.item(k) instanceof Element) {
child = (Element) childNodes.item(k);
break;
}
}
if (child == null) {
child = document.createElement(pathValuesplit[j]);
if (j == (i - 1)) {
child.appendChild(
document.createTextNode(pair.getValue() == null ? "" : pair.getValue()));
}
}
parentNode.appendChild(child);
parentNode = child;
j++;
}
} else {
// ignore any other root - add logger
System.out.println("Data not processed for node: " + pair.getKey());
}
}
}
TransformerFactory transformerFactory = TransformerFactory.newInstance();
Transformer transformer = transformerFactory.newTransformer();
DOMSource domSource = new DOMSource(document);
// to return a XMLstring in response to an API
StringWriter writer = new StringWriter();
StreamResult result = new StreamResult(writer);
StreamResult resultToFile = new StreamResult(new File("C:/EclipseProgramOutputs/GeneratedXMLFromPathValue.xml"));
transformer.transform(domSource, resultToFile);
transformer.transform(domSource, result);
return writer.toString();
}
public static void main(String args[])
{
Map<String, String> pathValueMap = new HashMap<String, String>();
String delimiter = "/";
pathValueMap.put("create/article__1/id", "1");
pathValueMap.put("create/article__1/description", "something");
pathValueMap.put("create/article__1/name", "Book Name");
pathValueMap.put("create/article__1/price/amount", "120" );
pathValueMap.put("create/article__1/price/currency", "INR");
pathValueMap.put("create/article__2/id", "2");
pathValueMap.put("create/article__2/description", "something else");
pathValueMap.put("create/article__2/name", "Book name 1");
pathValueMap.put("create/article__2/price/amount", "2100");
pathValueMap.put("create/article__2/price/currency", "USD");
try {
XMLUtils.transformToXML(pathValueMap, delimiter);
} catch (ParserConfigurationException | TransformerException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}}
Output:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<create>
<article__1>
<id>1</id>
<name>Book Name</name>
<description>something</description>
<price>
<currency>INR</currency>
<amount>120</amount>
</price>
</article__1>
<article__2>
<description>something else</description>
<name>Book name 1</name>
<id>2</id>
<price>
<currency>USD</currency>
<amount>2100</amount>
</price>
</article__2>
To remove __%num , can use regular expressions on final string. like:
resultString = resultString.replaceAll("(__[0-9][0-9])|(__[0-9])", "");
This would do the cleaning job