This is an automated email from the ASF dual-hosted git repository.
hansva pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git
The following commit(s) were added to refs/heads/main by this push:
new 4f9a865091 keep namespace declarations in the Add XML transform, fixes
#3320 (#8258)
4f9a865091 is described below
commit 4f9a8650915f76726a9a8511ce1b28e1825195bc
Author: Bart Maertens <[email protected]>
AuthorDate: Fri Sep 4 16:44:41 2026 +0200
keep namespace declarations in the Add XML transform, fixes #3320 (#8258)
---
.../ROOT/pages/pipeline/transforms/addxml.adoc | 19 +
integration-tests/xml/0020-add-xml-namespace.hpl | 594 +++++++++++++++++++++
.../xml/files/expected/0020-default-namespace.xml | 2 +
.../xml/files/expected/0020-prefixed-namespace.xml | 2 +
.../files/expected/0020-secondary-declaration.xml | 2 +
.../xml/main-0020-add-xml-namespace.hwf | 248 +++++++++
.../hop/pipeline/transforms/xml/addxml/AddXml.java | 94 +++-
.../pipeline/transforms/xml/addxml/AddXmlData.java | 13 +
.../xml/addxml/messages/messages_en_US.properties | 1 +
.../transforms/xml/addxml/AddXmlNamespaceTest.java | 262 +++++++++
10 files changed, 1234 insertions(+), 3 deletions(-)
diff --git
a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/addxml.adoc
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/addxml.adoc
index 27b1c66374..6aab9f258a 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/addxml.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/addxml.adoc
@@ -56,6 +56,25 @@ This XML is added to the row in the form of a String field.
This is often used to limit the size of the target XML.
|===
+=== Namespaces
+
+Namespaces are declared like any other value: add a field in the Fields tab,
mark it as an
+attribute and give it the element name of the declaration. Because the value
comes from the row,
+different rows can use different namespace URIs.
+
+* To put the root element in a default namespace, use the element name
`xmlns`. A root element
+`catalog` with a field `xmlns` holding `https://hop.apache.org` generates
+`<catalog xmlns="https://hop.apache.org">`. Child elements without a prefix
are in that namespace
+too.
+* To bind the root element to a prefix, name the root XML element
`prefix:element` and declare the
+matching `xmlns:prefix` field. A root element `cat:catalog` with a field
`xmlns:cat` generates
+`<cat:catalog xmlns:cat="https://hop.apache.org">`. Unprefixed child elements
are then *not* in
+that namespace; give them the prefix in their element name if they should be.
+* Any further declarations, e.g. `xmlns:xsi`, can be added as attribute fields
in the same way.
+
+A prefixed root XML element without a matching `xmlns:prefix` field is an
error: XML cannot use a
+prefix that is not bound to a namespace URI.
+
=== Fields Tab
The Fields tab is where you configure the output fields and their formats.
diff --git a/integration-tests/xml/0020-add-xml-namespace.hpl
b/integration-tests/xml/0020-add-xml-namespace.hpl
new file mode 100644
index 0000000000..f0b4ee1895
--- /dev/null
+++ b/integration-tests/xml/0020-add-xml-namespace.hpl
@@ -0,0 +1,594 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+-->
+<pipeline>
+ <info>
+ <name>0020-add-xml-namespace</name>
+ <name_sync_with_filename>Y</name_sync_with_filename>
+ <description>Integration test for apache/hop#3320: the Add XML transform
must honour a namespace on the root node, both as a default namespace and as a
prefixed one.</description>
+ <extended_description/>
+ <pipeline_version/>
+ <pipeline_type>Normal</pipeline_type>
+ <parameters>
+ </parameters>
+ <capture_transform_performance>N</capture_transform_performance>
+
<transform_performance_capturing_delay>1000</transform_performance_capturing_delay>
+
<transform_performance_capturing_size_limit>100</transform_performance_capturing_size_limit>
+ <created_user>-</created_user>
+ <created_date>2026/09/03 10:00:00.000</created_date>
+ <modified_user>-</modified_user>
+ <modified_date>2026/09/03 10:00:00.000</modified_date>
+ </info>
+ <notepads>
+ </notepads>
+ <order>
+ <hop>
+ <from>rows</from>
+ <to>add xml default namespace</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>rows</from>
+ <to>add xml prefixed namespace</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>rows</from>
+ <to>add xml secondary declaration</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>add xml secondary declaration</from>
+ <to>write secondary declaration</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>add xml default namespace</from>
+ <to>write default namespace</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>add xml prefixed namespace</from>
+ <to>write prefixed namespace</to>
+ <enabled>Y</enabled>
+ </hop>
+ </order>
+ <transform>
+ <name>rows</name>
+ <type>DataGrid</type>
+ <description>Two static rows, so the generated XML is byte-for-byte
predictable.</description>
+ <distribute>N</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <data>
+ <line>
+ <item>1</item>
+ <item>alpha</item>
+ <item>https://hop.apache.org/one</item>
+ <item>http://www.w3.org/2001/XMLSchema-instance</item>
+ <item>https://hop.apache.org/one one.xsd</item>
+ </line>
+ <line>
+ <item>2</item>
+ <item>beta</item>
+ <item>https://hop.apache.org/two</item>
+ <item>http://www.w3.org/2001/XMLSchema-instance</item>
+ <item>https://hop.apache.org/two two.xsd</item>
+ </line>
+ </data>
+ <fields>
+ <field>
+ <length>-1</length>
+ <precision>-1</precision>
+ <currency/>
+ <set_empty_string>N</set_empty_string>
+ <name>id</name>
+ <format/>
+ <group/>
+ <decimal/>
+ <type>String</type>
+ </field>
+ <field>
+ <length>-1</length>
+ <precision>-1</precision>
+ <currency/>
+ <set_empty_string>N</set_empty_string>
+ <name>name</name>
+ <format/>
+ <group/>
+ <decimal/>
+ <type>String</type>
+ </field>
+ <field>
+ <length>-1</length>
+ <precision>-1</precision>
+ <currency/>
+ <set_empty_string>N</set_empty_string>
+ <name>ns</name>
+ <format/>
+ <group/>
+ <decimal/>
+ <type>String</type>
+ </field>
+ <field>
+ <length>-1</length>
+ <precision>-1</precision>
+ <currency/>
+ <set_empty_string>N</set_empty_string>
+ <name>xsins</name>
+ <format/>
+ <group/>
+ <decimal/>
+ <type>String</type>
+ </field>
+ <field>
+ <length>-1</length>
+ <precision>-1</precision>
+ <currency/>
+ <set_empty_string>N</set_empty_string>
+ <name>schemaloc</name>
+ <format/>
+ <group/>
+ <decimal/>
+ <type>String</type>
+ </field>
+ </fields>
+ <attributes/>
+ <GUI>
+ <xloc>96</xloc>
+ <yloc>112</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>add xml default namespace</name>
+ <type>AddXML</type>
+ <description>Root node in a default namespace: expected <Row
xmlns="http://www.apache.org/hop/it/addxml">.</description>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <encoding>UTF-8</encoding>
+ <valueName>xmlDefault</valueName>
+ <xml_repeat_element>Row</xml_repeat_element>
+ <file>
+ <omitXMLheader>Y</omitXMLheader>
+ <omitNullValues>N</omitNullValues>
+ </file>
+ <fields>
+ <field>
+ <name>ns</name>
+ <element>xmlns</element>
+ <type>String</type>
+ <format/>
+ <currency/>
+ <decimal/>
+ <group/>
+ <nullif/>
+ <length>-1</length>
+ <precision>-1</precision>
+ <attribute>Y</attribute>
+ <attributeParentName/>
+ </field>
+ <field>
+ <name>ns</name>
+ <element>not_xmlns</element>
+ <type>String</type>
+ <format/>
+ <currency/>
+ <decimal/>
+ <group/>
+ <nullif/>
+ <length>-1</length>
+ <precision>-1</precision>
+ <attribute>Y</attribute>
+ <attributeParentName/>
+ </field>
+ <field>
+ <name>id</name>
+ <element>id</element>
+ <type>String</type>
+ <format/>
+ <currency/>
+ <decimal/>
+ <group/>
+ <nullif/>
+ <length>-1</length>
+ <precision>-1</precision>
+ <attribute>N</attribute>
+ <attributeParentName/>
+ </field>
+ <field>
+ <name>name</name>
+ <element>name</element>
+ <type>String</type>
+ <format/>
+ <currency/>
+ <decimal/>
+ <group/>
+ <nullif/>
+ <length>-1</length>
+ <precision>-1</precision>
+ <attribute>N</attribute>
+ <attributeParentName/>
+ </field>
+ </fields>
+ <attributes/>
+ <GUI>
+ <xloc>288</xloc>
+ <yloc>48</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>add xml prefixed namespace</name>
+ <type>AddXML</type>
+ <description>Root node bound to a prefix: expected <it:Row
xmlns:it="http://www.apache.org/hop/it/addxml">.</description>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <encoding>UTF-8</encoding>
+ <valueName>xmlPrefixed</valueName>
+ <xml_repeat_element>it:Row</xml_repeat_element>
+ <file>
+ <omitXMLheader>Y</omitXMLheader>
+ <omitNullValues>N</omitNullValues>
+ </file>
+ <fields>
+ <field>
+ <name>ns</name>
+ <element>xmlns:it</element>
+ <type>String</type>
+ <format/>
+ <currency/>
+ <decimal/>
+ <group/>
+ <nullif/>
+ <length>-1</length>
+ <precision>-1</precision>
+ <attribute>Y</attribute>
+ <attributeParentName/>
+ </field>
+ <field>
+ <name>id</name>
+ <element>id</element>
+ <type>String</type>
+ <format/>
+ <currency/>
+ <decimal/>
+ <group/>
+ <nullif/>
+ <length>-1</length>
+ <precision>-1</precision>
+ <attribute>N</attribute>
+ <attributeParentName/>
+ </field>
+ <field>
+ <name>name</name>
+ <element>name</element>
+ <type>String</type>
+ <format/>
+ <currency/>
+ <decimal/>
+ <group/>
+ <nullif/>
+ <length>-1</length>
+ <precision>-1</precision>
+ <attribute>N</attribute>
+ <attributeParentName/>
+ </field>
+ </fields>
+ <attributes/>
+ <GUI>
+ <xloc>288</xloc>
+ <yloc>176</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>write default namespace</name>
+ <type>TextFileOutput</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <separator/>
+ <enclosure/>
+ <enclosure_forced>N</enclosure_forced>
+ <enclosure_fix_disabled>Y</enclosure_fix_disabled>
+ <header>N</header>
+ <footer>N</footer>
+ <format>UNIX</format>
+ <compression>None</compression>
+ <encoding>UTF-8</encoding>
+ <endedLine/>
+ <fileNameInField>N</fileNameInField>
+ <fileNameField/>
+ <create_parent_folder>Y</create_parent_folder>
+ <file>
+
<name>${java.io.tmpdir}/hop-xml-integration-output/0020-default-namespace</name>
+ <servlet_output>N</servlet_output>
+ <do_not_open_new_file_init>N</do_not_open_new_file_init>
+ <extention>xml</extention>
+ <append>N</append>
+ <split>N</split>
+ <haspartno>N</haspartno>
+ <add_date>N</add_date>
+ <add_time>N</add_time>
+ <SpecifyFormat>N</SpecifyFormat>
+ <date_time_format/>
+ <add_to_result_filenames>N</add_to_result_filenames>
+ <pad>N</pad>
+ <fast_dump>N</fast_dump>
+ <splitevery/>
+ </file>
+ <fields>
+ <field>
+ <name>xmlDefault</name>
+ <type>String</type>
+ <format/>
+ <currency/>
+ <decimal/>
+ <group/>
+ <nullif/>
+ <trim_type>none</trim_type>
+ <length>-1</length>
+ <precision>-1</precision>
+ </field>
+ </fields>
+ <attributes/>
+ <GUI>
+ <xloc>496</xloc>
+ <yloc>48</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>write prefixed namespace</name>
+ <type>TextFileOutput</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <separator/>
+ <enclosure/>
+ <enclosure_forced>N</enclosure_forced>
+ <enclosure_fix_disabled>Y</enclosure_fix_disabled>
+ <header>N</header>
+ <footer>N</footer>
+ <format>UNIX</format>
+ <compression>None</compression>
+ <encoding>UTF-8</encoding>
+ <endedLine/>
+ <fileNameInField>N</fileNameInField>
+ <fileNameField/>
+ <create_parent_folder>Y</create_parent_folder>
+ <file>
+
<name>${java.io.tmpdir}/hop-xml-integration-output/0020-prefixed-namespace</name>
+ <servlet_output>N</servlet_output>
+ <do_not_open_new_file_init>N</do_not_open_new_file_init>
+ <extention>xml</extention>
+ <append>N</append>
+ <split>N</split>
+ <haspartno>N</haspartno>
+ <add_date>N</add_date>
+ <add_time>N</add_time>
+ <SpecifyFormat>N</SpecifyFormat>
+ <date_time_format/>
+ <add_to_result_filenames>N</add_to_result_filenames>
+ <pad>N</pad>
+ <fast_dump>N</fast_dump>
+ <splitevery/>
+ </file>
+ <fields>
+ <field>
+ <name>xmlPrefixed</name>
+ <type>String</type>
+ <format/>
+ <currency/>
+ <decimal/>
+ <group/>
+ <nullif/>
+ <trim_type>none</trim_type>
+ <length>-1</length>
+ <precision>-1</precision>
+ </field>
+ </fields>
+ <attributes/>
+ <GUI>
+ <xloc>496</xloc>
+ <yloc>176</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>add xml secondary declaration</name>
+ <type>AddXML</type>
+ <description>Default namespace alongside a secondary xmlns:xsi declaration
and an xsi:schemaLocation attribute that uses it. The secondary declaration is
not the root's own, so it is written as an attribute rather than folded into
the document element.</description>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <encoding>UTF-8</encoding>
+ <valueName>xmlSecondary</valueName>
+ <xml_repeat_element>Row</xml_repeat_element>
+ <file>
+ <omitXMLheader>Y</omitXMLheader>
+ <omitNullValues>N</omitNullValues>
+ </file>
+ <fields>
+ <field>
+ <name>ns</name>
+ <element>xmlns</element>
+ <type>String</type>
+ <format/>
+ <currency/>
+ <decimal/>
+ <group/>
+ <nullif/>
+ <length>-1</length>
+ <precision>-1</precision>
+ <attribute>Y</attribute>
+ <attributeParentName/>
+ </field>
+ <field>
+ <name>xsins</name>
+ <element>xmlns:xsi</element>
+ <type>String</type>
+ <format/>
+ <currency/>
+ <decimal/>
+ <group/>
+ <nullif/>
+ <length>-1</length>
+ <precision>-1</precision>
+ <attribute>Y</attribute>
+ <attributeParentName/>
+ </field>
+ <field>
+ <name>schemaloc</name>
+ <element>xsi:schemaLocation</element>
+ <type>String</type>
+ <format/>
+ <currency/>
+ <decimal/>
+ <group/>
+ <nullif/>
+ <length>-1</length>
+ <precision>-1</precision>
+ <attribute>Y</attribute>
+ <attributeParentName/>
+ </field>
+ <field>
+ <name>id</name>
+ <element>id</element>
+ <type>String</type>
+ <format/>
+ <currency/>
+ <decimal/>
+ <group/>
+ <nullif/>
+ <length>-1</length>
+ <precision>-1</precision>
+ <attribute>N</attribute>
+ <attributeParentName/>
+ </field>
+ <field>
+ <name>name</name>
+ <element>name</element>
+ <type>String</type>
+ <format/>
+ <currency/>
+ <decimal/>
+ <group/>
+ <nullif/>
+ <length>-1</length>
+ <precision>-1</precision>
+ <attribute>N</attribute>
+ <attributeParentName/>
+ </field>
+ </fields>
+ <attributes/>
+ <GUI>
+ <xloc>288</xloc>
+ <yloc>304</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>write secondary declaration</name>
+ <type>TextFileOutput</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <separator/>
+ <enclosure/>
+ <enclosure_forced>N</enclosure_forced>
+ <enclosure_fix_disabled>Y</enclosure_fix_disabled>
+ <header>N</header>
+ <footer>N</footer>
+ <format>UNIX</format>
+ <compression>None</compression>
+ <encoding>UTF-8</encoding>
+ <endedLine/>
+ <fileNameInField>N</fileNameInField>
+ <fileNameField/>
+ <create_parent_folder>Y</create_parent_folder>
+ <file>
+
<name>${java.io.tmpdir}/hop-xml-integration-output/0020-secondary-declaration</name>
+ <servlet_output>N</servlet_output>
+ <do_not_open_new_file_init>N</do_not_open_new_file_init>
+ <extention>xml</extention>
+ <append>N</append>
+ <split>N</split>
+ <haspartno>N</haspartno>
+ <add_date>N</add_date>
+ <add_time>N</add_time>
+ <SpecifyFormat>N</SpecifyFormat>
+ <date_time_format/>
+ <add_to_result_filenames>N</add_to_result_filenames>
+ <pad>N</pad>
+ <fast_dump>N</fast_dump>
+ <splitevery/>
+ </file>
+ <fields>
+ <field>
+ <name>xmlSecondary</name>
+ <type>String</type>
+ <format/>
+ <currency/>
+ <decimal/>
+ <group/>
+ <nullif/>
+ <trim_type>none</trim_type>
+ <length>-1</length>
+ <precision>-1</precision>
+ </field>
+ </fields>
+ <attributes/>
+ <GUI>
+ <xloc>496</xloc>
+ <yloc>304</yloc>
+ </GUI>
+ </transform>
+ <transform_error_handling>
+ </transform_error_handling>
+ <attributes/>
+</pipeline>
diff --git a/integration-tests/xml/files/expected/0020-default-namespace.xml
b/integration-tests/xml/files/expected/0020-default-namespace.xml
new file mode 100644
index 0000000000..846b18bef9
--- /dev/null
+++ b/integration-tests/xml/files/expected/0020-default-namespace.xml
@@ -0,0 +1,2 @@
+<Row xmlns="https://hop.apache.org/one"
not_xmlns="https://hop.apache.org/one"><id>1</id><name>alpha</name></Row>
+<Row xmlns="https://hop.apache.org/two"
not_xmlns="https://hop.apache.org/two"><id>2</id><name>beta</name></Row>
diff --git a/integration-tests/xml/files/expected/0020-prefixed-namespace.xml
b/integration-tests/xml/files/expected/0020-prefixed-namespace.xml
new file mode 100644
index 0000000000..9299f2f5a8
--- /dev/null
+++ b/integration-tests/xml/files/expected/0020-prefixed-namespace.xml
@@ -0,0 +1,2 @@
+<it:Row
xmlns:it="https://hop.apache.org/one"><id>1</id><name>alpha</name></it:Row>
+<it:Row
xmlns:it="https://hop.apache.org/two"><id>2</id><name>beta</name></it:Row>
diff --git
a/integration-tests/xml/files/expected/0020-secondary-declaration.xml
b/integration-tests/xml/files/expected/0020-secondary-declaration.xml
new file mode 100644
index 0000000000..05dd6e502f
--- /dev/null
+++ b/integration-tests/xml/files/expected/0020-secondary-declaration.xml
@@ -0,0 +1,2 @@
+<Row xmlns="https://hop.apache.org/one"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://hop.apache.org/one
one.xsd"><id>1</id><name>alpha</name></Row>
+<Row xmlns="https://hop.apache.org/two"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://hop.apache.org/two
two.xsd"><id>2</id><name>beta</name></Row>
diff --git a/integration-tests/xml/main-0020-add-xml-namespace.hwf
b/integration-tests/xml/main-0020-add-xml-namespace.hwf
new file mode 100644
index 0000000000..20d2002443
--- /dev/null
+++ b/integration-tests/xml/main-0020-add-xml-namespace.hwf
@@ -0,0 +1,248 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+-->
+<workflow>
+ <name>main-0020-add-xml-namespace</name>
+ <name_sync_with_filename>Y</name_sync_with_filename>
+ <description>Integration test for apache/hop#3320: Add XML must not drop the
namespace of the root node.</description>
+ <extended_description/>
+ <workflow_version/>
+ <created_user>-</created_user>
+ <created_date>2026/09/03 10:00:00.000</created_date>
+ <modified_user>-</modified_user>
+ <modified_date>2026/09/03 10:00:00.000</modified_date>
+ <parameters>
+ </parameters>
+ <actions>
+ <action>
+ <name>START</name>
+ <description/>
+ <type>SPECIAL</type>
+ <attributes/>
+ <DayOfMonth>1</DayOfMonth>
+ <hour>12</hour>
+ <intervalMinutes>60</intervalMinutes>
+ <intervalSeconds>0</intervalSeconds>
+ <minutes>0</minutes>
+ <repeat>N</repeat>
+ <schedulerType>0</schedulerType>
+ <weekDay>1</weekDay>
+ <parallel>N</parallel>
+ <xloc>64</xloc>
+ <yloc>64</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>clean output</name>
+ <description/>
+ <type>DELETE_FOLDERS</type>
+ <attributes/>
+ <arg_from_previous>N</arg_from_previous>
+ <fields>
+ <field>
+ <name>${java.io.tmpdir}/hop-xml-integration-output</name>
+ </field>
+ </fields>
+ <limit_folders>10</limit_folders>
+ <success_condition>success_if_no_errors</success_condition>
+ <parallel>N</parallel>
+ <xloc>192</xloc>
+ <yloc>64</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>create output</name>
+ <description/>
+ <type>CREATE_FOLDER</type>
+ <attributes/>
+ <foldername>${java.io.tmpdir}/hop-xml-integration-output</foldername>
+ <fail_of_folder_exists>N</fail_of_folder_exists>
+ <parallel>N</parallel>
+ <xloc>320</xloc>
+ <yloc>64</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>0020-add-xml-namespace.hpl</name>
+ <description/>
+ <type>PIPELINE</type>
+ <attributes/>
+ <add_date>N</add_date>
+ <add_time>N</add_time>
+ <clear_files>N</clear_files>
+ <clear_rows>N</clear_rows>
+ <create_parent_folder>N</create_parent_folder>
+ <exec_per_row>N</exec_per_row>
+ <filename>${PROJECT_HOME}/0020-add-xml-namespace.hpl</filename>
+ <loglevel>Basic</loglevel>
+ <parameters>
+ <pass_all_parameters>Y</pass_all_parameters>
+ </parameters>
+ <params_from_previous>N</params_from_previous>
+ <run_configuration>local</run_configuration>
+ <set_append_logfile>N</set_append_logfile>
+ <set_logfile>N</set_logfile>
+ <wait_until_finished>Y</wait_until_finished>
+ <parallel>N</parallel>
+ <xloc>448</xloc>
+ <yloc>64</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>verify default namespace</name>
+ <description/>
+ <type>FILE_COMPARE</type>
+ <attributes/>
+
<filename1>${java.io.tmpdir}/hop-xml-integration-output/0020-default-namespace.xml</filename1>
+
<filename2>${PROJECT_HOME}/files/expected/0020-default-namespace.xml</filename2>
+ <add_filename_result>N</add_filename_result>
+ <parallel>N</parallel>
+ <xloc>624</xloc>
+ <yloc>64</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>verify prefixed namespace</name>
+ <description/>
+ <type>FILE_COMPARE</type>
+ <attributes/>
+
<filename1>${java.io.tmpdir}/hop-xml-integration-output/0020-prefixed-namespace.xml</filename1>
+
<filename2>${PROJECT_HOME}/files/expected/0020-prefixed-namespace.xml</filename2>
+ <add_filename_result>N</add_filename_result>
+ <parallel>N</parallel>
+ <xloc>800</xloc>
+ <yloc>64</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>verify secondary declaration</name>
+ <description/>
+ <type>FILE_COMPARE</type>
+ <attributes/>
+
<filename1>${java.io.tmpdir}/hop-xml-integration-output/0020-secondary-declaration.xml</filename1>
+
<filename2>${PROJECT_HOME}/files/expected/0020-secondary-declaration.xml</filename2>
+ <add_filename_result>N</add_filename_result>
+ <parallel>N</parallel>
+ <xloc>976</xloc>
+ <yloc>64</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>secondary declaration mismatch</name>
+ <description/>
+ <type>ABORT</type>
+ <attributes/>
+ <message>Add XML mishandled a secondary namespace declaration: generated
XML does not match
${PROJECT_HOME}/files/expected/0020-secondary-declaration.xml (see
apache/hop#3320).</message>
+ <parallel>N</parallel>
+ <xloc>976</xloc>
+ <yloc>192</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>default namespace mismatch</name>
+ <description/>
+ <type>ABORT</type>
+ <attributes/>
+ <message>Add XML dropped the default namespace: generated XML does not
match ${PROJECT_HOME}/files/expected/0020-default-namespace.xml (see
apache/hop#3320).</message>
+ <parallel>N</parallel>
+ <xloc>624</xloc>
+ <yloc>192</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>prefixed namespace mismatch</name>
+ <description/>
+ <type>ABORT</type>
+ <attributes/>
+ <message>Add XML dropped the prefixed namespace: generated XML does not
match ${PROJECT_HOME}/files/expected/0020-prefixed-namespace.xml (see
apache/hop#3320).</message>
+ <parallel>N</parallel>
+ <xloc>800</xloc>
+ <yloc>192</yloc>
+ <attributes_hac/>
+ </action>
+ </actions>
+ <hops>
+ <hop>
+ <from>START</from>
+ <to>clean output</to>
+ <enabled>Y</enabled>
+ <evaluation>Y</evaluation>
+ <unconditional>Y</unconditional>
+ </hop>
+ <hop>
+ <from>clean output</from>
+ <to>create output</to>
+ <enabled>Y</enabled>
+ <evaluation>Y</evaluation>
+ <unconditional>N</unconditional>
+ </hop>
+ <hop>
+ <from>create output</from>
+ <to>0020-add-xml-namespace.hpl</to>
+ <enabled>Y</enabled>
+ <evaluation>Y</evaluation>
+ <unconditional>N</unconditional>
+ </hop>
+ <hop>
+ <from>0020-add-xml-namespace.hpl</from>
+ <to>verify default namespace</to>
+ <enabled>Y</enabled>
+ <evaluation>Y</evaluation>
+ <unconditional>N</unconditional>
+ </hop>
+ <hop>
+ <from>verify default namespace</from>
+ <to>default namespace mismatch</to>
+ <enabled>Y</enabled>
+ <evaluation>N</evaluation>
+ <unconditional>N</unconditional>
+ </hop>
+ <hop>
+ <from>verify default namespace</from>
+ <to>verify prefixed namespace</to>
+ <enabled>Y</enabled>
+ <evaluation>Y</evaluation>
+ <unconditional>N</unconditional>
+ </hop>
+ <hop>
+ <from>verify prefixed namespace</from>
+ <to>prefixed namespace mismatch</to>
+ <enabled>Y</enabled>
+ <evaluation>N</evaluation>
+ <unconditional>N</unconditional>
+ </hop>
+ <hop>
+ <from>verify prefixed namespace</from>
+ <to>verify secondary declaration</to>
+ <enabled>Y</enabled>
+ <evaluation>Y</evaluation>
+ <unconditional>N</unconditional>
+ </hop>
+ <hop>
+ <from>verify secondary declaration</from>
+ <to>secondary declaration mismatch</to>
+ <enabled>Y</enabled>
+ <evaluation>N</evaluation>
+ <unconditional>N</unconditional>
+ </hop>
+ </hops>
+ <notepads>
+ </notepads>
+ <attributes/>
+</workflow>
diff --git
a/plugins/transforms/xml/src/main/java/org/apache/hop/pipeline/transforms/xml/addxml/AddXml.java
b/plugins/transforms/xml/src/main/java/org/apache/hop/pipeline/transforms/xml/addxml/AddXml.java
index 36e850d0ea..b0434dfd15 100644
---
a/plugins/transforms/xml/src/main/java/org/apache/hop/pipeline/transforms/xml/addxml/AddXml.java
+++
b/plugins/transforms/xml/src/main/java/org/apache/hop/pipeline/transforms/xml/addxml/AddXml.java
@@ -19,6 +19,7 @@ package org.apache.hop.pipeline.transforms.xml.addxml;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
+import javax.xml.XMLConstants;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
@@ -48,6 +49,11 @@ import org.w3c.dom.NodeList;
public class AddXml extends BaseTransform<AddXmlMeta, AddXmlData> {
private static final Class<?> PKG = AddXml.class;
+ /** Attribute name declaring a default namespace, and the prefix of a
namespace declaration. */
+ private static final String XMLNS = "xmlns";
+
+ private static final String XMLNS_PREFIX = XMLNS + ":";
+
private DOMImplementation domImplentation;
private Transformer serializer;
@@ -76,9 +82,20 @@ public class AddXml extends BaseTransform<AddXmlMeta,
AddXmlData> {
processRowFirstCall();
}
- Document xmlDoc = getDomImplentation().createDocument(null,
meta.getRootNode(), null);
+ // A default namespace can only be established when the document is
created, so the value of
+ // the declaring field has to be read before the output fields are walked.
+ String documentNamespace = getDocumentNamespace(r);
+
+ Document xmlDoc =
+ getDomImplentation().createDocument(documentNamespace,
meta.getRootNode(), null);
Element root = xmlDoc.getDocumentElement();
for (int i = 0; i < meta.getOutputFields().size(); i++) {
+ if (i == data.namespaceFieldIndex) {
+ // Already applied to the document element; emitting it again would
duplicate the
+ // declaration.
+ continue;
+ }
+
XmlField outputField = meta.getOutputFields().get(i);
String fieldName = outputField.getFieldName();
@@ -112,12 +129,18 @@ public class AddXml extends BaseTransform<AddXmlMeta,
AddXmlData> {
}
}
- node.setAttribute(element, value);
+ if (XMLNS.equals(element) || element.startsWith(XMLNS_PREFIX)) {
+ // A namespace declaration only survives serialization when it is
set as an attribute
+ // in the xmlns namespace; as a plain attribute the serializer
discards its value.
+ node.setAttributeNS(XMLConstants.XMLNS_ATTRIBUTE_NS_URI, element,
value);
+ } else {
+ node.setAttribute(element, value);
+ }
} else {
/* encode as subnode */
if (!element.equals(meta.getRootNode())) {
- Element e = xmlDoc.createElement(element);
+ Element e = createElement(xmlDoc, documentNamespace, element);
Node n = xmlDoc.createTextNode(value);
e.appendChild(n);
root.appendChild(e);
@@ -145,6 +168,39 @@ public class AddXml extends BaseTransform<AddXmlMeta,
AddXmlData> {
return true;
}
+ /**
+ * Finds the output field declaring the namespace of the root node. An
unprefixed root node is
+ * declared by a field named {@code xmlns}, a prefixed one by {@code
xmlns:prefix}.
+ */
+ private void resolveNamespaceField() throws HopException {
+ data.namespaceFieldIndex = -1;
+
+ String rootNode = Const.NVL(meta.getRootNode(), "");
+ int colon = rootNode.indexOf(':');
+ String declaration = colon > 0 ? XMLNS_PREFIX + rootNode.substring(0,
colon) : XMLNS;
+ data.rootNamespaceIsDefault = colon <= 0;
+
+ for (int i = 0; i < meta.getOutputFields().size(); i++) {
+ XmlField field = meta.getOutputFields().get(i);
+ String element = field.getElementName();
+ if (Utils.isEmpty(element)) {
+ element = field.getFieldName();
+ }
+ if (declaration.equals(element)) {
+ data.namespaceFieldIndex = i;
+ return;
+ }
+ }
+
+ if (colon > 0) {
+ // The DOM refuses a prefixed root node without a namespace URI. Say
which field is missing
+ // rather than failing with a bare DOMException.
+ throw new HopException(
+ BaseMessages.getString(
+ PKG, "AddXML.Exception.MissingNamespaceDeclaration", rootNode,
declaration));
+ }
+ }
+
private void processRowFirstCall() throws HopException {
data.outputRowMeta = getInputRowMeta().clone();
meta.getFields(data.outputRowMeta, getTransformName(), null, null, this,
metadataProvider);
@@ -160,6 +216,38 @@ public class AddXml extends BaseTransform<AddXmlMeta,
AddXmlData> {
BaseMessages.getString(PKG, "AddXML.Exception.FieldNotFound",
fieldsName));
}
}
+
+ resolveNamespaceField();
+ }
+
+ /**
+ * Reads the namespace URI for the root node from the current row. The
declaring field is the one
+ * whose element name matches the root node's prefix: {@code xmlns} for an
unprefixed root node,
+ * {@code xmlns:prefix} otherwise.
+ *
+ * @return the namespace URI, or null when the root node has no namespace
+ */
+ private String getDocumentNamespace(Object[] r) throws HopValueException {
+ if (data.namespaceFieldIndex < 0) {
+ return null;
+ }
+ XmlField field = meta.getOutputFields().get(data.namespaceFieldIndex);
+ int index = data.fieldIndexes[data.namespaceFieldIndex];
+ String namespace = formatField(getInputRowMeta().getValueMeta(index),
r[index], field);
+ return Utils.isEmpty(namespace) ? null : namespace;
+ }
+
+ /**
+ * Creates a child element. When the root node declares a default namespace,
unprefixed children
+ * are created in it so that the DOM matches what is serialized. Under a
prefixed root node an
+ * unprefixed child is genuinely in no namespace, and names that already
carry a prefix are
+ * created as-is: the declaration for that prefix is the user's own.
+ */
+ private Element createElement(Document doc, String documentNamespace, String
name) {
+ if (documentNamespace == null || !data.rootNamespaceIsDefault ||
name.indexOf(':') >= 0) {
+ return doc.createElement(name);
+ }
+ return doc.createElementNS(documentNamespace, name);
}
private String formatField(IValueMeta valueMeta, Object valueData, XmlField
field)
diff --git
a/plugins/transforms/xml/src/main/java/org/apache/hop/pipeline/transforms/xml/addxml/AddXmlData.java
b/plugins/transforms/xml/src/main/java/org/apache/hop/pipeline/transforms/xml/addxml/AddXmlData.java
index 96c3afd8ed..11b38a7c78 100644
---
a/plugins/transforms/xml/src/main/java/org/apache/hop/pipeline/transforms/xml/addxml/AddXmlData.java
+++
b/plugins/transforms/xml/src/main/java/org/apache/hop/pipeline/transforms/xml/addxml/AddXmlData.java
@@ -56,6 +56,19 @@ public class AddXmlData extends BaseTransformData implements
ITransformData {
public int[] fieldIndexes;
+ /**
+ * Index, in the output fields, of the field that declares the namespace of
the root node, or -1
+ * when the root node has no namespace. Its value is read per row and
applied when the document is
+ * created, which is the only point at which a default namespace can be set.
+ */
+ public int namespaceFieldIndex = -1;
+
+ /**
+ * True when the root node's namespace is declared as a default namespace
({@code xmlns}) rather
+ * than a prefixed one. Only a default namespace is inherited by unprefixed
child elements.
+ */
+ public boolean rootNamespaceIsDefault;
+
/** */
public AddXmlData() {
super();
diff --git
a/plugins/transforms/xml/src/main/resources/org/apache/hop/pipeline/transforms/xml/addxml/messages/messages_en_US.properties
b/plugins/transforms/xml/src/main/resources/org/apache/hop/pipeline/transforms/xml/addxml/messages/messages_en_US.properties
index 4bb59c6510..22f1bc2992 100644
---
a/plugins/transforms/xml/src/main/resources/org/apache/hop/pipeline/transforms/xml/addxml/messages/messages_en_US.properties
+++
b/plugins/transforms/xml/src/main/resources/org/apache/hop/pipeline/transforms/xml/addxml/messages/messages_en_US.properties
@@ -19,6 +19,7 @@
AddXML.category=Transform
AddXML.description=Encode several fields into an XML fragment
AddXML.Exception.FieldNotFound=The specified field ''{0}'' could not be found
in the input.
+AddXML.Exception.MissingNamespaceDeclaration=Root XML element ''{0}'' uses a
namespace prefix, but no field declares that namespace. Add an attribute field
with element name ''{1}'' holding the namespace URI.
AddXML.name=Add XML
AddXMLDialog.Attribute.Column=Attribute
AddXMLDialog.AttributeParentName.Column=Attribute parent name
diff --git
a/plugins/transforms/xml/src/test/java/org/apache/hop/pipeline/transforms/xml/addxml/AddXmlNamespaceTest.java
b/plugins/transforms/xml/src/test/java/org/apache/hop/pipeline/transforms/xml/addxml/AddXmlNamespaceTest.java
new file mode 100644
index 0000000000..d6e09c28a0
--- /dev/null
+++
b/plugins/transforms/xml/src/test/java/org/apache/hop/pipeline/transforms/xml/addxml/AddXmlNamespaceTest.java
@@ -0,0 +1,262 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.pipeline.transforms.xml.addxml;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.ByteArrayInputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import javax.xml.parsers.DocumentBuilderFactory;
+import org.apache.hop.core.HopEnvironment;
+import org.apache.hop.core.RowMetaAndData;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.row.IValueMeta;
+import org.apache.hop.core.row.RowMeta;
+import org.apache.hop.core.row.value.ValueMetaString;
+import org.apache.hop.core.variables.Variables;
+import org.apache.hop.core.xml.XmlParserFactoryProducer;
+import org.apache.hop.pipeline.PipelineMeta;
+import org.apache.hop.pipeline.transforms.xml.PipelineTestFactory;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+/**
+ * Runtime tests for the namespace handling of the Add XML transform
(apache/hop#3320). Namespaces
+ * are declared through regular output fields, so each test runs an actual
pipeline and asserts on
+ * the generated XML.
+ */
+class AddXmlNamespaceTest {
+
+ private static final String XSI =
"http://www.w3.org/2001/XMLSchema-instance";
+ private static final String TRANSFORM = "addXml";
+ private static final String XML_FIELD = "xml";
+
+ @BeforeAll
+ static void setup() throws Exception {
+ HopEnvironment.init();
+ }
+
+ @Test
+ void defaultNamespaceIsTakenFromTheDeclaringField() throws Exception {
+ AddXmlMeta meta = meta("catalog", field("ns", "xmlns", true), field("id",
"id", false));
+
+ List<RowMetaAndData> out = run(meta, row("ns", "https://hop.apache.org",
"id", "1"));
+ Element root = firstRoot(out);
+
+ assertEquals("catalog", root.getTagName());
+ assertEquals("https://hop.apache.org", root.getNamespaceURI());
+ // the field is consumed as the declaration, so it is written once and not
also as an attribute
+ assertEquals(1, occurrences(xml(out, 0), "xmlns="));
+ }
+
+ @Test
+ void unprefixedChildrenInheritTheDefaultNamespace() throws Exception {
+ AddXmlMeta meta = meta("catalog", field("ns", "xmlns", true), field("id",
"id", false));
+
+ Element root = firstRoot(run(meta, row("ns", "https://hop.apache.org",
"id", "1")));
+ Element child = (Element) root.getElementsByTagName("id").item(0);
+
+ assertNotNull(child);
+ assertEquals("https://hop.apache.org", child.getNamespaceURI());
+ assertEquals("1", child.getTextContent());
+ }
+
+ @Test
+ void namespaceCanDifferPerRow() throws Exception {
+ AddXmlMeta meta = meta("catalog", field("ns", "xmlns", true), field("id",
"id", false));
+
+ List<RowMetaAndData> out =
+ run(
+ meta,
+ row("ns", "https://hop.apache.org/one", "id", "1"),
+ row("ns", "https://hop.apache.org/two", "id", "2"));
+
+ assertEquals(2, out.size());
+ assertEquals("https://hop.apache.org/one", root(out, 0).getNamespaceURI());
+ assertEquals("https://hop.apache.org/two", root(out, 1).getNamespaceURI());
+ }
+
+ @Test
+ void prefixedRootNodeBindsItsPrefix() throws Exception {
+ AddXmlMeta meta = meta("it:catalog", field("ns", "xmlns:it", true),
field("id", "id", false));
+
+ Element root = firstRoot(run(meta, row("ns", "https://hop.apache.org",
"id", "1")));
+
+ assertEquals("it:catalog", root.getTagName());
+ assertEquals("https://hop.apache.org", root.getNamespaceURI());
+ assertEquals("it", root.getPrefix());
+ }
+
+ @Test
+ void unprefixedChildrenAreNotInTheNamespaceOfAPrefixedRootNode() throws
Exception {
+ AddXmlMeta meta = meta("it:catalog", field("ns", "xmlns:it", true),
field("id", "id", false));
+
+ Element root = firstRoot(run(meta, row("ns", "https://hop.apache.org",
"id", "1")));
+ Element child = (Element) root.getElementsByTagName("id").item(0);
+
+ // A prefixed root node declares no default namespace, so an unprefixed
child is in none.
+ assertNotNull(child);
+ assertNull(child.getNamespaceURI());
+ }
+
+ @Test
+ void prefixedRootNodeWithoutADeclaringFieldFails() {
+ AddXmlMeta meta = meta("it:catalog", field("id", "id", false));
+
+ assertThrows(HopException.class, () -> run(meta, row("id", "1")));
+ }
+
+ @Test
+ void secondaryDeclarationAndAPrefixedAttributeAreKept() throws Exception {
+ AddXmlMeta meta =
+ meta(
+ "catalog",
+ field("ns", "xmlns", true),
+ field("xsins", "xmlns:xsi", true),
+ field("loc", "xsi:schemaLocation", true));
+
+ Element root =
+ firstRoot(
+ run(
+ meta,
+ row(
+ "ns", "https://hop.apache.org",
+ "xsins", XSI,
+ "loc", "https://hop.apache.org catalog.xsd")));
+
+ assertEquals("https://hop.apache.org", root.getNamespaceURI());
+ // the prefixed attribute has to resolve through the xsi declaration, not
the default namespace
+ assertEquals("https://hop.apache.org catalog.xsd",
root.getAttributeNS(XSI, "schemaLocation"));
+ }
+
+ @Test
+ void withoutADeclaringFieldTheXmlHasNoNamespace() throws Exception {
+ AddXmlMeta meta = meta("catalog", field("id", "id", false));
+
+ Element root = firstRoot(run(meta, row("id", "1")));
+
+ assertEquals("catalog", root.getTagName());
+ assertNull(root.getNamespaceURI());
+ }
+
+ @Test
+ void anEmptyNamespaceValueLeavesTheXmlWithoutANamespace() throws Exception {
+ AddXmlMeta meta = meta("catalog", field("ns", "xmlns", true), field("id",
"id", false));
+
+ Element root = firstRoot(run(meta, row("ns", "", "id", "1")));
+
+ assertNull(root.getNamespaceURI());
+ }
+
+ @Test
+ void aRegularAttributeIsStillWrittenAsAPlainAttribute() throws Exception {
+ AddXmlMeta meta =
+ meta("catalog", field("ns", "xmlns", true), field("other",
"not_xmlns", true));
+
+ Element root =
+ firstRoot(run(meta, row("ns", "https://hop.apache.org", "other",
"https://example.com")));
+
+ assertEquals("https://hop.apache.org", root.getNamespaceURI());
+ assertEquals("https://example.com", root.getAttribute("not_xmlns"));
+ }
+
+ //
---------------------------------------------------------------------------
+ // helpers
+ //
---------------------------------------------------------------------------
+
+ private static XmlField field(String fieldName, String elementName, boolean
attribute) {
+ XmlField field = new XmlField();
+ field.setFieldName(fieldName);
+ field.setElementName(elementName);
+ field.setType(IValueMeta.TYPE_STRING);
+ field.setLength(-1);
+ field.setPrecision(-1);
+ field.setAttribute(attribute);
+ return field;
+ }
+
+ private static AddXmlMeta meta(String rootNode, XmlField... fields) {
+ AddXmlMeta meta = new AddXmlMeta();
+ meta.setRootNode(rootNode);
+ meta.setValueName(XML_FIELD);
+ meta.getOmitDetails().setOmittingXmlHeader(true);
+ meta.setOutputFields(new ArrayList<>(List.of(fields)));
+ return meta;
+ }
+
+ /** Builds one input row from alternating field name / value pairs. */
+ private static RowMetaAndData row(String... nameValuePairs) {
+ RowMeta rowMeta = new RowMeta();
+ Object[] data = new Object[nameValuePairs.length / 2];
+ for (int i = 0; i < nameValuePairs.length; i += 2) {
+ rowMeta.addValueMeta(new ValueMetaString(nameValuePairs[i]));
+ data[i / 2] = nameValuePairs[i + 1];
+ }
+ return new RowMetaAndData(rowMeta, data);
+ }
+
+ private static List<RowMetaAndData> run(AddXmlMeta meta, RowMetaAndData...
rows)
+ throws HopException {
+ PipelineMeta pipelineMeta =
+ PipelineTestFactory.generateTestTransformation(new Variables(), meta,
TRANSFORM);
+ return PipelineTestFactory.executeTestTransformation(
+ pipelineMeta,
+ PipelineTestFactory.INJECTOR_TRANSFORMNAME,
+ TRANSFORM,
+ PipelineTestFactory.DUMMY_TRANSFORMNAME,
+ List.of(rows));
+ }
+
+ private static int occurrences(String haystack, String needle) {
+ int count = 0;
+ for (int i = haystack.indexOf(needle); i >= 0; i =
haystack.indexOf(needle, i + 1)) {
+ count++;
+ }
+ return count;
+ }
+
+ private static String xml(List<RowMetaAndData> rows, int index) throws
HopException {
+ String xml = rows.get(index).getString(XML_FIELD, null);
+ assertNotNull(xml, "no XML was added to the row");
+ return xml;
+ }
+
+ private static Element firstRoot(List<RowMetaAndData> rows) throws Exception
{
+ assertTrue(!rows.isEmpty(), "the transform produced no rows");
+ return root(rows, 0);
+ }
+
+ /** Parses the generated XML of one output row, namespace aware, and returns
its root element. */
+ private static Element root(List<RowMetaAndData> rows, int index) throws
Exception {
+ DocumentBuilderFactory factory =
XmlParserFactoryProducer.createSecureDocBuilderFactory();
+ factory.setNamespaceAware(true);
+ Document document =
+ factory
+ .newDocumentBuilder()
+ .parse(new ByteArrayInputStream(xml(rows,
index).getBytes(StandardCharsets.UTF_8)));
+ return document.getDocumentElement();
+ }
+}