You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hop.apache.org by ha...@apache.org on 2026/07/31 10:14:59 UTC

(hop) branch main updated: [SONAR] fix some more issues (#7713)

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 d6e493a6ce [SONAR] fix some more issues (#7713)
d6e493a6ce is described below

commit d6e493a6ce4c54f7f4bbf2b7839ddbce89389659
Author: Hans Van Akelyen <ha...@gmail.com>
AuthorDate: Fri Jul 31 12:14:52 2026 +0200

    [SONAR] fix some more issues (#7713)
---
 .../org/apache/hop/core/util/TempFileUtil.java     |  1 +
 .../hop/core/logging/LogChannelFileWriter.java     |  3 +
 .../java/org/apache/hop/www/ShutdownServlet.java   |  3 +
 .../org/apache/hop/www/StartWorkflowServlet.java   |  3 +
 .../engines/remote/RemoteWorkflowEngineTest.java   |  8 +--
 .../actions/as400command/ActionAs400Command.java   | 19 +++---
 .../apache/hop/databases/db2/DB2DatabaseMeta.java  |  1 +
 .../hop/databases/derby/DerbyDatabaseMeta.java     |  1 +
 .../hop/databases/duckdb/DuckDBDatabaseMeta.java   |  1 +
 .../databases/informix/InformixDatabaseMeta.java   |  1 +
 .../hop/databases/sqlite/SqliteDatabaseMeta.java   |  1 +
 .../databases/teradata/TeradataDatabaseMeta.java   |  1 +
 .../org/apache/hop/beam/core/fn/TimestampFn.java   |  4 ++
 .../workflow/actions/mail/ActionMailDialog.java    |  2 +
 .../passwords/aes/Aes2TwoWayPasswordEncoder.java   |  6 +-
 .../hop/projects/resources/ResourceAttributes.java |  2 +
 .../apache/hop/neo4j/core/data/GraphNodeData.java  |  1 +
 .../salesforce/SalesforceConnectionEditor.java     |  2 +
 .../transforms/databasejoin/DatabaseJoinMeta.java  | 67 +++++++++++-----------
 .../dynamicsqlrow/DynamicSqlRowMeta.java           | 65 +++++++++++----------
 .../transforms/excelwriter/ods/OdsExcelWriter.java | 36 +++++-------
 .../ui/hopgui/file/shared/DrillDownGuiPlugin.java  |  8 ++-
 .../file/shared/PipelineRowSamplerHelper.java      |  2 +
 .../perspective/explorer/ExplorerPerspective.java  | 11 +++-
 .../hop/ui/hopgui/terminal/JediTerminalWidget.java |  6 +-
 25 files changed, 144 insertions(+), 111 deletions(-)

diff --git a/core/src/main/java/org/apache/hop/core/util/TempFileUtil.java b/core/src/main/java/org/apache/hop/core/util/TempFileUtil.java
index ad9dd317a3..3b4af79ba8 100644
--- a/core/src/main/java/org/apache/hop/core/util/TempFileUtil.java
+++ b/core/src/main/java/org/apache/hop/core/util/TempFileUtil.java
@@ -56,6 +56,7 @@ public class TempFileUtil {
    * @return the path of the newly created file
    * @throws IOException in case the file could not be created
    */
+  @SuppressWarnings("java:S5443") // the permissions below are exactly what makes this safe
   public static Path createTempFile(String prefix, String suffix) throws IOException {
     if (POSIX) {
       return Files.createTempFile(prefix, suffix, OWNER_ONLY);
diff --git a/engine/src/main/java/org/apache/hop/core/logging/LogChannelFileWriter.java b/engine/src/main/java/org/apache/hop/core/logging/LogChannelFileWriter.java
index e2785079b0..e924094b40 100644
--- a/engine/src/main/java/org/apache/hop/core/logging/LogChannelFileWriter.java
+++ b/engine/src/main/java/org/apache/hop/core/logging/LogChannelFileWriter.java
@@ -95,6 +95,9 @@ public class LogChannelFileWriter {
     exception = null;
     active.set(true);
 
+    // A platform thread on purpose: this one lives for the duration of the logging channel and is
+    // referenced by callers to join on, a virtual thread buys us nothing here.
+    @SuppressWarnings("java:S6881")
     Thread thread =
         new Thread(
             () -> {
diff --git a/engine/src/main/java/org/apache/hop/www/ShutdownServlet.java b/engine/src/main/java/org/apache/hop/www/ShutdownServlet.java
index 4fb9ba3aea..27fd879140 100644
--- a/engine/src/main/java/org/apache/hop/www/ShutdownServlet.java
+++ b/engine/src/main/java/org/apache/hop/www/ShutdownServlet.java
@@ -31,6 +31,9 @@ public class ShutdownServlet extends BaseHttpServlet {
   /** Delay before the shutdown is performed, to give the servlet time to respond. */
   private static final long SHUTDOWN_DELAY_MS = 2000L;
 
+  // Hop runs its own embedded server, not a managed JEE container. The timer below is what lets
+  // this servlet answer the request before the server it runs on is taken down.
+  @SuppressWarnings("java:S2654")
   @Override
   protected void doGet(HttpServletRequest request, HttpServletResponse response)
       throws ServletException, IOException {
diff --git a/engine/src/main/java/org/apache/hop/www/StartWorkflowServlet.java b/engine/src/main/java/org/apache/hop/www/StartWorkflowServlet.java
index cf0a9e50ed..6ab9ab560f 100644
--- a/engine/src/main/java/org/apache/hop/www/StartWorkflowServlet.java
+++ b/engine/src/main/java/org/apache/hop/www/StartWorkflowServlet.java
@@ -58,6 +58,9 @@ public class StartWorkflowServlet extends BaseHttpServlet implements IHopServerP
     super(workflowMap);
   }
 
+  // Hop runs its own embedded server, not a managed JEE container. The lock below guards the
+  // re-creation of a finished workflow against concurrent start requests.
+  @SuppressWarnings("java:S2654")
   @Override
   public void doGet(HttpServletRequest request, HttpServletResponse response)
       throws ServletException, IOException {
diff --git a/engine/src/test/java/org/apache/hop/workflow/engines/remote/RemoteWorkflowEngineTest.java b/engine/src/test/java/org/apache/hop/workflow/engines/remote/RemoteWorkflowEngineTest.java
index af1f8170c3..0543b74754 100644
--- a/engine/src/test/java/org/apache/hop/workflow/engines/remote/RemoteWorkflowEngineTest.java
+++ b/engine/src/test/java/org/apache/hop/workflow/engines/remote/RemoteWorkflowEngineTest.java
@@ -136,12 +136,12 @@ class RemoteWorkflowEngineTest {
     engine.setMetadataProvider(metadataProvider);
     engine.setVariable("RUN_CONFIG", "remote");
 
+    WorkflowRunConfiguration runConfiguration =
+        metadataProvider.getSerializer(WorkflowRunConfiguration.class).load("remote");
+
     HopException e =
         assertThrows(
-            HopException.class,
-            () ->
-                engine.validateRunConfigurationChain(
-                    metadataProvider.getSerializer(WorkflowRunConfiguration.class).load("remote")));
+            HopException.class, () -> engine.validateRunConfigurationChain(runConfiguration));
 
     assertTrue(e.getMessage().contains("remote -> remote"), "The chain should be reported: " + e);
   }
diff --git a/plugins/databases/as400/src/main/java/org/apache/hop/workflow/actions/as400command/ActionAs400Command.java b/plugins/databases/as400/src/main/java/org/apache/hop/workflow/actions/as400command/ActionAs400Command.java
index 5d0210edb5..982c90dca2 100644
--- a/plugins/databases/as400/src/main/java/org/apache/hop/workflow/actions/as400command/ActionAs400Command.java
+++ b/plugins/databases/as400/src/main/java/org/apache/hop/workflow/actions/as400command/ActionAs400Command.java
@@ -317,20 +317,21 @@ public class ActionAs400Command extends ActionBase implements Cloneable, IAction
         this.getProxyServer(variables.resolve(proxyHost), variables.resolve(proxyPort));
 
     // Create an AS400 object
-    AS400 system =
+    try (AS400 system =
         new AS400(
             variables.resolve(server),
             variables.resolve(user),
             Utils.resolvePassword(this, password),
-            proxyServer);
-    try {
-      system.connectService(AS400.COMMAND);
-    } finally {
+            proxyServer)) {
       try {
-        // This is only a connection test, so release the service again
-        system.disconnectService(AS400.COMMAND);
-      } catch (Exception e) {
-        // Ignore
+        system.connectService(AS400.COMMAND);
+      } finally {
+        try {
+          // This is only a connection test, so release the service again
+          system.disconnectService(AS400.COMMAND);
+        } catch (Exception e) {
+          // Ignore
+        }
       }
     }
 
diff --git a/plugins/databases/db2/src/main/java/org/apache/hop/databases/db2/DB2DatabaseMeta.java b/plugins/databases/db2/src/main/java/org/apache/hop/databases/db2/DB2DatabaseMeta.java
index b379b13631..28a41317f7 100644
--- a/plugins/databases/db2/src/main/java/org/apache/hop/databases/db2/DB2DatabaseMeta.java
+++ b/plugins/databases/db2/src/main/java/org/apache/hop/databases/db2/DB2DatabaseMeta.java
@@ -61,6 +61,7 @@ public class DB2DatabaseMeta extends BaseDatabaseMeta implements IDatabase {
   }
 
   @Override
+  @SuppressWarnings("java:S1313") // the driver version is not an IP address
   public DriverDownload getDriverDownload() {
     return DriverDownload.builder()
         .mavenCoordinate("com.ibm.db2:jcc")
diff --git a/plugins/databases/derby/src/main/java/org/apache/hop/databases/derby/DerbyDatabaseMeta.java b/plugins/databases/derby/src/main/java/org/apache/hop/databases/derby/DerbyDatabaseMeta.java
index 29aa0f02db..90fc8364e4 100644
--- a/plugins/databases/derby/src/main/java/org/apache/hop/databases/derby/DerbyDatabaseMeta.java
+++ b/plugins/databases/derby/src/main/java/org/apache/hop/databases/derby/DerbyDatabaseMeta.java
@@ -60,6 +60,7 @@ public class DerbyDatabaseMeta extends BaseDatabaseMeta implements IDatabase {
   }
 
   @Override
+  @SuppressWarnings("java:S1313") // the driver version is not an IP address
   public DriverDownload getDriverDownload() {
     return DriverDownload.builder()
         .mavenCoordinate("org.apache.derby:derbyclient")
diff --git a/plugins/databases/duckdb/src/main/java/org/apache/hop/databases/duckdb/DuckDBDatabaseMeta.java b/plugins/databases/duckdb/src/main/java/org/apache/hop/databases/duckdb/DuckDBDatabaseMeta.java
index ed41946c24..e2ec8c6f1b 100644
--- a/plugins/databases/duckdb/src/main/java/org/apache/hop/databases/duckdb/DuckDBDatabaseMeta.java
+++ b/plugins/databases/duckdb/src/main/java/org/apache/hop/databases/duckdb/DuckDBDatabaseMeta.java
@@ -142,6 +142,7 @@ public class DuckDBDatabaseMeta extends BaseDatabaseMeta implements IDatabase {
   }
 
   @Override
+  @SuppressWarnings("java:S1313") // the driver version is not an IP address
   public DriverDownload getDriverDownload() {
     return DriverDownload.builder()
         .mavenCoordinate("org.duckdb:duckdb_jdbc")
diff --git a/plugins/databases/informix/src/main/java/org/apache/hop/databases/informix/InformixDatabaseMeta.java b/plugins/databases/informix/src/main/java/org/apache/hop/databases/informix/InformixDatabaseMeta.java
index 836abbb0b7..50293eeb45 100644
--- a/plugins/databases/informix/src/main/java/org/apache/hop/databases/informix/InformixDatabaseMeta.java
+++ b/plugins/databases/informix/src/main/java/org/apache/hop/databases/informix/InformixDatabaseMeta.java
@@ -75,6 +75,7 @@ public class InformixDatabaseMeta extends BaseDatabaseMeta implements IDatabase
   }
 
   @Override
+  @SuppressWarnings("java:S1313") // the driver version is not an IP address
   public DriverDownload getDriverDownload() {
     return DriverDownload.builder()
         .mavenCoordinate("com.ibm.informix:jdbc")
diff --git a/plugins/databases/sqlite/src/main/java/org/apache/hop/databases/sqlite/SqliteDatabaseMeta.java b/plugins/databases/sqlite/src/main/java/org/apache/hop/databases/sqlite/SqliteDatabaseMeta.java
index e935b8874b..b37a434250 100644
--- a/plugins/databases/sqlite/src/main/java/org/apache/hop/databases/sqlite/SqliteDatabaseMeta.java
+++ b/plugins/databases/sqlite/src/main/java/org/apache/hop/databases/sqlite/SqliteDatabaseMeta.java
@@ -61,6 +61,7 @@ public class SqliteDatabaseMeta extends BaseDatabaseMeta implements IDatabase {
   }
 
   @Override
+  @SuppressWarnings("java:S1313") // the driver version is not an IP address
   public DriverDownload getDriverDownload() {
     return DriverDownload.builder()
         .mavenCoordinate("org.xerial:sqlite-jdbc")
diff --git a/plugins/databases/teradata/src/main/java/org/apache/hop/databases/teradata/TeradataDatabaseMeta.java b/plugins/databases/teradata/src/main/java/org/apache/hop/databases/teradata/TeradataDatabaseMeta.java
index 11f3fe3ea3..ad9c238b50 100644
--- a/plugins/databases/teradata/src/main/java/org/apache/hop/databases/teradata/TeradataDatabaseMeta.java
+++ b/plugins/databases/teradata/src/main/java/org/apache/hop/databases/teradata/TeradataDatabaseMeta.java
@@ -67,6 +67,7 @@ public class TeradataDatabaseMeta extends BaseDatabaseMeta implements IDatabase
   }
 
   @Override
+  @SuppressWarnings("java:S1313") // the driver version is not an IP address
   public DriverDownload getDriverDownload() {
     return DriverDownload.builder()
         .mavenCoordinate("com.teradata.jdbc:terajdbc")
diff --git a/plugins/engines/beam/src/main/java/org/apache/hop/beam/core/fn/TimestampFn.java b/plugins/engines/beam/src/main/java/org/apache/hop/beam/core/fn/TimestampFn.java
index 7d163a2cca..e09025cea3 100644
--- a/plugins/engines/beam/src/main/java/org/apache/hop/beam/core/fn/TimestampFn.java
+++ b/plugins/engines/beam/src/main/java/org/apache/hop/beam/core/fn/TimestampFn.java
@@ -128,6 +128,10 @@ public class TimestampFn extends DoFn<HopRow, HopRow> {
           if (IValueMeta.TYPE_TIMESTAMP == fieldValueMeta.getType()) {
             java.sql.Timestamp timestamp =
                 ((ValueMetaTimestamp) fieldValueMeta).getTimestamp(fieldData);
+            if (timestamp == null) {
+              throw new HopException(
+                  "Timestamp field contains a null value, this can't be used to set a timestamp on a bounded/unbounded collection of data");
+            }
             instant = new Instant(timestamp.toInstant());
           } else {
             Date date = fieldValueMeta.getDate(fieldData);
diff --git a/plugins/misc/mail/src/main/java/org/apache/hop/mail/workflow/actions/mail/ActionMailDialog.java b/plugins/misc/mail/src/main/java/org/apache/hop/mail/workflow/actions/mail/ActionMailDialog.java
index 8b3ba6f7df..9c2ab1f97a 100644
--- a/plugins/misc/mail/src/main/java/org/apache/hop/mail/workflow/actions/mail/ActionMailDialog.java
+++ b/plugins/misc/mail/src/main/java/org/apache/hop/mail/workflow/actions/mail/ActionMailDialog.java
@@ -1204,6 +1204,8 @@ public class ActionMailDialog extends ActionDialog {
                   IMAGES_FILE_TYPES,
                   true);
           if (filename != null) {
+            // Created once per image the user picks, reuse would gain us nothing
+            @SuppressWarnings("java:S2119")
             Random random = new Random();
             wContentID.setText(Long.toString(Math.abs(random.nextLong()), 32));
           }
diff --git a/plugins/misc/passwords/src/main/java/org/apache/hop/passwords/aes/Aes2TwoWayPasswordEncoder.java b/plugins/misc/passwords/src/main/java/org/apache/hop/passwords/aes/Aes2TwoWayPasswordEncoder.java
index bcfd713469..c7679f582e 100644
--- a/plugins/misc/passwords/src/main/java/org/apache/hop/passwords/aes/Aes2TwoWayPasswordEncoder.java
+++ b/plugins/misc/passwords/src/main/java/org/apache/hop/passwords/aes/Aes2TwoWayPasswordEncoder.java
@@ -52,6 +52,9 @@ public class Aes2TwoWayPasswordEncoder implements ITwoWayPasswordEncoder {
   public static final String AES_PREFIX = "AES2 ";
   public static final String AES_ALGORITHM = "AES/GCM/NoPadding";
 
+  /** {@link SecureRandom} is thread safe, seeding it once per encoded password is wasteful. */
+  private static final SecureRandom SECURE_RANDOM = new SecureRandom();
+
   private SecretKeySpec secretKeySpec;
 
   @Override
@@ -103,8 +106,7 @@ public class Aes2TwoWayPasswordEncoder implements ITwoWayPasswordEncoder {
 
     // Generate a new, unique 12-byte IV for this encryption
     byte[] iv = new byte[12];
-    SecureRandom secureRandom = new SecureRandom();
-    secureRandom.nextBytes(iv);
+    SECURE_RANDOM.nextBytes(iv);
 
     // Initialize a new Cipher with the unique IV
     GCMParameterSpec parameterSpec = new GCMParameterSpec(128, iv);
diff --git a/plugins/misc/projects/src/main/java/org/apache/hop/projects/resources/ResourceAttributes.java b/plugins/misc/projects/src/main/java/org/apache/hop/projects/resources/ResourceAttributes.java
index 2d3878ccc0..50f8819c77 100644
--- a/plugins/misc/projects/src/main/java/org/apache/hop/projects/resources/ResourceAttributes.java
+++ b/plugins/misc/projects/src/main/java/org/apache/hop/projects/resources/ResourceAttributes.java
@@ -68,6 +68,8 @@ public final class ResourceAttributes {
    *   <li>Development / CI / CB / other → off
    * </ul>
    */
+  // False positive: StringUtils.isNotBlank() is false for null, so 'explicit' can't be null here
+  @SuppressWarnings("javabugs:S2259")
   public static String resolveOnEnable(IAttributes attributes, String purpose) {
     String explicit = attributes != null ? attributes.getAttribute(GROUP, KEY_ON_ENABLE) : null;
     if (StringUtils.isNotBlank(explicit)) {
diff --git a/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/core/data/GraphNodeData.java b/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/core/data/GraphNodeData.java
index 51522be616..f4380a5e34 100644
--- a/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/core/data/GraphNodeData.java
+++ b/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/core/data/GraphNodeData.java
@@ -110,6 +110,7 @@ public class GraphNodeData {
   }
 
   public GraphNodeData(JSONObject jNode) {
+    this();
 
     id = (String) jNode.get("id");
     JSONArray jLabels = (JSONArray) jNode.get("labels");
diff --git a/plugins/tech/salesforce/src/main/java/org/apache/hop/metadata/salesforce/SalesforceConnectionEditor.java b/plugins/tech/salesforce/src/main/java/org/apache/hop/metadata/salesforce/SalesforceConnectionEditor.java
index d7a40f2657..ed61d4cd48 100644
--- a/plugins/tech/salesforce/src/main/java/org/apache/hop/metadata/salesforce/SalesforceConnectionEditor.java
+++ b/plugins/tech/salesforce/src/main/java/org/apache/hop/metadata/salesforce/SalesforceConnectionEditor.java
@@ -778,6 +778,8 @@ public class SalesforceConnectionEditor extends MetadataEditor<SalesforceConnect
   private void generatePKCEParameters() {
     try {
       // Generate code verifier (43-128 characters, URL-safe)
+      // Created once per OAuth authorization flow, reuse would gain us nothing
+      @SuppressWarnings("java:S2119")
       SecureRandom secureRandom = new SecureRandom();
       byte[] randomBytes = new byte[32];
       secureRandom.nextBytes(randomBytes);
diff --git a/plugins/transforms/databasejoin/src/main/java/org/apache/hop/pipeline/transforms/databasejoin/DatabaseJoinMeta.java b/plugins/transforms/databasejoin/src/main/java/org/apache/hop/pipeline/transforms/databasejoin/DatabaseJoinMeta.java
index e36278e200..a8d9ce0902 100644
--- a/plugins/transforms/databasejoin/src/main/java/org/apache/hop/pipeline/transforms/databasejoin/DatabaseJoinMeta.java
+++ b/plugins/transforms/databasejoin/src/main/java/org/apache/hop/pipeline/transforms/databasejoin/DatabaseJoinMeta.java
@@ -283,50 +283,49 @@ public class DatabaseJoinMeta extends BaseTransformMeta<DatabaseJoin, DatabaseJo
           e);
     }
 
-    Database db = new Database(loggingObject, variables, databaseMeta);
-    databases = new Database[] {db}; // Keep track of this one for cancelQuery
-
-    // Which fields are parameters?
-    // info[0] comes from the database connection.
-    //
-    IRowMeta param = getParameterRow(row);
+    try (Database db = new Database(loggingObject, variables, databaseMeta)) {
+      databases = new Database[] {db}; // Keep track of this one for cancelQuery
 
-    // First try without connecting to the database... (can be S L O W)
-    // See if it's in the cache...
-    //
-    IRowMeta add = null;
-    try {
-      add = db.getQueryFields(variables.resolve(sql), true, param, new Object[param.size()]);
-    } catch (HopDatabaseException dbe) {
-      throw new HopTransformException(
-          BaseMessages.getString(PKG, "DatabaseJoinMeta.Exception.UnableToDetermineQueryFields")
-              + Const.CR
-              + sql,
-          dbe);
-    }
+      // Which fields are parameters?
+      // info[0] comes from the database connection.
+      //
+      IRowMeta param = getParameterRow(row);
 
-    if (add != null) { // Cache hit, just return it this...
-      for (int i = 0; i < add.size(); i++) {
-        IValueMeta v = add.getValueMeta(i);
-        v.setOrigin(name);
-      }
-      row.addRowMeta(add);
-    } else {
-      // No cache hit, connect to the database, do it the hard way...
+      // First try without connecting to the database... (can be S L O W)
+      // See if it's in the cache...
       //
+      IRowMeta add = null;
       try {
-        db.connect();
         add = db.getQueryFields(variables.resolve(sql), true, param, new Object[param.size()]);
+      } catch (HopDatabaseException dbe) {
+        throw new HopTransformException(
+            BaseMessages.getString(PKG, "DatabaseJoinMeta.Exception.UnableToDetermineQueryFields")
+                + Const.CR
+                + sql,
+            dbe);
+      }
+
+      if (add != null) { // Cache hit, just return it this...
         for (int i = 0; i < add.size(); i++) {
           IValueMeta v = add.getValueMeta(i);
           v.setOrigin(name);
         }
         row.addRowMeta(add);
-      } catch (HopDatabaseException dbe) {
-        throw new HopTransformException(
-            BaseMessages.getString(PKG, "DatabaseJoinMeta.Exception.ErrorObtainingFields"), dbe);
-      } finally {
-        db.close();
+      } else {
+        // No cache hit, connect to the database, do it the hard way...
+        //
+        try {
+          db.connect();
+          add = db.getQueryFields(variables.resolve(sql), true, param, new Object[param.size()]);
+          for (int i = 0; i < add.size(); i++) {
+            IValueMeta v = add.getValueMeta(i);
+            v.setOrigin(name);
+          }
+          row.addRowMeta(add);
+        } catch (HopDatabaseException dbe) {
+          throw new HopTransformException(
+              BaseMessages.getString(PKG, "DatabaseJoinMeta.Exception.ErrorObtainingFields"), dbe);
+        }
       }
     }
   }
diff --git a/plugins/transforms/dynamicsqlrow/src/main/java/org/apache/hop/pipeline/transforms/dynamicsqlrow/DynamicSqlRowMeta.java b/plugins/transforms/dynamicsqlrow/src/main/java/org/apache/hop/pipeline/transforms/dynamicsqlrow/DynamicSqlRowMeta.java
index ac169f6c30..b9eea3da64 100644
--- a/plugins/transforms/dynamicsqlrow/src/main/java/org/apache/hop/pipeline/transforms/dynamicsqlrow/DynamicSqlRowMeta.java
+++ b/plugins/transforms/dynamicsqlrow/src/main/java/org/apache/hop/pipeline/transforms/dynamicsqlrow/DynamicSqlRowMeta.java
@@ -150,48 +150,47 @@ public class DynamicSqlRowMeta extends BaseTransformMeta<DynamicSqlRow, DynamicS
       return;
     }
 
-    Database db = new Database(loggingObject, variables, databaseMeta);
-    // Keep track of this one for cancelQuery
-    databases = new Database[] {db};
+    try (Database db = new Database(loggingObject, variables, databaseMeta)) {
+      // Keep track of this one for cancelQuery
+      databases = new Database[] {db};
 
-    // First try without connecting to the database... (can be S L O W)
-    // See if it's in the cache...
-    IRowMeta add = null;
-    String realSql = sql;
-    if (replaceVariables) {
-      realSql = variables.resolve(realSql);
-    }
-    try {
-      add = db.getQueryFields(realSql, false);
-    } catch (HopDatabaseException dbe) {
-      throw new HopTransformException(
-          BaseMessages.getString(PKG, "DynamicSQLRowMeta.Exception.UnableToDetermineQueryFields")
-              + Const.CR
-              + sql,
-          dbe);
-    }
-    // Cache hit, just return it this...
-    if (add != null) {
-      for (int i = 0; i < add.size(); i++) {
-        IValueMeta v = add.getValueMeta(i);
-        v.setOrigin(name);
+      // First try without connecting to the database... (can be S L O W)
+      // See if it's in the cache...
+      IRowMeta add = null;
+      String realSql = sql;
+      if (replaceVariables) {
+        realSql = variables.resolve(realSql);
       }
-      row.addRowMeta(add);
-    } else {
-      // No cache hit, connect to the database, do it the hard way...
       try {
-        db.connect();
         add = db.getQueryFields(realSql, false);
+      } catch (HopDatabaseException dbe) {
+        throw new HopTransformException(
+            BaseMessages.getString(PKG, "DynamicSQLRowMeta.Exception.UnableToDetermineQueryFields")
+                + Const.CR
+                + sql,
+            dbe);
+      }
+      // Cache hit, just return it this...
+      if (add != null) {
         for (int i = 0; i < add.size(); i++) {
           IValueMeta v = add.getValueMeta(i);
           v.setOrigin(name);
         }
         row.addRowMeta(add);
-      } catch (HopDatabaseException dbe) {
-        throw new HopTransformException(
-            BaseMessages.getString(PKG, "DynamicSQLRowMeta.Exception.ErrorObtainingFields"), dbe);
-      } finally {
-        db.close();
+      } else {
+        // No cache hit, connect to the database, do it the hard way...
+        try {
+          db.connect();
+          add = db.getQueryFields(realSql, false);
+          for (int i = 0; i < add.size(); i++) {
+            IValueMeta v = add.getValueMeta(i);
+            v.setOrigin(name);
+          }
+          row.addRowMeta(add);
+        } catch (HopDatabaseException dbe) {
+          throw new HopTransformException(
+              BaseMessages.getString(PKG, "DynamicSQLRowMeta.Exception.ErrorObtainingFields"), dbe);
+        }
       }
     }
   }
diff --git a/plugins/transforms/excel/src/main/java/org/apache/hop/pipeline/transforms/excelwriter/ods/OdsExcelWriter.java b/plugins/transforms/excel/src/main/java/org/apache/hop/pipeline/transforms/excelwriter/ods/OdsExcelWriter.java
index 9c406c122c..3fa854f3e0 100644
--- a/plugins/transforms/excel/src/main/java/org/apache/hop/pipeline/transforms/excelwriter/ods/OdsExcelWriter.java
+++ b/plugins/transforms/excel/src/main/java/org/apache/hop/pipeline/transforms/excelwriter/ods/OdsExcelWriter.java
@@ -262,12 +262,9 @@ public class OdsExcelWriter {
   }
 
   public void closeOutputFile(ExcelWriterWorkbookDefinition fileDefinition) throws HopException {
-    OutputStream out = null;
-    CountingOutputStream countingOut = null;
-    try {
-      countingOut =
-          new CountingOutputStream(HopVfs.getOutputStream(fileDefinition.getFile(), false));
-      out = new BufferedOutputStream(countingOut);
+    try (CountingOutputStream countingOut =
+            new CountingOutputStream(HopVfs.getOutputStream(fileDefinition.getFile(), false));
+        OutputStream out = new BufferedOutputStream(countingOut)) {
 
       if (meta.isFooterEnabled()) {
         writeHeader(fileDefinition, fileDefinition.getPosX(), fileDefinition.getPosY());
@@ -285,23 +282,18 @@ public class OdsExcelWriter {
       if (meta.isForceFormulaRecalculation()) {
         OdsFormulaHelper.prepareForRecalculation(document);
       }
-      document.save(out);
-      document.close();
-    } catch (Exception e) {
-      throw new HopException(e);
-    } finally {
-      if (out != null) {
-        try {
-          out.flush();
-          if (countingOut != null) {
-            long written = countingOut.getCount();
-            transform.recordBytesWritten(written, fileDefinition.getFile());
-          }
-          out.close();
-        } catch (Exception e) {
-          throw new HopException("Error closing ODS file " + fileDefinition.getFile(), e);
-        }
+      try {
+        document.save(out);
+      } finally {
+        document.close();
       }
+
+      // Flush before reading the byte count, the stream is closed by the resource block
+      //
+      out.flush();
+      transform.recordBytesWritten(countingOut.getCount(), fileDefinition.getFile());
+    } catch (Exception e) {
+      throw new HopException("Error closing ODS file " + fileDefinition.getFile(), e);
     }
   }
 
diff --git a/ui/src/main/java/org/apache/hop/ui/hopgui/file/shared/DrillDownGuiPlugin.java b/ui/src/main/java/org/apache/hop/ui/hopgui/file/shared/DrillDownGuiPlugin.java
index a33b656b19..35b4abb877 100644
--- a/ui/src/main/java/org/apache/hop/ui/hopgui/file/shared/DrillDownGuiPlugin.java
+++ b/ui/src/main/java/org/apache/hop/ui/hopgui/file/shared/DrillDownGuiPlugin.java
@@ -243,7 +243,8 @@ public class DrillDownGuiPlugin {
    */
   private void findRunningChildAndOpen(
       Object parentEngine, String childName, Display display, HopGui hopGui) {
-    new Thread(
+    Thread.ofVirtual()
+        .start(
             () -> {
               try {
                 for (int i = 0; i < 50; i++) {
@@ -268,11 +269,12 @@ public class DrillDownGuiPlugin {
                   Thread.sleep(100);
                 }
                 display.asyncExec(() -> showNoRunningExecution(hopGui));
+              } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
               } catch (Exception e) {
                 // Ignore
               }
-            })
-        .start();
+            });
   }
 
   /**
diff --git a/ui/src/main/java/org/apache/hop/ui/hopgui/file/shared/PipelineRowSamplerHelper.java b/ui/src/main/java/org/apache/hop/ui/hopgui/file/shared/PipelineRowSamplerHelper.java
index 632858f014..f4412eedd7 100644
--- a/ui/src/main/java/org/apache/hop/ui/hopgui/file/shared/PipelineRowSamplerHelper.java
+++ b/ui/src/main/java/org/apache/hop/ui/hopgui/file/shared/PipelineRowSamplerHelper.java
@@ -135,6 +135,8 @@ public final class PipelineRowSamplerHelper {
     if (meta == null) {
       return;
     }
+    // Created once per pipeline start and shared by all the row listeners below
+    @SuppressWarnings("java:S2119")
     final Random random = new Random();
     // Per-key counters for Random sampling (transform name or hop key)
     final Map<String, AtomicInteger> rowCounters = new ConcurrentHashMap<>();
diff --git a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPerspective.java b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPerspective.java
index f294f82414..eba5a019ba 100644
--- a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPerspective.java
+++ b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPerspective.java
@@ -1807,7 +1807,12 @@ public class ExplorerPerspective implements IHopPerspective, TabClosable, IFileD
   private void updateClosedFileIfNameSynchronized(FileObject targetFile) {
     try {
       String filename = HopVfs.getFilename(targetFile);
-      String extension = filename.substring(filename.lastIndexOf('.'));
+      int dotIndex = filename.lastIndexOf('.');
+      if (dotIndex < 0) {
+        // No extension: not a pipeline or workflow file
+        return;
+      }
+      String extension = filename.substring(dotIndex);
 
       // Check if it's a pipeline or workflow file
       if (".hpl".equalsIgnoreCase(extension)) {
@@ -2575,6 +2580,10 @@ public class ExplorerPerspective implements IHopPerspective, TabClosable, IFileD
    * @return The selected explorer file or null if nothing is selected
    */
   public ExplorerFile getSelectedFile() {
+    if (tree == null) {
+      // The perspective was never initialized, there's nothing to select
+      return null;
+    }
     TreeItem[] selection = tree.getSelection();
     if (selection == null || selection.length == 0) {
       return null;
diff --git a/ui/src/main/java/org/apache/hop/ui/hopgui/terminal/JediTerminalWidget.java b/ui/src/main/java/org/apache/hop/ui/hopgui/terminal/JediTerminalWidget.java
index 4b4914ced8..91c865e5fe 100644
--- a/ui/src/main/java/org/apache/hop/ui/hopgui/terminal/JediTerminalWidget.java
+++ b/ui/src/main/java/org/apache/hop/ui/hopgui/terminal/JediTerminalWidget.java
@@ -340,7 +340,8 @@ public class JediTerminalWidget implements ITerminalWidget {
       jediTermWidget.start();
 
       // Request focus after terminal initialization
-      new Thread(
+      Thread.ofVirtual()
+          .start(
               () -> {
                 try {
                   Thread.sleep(100);
@@ -359,8 +360,7 @@ public class JediTerminalWidget implements ITerminalWidget {
                 } catch (Exception e) {
                   log.logDebug("Error requesting initial AWT focus: " + e.getMessage());
                 }
-              })
-          .start();
+              });
 
     } catch (Exception e) {
       log.logError("Error starting JediTerm shell process", e);