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 1b06db5978 Issue #3051 : Harden Filter Rows date constant parsing 
(#8185)
1b06db5978 is described below

commit 1b06db59785d4f450cd3fc127804ead5d8b4f7af
Author: Matt Casters <[email protected]>
AuthorDate: Mon Aug 31 15:25:45 2026 +0200

    Issue #3051 : Harden Filter Rows date constant parsing (#8185)
    
    Parse date/timestamp condition constants with the stored mask first,
    then fall back to Hop's compatible date format so legacy pipelines
    still evaluate. Keep the condition editor open when conversion fails.
---
 .../main/java/org/apache/hop/core/Condition.java   |  64 ++++++++-
 .../java/org/apache/hop/core/ConditionTest.java    | 154 +++++++++++++++++++++
 .../transforms/filterrows/FilterRowsTest.java      |  35 +++++
 .../transforms/joinrows/JoinRowsDialog.java        |   2 +-
 .../hop/ui/core/dialog/EnterValueDialog.java       |  11 +-
 .../apache/hop/ui/core/widget/ConditionEditor.java |  12 +-
 6 files changed, 266 insertions(+), 12 deletions(-)

diff --git a/core/src/main/java/org/apache/hop/core/Condition.java 
b/core/src/main/java/org/apache/hop/core/Condition.java
index ced122d131..3ac1437544 100644
--- a/core/src/main/java/org/apache/hop/core/Condition.java
+++ b/core/src/main/java/org/apache/hop/core/Condition.java
@@ -41,6 +41,7 @@ import org.apache.hop.core.exception.HopXmlException;
 import org.apache.hop.core.row.IRowMeta;
 import org.apache.hop.core.row.IValueMeta;
 import org.apache.hop.core.row.ValueMetaAndData;
+import org.apache.hop.core.row.value.ValueMetaBase;
 import org.apache.hop.core.row.value.ValueMetaFactory;
 import org.apache.hop.core.row.value.ValueMetaString;
 import org.apache.hop.core.util.Utils;
@@ -831,6 +832,14 @@ public class Condition implements Cloneable {
       length = valueMeta.getLength();
       precision = valueMeta.getPrecision();
       mask = valueMeta.getConversionMask();
+      // Persist the mask getString() actually used so later loads do not 
depend on
+      // HOP_DEFAULT_DATE_FORMAT remaining unchanged.
+      if (valueMeta.isDate() && Utils.isEmpty(mask)) {
+        mask =
+            valueMeta.getType() == IValueMeta.TYPE_TIMESTAMP
+                ? ValueMetaBase.DEFAULT_TIMESTAMP_FORMAT_MASK
+                : ValueMetaBase.DEFAULT_DATE_FORMAT_MASK;
+      }
     }
 
     public int getHopType() {
@@ -854,23 +863,64 @@ public class Condition implements Cloneable {
     }
 
     /**
-     * Convert the text stored to the desired data type in a compatible way
+     * Convert the text stored to the desired data type in a compatible way.
      *
-     * @return
+     * <p>Date and Timestamp constants historically stored {@code text} in 
Hop's compatible date
+     * format ({@code yyyy/MM/dd HH:mm:ss.SSS}) while keeping the 
user-selected conversion mask.
+     * Parse with the stored mask first, then fall back to that compatible 
format and the type
+     * default so both new and legacy pipelines work.
      */
     public Object createValueData() throws HopException {
       if (isNullValue()) {
         return null;
       }
       IValueMeta valueMeta = createValueMeta();
-
-      ValueMetaAndData val = new ValueMetaAndData(valueMeta.getName(), text);
-      val.setValueMeta(valueMeta);
-
       IValueMeta stringValueMeta = new ValueMetaString(valueMeta.getName());
+      if (valueMeta.isDate()) {
+        return convertDateConstant(valueMeta, stringValueMeta);
+      }
+
       stringValueMeta.setConversionMetadata(valueMeta);
+      return stringValueMeta.convertDataUsingConversionMetaData(text);
+    }
 
-      return 
stringValueMeta.convertDataUsingConversionMetaData(val.getValueData());
+    private Object convertDateConstant(IValueMeta valueMeta, IValueMeta 
stringValueMeta)
+        throws HopException {
+      HopValueException firstError = null;
+      for (String tryMask : dateConstantMasks(valueMeta)) {
+        IValueMeta parseMeta = valueMeta.clone();
+        parseMeta.setConversionMask(tryMask);
+        stringValueMeta.setConversionMetadata(parseMeta);
+        try {
+          return stringValueMeta.convertDataUsingConversionMetaData(text);
+        } catch (HopValueException e) {
+          if (firstError == null) {
+            firstError = e;
+          }
+        }
+      }
+      if (firstError != null) {
+        throw firstError;
+      }
+      throw new HopValueException("Unable to convert constant [" + text + "] 
to a date");
+    }
+
+    private List<String> dateConstantMasks(IValueMeta valueMeta) {
+      List<String> masks = new ArrayList<>();
+      addDateConstantMask(masks, mask);
+      addDateConstantMask(masks, ValueMetaBase.COMPATIBLE_DATE_FORMAT_PATTERN);
+      addDateConstantMask(
+          masks,
+          valueMeta.getType() == IValueMeta.TYPE_TIMESTAMP
+              ? ValueMetaBase.DEFAULT_TIMESTAMP_FORMAT_MASK
+              : ValueMetaBase.DEFAULT_DATE_FORMAT_MASK);
+      return masks;
+    }
+
+    private void addDateConstantMask(List<String> masks, String candidate) {
+      if (StringUtils.isNotEmpty(candidate) && !masks.contains(candidate)) {
+        masks.add(candidate);
+      }
     }
   }
 
diff --git a/core/src/test/java/org/apache/hop/core/ConditionTest.java 
b/core/src/test/java/org/apache/hop/core/ConditionTest.java
index eb911d84d0..d1bd19ddb5 100644
--- a/core/src/test/java/org/apache/hop/core/ConditionTest.java
+++ b/core/src/test/java/org/apache/hop/core/ConditionTest.java
@@ -20,15 +20,26 @@ package org.apache.hop.core;
 import static org.apache.hop.core.Condition.Function;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
+import java.math.BigDecimal;
+import java.sql.Timestamp;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import org.apache.hop.core.exception.HopException;
 import org.apache.hop.core.row.IRowMeta;
 import org.apache.hop.core.row.RowMeta;
 import org.apache.hop.core.row.ValueMetaAndData;
+import org.apache.hop.core.row.value.ValueMetaBase;
+import org.apache.hop.core.row.value.ValueMetaBigNumber;
+import org.apache.hop.core.row.value.ValueMetaDate;
 import org.apache.hop.core.row.value.ValueMetaInteger;
 import org.apache.hop.core.row.value.ValueMetaNumber;
+import org.apache.hop.core.row.value.ValueMetaTimestamp;
 import org.apache.hop.core.util.TestUtil;
 import org.apache.hop.core.xml.XmlHandler;
 import org.apache.hop.junit.rules.RestoreHopEnvironmentExtension;
@@ -126,4 +137,147 @@ class ConditionTest {
     assertNull(condition.getRightValue());
     assertEquals(Function.LARGER_EQUAL, condition.getFunction());
   }
+
+  @Test
+  void dateConstantWithMatchingMaskEvaluates() throws Exception {
+    Condition condition = dateLessThanConstant("2022-01-01", "yyyy-MM-dd");
+    assertDateLessThan(condition);
+  }
+
+  @Test
+  void dateConstantWithLegacyCompatibleTextEvaluates() throws Exception {
+    // Issue #3051: text stored in the canonical Hop date format, mask is the 
user format.
+    Condition condition = dateLessThanConstant("2022/01/01 00:00:00.000", 
"yyyy-MM-dd");
+    assertDateLessThan(condition);
+  }
+
+  @Test
+  void dateConstantXmlRoundTripKeepsCustomMask() throws Exception {
+    SimpleDateFormat iso = isoDate();
+    ValueMetaDate dateMeta = new ValueMetaDate("constant");
+    dateMeta.setConversionMask("yyyy-MM-dd");
+    Condition original =
+        new Condition(
+            "date",
+            Function.SMALLER,
+            null,
+            new ValueMetaAndData(dateMeta, iso.parse("2022-01-01")));
+
+    Condition copy = new Condition(original.getXml());
+    assertEquals("yyyy-MM-dd", copy.getRightValue().getMask());
+    assertDateLessThan(copy);
+  }
+
+  @Test
+  void dateConstantConstructorTextIsParseable() throws Exception {
+    SimpleDateFormat iso = isoDate();
+    Date date = iso.parse("2022-01-01");
+    ValueMetaDate dateMeta = new ValueMetaDate("constant");
+    dateMeta.setConversionMask("yyyy-MM-dd");
+
+    Condition.CValue value = new Condition.CValue(new 
ValueMetaAndData(dateMeta, date));
+    assertEquals("yyyy-MM-dd", value.getMask());
+    assertInstanceOf(Date.class, value.createValueData());
+  }
+
+  @Test
+  void dateConstantPersistsDefaultMaskWhenMissing() throws Exception {
+    Date date = isoDate().parse("2022-01-01");
+    Condition.CValue value =
+        new Condition.CValue(new ValueMetaAndData(new 
ValueMetaDate("constant"), date));
+    assertEquals(ValueMetaBase.DEFAULT_DATE_FORMAT_MASK, value.getMask());
+    assertInstanceOf(Date.class, value.createValueData());
+  }
+
+  @Test
+  void timestampConstantWithLegacyCompatibleTextConverts() throws Exception {
+    Condition.CValue value = new Condition.CValue();
+    value.setName("constant");
+    value.setType("Timestamp");
+    value.setText("2022/01/01 12:34:56.789");
+    value.setMask("yyyy-MM-dd HH:mm:ss.SSS");
+    value.setNullValue(false);
+    value.setLength(-1);
+    value.setPrecision(-1);
+
+    assertNotNull(value.createValueData());
+  }
+
+  @Test
+  void timestampConstantConstructorTextIsParseable() throws Exception {
+    ValueMetaTimestamp timestampMeta = new ValueMetaTimestamp("constant");
+    timestampMeta.setConversionMask("yyyy-MM-dd HH:mm:ss.SSS");
+    Timestamp timestamp = Timestamp.valueOf("2022-01-01 12:34:56.789");
+
+    Condition.CValue value = new Condition.CValue(new 
ValueMetaAndData(timestampMeta, timestamp));
+    assertEquals("yyyy-MM-dd HH:mm:ss.SSS", value.getMask());
+    assertNotNull(value.createValueData());
+  }
+
+  @Test
+  void nullDateConstantStaysNull() throws Exception {
+    Condition.CValue value =
+        new Condition.CValue(new ValueMetaAndData(new 
ValueMetaDate("constant"), null));
+    assertTrue(value.isNullValue());
+    assertNull(value.createValueData());
+  }
+
+  @Test
+  void unparseableDateConstantStillFails() {
+    Condition.CValue value = new Condition.CValue();
+    value.setName("constant");
+    value.setType("Date");
+    value.setText("not-a-date");
+    value.setMask("yyyy-MM-dd");
+    value.setNullValue(false);
+    value.setLength(-1);
+    value.setPrecision(-1);
+
+    assertThrows(HopException.class, value::createValueData);
+  }
+
+  @Test
+  void integerAndBigNumberConstantsStillConvert() throws Exception {
+    Condition.CValue integer =
+        new Condition.CValue(new ValueMetaAndData(new 
ValueMetaInteger("constant"), 100L));
+    assertEquals(100L, integer.createValueData());
+
+    Condition.CValue bigNumber =
+        new Condition.CValue(
+            new ValueMetaAndData(new ValueMetaBigNumber("constant"), new 
BigDecimal("123.45")));
+    assertEquals(0, new BigDecimal("123.45").compareTo((BigDecimal) 
bigNumber.createValueData()));
+  }
+
+  private static Condition dateLessThanConstant(String text, String mask) {
+    Condition.CValue constant = new Condition.CValue();
+    constant.setName("constant");
+    constant.setType("Date");
+    constant.setText(text);
+    constant.setMask(mask);
+    constant.setNullValue(false);
+    constant.setLength(-1);
+    constant.setPrecision(-1);
+
+    Condition condition = new Condition();
+    condition.setLeftValueName("date");
+    condition.setFunction(Function.SMALLER);
+    condition.setRightValue(constant);
+    return condition;
+  }
+
+  private static void assertDateLessThan(Condition condition) throws Exception 
{
+    SimpleDateFormat iso = isoDate();
+    IRowMeta rowMeta = new RowMeta();
+    rowMeta.addValueMeta(new ValueMetaDate("date"));
+
+    assertTrue(condition.evaluate(rowMeta, new Object[] 
{iso.parse("2021-12-31")}));
+    assertFalse(condition.evaluate(rowMeta, new Object[] 
{iso.parse("2022-01-01")}));
+    assertFalse(condition.evaluate(rowMeta, new Object[] 
{iso.parse("2022-01-02")}));
+  }
+
+  private static SimpleDateFormat isoDate() {
+    SimpleDateFormat iso = new SimpleDateFormat("yyyy-MM-dd");
+    iso.setLenient(false);
+    return iso;
+  }
 }
diff --git 
a/plugins/transforms/filterrows/src/test/java/org/apache/hop/pipeline/transforms/filterrows/FilterRowsTest.java
 
b/plugins/transforms/filterrows/src/test/java/org/apache/hop/pipeline/transforms/filterrows/FilterRowsTest.java
index a3cde5988d..23254bcdb2 100644
--- 
a/plugins/transforms/filterrows/src/test/java/org/apache/hop/pipeline/transforms/filterrows/FilterRowsTest.java
+++ 
b/plugins/transforms/filterrows/src/test/java/org/apache/hop/pipeline/transforms/filterrows/FilterRowsTest.java
@@ -19,6 +19,7 @@ package org.apache.hop.pipeline.transforms.filterrows;
 
 import static org.apache.hop.core.Condition.Function.EQUAL;
 import static org.apache.hop.core.Condition.Function.REGEXP;
+import static org.apache.hop.core.Condition.Function.SMALLER;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -200,6 +201,40 @@ class FilterRowsTest {
     assertFalse(meta.getCondition().evaluate(rowMeta, matchingRow));
   }
 
+  /**
+   * Regression for #3051: a Date constant whose stored text is Hop's 
compatible format while the
+   * mask is the user format must still evaluate after init() clones and 
caches the condition.
+   */
+  @Test
+  void dateConstantWithLegacyCompatibleTextEvaluatesAfterInit() throws 
Exception {
+    Condition.CValue constant = new Condition.CValue();
+    constant.setName("constant");
+    constant.setType("Date");
+    constant.setText("2022/01/01 00:00:00.000");
+    constant.setMask("yyyy-MM-dd");
+    constant.setNullValue(false);
+    constant.setLength(-1);
+    constant.setPrecision(-1);
+
+    Condition metadataCondition = new Condition();
+    metadataCondition.setLeftValueName("value");
+    metadataCondition.setFunction(SMALLER);
+    metadataCondition.setRightValue(constant);
+
+    FilterRows transform = createTransform(metadataCondition);
+    assertTrue(transform.init());
+    Condition runtimeCondition = transform.getData().condition;
+
+    SimpleDateFormat iso = new SimpleDateFormat("yyyy-MM-dd");
+    iso.setLenient(false);
+    RowMeta rowMeta = new RowMeta();
+    rowMeta.addValueMeta(new ValueMetaDate("value"));
+
+    assertTrue(runtimeCondition.evaluate(rowMeta, new Object[] 
{iso.parse("2021-12-31")}));
+    assertFalse(runtimeCondition.evaluate(rowMeta, new Object[] 
{iso.parse("2022-01-01")}));
+    assertFalse(runtimeCondition.evaluate(rowMeta, new Object[] 
{iso.parse("2022-01-02")}));
+  }
+
   private static Stream<Arguments> variableValueTypes() throws Exception {
     ValueMetaDate dateMeta = new ValueMetaDate("value");
     dateMeta.setConversionMask("yyyy-MM-dd");
diff --git 
a/plugins/transforms/joinrows/src/main/java/org/apache/hop/pipeline/transforms/joinrows/JoinRowsDialog.java
 
b/plugins/transforms/joinrows/src/main/java/org/apache/hop/pipeline/transforms/joinrows/JoinRowsDialog.java
index f111d6fb3b..3f79c40c56 100644
--- 
a/plugins/transforms/joinrows/src/main/java/org/apache/hop/pipeline/transforms/joinrows/JoinRowsDialog.java
+++ 
b/plugins/transforms/joinrows/src/main/java/org/apache/hop/pipeline/transforms/joinrows/JoinRowsDialog.java
@@ -199,7 +199,7 @@ public class JoinRowsDialog extends BaseTransformDialog {
           ke);
     }
 
-    wCondition = new ConditionEditor(shell, SWT.BORDER, condition, 
inputfields);
+    wCondition = new ConditionEditor(shell, SWT.BORDER, condition, 
inputfields, variables);
 
     FormData fdCondition = new FormData();
     fdCondition.left = new FormAttachment(0, 0);
diff --git 
a/ui/src/main/java/org/apache/hop/ui/core/dialog/EnterValueDialog.java 
b/ui/src/main/java/org/apache/hop/ui/core/dialog/EnterValueDialog.java
index 3f099951ab..ae58de38fa 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/dialog/EnterValueDialog.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/dialog/EnterValueDialog.java
@@ -324,14 +324,21 @@ public class EnterValueDialog extends Dialog {
   public void getData() {
     wValueType.setText(valueMeta.getTypeDesc());
     try {
-      if (valueData != null) {
+      if (valueData instanceof String stringData && !valueMeta.isString()) {
+        // Raw unparsed constant (variable expression or a date stored in 
another format).
+        setInputText(stringData);
+      } else if (valueData != null) {
         String value = valueMeta.getString(valueData);
         if (value != null) {
           setInputText(value);
         }
       }
     } catch (HopValueException e) {
-      setInputText(valueMeta.toString());
+      if (valueData instanceof String stringData) {
+        setInputText(stringData);
+      } else {
+        setInputText(valueMeta.toString());
+      }
     }
     setFormats();
 
diff --git 
a/ui/src/main/java/org/apache/hop/ui/core/widget/ConditionEditor.java 
b/ui/src/main/java/org/apache/hop/ui/core/widget/ConditionEditor.java
index c5d67e3809..c09c923cf3 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/widget/ConditionEditor.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/widget/ConditionEditor.java
@@ -358,9 +358,17 @@ public class ConditionEditor extends Canvas implements 
MouseMoveListener {
                                 new ValueMetaAndData(new 
ValueMetaString("constant"), null));
                       }
                     }
+                    IValueMeta valueMeta = v.createValueMeta();
+                    Object valueData;
+                    try {
+                      valueData = v.createValueData();
+                    } catch (Exception parseException) {
+                      // Keep the stored text so a mask/text mismatch or a 
variable expression
+                      // can still be edited instead of failing to open the 
dialog.
+                      valueData = v.getText();
+                    }
                     EnterValueDialog evd =
-                        new EnterValueDialog(
-                            shell, SWT.NONE, v.createValueMeta(), 
v.createValueData(), variables);
+                        new EnterValueDialog(shell, SWT.NONE, valueMeta, 
valueData, variables);
                     evd.setModalDialog(
                         true); // To keep the condition editor from being 
closed with a value dialog
                     // still

Reply via email to