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

hansva pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git


The following commit(s) were added to refs/heads/main by this push:
     new c4753932f0 Issue #1938 (code cleanup, extra integration tests) (#6756)
c4753932f0 is described below

commit c4753932f02aa96dec5b35b06a886464cfce63e9
Author: Matt Casters <[email protected]>
AuthorDate: Thu Mar 12 08:19:50 2026 +0100

    Issue #1938 (code cleanup, extra integration tests) (#6756)
---
 .../transforms/0086-string-operations.hpl          |  534 +++++++++
 .../datasets/golden-string-operations.csv          |    2 +
 .../datasets/golden-string-operations2.csv         |    2 +
 .../transforms/main-0086-string-operations.hwf     |   80 ++
 .../metadata/dataset/golden-string-operations.json |   64 ++
 .../dataset/golden-string-operations2.json         |   64 ++
 .../unit-test/0086-string-operations UNIT.json     |  101 ++
 .../stringoperations/StringOperations.java         |  382 +++----
 .../stringoperations/StringOperationsData.java     |   28 -
 .../stringoperations/StringOperationsDialog.java   |  229 ++--
 .../stringoperations/StringOperationsMeta.java     | 1157 +++++++-------------
 .../stringoperations/StringOperationsMetaTest.java |  247 +++--
 .../stringoperations/StringOperationsTest.java     |  149 ---
 .../src/test/resources/transform.xml               |   74 ++
 .../src/test/resources/transform2.xml              |   35 +
 15 files changed, 1697 insertions(+), 1451 deletions(-)

diff --git a/integration-tests/transforms/0086-string-operations.hpl 
b/integration-tests/transforms/0086-string-operations.hpl
new file mode 100644
index 0000000000..bd0873f114
--- /dev/null
+++ b/integration-tests/transforms/0086-string-operations.hpl
@@ -0,0 +1,534 @@
+<?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>0086-string-operations</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/10 18:03:52.637</created_date>
+    <modified_user>-</modified_user>
+    <modified_date>2026/03/10 18:03:52.637</modified_date>
+  </info>
+  <notepads>
+  </notepads>
+  <order>
+    <hop>
+      <from>Data grid</from>
+      <to>String operations</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>String operations</from>
+      <to>Validate</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>Data grid 2</from>
+      <to>String operations 2</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>String operations 2</from>
+      <to>Validate 2</to>
+      <enabled>Y</enabled>
+    </hop>
+  </order>
+  <transform>
+    <name>Data grid</name>
+    <type>DataGrid</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <data>
+      <line>
+        <item>  Apache Hop  </item>
+        <item>APACHE HOP</item>
+        <item>Apache Hop</item>
+        <item>apache hop</item>
+        <item>&lt;name>Apache Hop&lt;/name></item>
+        <item>123Apache Hop456</item>
+        <item>Apache 
+Hop</item>
+      </line>
+    </data>
+    <fields>
+      <field>
+        <length>-1</length>
+        <precision>-1</precision>
+        <currency/>
+        <set_empty_string>N</set_empty_string>
+        <name>s1</name>
+        <format/>
+        <group/>
+        <decimal/>
+        <type>String</type>
+      </field>
+      <field>
+        <length>-1</length>
+        <precision>-1</precision>
+        <currency/>
+        <set_empty_string>N</set_empty_string>
+        <name>s2</name>
+        <format/>
+        <group/>
+        <decimal/>
+        <type>String</type>
+      </field>
+      <field>
+        <length>-1</length>
+        <precision>-1</precision>
+        <currency/>
+        <set_empty_string>N</set_empty_string>
+        <name>s3</name>
+        <format/>
+        <group/>
+        <decimal/>
+        <type>String</type>
+      </field>
+      <field>
+        <length>-1</length>
+        <precision>-1</precision>
+        <currency/>
+        <set_empty_string>N</set_empty_string>
+        <name>s4</name>
+        <format/>
+        <group/>
+        <decimal/>
+        <type>String</type>
+      </field>
+      <field>
+        <length>-1</length>
+        <precision>-1</precision>
+        <currency/>
+        <set_empty_string>N</set_empty_string>
+        <name>s5</name>
+        <format/>
+        <group/>
+        <decimal/>
+        <type>String</type>
+      </field>
+      <field>
+        <length>-1</length>
+        <precision>-1</precision>
+        <currency/>
+        <set_empty_string>N</set_empty_string>
+        <name>s6</name>
+        <format/>
+        <group/>
+        <decimal/>
+        <type>String</type>
+      </field>
+      <field>
+        <length>-1</length>
+        <precision>-1</precision>
+        <currency/>
+        <set_empty_string>N</set_empty_string>
+        <name>s7</name>
+        <format/>
+        <group/>
+        <decimal/>
+        <type>String</type>
+      </field>
+    </fields>
+    <attributes/>
+    <GUI>
+      <xloc>96</xloc>
+      <yloc>64</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>String operations</name>
+    <type>StringOperations</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <fields>
+      <field>
+        <in_stream_name>s1</in_stream_name>
+        <out_stream_name/>
+        <trim_type>both</trim_type>
+        <lower_upper>none</lower_upper>
+        <padding_type>none</padding_type>
+        <pad_char/>
+        <pad_len/>
+        <init_cap>no</init_cap>
+        <mask_xml>none</mask_xml>
+        <digits>none</digits>
+        <remove_special_characters>none</remove_special_characters>
+      </field>
+      <field>
+        <in_stream_name>s2</in_stream_name>
+        <out_stream_name/>
+        <trim_type>none</trim_type>
+        <lower_upper>lower</lower_upper>
+        <padding_type>none</padding_type>
+        <pad_char/>
+        <pad_len/>
+        <init_cap>no</init_cap>
+        <mask_xml>none</mask_xml>
+        <digits>none</digits>
+        <remove_special_characters>none</remove_special_characters>
+      </field>
+      <field>
+        <in_stream_name>s3</in_stream_name>
+        <out_stream_name/>
+        <trim_type>none</trim_type>
+        <lower_upper>none</lower_upper>
+        <padding_type>right</padding_type>
+        <pad_char>20</pad_char>
+        <pad_len>-</pad_len>
+        <init_cap>no</init_cap>
+        <mask_xml>none</mask_xml>
+        <digits>none</digits>
+        <remove_special_characters>none</remove_special_characters>
+      </field>
+      <field>
+        <in_stream_name>s4</in_stream_name>
+        <out_stream_name/>
+        <trim_type>none</trim_type>
+        <lower_upper>none</lower_upper>
+        <padding_type>none</padding_type>
+        <pad_char/>
+        <pad_len/>
+        <init_cap>yes</init_cap>
+        <mask_xml>none</mask_xml>
+        <digits>none</digits>
+        <remove_special_characters>none</remove_special_characters>
+      </field>
+      <field>
+        <in_stream_name>s5</in_stream_name>
+        <out_stream_name/>
+        <trim_type>none</trim_type>
+        <lower_upper>none</lower_upper>
+        <padding_type>none</padding_type>
+        <pad_char/>
+        <pad_len/>
+        <init_cap>no</init_cap>
+        <mask_xml>escapexml</mask_xml>
+        <digits>none</digits>
+        <remove_special_characters>none</remove_special_characters>
+      </field>
+      <field>
+        <in_stream_name>s6</in_stream_name>
+        <out_stream_name/>
+        <trim_type>none</trim_type>
+        <lower_upper>none</lower_upper>
+        <padding_type>none</padding_type>
+        <pad_char/>
+        <pad_len/>
+        <init_cap>no</init_cap>
+        <mask_xml>none</mask_xml>
+        <digits>remove_digits</digits>
+        <remove_special_characters>none</remove_special_characters>
+      </field>
+      <field>
+        <in_stream_name>s7</in_stream_name>
+        <out_stream_name/>
+        <trim_type>none</trim_type>
+        <lower_upper>none</lower_upper>
+        <padding_type>none</padding_type>
+        <pad_char/>
+        <pad_len/>
+        <init_cap>no</init_cap>
+        <mask_xml>none</mask_xml>
+        <digits>none</digits>
+        <remove_special_characters>lf</remove_special_characters>
+      </field>
+    </fields>
+    <attributes/>
+    <GUI>
+      <xloc>224</xloc>
+      <yloc>64</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>368</xloc>
+      <yloc>64</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>Data grid 2</name>
+    <type>DataGrid</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <data>
+      <line>
+        <item>  Apache Hop  </item>
+        <item>apache hop</item>
+        <item>Apache Hop</item>
+        <item>apache hop</item>
+        <item>&lt;name>Apache Hop&lt;/name></item>
+        <item>123Apache Hop456</item>
+        <item>Apache-       Hop</item>
+      </line>
+    </data>
+    <fields>
+      <field>
+        <length>-1</length>
+        <precision>-1</precision>
+        <currency/>
+        <set_empty_string>N</set_empty_string>
+        <name>s1</name>
+        <format/>
+        <group/>
+        <decimal/>
+        <type>String</type>
+      </field>
+      <field>
+        <length>-1</length>
+        <precision>-1</precision>
+        <currency/>
+        <set_empty_string>N</set_empty_string>
+        <name>s2</name>
+        <format/>
+        <group/>
+        <decimal/>
+        <type>String</type>
+      </field>
+      <field>
+        <length>-1</length>
+        <precision>-1</precision>
+        <currency/>
+        <set_empty_string>N</set_empty_string>
+        <name>s3</name>
+        <format/>
+        <group/>
+        <decimal/>
+        <type>String</type>
+      </field>
+      <field>
+        <length>-1</length>
+        <precision>-1</precision>
+        <currency/>
+        <set_empty_string>N</set_empty_string>
+        <name>s4</name>
+        <format/>
+        <group/>
+        <decimal/>
+        <type>String</type>
+      </field>
+      <field>
+        <length>-1</length>
+        <precision>-1</precision>
+        <currency/>
+        <set_empty_string>N</set_empty_string>
+        <name>s5</name>
+        <format/>
+        <group/>
+        <decimal/>
+        <type>String</type>
+      </field>
+      <field>
+        <length>-1</length>
+        <precision>-1</precision>
+        <currency/>
+        <set_empty_string>N</set_empty_string>
+        <name>s6</name>
+        <format/>
+        <group/>
+        <decimal/>
+        <type>String</type>
+      </field>
+      <field>
+        <length>-1</length>
+        <precision>-1</precision>
+        <currency/>
+        <set_empty_string>N</set_empty_string>
+        <name>s7</name>
+        <format/>
+        <group/>
+        <decimal/>
+        <type>String</type>
+      </field>
+    </fields>
+    <attributes/>
+    <GUI>
+      <xloc>96</xloc>
+      <yloc>160</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>String operations 2</name>
+    <type>StringOperations</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <fields>
+      <field>
+        <in_stream_name>s1</in_stream_name>
+        <out_stream_name/>
+        <trim_type>both</trim_type>
+        <lower_upper>none</lower_upper>
+        <padding_type>none</padding_type>
+        <pad_char/>
+        <pad_len/>
+        <init_cap>no</init_cap>
+        <mask_xml>none</mask_xml>
+        <digits>none</digits>
+        <remove_special_characters>none</remove_special_characters>
+      </field>
+      <field>
+        <in_stream_name>s2</in_stream_name>
+        <out_stream_name/>
+        <trim_type>none</trim_type>
+        <lower_upper>upper</lower_upper>
+        <padding_type>none</padding_type>
+        <pad_char/>
+        <pad_len/>
+        <init_cap>no</init_cap>
+        <mask_xml>none</mask_xml>
+        <digits>none</digits>
+        <remove_special_characters>none</remove_special_characters>
+      </field>
+      <field>
+        <in_stream_name>s3</in_stream_name>
+        <out_stream_name/>
+        <trim_type>none</trim_type>
+        <lower_upper>none</lower_upper>
+        <padding_type>left</padding_type>
+        <pad_char>20</pad_char>
+        <pad_len>-</pad_len>
+        <init_cap>no</init_cap>
+        <mask_xml>none</mask_xml>
+        <digits>none</digits>
+        <remove_special_characters>none</remove_special_characters>
+      </field>
+      <field>
+        <in_stream_name>s4</in_stream_name>
+        <out_stream_name/>
+        <trim_type>none</trim_type>
+        <lower_upper>none</lower_upper>
+        <padding_type>none</padding_type>
+        <pad_char/>
+        <pad_len/>
+        <init_cap>yes</init_cap>
+        <mask_xml>none</mask_xml>
+        <digits>none</digits>
+        <remove_special_characters>none</remove_special_characters>
+      </field>
+      <field>
+        <in_stream_name>s5</in_stream_name>
+        <out_stream_name/>
+        <trim_type>none</trim_type>
+        <lower_upper>none</lower_upper>
+        <padding_type>none</padding_type>
+        <pad_char/>
+        <pad_len/>
+        <init_cap>no</init_cap>
+        <mask_xml>cdata</mask_xml>
+        <digits>none</digits>
+        <remove_special_characters>none</remove_special_characters>
+      </field>
+      <field>
+        <in_stream_name>s6</in_stream_name>
+        <out_stream_name/>
+        <trim_type>none</trim_type>
+        <lower_upper>none</lower_upper>
+        <padding_type>none</padding_type>
+        <pad_char/>
+        <pad_len/>
+        <init_cap>no</init_cap>
+        <mask_xml>none</mask_xml>
+        <digits>digits_only</digits>
+        <remove_special_characters>none</remove_special_characters>
+      </field>
+      <field>
+        <in_stream_name>s7</in_stream_name>
+        <out_stream_name/>
+        <trim_type>none</trim_type>
+        <lower_upper>none</lower_upper>
+        <padding_type>none</padding_type>
+        <pad_char/>
+        <pad_len/>
+        <init_cap>no</init_cap>
+        <mask_xml>none</mask_xml>
+        <digits>none</digits>
+        <remove_special_characters>espace</remove_special_characters>
+      </field>
+    </fields>
+    <attributes/>
+    <GUI>
+      <xloc>224</xloc>
+      <yloc>160</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>Validate 2</name>
+    <type>Dummy</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <attributes/>
+    <GUI>
+      <xloc>368</xloc>
+      <yloc>160</yloc>
+    </GUI>
+  </transform>
+  <transform_error_handling>
+  </transform_error_handling>
+  <attributes/>
+</pipeline>
diff --git a/integration-tests/transforms/datasets/golden-string-operations.csv 
b/integration-tests/transforms/datasets/golden-string-operations.csv
new file mode 100644
index 0000000000..1c4d9dc3ad
--- /dev/null
+++ b/integration-tests/transforms/datasets/golden-string-operations.csv
@@ -0,0 +1,2 @@
+s1,s2,s3,s4,s5,s6,s7
+Apache Hop,apache hop,Apache Hop,Apache Hop,&lt;name&gt;Apache 
Hop&lt;/name&gt;,Apache Hop,Apache Hop
diff --git 
a/integration-tests/transforms/datasets/golden-string-operations2.csv 
b/integration-tests/transforms/datasets/golden-string-operations2.csv
new file mode 100644
index 0000000000..08ccfc1367
--- /dev/null
+++ b/integration-tests/transforms/datasets/golden-string-operations2.csv
@@ -0,0 +1,2 @@
+s1,s2,s3,s4,s5,s6,s7
+Apache Hop,APACHE HOP,Apache Hop,Apache Hop,<![CDATA[<name>Apache 
Hop</name>]]>,123456,Apache-Hop
diff --git a/integration-tests/transforms/main-0086-string-operations.hwf 
b/integration-tests/transforms/main-0086-string-operations.hwf
new file mode 100644
index 0000000000..4fb869f2dd
--- /dev/null
+++ b/integration-tests/transforms/main-0086-string-operations.hwf
@@ -0,0 +1,80 @@
+<?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-0086-string-operations</name>
+  <name_sync_with_filename>Y</name_sync_with_filename>
+  <description/>
+  <extended_description/>
+  <workflow_version/>
+  <created_user>-</created_user>
+  <created_date>2025/12/07 14:10:31.342</created_date>
+  <modified_user>-</modified_user>
+  <modified_date>2025/12/07 14:10:31.342</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>Run 0086 tests</name>
+      <description/>
+      <type>RunPipelineTests</type>
+      <attributes/>
+      <test_names>
+        <test_name>
+          <name>0086-string-operations UNIT</name>
+        </test_name>
+      </test_names>
+      <parallel>N</parallel>
+      <xloc>256</xloc>
+      <yloc>64</yloc>
+      <attributes_hac/>
+    </action>
+  </actions>
+  <hops>
+    <hop>
+      <from>Start</from>
+      <to>Run 0086 tests</to>
+      <enabled>Y</enabled>
+      <evaluation>Y</evaluation>
+      <unconditional>Y</unconditional>
+    </hop>
+  </hops>
+  <notepads>
+  </notepads>
+  <attributes/>
+</workflow>
diff --git 
a/integration-tests/transforms/metadata/dataset/golden-string-operations.json 
b/integration-tests/transforms/metadata/dataset/golden-string-operations.json
new file mode 100644
index 0000000000..36c7be969d
--- /dev/null
+++ 
b/integration-tests/transforms/metadata/dataset/golden-string-operations.json
@@ -0,0 +1,64 @@
+{
+  "base_filename": "golden-string-operations.csv",
+  "name": "golden-string-operations",
+  "description": "",
+  "dataset_fields": [
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "s1",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "s2",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": 0,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "s3",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "s4",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "s5",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "s6",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "s7",
+      "field_format": ""
+    }
+  ],
+  "folder_name": ""
+}
\ No newline at end of file
diff --git 
a/integration-tests/transforms/metadata/dataset/golden-string-operations2.json 
b/integration-tests/transforms/metadata/dataset/golden-string-operations2.json
new file mode 100644
index 0000000000..89e5a82c64
--- /dev/null
+++ 
b/integration-tests/transforms/metadata/dataset/golden-string-operations2.json
@@ -0,0 +1,64 @@
+{
+  "base_filename": "golden-string-operations2.csv",
+  "name": "golden-string-operations2",
+  "description": "",
+  "dataset_fields": [
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "s1",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "s2",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": 0,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "s3",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "s4",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "s5",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "s6",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "s7",
+      "field_format": ""
+    }
+  ],
+  "folder_name": ""
+}
\ No newline at end of file
diff --git 
a/integration-tests/transforms/metadata/unit-test/0086-string-operations 
UNIT.json 
b/integration-tests/transforms/metadata/unit-test/0086-string-operations 
UNIT.json
new file mode 100644
index 0000000000..28d172102e
--- /dev/null
+++ b/integration-tests/transforms/metadata/unit-test/0086-string-operations 
UNIT.json  
@@ -0,0 +1,101 @@
+{
+  "database_replacements": [],
+  "autoOpening": true,
+  "description": "",
+  "persist_filename": "",
+  "test_type": "UNIT_TEST",
+  "variableValues": [],
+  "basePath": "${HOP_UNIT_TESTS_FOLDER}",
+  "golden_data_sets": [
+    {
+      "field_mappings": [
+        {
+          "transform_field": "s1",
+          "data_set_field": "s1"
+        },
+        {
+          "transform_field": "s2",
+          "data_set_field": "s2"
+        },
+        {
+          "transform_field": "s3",
+          "data_set_field": "s3"
+        },
+        {
+          "transform_field": "s4",
+          "data_set_field": "s4"
+        },
+        {
+          "transform_field": "s5",
+          "data_set_field": "s5"
+        },
+        {
+          "transform_field": "s6",
+          "data_set_field": "s6"
+        },
+        {
+          "transform_field": "s7",
+          "data_set_field": "s7"
+        }
+      ],
+      "field_order": [
+        "s1",
+        "s2",
+        "s3",
+        "s4",
+        "s5",
+        "s6",
+        "s7"
+      ],
+      "data_set_name": "golden-string-operations",
+      "transform_name": "Validate"
+    },
+    {
+      "field_mappings": [
+        {
+          "transform_field": "s1",
+          "data_set_field": "s1"
+        },
+        {
+          "transform_field": "s2",
+          "data_set_field": "s2"
+        },
+        {
+          "transform_field": "s3",
+          "data_set_field": "s3"
+        },
+        {
+          "transform_field": "s4",
+          "data_set_field": "s4"
+        },
+        {
+          "transform_field": "s5",
+          "data_set_field": "s5"
+        },
+        {
+          "transform_field": "s6",
+          "data_set_field": "s6"
+        },
+        {
+          "transform_field": "s7",
+          "data_set_field": "s7"
+        }
+      ],
+      "field_order": [
+        "s1",
+        "s2",
+        "s3",
+        "s4",
+        "s5",
+        "s6",
+        "s7"
+      ],
+      "data_set_name": "golden-string-operations2",
+      "transform_name": "Validate 2"
+    }
+  ],
+  "input_data_sets": [],
+  "name": "0086-string-operations UNIT",
+  "trans_test_tweaks": [],
+  "pipeline_filename": "./0086-string-operations.hpl"
+}
\ No newline at end of file
diff --git 
a/plugins/transforms/stringoperations/src/main/java/org/apache/hop/pipeline/transforms/stringoperations/StringOperations.java
 
b/plugins/transforms/stringoperations/src/main/java/org/apache/hop/pipeline/transforms/stringoperations/StringOperations.java
index f885dab8d4..cb439a6dc3 100644
--- 
a/plugins/transforms/stringoperations/src/main/java/org/apache/hop/pipeline/transforms/stringoperations/StringOperations.java
+++ 
b/plugins/transforms/stringoperations/src/main/java/org/apache/hop/pipeline/transforms/stringoperations/StringOperations.java
@@ -20,8 +20,8 @@ package org.apache.hop.pipeline.transforms.stringoperations;
 import org.apache.hop.core.Const;
 import org.apache.hop.core.exception.HopException;
 import org.apache.hop.core.exception.HopTransformException;
-import org.apache.hop.core.row.IRowMeta;
 import org.apache.hop.core.row.IValueMeta;
+import org.apache.hop.core.row.RowDataUtil;
 import org.apache.hop.core.row.ValueDataUtil;
 import org.apache.hop.core.util.Utils;
 import org.apache.hop.i18n.BaseMessages;
@@ -29,8 +29,9 @@ import org.apache.hop.pipeline.Pipeline;
 import org.apache.hop.pipeline.PipelineMeta;
 import org.apache.hop.pipeline.transform.BaseTransform;
 import org.apache.hop.pipeline.transform.TransformMeta;
+import org.jetbrains.annotations.Nullable;
 
-/** Apply certain operations too string. */
+/** Apply certain operations to string. */
 public class StringOperations extends BaseTransform<StringOperationsMeta, 
StringOperationsData> {
   private static final Class<?> PKG = StringOperationsMeta.class;
 
@@ -44,170 +45,127 @@ public class StringOperations extends 
BaseTransform<StringOperationsMeta, String
     super(transformMeta, meta, data, copyNr, pipelineMeta, pipeline);
   }
 
-  private String processString(
-      String string,
-      int trimType,
-      int lowerUpper,
-      int padType,
-      String padChar,
-      int padLen,
-      int iniCap,
-      int maskHTML,
-      int digits,
-      int removeSpecialCharacters) {
-    String rcode = string;
+  private String processString(String string, 
StringOperationsMeta.StringOperation operation) {
+    String processed = processStringTrim(operation.getTrimType(), string);
+    processed = processStringLowerUpper(operation.getLowerUpper(), processed);
+    processed =
+        processStringPadding(
+            operation.getPaddingType(),
+            operation.getPadChar(),
+            Const.toInt(operation.getPadLen(), -1),
+            processed);
+    processed = processStringInitCap(operation.getInitCap(), processed);
+    processed = processStringMaskXml(operation.getMaskXml(), processed);
+    processed = processStringDigits(operation.getDigits(), processed);
+    processed = 
processStringRemoveSpecialCharacters(operation.getRemoveSpecialChars(), 
processed);
+
+    return processed;
+  }
 
-    // Trim ?
-    if (!Utils.isEmpty(rcode)) {
-      switch (trimType) {
-        case StringOperationsMeta.TRIM_RIGHT:
-          rcode = Const.rtrim(rcode);
-          break;
-        case StringOperationsMeta.TRIM_LEFT:
-          rcode = Const.ltrim(rcode);
-          break;
-        case StringOperationsMeta.TRIM_BOTH:
-          rcode = Const.trim(rcode);
-          break;
-        default:
-          break;
-      }
-    }
-    // Lower/Upper ?
-    if (!Utils.isEmpty(rcode)) {
-      switch (lowerUpper) {
-        case StringOperationsMeta.LOWER_UPPER_LOWER:
-          rcode = rcode.toLowerCase();
-          break;
-        case StringOperationsMeta.LOWER_UPPER_UPPER:
-          rcode = rcode.toUpperCase();
-          break;
-        default:
-          break;
-      }
+  private static @Nullable String processStringRemoveSpecialCharacters(
+      StringOperationsMeta.RemoveSpecialChars removeSpecialCharacters, String 
string) {
+
+    if (!Utils.isEmpty(string)) {
+      return switch (removeSpecialCharacters) {
+        case NONE -> string;
+        case CR -> Const.removeCR(string);
+        case LF -> Const.removeLF(string);
+        case CRLF -> Const.removeCRLF(string);
+        case TAB -> Const.removeTAB(string);
+        case SPACE -> string.replace(" ", "");
+      };
     }
+    return string;
+  }
 
-    // pad String?
-    if (!Utils.isEmpty(rcode)) {
-      switch (padType) {
-        case StringOperationsMeta.PADDING_LEFT:
-          rcode = Const.lpad(rcode, padChar, padLen);
-          break;
-        case StringOperationsMeta.PADDING_RIGHT:
-          rcode = Const.rpad(rcode, padChar, padLen);
-          break;
-        default:
-          break;
-      }
+  private static @Nullable String processStringDigits(
+      StringOperationsMeta.Digits digits, String string) {
+    if (!Utils.isEmpty(string)) {
+      return switch (digits) {
+        case NONE -> string;
+        case DIGITS_ONLY -> Const.getDigitsOnly(string);
+        case DIGITS_REMOVE -> Const.removeDigits(string);
+      };
     }
+    return string;
+  }
 
-    // InitCap ?
-    if (!Utils.isEmpty(rcode)) {
-      switch (iniCap) {
-        case StringOperationsMeta.INIT_CAP_NO:
-          break;
-        case StringOperationsMeta.INIT_CAP_YES:
-          rcode = ValueDataUtil.initCap(rcode);
-          break;
-        default:
-          break;
-      }
+  private static @Nullable String processStringMaskXml(
+      StringOperationsMeta.MaskXml maskXml, String string) {
+    if (!Utils.isEmpty(string)) {
+      return switch (maskXml) {
+        case NONE -> string;
+        case ESCAPE_XML -> Const.escapeXml(string);
+        case CDATA -> Const.protectXmlCdata(string);
+        case UNESCAPE_XML -> Const.unEscapeXml(string);
+        case ESCAPE_HTML -> Const.escapeHtml(string);
+        case UNESCAPE_HTML -> Const.unEscapeHtml(string);
+        case ESCAPE_SQL -> Const.escapeSql(string);
+      };
     }
+    return string;
+  }
 
-    // escape ?
-    if (!Utils.isEmpty(rcode)) {
-      switch (maskHTML) {
-        case StringOperationsMeta.MASK_ESCAPE_XML:
-          rcode = Const.escapeXml(rcode);
-          break;
-        case StringOperationsMeta.MASK_CDATA:
-          rcode = Const.protectXmlCdata(rcode);
-          break;
-        case StringOperationsMeta.MASK_UNESCAPE_XML:
-          rcode = Const.unEscapeXml(rcode);
-          break;
-        case StringOperationsMeta.MASK_ESCAPE_HTML:
-          rcode = Const.escapeHtml(rcode);
-          break;
-        case StringOperationsMeta.MASK_UNESCAPE_HTML:
-          rcode = Const.unEscapeHtml(rcode);
-          break;
-        case StringOperationsMeta.MASK_ESCAPE_SQL:
-          rcode = Const.escapeSql(rcode);
-          break;
-        default:
-          break;
-      }
+  private static @Nullable String processStringInitCap(
+      StringOperationsMeta.InitCap iniCap, String string) {
+    if (!Utils.isEmpty(string)) {
+      return switch (iniCap) {
+        case NO -> string;
+        case YES -> ValueDataUtil.initCap(string);
+      };
     }
+    return string;
+  }
 
-    // digits only or remove digits ?
-    if (!Utils.isEmpty(rcode)) {
-      switch (digits) {
-        case StringOperationsMeta.DIGITS_NONE:
-          break;
-        case StringOperationsMeta.DIGITS_ONLY:
-          rcode = Const.getDigitsOnly(rcode);
-          break;
-        case StringOperationsMeta.DIGITS_REMOVE:
-          rcode = Const.removeDigits(rcode);
-          break;
-        default:
-          break;
-      }
+  private static @Nullable String processStringPadding(
+      StringOperationsMeta.Padding padType, String padChar, int padLen, String 
string) {
+    if (!Utils.isEmpty(string)) {
+      return switch (padType) {
+        case LEFT -> Const.lpad(string, padChar, padLen);
+        case RIGHT -> Const.rpad(string, padChar, padLen);
+        case NONE -> string;
+      };
     }
+    return string;
+  }
 
-    // remove special characters ?
-    if (!Utils.isEmpty(rcode)) {
-      switch (removeSpecialCharacters) {
-        case StringOperationsMeta.REMOVE_SPECIAL_CHARACTERS_NONE:
-          break;
-        case StringOperationsMeta.REMOVE_SPECIAL_CHARACTERS_CR:
-          rcode = Const.removeCR(rcode);
-          break;
-        case StringOperationsMeta.REMOVE_SPECIAL_CHARACTERS_LF:
-          rcode = Const.removeLF(rcode);
-          break;
-        case StringOperationsMeta.REMOVE_SPECIAL_CHARACTERS_CRLF:
-          rcode = Const.removeCRLF(rcode);
-          break;
-        case StringOperationsMeta.REMOVE_SPECIAL_CHARACTERS_TAB:
-          rcode = Const.removeTAB(rcode);
-          break;
-        case StringOperationsMeta.REMOVE_SPECIAL_CHARACTERS_ESPACE:
-          rcode = rcode.replace(" ", "");
-          break;
-        default:
-          break;
-      }
+  private static @Nullable String processStringLowerUpper(
+      StringOperationsMeta.LowerUpper lowerUpper, String string) {
+    if (!Utils.isEmpty(string)) {
+      return switch (lowerUpper) {
+        case NONE -> string;
+        case LOWER -> string.toLowerCase();
+        case UPPER -> string.toUpperCase();
+      };
     }
+    return string;
+  }
 
-    return rcode;
+  private static @Nullable String processStringTrim(
+      StringOperationsMeta.TrimType trimType, String string) {
+    if (!Utils.isEmpty(string)) {
+      return switch (trimType) {
+        case RIGHT -> Const.rtrim(string);
+        case LEFT -> Const.ltrim(string);
+        case BOTH -> Const.trim(string);
+        case NONE -> string;
+      };
+    }
+    return string;
   }
 
-  private Object[] processRow(IRowMeta rowMeta, Object[] row) throws 
HopException {
+  private Object[] performStringOperations(Object[] row) throws HopException {
+    Object[] rowData = RowDataUtil.createResizedCopy(row, 
data.outputRowMeta.size());
 
-    Object[] rowData = new Object[data.outputRowMeta.size()];
-    // Copy the input fields.
-    System.arraycopy(row, 0, rowData, 0, rowMeta.size());
     int j = 0; // Index into "new fields" area, past the first 
{data.inputFieldsNr} records
-    for (int i = 0; i < data.nrFieldsInStream; i++) {
+    for (int i = 0; i < meta.getOperations().size(); i++) {
+      StringOperationsMeta.StringOperation operation = 
meta.getOperations().get(i);
       if (data.inStreamNrs[i] >= 0) {
         // Get source value
         String value = getInputRowMeta().getString(row, data.inStreamNrs[i]);
         // Apply String operations and return result value
-        value =
-            processString(
-                value,
-                data.trimOperators[i],
-                data.lowerUpperOperators[i],
-                data.padType[i],
-                data.padChar[i],
-                data.padLen[i],
-                data.initCap[i],
-                data.maskHTML[i],
-                data.digits[i],
-                data.removeSpecialCharacters[i]);
-        if (Utils.isEmpty(data.outStreamNrs[i])) {
+        value = processString(value, operation);
+        if (Utils.isEmpty(operation.getFieldOutStream())) {
           // Update field
           rowData[data.inStreamNrs[i]] = value;
           data.outputRowMeta
@@ -215,7 +173,7 @@ public class StringOperations extends 
BaseTransform<StringOperationsMeta, String
               .setStorageType(IValueMeta.STORAGE_TYPE_NORMAL);
         } else {
           // create a new Field
-          rowData[data.inputFieldsNr + j] = value;
+          rowData[getInputRowMeta().size() + j] = value;
           j++;
         }
       }
@@ -225,7 +183,6 @@ public class StringOperations extends 
BaseTransform<StringOperationsMeta, String
 
   @Override
   public boolean processRow() throws HopException {
-
     Object[] r = getRow(); // Get row from input rowset & set row busy!
     if (r == null) {
       // no more input to be expected...
@@ -235,90 +192,11 @@ public class StringOperations extends 
BaseTransform<StringOperationsMeta, String
 
     if (first) {
       first = false;
-
-      // What's the format of the output row?
-      data.outputRowMeta = getInputRowMeta().clone();
-      data.inputFieldsNr = data.outputRowMeta.size();
-      meta.getFields(data.outputRowMeta, getTransformName(), null, null, this, 
metadataProvider);
-      data.nrFieldsInStream = meta.getFieldInStream().length;
-      data.inStreamNrs = new int[data.nrFieldsInStream];
-      for (int i = 0; i < meta.getFieldInStream().length; i++) {
-        data.inStreamNrs[i] = 
getInputRowMeta().indexOfValue(meta.getFieldInStream()[i]);
-        if (data.inStreamNrs[i] < 0) { // couldn't find field!
-
-          throw new HopTransformException(
-              BaseMessages.getString(
-                  PKG, "StringOperations.Exception.FieldRequired", 
meta.getFieldInStream()[i]));
-        }
-        // check field type
-        if (!getInputRowMeta().getValueMeta(data.inStreamNrs[i]).isString()) {
-          throw new HopTransformException(
-              BaseMessages.getString(
-                  PKG,
-                  "StringOperations.Exception.FieldTypeNotString",
-                  meta.getFieldInStream()[i]));
-        }
-      }
-
-      data.outStreamNrs = new String[data.nrFieldsInStream];
-      for (int i = 0; i < meta.getFieldInStream().length; i++) {
-        data.outStreamNrs[i] = meta.getFieldOutStream()[i];
-      }
-
-      // Keep track of the trim operators locally for a very small
-      // optimization.
-      data.trimOperators = new int[data.nrFieldsInStream];
-      for (int i = 0; i < meta.getFieldInStream().length; i++) {
-        data.trimOperators[i] = meta.getTrimType()[i];
-      }
-      // lower Upper
-      data.lowerUpperOperators = new int[data.nrFieldsInStream];
-      for (int i = 0; i < meta.getFieldInStream().length; i++) {
-        data.lowerUpperOperators[i] = meta.getLowerUpper()[i];
-      }
-
-      // padding type?
-      data.padType = new int[data.nrFieldsInStream];
-      for (int i = 0; i < meta.getFieldInStream().length; i++) {
-        data.padType[i] = meta.getPaddingType()[i];
-      }
-
-      // padding char
-      data.padChar = new String[data.nrFieldsInStream];
-      for (int i = 0; i < meta.getFieldInStream().length; i++) {
-        data.padChar[i] = resolve(meta.getPadChar()[i]);
-      }
-
-      // padding len
-      data.padLen = new int[data.nrFieldsInStream];
-      for (int i = 0; i < meta.getFieldInStream().length; i++) {
-        data.padLen[i] = Const.toInt(resolve(meta.getPadLen()[i]), 0);
-      }
-      // InitCap?
-      data.initCap = new int[data.nrFieldsInStream];
-      for (int i = 0; i < meta.getFieldInStream().length; i++) {
-        data.initCap[i] = meta.getInitCap()[i];
-      }
-      // MaskXML?
-      data.maskHTML = new int[data.nrFieldsInStream];
-      for (int i = 0; i < meta.getFieldInStream().length; i++) {
-        data.maskHTML[i] = meta.getMaskXML()[i];
-      }
-      // digits?
-      data.digits = new int[data.nrFieldsInStream];
-      for (int i = 0; i < meta.getFieldInStream().length; i++) {
-        data.digits[i] = meta.getDigits()[i];
-      }
-      // remove special characters?
-      data.removeSpecialCharacters = new int[data.nrFieldsInStream];
-      for (int i = 0; i < meta.getFieldInStream().length; i++) {
-        data.removeSpecialCharacters[i] = meta.getRemoveSpecialCharacters()[i];
-      }
-    } // end if first
+      firstProcessRow();
+    }
 
     try {
-      Object[] output = processRow(getInputRowMeta(), r);
-
+      Object[] output = performStringOperations(r);
       putRow(data.outputRowMeta, output);
 
       if (checkFeedback(getLinesRead()) && isDetailed()) {
@@ -326,12 +204,9 @@ public class StringOperations extends 
BaseTransform<StringOperationsMeta, String
             BaseMessages.getString(PKG, "StringOperations.Log.LineNumber") + 
getLinesRead());
       }
     } catch (HopException e) {
-
-      boolean sendToErrorRow = false;
-      String errorMessage = null;
+      String errorMessage;
 
       if (getTransformMeta().isDoingErrorHandling()) {
-        sendToErrorRow = true;
         errorMessage = e.toString();
       } else {
         logError(
@@ -341,28 +216,33 @@ public class StringOperations extends 
BaseTransform<StringOperationsMeta, String
         setOutputDone(); // signal end to receiver(s)
         return false;
       }
-      if (sendToErrorRow) {
-        // Simply add this row to the error row
-        putError(getInputRowMeta(), r, 1, errorMessage, null, 
"StringOperations001");
-      }
+      // Simply add this row to the error row
+      putError(getInputRowMeta(), r, 1, errorMessage, null, 
"StringOperations001");
     }
     return true;
   }
 
-  @Override
-  public boolean init() {
-    boolean rCode = true;
-
-    if (super.init()) {
-
-      return rCode;
+  private void firstProcessRow() throws HopTransformException {
+    // What's the format of the output row?
+    data.outputRowMeta = getInputRowMeta().clone();
+    meta.getFields(data.outputRowMeta, getTransformName(), null, null, this, 
metadataProvider);
+    data.inStreamNrs = new int[meta.getOperations().size()];
+    for (int i = 0; i < meta.getOperations().size(); i++) {
+      StringOperationsMeta.StringOperation operation = 
meta.getOperations().get(i);
+      data.inStreamNrs[i] = 
getInputRowMeta().indexOfValue(operation.getFieldInStream());
+      if (data.inStreamNrs[i] < 0) { // couldn't find field!
+        throw new HopTransformException(
+            BaseMessages.getString(
+                PKG, "StringOperations.Exception.FieldRequired", 
operation.getFieldInStream()));
+      }
+      // check field type
+      if (!getInputRowMeta().getValueMeta(data.inStreamNrs[i]).isString()) {
+        throw new HopTransformException(
+            BaseMessages.getString(
+                PKG,
+                "StringOperations.Exception.FieldTypeNotString",
+                operation.getFieldInStream()));
+      }
     }
-    return false;
-  }
-
-  @Override
-  public void dispose() {
-
-    super.dispose();
   }
 }
diff --git 
a/plugins/transforms/stringoperations/src/main/java/org/apache/hop/pipeline/transforms/stringoperations/StringOperationsData.java
 
b/plugins/transforms/stringoperations/src/main/java/org/apache/hop/pipeline/transforms/stringoperations/StringOperationsData.java
index 24eaf6dd2a..7c08705273 100644
--- 
a/plugins/transforms/stringoperations/src/main/java/org/apache/hop/pipeline/transforms/stringoperations/StringOperationsData.java
+++ 
b/plugins/transforms/stringoperations/src/main/java/org/apache/hop/pipeline/transforms/stringoperations/StringOperationsData.java
@@ -27,38 +27,10 @@ public class StringOperationsData extends BaseTransformData 
implements ITransfor
 
   public int[] inStreamNrs; // string infields
 
-  public String[] outStreamNrs;
-
-  /** Runtime trim operators */
-  public int[] trimOperators;
-
-  /** Runtime trim operators */
-  public int[] lowerUpperOperators;
-
-  public int[] padType;
-
-  public String[] padChar;
-
-  public int[] padLen;
-
-  public int[] initCap;
-
-  public int[] maskHTML;
-
-  public int[] digits;
-
-  public int[] removeSpecialCharacters;
-
   public IRowMeta outputRowMeta;
 
-  public int inputFieldsNr;
-
-  public int nrFieldsInStream;
-
   /** Default constructor. */
   public StringOperationsData() {
     super();
-    this.inputFieldsNr = 0;
-    this.nrFieldsInStream = 0;
   }
 }
diff --git 
a/plugins/transforms/stringoperations/src/main/java/org/apache/hop/pipeline/transforms/stringoperations/StringOperationsDialog.java
 
b/plugins/transforms/stringoperations/src/main/java/org/apache/hop/pipeline/transforms/stringoperations/StringOperationsDialog.java
index 8b35336f88..346348be9d 100644
--- 
a/plugins/transforms/stringoperations/src/main/java/org/apache/hop/pipeline/transforms/stringoperations/StringOperationsDialog.java
+++ 
b/plugins/transforms/stringoperations/src/main/java/org/apache/hop/pipeline/transforms/stringoperations/StringOperationsDialog.java
@@ -19,6 +19,7 @@ package org.apache.hop.pipeline.transforms.stringoperations;
 
 import java.util.ArrayList;
 import java.util.List;
+import org.apache.hop.core.Const;
 import org.apache.hop.core.exception.HopException;
 import org.apache.hop.core.row.IRowMeta;
 import org.apache.hop.core.row.IValueMeta;
@@ -86,8 +87,75 @@ public class StringOperationsDialog extends 
BaseTransformDialog {
     fdlKey.top = new FormAttachment(wSpacer, margin);
     wlKey.setLayoutData(fdlKey);
 
+    buildColumnsForFields();
+
+    wFields =
+        new TableView(
+            variables,
+            shell,
+            SWT.BORDER | SWT.FULL_SELECTION | SWT.MULTI | SWT.V_SCROLL | 
SWT.H_SCROLL,
+            ciKey,
+            1,
+            lsMod,
+            props);
+
+    FormData fdKey = new FormData();
+    fdKey.left = new FormAttachment(0, 0);
+    fdKey.top = new FormAttachment(wlKey, margin);
+    fdKey.right = new FormAttachment(100, -margin);
+    fdKey.bottom = new FormAttachment(wOk, -margin);
+    wFields.setLayoutData(fdKey);
+
+    getData();
+
+    findFieldsInBackground(display);
+
+    input.setChanged(changed);
+    focusTransformName();
+    BaseDialog.defaultShellHandling(shell, c -> ok(), c -> cancel());
+
+    return transformName;
+  }
+
+  private void findFieldsInBackground(Display display) {
+    final Runnable runnable =
+        () -> {
+          TransformMeta transformMeta = 
pipelineMeta.findTransform(transformName);
+          if (transformMeta == null) {
+            return;
+          }
+          try {
+            IRowMeta row = pipelineMeta.getPrevTransformFields(variables, 
transformMeta);
+            if (row != null) {
+              // Remember these fields...
+              for (int i = 0; i < row.size(); i++) {
+                inputFields.add(row.getValueMeta(i).getName());
+              }
+
+              setComboBoxes();
+            }
+
+            // Dislay in red missing field names
+            display.asyncExec(
+                () -> {
+                  if (!wFields.isDisposed()) {
+                    for (int i = 0; i < wFields.table.getItemCount(); i++) {
+                      TableItem it = wFields.table.getItem(i);
+                      if (!Utils.isEmpty(it.getText(1)) && 
(!inputFields.contains(it.getText(1)))) {
+                        
it.setBackground(GuiResource.getInstance().getColorRed());
+                      }
+                    }
+                  }
+                });
+          } catch (HopException e) {
+            logError("Error getting fields from incoming stream!", e);
+          }
+        };
+    new Thread(runnable).start();
+  }
+
+  private void buildColumnsForFields() {
     int nrFieldCols = 11;
-    int nrFieldRows = (input.getFieldInStream() != null ? 
input.getFieldInStream().length : 1);
 
     ciKey = new ColumnInfo[nrFieldCols];
     ciKey[0] =
@@ -105,19 +173,19 @@ public class StringOperationsDialog extends 
BaseTransformDialog {
         new ColumnInfo(
             BaseMessages.getString(PKG, 
"StringOperationsDialog.ColumnInfo.Trim"),
             ColumnInfo.COLUMN_TYPE_CCOMBO,
-            StringOperationsMeta.trimTypeDesc,
+            StringOperationsMeta.TrimType.getDescriptions(),
             true);
     ciKey[3] =
         new ColumnInfo(
             BaseMessages.getString(PKG, 
"StringOperationsDialog.ColumnInfo.LowerUpper"),
             ColumnInfo.COLUMN_TYPE_CCOMBO,
-            StringOperationsMeta.lowerUpperDesc,
+            StringOperationsMeta.LowerUpper.getDescriptions(),
             true);
     ciKey[4] =
         new ColumnInfo(
             BaseMessages.getString(PKG, 
"StringOperationsDialog.ColumnInfo.Padding"),
             ColumnInfo.COLUMN_TYPE_CCOMBO,
-            StringOperationsMeta.paddingDesc,
+            StringOperationsMeta.Padding.getDescriptions(),
             true);
     ciKey[5] =
         new ColumnInfo(
@@ -133,23 +201,23 @@ public class StringOperationsDialog extends 
BaseTransformDialog {
         new ColumnInfo(
             BaseMessages.getString(PKG, 
"StringOperationsDialog.ColumnInfo.InitCap"),
             ColumnInfo.COLUMN_TYPE_CCOMBO,
-            StringOperationsMeta.initCapDesc);
+            StringOperationsMeta.InitCap.getDescriptions());
     ciKey[8] =
         new ColumnInfo(
             BaseMessages.getString(PKG, 
"StringOperationsDialog.ColumnInfo.MaskXML"),
             ColumnInfo.COLUMN_TYPE_CCOMBO,
-            StringOperationsMeta.maskXMLDesc);
+            StringOperationsMeta.MaskXml.getDescriptions());
     ciKey[9] =
         new ColumnInfo(
             BaseMessages.getString(PKG, 
"StringOperationsDialog.ColumnInfo.Digits"),
             ColumnInfo.COLUMN_TYPE_CCOMBO,
-            StringOperationsMeta.digitsDesc);
+            StringOperationsMeta.Digits.getDescriptions());
     ciKey[10] =
         new ColumnInfo(
             BaseMessages.getString(
                 PKG, 
"StringOperationsDialog.ColumnInfo.RemoveSpecialCharacters"),
             ColumnInfo.COLUMN_TYPE_CCOMBO,
-            StringOperationsMeta.removeSpecialCharactersDesc);
+            StringOperationsMeta.RemoveSpecialChars.getDescriptions());
 
     ciKey[1].setToolTip(
         BaseMessages.getString(PKG, 
"StringOperationsDialog.ColumnInfo.OutStreamField.Tooltip"));
@@ -158,71 +226,6 @@ public class StringOperationsDialog extends 
BaseTransformDialog {
     ciKey[5].setUsingVariables(true);
     ciKey[6].setUsingVariables(true);
     ciKey[7].setUsingVariables(true);
-
-    wFields =
-        new TableView(
-            variables,
-            shell,
-            SWT.BORDER | SWT.FULL_SELECTION | SWT.MULTI | SWT.V_SCROLL | 
SWT.H_SCROLL,
-            ciKey,
-            nrFieldRows,
-            lsMod,
-            props);
-
-    FormData fdKey = new FormData();
-    fdKey.left = new FormAttachment(0, 0);
-    fdKey.top = new FormAttachment(wlKey, margin);
-    fdKey.right = new FormAttachment(100, -margin);
-    fdKey.bottom = new FormAttachment(wOk, -margin);
-    wFields.setLayoutData(fdKey);
-
-    getData();
-
-    //
-    // Search the fields in the background
-    //
-
-    final Runnable runnable =
-        () -> {
-          TransformMeta transformMeta = 
pipelineMeta.findTransform(transformName);
-          if (transformMeta != null) {
-            try {
-              IRowMeta row = pipelineMeta.getPrevTransformFields(variables, 
transformMeta);
-              if (row != null) {
-                // Remember these fields...
-                for (int i = 0; i < row.size(); i++) {
-                  inputFields.add(row.getValueMeta(i).getName());
-                }
-
-                setComboBoxes();
-              }
-
-              // Dislay in red missing field names
-              display.asyncExec(
-                  () -> {
-                    if (!wFields.isDisposed()) {
-                      for (int i = 0; i < wFields.table.getItemCount(); i++) {
-                        TableItem it = wFields.table.getItem(i);
-                        if (!Utils.isEmpty(it.getText(1))
-                            && (!inputFields.contains(it.getText(1)))) {
-                          
it.setBackground(GuiResource.getInstance().getColorRed());
-                        }
-                      }
-                    }
-                  });
-
-            } catch (HopException e) {
-              logError("Error getting fields from incoming stream!", e);
-            }
-          }
-        };
-    new Thread(runnable).start();
-
-    input.setChanged(changed);
-    focusTransformName();
-    BaseDialog.defaultShellHandling(shell, c -> ok(), c -> cancel());
-
-    return transformName;
   }
 
   protected void setComboBoxes() {
@@ -232,32 +235,19 @@ public class StringOperationsDialog extends 
BaseTransformDialog {
 
   /** Copy information from the meta-data input to the dialog fields. */
   public void getData() {
-    if (input.getFieldInStream() != null) {
-      for (int i = 0; i < input.getFieldInStream().length; i++) {
-        TableItem item = wFields.table.getItem(i);
-        if (input.getFieldInStream()[i] != null) {
-          item.setText(1, input.getFieldInStream()[i]);
-        }
-        if (input.getFieldOutStream()[i] != null) {
-          item.setText(2, input.getFieldOutStream()[i]);
-        }
-        item.setText(3, 
StringOperationsMeta.getTrimTypeDesc(input.getTrimType()[i]));
-        item.setText(4, 
StringOperationsMeta.getLowerUpperDesc(input.getLowerUpper()[i]));
-        item.setText(5, 
StringOperationsMeta.getPaddingDesc(input.getPaddingType()[i]));
-        if (input.getPadChar()[i] != null) {
-          item.setText(6, input.getPadChar()[i]);
-        }
-        if (input.getPadLen()[i] != null) {
-          item.setText(7, input.getPadLen()[i]);
-        }
-        item.setText(8, 
StringOperationsMeta.getInitCapDesc(input.getInitCap()[i]));
-        item.setText(9, 
StringOperationsMeta.getMaskXMLDesc(input.getMaskXML()[i]));
-        item.setText(10, 
StringOperationsMeta.getDigitsDesc(input.getDigits()[i]));
-        item.setText(
-            11,
-            StringOperationsMeta.getRemoveSpecialCharactersDesc(
-                input.getRemoveSpecialCharacters()[i]));
-      }
+    for (StringOperationsMeta.StringOperation operation : 
input.getOperations()) {
+      TableItem item = new TableItem(wFields.table, SWT.NONE);
+      item.setText(1, Const.NVL(operation.getFieldInStream(), ""));
+      item.setText(2, Const.NVL(operation.getFieldOutStream(), ""));
+      item.setText(3, operation.getTrimType().getDescription());
+      item.setText(4, operation.getLowerUpper().getDescription());
+      item.setText(5, operation.getPaddingType().getDescription());
+      item.setText(6, Const.NVL(operation.getPadChar(), ""));
+      item.setText(7, Const.NVL(operation.getPadLen(), ""));
+      item.setText(8, operation.getInitCap().getDescription());
+      item.setText(9, operation.getMaskXml().getDescription());
+      item.setText(10, operation.getDigits().getDescription());
+      item.setText(11, operation.getRemoveSpecialChars().getDescription());
     }
 
     wFields.setRowNums();
@@ -270,30 +260,23 @@ public class StringOperationsDialog extends 
BaseTransformDialog {
     dispose();
   }
 
-  private void getInfo(StringOperationsMeta inf) {
-    int nrkeys = wFields.nrNonEmpty();
-
-    inf.allocate(nrkeys);
-    if (isDebug()) {
-      logDebug(
-          BaseMessages.getString(
-              PKG, "StringOperationsDialog.Log.FoundFields", 
String.valueOf(nrkeys)));
-    }
-
-    for (int i = 0; i < nrkeys; i++) {
-      TableItem item = wFields.getNonEmpty(i);
-      inf.getFieldInStream()[i] = item.getText(1);
-      inf.getFieldOutStream()[i] = item.getText(2);
-      inf.getTrimType()[i] = 
StringOperationsMeta.getTrimTypeByDesc(item.getText(3));
-      inf.getLowerUpper()[i] = 
StringOperationsMeta.getLowerUpperByDesc(item.getText(4));
-      inf.getPaddingType()[i] = 
StringOperationsMeta.getPaddingByDesc(item.getText(5));
-      inf.getPadChar()[i] = item.getText(6);
-      inf.getPadLen()[i] = item.getText(7);
-      inf.getInitCap()[i] = 
StringOperationsMeta.getInitCapByDesc(item.getText(8));
-      inf.getMaskXML()[i] = 
StringOperationsMeta.getMaskXMLByDesc(item.getText(9));
-      inf.getDigits()[i] = 
StringOperationsMeta.getDigitsByDesc(item.getText(10));
-      inf.getRemoveSpecialCharacters()[i] =
-          
StringOperationsMeta.getRemoveSpecialCharactersByDesc(item.getText(11));
+  private void getInfo(StringOperationsMeta m) {
+    m.getOperations().clear();
+    for (TableItem item : wFields.getNonEmptyItems()) {
+      StringOperationsMeta.StringOperation o = new 
StringOperationsMeta.StringOperation();
+      o.setFieldInStream(item.getText(1));
+      o.setFieldOutStream(item.getText(2));
+      
o.setTrimType(StringOperationsMeta.TrimType.lookupDescription(item.getText(3)));
+      
o.setLowerUpper(StringOperationsMeta.LowerUpper.lookupDescription(item.getText(4)));
+      
o.setPaddingType(StringOperationsMeta.Padding.lookupDescription(item.getText(5)));
+      o.setPadChar(item.getText(6));
+      o.setPadLen(item.getText(7));
+      
o.setInitCap(StringOperationsMeta.InitCap.lookupDescription(item.getText(8)));
+      
o.setMaskXml(StringOperationsMeta.MaskXml.lookupDescription(item.getText(9)));
+      
o.setDigits(StringOperationsMeta.Digits.lookupDescription(item.getText(10)));
+      o.setRemoveSpecialChars(
+          
StringOperationsMeta.RemoveSpecialChars.lookupDescription(item.getText(11)));
+      m.getOperations().add(o);
     }
 
     transformName = wTransformName.getText(); // return value
diff --git 
a/plugins/transforms/stringoperations/src/main/java/org/apache/hop/pipeline/transforms/stringoperations/StringOperationsMeta.java
 
b/plugins/transforms/stringoperations/src/main/java/org/apache/hop/pipeline/transforms/stringoperations/StringOperationsMeta.java
index eecf2b205d..5b79086e33 100644
--- 
a/plugins/transforms/stringoperations/src/main/java/org/apache/hop/pipeline/transforms/stringoperations/StringOperationsMeta.java
+++ 
b/plugins/transforms/stringoperations/src/main/java/org/apache/hop/pipeline/transforms/stringoperations/StringOperationsMeta.java
@@ -17,27 +17,32 @@
 
 package org.apache.hop.pipeline.transforms.stringoperations;
 
+import java.util.ArrayList;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
+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.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.ValueMetaString;
 import org.apache.hop.core.util.Utils;
 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.IEnumHasCodeAndDescription;
 import org.apache.hop.metadata.api.IHopMetadataProvider;
 import org.apache.hop.pipeline.PipelineMeta;
 import org.apache.hop.pipeline.transform.BaseTransformMeta;
 import org.apache.hop.pipeline.transform.TransformMeta;
-import org.w3c.dom.Node;
+import org.jetbrains.annotations.NotNull;
 
 @Transform(
     id = "StringOperations",
@@ -48,476 +53,34 @@ import org.w3c.dom.Node;
     keywords = "i18n::StringOperationsMeta.keyword",
     documentationUrl = "/pipeline/transforms/stringoperations.html")
 @InjectionSupported(localizationPrefix = "StringOperationsDialog.Injection.")
+@Getter
+@Setter
 public class StringOperationsMeta
     extends BaseTransformMeta<StringOperations, StringOperationsData> {
 
   private static final Class<?> PKG = StringOperationsMeta.class;
   public static final String CONST_SPACES = "        ";
 
-  /** which field in input stream to compare with? */
-  @Injection(name = "SOURCEFIELDS")
-  private String[] fieldInStream;
-
-  /** output field */
-  @Injection(name = "TARGETFIELDS")
-  private String[] fieldOutStream;
-
-  /** Trim type */
-  @Injection(name = "TRIMTYPE")
-  private int[] trimType;
-
-  /** Lower/Upper type */
-  @Injection(name = "LOWERUPPER")
-  private int[] lowerUpper;
-
-  /** InitCap */
-  @Injection(name = "INITCAP")
-  private int[] initCap;
-
-  @Injection(name = "MASKXML")
-  private int[] maskXML;
-
-  @Injection(name = "DIGITS")
-  private int[] digits;
-
-  @Injection(name = "SPECIALCHARS")
-  private int[] remove_special_characters;
-
-  /** padding type */
-  @Injection(name = "PADDING")
-  private int[] paddingType;
-
-  /** Pad length */
-  @Injection(name = "PADLEN")
-  private String[] padLen;
-
-  @Injection(name = "PADCHAR")
-  private String[] padChar;
-
-  /** The trim type codes */
-  public static final String[] trimTypeCode = {"none", "left", "right", 
"both"};
-
-  public static final int TRIM_NONE = 0;
-
-  public static final int TRIM_LEFT = 1;
-
-  public static final int TRIM_RIGHT = 2;
-
-  public static final int TRIM_BOTH = 3;
-
-  /** The trim description */
-  public static final String[] trimTypeDesc = {
-    BaseMessages.getString(PKG, "StringOperationsMeta.TrimType.None"),
-    BaseMessages.getString(PKG, "StringOperationsMeta.TrimType.Left"),
-    BaseMessages.getString(PKG, "StringOperationsMeta.TrimType.Right"),
-    BaseMessages.getString(PKG, "StringOperationsMeta.TrimType.Both")
-  };
-
-  /** The lower upper codes */
-  public static final String[] lowerUpperCode = {"none", "lower", "upper"};
-
-  public static final int LOWER_UPPER_NONE = 0;
-
-  public static final int LOWER_UPPER_LOWER = 1;
-
-  public static final int LOWER_UPPER_UPPER = 2;
-
-  /** The lower upper description */
-  public static final String[] lowerUpperDesc = {
-    BaseMessages.getString(PKG, "StringOperationsMeta.LowerUpper.None"),
-    BaseMessages.getString(PKG, "StringOperationsMeta.LowerUpper.Lower"),
-    BaseMessages.getString(PKG, "StringOperationsMeta.LowerUpper.Upper")
-  };
-
-  public static final String[] initCapDesc =
-      new String[] {
-        BaseMessages.getString(PKG, "System.Combo.No"),
-        BaseMessages.getString(PKG, "System.Combo.Yes")
-      };
-
-  public static final String[] initCapCode = {"no", "yes"};
-
-  public static final int INIT_CAP_NO = 0;
-
-  public static final int INIT_CAP_YES = 1;
-
-  // digits
-  public static final String[] digitsCode = {"none", "digits_only", 
"remove_digits"};
-
-  public static final int DIGITS_NONE = 0;
-
-  public static final int DIGITS_ONLY = 1;
-
-  public static final int DIGITS_REMOVE = 2;
-
-  public static final String[] digitsDesc =
-      new String[] {
-        BaseMessages.getString(PKG, "StringOperationsMeta.Digits.None"),
-        BaseMessages.getString(PKG, "StringOperationsMeta.Digits.Only"),
-        BaseMessages.getString(PKG, "StringOperationsMeta.Digits.Remove")
-      };
-
-  // mask XML
-
-  public static final String[] maskXMLDesc =
-      new String[] {
-        BaseMessages.getString(PKG, "StringOperationsMeta.MaskXML.None"),
-        BaseMessages.getString(PKG, "StringOperationsMeta.MaskXML.EscapeXML"),
-        BaseMessages.getString(PKG, "StringOperationsMeta.MaskXML.CDATA"),
-        BaseMessages.getString(PKG, 
"StringOperationsMeta.MaskXML.UnEscapeXML"),
-        BaseMessages.getString(PKG, "StringOperationsMeta.MaskXML.EscapeSQL"),
-        BaseMessages.getString(PKG, "StringOperationsMeta.MaskXML.EscapeHTML"),
-        BaseMessages.getString(PKG, 
"StringOperationsMeta.MaskXML.UnEscapeHTML"),
-      };
-
-  public static final String[] maskXMLCode = {
-    "none", "escapexml", "cdata", "unescapexml", "escapesql", "escapehtml", 
"unescapehtml"
-  };
-
-  public static final int MASK_NONE = 0;
-  public static final int MASK_ESCAPE_XML = 1;
-  public static final int MASK_CDATA = 2;
-  public static final int MASK_UNESCAPE_XML = 3;
-  public static final int MASK_ESCAPE_SQL = 4;
-  public static final int MASK_ESCAPE_HTML = 5;
-  public static final int MASK_UNESCAPE_HTML = 6;
-
-  // remove special characters
-  public static final String[] removeSpecialCharactersCode = {
-    "none", "cr", "lf", "crlf", "tab", "espace"
-  };
-
-  public static final int REMOVE_SPECIAL_CHARACTERS_NONE = 0;
-
-  public static final int REMOVE_SPECIAL_CHARACTERS_CR = 1;
-
-  public static final int REMOVE_SPECIAL_CHARACTERS_LF = 2;
-
-  public static final int REMOVE_SPECIAL_CHARACTERS_CRLF = 3;
-
-  public static final int REMOVE_SPECIAL_CHARACTERS_TAB = 4;
-
-  public static final int REMOVE_SPECIAL_CHARACTERS_ESPACE = 5;
-
-  public static final String[] removeSpecialCharactersDesc =
-      new String[] {
-        BaseMessages.getString(PKG, 
"StringOperationsMeta.RemoveSpecialCharacters.None"),
-        BaseMessages.getString(PKG, 
"StringOperationsMeta.RemoveSpecialCharacters.CR"),
-        BaseMessages.getString(PKG, 
"StringOperationsMeta.RemoveSpecialCharacters.LF"),
-        BaseMessages.getString(PKG, 
"StringOperationsMeta.RemoveSpecialCharacters.CRLF"),
-        BaseMessages.getString(PKG, 
"StringOperationsMeta.RemoveSpecialCharacters.TAB"),
-        BaseMessages.getString(PKG, 
"StringOperationsMeta.RemoveSpecialCharacters.Space")
-      };
-
-  /** The padding description */
-  public static final String[] paddingDesc = {
-    BaseMessages.getString(PKG, "StringOperationsMeta.Padding.None"),
-    BaseMessages.getString(PKG, "StringOperationsMeta.Padding.Left"),
-    BaseMessages.getString(PKG, "StringOperationsMeta.Padding.Right")
-  };
-
-  public static final String[] paddingCode = {"none", "left", "right"};
-
-  public static final int PADDING_NONE = 0;
-
-  public static final int PADDING_LEFT = 1;
-
-  public static final int PADDING_RIGHT = 2;
+  @HopMetadataProperty(
+      groupKey = "fields",
+      key = "field",
+      injectionGroupKey = "FIELDS",
+      injectionKey = "FIELD")
+  private List<StringOperation> operations;
 
   public StringOperationsMeta() {
-    super(); // allocate BaseTransformMeta
-  }
-
-  /**
-   * @return Returns the fieldInStream.
-   */
-  public String[] getFieldInStream() {
-    return fieldInStream;
-  }
-
-  /**
-   * @param keyStream The fieldInStream to set.
-   */
-  public void setFieldInStream(String[] keyStream) {
-    this.fieldInStream = keyStream;
-  }
-
-  /**
-   * @return Returns the fieldOutStream.
-   */
-  public String[] getFieldOutStream() {
-    return fieldOutStream;
-  }
-
-  /**
-   * @param keyStream The fieldOutStream to set.
-   */
-  public void setFieldOutStream(String[] keyStream) {
-    this.fieldOutStream = keyStream;
-  }
-
-  public String[] getPadLen() {
-    return padLen;
+    super();
+    this.operations = new ArrayList<>();
   }
 
-  public void setPadLen(String[] value) {
-    padLen = value;
-  }
-
-  public String[] getPadChar() {
-    return padChar;
-  }
-
-  public void setPadChar(String[] value) {
-    padChar = value;
-  }
-
-  public int[] getTrimType() {
-    return trimType;
-  }
-
-  public void setTrimType(int[] trimType) {
-    this.trimType = trimType;
-  }
-
-  public int[] getLowerUpper() {
-    return lowerUpper;
-  }
-
-  public void setLowerUpper(int[] lowerUpper) {
-    this.lowerUpper = lowerUpper;
-  }
-
-  public int[] getInitCap() {
-    return initCap;
-  }
-
-  public void setInitCap(int[] value) {
-    initCap = value;
-  }
-
-  public int[] getMaskXML() {
-    return maskXML;
-  }
-
-  public void setMaskXML(int[] value) {
-    maskXML = value;
-  }
-
-  public int[] getDigits() {
-    return digits;
-  }
-
-  public void setDigits(int[] value) {
-    digits = value;
-  }
-
-  public int[] getRemoveSpecialCharacters() {
-    return remove_special_characters;
-  }
-
-  public void setRemoveSpecialCharacters(int[] value) {
-    remove_special_characters = value;
-  }
-
-  public int[] getPaddingType() {
-    return paddingType;
-  }
-
-  public void setPaddingType(int[] value) {
-    paddingType = value;
-  }
-
-  @Override
-  public void loadXml(Node transformNode, IHopMetadataProvider 
metadataProvider)
-      throws HopXmlException {
-    readData(transformNode, metadataProvider);
-  }
-
-  public void allocate(int nrkeys) {
-    fieldInStream = new String[nrkeys];
-    fieldOutStream = new String[nrkeys];
-    trimType = new int[nrkeys];
-    lowerUpper = new int[nrkeys];
-    paddingType = new int[nrkeys];
-    padChar = new String[nrkeys];
-    padLen = new String[nrkeys];
-    initCap = new int[nrkeys];
-    maskXML = new int[nrkeys];
-    digits = new int[nrkeys];
-    remove_special_characters = new int[nrkeys];
+  public StringOperationsMeta(StringOperationsMeta m) {
+    this();
+    m.operations.forEach(op -> this.operations.add(new StringOperation(op)));
   }
 
   @Override
   public Object clone() {
-    StringOperationsMeta retval = (StringOperationsMeta) super.clone();
-    int nrkeys = fieldInStream.length;
-
-    retval.allocate(nrkeys);
-    System.arraycopy(fieldInStream, 0, retval.fieldInStream, 0, nrkeys);
-    System.arraycopy(fieldOutStream, 0, retval.fieldOutStream, 0, nrkeys);
-    System.arraycopy(trimType, 0, retval.trimType, 0, nrkeys);
-    System.arraycopy(lowerUpper, 0, retval.lowerUpper, 0, nrkeys);
-    System.arraycopy(paddingType, 0, retval.paddingType, 0, nrkeys);
-    System.arraycopy(padChar, 0, retval.padChar, 0, nrkeys);
-    System.arraycopy(padLen, 0, retval.padLen, 0, nrkeys);
-    System.arraycopy(initCap, 0, retval.initCap, 0, nrkeys);
-    System.arraycopy(maskXML, 0, retval.maskXML, 0, nrkeys);
-    System.arraycopy(digits, 0, retval.digits, 0, nrkeys);
-    System.arraycopy(remove_special_characters, 0, 
retval.remove_special_characters, 0, nrkeys);
-
-    return retval;
-  }
-
-  private void readData(Node transformNode, IHopMetadataProvider 
metadataProvider)
-      throws HopXmlException {
-    try {
-      int nrkeys;
-
-      Node lookup = XmlHandler.getSubNode(transformNode, "fields");
-      nrkeys = XmlHandler.countNodes(lookup, "field");
-      allocate(nrkeys);
-
-      for (int i = 0; i < nrkeys; i++) {
-        Node fnode = XmlHandler.getSubNodeByNr(lookup, "field", i);
-
-        fieldInStream[i] = Const.NVL(XmlHandler.getTagValue(fnode, 
"in_stream_name"), "");
-        fieldOutStream[i] = Const.NVL(XmlHandler.getTagValue(fnode, 
"out_stream_name"), "");
-
-        trimType[i] = 
getTrimTypeByCode(Const.NVL(XmlHandler.getTagValue(fnode, "trim_type"), ""));
-        lowerUpper[i] =
-            getLowerUpperByCode(Const.NVL(XmlHandler.getTagValue(fnode, 
"lower_upper"), ""));
-        paddingType[i] =
-            getPaddingByCode(Const.NVL(XmlHandler.getTagValue(fnode, 
"padding_type"), ""));
-        padChar[i] = Const.NVL(XmlHandler.getTagValue(fnode, "pad_char"), "");
-        padLen[i] = Const.NVL(XmlHandler.getTagValue(fnode, "pad_len"), "");
-        initCap[i] = getInitCapByCode(Const.NVL(XmlHandler.getTagValue(fnode, 
"init_cap"), ""));
-        maskXML[i] = getMaskXMLByCode(Const.NVL(XmlHandler.getTagValue(fnode, 
"mask_xml"), ""));
-        digits[i] = getDigitsByCode(Const.NVL(XmlHandler.getTagValue(fnode, 
"digits"), ""));
-        remove_special_characters[i] =
-            getRemoveSpecialCharactersByCode(
-                Const.NVL(XmlHandler.getTagValue(fnode, 
"remove_special_characters"), ""));
-      }
-    } catch (Exception e) {
-      throw new HopXmlException(
-          BaseMessages.getString(
-              PKG, 
"StringOperationsMeta.Exception.UnableToReadTransformMetaFromXML"),
-          e);
-    }
-  }
-
-  @Override
-  public void setDefault() {
-    fieldInStream = null;
-    fieldOutStream = null;
-    allocate(0);
-  }
-
-  @Override
-  public String getXml() {
-    StringBuilder retval = new StringBuilder(500);
-
-    retval.append("    <fields>").append(Const.CR);
-
-    for (int i = 0; i < fieldInStream.length; i++) {
-      // defaults when not present
-      String lPadChar = (padChar.length == 0 || padChar.length <= i) ? "" : 
padChar[i];
-      String lPadLen = (padLen.length == 0 || padLen.length <= i) ? "" : 
padLen[i];
-
-      retval.append("      <field>").append(Const.CR);
-      retval
-          .append(CONST_SPACES)
-          .append(XmlHandler.addTagValue("in_stream_name", fieldInStream[i]));
-
-      retval
-          .append(CONST_SPACES)
-          .append(
-              XmlHandler.addTagValue(
-                  "out_stream_name",
-                  (fieldOutStream == null
-                          || fieldOutStream.length == 0
-                          || fieldOutStream.length <= i)
-                      ? ""
-                      : !Utils.isEmpty(fieldOutStream[i]) ? fieldOutStream[i] 
: ""));
-      retval
-          .append(CONST_SPACES)
-          .append(
-              XmlHandler.addTagValue(
-                  "trim_type",
-                  (trimType == null || trimType.length == 0 || trimType.length 
<= i)
-                      ? ""
-                      : getTrimTypeCode(trimType[i])));
-      retval
-          .append(CONST_SPACES)
-          .append(
-              XmlHandler.addTagValue(
-                  "lower_upper",
-                  (lowerUpper == null || lowerUpper.length == 0 || 
lowerUpper.length <= i)
-                      ? ""
-                      : getLowerUpperCode(lowerUpper[i])));
-      retval
-          .append(CONST_SPACES)
-          .append(
-              XmlHandler.addTagValue(
-                  "padding_type",
-                  (paddingType == null || paddingType.length == 0 || 
paddingType.length <= i)
-                      ? ""
-                      : getPaddingCode(paddingType[i])));
-      retval
-          .append(CONST_SPACES)
-          .append(
-              XmlHandler.addTagValue(
-                  "pad_char",
-                  (padChar == null || padChar.length == 0 || padChar.length <= 
i)
-                      ? ""
-                      : padChar[i]));
-      retval
-          .append(CONST_SPACES)
-          .append(
-              XmlHandler.addTagValue(
-                  "pad_len",
-                  (padLen == null || padLen.length == 0 || padLen.length <= i) 
? "" : padLen[i]));
-      retval
-          .append(CONST_SPACES)
-          .append(
-              XmlHandler.addTagValue(
-                  "init_cap",
-                  (initCap == null || initCap.length == 0 || initCap.length <= 
i)
-                      ? ""
-                      : getInitCapCode(initCap[i])));
-      retval
-          .append(CONST_SPACES)
-          .append(
-              XmlHandler.addTagValue(
-                  "mask_xml",
-                  (maskXML == null || maskXML.length == 0 || maskXML.length <= 
i)
-                      ? ""
-                      : getMaskXMLCode(maskXML[i])));
-      retval
-          .append(CONST_SPACES)
-          .append(
-              XmlHandler.addTagValue(
-                  "digits",
-                  (digits == null || digits.length == 0 || digits.length <= i)
-                      ? ""
-                      : getDigitsCode(digits[i])));
-      retval
-          .append(CONST_SPACES)
-          .append(
-              XmlHandler.addTagValue(
-                  "remove_special_characters",
-                  (remove_special_characters == null
-                          || remove_special_characters.length == 0
-                          || remove_special_characters.length <= i)
-                      ? ""
-                      : 
getRemoveSpecialCharactersCode(remove_special_characters[i])));
-
-      retval.append("      </field>").append(Const.CR);
-    }
-
-    retval.append("    </fields>").append(Const.CR);
-
-    return retval.toString();
+    return new StringOperationsMeta(this);
   }
 
   @Override
@@ -530,9 +93,9 @@ public class StringOperationsMeta
       IHopMetadataProvider metadataProvider)
       throws HopTransformException {
     // Add new field?
-    for (int i = 0; i < fieldOutStream.length; i++) {
+    for (StringOperation operation : operations) {
       IValueMeta v;
-      String outputField = variables.resolve(fieldOutStream[i]);
+      String outputField = variables.resolve(operation.fieldOutStream);
       if (!Utils.isEmpty(outputField)) {
         // Add a new field
         v = new ValueMetaString(outputField);
@@ -540,14 +103,13 @@ public class StringOperationsMeta
         v.setOrigin(name);
         inputRowMeta.addValueMeta(v);
       } else {
-        v = inputRowMeta.searchValueMeta(fieldInStream[i]);
+        v = inputRowMeta.searchValueMeta(operation.fieldInStream);
         if (v == null) {
           continue;
         }
         v.setStorageType(IValueMeta.STORAGE_TYPE_NORMAL);
-        int paddingType = getPaddingType()[i];
-        if (paddingType == PADDING_LEFT || paddingType == PADDING_RIGHT) {
-          int padLen = Const.toInt(variables.resolve(getPadLen()[i]), 0);
+        if (operation.paddingType == Padding.LEFT || operation.paddingType == 
Padding.RIGHT) {
+          int padLen = Const.toInt(variables.resolve(operation.padLen), 0);
           if (padLen > v.getLength()) {
             // alter meta data
             v.setLength(padLen);
@@ -561,7 +123,7 @@ public class StringOperationsMeta
   public void check(
       List<ICheckResult> remarks,
       PipelineMeta pipelineMeta,
-      TransformMeta transforminfo,
+      TransformMeta transformMeta,
       IRowMeta prev,
       String[] input,
       String[] output,
@@ -569,103 +131,139 @@ public class StringOperationsMeta
       IVariables variables,
       IHopMetadataProvider metadataProvider) {
 
-    CheckResult cr;
-    String errorMessage = "";
-    boolean first = true;
-    boolean errorFound = false;
+    if (checkNoInputReceived(remarks, transformMeta, prev)) {
+      return;
+    }
+    StringBuilder errorMessage = checkMissingFields(remarks, transformMeta, 
prev);
+    checkAllFieldsAreStrings(remarks, transformMeta, prev, errorMessage);
+    checkMissingInputFields(remarks, transformMeta);
+    checkDistinctInputFields(remarks, transformMeta);
+  }
 
+  private static boolean checkNoInputReceived(
+      List<ICheckResult> remarks, TransformMeta transformMeta, IRowMeta prev) {
+    CheckResult cr;
     if (prev == null) {
-
-      errorMessage +=
-          BaseMessages.getString(PKG, 
"StringOperationsMeta.CheckResult.NoInputReceived")
-              + Const.CR;
-      cr = new CheckResult(ICheckResult.TYPE_RESULT_ERROR, errorMessage, 
transforminfo);
+      cr =
+          new CheckResult(
+              ICheckResult.TYPE_RESULT_ERROR,
+              BaseMessages.getString(PKG, 
"StringOperationsMeta.CheckResult.NoInputReceived")
+                  + Const.CR,
+              transformMeta);
       remarks.add(cr);
-    } else {
+      // Nothing more to do.
+      //
+      return true;
+    }
+    return false;
+  }
 
-      for (String field : fieldInStream) {
-        IValueMeta v = prev.searchValueMeta(field);
-        if (v == null) {
-          if (first) {
-            first = false;
-            errorMessage +=
-                BaseMessages.getString(
-                        PKG, 
"StringOperationsMeta.CheckResult.MissingInStreamFields")
-                    + Const.CR;
-          }
-          errorFound = true;
-          errorMessage += "\t\t" + field + Const.CR;
-        }
-      }
-      if (errorFound) {
-        cr = new CheckResult(ICheckResult.TYPE_RESULT_ERROR, errorMessage, 
transforminfo);
-      } else {
+  private void checkDistinctInputFields(List<ICheckResult> remarks, 
TransformMeta transformMeta) {
+    CheckResult cr;
+    // Check if all input fields are distinct.
+    Set<String> inFields = new HashSet<>();
+    for (StringOperation operation : operations) {
+      if (inFields.contains(operation.fieldInStream)) {
         cr =
             new CheckResult(
-                ICheckResult.TYPE_RESULT_OK,
-                BaseMessages.getString(PKG, 
"StringOperationsMeta.CheckResult.FoundInStreamFields"),
-                transforminfo);
-      }
-      remarks.add(cr);
-
-      // Check whether all are strings
-      first = true;
-      errorFound = false;
-      for (String field : fieldInStream) {
-        IValueMeta v = prev.searchValueMeta(field);
-        if (v != null && v.getType() != IValueMeta.TYPE_STRING) {
-          if (first) {
-            first = false;
-            errorMessage +=
+                ICheckResult.TYPE_RESULT_ERROR,
                 BaseMessages.getString(
-                        PKG, 
"StringOperationsMeta.CheckResult.OperationOnNonStringFields")
-                    + Const.CR;
-          }
-          errorFound = true;
-          errorMessage += "\t\t" + field + Const.CR;
-        }
+                    PKG,
+                    "StringOperationsMeta.CheckResult.FieldInputError",
+                    operation.fieldInStream),
+                transformMeta);
+        remarks.add(cr);
       }
-      if (errorFound) {
-        cr = new CheckResult(ICheckResult.TYPE_RESULT_ERROR, errorMessage, 
transforminfo);
-      } else {
+      inFields.add(operation.fieldInStream);
+    }
+  }
+
+  private void checkMissingInputFields(List<ICheckResult> remarks, 
TransformMeta transformMeta) {
+    CheckResult cr;
+    int idx = 1;
+    for (StringOperation operation : operations) {
+      if (Utils.isEmpty(operation.fieldInStream)) {
         cr =
             new CheckResult(
-                ICheckResult.TYPE_RESULT_OK,
+                ICheckResult.TYPE_RESULT_ERROR,
                 BaseMessages.getString(
-                    PKG, 
"StringOperationsMeta.CheckResult.AllOperationsOnStringFields"),
-                transforminfo);
+                    PKG,
+                    "StringOperationsMeta.CheckResult.InStreamFieldMissing",
+                    Integer.toString(idx)),
+                transformMeta);
+        remarks.add(cr);
       }
-      remarks.add(cr);
+      idx++;
+    }
+  }
 
-      if (fieldInStream.length > 0) {
-        for (int idx = 0; idx < fieldInStream.length; idx++) {
-          if (Utils.isEmpty(fieldInStream[idx])) {
-            cr =
-                new CheckResult(
-                    ICheckResult.TYPE_RESULT_ERROR,
-                    BaseMessages.getString(
-                        PKG,
-                        
"StringOperationsMeta.CheckResult.InStreamFieldMissing",
-                        Integer.toString(idx + 1)),
-                    transforminfo);
-            remarks.add(cr);
-          }
+  private void checkAllFieldsAreStrings(
+      List<ICheckResult> remarks,
+      TransformMeta transformMeta,
+      IRowMeta prev,
+      StringBuilder errorMessage) {
+    boolean first;
+    boolean errorFound;
+    CheckResult cr;
+    // Check whether all are strings
+    first = true;
+    errorFound = false;
+    for (StringOperation operation : operations) {
+      IValueMeta v = prev.searchValueMeta(operation.fieldInStream);
+      if (v != null && v.getType() != IValueMeta.TYPE_STRING) {
+        if (first) {
+          first = false;
+          errorMessage
+              .append(
+                  BaseMessages.getString(
+                      PKG, 
"StringOperationsMeta.CheckResult.OperationOnNonStringFields"))
+              .append(Const.CR);
         }
+        errorFound = true;
+        
errorMessage.append("\t\t").append(operation.fieldInStream).append(Const.CR);
       }
+    }
+    if (errorFound) {
+      cr = new CheckResult(ICheckResult.TYPE_RESULT_ERROR, 
errorMessage.toString(), transformMeta);
+    } else {
+      cr =
+          new CheckResult(
+              ICheckResult.TYPE_RESULT_OK,
+              BaseMessages.getString(
+                  PKG, 
"StringOperationsMeta.CheckResult.AllOperationsOnStringFields"),
+              transformMeta);
+    }
+    remarks.add(cr);
+  }
 
-      // Check if all input fields are distinct.
-      for (int idx = 0; idx < fieldInStream.length; idx++) {
-        for (int jdx = 0; jdx < fieldInStream.length; jdx++) {
-          if (fieldInStream[idx].equals(fieldInStream[jdx]) && idx != jdx && 
idx < jdx) {
-            errorMessage =
+  private @NotNull StringBuilder checkMissingFields(
+      List<ICheckResult> remarks, TransformMeta transformMeta, IRowMeta prev) {
+    CheckResult cr;
+    boolean errorFound = false;
+    StringBuilder errorMessage = new StringBuilder();
+    for (StringOperation operation : operations) {
+      IValueMeta v = prev.searchValueMeta(operation.fieldInStream);
+      if (v == null) {
+        errorMessage
+            .append(
                 BaseMessages.getString(
-                    PKG, "StringOperationsMeta.CheckResult.FieldInputError", 
fieldInStream[idx]);
-            cr = new CheckResult(ICheckResult.TYPE_RESULT_ERROR, errorMessage, 
transforminfo);
-            remarks.add(cr);
-          }
-        }
+                    PKG, 
"StringOperationsMeta.CheckResult.MissingInStreamFields"))
+            .append(Const.CR);
       }
+      errorFound = true;
+      
errorMessage.append("\t\t").append(operation.fieldInStream).append(Const.CR);
     }
+    if (errorFound) {
+      cr = new CheckResult(ICheckResult.TYPE_RESULT_ERROR, 
errorMessage.toString(), transformMeta);
+    } else {
+      cr =
+          new CheckResult(
+              ICheckResult.TYPE_RESULT_OK,
+              BaseMessages.getString(PKG, 
"StringOperationsMeta.CheckResult.FoundInStreamFields"),
+              transformMeta);
+    }
+    remarks.add(cr);
+    return errorMessage;
   }
 
   @Override
@@ -673,297 +271,288 @@ public class StringOperationsMeta
     return true;
   }
 
-  private static String getTrimTypeCode(int i) {
-    if (i < 0 || i >= trimTypeCode.length) {
-      return trimTypeCode[0];
-    }
-    return trimTypeCode[i];
-  }
-
-  private static String getLowerUpperCode(int i) {
-    if (i < 0 || i >= lowerUpperCode.length) {
-      return lowerUpperCode[0];
-    }
-    return lowerUpperCode[i];
-  }
-
-  private static String getInitCapCode(int i) {
-    if (i < 0 || i >= initCapCode.length) {
-      return initCapCode[0];
-    }
-    return initCapCode[i];
-  }
-
-  private static String getMaskXMLCode(int i) {
-    if (i < 0 || i >= maskXMLCode.length) {
-      return maskXMLCode[0];
-    }
-    return maskXMLCode[i];
-  }
-
-  private static String getDigitsCode(int i) {
-    if (i < 0 || i >= digitsCode.length) {
-      return digitsCode[0];
-    }
-    return digitsCode[i];
-  }
-
-  private static String getRemoveSpecialCharactersCode(int i) {
-    if (i < 0 || i >= removeSpecialCharactersCode.length) {
-      return removeSpecialCharactersCode[0];
-    }
-    return removeSpecialCharactersCode[i];
-  }
-
-  private static String getPaddingCode(int i) {
-    if (i < 0 || i >= paddingCode.length) {
-      return paddingCode[0];
-    }
-    return paddingCode[i];
-  }
-
-  public static String getTrimTypeDesc(int i) {
-    if (i < 0 || i >= trimTypeDesc.length) {
-      return trimTypeDesc[0];
-    }
-    return trimTypeDesc[i];
-  }
-
-  public static String getLowerUpperDesc(int i) {
-    if (i < 0 || i >= lowerUpperDesc.length) {
-      return lowerUpperDesc[0];
-    }
-    return lowerUpperDesc[i];
-  }
-
-  public static String getInitCapDesc(int i) {
-    if (i < 0 || i >= initCapDesc.length) {
-      return initCapDesc[0];
-    }
-    return initCapDesc[i];
-  }
-
-  public static String getMaskXMLDesc(int i) {
-    if (i < 0 || i >= maskXMLDesc.length) {
-      return maskXMLDesc[0];
-    }
-    return maskXMLDesc[i];
-  }
-
-  public static String getDigitsDesc(int i) {
-    if (i < 0 || i >= digitsDesc.length) {
-      return digitsDesc[0];
-    }
-    return digitsDesc[i];
-  }
-
-  public static String getRemoveSpecialCharactersDesc(int i) {
-    if (i < 0 || i >= removeSpecialCharactersDesc.length) {
-      return removeSpecialCharactersDesc[0];
-    }
-    return removeSpecialCharactersDesc[i];
-  }
+  @Getter
+  @Setter
+  public static class StringOperation {
+    /** which field in input stream to compare with? */
+    @HopMetadataProperty(
+        key = "in_stream_name",
+        injectionKey = "SOURCEFIELDS",
+        injectionKeyDescription = 
"StringOperationsDialog.Injection.SOURCEFIELDS")
+    private String fieldInStream;
+
+    /** output field */
+    @HopMetadataProperty(
+        key = "out_stream_name",
+        injectionKey = "TARGETFIELDS",
+        injectionKeyDescription = 
"StringOperationsDialog.Injection.TARGETFIELDS")
+    private String fieldOutStream;
+
+    /** Trim type */
+    @HopMetadataProperty(
+        key = "trim_type",
+        storeWithCode = true,
+        injectionKey = "TRIMTYPE",
+        injectionKeyDescription = "StringOperationsDialog.Injection.TRIMTYPE")
+    private TrimType trimType;
+
+    /** Lower/Upper type */
+    @HopMetadataProperty(
+        key = "lower_upper",
+        storeWithCode = true,
+        injectionKey = "LOWERUPPER",
+        injectionKeyDescription = 
"StringOperationsDialog.Injection.LOWERUPPER")
+    private LowerUpper lowerUpper;
+
+    /** InitCap */
+    @HopMetadataProperty(
+        key = "init_cap",
+        storeWithCode = true,
+        injectionKey = "INITCAP",
+        injectionKeyDescription = "StringOperationsDialog.Injection.INITCAP")
+    private InitCap initCap;
+
+    @HopMetadataProperty(
+        key = "mask_xml",
+        storeWithCode = true,
+        injectionKey = "MASKXML",
+        injectionKeyDescription = "StringOperationsDialog.Injection.MASKXML")
+    private MaskXml maskXml;
+
+    @HopMetadataProperty(
+        key = "digits",
+        storeWithCode = true,
+        injectionKey = "DIGITS",
+        injectionKeyDescription = "StringOperationsDialog.Injection.DIGITS")
+    private Digits digits;
+
+    @HopMetadataProperty(
+        key = "remove_special_characters",
+        storeWithCode = true,
+        injectionKey = "SPECIALCHARS",
+        injectionKeyDescription = 
"StringOperationsDialog.Injection.SPECIALCHARS")
+    private RemoveSpecialChars removeSpecialChars;
+
+    /** padding type */
+    @HopMetadataProperty(
+        key = "padding_type",
+        storeWithCode = true,
+        injectionKey = "PADDING",
+        injectionKeyDescription = "StringOperationsDialog.Injection.PADDING")
+    private Padding paddingType;
+
+    /** Pad length */
+    @HopMetadataProperty(
+        key = "pad_len",
+        injectionKey = "PADLEN",
+        injectionKeyDescription = "StringOperationsDialog.Injection.PADLEN")
+    private String padLen;
+
+    @HopMetadataProperty(
+        key = "pad_char",
+        injectionKey = "PADCHAR",
+        injectionKeyDescription = "StringOperationsDialog.Injection.PADCHAR")
+    private String padChar;
+
+    public StringOperation() {
+      this.trimType = TrimType.NONE;
+      this.lowerUpper = LowerUpper.NONE;
+      this.initCap = InitCap.NO;
+      this.maskXml = MaskXml.NONE;
+      this.paddingType = Padding.NONE;
+      this.removeSpecialChars = RemoveSpecialChars.NONE;
+      this.digits = Digits.NONE;
+    }
+
+    public StringOperation(StringOperation op) {
+      this();
+      this.digits = op.digits;
+      this.fieldInStream = op.fieldInStream;
+      this.fieldOutStream = op.fieldOutStream;
+      this.initCap = op.initCap;
+      this.lowerUpper = op.lowerUpper;
+      this.maskXml = op.maskXml;
+      this.padChar = op.padChar;
+      this.paddingType = op.paddingType;
+      this.padLen = op.padLen;
+      this.removeSpecialChars = op.removeSpecialChars;
+      this.trimType = op.trimType;
+    }
+  }
+
+  @Getter
+  public enum TrimType implements IEnumHasCodeAndDescription {
+    NONE(ValueMetaBase.trimTypeCode[0], ValueMetaBase.trimTypeDesc[0]),
+    LEFT(ValueMetaBase.trimTypeCode[1], ValueMetaBase.trimTypeDesc[1]),
+    RIGHT(ValueMetaBase.trimTypeCode[2], ValueMetaBase.trimTypeDesc[2]),
+    BOTH(ValueMetaBase.trimTypeCode[3], ValueMetaBase.trimTypeDesc[3]),
+    ;
+    private final String code;
+    private final String description;
+
+    TrimType(String code, String description) {
+      this.code = code;
+      this.description = description;
+    }
 
-  public static String getPaddingDesc(int i) {
-    if (i < 0 || i >= paddingDesc.length) {
-      return paddingDesc[0];
+    public static String[] getDescriptions() {
+      return IEnumHasCodeAndDescription.getDescriptions(TrimType.class);
     }
-    return paddingDesc[i];
-  }
 
-  private static int getTrimTypeByCode(String tt) {
-    if (tt == null) {
-      return 0;
+    public static TrimType lookupDescription(String description) {
+      return IEnumHasCodeAndDescription.lookupDescription(TrimType.class, 
description, NONE);
     }
-
-    for (int i = 0; i < trimTypeCode.length; i++) {
-      if (trimTypeCode[i].equalsIgnoreCase(tt)) {
-        return i;
-      }
-    }
-    return 0;
   }
 
-  private static int getLowerUpperByCode(String tt) {
-    if (tt == null) {
-      return 0;
-    }
+  @Getter
+  public enum LowerUpper implements IEnumHasCodeAndDescription {
+    NONE("none", BaseMessages.getString(PKG, 
"StringOperationsMeta.LowerUpper.None")),
+    LOWER("lower", BaseMessages.getString(PKG, 
"StringOperationsMeta.LowerUpper.Lower")),
+    UPPER("upper", BaseMessages.getString(PKG, 
"StringOperationsMeta.LowerUpper.Upper")),
+    ;
+    private final String code;
+    private final String description;
 
-    for (int i = 0; i < lowerUpperCode.length; i++) {
-      if (lowerUpperCode[i].equalsIgnoreCase(tt)) {
-        return i;
-      }
+    LowerUpper(String code, String description) {
+      this.code = code;
+      this.description = description;
     }
-    return 0;
-  }
 
-  private static int getInitCapByCode(String tt) {
-    if (tt == null) {
-      return 0;
+    public static String[] getDescriptions() {
+      return IEnumHasCodeAndDescription.getDescriptions(LowerUpper.class);
     }
 
-    for (int i = 0; i < initCapCode.length; i++) {
-      if (initCapCode[i].equalsIgnoreCase(tt)) {
-        return i;
-      }
+    public static LowerUpper lookupDescription(String description) {
+      return IEnumHasCodeAndDescription.lookupDescription(LowerUpper.class, 
description, NONE);
     }
-    return 0;
   }
 
-  private static int getMaskXMLByCode(String tt) {
-    if (tt == null) {
-      return 0;
-    }
+  @Getter
+  public enum InitCap implements IEnumHasCodeAndDescription {
+    NO("no", BaseMessages.getString("System.Combo.No")),
+    YES("yes", BaseMessages.getString("System.Combo.Yes")),
+    ;
+    private final String code;
+    private final String description;
 
-    for (int i = 0; i < maskXMLCode.length; i++) {
-      if (maskXMLCode[i].equalsIgnoreCase(tt)) {
-        return i;
-      }
+    InitCap(String code, String description) {
+      this.code = code;
+      this.description = description;
     }
-    return 0;
-  }
 
-  private static int getDigitsByCode(String tt) {
-    if (tt == null) {
-      return 0;
+    public static String[] getDescriptions() {
+      return IEnumHasCodeAndDescription.getDescriptions(InitCap.class);
     }
 
-    for (int i = 0; i < digitsCode.length; i++) {
-      if (digitsCode[i].equalsIgnoreCase(tt)) {
-        return i;
-      }
+    public static InitCap lookupDescription(String description) {
+      return IEnumHasCodeAndDescription.lookupDescription(InitCap.class, 
description, NO);
     }
-    return 0;
   }
 
-  private static int getRemoveSpecialCharactersByCode(String tt) {
-    if (tt == null) {
-      return 0;
-    }
+  @Getter
+  public enum Digits implements IEnumHasCodeAndDescription {
+    NONE("none", BaseMessages.getString(PKG, 
"StringOperationsMeta.Digits.None")),
+    DIGITS_ONLY("digits_only", BaseMessages.getString(PKG, 
"StringOperationsMeta.Digits.Only")),
+    DIGITS_REMOVE(
+        "remove_digits", BaseMessages.getString(PKG, 
"StringOperationsMeta.Digits.Remove")),
+    ;
+    private final String code;
+    private final String description;
 
-    for (int i = 0; i < removeSpecialCharactersCode.length; i++) {
-      if (removeSpecialCharactersCode[i].equalsIgnoreCase(tt)) {
-        return i;
-      }
+    Digits(String code, String description) {
+      this.code = code;
+      this.description = description;
     }
-    return 0;
-  }
 
-  private static int getPaddingByCode(String tt) {
-    if (tt == null) {
-      return 0;
+    public static String[] getDescriptions() {
+      return IEnumHasCodeAndDescription.getDescriptions(Digits.class);
     }
 
-    for (int i = 0; i < paddingCode.length; i++) {
-      if (paddingCode[i].equalsIgnoreCase(tt)) {
-        return i;
-      }
+    public static Digits lookupDescription(String description) {
+      return IEnumHasCodeAndDescription.lookupDescription(Digits.class, 
description, NONE);
     }
-    return 0;
   }
 
-  public static int getTrimTypeByDesc(String tt) {
-    if (tt == null) {
-      return 0;
-    }
-
-    for (int i = 0; i < trimTypeDesc.length; i++) {
-      if (trimTypeDesc[i].equalsIgnoreCase(tt)) {
-        return i;
-      }
-    }
-
-    // If this fails, try to match using the code.
-    return getTrimTypeByCode(tt);
-  }
+  @Getter
+  public enum MaskXml implements IEnumHasCodeAndDescription {
+    NONE("none", BaseMessages.getString(PKG, 
"StringOperationsMeta.MaskXML.None")),
+    ESCAPE_XML("escapexml", BaseMessages.getString(PKG, 
"StringOperationsMeta.MaskXML.EscapeXML")),
+    CDATA("cdata", BaseMessages.getString(PKG, 
"StringOperationsMeta.MaskXML.CDATA")),
+    UNESCAPE_XML(
+        "unescapexml", BaseMessages.getString(PKG, 
"StringOperationsMeta.MaskXML.UnEscapeXML")),
+    ESCAPE_SQL("escapesql", BaseMessages.getString(PKG, 
"StringOperationsMeta.MaskXML.EscapeSQL")),
+    ESCAPE_HTML(
+        "escapehtml", BaseMessages.getString(PKG, 
"StringOperationsMeta.MaskXML.EscapeHTML")),
+    UNESCAPE_HTML(
+        "unescapehtml", BaseMessages.getString(PKG, 
"StringOperationsMeta.MaskXML.UnEscapeHTML")),
+    ;
 
-  public static int getLowerUpperByDesc(String tt) {
-    if (tt == null) {
-      return 0;
-    }
+    private final String code;
+    private final String description;
 
-    for (int i = 0; i < lowerUpperDesc.length; i++) {
-      if (lowerUpperDesc[i].equalsIgnoreCase(tt)) {
-        return i;
-      }
+    MaskXml(String code, String description) {
+      this.code = code;
+      this.description = description;
     }
 
-    // If this fails, try to match using the code.
-    return getLowerUpperByCode(tt);
-  }
-
-  public static int getInitCapByDesc(String tt) {
-    if (tt == null) {
-      return 0;
+    public static String[] getDescriptions() {
+      return IEnumHasCodeAndDescription.getDescriptions(MaskXml.class);
     }
 
-    for (int i = 0; i < initCapDesc.length; i++) {
-      if (initCapDesc[i].equalsIgnoreCase(tt)) {
-        return i;
-      }
+    public static MaskXml lookupDescription(String description) {
+      return IEnumHasCodeAndDescription.lookupDescription(MaskXml.class, 
description, NONE);
     }
-
-    // If this fails, try to match using the code.
-    return getInitCapByCode(tt);
   }
 
-  public static int getMaskXMLByDesc(String tt) {
-    if (tt == null) {
-      return 0;
-    }
+  @Getter
+  public enum RemoveSpecialChars implements IEnumHasCodeAndDescription {
+    NONE("none", BaseMessages.getString(PKG, 
"StringOperationsMeta.RemoveSpecialCharacters.None")),
+    CR("cr", BaseMessages.getString(PKG, 
"StringOperationsMeta.RemoveSpecialCharacters.CR")),
+    LF("lf", BaseMessages.getString(PKG, 
"StringOperationsMeta.RemoveSpecialCharacters.LF")),
+    CRLF("crlf", BaseMessages.getString(PKG, 
"StringOperationsMeta.RemoveSpecialCharacters.CRLF")),
+    TAB("tab", BaseMessages.getString(PKG, 
"StringOperationsMeta.RemoveSpecialCharacters.TAB")),
+    SPACE(
+        "espace",
+        BaseMessages.getString(PKG, 
"StringOperationsMeta.RemoveSpecialCharacters.Space")),
+    ;
+    private final String code;
+    private final String description;
 
-    for (int i = 0; i < maskXMLDesc.length; i++) {
-      if (maskXMLDesc[i].equalsIgnoreCase(tt)) {
-        return i;
-      }
+    RemoveSpecialChars(String code, String description) {
+      this.code = code;
+      this.description = description;
     }
 
-    // If this fails, try to match using the code.
-    return getMaskXMLByCode(tt);
-  }
-
-  public static int getDigitsByDesc(String tt) {
-    if (tt == null) {
-      return 0;
+    public static String[] getDescriptions() {
+      return 
IEnumHasCodeAndDescription.getDescriptions(RemoveSpecialChars.class);
     }
 
-    for (int i = 0; i < digitsDesc.length; i++) {
-      if (digitsDesc[i].equalsIgnoreCase(tt)) {
-        return i;
-      }
+    public static RemoveSpecialChars lookupDescription(String description) {
+      return IEnumHasCodeAndDescription.lookupDescription(
+          RemoveSpecialChars.class, description, NONE);
     }
-
-    // If this fails, try to match using the code.
-    return getDigitsByCode(tt);
   }
 
-  public static int getRemoveSpecialCharactersByDesc(String tt) {
-    if (tt == null) {
-      return 0;
-    }
+  @Getter
+  public enum Padding implements IEnumHasCodeAndDescription {
+    NONE("none", BaseMessages.getString(PKG, 
"StringOperationsMeta.Padding.None")),
+    LEFT("left", BaseMessages.getString(PKG, 
"StringOperationsMeta.Padding.Left")),
+    RIGHT("right", BaseMessages.getString(PKG, 
"StringOperationsMeta.Padding.Right")),
+    ;
+    private final String code;
+    private final String description;
 
-    for (int i = 0; i < removeSpecialCharactersDesc.length; i++) {
-      if (removeSpecialCharactersDesc[i].equalsIgnoreCase(tt)) {
-        return i;
-      }
+    Padding(String code, String description) {
+      this.code = code;
+      this.description = description;
     }
 
-    // If this fails, try to match using the code.
-    return getRemoveSpecialCharactersByCode(tt);
-  }
-
-  public static int getPaddingByDesc(String tt) {
-    if (tt == null) {
-      return 0;
+    public static String[] getDescriptions() {
+      return IEnumHasCodeAndDescription.getDescriptions(Padding.class);
     }
 
-    for (int i = 0; i < paddingDesc.length; i++) {
-      if (paddingDesc[i].equalsIgnoreCase(tt)) {
-        return i;
-      }
+    public static Padding lookupDescription(String description) {
+      return IEnumHasCodeAndDescription.lookupDescription(Padding.class, 
description, NONE);
     }
-
-    // If this fails, try to match using the code.
-    return getPaddingByCode(tt);
   }
 }
diff --git 
a/plugins/transforms/stringoperations/src/test/java/org/apache/hop/pipeline/transforms/stringoperations/StringOperationsMetaTest.java
 
b/plugins/transforms/stringoperations/src/test/java/org/apache/hop/pipeline/transforms/stringoperations/StringOperationsMetaTest.java
index b69af50f9f..f1f1a47049 100644
--- 
a/plugins/transforms/stringoperations/src/test/java/org/apache/hop/pipeline/transforms/stringoperations/StringOperationsMetaTest.java
+++ 
b/plugins/transforms/stringoperations/src/test/java/org/apache/hop/pipeline/transforms/stringoperations/StringOperationsMetaTest.java
@@ -18,136 +18,151 @@
 package org.apache.hop.pipeline.transforms.stringoperations;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.mockito.Mockito.mock;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+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 org.apache.hop.core.HopEnvironment;
-import org.apache.hop.core.exception.HopException;
-import org.apache.hop.core.plugins.PluginRegistry;
-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.junit.rules.RestoreHopEngineEnvironmentExtension;
-import org.apache.hop.pipeline.transform.ITransformMeta;
-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.junit.jupiter.api.BeforeEach;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Objects;
+import org.apache.commons.lang.StringUtils;
+import org.apache.hop.core.xml.XmlHandler;
+import org.apache.hop.metadata.serializer.memory.MemoryMetadataProvider;
+import org.apache.hop.metadata.serializer.xml.XmlMetadataUtil;
+import org.apache.hop.pipeline.transform.TransformMeta;
 import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.RegisterExtension;
 
-/** User: Dzmitry Stsiapanau Date: 2/3/14 Time: 5:41 PM */
-class StringOperationsMetaTest implements IInitializer<ITransformMeta> {
-  LoadSaveTester loadSaveTester;
-  Class<StringOperationsMeta> testMetaClass = StringOperationsMeta.class;
-
-  @RegisterExtension
-  static RestoreHopEngineEnvironmentExtension env = new 
RestoreHopEngineEnvironmentExtension();
-
-  @BeforeEach
-  void setUpLoadSave() throws Exception {
-    HopEnvironment.init();
-    PluginRegistry.init();
-    List<String> attributes =
-        Arrays.asList(
-            "padLen",
-            "padChar",
-            "fieldInStream",
-            "fieldOutStream",
-            "trimType",
-            "lowerUpper",
-            "initCap",
-            "maskXML",
-            "digits",
-            "removeSpecialCharacters",
-            "paddingType");
+class StringOperationsMetaTest {
+  @Test
+  void testLoadSave() throws Exception {
+    Path path = 
Paths.get(Objects.requireNonNull(getClass().getResource("/transform.xml")).toURI());
+    String xml = Files.readString(path);
+    StringOperationsMeta meta = new StringOperationsMeta();
+    XmlMetadataUtil.deSerializeFromXml(
+        XmlHandler.loadXmlString(xml, TransformMeta.XML_TAG),
+        StringOperationsMeta.class,
+        meta,
+        new MemoryMetadataProvider());
 
-    IFieldLoadSaveValidator<String[]> stringArrayLoadSaveValidator =
-        new ArrayLoadSaveValidator<>(new StringLoadSaveValidator(), 5);
+    validate(meta);
 
-    Map<String, IFieldLoadSaveValidator<?>> attrValidatorMap = new HashMap<>();
-    attrValidatorMap.put("padLen", stringArrayLoadSaveValidator);
-    attrValidatorMap.put("padChar", stringArrayLoadSaveValidator);
-    attrValidatorMap.put("fieldInStream", stringArrayLoadSaveValidator);
-    attrValidatorMap.put("fieldOutStream", stringArrayLoadSaveValidator);
-    attrValidatorMap.put(
-        "trimType", new PrimitiveIntArrayLoadSaveValidator(new 
IntLoadSaveValidator(4), 5));
-    attrValidatorMap.put(
-        "lowerUpper",
-        new PrimitiveIntArrayLoadSaveValidator(
-            new 
IntLoadSaveValidator(StringOperationsMeta.lowerUpperCode.length), 5));
-    attrValidatorMap.put(
-        "initCap",
-        new PrimitiveIntArrayLoadSaveValidator(
-            new IntLoadSaveValidator(StringOperationsMeta.initCapCode.length), 
5));
-    attrValidatorMap.put(
-        "maskXML",
-        new PrimitiveIntArrayLoadSaveValidator(
-            new IntLoadSaveValidator(StringOperationsMeta.maskXMLCode.length), 
5));
-    attrValidatorMap.put(
-        "digits",
-        new PrimitiveIntArrayLoadSaveValidator(
-            new IntLoadSaveValidator(StringOperationsMeta.digitsCode.length), 
5));
-    attrValidatorMap.put(
-        "removeSpecialCharacters",
-        new PrimitiveIntArrayLoadSaveValidator(
-            new 
IntLoadSaveValidator(StringOperationsMeta.removeSpecialCharactersCode.length), 
5));
-    attrValidatorMap.put(
-        "paddingType",
-        new PrimitiveIntArrayLoadSaveValidator(
-            new IntLoadSaveValidator(StringOperationsMeta.paddingCode.length), 
5));
+    // Do a round trip:
+    //
+    String xmlCopy =
+        XmlHandler.openTag(TransformMeta.XML_TAG)
+            + XmlMetadataUtil.serializeObjectToXml(meta)
+            + XmlHandler.closeTag(TransformMeta.XML_TAG);
+    StringOperationsMeta metaCopy = new StringOperationsMeta();
+    XmlMetadataUtil.deSerializeFromXml(
+        XmlHandler.loadXmlString(xmlCopy, TransformMeta.XML_TAG),
+        StringOperationsMeta.class,
+        metaCopy,
+        new MemoryMetadataProvider());
+    validate(metaCopy);
+  }
 
-    Map<String, IFieldLoadSaveValidator<?>> typeValidatorMap = new HashMap<>();
+  private static void validate(StringOperationsMeta meta) {
+    assertNotNull(meta.getOperations());
+    assertFalse(meta.getOperations().isEmpty());
+    assertEquals(4, meta.getOperations().size());
+    StringOperationsMeta.StringOperation o1 = meta.getOperations().get(0);
+    assertEquals("desc_t", o1.getFieldInStream());
+    assertEquals("desc_trimmed", o1.getFieldOutStream());
+    assertEquals(StringOperationsMeta.TrimType.BOTH, o1.getTrimType());
+    assertEquals(StringOperationsMeta.LowerUpper.NONE, o1.getLowerUpper());
+    assertEquals(StringOperationsMeta.Padding.NONE, o1.getPaddingType());
+    assertTrue(StringUtils.isEmpty(o1.getPadChar()));
+    assertTrue(StringUtils.isEmpty(o1.getPadLen()));
+    assertEquals(StringOperationsMeta.InitCap.NO, o1.getInitCap());
+    assertEquals(StringOperationsMeta.MaskXml.NONE, o1.getMaskXml());
+    assertEquals(StringOperationsMeta.Digits.NONE, o1.getDigits());
+    assertEquals(StringOperationsMeta.RemoveSpecialChars.NONE, 
o1.getRemoveSpecialChars());
 
-    loadSaveTester =
-        new LoadSaveTester(
-            testMetaClass,
-            attributes,
-            new HashMap<>(),
-            new HashMap<>(),
-            attrValidatorMap,
-            typeValidatorMap,
-            this);
-  }
+    StringOperationsMeta.StringOperation o2 = meta.getOperations().get(1);
+    assertEquals("desc_u", o2.getFieldInStream());
+    assertEquals("desc_upper", o2.getFieldOutStream());
+    assertEquals(StringOperationsMeta.TrimType.NONE, o2.getTrimType());
+    assertEquals(StringOperationsMeta.LowerUpper.UPPER, o2.getLowerUpper());
+    assertEquals(StringOperationsMeta.Padding.NONE, o2.getPaddingType());
+    assertTrue(StringUtils.isEmpty(o2.getPadChar()));
+    assertTrue(StringUtils.isEmpty(o2.getPadLen()));
+    assertEquals(StringOperationsMeta.InitCap.NO, o2.getInitCap());
+    assertEquals(StringOperationsMeta.MaskXml.NONE, o2.getMaskXml());
+    assertEquals(StringOperationsMeta.Digits.NONE, o2.getDigits());
+    assertEquals(StringOperationsMeta.RemoveSpecialChars.NONE, 
o2.getRemoveSpecialChars());
 
-  // Call the allocate method on the LoadSaveTester meta class
-  @Override
-  public void modify(ITransformMeta someMeta) {
-    if (someMeta instanceof StringOperationsMeta) {
-      ((StringOperationsMeta) someMeta).allocate(5);
-    }
-  }
+    StringOperationsMeta.StringOperation o3 = meta.getOperations().get(2);
+    assertEquals("desc_p", o3.getFieldInStream());
+    assertEquals("desc_padded", o3.getFieldOutStream());
+    assertEquals(StringOperationsMeta.TrimType.NONE, o3.getTrimType());
+    assertEquals(StringOperationsMeta.LowerUpper.NONE, o3.getLowerUpper());
+    assertEquals(StringOperationsMeta.Padding.LEFT, o3.getPaddingType());
+    assertEquals("#", o3.getPadChar());
+    assertEquals("25", o3.getPadLen());
+    assertEquals(StringOperationsMeta.InitCap.NO, o3.getInitCap());
+    assertEquals(StringOperationsMeta.MaskXml.NONE, o3.getMaskXml());
+    assertEquals(StringOperationsMeta.Digits.NONE, o3.getDigits());
+    assertEquals(StringOperationsMeta.RemoveSpecialChars.NONE, 
o3.getRemoveSpecialChars());
 
-  @Test
-  void testSerialization() throws HopException {
-    loadSaveTester.testSerialization();
+    StringOperationsMeta.StringOperation o4 = meta.getOperations().get(3);
+    assertEquals("desc_i", o4.getFieldInStream());
+    assertEquals("desc_initcapped", o4.getFieldOutStream());
+    assertEquals(StringOperationsMeta.TrimType.NONE, o4.getTrimType());
+    assertEquals(StringOperationsMeta.LowerUpper.NONE, o4.getLowerUpper());
+    assertEquals(StringOperationsMeta.Padding.NONE, o4.getPaddingType());
+    assertTrue(StringUtils.isEmpty(o2.getPadChar()));
+    assertTrue(StringUtils.isEmpty(o2.getPadLen()));
+    assertEquals(StringOperationsMeta.InitCap.YES, o4.getInitCap());
+    assertEquals(StringOperationsMeta.MaskXml.NONE, o4.getMaskXml());
+    assertEquals(StringOperationsMeta.Digits.NONE, o4.getDigits());
+    assertEquals(StringOperationsMeta.RemoveSpecialChars.NONE, 
o4.getRemoveSpecialChars());
   }
 
   @Test
-  void testGetFields() throws Exception {
+  void testLoadSave2() throws Exception {
+    Path path =
+        
Paths.get(Objects.requireNonNull(getClass().getResource("/transform2.xml")).toURI());
+    String xml = Files.readString(path);
     StringOperationsMeta meta = new StringOperationsMeta();
-    meta.allocate(1);
-    meta.setFieldInStream(new String[] {"field1"});
+    XmlMetadataUtil.deSerializeFromXml(
+        XmlHandler.loadXmlString(xml, TransformMeta.XML_TAG),
+        StringOperationsMeta.class,
+        meta,
+        new MemoryMetadataProvider());
 
-    IRowMeta iRowMeta = new RowMeta();
-    IValueMeta valueMeta = new ValueMetaString("field1");
-    valueMeta.setStorageMetadata(new ValueMetaString("field1"));
-    valueMeta.setStorageType(IValueMeta.STORAGE_TYPE_BINARY_STRING);
-    iRowMeta.addValueMeta(valueMeta);
+    validate2(meta);
+
+    // Do a round trip:
+    //
+    String xmlCopy =
+        XmlHandler.openTag(TransformMeta.XML_TAG)
+            + XmlMetadataUtil.serializeObjectToXml(meta)
+            + XmlHandler.closeTag(TransformMeta.XML_TAG);
+    StringOperationsMeta metaCopy = new StringOperationsMeta();
+    XmlMetadataUtil.deSerializeFromXml(
+        XmlHandler.loadXmlString(xmlCopy, TransformMeta.XML_TAG),
+        StringOperationsMeta.class,
+        metaCopy,
+        new MemoryMetadataProvider());
+    validate2(metaCopy);
+  }
 
-    IVariables variables = mock(IVariables.class);
-    meta.getFields(iRowMeta, "STRING_OPERATIONS", null, null, variables, null);
-    IRowMeta expectedRowMeta = new RowMeta();
-    expectedRowMeta.addValueMeta(new ValueMetaString("field1"));
-    assertEquals(expectedRowMeta.toString(), iRowMeta.toString());
+  private void validate2(StringOperationsMeta meta) {
+    assertNotNull(meta.getOperations());
+    assertFalse(meta.getOperations().isEmpty());
+    assertEquals(1, meta.getOperations().size());
+    StringOperationsMeta.StringOperation o1 = meta.getOperations().get(0);
+    assertEquals("in_field", o1.getFieldInStream());
+    assertEquals("out_field", o1.getFieldOutStream());
+    assertEquals(StringOperationsMeta.TrimType.BOTH, o1.getTrimType());
+    assertEquals(StringOperationsMeta.LowerUpper.UPPER, o1.getLowerUpper());
+    assertEquals(StringOperationsMeta.Padding.RIGHT, o1.getPaddingType());
+    assertEquals(" ", o1.getPadChar());
+    assertEquals("20", o1.getPadLen());
+    assertEquals(StringOperationsMeta.InitCap.YES, o1.getInitCap());
+    assertEquals(StringOperationsMeta.MaskXml.UNESCAPE_XML, o1.getMaskXml());
+    assertEquals(StringOperationsMeta.Digits.DIGITS_REMOVE, o1.getDigits());
+    assertEquals(StringOperationsMeta.RemoveSpecialChars.CRLF, 
o1.getRemoveSpecialChars());
   }
 }
diff --git 
a/plugins/transforms/stringoperations/src/test/java/org/apache/hop/pipeline/transforms/stringoperations/StringOperationsTest.java
 
b/plugins/transforms/stringoperations/src/test/java/org/apache/hop/pipeline/transforms/stringoperations/StringOperationsTest.java
deleted file mode 100644
index 2015abdac4..0000000000
--- 
a/plugins/transforms/stringoperations/src/test/java/org/apache/hop/pipeline/transforms/stringoperations/StringOperationsTest.java
+++ /dev/null
@@ -1,149 +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.stringoperations;
-
-import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.mock;
-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.HopValueException;
-import org.apache.hop.core.logging.ILoggingObject;
-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.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;
-
-/**
- * Tests for StringOperations transform
- *
- * @see StringOperations
- */
-class StringOperationsTest {
-  private static TransformMockHelper<StringOperationsMeta, 
StringOperationsData> smh;
-
-  @BeforeEach
-  void setup() throws Exception {
-    smh =
-        new TransformMockHelper<>(
-            "StringOperations", StringOperationsMeta.class, 
StringOperationsData.class);
-    when(smh.logChannelFactory.create(any(), any(ILoggingObject.class)))
-        .thenReturn(smh.iLogChannel);
-    when(smh.pipeline.isRunning()).thenReturn(true);
-  }
-
-  @AfterEach
-  void cleanUp() {
-    smh.cleanUp();
-  }
-
-  private IRowSet mockInputRowSet() {
-    ValueMetaString valueMeta = new ValueMetaString("Value");
-    valueMeta.setStorageType(IValueMeta.STORAGE_TYPE_BINARY_STRING);
-    valueMeta.setStorageMetadata(new ValueMetaString("Value"));
-
-    RowMeta inputRowMeta = new RowMeta();
-    inputRowMeta.addValueMeta(valueMeta);
-
-    IRowSet inputRowSet = smh.getMockInputRowSet(new Object[][] {{" Value 
".getBytes()}});
-    doReturn(inputRowMeta).when(inputRowSet).getRowMeta();
-
-    return inputRowSet;
-  }
-
-  private StringOperationsMeta mockTransformMeta() {
-    StringOperationsMeta meta = mock(StringOperationsMeta.class);
-    doReturn(new String[] {"Value"}).when(meta).getFieldInStream();
-    doReturn(new String[] {""}).when(meta).getFieldOutStream();
-    doReturn(new int[] 
{StringOperationsMeta.TRIM_BOTH}).when(meta).getTrimType();
-    doReturn(new int[] 
{StringOperationsMeta.LOWER_UPPER_NONE}).when(meta).getLowerUpper();
-    doReturn(new int[] 
{StringOperationsMeta.PADDING_NONE}).when(meta).getPaddingType();
-    doReturn(new String[] {""}).when(meta).getPadChar();
-    doReturn(new String[] {""}).when(meta).getPadLen();
-    doReturn(new int[] 
{StringOperationsMeta.INIT_CAP_NO}).when(meta).getInitCap();
-    doReturn(new int[] 
{StringOperationsMeta.MASK_NONE}).when(meta).getMaskXML();
-    doReturn(new int[] 
{StringOperationsMeta.DIGITS_NONE}).when(meta).getDigits();
-    doReturn(new int[] {StringOperationsMeta.REMOVE_SPECIAL_CHARACTERS_NONE})
-        .when(meta)
-        .getRemoveSpecialCharacters();
-
-    return meta;
-  }
-
-  private StringOperationsData mockTransformData() {
-    return mock(StringOperationsData.class);
-  }
-
-  private boolean verifyOutput(Object[][] expectedRows, IRowSet outputRowSet)
-      throws HopValueException {
-    if (expectedRows.length == outputRowSet.size()) {
-      for (Object[] expectedRow : expectedRows) {
-        Object[] row = outputRowSet.getRow();
-        if (expectedRow.length == outputRowSet.getRowMeta().size()) {
-          for (int j = 0; j < expectedRow.length; j++) {
-            if 
(!expectedRow[j].equals(outputRowSet.getRowMeta().getString(row, j))) {
-              return false;
-            }
-          }
-          return true;
-        }
-      }
-    }
-    return false;
-  }
-
-  @Test
-  @Disabled("This test needs to be reviewed")
-  void testProcessBinaryInput() throws HopException {
-    StringOperations transform =
-        new StringOperations(
-            smh.transformMeta,
-            smh.iTransformMeta,
-            smh.iTransformData,
-            0,
-            smh.pipelineMeta,
-            smh.pipeline);
-    transform.addRowSetToInputRowSets(mockInputRowSet());
-
-    IRowSet outputRowSet = new QueueRowSet();
-    transform.addRowSetToOutputRowSets(outputRowSet);
-
-    StringOperationsMeta meta = mockTransformMeta();
-    StringOperationsData data = mockTransformData();
-
-    transform.init();
-
-    boolean processResult;
-
-    do {
-      processResult = transform.init();
-    } while (processResult);
-
-    assertTrue(outputRowSet.isDone());
-
-    assertTrue(verifyOutput(new Object[][] {{"Value"}}, outputRowSet), 
"Unexpected output");
-  }
-}
diff --git 
a/plugins/transforms/stringoperations/src/test/resources/transform.xml 
b/plugins/transforms/stringoperations/src/test/resources/transform.xml
new file mode 100644
index 0000000000..341862b96b
--- /dev/null
+++ b/plugins/transforms/stringoperations/src/test/resources/transform.xml
@@ -0,0 +1,74 @@
+<!--
+  ~ 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>
+    <fields>
+        <field>
+            <in_stream_name>desc_t</in_stream_name>
+            <out_stream_name>desc_trimmed</out_stream_name>
+            <trim_type>both</trim_type>
+            <lower_upper>none</lower_upper>
+            <padding_type>none</padding_type>
+            <pad_char/>
+            <pad_len/>
+            <init_cap>no</init_cap>
+            <mask_xml>none</mask_xml>
+            <digits>none</digits>
+            <remove_special_characters>none</remove_special_characters>
+        </field>
+        <field>
+            <in_stream_name>desc_u</in_stream_name>
+            <out_stream_name>desc_upper</out_stream_name>
+            <trim_type>none</trim_type>
+            <lower_upper>upper</lower_upper>
+            <padding_type>none</padding_type>
+            <pad_char/>
+            <pad_len/>
+            <init_cap>no</init_cap>
+            <mask_xml>none</mask_xml>
+            <digits>none</digits>
+            <remove_special_characters>none</remove_special_characters>
+        </field>
+        <field>
+            <in_stream_name>desc_p</in_stream_name>
+            <out_stream_name>desc_padded</out_stream_name>
+            <trim_type>none</trim_type>
+            <lower_upper>none</lower_upper>
+            <padding_type>left</padding_type>
+            <pad_char>#</pad_char>
+            <pad_len>25</pad_len>
+            <init_cap>no</init_cap>
+            <mask_xml>none</mask_xml>
+            <digits>none</digits>
+            <remove_special_characters>none</remove_special_characters>
+        </field>
+        <field>
+            <in_stream_name>desc_i</in_stream_name>
+            <out_stream_name>desc_initcapped</out_stream_name>
+            <trim_type>none</trim_type>
+            <lower_upper>none</lower_upper>
+            <padding_type>none</padding_type>
+            <pad_char/>
+            <pad_len/>
+            <init_cap>yes</init_cap>
+            <mask_xml>none</mask_xml>
+            <digits>none</digits>
+            <remove_special_characters>none</remove_special_characters>
+        </field>
+    </fields>
+</transform>
diff --git 
a/plugins/transforms/stringoperations/src/test/resources/transform2.xml 
b/plugins/transforms/stringoperations/src/test/resources/transform2.xml
new file mode 100644
index 0000000000..7d4caaf9bc
--- /dev/null
+++ b/plugins/transforms/stringoperations/src/test/resources/transform2.xml
@@ -0,0 +1,35 @@
+<!--
+  ~ 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>
+    <fields>
+        <field>
+            <in_stream_name>in_field</in_stream_name>
+            <out_stream_name>out_field</out_stream_name>
+            <trim_type>both</trim_type>
+            <lower_upper>upper</lower_upper>
+            <padding_type>right</padding_type>
+            <pad_char> </pad_char>
+            <pad_len>20</pad_len>
+            <init_cap>yes</init_cap>
+            <mask_xml>unescapexml</mask_xml>
+            <digits>remove_digits</digits>
+            <remove_special_characters>crlf</remove_special_characters>
+        </field>
+    </fields>
+</transform>
\ No newline at end of file

Reply via email to