This is an automated email from the ASF dual-hosted git repository.

mcasters 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 a7ce99e73a issue #1936 (XML cleanup Stream Lookup) (#6874)
a7ce99e73a is described below

commit a7ce99e73af1e43a7484537c9ef6446a57aef08c
Author: Matt Casters <[email protected]>
AuthorDate: Fri Mar 27 13:00:22 2026 +0100

    issue #1936 (XML cleanup Stream Lookup) (#6874)
---
 .../mdi/0048-stream-lookup-template.hpl            |  68 +++
 .../mdi/0048-stream-lookup-validate.hpl            | 435 ++++++++++++++++++
 integration-tests/mdi/0048-stream-lookup.hpl       | 446 +++++++++++++++++++
 .../mdi/datasets/golden-stream-lookup-keys.csv     |   3 +
 .../mdi/datasets/golden-stream-lookup-values.csv   |   4 +
 .../mdi/datasets/golden-stream-lookup.csv          |   2 +
 integration-tests/mdi/files/stream-lookup.xml      |  52 +++
 integration-tests/mdi/main-0048-stream-lookup.hwf  | 113 +++++
 .../dataset/golden-stream-lookup-keys.json         |  24 +
 .../dataset/golden-stream-lookup-values.json       |  40 ++
 .../mdi/metadata/dataset/golden-stream-lookup.json |  48 ++
 .../0048-stream-lookup-validate UNIT.json          |  90 ++++
 ...PipelineTest.java => MatchKeyPipelineTest.java} |   2 +-
 .../apache/hop/beam/util/BeamPipelineMetaUtil.java |  24 +-
 .../transforms/streamlookup/StreamLookup.java      | 163 +++----
 .../streamlookup/StreamLookupDialog.java           | 105 ++---
 .../transforms/streamlookup/StreamLookupMeta.java  | 491 +++++++--------------
 .../messages/messages_en_US.properties             |   4 +
 .../streamlookup/StreamLookupMetaTest.java         | 160 ++-----
 .../transforms/streamlookup/StreamLookupTest.java  | 259 -----------
 .../src/test/resources/stream-lookup.xml           |  52 +++
 21 files changed, 1711 insertions(+), 874 deletions(-)

diff --git a/integration-tests/mdi/0048-stream-lookup-template.hpl 
b/integration-tests/mdi/0048-stream-lookup-template.hpl
new file mode 100644
index 0000000000..b9d0248c41
--- /dev/null
+++ b/integration-tests/mdi/0048-stream-lookup-template.hpl
@@ -0,0 +1,68 @@
+<?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>0048-stream-lookup-template</name>
+    <name_sync_with_filename>Y</name_sync_with_filename>
+    <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/03/25 18:24:50.466</created_date>
+    <modified_user>-</modified_user>
+    <modified_date>2026/03/25 18:24:50.466</modified_date>
+  </info>
+  <notepads>
+  </notepads>
+  <order>
+  </order>
+  <transform>
+    <name>Stream lookup</name>
+    <type>StreamLookup</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <input_sorted>N</input_sorted>
+    <integer_pair>N</integer_pair>
+    <lookup>
+</lookup>
+    <preserve_memory>Y</preserve_memory>
+    <sorted_list>N</sorted_list>
+    <attributes/>
+    <GUI>
+      <xloc>240</xloc>
+      <yloc>112</yloc>
+    </GUI>
+  </transform>
+  <transform_error_handling>
+  </transform_error_handling>
+  <attributes/>
+</pipeline>
diff --git a/integration-tests/mdi/0048-stream-lookup-validate.hpl 
b/integration-tests/mdi/0048-stream-lookup-validate.hpl
new file mode 100644
index 0000000000..345c54bfda
--- /dev/null
+++ b/integration-tests/mdi/0048-stream-lookup-validate.hpl
@@ -0,0 +1,435 @@
+<?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>0048-stream-lookup-validate</name>
+    <name_sync_with_filename>Y</name_sync_with_filename>
+    <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/03/25 18:30:51.794</created_date>
+    <modified_user>-</modified_user>
+    <modified_date>2026/03/25 18:30:51.794</modified_date>
+  </info>
+  <notepads>
+  </notepads>
+  <order>
+    <hop>
+      <from>/tmp/stream-lookup.hpl</from>
+      <to>validate</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>/tmp/stream-lookup.hpl keys</from>
+      <to>validate keys</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>/tmp/stream-lookup.hpl values</from>
+      <to>validate values</to>
+      <enabled>Y</enabled>
+    </hop>
+  </order>
+  <transform>
+    <name>/tmp/stream-lookup.hpl</name>
+    <type>getXMLData</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <IsAFile>N</IsAFile>
+    <IsIgnoreEmptyFile>N</IsIgnoreEmptyFile>
+    <IsInFields>N</IsInFields>
+    <XmlField/>
+    <addresultfile>N</addresultfile>
+    <doNotFailIfNoFile>Y</doNotFailIfNoFile>
+    <encoding>UTF-8</encoding>
+    <extensionFieldName/>
+    <fields>
+      <field>
+        <currency/>
+        <decimal/>
+        <element_type>Node</element_type>
+        <format/>
+        <group/>
+        <length>-1</length>
+        <name>from</name>
+        <precision>-1</precision>
+        <repeat>N</repeat>
+        <result_type>valueof</result_type>
+        <trim_type>none</trim_type>
+        <type>String</type>
+        <xPath>from</xPath>
+      </field>
+      <field>
+        <currency/>
+        <decimal/>
+        <element_type>Node</element_type>
+        <format/>
+        <group/>
+        <length>-1</length>
+        <name>input_sorted</name>
+        <precision>-1</precision>
+        <repeat>N</repeat>
+        <result_type>valueof</result_type>
+        <trim_type>none</trim_type>
+        <type>String</type>
+        <xPath>input_sorted</xPath>
+      </field>
+      <field>
+        <currency/>
+        <decimal/>
+        <element_type>Node</element_type>
+        <format/>
+        <group/>
+        <length>-1</length>
+        <name>integer_pair</name>
+        <precision>-1</precision>
+        <repeat>N</repeat>
+        <result_type>valueof</result_type>
+        <trim_type>none</trim_type>
+        <type>String</type>
+        <xPath>integer_pair</xPath>
+      </field>
+      <field>
+        <currency/>
+        <decimal/>
+        <element_type>Node</element_type>
+        <format/>
+        <group/>
+        <length>-1</length>
+        <name>preserve_memory</name>
+        <precision>-1</precision>
+        <repeat>N</repeat>
+        <result_type>valueof</result_type>
+        <trim_type>none</trim_type>
+        <type>String</type>
+        <xPath>preserve_memory</xPath>
+      </field>
+      <field>
+        <currency/>
+        <decimal/>
+        <element_type>Node</element_type>
+        <format/>
+        <group/>
+        <length>-1</length>
+        <name>sorted_list</name>
+        <precision>-1</precision>
+        <repeat>N</repeat>
+        <result_type>valueof</result_type>
+        <trim_type>none</trim_type>
+        <type>String</type>
+        <xPath>sorted_list</xPath>
+      </field>
+    </fields>
+    <file>
+      <exclude_filemask/>
+      <file_required>N</file_required>
+      <filemask/>
+      <include_subfolders>N</include_subfolders>
+      <name>${java.io.tmpdir}/stream-lookup.hpl</name>
+    </file>
+    <hiddenFieldName/>
+    <ignorecomments>N</ignorecomments>
+    <include>N</include>
+    <include_field/>
+    <lastModificationTimeFieldName/>
+    <limit>0</limit>
+    <loopxpath>/pipeline/transform</loopxpath>
+    <namespaceaware>N</namespaceaware>
+    <pathFieldName/>
+    <prunePath/>
+    <readurl>N</readurl>
+    <rootUriNameFieldName/>
+    <rownum>N</rownum>
+    <rownum_field/>
+    <shortFileFieldName/>
+    <sizeFieldName/>
+    <uriNameFieldName/>
+    <usetoken>N</usetoken>
+    <validating>N</validating>
+    <attributes/>
+    <GUI>
+      <xloc>112</xloc>
+      <yloc>64</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>/tmp/stream-lookup.hpl keys</name>
+    <type>getXMLData</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <IsAFile>N</IsAFile>
+    <IsIgnoreEmptyFile>N</IsIgnoreEmptyFile>
+    <IsInFields>N</IsInFields>
+    <XmlField/>
+    <addresultfile>N</addresultfile>
+    <doNotFailIfNoFile>Y</doNotFailIfNoFile>
+    <encoding>UTF-8</encoding>
+    <extensionFieldName/>
+    <fields>
+      <field>
+        <currency/>
+        <decimal/>
+        <element_type>Node</element_type>
+        <format/>
+        <group/>
+        <length>-1</length>
+        <name>field</name>
+        <precision>-1</precision>
+        <repeat>N</repeat>
+        <result_type>valueof</result_type>
+        <trim_type>none</trim_type>
+        <type>String</type>
+        <xPath>field</xPath>
+      </field>
+      <field>
+        <currency/>
+        <decimal/>
+        <element_type>Node</element_type>
+        <format/>
+        <group/>
+        <length>-1</length>
+        <name>name</name>
+        <precision>-1</precision>
+        <repeat>N</repeat>
+        <result_type>valueof</result_type>
+        <trim_type>none</trim_type>
+        <type>String</type>
+        <xPath>name</xPath>
+      </field>
+    </fields>
+    <file>
+      <exclude_filemask/>
+      <file_required>N</file_required>
+      <filemask/>
+      <include_subfolders>N</include_subfolders>
+      <name>${java.io.tmpdir}/stream-lookup.hpl</name>
+    </file>
+    <hiddenFieldName/>
+    <ignorecomments>N</ignorecomments>
+    <include>N</include>
+    <include_field/>
+    <lastModificationTimeFieldName/>
+    <limit>0</limit>
+    <loopxpath>/pipeline/transform/lookup/key</loopxpath>
+    <namespaceaware>N</namespaceaware>
+    <pathFieldName/>
+    <prunePath/>
+    <readurl>N</readurl>
+    <rootUriNameFieldName/>
+    <rownum>N</rownum>
+    <rownum_field/>
+    <shortFileFieldName/>
+    <sizeFieldName/>
+    <uriNameFieldName/>
+    <usetoken>N</usetoken>
+    <validating>N</validating>
+    <attributes/>
+    <GUI>
+      <xloc>112</xloc>
+      <yloc>144</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>validate</name>
+    <type>Dummy</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <attributes/>
+    <GUI>
+      <xloc>256</xloc>
+      <yloc>64</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>validate keys</name>
+    <type>Dummy</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <attributes/>
+    <GUI>
+      <xloc>256</xloc>
+      <yloc>144</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>/tmp/stream-lookup.hpl values</name>
+    <type>getXMLData</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <IsAFile>N</IsAFile>
+    <IsIgnoreEmptyFile>N</IsIgnoreEmptyFile>
+    <IsInFields>N</IsInFields>
+    <XmlField/>
+    <addresultfile>N</addresultfile>
+    <doNotFailIfNoFile>Y</doNotFailIfNoFile>
+    <encoding>UTF-8</encoding>
+    <extensionFieldName/>
+    <fields>
+      <field>
+        <currency/>
+        <decimal/>
+        <element_type>Node</element_type>
+        <format/>
+        <group/>
+        <length>-1</length>
+        <name>default</name>
+        <precision>-1</precision>
+        <repeat>N</repeat>
+        <result_type>valueof</result_type>
+        <trim_type>none</trim_type>
+        <type>String</type>
+        <xPath>default</xPath>
+      </field>
+      <field>
+        <currency/>
+        <decimal/>
+        <element_type>Node</element_type>
+        <format/>
+        <group/>
+        <length>-1</length>
+        <name>name</name>
+        <precision>-1</precision>
+        <repeat>N</repeat>
+        <result_type>valueof</result_type>
+        <trim_type>none</trim_type>
+        <type>String</type>
+        <xPath>name</xPath>
+      </field>
+      <field>
+        <currency/>
+        <decimal/>
+        <element_type>Node</element_type>
+        <format/>
+        <group/>
+        <length>-1</length>
+        <name>rename</name>
+        <precision>-1</precision>
+        <repeat>N</repeat>
+        <result_type>valueof</result_type>
+        <trim_type>none</trim_type>
+        <type>String</type>
+        <xPath>rename</xPath>
+      </field>
+      <field>
+        <currency/>
+        <decimal/>
+        <element_type>Node</element_type>
+        <format/>
+        <group/>
+        <length>-1</length>
+        <name>type</name>
+        <precision>-1</precision>
+        <repeat>N</repeat>
+        <result_type>valueof</result_type>
+        <trim_type>none</trim_type>
+        <type>String</type>
+        <xPath>type</xPath>
+      </field>
+    </fields>
+    <file>
+      <exclude_filemask/>
+      <file_required>N</file_required>
+      <filemask/>
+      <include_subfolders>N</include_subfolders>
+      <name>${java.io.tmpdir}/stream-lookup.hpl</name>
+    </file>
+    <hiddenFieldName/>
+    <ignorecomments>N</ignorecomments>
+    <include>N</include>
+    <include_field/>
+    <lastModificationTimeFieldName/>
+    <limit>0</limit>
+    <loopxpath>/pipeline/transform/lookup/value</loopxpath>
+    <namespaceaware>N</namespaceaware>
+    <pathFieldName/>
+    <prunePath/>
+    <readurl>N</readurl>
+    <rootUriNameFieldName/>
+    <rownum>N</rownum>
+    <rownum_field/>
+    <shortFileFieldName/>
+    <sizeFieldName/>
+    <uriNameFieldName/>
+    <usetoken>N</usetoken>
+    <validating>N</validating>
+    <attributes/>
+    <GUI>
+      <xloc>112</xloc>
+      <yloc>224</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>validate values</name>
+    <type>Dummy</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <attributes/>
+    <GUI>
+      <xloc>256</xloc>
+      <yloc>224</yloc>
+    </GUI>
+  </transform>
+  <transform_error_handling>
+  </transform_error_handling>
+  <attributes/>
+</pipeline>
diff --git a/integration-tests/mdi/0048-stream-lookup.hpl 
b/integration-tests/mdi/0048-stream-lookup.hpl
new file mode 100644
index 0000000000..3029a4dc84
--- /dev/null
+++ b/integration-tests/mdi/0048-stream-lookup.hpl
@@ -0,0 +1,446 @@
+<?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>0048-stream-lookup</name>
+    <name_sync_with_filename>Y</name_sync_with_filename>
+    <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/03/25 18:25:54.053</created_date>
+    <modified_user>-</modified_user>
+    <modified_date>2026/03/25 18:25:54.053</modified_date>
+  </info>
+  <notepads>
+  </notepads>
+  <order>
+    <hop>
+      <from>files/stream-lookup.xml</from>
+      <to>0048-stream-lookup.hpl</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>files/stream-lookup.xml keys</from>
+      <to>0048-stream-lookup.hpl</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>files/stream-lookup.xml values</from>
+      <to>0048-stream-lookup.hpl</to>
+      <enabled>Y</enabled>
+    </hop>
+  </order>
+  <transform>
+    <name>0048-stream-lookup.hpl</name>
+    <type>MetaInject</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <allow_empty_stream_on_execution>N</allow_empty_stream_on_execution>
+    <create_parent_folder>Y</create_parent_folder>
+    <filename>${PROJECT_HOME}/0048-stream-lookup-template.hpl</filename>
+    <mappings>
+      <mapping>
+        <source_field>from</source_field>
+        <source_transform>files/stream-lookup.xml</source_transform>
+        <target_attribute_key>SOURCE_TRANSFORM</target_attribute_key>
+        <target_detail>N</target_detail>
+        <target_transform_name>Stream lookup</target_transform_name>
+      </mapping>
+      <mapping>
+        <source_field>input_sorted</source_field>
+        <source_transform>files/stream-lookup.xml</source_transform>
+        <target_attribute_key>INPUT_SORTED</target_attribute_key>
+        <target_detail>N</target_detail>
+        <target_transform_name>Stream lookup</target_transform_name>
+      </mapping>
+      <mapping>
+        <source_field>preserve_memory</source_field>
+        <source_transform>files/stream-lookup.xml</source_transform>
+        <target_attribute_key>PRESERVE_MEMORY</target_attribute_key>
+        <target_detail>N</target_detail>
+        <target_transform_name>Stream lookup</target_transform_name>
+      </mapping>
+      <mapping>
+        <source_field>sorted_list</source_field>
+        <source_transform>files/stream-lookup.xml</source_transform>
+        <target_attribute_key>SORTED_LIST</target_attribute_key>
+        <target_detail>N</target_detail>
+        <target_transform_name>Stream lookup</target_transform_name>
+      </mapping>
+      <mapping>
+        <source_field>integer_pair</source_field>
+        <source_transform>files/stream-lookup.xml</source_transform>
+        <target_attribute_key>INTEGER_PAIR</target_attribute_key>
+        <target_detail>N</target_detail>
+        <target_transform_name>Stream lookup</target_transform_name>
+      </mapping>
+      <mapping>
+        <source_field>name</source_field>
+        <source_transform>files/stream-lookup.xml keys</source_transform>
+        <target_attribute_key>KEY_STREAM</target_attribute_key>
+        <target_detail>Y</target_detail>
+        <target_transform_name>Stream lookup</target_transform_name>
+      </mapping>
+      <mapping>
+        <source_field>field</source_field>
+        <source_transform>files/stream-lookup.xml keys</source_transform>
+        <target_attribute_key>KEY_LOOKUP</target_attribute_key>
+        <target_detail>Y</target_detail>
+        <target_transform_name>Stream lookup</target_transform_name>
+      </mapping>
+      <mapping>
+        <source_field>name</source_field>
+        <source_transform>files/stream-lookup.xml values</source_transform>
+        <target_attribute_key>RETRIEVE_VALUE</target_attribute_key>
+        <target_detail>Y</target_detail>
+        <target_transform_name>Stream lookup</target_transform_name>
+      </mapping>
+      <mapping>
+        <source_field>rename</source_field>
+        <source_transform>files/stream-lookup.xml values</source_transform>
+        <target_attribute_key>RETRIEVE_VALUE_NAME</target_attribute_key>
+        <target_detail>Y</target_detail>
+        <target_transform_name>Stream lookup</target_transform_name>
+      </mapping>
+      <mapping>
+        <source_field>default</source_field>
+        <source_transform>files/stream-lookup.xml values</source_transform>
+        <target_attribute_key>RETRIEVE_VALUE_DEFAULT</target_attribute_key>
+        <target_detail>Y</target_detail>
+        <target_transform_name>Stream lookup</target_transform_name>
+      </mapping>
+      <mapping>
+        <source_field>type</source_field>
+        <source_transform>files/stream-lookup.xml values</source_transform>
+        <target_attribute_key>RETRIEVE_DEFAULT_TYPE</target_attribute_key>
+        <target_detail>Y</target_detail>
+        <target_transform_name>Stream lookup</target_transform_name>
+      </mapping>
+    </mappings>
+    <no_execution>Y</no_execution>
+    <run_configuration>local</run_configuration>
+    <source_output_fields>
+</source_output_fields>
+    <target_file>${java.io.tmpdir}/stream-lookup.hpl</target_file>
+    <attributes/>
+    <GUI>
+      <xloc>320</xloc>
+      <yloc>64</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>files/stream-lookup.xml</name>
+    <type>getXMLData</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <IsAFile>N</IsAFile>
+    <IsIgnoreEmptyFile>N</IsIgnoreEmptyFile>
+    <IsInFields>N</IsInFields>
+    <addresultfile>N</addresultfile>
+    <doNotFailIfNoFile>Y</doNotFailIfNoFile>
+    <encoding>UTF-8</encoding>
+    <fields>
+      <field>
+        <currency/>
+        <decimal/>
+        <element_type>Node</element_type>
+        <format/>
+        <group/>
+        <length>-1</length>
+        <name>from</name>
+        <precision>-1</precision>
+        <repeat>N</repeat>
+        <result_type>valueof</result_type>
+        <trim_type>none</trim_type>
+        <type>String</type>
+        <xPath>from</xPath>
+      </field>
+      <field>
+        <currency/>
+        <decimal/>
+        <element_type>Node</element_type>
+        <format/>
+        <group/>
+        <length>-1</length>
+        <name>input_sorted</name>
+        <precision>-1</precision>
+        <repeat>N</repeat>
+        <result_type>valueof</result_type>
+        <trim_type>none</trim_type>
+        <type>String</type>
+        <xPath>input_sorted</xPath>
+      </field>
+      <field>
+        <currency/>
+        <decimal/>
+        <element_type>Node</element_type>
+        <format/>
+        <group/>
+        <length>-1</length>
+        <name>preserve_memory</name>
+        <precision>-1</precision>
+        <repeat>N</repeat>
+        <result_type>valueof</result_type>
+        <trim_type>none</trim_type>
+        <type>String</type>
+        <xPath>preserve_memory</xPath>
+      </field>
+      <field>
+        <currency/>
+        <decimal/>
+        <element_type>Node</element_type>
+        <format/>
+        <group/>
+        <length>-1</length>
+        <name>sorted_list</name>
+        <precision>-1</precision>
+        <repeat>N</repeat>
+        <result_type>valueof</result_type>
+        <trim_type>none</trim_type>
+        <type>String</type>
+        <xPath>sorted_list</xPath>
+      </field>
+      <field>
+        <currency/>
+        <decimal/>
+        <element_type>Node</element_type>
+        <format/>
+        <group/>
+        <length>-1</length>
+        <name>integer_pair</name>
+        <precision>-1</precision>
+        <repeat>N</repeat>
+        <result_type>valueof</result_type>
+        <trim_type>none</trim_type>
+        <type>String</type>
+        <xPath>integer_pair</xPath>
+      </field>
+    </fields>
+    <file>
+      <file_required>N</file_required>
+      <include_subfolders>N</include_subfolders>
+      <name>${PROJECT_HOME}/files/stream-lookup.xml</name>
+    </file>
+    <ignorecomments>N</ignorecomments>
+    <include>N</include>
+    <limit>0</limit>
+    <loopxpath>/transform</loopxpath>
+    <namespaceaware>N</namespaceaware>
+    <readurl>N</readurl>
+    <rownum>N</rownum>
+    <usetoken>N</usetoken>
+    <validating>N</validating>
+    <attributes/>
+    <GUI>
+      <xloc>96</xloc>
+      <yloc>64</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>files/stream-lookup.xml keys</name>
+    <type>getXMLData</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <IsAFile>N</IsAFile>
+    <IsIgnoreEmptyFile>N</IsIgnoreEmptyFile>
+    <IsInFields>N</IsInFields>
+    <addresultfile>N</addresultfile>
+    <doNotFailIfNoFile>Y</doNotFailIfNoFile>
+    <encoding>UTF-8</encoding>
+    <fields>
+      <field>
+        <currency/>
+        <decimal/>
+        <element_type>Node</element_type>
+        <format/>
+        <group/>
+        <length>-1</length>
+        <name>name</name>
+        <precision>-1</precision>
+        <repeat>N</repeat>
+        <result_type>valueof</result_type>
+        <trim_type>none</trim_type>
+        <type>String</type>
+        <xPath>name</xPath>
+      </field>
+      <field>
+        <currency/>
+        <decimal/>
+        <element_type>Node</element_type>
+        <format/>
+        <group/>
+        <length>-1</length>
+        <name>field</name>
+        <precision>-1</precision>
+        <repeat>N</repeat>
+        <result_type>valueof</result_type>
+        <trim_type>none</trim_type>
+        <type>String</type>
+        <xPath>field</xPath>
+      </field>
+    </fields>
+    <file>
+      <file_required>N</file_required>
+      <include_subfolders>N</include_subfolders>
+      <name>${PROJECT_HOME}/files/stream-lookup.xml</name>
+    </file>
+    <ignorecomments>N</ignorecomments>
+    <include>N</include>
+    <limit>0</limit>
+    <loopxpath>/transform/lookup/key</loopxpath>
+    <namespaceaware>N</namespaceaware>
+    <readurl>N</readurl>
+    <rownum>N</rownum>
+    <usetoken>N</usetoken>
+    <validating>N</validating>
+    <attributes/>
+    <GUI>
+      <xloc>96</xloc>
+      <yloc>128</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>files/stream-lookup.xml values</name>
+    <type>getXMLData</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <IsAFile>N</IsAFile>
+    <IsIgnoreEmptyFile>N</IsIgnoreEmptyFile>
+    <IsInFields>N</IsInFields>
+    <addresultfile>N</addresultfile>
+    <doNotFailIfNoFile>Y</doNotFailIfNoFile>
+    <encoding>UTF-8</encoding>
+    <fields>
+      <field>
+        <currency/>
+        <decimal/>
+        <element_type>Node</element_type>
+        <format/>
+        <group/>
+        <length>-1</length>
+        <name>name</name>
+        <precision>-1</precision>
+        <repeat>N</repeat>
+        <result_type>valueof</result_type>
+        <trim_type>none</trim_type>
+        <type>String</type>
+        <xPath>name</xPath>
+      </field>
+      <field>
+        <currency/>
+        <decimal/>
+        <element_type>Node</element_type>
+        <format/>
+        <group/>
+        <length>-1</length>
+        <name>rename</name>
+        <precision>-1</precision>
+        <repeat>N</repeat>
+        <result_type>valueof</result_type>
+        <trim_type>none</trim_type>
+        <type>String</type>
+        <xPath>rename</xPath>
+      </field>
+      <field>
+        <currency/>
+        <decimal/>
+        <element_type>Node</element_type>
+        <format/>
+        <group/>
+        <length>-1</length>
+        <name>default</name>
+        <precision>-1</precision>
+        <repeat>N</repeat>
+        <result_type>valueof</result_type>
+        <trim_type>none</trim_type>
+        <type>String</type>
+        <xPath>default</xPath>
+      </field>
+      <field>
+        <currency/>
+        <decimal/>
+        <element_type>Node</element_type>
+        <format/>
+        <group/>
+        <length>-1</length>
+        <name>type</name>
+        <precision>-1</precision>
+        <repeat>N</repeat>
+        <result_type>valueof</result_type>
+        <trim_type>none</trim_type>
+        <type>String</type>
+        <xPath>type</xPath>
+      </field>
+    </fields>
+    <file>
+      <file_required>N</file_required>
+      <include_subfolders>N</include_subfolders>
+      <name>${PROJECT_HOME}/files/stream-lookup.xml</name>
+    </file>
+    <ignorecomments>N</ignorecomments>
+    <include>N</include>
+    <limit>0</limit>
+    <loopxpath>/transform/lookup/value</loopxpath>
+    <namespaceaware>N</namespaceaware>
+    <readurl>N</readurl>
+    <rownum>N</rownum>
+    <usetoken>N</usetoken>
+    <validating>N</validating>
+    <attributes/>
+    <GUI>
+      <xloc>96</xloc>
+      <yloc>192</yloc>
+    </GUI>
+  </transform>
+  <transform_error_handling>
+  </transform_error_handling>
+  <attributes/>
+</pipeline>
diff --git a/integration-tests/mdi/datasets/golden-stream-lookup-keys.csv 
b/integration-tests/mdi/datasets/golden-stream-lookup-keys.csv
new file mode 100644
index 0000000000..dbd48d343f
--- /dev/null
+++ b/integration-tests/mdi/datasets/golden-stream-lookup-keys.csv
@@ -0,0 +1,3 @@
+name,field
+keyStream1,keyLookup1
+keyStream2,keyLookup2
diff --git a/integration-tests/mdi/datasets/golden-stream-lookup-values.csv 
b/integration-tests/mdi/datasets/golden-stream-lookup-values.csv
new file mode 100644
index 0000000000..bd9a572a9c
--- /dev/null
+++ b/integration-tests/mdi/datasets/golden-stream-lookup-values.csv
@@ -0,0 +1,4 @@
+name,rename,default,type
+returnValue1,value1,,String
+returnValue2,value2,123,Integer
+returnValue3,value3,true,Boolean
diff --git a/integration-tests/mdi/datasets/golden-stream-lookup.csv 
b/integration-tests/mdi/datasets/golden-stream-lookup.csv
new file mode 100644
index 0000000000..7cfc04651f
--- /dev/null
+++ b/integration-tests/mdi/datasets/golden-stream-lookup.csv
@@ -0,0 +1,2 @@
+from,input_sorted,preserve_memory,sorted_list,integer_pair
+Countries,Y,Y,Y,Y
diff --git a/integration-tests/mdi/files/stream-lookup.xml 
b/integration-tests/mdi/files/stream-lookup.xml
new file mode 100644
index 0000000000..ee2640a3c0
--- /dev/null
+++ b/integration-tests/mdi/files/stream-lookup.xml
@@ -0,0 +1,52 @@
+<?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.
+  -->
+<transform>
+    <from>Countries</from>
+    <input_sorted>Y</input_sorted>
+    <preserve_memory>Y</preserve_memory>
+    <sorted_list>Y</sorted_list>
+    <integer_pair>Y</integer_pair>
+    <lookup>
+        <key>
+            <name>keyStream1</name>
+            <field>keyLookup1</field>
+        </key>
+        <key>
+            <name>keyStream2</name>
+            <field>keyLookup2</field>
+        </key>
+        <value>
+            <name>returnValue1</name>
+            <rename>value1</rename>
+            <default/>
+            <type>String</type>
+        </value>
+        <value>
+            <name>returnValue2</name>
+            <rename>value2</rename>
+            <default>123</default>
+            <type>Integer</type>
+        </value>
+        <value>
+            <name>returnValue3</name>
+            <rename>value3</rename>
+            <default>true</default>
+            <type>Boolean</type>
+        </value>
+    </lookup>
+</transform>
diff --git a/integration-tests/mdi/main-0048-stream-lookup.hwf 
b/integration-tests/mdi/main-0048-stream-lookup.hwf
new file mode 100644
index 0000000000..a5651e16b7
--- /dev/null
+++ b/integration-tests/mdi/main-0048-stream-lookup.hwf
@@ -0,0 +1,113 @@
+<?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-0048-stream-lookup</name>
+  <name_sync_with_filename>Y</name_sync_with_filename>
+  <description/>
+  <extended_description/>
+  <workflow_version/>
+  <created_user>-</created_user>
+  <created_date>2026/03/25 18:24:13.232</created_date>
+  <modified_user>-</modified_user>
+  <modified_date>2026/03/25 18:24:13.232</modified_date>
+  <parameters>
+    </parameters>
+  <actions>
+    <action>
+      <name>Start</name>
+      <description/>
+      <type>SPECIAL</type>
+      <attributes/>
+      <DayOfMonth>1</DayOfMonth>
+      <doNotWaitOnFirstExecution>N</doNotWaitOnFirstExecution>
+      <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>80</xloc>
+      <yloc>64</yloc>
+      <attributes_hac/>
+    </action>
+    <action>
+      <name>0048-stream-lookup</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}/0048-stream-lookup.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>224</xloc>
+      <yloc>64</yloc>
+      <attributes_hac/>
+    </action>
+    <action>
+      <name>0048-stream-lookup-validate UNIT</name>
+      <description/>
+      <type>RunPipelineTests</type>
+      <attributes/>
+      <test_names>
+        <test_name>
+          <name>0048-stream-lookup-validate UNIT</name>
+        </test_name>
+      </test_names>
+      <parallel>N</parallel>
+      <xloc>416</xloc>
+      <yloc>64</yloc>
+      <attributes_hac/>
+    </action>
+  </actions>
+  <hops>
+    <hop>
+      <from>Start</from>
+      <to>0048-stream-lookup</to>
+      <enabled>Y</enabled>
+      <evaluation>Y</evaluation>
+      <unconditional>Y</unconditional>
+    </hop>
+    <hop>
+      <from>0048-stream-lookup</from>
+      <to>0048-stream-lookup-validate UNIT</to>
+      <enabled>Y</enabled>
+      <evaluation>Y</evaluation>
+      <unconditional>N</unconditional>
+    </hop>
+  </hops>
+  <notepads>
+  </notepads>
+  <attributes/>
+</workflow>
diff --git 
a/integration-tests/mdi/metadata/dataset/golden-stream-lookup-keys.json 
b/integration-tests/mdi/metadata/dataset/golden-stream-lookup-keys.json
new file mode 100644
index 0000000000..052242722e
--- /dev/null
+++ b/integration-tests/mdi/metadata/dataset/golden-stream-lookup-keys.json
@@ -0,0 +1,24 @@
+{
+  "base_filename": "golden-stream-lookup-keys.csv",
+  "name": "golden-stream-lookup-keys",
+  "description": "",
+  "dataset_fields": [
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "name",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "field",
+      "field_format": ""
+    }
+  ],
+  "folder_name": ""
+}
\ No newline at end of file
diff --git 
a/integration-tests/mdi/metadata/dataset/golden-stream-lookup-values.json 
b/integration-tests/mdi/metadata/dataset/golden-stream-lookup-values.json
new file mode 100644
index 0000000000..dff160dab8
--- /dev/null
+++ b/integration-tests/mdi/metadata/dataset/golden-stream-lookup-values.json
@@ -0,0 +1,40 @@
+{
+  "base_filename": "golden-stream-lookup-values.csv",
+  "name": "golden-stream-lookup-values",
+  "description": "",
+  "dataset_fields": [
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "name",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "rename",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "default",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "type",
+      "field_format": ""
+    }
+  ],
+  "folder_name": ""
+}
\ No newline at end of file
diff --git a/integration-tests/mdi/metadata/dataset/golden-stream-lookup.json 
b/integration-tests/mdi/metadata/dataset/golden-stream-lookup.json
new file mode 100644
index 0000000000..56ece7d7fb
--- /dev/null
+++ b/integration-tests/mdi/metadata/dataset/golden-stream-lookup.json
@@ -0,0 +1,48 @@
+{
+  "base_filename": "golden-stream-lookup.csv",
+  "name": "golden-stream-lookup",
+  "description": "",
+  "dataset_fields": [
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "from",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "input_sorted",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "preserve_memory",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "sorted_list",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "integer_pair",
+      "field_format": ""
+    }
+  ],
+  "folder_name": ""
+}
\ No newline at end of file
diff --git 
a/integration-tests/mdi/metadata/unit-test/0048-stream-lookup-validate 
UNIT.json 
b/integration-tests/mdi/metadata/unit-test/0048-stream-lookup-validate UNIT.json
new file mode 100644
index 0000000000..3c48f28b78
--- /dev/null
+++ b/integration-tests/mdi/metadata/unit-test/0048-stream-lookup-validate 
UNIT.json    
@@ -0,0 +1,90 @@
+{
+  "database_replacements": [],
+  "autoOpening": true,
+  "description": "",
+  "persist_filename": "",
+  "test_type": "UNIT_TEST",
+  "variableValues": [],
+  "basePath": "${HOP_UNIT_TESTS_FOLDER}",
+  "golden_data_sets": [
+    {
+      "field_mappings": [
+        {
+          "transform_field": "from",
+          "data_set_field": "from"
+        },
+        {
+          "transform_field": "input_sorted",
+          "data_set_field": "input_sorted"
+        },
+        {
+          "transform_field": "integer_pair",
+          "data_set_field": "integer_pair"
+        },
+        {
+          "transform_field": "preserve_memory",
+          "data_set_field": "preserve_memory"
+        },
+        {
+          "transform_field": "sorted_list",
+          "data_set_field": "sorted_list"
+        }
+      ],
+      "field_order": [
+        "from",
+        "input_sorted",
+        "preserve_memory",
+        "sorted_list",
+        "integer_pair"
+      ],
+      "data_set_name": "golden-stream-lookup",
+      "transform_name": "validate"
+    },
+    {
+      "field_mappings": [
+        {
+          "transform_field": "field",
+          "data_set_field": "field"
+        },
+        {
+          "transform_field": "name",
+          "data_set_field": "name"
+        }
+      ],
+      "field_order": [
+        "name"
+      ],
+      "data_set_name": "golden-stream-lookup-keys",
+      "transform_name": "validate keys"
+    },
+    {
+      "field_mappings": [
+        {
+          "transform_field": "default",
+          "data_set_field": "default"
+        },
+        {
+          "transform_field": "name",
+          "data_set_field": "name"
+        },
+        {
+          "transform_field": "rename",
+          "data_set_field": "rename"
+        },
+        {
+          "transform_field": "type",
+          "data_set_field": "type"
+        }
+      ],
+      "field_order": [
+        "name"
+      ],
+      "data_set_name": "golden-stream-lookup-values",
+      "transform_name": "validate values"
+    }
+  ],
+  "input_data_sets": [],
+  "name": "0048-stream-lookup-validate UNIT",
+  "trans_test_tweaks": [],
+  "pipeline_filename": "./0048-stream-lookup-validate.hpl"
+}
\ No newline at end of file
diff --git 
a/plugins/engines/beam/src/test/java/org/apache/hop/beam/transform/StreamLookupPipelineTest.java
 
b/plugins/engines/beam/src/test/java/org/apache/hop/beam/transform/MatchKeyPipelineTest.java
similarity index 95%
rename from 
plugins/engines/beam/src/test/java/org/apache/hop/beam/transform/StreamLookupPipelineTest.java
rename to 
plugins/engines/beam/src/test/java/org/apache/hop/beam/transform/MatchKeyPipelineTest.java
index b7671e1bfa..3b2a7917f5 100644
--- 
a/plugins/engines/beam/src/test/java/org/apache/hop/beam/transform/StreamLookupPipelineTest.java
+++ 
b/plugins/engines/beam/src/test/java/org/apache/hop/beam/transform/MatchKeyPipelineTest.java
@@ -21,7 +21,7 @@ import org.apache.hop.beam.util.BeamPipelineMetaUtil;
 import org.apache.hop.pipeline.PipelineMeta;
 import org.junit.jupiter.api.Test;
 
-class StreamLookupPipelineTest extends PipelineTestBase {
+class MatchKeyPipelineTest extends PipelineTestBase {
 
   @Test
   void testStreamLookupPipeline() throws Exception {
diff --git 
a/plugins/engines/beam/src/test/java/org/apache/hop/beam/util/BeamPipelineMetaUtil.java
 
b/plugins/engines/beam/src/test/java/org/apache/hop/beam/util/BeamPipelineMetaUtil.java
index d74956e380..a2d9f3d2ee 100644
--- 
a/plugins/engines/beam/src/test/java/org/apache/hop/beam/util/BeamPipelineMetaUtil.java
+++ 
b/plugins/engines/beam/src/test/java/org/apache/hop/beam/util/BeamPipelineMetaUtil.java
@@ -343,8 +343,6 @@ public class BeamPipelineMetaUtil {
     
beamInputTransformMeta.setTransformPluginId(BeamConst.STRING_BEAM_INPUT_PLUGIN_ID);
     pipelineMeta.addTransform(beamInputTransformMeta);
 
-    TransformMeta lookupBeamInputTransformMeta = beamInputTransformMeta;
-
     // Add a Memory Group By transform which will
     MemoryGroupByMeta memoryGroupByMeta = new MemoryGroupByMeta();
     memoryGroupByMeta.setGroups(List.of(new GGroup("stateCode")));
@@ -355,23 +353,27 @@ public class BeamPipelineMetaUtil {
     TransformMeta memoryGroupByTransformMeta = new 
TransformMeta("rowsPerState", memoryGroupByMeta);
     pipelineMeta.addTransform(memoryGroupByTransformMeta);
     pipelineMeta.addPipelineHop(
-        new PipelineHopMeta(lookupBeamInputTransformMeta, 
memoryGroupByTransformMeta));
+        new PipelineHopMeta(beamInputTransformMeta, 
memoryGroupByTransformMeta));
 
     // Add a Stream Lookup transform ...
     //
     StreamLookupMeta streamLookupMeta = new StreamLookupMeta();
-    streamLookupMeta.allocate(1, 1);
-    streamLookupMeta.getKeystream()[0] = "stateCode";
-    streamLookupMeta.getKeylookup()[0] = "stateCode";
-    streamLookupMeta.getValue()[0] = "rowsPerState";
-    streamLookupMeta.getValueName()[0] = "nrPerState";
-    streamLookupMeta.getValueDefault()[0] = null;
-    streamLookupMeta.getValueDefaultType()[0] = IValueMeta.TYPE_INTEGER;
+    StreamLookupMeta.MatchKey matchKey = new StreamLookupMeta.MatchKey();
+    matchKey.setKeyStream("stateCode");
+    matchKey.setKeyLookup("stateCode");
+    streamLookupMeta.getLookup().getMatchKeys().add(matchKey);
+
+    StreamLookupMeta.ReturnValue returnValue = new 
StreamLookupMeta.ReturnValue();
+    returnValue.setValue("rowsPerState");
+    returnValue.setValueName("nrPerState");
+    returnValue.setValueDefaultType(IValueMeta.TYPE_INTEGER);
+    streamLookupMeta.getLookup().getReturnValues().add(returnValue);
+
     streamLookupMeta.setMemoryPreservationActive(false);
     streamLookupMeta
         .getTransformIOMeta()
         .getInfoStreams()
-        .get(0)
+        .getFirst()
         .setTransformMeta(memoryGroupByTransformMeta); // Read from Mem.GroupBy
     TransformMeta streamLookupTransformMeta = new TransformMeta("Stream 
Lookup", streamLookupMeta);
     pipelineMeta.addTransform(streamLookupTransformMeta);
diff --git 
a/plugins/transforms/streamlookup/src/main/java/org/apache/hop/pipeline/transforms/streamlookup/StreamLookup.java
 
b/plugins/transforms/streamlookup/src/main/java/org/apache/hop/pipeline/transforms/streamlookup/StreamLookup.java
index 10ce69a7fb..b0c25b3296 100644
--- 
a/plugins/transforms/streamlookup/src/main/java/org/apache/hop/pipeline/transforms/streamlookup/StreamLookup.java
+++ 
b/plugins/transforms/streamlookup/src/main/java/org/apache/hop/pipeline/transforms/streamlookup/StreamLookup.java
@@ -58,15 +58,14 @@ public class StreamLookup extends 
BaseTransform<StreamLookupMeta, StreamLookupDa
   }
 
   private void handleNullIf() {
-    data.nullIf = new Object[meta.getValue().length];
-
-    for (int i = 0; i < meta.getValue().length; i++) {
-      if (meta.getValueDefaultType()[i] < 0) {
-
-        meta.getValueDefaultType()[i] = IValueMeta.TYPE_STRING;
+    data.nullIf = new Object[meta.getLookup().getReturnValues().size()];
+    for (int i = 0; i < data.nullIf.length; i++) {
+      StreamLookupMeta.ReturnValue returnValue = 
meta.getLookup().getReturnValues().get(i);
+      if (returnValue.getValueDefaultType() < 0) {
+        returnValue.setValueDefaultType(IValueMeta.TYPE_STRING);
       }
       data.nullIf[i] = null;
-      switch (meta.getValueDefaultType()[i]) {
+      switch (returnValue.getValueDefaultType()) {
         case IValueMeta.TYPE_STRING:
           if (Utils.isEmpty(data.valueDefault[i])) {
             data.nullIf[i] = null;
@@ -116,7 +115,7 @@ public class StreamLookup extends 
BaseTransform<StreamLookupMeta, StreamLookupDa
             throw new RuntimeException(
                 BaseMessages.getString(PKG, 
"StreamLookup.Exception.ConversionNotImplemented")
                     + " "
-                    + 
ValueMetaFactory.getValueMetaName(meta.getValueDefaultType()[i]));
+                    + 
ValueMetaFactory.getValueMetaName(returnValue.getValueDefaultType()));
           } else {
             // no default value given: just set it to null
             data.nullIf[i] = null;
@@ -127,7 +126,7 @@ public class StreamLookup extends 
BaseTransform<StreamLookupMeta, StreamLookupDa
   }
 
   private boolean readLookupValues() throws HopException {
-    data.infoStream = meta.getTransformIOMeta().getInfoStreams().get(0);
+    data.infoStream = meta.getTransformIOMeta().getInfoStreams().getFirst();
     if (data.infoStream.getTransformMeta() == null) {
       logError(BaseMessages.getString(PKG, 
"StreamLookup.Log.NoLookupTransformSpecified"));
       return false;
@@ -139,8 +138,8 @@ public class StreamLookup extends 
BaseTransform<StreamLookupMeta, StreamLookupDa
               + "]");
     }
 
-    int[] keyNrs = new int[meta.getKeylookup().length];
-    int[] valueNrs = new int[meta.getValue().length];
+    int[] keyNrs = new int[meta.getLookup().getMatchKeys().size()];
+    int[] valueNrs = new int[meta.getLookup().getReturnValues().size()];
     boolean firstRun = true;
 
     // Which row set do we read from?
@@ -163,12 +162,13 @@ public class StreamLookup extends 
BaseTransform<StreamLookupMeta, StreamLookupDa
         IRowMeta cacheValueMeta = new RowMeta();
 
         // Look up the keys in the source rows
-        for (int i = 0; i < meta.getKeylookup().length; i++) {
-          keyNrs[i] = rowSet.getRowMeta().indexOfValue(meta.getKeylookup()[i]);
+        for (int i = 0; i < meta.getLookup().getMatchKeys().size(); i++) {
+          StreamLookupMeta.MatchKey matchKey = 
meta.getLookup().getMatchKeys().get(i);
+          keyNrs[i] = 
rowSet.getRowMeta().indexOfValue(matchKey.getKeyLookup());
           if (keyNrs[i] < 0) {
             throw new HopTransformException(
                 BaseMessages.getString(
-                    PKG, "StreamLookup.Exception.UnableToFindField", 
meta.getKeylookup()[i]));
+                    PKG, "StreamLookup.Exception.UnableToFindField", 
matchKey.getKeyLookup()));
           }
           
cacheKeyMeta.addValueMeta(rowSet.getRowMeta().getValueMeta(keyNrs[i]));
         }
@@ -182,12 +182,13 @@ public class StreamLookup extends 
BaseTransform<StreamLookupMeta, StreamLookupDa
           
cacheKeyMeta.getValueMeta(i).setStorageType(IValueMeta.STORAGE_TYPE_NORMAL);
         }
 
-        for (int v = 0; v < meta.getValue().length; v++) {
-          valueNrs[v] = rowSet.getRowMeta().indexOfValue(meta.getValue()[v]);
+        for (int v = 0; v < meta.getLookup().getReturnValues().size(); v++) {
+          StreamLookupMeta.ReturnValue returnValue = 
meta.getLookup().getReturnValues().get(v);
+          valueNrs[v] = 
rowSet.getRowMeta().indexOfValue(returnValue.getValue());
           if (valueNrs[v] < 0) {
             throw new HopTransformException(
                 BaseMessages.getString(
-                    PKG, "StreamLookup.Exception.UnableToFindField", 
meta.getValue()[v]));
+                    PKG, "StreamLookup.Exception.UnableToFindField", 
returnValue.getValue()));
           }
           
cacheValueMeta.addValueMeta(rowSet.getRowMeta().getValueMeta(valueNrs[v]));
         }
@@ -274,7 +275,7 @@ public class StreamLookup extends 
BaseTransform<StreamLookupMeta, StreamLookupDa
 
     if (data.hasLookupRows) {
       try {
-        if (meta.getKeystream().length > 0) {
+        if (!meta.getLookup().getMatchKeys().isEmpty()) {
           add = getFromCache(data.cacheKeyMeta, lu);
         } else {
           // Just take the first element in the hashtable...
@@ -379,7 +380,6 @@ public class StreamLookup extends 
BaseTransform<StreamLookupMeta, StreamLookupDa
 
   @Override
   public boolean processRow() throws HopException {
-
     if (data.readLookupValues) {
       data.readLookupValues = false;
 
@@ -391,15 +391,7 @@ public class StreamLookup extends 
BaseTransform<StreamLookupMeta, StreamLookupDa
         return false;
       }
 
-      // Calculate the default values by doing variable replacement.
-      //
-      data.valueDefault = new String[meta.getValueDefault().length];
-      for (int i = 0; i < data.valueDefault.length; i++) {
-        // We want to do this variable replacement only once since it's a 
relatively slow operation.
-        //
-        data.valueDefault[i] = resolve(meta.getValueDefault()[i]);
-      }
-
+      calculateDefaultValues();
       return true;
     }
 
@@ -418,66 +410,17 @@ public class StreamLookup extends 
BaseTransform<StreamLookupMeta, StreamLookupDa
 
     if (first) {
       first = false;
-
-      // read the lookup values!
-      data.keynrs = new int[meta.getKeystream().length];
-      data.lookupMeta = new RowMeta();
-      data.convertKeysToNative = new boolean[meta.getKeystream().length];
-
-      for (int i = 0; i < meta.getKeystream().length; i++) {
-        // Find the keynr in the row (only once)
-        data.keynrs[i] = 
getInputRowMeta().indexOfValue(meta.getKeystream()[i]);
-        if (data.keynrs[i] < 0) {
-          throw new HopTransformException(
-              BaseMessages.getString(
-                  PKG,
-                  "StreamLookup.Log.FieldNotFound",
-                  meta.getKeystream()[i],
-                  "" + getInputRowMeta().getString(r)));
-        } else {
-          if (isDetailed()) {
-            logDetailed(
-                BaseMessages.getString(
-                    PKG,
-                    "StreamLookup.Log.FieldInfo",
-                    meta.getKeystream()[i],
-                    "" + data.keynrs[i]));
-          }
-        }
-
-        
data.lookupMeta.addValueMeta(getInputRowMeta().getValueMeta(data.keynrs[i]).clone());
-
-        // If we have binary storage data coming in, we convert it to normal 
data storage.
-        // The storage in the lookup data store is also normal data storage. 
TODO: enforce normal
-        // data storage??
-        //
-        data.convertKeysToNative[i] =
-            
getInputRowMeta().getValueMeta(data.keynrs[i]).isStorageBinaryString();
-      }
-
-      data.outputRowMeta = getInputRowMeta().clone();
-      meta.getFields(
-          data.outputRowMeta,
-          getTransformName(),
-          new IRowMeta[] {data.infoMeta},
-          null,
-          this,
-          metadataProvider);
-
-      // Handle the NULL values (not found...)
-      handleNullIf();
+      processRowFirstCall(r);
     }
 
     Object[] outputRow =
         lookupValues(getInputRowMeta(), r); // Do the actual lookup in the 
hastable.
     if (outputRow == null) {
       setOutputDone(); // signal end to receiver(s)
-
       return false;
     }
 
     putRow(data.outputRowMeta, outputRow); // copy row to output rowset(s)
-
     if (checkFeedback(getLinesRead()) && isBasic()) {
       logBasic(BaseMessages.getString(PKG, "StreamLookup.Log.LineNumber") + 
getLinesRead());
     }
@@ -485,15 +428,73 @@ public class StreamLookup extends 
BaseTransform<StreamLookupMeta, StreamLookupDa
     return true;
   }
 
+  private void processRowFirstCall(Object[] r) throws HopTransformException, 
HopValueException {
+    // read the lookup values!
+    data.keynrs = new int[meta.getLookup().getMatchKeys().size()];
+    data.lookupMeta = new RowMeta();
+    data.convertKeysToNative = new 
boolean[meta.getLookup().getMatchKeys().size()];
+
+    for (int i = 0; i < meta.getLookup().getMatchKeys().size(); i++) {
+      StreamLookupMeta.MatchKey key = meta.getLookup().getMatchKeys().get(i);
+
+      // Find the keynr in the row (only once)
+      data.keynrs[i] = getInputRowMeta().indexOfValue(key.getKeyStream());
+      if (data.keynrs[i] < 0) {
+        throw new HopTransformException(
+            BaseMessages.getString(
+                PKG,
+                "StreamLookup.Log.FieldNotFound",
+                key.getKeyStream(),
+                getInputRowMeta().getString(r)));
+      } else {
+        if (isDetailed()) {
+          logDetailed(
+              BaseMessages.getString(
+                  PKG, "StreamLookup.Log.FieldInfo", key.getKeyStream(), "" + 
data.keynrs[i]));
+        }
+      }
+
+      
data.lookupMeta.addValueMeta(getInputRowMeta().getValueMeta(data.keynrs[i]).clone());
+
+      // If we have binary storage data coming in, we convert it to normal 
data storage.
+      // The storage in the lookup data store is also normal data storage.
+      //
+      data.convertKeysToNative[i] =
+          
getInputRowMeta().getValueMeta(data.keynrs[i]).isStorageBinaryString();
+    }
+
+    data.outputRowMeta = getInputRowMeta().clone();
+    meta.getFields(
+        data.outputRowMeta,
+        getTransformName(),
+        new IRowMeta[] {data.infoMeta},
+        null,
+        this,
+        metadataProvider);
+
+    // Handle the NULL values (not found...)
+    handleNullIf();
+  }
+
+  private void calculateDefaultValues() {
+    // Calculate the default values by doing variable replacement.
+    //
+    data.valueDefault = new String[meta.getLookup().getReturnValues().size()];
+    for (int i = 0; i < data.valueDefault.length; i++) {
+      StreamLookupMeta.ReturnValue returnValue = 
meta.getLookup().getReturnValues().get(i);
+
+      // We want to do this variable replacement only once since it's a 
relatively slow operation.
+      //
+      data.valueDefault[i] = resolve(returnValue.getValueDefault());
+    }
+  }
+
   @Override
   public boolean init() {
-
     if (super.init()) {
       data.readLookupValues = true;
-
       return true;
     }
-
     return false;
   }
 
diff --git 
a/plugins/transforms/streamlookup/src/main/java/org/apache/hop/pipeline/transforms/streamlookup/StreamLookupDialog.java
 
b/plugins/transforms/streamlookup/src/main/java/org/apache/hop/pipeline/transforms/streamlookup/StreamLookupDialog.java
index 34d7d8476e..d9fe74c08f 100644
--- 
a/plugins/transforms/streamlookup/src/main/java/org/apache/hop/pipeline/transforms/streamlookup/StreamLookupDialog.java
+++ 
b/plugins/transforms/streamlookup/src/main/java/org/apache/hop/pipeline/transforms/streamlookup/StreamLookupDialog.java
@@ -18,6 +18,7 @@
 package org.apache.hop.pipeline.transforms.streamlookup;
 
 import java.util.List;
+import org.apache.commons.lang.StringUtils;
 import org.apache.hop.core.Const;
 import org.apache.hop.core.exception.HopException;
 import org.apache.hop.core.row.IRowMeta;
@@ -87,7 +88,7 @@ public class StreamLookupDialog extends BaseTransformDialog {
         .get(e -> get())
         .custom(
             BaseMessages.getString(PKG, 
"StreamLookupDialog.GetLookupFields.Button"),
-            e -> getlookup())
+            e -> getLookup())
         .build();
 
     ModifyListener lsMod = e -> input.setChanged();
@@ -134,7 +135,6 @@ public class StreamLookupDialog extends BaseTransformDialog 
{
     wlKey.setLayoutData(fdlKey);
 
     int nrKeyCols = 2;
-    int nrKeyRows = (input.getKeystream() != null ? 
input.getKeystream().length : 1);
 
     ciKey = new ColumnInfo[nrKeyCols];
     ciKey[0] =
@@ -156,7 +156,7 @@ public class StreamLookupDialog extends BaseTransformDialog 
{
             shell,
             SWT.BORDER | SWT.FULL_SELECTION | SWT.MULTI | SWT.V_SCROLL | 
SWT.H_SCROLL,
             ciKey,
-            nrKeyRows,
+            1,
             lsMod,
             props);
 
@@ -240,7 +240,6 @@ public class StreamLookupDialog extends BaseTransformDialog 
{
     wlReturn.setLayoutData(fdlReturn);
 
     int upInsCols = 4;
-    int upInsRows = (input.getValue() != null ? input.getValue().length : 1);
 
     ciReturn = new ColumnInfo[upInsCols];
     ciReturn[0] =
@@ -272,7 +271,7 @@ public class StreamLookupDialog extends BaseTransformDialog 
{
             shell,
             SWT.BORDER | SWT.FULL_SELECTION | SWT.MULTI | SWT.V_SCROLL | 
SWT.H_SCROLL,
             ciReturn,
-            upInsRows,
+            1,
             lsMod,
             props);
 
@@ -360,37 +359,26 @@ public class StreamLookupDialog extends 
BaseTransformDialog {
       logDebug(BaseMessages.getString(PKG, 
"StreamLookupDialog.Log.GettingKeyInfo"));
     }
 
-    if (input.getKeystream() != null) {
-      for (int i = 0; i < input.getKeystream().length; i++) {
-        TableItem item = wKey.table.getItem(i);
-        if (input.getKeystream()[i] != null) {
-          item.setText(1, input.getKeystream()[i]);
-        }
-        if (input.getKeylookup()[i] != null) {
-          item.setText(2, input.getKeylookup()[i]);
-        }
-      }
+    for (StreamLookupMeta.MatchKey matchKey : 
input.getLookup().getMatchKeys()) {
+      TableItem item = new TableItem(wKey.table, SWT.NONE);
+      item.setText(1, Const.NVL(matchKey.getKeyStream(), ""));
+      item.setText(2, Const.NVL(matchKey.getKeyLookup(), ""));
     }
-
-    if (input.getValue() != null) {
-      for (int i = 0; i < input.getValue().length; i++) {
-        TableItem item = wReturn.table.getItem(i);
-        if (input.getValue()[i] != null) {
-          item.setText(1, input.getValue()[i]);
-        }
-        if (input.getValueName()[i] != null
-            && !input.getValueName()[i].equals(input.getValue()[i])) {
-          item.setText(2, input.getValueName()[i]);
-        }
-        if (input.getValueDefault()[i] != null) {
-          item.setText(3, input.getValueDefault()[i]);
-        }
-        item.setText(4, 
ValueMetaFactory.getValueMetaName(input.getValueDefaultType()[i]));
+    wKey.optimizeTableView();
+
+    for (StreamLookupMeta.ReturnValue returnValue : 
input.getLookup().getReturnValues()) {
+      TableItem item = new TableItem(wReturn.table, SWT.NONE);
+      item.setText(1, Const.NVL(returnValue.getValue(), ""));
+      if (StringUtils.isNotEmpty(returnValue.getValueName())
+          && 
!returnValue.getValueName().equalsIgnoreCase(returnValue.getValue())) {
+        item.setText(2, returnValue.getValueName());
       }
+      item.setText(3, Const.NVL(returnValue.getValueDefault(), ""));
+      item.setText(4, 
ValueMetaFactory.getValueMetaName(returnValue.getValueDefaultType()));
     }
+    wReturn.optimizeTableView();
 
-    IStream infoStream = input.getTransformIOMeta().getInfoStreams().get(0);
-    wTransform.setText(Const.NVL(infoStream.getTransformName(), ""));
+    wTransform.setText(Const.NVL(input.getSourceTransformName(), ""));
 
     boolean isPreserveMemory = input.isMemoryPreservationActive();
     wPreserveMemory.setSelection(isPreserveMemory);
@@ -403,11 +391,6 @@ public class StreamLookupDialog extends 
BaseTransformDialog {
     // but user will be forced to choose only one option later.
     wSortedList.setSelection(input.isUsingSortedList());
     wIntegerPair.setSelection(input.isUsingIntegerPair());
-
-    wKey.setRowNums();
-    wKey.optWidth(true);
-    wReturn.setRowNums();
-    wReturn.optWidth(true);
   }
 
   private void cancel() {
@@ -421,40 +404,36 @@ public class StreamLookupDialog extends 
BaseTransformDialog {
       return;
     }
 
-    int nrkeys = wKey.nrNonEmpty();
-    int nrvalues = wReturn.nrNonEmpty();
-    input.allocate(nrkeys, nrvalues);
     input.setMemoryPreservationActive(wPreserveMemory.getSelection());
     input.setUsingSortedList(wSortedList.getSelection());
     input.setUsingIntegerPair(wIntegerPair.getSelection());
-
-    if (log.isDebug()) {
-      logDebug(BaseMessages.getString(PKG, "StreamLookupDialog.Log.FoundKeys", 
nrkeys + ""));
+    StreamLookupMeta.Lookup lookup = input.getLookup();
+    lookup.getMatchKeys().clear();
+    for (TableItem item : wKey.getNonEmptyItems()) {
+      StreamLookupMeta.MatchKey matchKey = new StreamLookupMeta.MatchKey();
+      lookup.getMatchKeys().add(matchKey);
+
+      matchKey.setKeyStream(item.getText(1));
+      matchKey.setKeyLookup(item.getText(2));
     }
 
-    for (int i = 0; i < nrkeys; i++) {
-      TableItem item = wKey.getNonEmpty(i);
-      input.getKeystream()[i] = item.getText(1);
-      input.getKeylookup()[i] = item.getText(2);
-    }
-
-    if (log.isDebug()) {
-      logDebug(BaseMessages.getString(PKG, 
"StreamLookupDialog.Log.FoundFields", nrvalues + ""));
-    }
+    lookup.getReturnValues().clear();
+    for (TableItem item : wReturn.getNonEmptyItems()) {
+      StreamLookupMeta.ReturnValue returnValue = new 
StreamLookupMeta.ReturnValue();
+      lookup.getReturnValues().add(returnValue);
 
-    for (int i = 0; i < nrvalues; i++) {
-      TableItem item = wReturn.getNonEmpty(i);
-      input.getValue()[i] = item.getText(1);
-      input.getValueName()[i] = item.getText(2);
-      if (Utils.isEmpty(input.getValueName()[i])) {
-        input.getValueName()[i] = input.getValue()[i];
+      returnValue.setValue(item.getText(1));
+      returnValue.setValueName(item.getText(2));
+      if (Utils.isEmpty(returnValue.getValueName())) {
+        returnValue.setValueName(returnValue.getValue());
       }
-      input.getValueDefault()[i] = item.getText(3);
-      input.getValueDefaultType()[i] = 
ValueMetaFactory.getIdForValueMeta(item.getText(4));
+      returnValue.setValueDefault(item.getText(3));
+      
returnValue.setValueDefaultType(ValueMetaFactory.getIdForValueMeta(item.getText(4)));
     }
 
-    IStream infoStream = input.getTransformIOMeta().getInfoStreams().get(0);
-    
infoStream.setTransformMeta(pipelineMeta.findTransform(wTransform.getText()));
+    input.setSourceTransformName(wTransform.getText());
+    input.searchInfoAndTargetTransforms(pipelineMeta.getTransforms());
+    IStream infoStream = 
input.getTransformIOMeta().getInfoStreams().getFirst();
     if (infoStream.getTransformMeta() == null) {
       MessageBox mb = new MessageBox(shell, SWT.OK | SWT.ICON_ERROR);
       if (Utils.isEmpty(wTransform.getText())) {
@@ -533,7 +512,7 @@ public class StreamLookupDialog extends BaseTransformDialog 
{
     }
   }
 
-  private void getlookup() {
+  private void getLookup() {
     try {
       String transformFrom = wTransform.getText();
       if (!Utils.isEmpty(transformFrom)) {
diff --git 
a/plugins/transforms/streamlookup/src/main/java/org/apache/hop/pipeline/transforms/streamlookup/StreamLookupMeta.java
 
b/plugins/transforms/streamlookup/src/main/java/org/apache/hop/pipeline/transforms/streamlookup/StreamLookupMeta.java
index 3f8b55c233..5a8fe0ab1e 100644
--- 
a/plugins/transforms/streamlookup/src/main/java/org/apache/hop/pipeline/transforms/streamlookup/StreamLookupMeta.java
+++ 
b/plugins/transforms/streamlookup/src/main/java/org/apache/hop/pipeline/transforms/streamlookup/StreamLookupMeta.java
@@ -17,22 +17,22 @@
 
 package org.apache.hop.pipeline.transforms.streamlookup;
 
+import java.util.ArrayList;
 import java.util.List;
+import lombok.Getter;
+import lombok.Setter;
 import org.apache.hop.core.CheckResult;
 import org.apache.hop.core.Const;
 import org.apache.hop.core.ICheckResult;
 import org.apache.hop.core.annotations.Transform;
 import org.apache.hop.core.exception.HopTransformException;
-import org.apache.hop.core.exception.HopXmlException;
-import org.apache.hop.core.injection.AfterInjection;
-import org.apache.hop.core.injection.Injection;
-import org.apache.hop.core.injection.InjectionSupported;
 import org.apache.hop.core.row.IRowMeta;
 import org.apache.hop.core.row.IValueMeta;
+import org.apache.hop.core.row.value.ValueMetaBase;
 import org.apache.hop.core.row.value.ValueMetaFactory;
 import org.apache.hop.core.variables.IVariables;
-import org.apache.hop.core.xml.XmlHandler;
 import org.apache.hop.i18n.BaseMessages;
+import org.apache.hop.metadata.api.HopMetadataProperty;
 import org.apache.hop.metadata.api.IHopMetadataProvider;
 import org.apache.hop.pipeline.PipelineMeta;
 import org.apache.hop.pipeline.transform.BaseTransformMeta;
@@ -43,9 +43,7 @@ import org.apache.hop.pipeline.transform.stream.IStream;
 import org.apache.hop.pipeline.transform.stream.IStream.StreamType;
 import org.apache.hop.pipeline.transform.stream.Stream;
 import org.apache.hop.pipeline.transform.stream.StreamIcon;
-import org.w3c.dom.Node;
 
-@InjectionSupported(localizationPrefix = "StreamLookupMeta.Injection.")
 @Transform(
     id = "StreamLookup",
     image = "streamlookup.svg",
@@ -54,149 +52,68 @@ import org.w3c.dom.Node;
     categoryDescription = 
"i18n:org.apache.hop.pipeline.transform:BaseTransform.Category.Lookup",
     keywords = "i18n::StreamLookupMeta.keyword",
     documentationUrl = "/pipeline/transforms/streamlookup.html")
+@Getter
+@Setter
 public class StreamLookupMeta extends BaseTransformMeta<StreamLookup, 
StreamLookupData> {
   private static final Class<?> PKG = StreamLookupMeta.class;
   public static final String CONST_SPACES = "        ";
 
-  /** fields in input streams with which we look up values */
-  @Injection(name = "KEY_STREAM")
-  private String[] keystream;
-
-  /** fields in lookup stream with which we look up values */
-  @Injection(name = "KEY_LOOKUP")
-  private String[] keylookup;
-
-  /** return these field values from lookup */
-  @Injection(name = "RETRIEVE_VALUE")
-  private String[] value;
-
-  /** rename to this after lookup */
-  @Injection(name = "RETRIEVE_VALUE_NAME")
-  private String[] valueName;
-
-  /** default value in case not found... */
-  @Injection(name = "RETRIEVE_VALUE_DEFAULT")
-  private String[] valueDefault;
-
-  /** type of default value */
-  @Injection(name = "RETRIEVE_DEFAULT_TYPE")
-  private int[] valueDefaultType;
+  @HopMetadataProperty(
+      key = "from",
+      injectionKey = "SOURCE_TRANSFORM",
+      injectionKeyDescription = "StreamLookupMeta.Injection.SOURCE_TRANSFORM")
+  private String sourceTransformName;
 
   /** Indicate that the input is considered sorted! */
+  @HopMetadataProperty(
+      key = "input_sorted",
+      injectionKey = "INPUT_SORTED",
+      injectionKeyDescription = "StreamLookupMeta.Injection.INPUT_SORTED")
   private boolean inputSorted;
 
   /** Indicate that we need to preserve memory by serializing objects */
-  @Injection(name = "PRESERVE_MEMORY")
+  @HopMetadataProperty(
+      key = "preserve_memory",
+      injectionKey = "PRESERVE_MEMORY",
+      injectionKeyDescription = "StreamLookupMeta.Injection.PRESERVE_MEMORY")
   private boolean memoryPreservationActive;
 
   /** Indicate that we want to use a sorted list vs. a hashtable */
-  @Injection(name = "SORTED_LIST")
+  @HopMetadataProperty(
+      key = "sorted_list",
+      injectionKey = "SORTED_LIST",
+      injectionKeyDescription = "StreamLookupMeta.Injection.SORTED_LIST")
   private boolean usingSortedList;
 
   /** The content of the key and lookup is a single Integer (long) */
-  @Injection(name = "INTEGER_PAIR")
+  @HopMetadataProperty(
+      key = "integer_pair",
+      injectionKey = "INTEGER_PAIR",
+      injectionKeyDescription = "StreamLookupMeta.Injection.INTEGER_PAIR")
   private boolean usingIntegerPair;
 
-  public StreamLookupMeta() {
-    super(); // allocate BaseTransformMeta
-  }
+  @HopMetadataProperty(key = "lookup")
+  private Lookup lookup;
 
-  @Override
-  public void loadXml(Node transformNode, IHopMetadataProvider 
metadataProvider)
-      throws HopXmlException {
-    readData(transformNode);
+  public StreamLookupMeta() {
+    super();
+    memoryPreservationActive = true;
+    lookup = new Lookup();
   }
 
-  public void allocate(int nrkeys, int nrvalues) {
-    setKeystream(new String[nrkeys]);
-    setKeylookup(new String[nrkeys]);
-
-    setValue(new String[nrvalues]);
-    setValueName(new String[nrvalues]);
-    setValueDefault(new String[nrvalues]);
-    setValueDefaultType(new int[nrvalues]);
+  public StreamLookupMeta(StreamLookupMeta m) {
+    this();
+    this.sourceTransformName = m.sourceTransformName;
+    this.inputSorted = m.inputSorted;
+    this.memoryPreservationActive = m.memoryPreservationActive;
+    this.usingSortedList = m.usingSortedList;
+    this.usingIntegerPair = m.usingIntegerPair;
+    this.lookup = new Lookup(m.lookup);
   }
 
   @Override
   public Object clone() {
-    StreamLookupMeta retval = (StreamLookupMeta) super.clone();
-    ITransformIOMeta thisTransformIO = getTransformIOMeta();
-    ITransformIOMeta thatTransformIO = retval.getTransformIOMeta();
-    if (thisTransformIO != null
-        && thisTransformIO.getInfoStreams() != null
-        && thatTransformIO != null
-        && thatTransformIO.getInfoStreams() != null) {
-      List<IStream> thisInfoStream = thisTransformIO.getInfoStreams();
-      List<IStream> thatInfoStream = thatTransformIO.getInfoStreams();
-      
thatInfoStream.get(0).setStreamType(thisInfoStream.get(0).getStreamType());
-      
thatInfoStream.get(0).setTransformMeta(thisInfoStream.get(0).getTransformMeta());
-      
thatInfoStream.get(0).setDescription(thisInfoStream.get(0).getDescription());
-      
thatInfoStream.get(0).setStreamIcon(thisInfoStream.get(0).getStreamIcon());
-      thatInfoStream.get(0).setSubject(thisInfoStream.get(0).getSubject());
-    }
-
-    int nrkeys = keystream.length;
-    int nrvals = value.length;
-    retval.allocate(nrkeys, nrvals);
-    System.arraycopy(keystream, 0, retval.keystream, 0, nrkeys);
-    System.arraycopy(keylookup, 0, retval.keylookup, 0, nrkeys);
-    System.arraycopy(value, 0, retval.value, 0, nrvals);
-    System.arraycopy(valueName, 0, retval.valueName, 0, nrvals);
-    System.arraycopy(valueDefault, 0, retval.valueDefault, 0, nrvals);
-    System.arraycopy(valueDefaultType, 0, retval.valueDefaultType, 0, nrvals);
-    return retval;
-  }
-
-  private void readData(Node transformNode) throws HopXmlException {
-    try {
-      String dtype;
-      int nrkeys;
-      int nrvalues;
-
-      String lookupFromTransformName = XmlHandler.getTagValue(transformNode, 
"from");
-      IStream infoStream = getTransformIOMeta().getInfoStreams().get(0);
-      infoStream.setSubject(lookupFromTransformName);
-
-      
setInputSorted("Y".equalsIgnoreCase(XmlHandler.getTagValue(transformNode, 
"input_sorted")));
-      setMemoryPreservationActive(
-          "Y".equalsIgnoreCase(XmlHandler.getTagValue(transformNode, 
"preserve_memory")));
-      setUsingSortedList(
-          "Y".equalsIgnoreCase(XmlHandler.getTagValue(transformNode, 
"sorted_list")));
-      setUsingIntegerPair(
-          "Y".equalsIgnoreCase(XmlHandler.getTagValue(transformNode, 
"integer_pair")));
-
-      Node lookup = XmlHandler.getSubNode(transformNode, "lookup");
-      nrkeys = XmlHandler.countNodes(lookup, "key");
-      nrvalues = XmlHandler.countNodes(lookup, "value");
-
-      allocate(nrkeys, nrvalues);
-
-      for (int i = 0; i < nrkeys; i++) {
-        Node knode = XmlHandler.getSubNodeByNr(lookup, "key", i);
-
-        getKeystream()[i] = XmlHandler.getTagValue(knode, "name");
-        getKeylookup()[i] = XmlHandler.getTagValue(knode, "field");
-      }
-
-      for (int i = 0; i < nrvalues; i++) {
-        Node vnode = XmlHandler.getSubNodeByNr(lookup, "value", i);
-
-        getValue()[i] = XmlHandler.getTagValue(vnode, "name");
-        getValueName()[i] = XmlHandler.getTagValue(vnode, "rename");
-        if (getValueName()[i] == null) {
-          getValueName()[i] = getValue()[i]; // default: same name to return!
-        }
-
-        getValueDefault()[i] = XmlHandler.getTagValue(vnode, "default");
-        dtype = XmlHandler.getTagValue(vnode, "type");
-        getValueDefaultType()[i] = ValueMetaFactory.getIdForValueMeta(dtype);
-      }
-    } catch (Exception e) {
-      throw new HopXmlException(
-          BaseMessages.getString(
-              PKG, 
"StreamLookupMeta.Exception.UnableToLoadTransformMetaFromXML"),
-          e);
-    }
+    return new StreamLookupMeta(this);
   }
 
   @Override
@@ -207,15 +124,6 @@ public class StreamLookupMeta extends 
BaseTransformMeta<StreamLookup, StreamLook
     }
   }
 
-  @Override
-  public void setDefault() {
-    setMemoryPreservationActive(true);
-    setUsingSortedList(false);
-    setUsingIntegerPair(false);
-
-    allocate(0, 0);
-  }
-
   @Override
   public void getFields(
       IRowMeta row,
@@ -226,24 +134,24 @@ public class StreamLookupMeta extends 
BaseTransformMeta<StreamLookup, StreamLook
       IHopMetadataProvider metadataProvider)
       throws HopTransformException {
     if (info != null && info.length == 1 && info[0] != null) {
-      for (int i = 0; i < getValueName().length; i++) {
-        IValueMeta valueMeta = info[0].searchValueMeta(getValue()[i]);
+      for (ReturnValue value : lookup.returnValues) {
+        IValueMeta valueMeta = info[0].searchValueMeta(value.getValue());
         if (valueMeta != null) {
           // Configuration error/missing resources...
-          valueMeta.setName(getValueName()[i]);
+          valueMeta.setName(value.getValueName());
           valueMeta.setOrigin(origin);
           row.addValueMeta(valueMeta);
         } else {
           throw new HopTransformException(
               BaseMessages.getString(
-                  PKG, "StreamLookupMeta.Exception.ReturnValueCanNotBeFound", 
getValue()[i]));
+                  PKG, "StreamLookupMeta.Exception.ReturnValueCanNotBeFound", 
value.getValue()));
         }
       }
     } else {
-      for (int i = 0; i < getValueName().length; i++) {
+      for (ReturnValue value : lookup.returnValues) {
         try {
           IValueMeta v =
-              ValueMetaFactory.createValueMeta(getValueName()[i], 
getValueDefaultType()[i]);
+              ValueMetaFactory.createValueMeta(value.getValueName(), 
value.getValueDefaultType());
           v.setOrigin(origin);
           row.addValueMeta(v);
         } catch (Exception e) {
@@ -253,44 +161,6 @@ public class StreamLookupMeta extends 
BaseTransformMeta<StreamLookup, StreamLook
     }
   }
 
-  @Override
-  public String getXml() {
-    StringBuilder retval = new StringBuilder();
-
-    IStream infoStream = getTransformIOMeta().getInfoStreams().get(0);
-    retval.append("    ").append(XmlHandler.addTagValue("from", 
infoStream.getTransformName()));
-    retval.append("    ").append(XmlHandler.addTagValue("input_sorted", 
isInputSorted()));
-    retval
-        .append("    ")
-        .append(XmlHandler.addTagValue("preserve_memory", 
isMemoryPreservationActive()));
-    retval.append("    ").append(XmlHandler.addTagValue("sorted_list", 
isUsingSortedList()));
-    retval.append("    ").append(XmlHandler.addTagValue("integer_pair", 
isUsingIntegerPair()));
-
-    retval.append("    <lookup>").append(Const.CR);
-    for (int i = 0; i < getKeystream().length; i++) {
-      retval.append("      <key>").append(Const.CR);
-      retval.append(CONST_SPACES).append(XmlHandler.addTagValue("name", 
getKeystream()[i]));
-      retval.append(CONST_SPACES).append(XmlHandler.addTagValue("field", 
getKeylookup()[i]));
-      retval.append("      </key>").append(Const.CR);
-    }
-
-    for (int i = 0; i < getValue().length; i++) {
-      retval.append("      <value>").append(Const.CR);
-      retval.append(CONST_SPACES).append(XmlHandler.addTagValue("name", 
getValue()[i]));
-      retval.append(CONST_SPACES).append(XmlHandler.addTagValue("rename", 
getValueName()[i]));
-      retval.append(CONST_SPACES).append(XmlHandler.addTagValue("default", 
getValueDefault()[i]));
-      retval
-          .append(CONST_SPACES)
-          .append(
-              XmlHandler.addTagValue(
-                  "type", 
ValueMetaFactory.getValueMetaName(getValueDefaultType()[i])));
-      retval.append("      </value>").append(Const.CR);
-    }
-    retval.append("    </lookup>").append(Const.CR);
-
-    return retval.toString();
-  }
-
   @Override
   public void check(
       List<ICheckResult> remarks,
@@ -318,10 +188,10 @@ public class StreamLookupMeta extends 
BaseTransformMeta<StreamLookup, StreamLook
 
       // Starting from selected fields in ...
       // Check the fields from the previous stream!
-      for (String aKeystream : getKeystream()) {
-        int idx = prev.indexOfValue(aKeystream);
+      for (MatchKey key : lookup.matchKeys) {
+        int idx = prev.indexOfValue(key.keyStream);
         if (idx < 0) {
-          errorMessage += "\t\t" + aKeystream + Const.CR;
+          errorMessage += "\t\t" + key.keyStream + Const.CR;
           errorFound = true;
         }
       }
@@ -367,10 +237,10 @@ public class StreamLookupMeta extends 
BaseTransformMeta<StreamLookup, StreamLook
       boolean errorFound = false;
 
       // Check the fields from the lookup stream!
-      for (String aKeylookup : getKeylookup()) {
-        int idx = info.indexOfValue(aKeylookup);
+      for (MatchKey key : lookup.matchKeys) {
+        int idx = info.indexOfValue(key.keyLookup);
         if (idx < 0) {
-          errorMessage += "\t\t" + aKeylookup + Const.CR;
+          errorMessage += "\t\t" + key.keyLookup + Const.CR;
           errorFound = true;
         }
       }
@@ -394,10 +264,10 @@ public class StreamLookupMeta extends 
BaseTransformMeta<StreamLookup, StreamLook
       }
 
       // Check the values to retrieve from the lookup stream!
-      for (String aValue : getValue()) {
-        int idx = info.indexOfValue(aValue);
+      for (ReturnValue value : lookup.returnValues) {
+        int idx = info.indexOfValue(value.getValue());
         if (idx < 0) {
-          errorMessage += "\t\t" + aValue + Const.CR;
+          errorMessage += "\t\t" + value.getValue() + Const.CR;
           errorFound = true;
         }
       }
@@ -431,7 +301,7 @@ public class StreamLookupMeta extends 
BaseTransformMeta<StreamLookup, StreamLook
     }
 
     // See if the source transform is filled in!
-    IStream infoStream = getTransformIOMeta().getInfoStreams().get(0);
+    IStream infoStream = getTransformIOMeta().getInfoStreams().getFirst();
     if (infoStream.getTransformMeta() == null) {
       cr =
           new CheckResult(
@@ -510,14 +380,13 @@ public class StreamLookupMeta extends 
BaseTransformMeta<StreamLookup, StreamLook
     if (ioMeta == null) {
 
       ioMeta = new TransformIOMeta(true, true, false, false, false, false);
-
       IStream stream =
           new Stream(
               StreamType.INFO,
               null,
               BaseMessages.getString(PKG, 
"StreamLookupMeta.InfoStream.Description"),
               StreamIcon.INFO,
-              null);
+              sourceTransformName);
       ioMeta.addStream(stream);
       setTransformIOMeta(ioMeta);
     }
@@ -530,163 +399,99 @@ public class StreamLookupMeta extends 
BaseTransformMeta<StreamLookup, StreamLook
     // Do nothing, don't reset as there is no need to do this.
   }
 
-  /**
-   * @return Returns the inputSorted.
-   */
-  public boolean isInputSorted() {
-    return inputSorted;
-  }
-
-  /**
-   * @param inputSorted The inputSorted to set.
-   */
-  public void setInputSorted(boolean inputSorted) {
-    this.inputSorted = inputSorted;
-  }
-
-  /**
-   * @return Returns the keylookup.
-   */
-  public String[] getKeylookup() {
-    return keylookup;
-  }
-
-  /**
-   * @param keylookup The keylookup to set.
-   */
-  public void setKeylookup(String[] keylookup) {
-    this.keylookup = keylookup;
-  }
-
-  /**
-   * @return Returns the keystream.
-   */
-  public String[] getKeystream() {
-    return keystream;
-  }
-
-  /**
-   * @param keystream The keystream to set.
-   */
-  public void setKeystream(String[] keystream) {
-    this.keystream = keystream;
-  }
-
-  /**
-   * @return Returns the value.
-   */
-  public String[] getValue() {
-    return value;
-  }
-
-  /**
-   * @param value The value to set.
-   */
-  public void setValue(String[] value) {
-    this.value = value;
-  }
-
-  /**
-   * @return Returns the valueDefault.
-   */
-  public String[] getValueDefault() {
-    return valueDefault;
-  }
-
-  /**
-   * @param valueDefault The valueDefault to set.
-   */
-  public void setValueDefault(String[] valueDefault) {
-    this.valueDefault = valueDefault;
-  }
-
-  /**
-   * @return Returns the valueDefaultType.
-   */
-  public int[] getValueDefaultType() {
-    return valueDefaultType;
-  }
-
-  /**
-   * @param valueDefaultType The valueDefaultType to set.
-   */
-  public void setValueDefaultType(int[] valueDefaultType) {
-    this.valueDefaultType = valueDefaultType;
-  }
-
-  /**
-   * @return Returns the valueName.
-   */
-  public String[] getValueName() {
-    return valueName;
-  }
-
-  /**
-   * @param valueName The valueName to set.
-   */
-  public void setValueName(String[] valueName) {
-    this.valueName = valueName;
-  }
-
-  public boolean isMemoryPreservationActive() {
-    return memoryPreservationActive;
-  }
-
-  public void setMemoryPreservationActive(boolean memoryPreservationActive) {
-    this.memoryPreservationActive = memoryPreservationActive;
-  }
-
-  public boolean isUsingSortedList() {
-    return usingSortedList;
-  }
-
-  public void setUsingSortedList(boolean usingSortedList) {
-    this.usingSortedList = usingSortedList;
-  }
-
-  /**
-   * @return the usingIntegerPair
-   */
-  public boolean isUsingIntegerPair() {
-    return usingIntegerPair;
-  }
+  @Getter
+  @Setter
+  public static class Lookup {
+    @HopMetadataProperty(
+        key = "key",
+        injectionGroupKey = "KEYS",
+        injectionGroupDescription = "StreamLookupMeta.Injection.KEYS")
+    private List<MatchKey> matchKeys;
+
+    @HopMetadataProperty(
+        key = "value",
+        injectionGroupKey = "VALUES",
+        injectionGroupDescription = "StreamLookupMeta.Injection.VALUES")
+    private List<ReturnValue> returnValues;
+
+    public Lookup() {
+      matchKeys = new ArrayList<>();
+      returnValues = new ArrayList<>();
+    }
 
-  /**
-   * @param usingIntegerPair the usingIntegerPair to set
-   */
-  public void setUsingIntegerPair(boolean usingIntegerPair) {
-    this.usingIntegerPair = usingIntegerPair;
+    public Lookup(Lookup l) {
+      this();
+      l.matchKeys.forEach(k -> matchKeys.add(new MatchKey(k)));
+      l.returnValues.forEach(v -> returnValues.add(new ReturnValue(v)));
+    }
   }
 
-  /**
-   * If we use injection we can have different arrays lengths. We need 
synchronize them for
-   * consistency behavior with UI
-   */
-  @AfterInjection
-  public void afterInjectionSynchronization() {
-    if (value == null || value.length == 0) {
-      return;
-    }
-    int nrFields = value.length;
-    if (valueDefaultType.length < nrFields) {
-      int[] newValueDefaultType = new int[nrFields];
-      System.arraycopy(valueDefaultType, 0, newValueDefaultType, 0, 
valueDefaultType.length);
-      for (int i = valueDefaultType.length; i < newValueDefaultType.length; 
i++) {
-        newValueDefaultType[i] =
-            -1; // set a undefined value (<0). It will be correct processed in 
a handleNullIf method
-      }
-      valueDefaultType = newValueDefaultType;
-    }
-    if (valueName.length < nrFields) {
-      String[] newValueName = new String[nrFields];
-      System.arraycopy(valueName, 0, newValueName, 0, valueName.length);
-      valueName = newValueName;
+  @Getter
+  @Setter
+  public static class MatchKey {
+    /** fields in input streams with which we look up values */
+    @HopMetadataProperty(
+        key = "name",
+        injectionKey = "KEY_STREAM",
+        injectionKeyDescription = "StreamLookupMeta.Injection.KEY_STREAM")
+    private String keyStream;
+
+    /** fields in lookup stream with which we look up values */
+    @HopMetadataProperty(
+        key = "field",
+        injectionKey = "KEY_LOOKUP",
+        injectionKeyDescription = "StreamLookupMeta.Injection.KEY_LOOKUP")
+    private String keyLookup;
+
+    public MatchKey() {}
+
+    public MatchKey(MatchKey k) {
+      this();
+      this.keyStream = k.keyStream;
+      this.keyLookup = k.keyLookup;
     }
+  }
 
-    if (valueDefault.length < nrFields) {
-      String[] newValueDefault = new String[nrFields];
-      System.arraycopy(valueDefault, 0, newValueDefault, 0, 
valueDefault.length);
-      valueDefault = newValueDefault;
+  @Getter
+  @Setter
+  public static class ReturnValue {
+    /** return these field values from lookup */
+    @HopMetadataProperty(
+        key = "name",
+        injectionKey = "RETRIEVE_VALUE",
+        injectionKeyDescription = "StreamLookupMeta.Injection.RETRIEVE_VALUE")
+    private String value;
+
+    /** rename to this after lookup */
+    @HopMetadataProperty(
+        key = "rename",
+        injectionKey = "RETRIEVE_VALUE_NAME",
+        injectionKeyDescription = 
"StreamLookupMeta.Injection.RETRIEVE_VALUE_NAME")
+    private String valueName;
+
+    /** default value in case not found... */
+    @HopMetadataProperty(
+        key = "default",
+        injectionKey = "RETRIEVE_VALUE_DEFAULT",
+        injectionKeyDescription = 
"StreamLookupMeta.Injection.RETRIEVE_VALUE_DEFAULT")
+    private String valueDefault;
+
+    /** type of default value */
+    @HopMetadataProperty(
+        key = "type",
+        intCodeConverter = ValueMetaBase.ValueTypeCodeConverter.class,
+        injectionKey = "RETRIEVE_DEFAULT_TYPE",
+        injectionKeyDescription = 
"StreamLookupMeta.Injection.RETRIEVE_DEFAULT_TYPE")
+    private int valueDefaultType;
+
+    public ReturnValue() {}
+
+    public ReturnValue(ReturnValue v) {
+      this();
+      this.value = v.value;
+      this.valueName = v.valueName;
+      this.valueDefault = v.valueDefault;
+      this.valueDefaultType = v.valueDefaultType;
     }
   }
 }
diff --git 
a/plugins/transforms/streamlookup/src/main/resources/org/apache/hop/pipeline/transforms/streamlookup/messages/messages_en_US.properties
 
b/plugins/transforms/streamlookup/src/main/resources/org/apache/hop/pipeline/transforms/streamlookup/messages/messages_en_US.properties
index 5493e858f7..212a5cefae 100644
--- 
a/plugins/transforms/streamlookup/src/main/resources/org/apache/hop/pipeline/transforms/streamlookup/messages/messages_en_US.properties
+++ 
b/plugins/transforms/streamlookup/src/main/resources/org/apache/hop/pipeline/transforms/streamlookup/messages/messages_en_US.properties
@@ -88,3 +88,7 @@ StreamLookupMeta.Injection.RETRIEVE_VALUE_DEFAULT=The default 
value if no value
 StreamLookupMeta.Injection.RETRIEVE_VALUE_NAME=After a lookup, the list of 
values retrieved will be used instead of the existing values.
 StreamLookupMeta.Injection.SORTED_LIST=Indicates that the input is sorted.
 StreamLookupMeta.keyword=stream,lookup
+StreamLookupMeta.Injection.SOURCE_TRANSFORM=The name of the source transform 
to read from.
+StreamLookupMeta.Injection.INPUT_SORTED=Is the input considered sorted?
+StreamLookupMeta.Injection.KEYS=Match keys
+StreamLookupMeta.Injection.VALUES=Return values
diff --git 
a/plugins/transforms/streamlookup/src/test/java/org/apache/hop/pipeline/transforms/streamlookup/StreamLookupMetaTest.java
 
b/plugins/transforms/streamlookup/src/test/java/org/apache/hop/pipeline/transforms/streamlookup/StreamLookupMetaTest.java
index 05e0b4bfde..1a4741c8c4 100644
--- 
a/plugins/transforms/streamlookup/src/test/java/org/apache/hop/pipeline/transforms/streamlookup/StreamLookupMetaTest.java
+++ 
b/plugins/transforms/streamlookup/src/test/java/org/apache/hop/pipeline/transforms/streamlookup/StreamLookupMetaTest.java
@@ -17,129 +17,57 @@
 package org.apache.hop.pipeline.transforms.streamlookup;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNotSame;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.UUID;
-import org.apache.hop.core.HopEnvironment;
-import org.apache.hop.core.exception.HopException;
-import org.apache.hop.core.plugins.PluginRegistry;
-import org.apache.hop.junit.rules.RestoreHopEngineEnvironmentExtension;
-import org.apache.hop.pipeline.transform.ITransformMeta;
-import org.apache.hop.pipeline.transform.TransformMeta;
-import org.apache.hop.pipeline.transforms.loadsave.LoadSaveTester;
-import org.apache.hop.pipeline.transforms.loadsave.initializer.IInitializer;
-import 
org.apache.hop.pipeline.transforms.loadsave.validator.ArrayLoadSaveValidator;
-import 
org.apache.hop.pipeline.transforms.loadsave.validator.IFieldLoadSaveValidator;
-import 
org.apache.hop.pipeline.transforms.loadsave.validator.IntLoadSaveValidator;
-import 
org.apache.hop.pipeline.transforms.loadsave.validator.PrimitiveIntArrayLoadSaveValidator;
-import 
org.apache.hop.pipeline.transforms.loadsave.validator.StringLoadSaveValidator;
+import org.apache.commons.lang.StringUtils;
+import org.apache.hop.core.row.IValueMeta;
+import org.apache.hop.core.row.value.ValueMetaPluginType;
+import org.apache.hop.pipeline.transform.TransformSerializationTestUtil;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.RegisterExtension;
-
-class StreamLookupMetaTest implements IInitializer<ITransformMeta> {
-  LoadSaveTester loadSaveTester;
-  Class<StreamLookupMeta> testMetaClass = StreamLookupMeta.class;
-
-  @RegisterExtension
-  static RestoreHopEngineEnvironmentExtension env = new 
RestoreHopEngineEnvironmentExtension();
 
+class StreamLookupMetaTest {
   @BeforeEach
-  void setUpLoadSave() throws Exception {
-    HopEnvironment.init();
-    PluginRegistry.init();
-    List<String> attributes =
-        Arrays.asList(
-            "inputSorted",
-            "memoryPreservationActive",
-            "usingSortedList",
-            "usingIntegerPair",
-            "keystream",
-            "keylookup",
-            "value",
-            "valueName",
-            "valueDefault",
-            "valueDefaultType");
-
-    IFieldLoadSaveValidator<String[]> stringArrayLoadSaveValidator =
-        new ArrayLoadSaveValidator<>(new StringLoadSaveValidator(), 5);
-
-    Map<String, IFieldLoadSaveValidator<?>> attrValidatorMap = new HashMap<>();
-    attrValidatorMap.put("keystream", stringArrayLoadSaveValidator);
-    attrValidatorMap.put("keylookup", stringArrayLoadSaveValidator);
-    attrValidatorMap.put("value", stringArrayLoadSaveValidator);
-    attrValidatorMap.put("valueName", stringArrayLoadSaveValidator);
-    attrValidatorMap.put("valueDefault", stringArrayLoadSaveValidator);
-    attrValidatorMap.put(
-        "valueDefaultType", new PrimitiveIntArrayLoadSaveValidator(new 
IntLoadSaveValidator(7), 5));
-
-    Map<String, IFieldLoadSaveValidator<?>> typeValidatorMap = new HashMap<>();
-
-    loadSaveTester =
-        new LoadSaveTester(
-            testMetaClass,
-            attributes,
-            new HashMap<>(),
-            new HashMap<>(),
-            attrValidatorMap,
-            typeValidatorMap,
-            this);
-  }
-
-  // Call the allocate method on the LoadSaveTester meta class
-  @Override
-  public void modify(ITransformMeta someMeta) {
-    if (someMeta instanceof StreamLookupMeta) {
-      ((StreamLookupMeta) someMeta).allocate(5, 5);
-    }
-  }
-
-  @Test
-  void testSerialization() throws HopException {
-    loadSaveTester.testSerialization();
+  void beforeEach() throws Exception {
+    ValueMetaPluginType.getInstance().searchPlugins();
   }
 
   @Test
-  void testCloneInfoTransforms() {
-    StreamLookupMeta meta = new StreamLookupMeta();
-    meta.setDefault();
-
-    final String transformName = UUID.randomUUID().toString();
-    TransformMeta infoTransform = mock(TransformMeta.class);
-    when(infoTransform.getName()).thenReturn(transformName);
-    
meta.getTransformIOMeta().getInfoStreams().get(0).setTransformMeta(infoTransform);
-
-    StreamLookupMeta cloned = (StreamLookupMeta) meta.clone();
-    assertEquals(
-        transformName, 
cloned.getTransformIOMeta().getInfoStreams().get(0).getTransformName());
-    assertNotSame(
-        meta.getTransformIOMeta().getInfoStreams().get(0),
-        cloned.getTransformIOMeta().getInfoStreams().get(0));
-  }
-
-  @Test
-  void testGetXml() {
-    StreamLookupMeta streamLookupMeta = new StreamLookupMeta();
-    streamLookupMeta.setKeystream(new String[] {"testKeyStreamValue"});
-    streamLookupMeta.setKeylookup(new String[] {"testKeyLookupValue"});
-    streamLookupMeta.setValue(new String[] {"testValue"});
-    streamLookupMeta.setValueName(new String[] {});
-    streamLookupMeta.setValueDefault(new String[] {});
-    streamLookupMeta.setValueDefaultType(new int[] {});
-
-    // run without exception
-    streamLookupMeta.afterInjectionSynchronization();
-    streamLookupMeta.getXml();
-
-    assertEquals(streamLookupMeta.getKeystream().length, 
streamLookupMeta.getValueName().length);
-    assertEquals(streamLookupMeta.getKeystream().length, 
streamLookupMeta.getValueDefault().length);
-    assertEquals(
-        streamLookupMeta.getKeystream().length, 
streamLookupMeta.getValueDefaultType().length);
+  void testSerializationRoundTrip() throws Exception {
+    StreamLookupMeta meta =
+        TransformSerializationTestUtil.testSerialization(
+            "/stream-lookup.xml", StreamLookupMeta.class);
+
+    assertTrue(meta.isMemoryPreservationActive());
+    assertTrue(meta.isUsingIntegerPair());
+    assertTrue(meta.isUsingSortedList());
+
+    assertEquals(2, meta.getLookup().getMatchKeys().size());
+    StreamLookupMeta.MatchKey k = meta.getLookup().getMatchKeys().getFirst();
+    assertEquals("keyStream1", k.getKeyStream());
+    assertEquals("keyLookup1", k.getKeyLookup());
+
+    k = meta.getLookup().getMatchKeys().getLast();
+    assertEquals("keyStream2", k.getKeyStream());
+    assertEquals("keyLookup2", k.getKeyLookup());
+
+    assertEquals(3, meta.getLookup().getReturnValues().size());
+    StreamLookupMeta.ReturnValue v = 
meta.getLookup().getReturnValues().getFirst();
+    assertEquals("returnValue1", v.getValue());
+    assertEquals("value1", v.getValueName());
+    assertTrue(StringUtils.isEmpty(v.getValueDefault()));
+    assertEquals(IValueMeta.TYPE_STRING, v.getValueDefaultType());
+
+    v = meta.getLookup().getReturnValues().get(1);
+    assertEquals("returnValue2", v.getValue());
+    assertEquals("value2", v.getValueName());
+    assertEquals("123", v.getValueDefault());
+    assertEquals(IValueMeta.TYPE_INTEGER, v.getValueDefaultType());
+
+    v = meta.getLookup().getReturnValues().getLast();
+    assertEquals("returnValue3", v.getValue());
+    assertEquals("value3", v.getValueName());
+    assertEquals("true", v.getValueDefault());
+    assertEquals(IValueMeta.TYPE_BOOLEAN, v.getValueDefaultType());
   }
 }
diff --git 
a/plugins/transforms/streamlookup/src/test/java/org/apache/hop/pipeline/transforms/streamlookup/StreamLookupTest.java
 
b/plugins/transforms/streamlookup/src/test/java/org/apache/hop/pipeline/transforms/streamlookup/StreamLookupTest.java
deleted file mode 100644
index 8878a0df44..0000000000
--- 
a/plugins/transforms/streamlookup/src/test/java/org/apache/hop/pipeline/transforms/streamlookup/StreamLookupTest.java
+++ /dev/null
@@ -1,259 +0,0 @@
-/*
- * 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.streamlookup;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.doCallRealMethod;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.nullable;
-import static org.mockito.Mockito.when;
-
-import org.apache.hop.core.IRowSet;
-import org.apache.hop.core.QueueRowSet;
-import org.apache.hop.core.exception.HopException;
-import org.apache.hop.core.exception.HopTransformException;
-import org.apache.hop.core.logging.ILoggingObject;
-import org.apache.hop.core.row.IRowMeta;
-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.IVariables;
-import org.apache.hop.metadata.api.IHopMetadataProvider;
-import org.apache.hop.pipeline.transform.TransformIOMeta;
-import org.apache.hop.pipeline.transform.TransformMeta;
-import org.apache.hop.pipeline.transform.stream.IStream;
-import org.apache.hop.pipeline.transform.stream.Stream;
-import org.apache.hop.pipeline.transform.stream.StreamIcon;
-import org.apache.hop.pipeline.transforms.mock.TransformMockHelper;
-import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Disabled;
-import org.junit.jupiter.api.Test;
-
-/**
- * Test for StreamLookup transform
- *
- * @see StreamLookup
- */
-class StreamLookupTest {
-  private TransformMockHelper<StreamLookupMeta, StreamLookupData> smh;
-
-  @BeforeEach
-  void setUp() {
-    smh = new TransformMockHelper<>("StreamLookup", StreamLookupMeta.class, 
StreamLookupData.class);
-    when(smh.logChannelFactory.create(any(), nullable(ILoggingObject.class)))
-        .thenReturn(smh.iLogChannel);
-    when(smh.pipeline.isRunning()).thenReturn(true);
-  }
-
-  @AfterEach
-  void cleanUp() {
-    smh.cleanUp();
-  }
-
-  private void convertDataToBinary(Object[][] data) {
-    for (int i = 0; i < data.length; i++) {
-      for (int j = 0; j < data[i].length; j++) {
-        data[i][j] = ((String) data[i][j]).getBytes();
-      }
-    }
-  }
-
-  private IRowSet mockLookupRowSet(boolean binary) {
-    final int storageType =
-        binary ? IValueMeta.STORAGE_TYPE_BINARY_STRING : 
IValueMeta.STORAGE_TYPE_NORMAL;
-    Object[][] data = {{"Value1", "1"}, {"Value2", "2"}};
-
-    if (binary) {
-      convertDataToBinary(data);
-    }
-
-    IRowSet lookupRowSet = smh.getMockInputRowSet(data);
-    doReturn("Lookup").when(lookupRowSet).getOriginTransformName();
-    doReturn("StreamLookup").when(lookupRowSet).getDestinationTransformName();
-
-    RowMeta lookupRowMeta = new RowMeta();
-    ValueMetaString valueMeta = new ValueMetaString("Value");
-    valueMeta.setStorageType(storageType);
-    valueMeta.setStorageMetadata(new ValueMetaString());
-    lookupRowMeta.addValueMeta(valueMeta);
-    ValueMetaString idMeta = new ValueMetaString("Id");
-    idMeta.setStorageType(storageType);
-    idMeta.setStorageMetadata(new ValueMetaString());
-    lookupRowMeta.addValueMeta(idMeta);
-
-    doReturn(lookupRowMeta).when(lookupRowSet).getRowMeta();
-
-    return lookupRowSet;
-  }
-
-  private IRowSet mockDataRowSet(boolean binary) {
-    final int storageType =
-        binary ? IValueMeta.STORAGE_TYPE_BINARY_STRING : 
IValueMeta.STORAGE_TYPE_NORMAL;
-    Object[][] data = {{"Name1", "1"}, {"Name2", "2"}};
-
-    if (binary) {
-      convertDataToBinary(data);
-    }
-
-    IRowSet dataRowSet = smh.getMockInputRowSet(data);
-
-    RowMeta dataRowMeta = new RowMeta();
-    ValueMetaString valueMeta = new ValueMetaString("Name");
-    valueMeta.setStorageType(storageType);
-    valueMeta.setStorageMetadata(new ValueMetaString());
-    dataRowMeta.addValueMeta(valueMeta);
-    ValueMetaString idMeta = new ValueMetaString("Id");
-    idMeta.setStorageType(storageType);
-    idMeta.setStorageMetadata(new ValueMetaString());
-    dataRowMeta.addValueMeta(idMeta);
-
-    doReturn(dataRowMeta).when(dataRowSet).getRowMeta();
-
-    return dataRowSet;
-  }
-
-  private StreamLookupMeta mockProcessRowMeta(boolean memoryPreservationActive)
-      throws HopTransformException {
-    StreamLookupMeta meta = smh.iTransformMeta;
-
-    TransformMeta lookupTransformMeta =
-        
when(mock(TransformMeta.class).getName()).thenReturn("Lookup").getMock();
-    
doReturn(lookupTransformMeta).when(smh.pipelineMeta).findTransform("Lookup");
-
-    TransformIOMeta transformIOMeta = new TransformIOMeta(true, true, false, 
false, false, false);
-    transformIOMeta.addStream(
-        new Stream(IStream.StreamType.INFO, lookupTransformMeta, null, 
StreamIcon.INFO, null));
-
-    doReturn(transformIOMeta).when(meta).getTransformIOMeta();
-    doReturn(new String[] {"Id"}).when(meta).getKeylookup();
-    doReturn(new String[] {"Id"}).when(meta).getKeystream();
-    doReturn(new String[] {"Value"}).when(meta).getValue();
-    doReturn(memoryPreservationActive).when(meta).isMemoryPreservationActive();
-    doReturn(false).when(meta).isUsingSortedList();
-    doReturn(false).when(meta).isUsingIntegerPair();
-    doReturn(new int[] {-1}).when(meta).getValueDefaultType();
-    doReturn(new String[] {""}).when(meta).getValueDefault();
-    doReturn(new String[] {"Value"}).when(meta).getValueName();
-    doReturn(new String[] {"Value"}).when(meta).getValue();
-    doCallRealMethod()
-        .when(meta)
-        .getFields(
-            nullable(IRowMeta.class),
-            nullable(String.class),
-            nullable(IRowMeta[].class),
-            nullable(TransformMeta.class),
-            nullable(IVariables.class),
-            nullable(IHopMetadataProvider.class));
-
-    return meta;
-  }
-
-  private void doTest(
-      boolean memoryPreservationActive, boolean binaryLookupStream, boolean 
binaryDataStream)
-      throws HopException {
-    StreamLookup transform =
-        new StreamLookup(
-            smh.transformMeta,
-            smh.iTransformMeta,
-            smh.iTransformData,
-            0,
-            smh.pipelineMeta,
-            smh.pipeline);
-    transform.init();
-    transform.addRowSetToInputRowSets(mockLookupRowSet(binaryLookupStream));
-    transform.addRowSetToInputRowSets(mockDataRowSet(binaryDataStream));
-    transform.addRowSetToOutputRowSets(new QueueRowSet());
-
-    StreamLookupMeta meta = mockProcessRowMeta(memoryPreservationActive);
-    StreamLookupData data = new StreamLookupData();
-    data.readLookupValues = true;
-
-    IRowSet outputRowSet = transform.getOutputRowSets().get(0);
-
-    // Process rows and collect output
-    int rowNumber = 0;
-    String[] expectedOutput = {"Name", "", "Value"};
-    while (transform.processRow()) {
-      Object[] rowData = outputRowSet.getRow();
-      if (rowData != null) {
-        IRowMeta rowMeta = outputRowSet.getRowMeta();
-        assertEquals(3, rowMeta.size(), "Output row is of wrong size");
-        rowNumber++;
-        // Verify output
-        for (int valueIndex = 0; valueIndex < rowMeta.size(); valueIndex++) {
-          String expectedValue = expectedOutput[valueIndex] + rowNumber;
-          Object actualValue =
-              
rowMeta.getValueMeta(valueIndex).convertToNormalStorageType(rowData[valueIndex]);
-          assertEquals(
-              expectedValue,
-              actualValue,
-              "Unexpected value at row " + rowNumber + " position " + 
valueIndex);
-        }
-      }
-    }
-
-    assertEquals(2, rowNumber, "Incorrect output row number");
-  }
-
-  @Test
-  @Disabled("This test needs to be reviewed")
-  void testWithNormalStreams() throws HopException {
-    doTest(false, false, false);
-  }
-
-  @Test
-  @Disabled("This test needs to be reviewed")
-  void testWithBinaryLookupStream() throws HopException {
-    doTest(false, true, false);
-  }
-
-  @Test
-  @Disabled("This test needs to be reviewed")
-  void testWithBinaryDateStream() throws HopException {
-    doTest(false, false, true);
-  }
-
-  @Test
-  @Disabled("This test needs to be reviewed")
-  void testWithBinaryStreams() throws HopException {
-    doTest(false, false, true);
-  }
-
-  @Test
-  void testMemoryPreservationWithNormalStreams() throws HopException {
-    doTest(true, false, false);
-  }
-
-  @Test
-  void testMemoryPreservationWithBinaryLookupStream() throws HopException {
-    doTest(true, true, false);
-  }
-
-  @Test
-  void testMemoryPreservationWithBinaryDateStream() throws HopException {
-    doTest(true, false, true);
-  }
-
-  @Test
-  void testMemoryPreservationWithBinaryStreams() throws HopException {
-    doTest(true, false, true);
-  }
-}
diff --git 
a/plugins/transforms/streamlookup/src/test/resources/stream-lookup.xml 
b/plugins/transforms/streamlookup/src/test/resources/stream-lookup.xml
new file mode 100644
index 0000000000..ee2640a3c0
--- /dev/null
+++ b/plugins/transforms/streamlookup/src/test/resources/stream-lookup.xml
@@ -0,0 +1,52 @@
+<?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.
+  -->
+<transform>
+    <from>Countries</from>
+    <input_sorted>Y</input_sorted>
+    <preserve_memory>Y</preserve_memory>
+    <sorted_list>Y</sorted_list>
+    <integer_pair>Y</integer_pair>
+    <lookup>
+        <key>
+            <name>keyStream1</name>
+            <field>keyLookup1</field>
+        </key>
+        <key>
+            <name>keyStream2</name>
+            <field>keyLookup2</field>
+        </key>
+        <value>
+            <name>returnValue1</name>
+            <rename>value1</rename>
+            <default/>
+            <type>String</type>
+        </value>
+        <value>
+            <name>returnValue2</name>
+            <rename>value2</rename>
+            <default>123</default>
+            <type>Integer</type>
+        </value>
+        <value>
+            <name>returnValue3</name>
+            <rename>value3</rename>
+            <default>true</default>
+            <type>Boolean</type>
+        </value>
+    </lookup>
+</transform>

Reply via email to