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 d29469e959 NullPointerException in hop/registerPipeline Rest API when 
log_file flag is enabled, fixes  #4677 (#7557)
d29469e959 is described below

commit d29469e959177816b54c54254a4111108cb7a99c
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Fri Jul 17 16:01:18 2026 +0200

    NullPointerException in hop/registerPipeline Rest API when log_file flag is 
enabled, fixes  #4677 (#7557)
---
 .../hop/core/logging/SimpleLoggingObject.java      |  17 ++-
 .../hop/core/logging/SimpleLoggingObjectTest.java  |  49 +++++++
 .../org/apache/hop/www/AddPipelineServlet.java     |   6 +-
 .../java/org/apache/hop/www/BaseHttpServlet.java   |  28 ++++
 .../org/apache/hop/www/BaseWorkflowServlet.java    |  16 +--
 .../www/RegisterPipelineServletLogFileTest.java    | 155 +++++++++++++++++++++
 6 files changed, 254 insertions(+), 17 deletions(-)

diff --git 
a/core/src/main/java/org/apache/hop/core/logging/SimpleLoggingObject.java 
b/core/src/main/java/org/apache/hop/core/logging/SimpleLoggingObject.java
index 0dda734319..22cf5111dd 100644
--- a/core/src/main/java/org/apache/hop/core/logging/SimpleLoggingObject.java
+++ b/core/src/main/java/org/apache/hop/core/logging/SimpleLoggingObject.java
@@ -26,6 +26,7 @@ public class SimpleLoggingObject implements ILoggingObject {
   private ILoggingObject parent;
   private LogLevel logLevel = DefaultLogLevel.getLogLevel();
   private String containerObjectId;
+  private String logChannelId;
   private Date registrationDate;
   private boolean gatheringMetrics;
   private boolean forcingSeparateLogging;
@@ -96,9 +97,23 @@ public class SimpleLoggingObject implements ILoggingObject {
     return null;
   }
 
+  /**
+   * @return the log channel this object logs to, null when it does not have 
one of its own
+   */
   @Override
   public String getLogChannelId() {
-    return null;
+    return logChannelId;
+  }
+
+  /**
+   * Give this object a log channel of its own. Everything that logs with this 
object as its parent
+   * is then found back through it, which is what writing the log of a child 
to a file needs. See
+   * issue #4677.
+   *
+   * @param logChannelId the log channel id to set
+   */
+  public void setLogChannelId(String logChannelId) {
+    this.logChannelId = logChannelId;
   }
 
   @Override
diff --git 
a/core/src/test/java/org/apache/hop/core/logging/SimpleLoggingObjectTest.java 
b/core/src/test/java/org/apache/hop/core/logging/SimpleLoggingObjectTest.java
new file mode 100644
index 0000000000..9e981c9b33
--- /dev/null
+++ 
b/core/src/test/java/org/apache/hop/core/logging/SimpleLoggingObjectTest.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.core.logging;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import org.junit.jupiter.api.Test;
+
+class SimpleLoggingObjectTest {
+
+  /** Not every logging object logs itself, so it does not have to have a log 
channel. */
+  @Test
+  void withoutALogChannelOfItsOwnItReportsNone() {
+    SimpleLoggingObject loggingObject =
+        new SimpleLoggingObject("a-servlet", LoggingObjectType.HOP_SERVER, 
null);
+
+    assertNull(loggingObject.getLogChannelId());
+  }
+
+  /**
+   * A logging object that is the parent of what a server runs needs a log 
channel of its own: it is
+   * what a log file writer is hung on. See issue #4677.
+   */
+  @Test
+  void itReportsTheLogChannelItWasGiven() {
+    SimpleLoggingObject loggingObject =
+        new SimpleLoggingObject("a-servlet", LoggingObjectType.HOP_SERVER, 
null);
+
+    loggingObject.setLogChannelId("a-log-channel");
+
+    assertEquals("a-log-channel", loggingObject.getLogChannelId());
+  }
+}
diff --git a/engine/src/main/java/org/apache/hop/www/AddPipelineServlet.java 
b/engine/src/main/java/org/apache/hop/www/AddPipelineServlet.java
index 008cc348f4..abdefba115 100644
--- a/engine/src/main/java/org/apache/hop/www/AddPipelineServlet.java
+++ b/engine/src/main/java/org/apache/hop/www/AddPipelineServlet.java
@@ -29,7 +29,6 @@ import org.apache.hop.core.Const;
 import org.apache.hop.core.annotations.HopServerServlet;
 import org.apache.hop.core.exception.HopException;
 import org.apache.hop.core.logging.LogChannelFileWriter;
-import org.apache.hop.core.logging.LoggingObjectType;
 import org.apache.hop.core.logging.SimpleLoggingObject;
 import org.apache.hop.core.util.FileUtil;
 import org.apache.hop.core.vfs.HopVfs;
@@ -123,9 +122,8 @@ public class AddPipelineServlet extends BaseHttpServlet 
implements IHopServerPlu
 
       String serverObjectId = UUID.randomUUID().toString();
       SimpleLoggingObject servletLoggingObject =
-          new SimpleLoggingObject(CONTEXT_PATH, LoggingObjectType.HOP_SERVER, 
null);
-      servletLoggingObject.setContainerObjectId(serverObjectId);
-      
servletLoggingObject.setLogLevel(pipelineExecutionConfiguration.getLogLevel());
+          getServletLogging(
+              CONTEXT_PATH, serverObjectId, 
pipelineExecutionConfiguration.getLogLevel());
 
       IHopMetadataProvider metadataProvider =
           new MultiMetadataProvider(
diff --git a/engine/src/main/java/org/apache/hop/www/BaseHttpServlet.java 
b/engine/src/main/java/org/apache/hop/www/BaseHttpServlet.java
index 85650eee28..f7f969d501 100644
--- a/engine/src/main/java/org/apache/hop/www/BaseHttpServlet.java
+++ b/engine/src/main/java/org/apache/hop/www/BaseHttpServlet.java
@@ -33,6 +33,10 @@ import org.apache.hc.core5.http.ContentType;
 import org.apache.hop.core.Const;
 import org.apache.hop.core.logging.ILogChannel;
 import org.apache.hop.core.logging.LogChannel;
+import org.apache.hop.core.logging.LogLevel;
+import org.apache.hop.core.logging.LoggingObjectType;
+import org.apache.hop.core.logging.LoggingRegistry;
+import org.apache.hop.core.logging.SimpleLoggingObject;
 import org.apache.hop.core.variables.IVariables;
 import org.apache.hop.core.variables.Variables;
 import org.apache.hop.core.xml.XmlHandler;
@@ -378,4 +382,28 @@ public class BaseHttpServlet extends HttpServlet {
     }
     return null;
   }
+
+  /**
+   * The logging object a servlet hands to the pipeline or workflow it 
creates, as its logging
+   * parent.
+   *
+   * <p>It is given a log channel of its own: everything the pipeline or 
workflow logs is a child of
+   * it, so writing that log to a file hangs the file writer on this log 
channel. Without one there
+   * is nothing to hang it on. See issue #4677.
+   *
+   * @param contextPath the path of the servlet, to name the logging object 
after
+   * @param serverObjectId the id this server knows the pipeline or workflow by
+   * @param level the log level that was asked for
+   * @return the logging object to use as a parent
+   */
+  protected SimpleLoggingObject getServletLogging(
+      final String contextPath, final String serverObjectId, final LogLevel 
level) {
+    SimpleLoggingObject servletLoggingObject =
+        new SimpleLoggingObject(contextPath, LoggingObjectType.HOP_SERVER, 
null);
+    servletLoggingObject.setContainerObjectId(serverObjectId);
+    servletLoggingObject.setLogLevel(level);
+    servletLoggingObject.setLogChannelId(
+        
LoggingRegistry.getInstance().registerLoggingSource(servletLoggingObject, 
true));
+    return servletLoggingObject;
+  }
 }
diff --git a/engine/src/main/java/org/apache/hop/www/BaseWorkflowServlet.java 
b/engine/src/main/java/org/apache/hop/www/BaseWorkflowServlet.java
index 603d1a022e..79470adbe0 100644
--- a/engine/src/main/java/org/apache/hop/www/BaseWorkflowServlet.java
+++ b/engine/src/main/java/org/apache/hop/www/BaseWorkflowServlet.java
@@ -23,8 +23,6 @@ import org.apache.commons.lang3.StringUtils;
 import org.apache.hop.core.Const;
 import org.apache.hop.core.exception.HopException;
 import org.apache.hop.core.logging.LogChannelFileWriter;
-import org.apache.hop.core.logging.LogLevel;
-import org.apache.hop.core.logging.LoggingObjectType;
 import org.apache.hop.core.logging.SimpleLoggingObject;
 import org.apache.hop.core.parameters.INamedParameters;
 import org.apache.hop.core.parameters.UnknownParamException;
@@ -64,7 +62,8 @@ public abstract class BaseWorkflowServlet extends 
BodyHttpServlet {
     String serverObjectId = UUID.randomUUID().toString();
 
     SimpleLoggingObject servletLoggingObject =
-        getServletLogging(serverObjectId, 
workflowExecutionConfiguration.getLogLevel());
+        getServletLogging(
+            getContextPath(), serverObjectId, 
workflowExecutionConfiguration.getLogLevel());
 
     // Create the workflow and store in the list...
     //
@@ -114,7 +113,8 @@ public abstract class BaseWorkflowServlet extends 
BodyHttpServlet {
 
     String serverObjectId = UUID.randomUUID().toString();
     SimpleLoggingObject servletLoggingObject =
-        getServletLogging(serverObjectId, 
pipelineExecutionConfiguration.getLogLevel());
+        getServletLogging(
+            getContextPath(), serverObjectId, 
pipelineExecutionConfiguration.getLogLevel());
 
     // Create the pipeline and store in the list...
     //
@@ -192,12 +192,4 @@ public abstract class BaseWorkflowServlet extends 
BodyHttpServlet {
     }
     workflow.activateParameters(workflow);
   }
-
-  private SimpleLoggingObject getServletLogging(final String serverObjectId, 
final LogLevel level) {
-    SimpleLoggingObject servletLoggingObject =
-        new SimpleLoggingObject(getContextPath(), 
LoggingObjectType.HOP_SERVER, null);
-    servletLoggingObject.setContainerObjectId(serverObjectId);
-    servletLoggingObject.setLogLevel(level);
-    return servletLoggingObject;
-  }
 }
diff --git 
a/engine/src/test/java/org/apache/hop/www/RegisterPipelineServletLogFileTest.java
 
b/engine/src/test/java/org/apache/hop/www/RegisterPipelineServletLogFileTest.java
new file mode 100644
index 0000000000..7e3a25bc5d
--- /dev/null
+++ 
b/engine/src/test/java/org/apache/hop/www/RegisterPipelineServletLogFileTest.java
@@ -0,0 +1,155 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.www;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import org.apache.hop.core.HopEnvironment;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.logging.LoggingRegistry;
+import org.apache.hop.core.metadata.SerializableMetadataProvider;
+import org.apache.hop.core.variables.Variables;
+import org.apache.hop.metadata.serializer.memory.MemoryMetadataProvider;
+import org.apache.hop.metadata.serializer.multi.MultiMetadataProvider;
+import org.apache.hop.pipeline.PipelineConfiguration;
+import org.apache.hop.pipeline.PipelineExecutionConfiguration;
+import org.apache.hop.pipeline.PipelineMeta;
+import org.apache.hop.pipeline.config.PipelineRunConfiguration;
+import org.apache.hop.pipeline.engine.IPipelineEngine;
+import org.apache.hop.pipeline.engines.local.LocalPipelineRunConfiguration;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Registering a pipeline with a log file used to fail with a 
NullPointerException, because the
+ * logging object of the servlet had no log channel to hang the log file 
writer on. See issue #4677.
+ */
+class RegisterPipelineServletLogFileTest {
+
+  private static final String RUN_CONFIGURATION_NAME = "local";
+
+  private Path logFile;
+
+  @BeforeAll
+  static void setUpBeforeClass() throws HopException {
+    HopEnvironment.init();
+  }
+
+  @BeforeEach
+  void setUp() throws Exception {
+    logFile = Files.createTempFile("register-pipeline-servlet", ".log");
+    Files.delete(logFile);
+  }
+
+  @AfterEach
+  void tearDown() throws Exception {
+    Files.deleteIfExists(logFile);
+  }
+
+  @Test
+  void registeringAPipelineWithALogFileWorks() throws Exception {
+    RegisterPipelineServlet servlet = createServlet();
+
+    IPipelineEngine<PipelineMeta> pipeline =
+        assertDoesNotThrow(() -> 
servlet.createPipeline(createPipelineConfiguration()));
+
+    assertNotNull(
+        pipeline.getParent().getLogChannelId(),
+        "The pipeline should log to a channel the log file writer can be found 
by");
+  }
+
+  /**
+   * Everything the pipeline logs has the logging object of the servlet as its 
parent, so the log
+   * file writer registered on it has to be found back from the log channel of 
the pipeline. Without
+   * that the log file stays empty.
+   */
+  @Test
+  void theLogFileWriterIsFoundFromTheLogChannelOfThePipeline() throws 
Exception {
+    RegisterPipelineServlet servlet = createServlet();
+
+    IPipelineEngine<PipelineMeta> pipeline = 
servlet.createPipeline(createPipelineConfiguration());
+
+    // This is what the server does next: it is only then that the pipeline 
creates the log channel
+    // it really runs with.
+    pipeline.prepareExecution();
+
+    assertNotNull(
+        LoggingRegistry.getInstance()
+            
.getLogChannelFileWriterBuffer(pipeline.getLogChannel().getLogChannelId()),
+        "The pipeline should write its log to the log file that was asked 
for");
+  }
+
+  /** Registering a pipeline without a log file keeps working. */
+  @Test
+  void registeringAPipelineWithoutALogFileWorks() throws Exception {
+    RegisterPipelineServlet servlet = createServlet();
+    PipelineConfiguration pipelineConfiguration = 
createPipelineConfiguration();
+    
pipelineConfiguration.getPipelineExecutionConfiguration().setSetLogfile(false);
+
+    IPipelineEngine<PipelineMeta> pipeline =
+        assertDoesNotThrow(() -> 
servlet.createPipeline(pipelineConfiguration));
+
+    assertNotNull(pipeline);
+  }
+
+  private RegisterPipelineServlet createServlet() {
+    HopServerConfig serverConfig = new HopServerConfig();
+    serverConfig.setMetadataProvider(new MultiMetadataProvider(new 
Variables()));
+
+    PipelineMap pipelineMap = new PipelineMap();
+    pipelineMap.setHopServerConfig(serverConfig);
+
+    RegisterPipelineServlet servlet = new RegisterPipelineServlet();
+    servlet.setup(pipelineMap, new WorkflowMap());
+    return servlet;
+  }
+
+  private PipelineConfiguration createPipelineConfiguration() throws 
HopException {
+    PipelineMeta pipelineMeta = new PipelineMeta();
+    pipelineMeta.setName("a-pipeline");
+
+    PipelineExecutionConfiguration executionConfiguration = new 
PipelineExecutionConfiguration();
+    executionConfiguration.setRunConfiguration(RUN_CONFIGURATION_NAME);
+    executionConfiguration.setSetLogfile(true);
+    executionConfiguration.setLogFileName(logFile.toString());
+
+    MemoryMetadataProvider metadataProvider = new MemoryMetadataProvider();
+    LocalPipelineRunConfiguration engineConfiguration = new 
LocalPipelineRunConfiguration();
+    engineConfiguration.setEnginePluginId("Local");
+    metadataProvider
+        .getSerializer(PipelineRunConfiguration.class)
+        .save(
+            new PipelineRunConfiguration(
+                RUN_CONFIGURATION_NAME,
+                "",
+                null,
+                new ArrayList<>(),
+                engineConfiguration,
+                null,
+                false));
+
+    return new PipelineConfiguration(
+        pipelineMeta, executionConfiguration, new 
SerializableMetadataProvider(metadataProvider));
+  }
+}

Reply via email to