You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hop.apache.org by mc...@apache.org on 2026/10/09 13:15:22 UTC

(hop) branch main updated: fix(metainject): decode lazy binary-string values when injecting list items, fixes #8812 (#8813)

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

mattcasters 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 ffd3fd3736 fix(metainject): decode lazy binary-string values when injecting list items, fixes #8812 (#8813)
ffd3fd3736 is described below

commit ffd3fd3736ef1e4f76b5d36aa00ffa5795e23967
Author: Romain Manni-Bucau <rm...@gmail.com>
AuthorDate: Fri Oct 9 15:15:16 2026 +0200

    fix(metainject): decode lazy binary-string values when injecting list items, fixes #8812 (#8813)
    
    The HopMetadataInjector rewrite (4c51db7edc) copies values from injection
    row buffers into String fields and List<String> fields as-is. When the
    source is a lazy-conversion transform (e.g. CSV Input with lazy
    conversion enabled), those values are raw byte[] and end up injected as
    their toString() representation ("[B@..."), which later breaks
    SimpleDateFormat with "Illegal pattern character 'B'" when the value is
    used as a format mask (e.g. Calculator conversion_mask).
    
    Decode values with STORAGE_TYPE_BINARY_STRING to their native type via
    convertBinaryStringToNativeType() in both injectList() branches, matching
    the legacy BeanInjector path which already decoded through
    RowMetaAndData.getString()/getAsJavaType().
    
    Adds regression tests for List<String> injection and String fields of
    list items fed from lazy byte[] row buffers.
---
 .../hop/metadata/inject/HopMetadataInjector.java   | 29 +++++++++-
 .../metadata/inject/HopMetadataInjectorTest.java   | 66 ++++++++++++++++++++++
 2 files changed, 93 insertions(+), 2 deletions(-)

diff --git a/core/src/main/java/org/apache/hop/metadata/inject/HopMetadataInjector.java b/core/src/main/java/org/apache/hop/metadata/inject/HopMetadataInjector.java
index 88ed856739..17b905209a 100644
--- a/core/src/main/java/org/apache/hop/metadata/inject/HopMetadataInjector.java
+++ b/core/src/main/java/org/apache/hop/metadata/inject/HopMetadataInjector.java
@@ -614,8 +614,16 @@ public class HopMetadataInjector {
         // Is this a primitive type we're dealing with?
         //
         if (listItemClass.isPrimitive() || listItemClass.equals(String.class)) {
-          // The row contains a single value that we simply need to add to the list
-          list.add(row[0]);
+          // The row contains a single value that we simply need to add to the list.
+          // Lazy-converted sources (e.g. CSV Input with lazy conversion) store Strings as
+          // byte[]; decode them so a List<String> receives a String, not "[B@...".
+          //
+          IValueMeta singleValueMeta = rowMeta.getValueMeta(0);
+          Object value = row[0];
+          if (isLazyBinaryString(singleValueMeta, value)) {
+            value = singleValueMeta.convertBinaryStringToNativeType((byte[]) value);
+          }
+          list.add(value);
         } else {
           // Reuse the pre-defined template item at the same position so its non-injected fields
           // are preserved; only create a new item when injecting beyond the pre-defined rows.
@@ -634,6 +642,13 @@ public class HopMetadataInjector {
             String valueKey = valueMeta.getName();
             Object value = row[valueIndex];
 
+            // Lazy-converted sources (e.g. CSV Input with lazy conversion) store Strings as
+            // byte[]; decode them so String fields receive a String, not "[B@...".
+            //
+            if (isLazyBinaryString(valueMeta, value)) {
+              value = valueMeta.convertBinaryStringToNativeType((byte[]) value);
+            }
+
             // The map will contain just a single value matching the expected key in the underlying
             // object
             // class field annotation.  Only that field will be set.
@@ -659,6 +674,16 @@ public class HopMetadataInjector {
     }
   }
 
+  /**
+   * True when the value is a raw lazy-conversion byte array (binary-string storage) that should be
+   * decoded to its native type before it is injected into a String field or list.
+   */
+  private static boolean isLazyBinaryString(IValueMeta valueMeta, Object value) {
+    return valueMeta != null
+        && valueMeta.getStorageType() == IValueMeta.STORAGE_TYPE_BINARY_STRING
+        && value instanceof byte[];
+  }
+
   private static void setValue(Object object, Field field, Object valueToSet) throws Exception {
     try {
       if (valueToSet == null) {
diff --git a/core/src/test/java/org/apache/hop/metadata/inject/HopMetadataInjectorTest.java b/core/src/test/java/org/apache/hop/metadata/inject/HopMetadataInjectorTest.java
index 194c4e2a83..95dda9ebd0 100644
--- a/core/src/test/java/org/apache/hop/metadata/inject/HopMetadataInjectorTest.java
+++ b/core/src/test/java/org/apache/hop/metadata/inject/HopMetadataInjectorTest.java
@@ -24,6 +24,8 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -35,6 +37,7 @@ import org.apache.hop.core.database.NoneDatabaseMeta;
 import org.apache.hop.core.plugins.PluginRegistry;
 import org.apache.hop.core.row.IValueMeta;
 import org.apache.hop.core.row.RowBuffer;
+import org.apache.hop.core.row.RowMeta;
 import org.apache.hop.core.row.RowMetaBuilder;
 import org.apache.hop.core.row.value.ValueMetaDate;
 import org.apache.hop.core.row.value.ValueMetaInteger;
@@ -387,4 +390,67 @@ class HopMetadataInjectorTest {
       return description;
     }
   }
+
+  /**
+   * Regression test: a lazy-conversion source (e.g. CSV Input with lazy conversion enabled) stores
+   * values as raw {@code byte[]} with {@link IValueMeta#STORAGE_TYPE_BINARY_STRING}. Injecting such
+   * a value into a {@code List<String>} must decode the bytes to a String, not store the raw array
+   * (which {@code toString()} renders as {@code "[B@..."}).
+   *
+   * <p>The {@code STRING} key on {@link Company} is not a list, so this test verifies the
+   * scalar-path + list-item path via {@code EMPLOYEES/FIRST_NAME} below; this one uses a dedicated
+   * {@code List<String>} sample.
+   */
+  @Test
+  void injectLazyBinaryValueIntoStringList() throws Exception {
+    IValueMeta stringMeta = new ValueMetaString("STRINGS_ITEM");
+    stringMeta.setStorageType(IValueMeta.STORAGE_TYPE_BINARY_STRING);
+    stringMeta.setStorageMetadata(new ValueMetaString("STRINGS_ITEM"));
+
+    RowBuffer rowBuffer = new RowBuffer();
+    RowMeta rowMeta = new RowMeta();
+    rowMeta.addValueMeta(stringMeta);
+    rowBuffer.setRowMeta(rowMeta);
+    rowBuffer.addRow("lazy-value".getBytes(StandardCharsets.UTF_8));
+
+    StringListMeta meta = new StringListMeta();
+    HopMetadataInjector.inject(
+        new MemoryMetadataProvider(), meta, Map.of(), Map.of("STRINGS", rowBuffer));
+
+    assertEquals("lazy-value", meta.getStrings().get(0));
+  }
+
+  /**
+   * Regression test: same lazy-conversion scenario, but the value lands in a String field of a list
+   * item. It must be decoded to a String instead of becoming {@code byte[].toString()} ({@code
+   * "[B@..."}), which later breaks {@code SimpleDateFormat} with "Illegal pattern character 'B'".
+   */
+  @Test
+  void injectLazyBinaryValueIntoListItemStringField() throws Exception {
+    IValueMeta nameMeta = new ValueMetaString("FIRST_NAME");
+    nameMeta.setStorageType(IValueMeta.STORAGE_TYPE_BINARY_STRING);
+    nameMeta.setStorageMetadata(new ValueMetaString("FIRST_NAME"));
+
+    RowBuffer rowBuffer = new RowBuffer();
+    RowMeta rowMeta = new RowMeta();
+    rowMeta.addValueMeta(nameMeta);
+    rowBuffer.setRowMeta(rowMeta);
+    rowBuffer.addRow("Donald".getBytes(StandardCharsets.UTF_8));
+
+    Company company = new Company();
+    HopMetadataInjector.inject(
+        new MemoryMetadataProvider(), company, Map.of(), Map.of("EMPLOYEES", rowBuffer));
+
+    assertEquals("Donald", company.getEmployees().get(0).getFirstName());
+  }
+
+  /** Minimal sample with a {@code List<String>} injection group. */
+  private static class StringListMeta {
+    @HopMetadataProperty(injectionGroupKey = "STRINGS")
+    private final List<String> strings = new ArrayList<>();
+
+    public List<String> getStrings() {
+      return strings;
+    }
+  }
 }