From f4561394eab37c3238bec8bcb30b870e748801a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=99=ED=99=98?= <66408194+dev-donghwan@users.noreply.github.com> Date: Fri, 31 Jul 2026 23:09:29 +0900 Subject: [PATCH 1/2] [ZEPPELIN-6483] Format HDFS modification time in GMT to match the printed label MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### What is this PR for? In the HDFS file interpreter, `listOne()` appends a hard-coded `GMT` label to the modification time, but `listDate()` formats the timestamp using the JVM default time zone. On any server not running in UTC, the displayed value does not match the label (e.g. a file modified at `2015-08-02 20:43` GMT is shown as `2015-08-03 05:43GMT` on a KST server). This PR sets the formatter's time zone to GMT in `listDate()` so the rendered value matches the existing label. Why format in GMT (option A) rather than keep local time and fix the label (option B): - `modificationTime` is an absolute epoch value, so the time zone is only a display choice. Formatting in GMT keeps the output identical regardless of the host/JVM default zone and consistent with the label already printed. - Showing the interpreter JVM's local time would be ambiguous in shared HDFS / remote-interpreter, multi-user setups ("whose local time?"), and the output would vary per deployment, making it harder to reproduce and test. ### What type of PR is it? Bug Fix ### Todos * [x] - Format the modification time in GMT in `listDate()` * [x] - Add a regression test that runs under a non-UTC default zone (`Asia/Seoul`) and asserts the value is rendered in GMT to match the label ### What is the Jira issue? * https://issues.apache.org/jira/browse/ZEPPELIN-6483 ### How should this be tested? * `./mvnw test -pl file -Dtest=HDFSFileInterpreterTest` * The new test `testListDateFormatsInGmtToMatchLabel` pins a known `modificationTime` (1438548219672 = 2015-08-02 20:43 GMT) under an `Asia/Seoul` default zone and asserts the output contains `2015-08-02 20:43GMT`. ### Screenshots (if appropriate) N/A ### Questions: * Does the license files need to update? No * Is there breaking changes for older versions? No — display-only change; on non-UTC servers the shown value changes, but it now correctly matches the label * Does this needs documentation? No Closes #5351 from dev-donghwan/ZEPPELIN-6483. Signed-off-by: ChanHo Lee --- .../zeppelin/file/HDFSFileInterpreter.java | 7 +++++- .../file/HDFSFileInterpreterTest.java | 22 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/file/src/main/java/org/apache/zeppelin/file/HDFSFileInterpreter.java b/file/src/main/java/org/apache/zeppelin/file/HDFSFileInterpreter.java index 3429de0b55a..662d02add81 100644 --- a/file/src/main/java/org/apache/zeppelin/file/HDFSFileInterpreter.java +++ b/file/src/main/java/org/apache/zeppelin/file/HDFSFileInterpreter.java @@ -28,6 +28,7 @@ import java.util.Date; import java.util.List; import java.util.Properties; +import java.util.TimeZone; import org.apache.zeppelin.completer.CompletionType; import org.apache.zeppelin.interpreter.InterpreterContext; @@ -173,7 +174,11 @@ private String listPermission(OneFileStatus fs){ } private String listDate(OneFileStatus fs) { - return new SimpleDateFormat("yyyy-MM-dd HH:mm").format(new Date(fs.modificationTime)); + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm"); + // Format in GMT so the value matches the "GMT" label appended in listOne(), + // regardless of the JVM default time zone. + sdf.setTimeZone(TimeZone.getTimeZone("GMT")); + return sdf.format(new Date(fs.modificationTime)); } private String listOne(String path, OneFileStatus fs) { diff --git a/file/src/test/java/org/apache/zeppelin/file/HDFSFileInterpreterTest.java b/file/src/test/java/org/apache/zeppelin/file/HDFSFileInterpreterTest.java index 5c9e268e5d4..dc4dcbe5cc8 100644 --- a/file/src/test/java/org/apache/zeppelin/file/HDFSFileInterpreterTest.java +++ b/file/src/test/java/org/apache/zeppelin/file/HDFSFileInterpreterTest.java @@ -20,6 +20,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import com.google.gson.Gson; @@ -30,6 +31,7 @@ import java.util.HashMap; import java.util.List; import java.util.Properties; +import java.util.TimeZone; import org.apache.zeppelin.completer.CompletionType; import org.apache.zeppelin.interpreter.InterpreterResult; @@ -183,6 +185,26 @@ void testNoSuchFile() { t.close(); } + @Test + void testListDateFormatsInGmtToMatchLabel() { + // ZEPPELIN-6483: the timestamp must be formatted in GMT to match the trailing + // "GMT" label, regardless of the JVM default time zone. + TimeZone original = TimeZone.getDefault(); + try { + TimeZone.setDefault(TimeZone.getTimeZone("Asia/Seoul")); + HDFSFileInterpreter t = new MockHDFSFileInterpreter(new Properties()); + t.open(); + InterpreterResult result = t.interpret("ls -l /", null); + String out = result.message().get(0).getData(); + // modificationTime 1438548219672 == 2015-08-02 20:43 GMT (2015-08-03 05:43 in KST) + assertTrue(out.contains("2015-08-02 20:43GMT"), + "modification time should be shown in GMT to match the label, but was:\n" + out); + t.close(); + } finally { + TimeZone.setDefault(original); + } + } + } /** From fafc11347354fdfbbfb6f341b19710a12ea0e3b0 Mon Sep 17 00:00:00 2001 From: Seoyeon Lee <68765200+sylee6529@users.noreply.github.com> Date: Fri, 31 Jul 2026 23:18:21 +0900 Subject: [PATCH 2/2] [ZEPPELIN-6474] Handle NumberFormatException when parsing MongoDB interpreter numeric properties MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 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 catch block, so they were left out rather than duplicated. * `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: but was: `, 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 --- .../zeppelin/mongodb/MongoDbInterpreter.java | 22 ++++++- .../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 54c121fcdff..5521135a391 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.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 MongoDbInterpreter(Properties property) { } @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 08991c3a844..cd57db0a96a 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 @@ 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 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 static void setup() { } @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 @@ void testBadConf() { 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) {