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

tbonelee pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/zeppelin.git


The following commit(s) were added to refs/heads/master by this push:
     new fafc113473 [ZEPPELIN-6474] Handle NumberFormatException when parsing 
MongoDB interpreter numeric properties
fafc113473 is described below

commit fafc11347354fdfbbfb6f341b19710a12ea0e3b0
Author: Seoyeon Lee <[email protected]>
AuthorDate: Fri Jul 31 23:18:21 2026 +0900

    [ZEPPELIN-6474] Handle NumberFormatException when parsing MongoDB 
interpreter numeric properties
    
    ### What is this PR for?
    
    `MongoDbInterpreter.open()` parses two numeric properties, 
`mongo.shell.command.timeout` and `mongo.interpreter.concurrency.max`, with 
`Long.parseLong()` / `Integer.parseInt()` outside any `try`/`catch` — the 
existing try-with-resources in that method covers only the `Scanner` that loads 
the shell extension. When either value is empty, missing, or non-numeric, a raw 
`NumberFormatException` escapes `open()`.
    
    Because `open()` is triggered lazily by the first paragraph run, this lands 
in the notebook paragraph as a bare stack trace that never names the property 
at fault. The MongoDB interpreter has several numeric properties, so the only 
way to tell which one failed today is to read the line number off the trace and 
open the source — which is not something a Zeppelin user should have to do.
    
    Reproduced on JDK 11: an empty value yields `NumberFormatException: For 
input string: ""`, a non-numeric value yields `For input string: "60s"`, and a 
missing property yields a message of just `null`. One note on that last case — 
the issue describes it as `Cannot parse null string`, but that wording comes 
from newer JDKs. On the JDK 11 this project builds with, `Long.parseLong(null)` 
throws `NumberFormatException("null")`, so the message carries even less 
information than the issue suggests.
    
    ### What does this PR do?
    
    - Wraps each parse and re-throws the `NumberFormatException` as an 
`InterpreterException` that names the property and its invalid value, keeping 
the original exception as the cause:
      ```
      Invalid value for property 'mongo.shell.command.timeout': 60s
      ```
    - Keeps the two parses separate so the message always points at the exact 
property that failed.
    - Adds `throws InterpreterException` to the `open()` override. The base 
`Interpreter.open()` already declares it, so no caller contract changes — at 
runtime the call goes through `LazyOpenInterpreter.open()`, which already 
declares it too, and the only direct caller was the test.
    
    Per the issue, the scope is deliberately narrow: no range validation, no 
silent fallback to default values, no unrelated changes. An invalid 
configuration still fails exactly as before; it just fails understandably.
    
    ### What type of PR is it?
    Improvement
    
    ### What is the Jira issue?
    * https://issues.apache.org/jira/browse/ZEPPELIN-6474
    
    ### How should this be tested?
    
    * `./mvnw test -pl mongodb`
    * Four tests were added to the existing `MongoDbInterpreterTest`, covering 
both properties across the three failure modes named in the issue: non-numeric 
and missing for `mongo.shell.command.timeout`, empty and missing for 
`mongo.interpreter.concurrency.max`. Each asserts that an 
`InterpreterException` is thrown, that its message names the offending 
property, and that the original `NumberFormatException` is preserved as the 
cause. The two remaining combinations exercise the identical  [...]
    * `MongoDbInterpreterTest.init()` now declares `throws 
InterpreterException`, since it calls `open()` on the concrete type.
    * To confirm the new tests are meaningful, I reverted the change to 
`MongoDbInterpreter` and re-ran the suite: exactly the four new tests fail with 
`expected: <InterpreterException> but was: <NumberFormatException>`, while the 
two pre-existing tests still pass. With the change applied, all six pass.
    
    ### Questions:
    * Does the license files need to update? No
    * Is there breaking changes for older versions? No — a valid configuration 
behaves exactly as before, and an invalid one already failed. Only the 
exception type and its message change.
    * Does this needs documentation? No
    
    
    Closes #5356 from sylee6529/ZEPPELIN-6474-mongodb-numberformat.
    
    Signed-off-by: ChanHo Lee <[email protected]>
---
 .../zeppelin/mongodb/MongoDbInterpreter.java       | 22 ++++++--
 .../zeppelin/mongodb/MongoDbInterpreterTest.java   | 59 +++++++++++++++++++++-
 2 files changed, 77 insertions(+), 4 deletions(-)

diff --git 
a/mongodb/src/main/java/org/apache/zeppelin/mongodb/MongoDbInterpreter.java 
b/mongodb/src/main/java/org/apache/zeppelin/mongodb/MongoDbInterpreter.java
index 54c121fcdf..5521135a39 100644
--- a/mongodb/src/main/java/org/apache/zeppelin/mongodb/MongoDbInterpreter.java
+++ b/mongodb/src/main/java/org/apache/zeppelin/mongodb/MongoDbInterpreter.java
@@ -35,6 +35,7 @@ import org.apache.commons.io.FileUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.zeppelin.interpreter.Interpreter;
 import org.apache.zeppelin.interpreter.InterpreterContext;
+import org.apache.zeppelin.interpreter.InterpreterException;
 import org.apache.zeppelin.interpreter.InterpreterResult;
 import org.apache.zeppelin.interpreter.InterpreterResult.Code;
 import org.apache.zeppelin.scheduler.Scheduler;
@@ -66,13 +67,28 @@ public class MongoDbInterpreter extends Interpreter {
   }
 
   @Override
-  public void open() {
+  public void open() throws InterpreterException {
     try (final Scanner scanner = new 
Scanner(MongoDbInterpreter.class.getResourceAsStream("/shell_extension.js"),
             "UTF-8").useDelimiter("\\A")) {
         shellExtension = scanner.next();
     }
-    commandTimeout = 
Long.parseLong(getProperty("mongo.shell.command.timeout"));
-    maxConcurrency = 
Integer.parseInt(getProperty("mongo.interpreter.concurrency.max"));
+
+    final String commandTimeoutValue = 
getProperty("mongo.shell.command.timeout");
+    try {
+      commandTimeout = Long.parseLong(commandTimeoutValue);
+    } catch (NumberFormatException e) {
+      throw new InterpreterException("Invalid value for property "
+          + "'mongo.shell.command.timeout': " + commandTimeoutValue, e);
+    }
+
+    final String maxConcurrencyValue = 
getProperty("mongo.interpreter.concurrency.max");
+    try {
+      maxConcurrency = Integer.parseInt(maxConcurrencyValue);
+    } catch (NumberFormatException e) {
+      throw new InterpreterException("Invalid value for property "
+          + "'mongo.interpreter.concurrency.max': " + maxConcurrencyValue, e);
+    }
+
     dbAddress = getProperty("mongo.server.host") + ":" + 
getProperty("mongo.server.port");
     prepareShellExtension();
   }
diff --git 
a/mongodb/src/test/java/org/apache/zeppelin/mongodb/MongoDbInterpreterTest.java 
b/mongodb/src/test/java/org/apache/zeppelin/mongodb/MongoDbInterpreterTest.java
index 08991c3a84..cd57db0a96 100644
--- 
a/mongodb/src/test/java/org/apache/zeppelin/mongodb/MongoDbInterpreterTest.java
+++ 
b/mongodb/src/test/java/org/apache/zeppelin/mongodb/MongoDbInterpreterTest.java
@@ -19,6 +19,8 @@ package org.apache.zeppelin.mongodb;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.io.File;
 import java.io.IOException;
@@ -30,6 +32,7 @@ import java.util.Scanner;
 import org.apache.commons.io.FileUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.zeppelin.interpreter.InterpreterContext;
+import org.apache.zeppelin.interpreter.InterpreterException;
 import org.apache.zeppelin.interpreter.InterpreterOutput;
 import org.apache.zeppelin.interpreter.InterpreterOutputListener;
 import org.apache.zeppelin.interpreter.InterpreterResult;
@@ -80,7 +83,7 @@ public class MongoDbInterpreterTest implements 
InterpreterOutputListener {
   }
 
   @BeforeEach
-  public void init() {
+  public void init() throws InterpreterException {
     buffer = ByteBuffer.allocate(10000);
     props.put("mongo.shell.path", (IS_WINDOWS ? "" : "sh ") + MONGO_SHELL);
     props.put("mongo.shell.command.table.limit", "10000");
@@ -137,6 +140,60 @@ public class MongoDbInterpreterTest implements 
InterpreterOutputListener {
     assertSame(Code.ERROR, res.code());
   }
 
+  @Test
+  void testInvalidCommandTimeout() {
+    props.setProperty("mongo.shell.command.timeout", "not-a-number");
+
+    final InterpreterException e =
+        assertThrows(InterpreterException.class, () -> interpreter.open());
+
+    assertTrue(e.getMessage().contains("mongo.shell.command.timeout"),
+        "The message must name the offending property: " + e.getMessage());
+    assertTrue(e.getMessage().contains("not-a-number"),
+        "The message must show the invalid value: " + e.getMessage());
+    assertTrue(e.getCause() instanceof NumberFormatException,
+        "The original NumberFormatException must be preserved as the cause");
+  }
+
+  @Test
+  void testMissingCommandTimeout() {
+    props.remove("mongo.shell.command.timeout");
+
+    final InterpreterException e =
+        assertThrows(InterpreterException.class, () -> interpreter.open());
+
+    assertTrue(e.getMessage().contains("mongo.shell.command.timeout"),
+        "The message must name the offending property: " + e.getMessage());
+    assertTrue(e.getCause() instanceof NumberFormatException,
+        "The original NumberFormatException must be preserved as the cause");
+  }
+
+  @Test
+  void testEmptyMaxConcurrency() {
+    props.setProperty("mongo.interpreter.concurrency.max", "");
+
+    final InterpreterException e =
+        assertThrows(InterpreterException.class, () -> interpreter.open());
+
+    assertTrue(e.getMessage().contains("mongo.interpreter.concurrency.max"),
+        "The message must name the offending property: " + e.getMessage());
+    assertTrue(e.getCause() instanceof NumberFormatException,
+        "The original NumberFormatException must be preserved as the cause");
+  }
+
+  @Test
+  void testMissingMaxConcurrency() {
+    props.remove("mongo.interpreter.concurrency.max");
+
+    final InterpreterException e =
+        assertThrows(InterpreterException.class, () -> interpreter.open());
+
+    assertTrue(e.getMessage().contains("mongo.interpreter.concurrency.max"),
+        "The message must name the offending property: " + e.getMessage());
+    assertTrue(e.getCause() instanceof NumberFormatException,
+        "The original NumberFormatException must be preserved as the cause");
+  }
+
   @Override
   public void onUpdateAll(InterpreterOutput interpreterOutput) {
 

Reply via email to