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 003677bce8 hop web CTabFolder escape and unit testing #6227 #6230  
(#6237)
003677bce8 is described below

commit 003677bce86dc703e26763687df0da84bf917cdb
Author: Bart Maertens <[email protected]>
AuthorDate: Sat Dec 20 07:54:37 2025 +0000

    hop web CTabFolder escape and unit testing #6227 #6230  (#6237)
    
    * #6227 Fix unit testing context menu options in hop-web
    
    The filterTestingActions() method uses getCurrentUnitTest() which iterates
    through HopGui.getExplorerPerspective().getItems() to find the pipeline's
    state map. This doesn't work in RAP/web mode.
    
    Fixed by adding getUnitTestFromContext() helper that directly accesses
    context.getPipelineGraph().getStateMap() instead.
    
    * Fix hop-web startup NoClassDefFoundError for CTabFolderRenderer
    
    The SafeCTabFolderRenderer inner class in PropsUi caused a 
NoClassDefFoundError
    when PropsUi was loaded in RAP/web mode, because Java resolves parent 
classes
    (CTabFolderRenderer) at class load time, not at instantiation time.
    
    Fixed by:
    1. Moving SafeCTabFolderRenderer to a separate file so it's only loaded when
       actually instantiated
    2. Using reflection in ensureSafeRenderer() to avoid any compile-time 
references
       to SafeCTabFolderRenderer or CTabFolderRenderer, preventing them from 
being
       loaded when PropsUi class is loaded
    3. Adding isWeb() check at the start of ensureSafeRenderer() to 
short-circuit
       in web mode
    
    This fixes a regression that broke the apache/hop-web:Development container.
    
    * excluded SafeCTabFolderRenderer from hop web image build. fixes #6230
    
    * fixes/changes to allow unit tests to show up and work in hop web. fixes 
#6227
    
    * Simplify SafeCTabFolderRenderer reflection code in PropsUi
    
    Replaces method iteration with direct cast for cleaner code while 
maintaining the same reflection approach to avoid class resolution at load time.
---
 docker/unified.Dockerfile                          |  16 +++-
 .../apache/hop/testing/gui/TestingGuiPlugin.java   | 103 ++++++++++-----------
 ...DrawGoldenDataSetOnTransformExtensionPoint.java |   8 +-
 .../DrawInputDataSetOnTransformExtensionPoint.java |   7 +-
 ...opGuiFlagPipelineForUnitTestExtensionPoint.java |  20 ++--
 .../xp/HopGuiFlagUnitTestExtensionPoint.java       |  17 ++--
 .../main/java/org/apache/hop/ui/core/PropsUi.java  |  79 ++++------------
 .../apache/hop/ui/core/SafeCTabFolderRenderer.java |  85 +++++++++++++++++
 8 files changed, 198 insertions(+), 137 deletions(-)

diff --git a/docker/unified.Dockerfile b/docker/unified.Dockerfile
index d828fae106..64c48df365 100644
--- a/docker/unified.Dockerfile
+++ b/docker/unified.Dockerfile
@@ -138,7 +138,7 @@ FROM builder-${BUILDER_TYPE} AS builder-selected
 FROM alpine:latest AS builder
 
 # Install tools needed for preparation
-RUN apk add --no-cache unzip bash openjdk17-jre
+RUN apk add --no-cache unzip zip bash openjdk17-jre
 
 WORKDIR /build
 
@@ -189,6 +189,20 @@ RUN mkdir -p /build/hop-web-prepared/webapps/ROOT && \
     cp -r /build/assemblies/client/target/hop/lib/beam/* 
/build/hop-web-prepared/webapps/ROOT/WEB-INF/lib/ && \
     cp -r /build/assemblies/client/target/hop/lib/core/* 
/build/hop-web-prepared/webapps/ROOT/WEB-INF/lib/ && \
     rm /build/hop-web-prepared/webapps/ROOT/WEB-INF/lib/hop-ui-rcp* && \
+    # Remove SafeCTabFolderRenderer.class from hop-ui JAR - it extends 
CTabFolderRenderer
+    # which doesn't exist in RAP/web mode, causing NoClassDefFoundError
+    for jar in /build/hop-web-prepared/webapps/ROOT/WEB-INF/lib/hop-ui-*.jar; 
do \
+        if [ -f "$jar" ]; then \
+            cd /tmp && \
+            unzip -q "$jar" -d hop-ui-temp && \
+            rm -f 
hop-ui-temp/org/apache/hop/ui/core/SafeCTabFolderRenderer.class && \
+            rm -f "$jar" && \
+            cd hop-ui-temp && \
+            zip -q -r "$jar" . && \
+            cd /build && \
+            rm -rf /tmp/hop-ui-temp; \
+        fi; \
+    done && \
     mkdir -p /build/hop-web-prepared/bin && \
     cp -r /build/docker/resources/run-web.sh 
/build/hop-web-prepared/bin/run-web.sh
 
diff --git 
a/plugins/misc/testing/src/main/java/org/apache/hop/testing/gui/TestingGuiPlugin.java
 
b/plugins/misc/testing/src/main/java/org/apache/hop/testing/gui/TestingGuiPlugin.java
index 69d6cd2a8a..958714e8e9 100644
--- 
a/plugins/misc/testing/src/main/java/org/apache/hop/testing/gui/TestingGuiPlugin.java
+++ 
b/plugins/misc/testing/src/main/java/org/apache/hop/testing/gui/TestingGuiPlugin.java
@@ -162,10 +162,10 @@ public class TestingGuiPlugin {
     PipelineMeta pipelineMeta = context.getPipelineMeta();
     TransformMeta transformMeta = context.getTransformMeta();
 
-    if (checkTestPresent(hopGui, pipelineMeta)) {
+    if (checkTestPresent(hopGui, context)) {
       return;
     }
-    PipelineUnitTest unitTest = getCurrentUnitTest(pipelineMeta);
+    PipelineUnitTest unitTest = getUnitTestFromContext(context);
 
     try {
 
@@ -311,12 +311,12 @@ public class TestingGuiPlugin {
     TransformMeta transformMeta = context.getTransformMeta();
     IVariables variables = context.getPipelineGraph().getVariables();
 
-    if (checkTestPresent(hopGui, pipelineMeta)) {
+    if (checkTestPresent(hopGui, context)) {
       return;
     }
 
     try {
-      PipelineUnitTest currentUnitTest = getCurrentUnitTest(pipelineMeta);
+      PipelineUnitTest currentUnitTest = getUnitTestFromContext(context);
 
       PipelineUnitTestSetLocation inputLocation =
           currentUnitTest.findInputLocation(transformMeta.getName());
@@ -338,9 +338,9 @@ public class TestingGuiPlugin {
     }
   }
 
-  private boolean checkTestPresent(HopGui hopGui, PipelineMeta pipelineMeta) {
-
-    PipelineUnitTest activeTest = getCurrentUnitTest(pipelineMeta);
+  private boolean checkTestPresent(HopGui hopGui, 
HopGuiPipelineTransformContext context) {
+    // Get the unit test directly from the pipeline graph context (works in 
web/RAP mode)
+    PipelineUnitTest activeTest = getUnitTestFromContext(context);
     if (activeTest != null) {
       return false;
     }
@@ -376,10 +376,10 @@ public class TestingGuiPlugin {
     HopGui hopGui = HopGui.getInstance();
     IHopMetadataProvider metadataProvider = hopGui.getMetadataProvider();
 
-    if (checkTestPresent(hopGui, sourcePipelineMeta)) {
+    if (checkTestPresent(hopGui, context)) {
       return;
     }
-    PipelineUnitTest unitTest = getCurrentUnitTest(sourcePipelineMeta);
+    PipelineUnitTest unitTest = getUnitTestFromContext(context);
 
     try {
       // Create a copy and modify the pipeline
@@ -528,12 +528,12 @@ public class TestingGuiPlugin {
     TransformMeta transformMeta = context.getTransformMeta();
     IVariables variables = context.getPipelineGraph().getVariables();
 
-    if (checkTestPresent(hopGui, pipelineMeta)) {
+    if (checkTestPresent(hopGui, context)) {
       return;
     }
 
     try {
-      PipelineUnitTest currentUnitTest = getCurrentUnitTest(pipelineMeta);
+      PipelineUnitTest currentUnitTest = getUnitTestFromContext(context);
 
       PipelineUnitTestSetLocation goldenLocation =
           currentUnitTest.findGoldenLocation(transformMeta.getName());
@@ -558,7 +558,10 @@ public class TestingGuiPlugin {
   @GuiContextActionFilter(parentId = HopGuiPipelineTransformContext.CONTEXT_ID)
   public boolean filterTestingActions(
       String contextActionId, HopGuiPipelineTransformContext context) {
-    PipelineUnitTest currentTest = 
getCurrentUnitTest(context.getPipelineMeta());
+    // Get the unit test directly from the pipeline graph context
+    // Using getCurrentUnitTest(pipelineMeta) doesn't work in web/RAP mode 
because
+    // it looks for the pipeline in HopGui.getExplorerPerspective().getItems()
+    PipelineUnitTest currentTest = getUnitTestFromContext(context);
 
     // Input & golden data set handling
     //
@@ -605,6 +608,22 @@ public class TestingGuiPlugin {
     return true;
   }
 
+  /**
+   * Get the active unit test directly from the pipeline graph context. This 
method is used by
+   * filterTestingActions as an alternative to 
getCurrentUnitTest(pipelineMeta) which doesn't work
+   * in web/RAP mode because it relies on 
HopGui.getExplorerPerspective().getItems().
+   *
+   * @param context The pipeline transform context
+   * @return The active PipelineUnitTest or null if none is active
+   */
+  private PipelineUnitTest 
getUnitTestFromContext(HopGuiPipelineTransformContext context) {
+    Map<String, Object> stateMap = context.getPipelineGraph().getStateMap();
+    if (stateMap == null) {
+      return null;
+    }
+    return (PipelineUnitTest) 
stateMap.get(DataSetConst.STATE_KEY_ACTIVE_UNIT_TEST);
+  }
+
   /** Create a new data set with the output from */
   @GuiContextAction(
       id = "pipeline-graph-transform-20400-create-data-set-from-transform",
@@ -649,7 +668,7 @@ public class TestingGuiPlugin {
               hopGui.getShell());
       if (manager.newMetadata(dataSet) != null) {
 
-        PipelineUnitTest unitTest = getCurrentUnitTest(pipelineMeta);
+        PipelineUnitTest unitTest = getUnitTestFromContext(context);
         if (unitTest == null) {
           return;
         }
@@ -1205,9 +1224,7 @@ public class TestingGuiPlugin {
       category = "i18n::TestingGuiPlugin.Category",
       categoryOrder = "8")
   public void 
enableTweakRemoveTransformInUnitTest(HopGuiPipelineTransformContext context) {
-    IVariables variables = context.getPipelineGraph().getVariables();
-    tweakRemoveTransformInUnitTest(
-        variables, context.getPipelineMeta(), context.getTransformMeta(), 
true);
+    tweakRemoveTransformInUnitTest(context, true);
   }
 
   @GuiContextAction(
@@ -1220,20 +1237,12 @@ public class TestingGuiPlugin {
       category = "i18n::TestingGuiPlugin.Category",
       categoryOrder = "8")
   public void 
disableTweakRemoveTransformInUnitTest(HopGuiPipelineTransformContext context) {
-    tweakRemoveTransformInUnitTest(
-        context.getPipelineGraph().getVariables(),
-        context.getPipelineMeta(),
-        context.getTransformMeta(),
-        false);
+    tweakRemoveTransformInUnitTest(context, false);
   }
 
-  public void tweakRemoveTransformInUnitTest(
-      IVariables variables,
-      PipelineMeta pipelineMeta,
-      TransformMeta transformMeta,
-      boolean enable) {
-    tweakUnitTestTransform(
-        variables, pipelineMeta, transformMeta, 
PipelineTweak.REMOVE_TRANSFORM, enable);
+  private void tweakRemoveTransformInUnitTest(
+      HopGuiPipelineTransformContext context, boolean enable) {
+    tweakUnitTestTransform(context, PipelineTweak.REMOVE_TRANSFORM, enable);
   }
 
   @GuiContextAction(
@@ -1246,11 +1255,7 @@ public class TestingGuiPlugin {
       category = "i18n::TestingGuiPlugin.Category",
       categoryOrder = "8")
   public void 
enableTweakBypassTransformInUnitTest(HopGuiPipelineTransformContext context) {
-    tweakBypassTransformInUnitTest(
-        context.getPipelineGraph().getVariables(),
-        context.getPipelineMeta(),
-        context.getTransformMeta(),
-        true);
+    tweakBypassTransformInUnitTest(context, true);
   }
 
   @GuiContextAction(
@@ -1263,39 +1268,31 @@ public class TestingGuiPlugin {
       category = "i18n::TestingGuiPlugin.Category",
       categoryOrder = "8")
   public void 
disableTweakBypassTransformInUnitTest(HopGuiPipelineTransformContext context) {
-    tweakBypassTransformInUnitTest(
-        context.getPipelineGraph().getVariables(),
-        context.getPipelineMeta(),
-        context.getTransformMeta(),
-        false);
+    tweakBypassTransformInUnitTest(context, false);
   }
 
-  public void tweakBypassTransformInUnitTest(
-      IVariables variables,
-      PipelineMeta pipelineMeta,
-      TransformMeta transformMeta,
-      boolean enable) {
-    tweakUnitTestTransform(
-        variables, pipelineMeta, transformMeta, 
PipelineTweak.BYPASS_TRANSFORM, enable);
+  private void tweakBypassTransformInUnitTest(
+      HopGuiPipelineTransformContext context, boolean enable) {
+    tweakUnitTestTransform(context, PipelineTweak.BYPASS_TRANSFORM, enable);
   }
 
   private void tweakUnitTestTransform(
-      IVariables variables,
-      PipelineMeta pipelineMeta,
-      TransformMeta transformMeta,
-      PipelineTweak tweak,
-      boolean enable) {
+      HopGuiPipelineTransformContext context, PipelineTweak tweak, boolean 
enable) {
     HopGui hopGui = HopGui.getInstance();
     IHopMetadataProvider metadataProvider = hopGui.getMetadataProvider();
+    PipelineMeta pipelineMeta = context.getPipelineMeta();
+    TransformMeta transformMeta = context.getTransformMeta();
+    IVariables variables = context.getPipelineGraph().getVariables();
+
     if (transformMeta == null || pipelineMeta == null) {
       return;
     }
-    if (checkTestPresent(hopGui, pipelineMeta)) {
+    if (checkTestPresent(hopGui, context)) {
       return;
     }
 
     try {
-      PipelineUnitTest unitTest = getCurrentUnitTest(pipelineMeta);
+      PipelineUnitTest unitTest = getUnitTestFromContext(context);
       PipelineUnitTestTweak unitTestTweak = 
unitTest.findTweak(transformMeta.getName());
       if (unitTestTweak != null) {
         unitTest.getTweaks().remove(unitTestTweak);
@@ -1308,7 +1305,7 @@ public class TestingGuiPlugin {
 
       selectUnitTest(pipelineMeta, unitTest);
 
-      hopGui.getActiveFileTypeHandler().updateGui();
+      context.getPipelineGraph().updateGui();
     } catch (Exception exception) {
       new ErrorDialog(
           hopGui.getShell(),
diff --git 
a/plugins/misc/testing/src/main/java/org/apache/hop/testing/xp/DrawGoldenDataSetOnTransformExtensionPoint.java
 
b/plugins/misc/testing/src/main/java/org/apache/hop/testing/xp/DrawGoldenDataSetOnTransformExtensionPoint.java
index e2cdae000b..71b12cf6e4 100644
--- 
a/plugins/misc/testing/src/main/java/org/apache/hop/testing/xp/DrawGoldenDataSetOnTransformExtensionPoint.java
+++ 
b/plugins/misc/testing/src/main/java/org/apache/hop/testing/xp/DrawGoldenDataSetOnTransformExtensionPoint.java
@@ -34,7 +34,6 @@ import org.apache.hop.pipeline.PipelinePainterExtension;
 import org.apache.hop.pipeline.transform.TransformMeta;
 import org.apache.hop.testing.PipelineUnitTest;
 import org.apache.hop.testing.PipelineUnitTestSetLocation;
-import org.apache.hop.testing.gui.TestingGuiPlugin;
 import org.apache.hop.testing.util.DataSetConst;
 import org.apache.hop.ui.core.ConstUi;
 
@@ -51,7 +50,12 @@ public class DrawGoldenDataSetOnTransformExtensionPoint
       ILogChannel log, IVariables variables, PipelinePainterExtension ext) 
throws HopException {
     TransformMeta transformMeta = ext.transformMeta;
     PipelineMeta pipelineMeta = ext.pipelineMeta;
-    PipelineUnitTest unitTest = 
TestingGuiPlugin.getCurrentUnitTest(pipelineMeta);
+
+    // Get unit test directly from stateMap (works in both desktop and web/RAP 
mode)
+    PipelineUnitTest unitTest = null;
+    if (ext.stateMap != null) {
+      unitTest = (PipelineUnitTest) 
ext.stateMap.get(DataSetConst.STATE_KEY_ACTIVE_UNIT_TEST);
+    }
     if (unitTest != null) {
       drawGoldenSetMarker(ext, transformMeta, unitTest, ext.areaOwners);
     }
diff --git 
a/plugins/misc/testing/src/main/java/org/apache/hop/testing/xp/DrawInputDataSetOnTransformExtensionPoint.java
 
b/plugins/misc/testing/src/main/java/org/apache/hop/testing/xp/DrawInputDataSetOnTransformExtensionPoint.java
index d103e449e3..1fe19cec55 100644
--- 
a/plugins/misc/testing/src/main/java/org/apache/hop/testing/xp/DrawInputDataSetOnTransformExtensionPoint.java
+++ 
b/plugins/misc/testing/src/main/java/org/apache/hop/testing/xp/DrawInputDataSetOnTransformExtensionPoint.java
@@ -32,7 +32,6 @@ import org.apache.hop.pipeline.PipelinePainterExtension;
 import org.apache.hop.pipeline.transform.TransformMeta;
 import org.apache.hop.testing.PipelineUnitTest;
 import org.apache.hop.testing.PipelineUnitTestSetLocation;
-import org.apache.hop.testing.gui.TestingGuiPlugin;
 import org.apache.hop.testing.util.DataSetConst;
 
 @ExtensionPoint(
@@ -48,7 +47,11 @@ public class DrawInputDataSetOnTransformExtensionPoint
     TransformMeta transformMeta = ext.transformMeta;
     PipelineMeta pipelineMeta = ext.pipelineMeta;
 
-    PipelineUnitTest unitTest = 
TestingGuiPlugin.getCurrentUnitTest(pipelineMeta);
+    // Get unit test directly from stateMap (works in both desktop and web/RAP 
mode)
+    PipelineUnitTest unitTest = null;
+    if (ext.stateMap != null) {
+      unitTest = (PipelineUnitTest) 
ext.stateMap.get(DataSetConst.STATE_KEY_ACTIVE_UNIT_TEST);
+    }
     if (unitTest != null) {
       drawInputDataSetMarker(ext, transformMeta, unitTest, ext.areaOwners);
     }
diff --git 
a/plugins/misc/testing/src/main/java/org/apache/hop/testing/xp/HopGuiFlagPipelineForUnitTestExtensionPoint.java
 
b/plugins/misc/testing/src/main/java/org/apache/hop/testing/xp/HopGuiFlagPipelineForUnitTestExtensionPoint.java
index a217d34211..9283de23d0 100644
--- 
a/plugins/misc/testing/src/main/java/org/apache/hop/testing/xp/HopGuiFlagPipelineForUnitTestExtensionPoint.java
+++ 
b/plugins/misc/testing/src/main/java/org/apache/hop/testing/xp/HopGuiFlagPipelineForUnitTestExtensionPoint.java
@@ -26,8 +26,6 @@ import org.apache.hop.core.variables.IVariables;
 import org.apache.hop.pipeline.config.IPipelineEngineRunConfiguration;
 import org.apache.hop.pipeline.engine.IPipelineEngine;
 import org.apache.hop.pipeline.engines.local.LocalPipelineRunConfiguration;
-import org.apache.hop.testing.PipelineUnitTest;
-import org.apache.hop.testing.gui.TestingGuiPlugin;
 import org.apache.hop.testing.util.DataSetConst;
 
 @ExtensionPoint(
@@ -54,21 +52,17 @@ public class HopGuiFlagPipelineForUnitTestExtensionPoint
       return;
     }
 
-    PipelineUnitTest unitTest = 
TestingGuiPlugin.getCurrentUnitTest(pipeline.getPipelineMeta());
-    if (unitTest == null) {
-      return;
-    }
-
-    String unitTestName = unitTest.getName();
+    // The unit test variables should already be set by 
HopGuiFlagUnitTestExtensionPoint
+    // Just pass them through to the pipeline engine
+    String runUnitTest = variables.getVariable(DataSetConst.VAR_RUN_UNIT_TEST);
+    String unitTestName = 
variables.getVariable(DataSetConst.VAR_UNIT_TEST_NAME);
 
-    if (!StringUtil.isEmpty(unitTestName)) {
+    if ("Y".equalsIgnoreCase(runUnitTest) && 
!StringUtil.isEmpty(unitTestName)) {
       // We found the variables in the GUI and pass them to the pipeline right 
before (prepare)
       // execution
       //
-      pipeline.setVariable(
-          DataSetConst.VAR_RUN_UNIT_TEST, 
variables.getVariable(DataSetConst.VAR_RUN_UNIT_TEST));
-      pipeline.setVariable(
-          DataSetConst.VAR_UNIT_TEST_NAME, 
variables.getVariable(DataSetConst.VAR_UNIT_TEST_NAME));
+      pipeline.setVariable(DataSetConst.VAR_RUN_UNIT_TEST, runUnitTest);
+      pipeline.setVariable(DataSetConst.VAR_UNIT_TEST_NAME, unitTestName);
     }
   }
 }
diff --git 
a/plugins/misc/testing/src/main/java/org/apache/hop/testing/xp/HopGuiFlagUnitTestExtensionPoint.java
 
b/plugins/misc/testing/src/main/java/org/apache/hop/testing/xp/HopGuiFlagUnitTestExtensionPoint.java
index 2c91d5cdec..dacf392b67 100644
--- 
a/plugins/misc/testing/src/main/java/org/apache/hop/testing/xp/HopGuiFlagUnitTestExtensionPoint.java
+++ 
b/plugins/misc/testing/src/main/java/org/apache/hop/testing/xp/HopGuiFlagUnitTestExtensionPoint.java
@@ -25,7 +25,6 @@ import org.apache.hop.core.util.StringUtil;
 import org.apache.hop.core.variables.IVariables;
 import org.apache.hop.pipeline.PipelineMeta;
 import org.apache.hop.testing.PipelineUnitTest;
-import org.apache.hop.testing.gui.TestingGuiPlugin;
 import org.apache.hop.testing.util.DataSetConst;
 import org.apache.hop.ui.hopgui.HopGui;
 import org.apache.hop.ui.hopgui.file.pipeline.HopGuiPipelineGraph;
@@ -46,11 +45,6 @@ public class HopGuiFlagUnitTestExtensionPoint implements 
IExtensionPoint<Pipelin
   public void callExtensionPoint(ILogChannel log, IVariables variables, 
PipelineMeta pipelineMeta)
       throws HopException {
 
-    PipelineUnitTest unitTest = 
TestingGuiPlugin.getCurrentUnitTest(pipelineMeta);
-    if (unitTest == null) {
-      return;
-    }
-
     // Look up the variables of the current active pipeline graph...
     //
     HopGuiPipelineGraph activePipelineGraph = HopGui.getActivePipelineGraph();
@@ -58,6 +52,17 @@ public class HopGuiFlagUnitTestExtensionPoint implements 
IExtensionPoint<Pipelin
       return;
     }
 
+    // Get unit test directly from the pipeline graph's state map (works in 
both desktop and web/RAP
+    // mode)
+    PipelineUnitTest unitTest = null;
+    java.util.Map<String, Object> stateMap = activePipelineGraph.getStateMap();
+    if (stateMap != null) {
+      unitTest = (PipelineUnitTest) 
stateMap.get(DataSetConst.STATE_KEY_ACTIVE_UNIT_TEST);
+    }
+    if (unitTest == null) {
+      return;
+    }
+
     String unitTestName = unitTest.getName();
 
     if (!StringUtil.isEmpty(unitTestName)) {
diff --git a/ui/src/main/java/org/apache/hop/ui/core/PropsUi.java 
b/ui/src/main/java/org/apache/hop/ui/core/PropsUi.java
index 00cf036b94..9fa1db70ac 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/PropsUi.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/PropsUi.java
@@ -37,15 +37,10 @@ import org.apache.hop.ui.hopgui.TextSizeUtilFacade;
 import org.apache.hop.ui.util.EnvironmentUtils;
 import org.eclipse.swt.SWT;
 import org.eclipse.swt.custom.CTabFolder;
-import org.eclipse.swt.custom.CTabFolderRenderer;
-import org.eclipse.swt.custom.CTabItem;
 import org.eclipse.swt.graphics.Color;
 import org.eclipse.swt.graphics.Font;
 import org.eclipse.swt.graphics.FontData;
-import org.eclipse.swt.graphics.GC;
-import org.eclipse.swt.graphics.Image;
 import org.eclipse.swt.graphics.RGB;
-import org.eclipse.swt.graphics.Rectangle;
 import org.eclipse.swt.layout.FormLayout;
 import org.eclipse.swt.widgets.Button;
 import org.eclipse.swt.widgets.Combo;
@@ -530,64 +525,28 @@ public class PropsUi extends Props {
    * @param tabFolder the CTabFolder to protect
    */
   private static void ensureSafeRenderer(CTabFolder tabFolder) {
-    // Only apply safe renderer on Linux where the issue occurs
-    if (Const.isLinux() && !(tabFolder.getRenderer() instanceof 
SafeCTabFolderRenderer)) {
-      tabFolder.setRenderer(new SafeCTabFolderRenderer(tabFolder));
-    }
-  }
-
-  /**
-   * A safe CTabFolderRenderer that catches IllegalArgumentException when 
drawing tab images. This
-   * prevents crashes on some Linux desktop environments where images may be 
invalid or disposed
-   * during rendering. When an image drawing error occurs, the image is 
skipped but the text is
-   * still rendered.
-   */
-  private static class SafeCTabFolderRenderer extends CTabFolderRenderer {
-    private final CTabFolder parentFolder;
-
-    SafeCTabFolderRenderer(CTabFolder parent) {
-      super(parent);
-      this.parentFolder = parent;
+    // CTabFolderRenderer and SafeCTabFolderRenderer are not available in RAP 
(web mode),
+    // so skip entirely. The class is also removed from the hop-ui JAR during 
Docker build.
+    // Use reflection to avoid compile-time class resolution that would cause 
NoClassDefFoundError
+    // in web mode when the class doesn't exist in the JAR.
+    if (EnvironmentUtils.getInstance().isWeb()) {
+      return;
     }
-
-    @Override
-    protected void draw(int part, int state, Rectangle bounds, GC gc) {
-      if (bounds != null && (bounds.width <= 0 || bounds.height <= 0)) {
-        return;
-      }
+    // Only apply safe renderer on Linux where the issue occurs
+    if (Const.isLinux()) {
       try {
-        super.draw(part, state, bounds, gc);
-      } catch (IllegalArgumentException e) {
-        // If image drawing fails, temporarily remove images from tab items 
and redraw
-        // This allows text to be rendered even when images are invalid
-        if (parentFolder != null && !parentFolder.isDisposed()) {
-          CTabItem[] items = parentFolder.getItems();
-          Image[] savedImages = new Image[items.length];
-          boolean hadImages = false;
-
-          // Save and temporarily clear images
-          for (int i = 0; i < items.length; i++) {
-            savedImages[i] = items[i].getImage();
-            if (savedImages[i] != null) {
-              items[i].setImage(null);
-              hadImages = true;
-            }
-          }
-
-          if (hadImages) {
-            try {
-              // Redraw without images - this should succeed and render the 
text
-              super.draw(part, state, bounds, gc);
-            } finally {
-              // Restore images (even if they're invalid, we'll catch the 
exception next time)
-              for (int i = 0; i < items.length; i++) {
-                if (savedImages[i] != null) {
-                  items[i].setImage(savedImages[i]);
-                }
-              }
-            }
-          }
+        // Use reflection to avoid compile-time dependency on 
SafeCTabFolderRenderer
+        // This prevents Java's bytecode verifier from trying to resolve the 
class at load time
+        Class<?> rendererClass = 
Class.forName("org.apache.hop.ui.core.SafeCTabFolderRenderer");
+        Object currentRenderer = tabFolder.getRenderer();
+        if (currentRenderer == null || 
!rendererClass.isInstance(currentRenderer)) {
+          Object safeRenderer =
+              
rendererClass.getConstructor(CTabFolder.class).newInstance(tabFolder);
+          tabFolder.setRenderer((org.eclipse.swt.custom.CTabFolderRenderer) 
safeRenderer);
         }
+      } catch (Exception e) {
+        // If SafeCTabFolderRenderer can't be loaded, just continue without 
the safe renderer
+        LogChannel.GENERAL.logDetailed("Could not apply 
SafeCTabFolderRenderer: " + e.getMessage());
       }
     }
   }
diff --git 
a/ui/src/main/java/org/apache/hop/ui/core/SafeCTabFolderRenderer.java 
b/ui/src/main/java/org/apache/hop/ui/core/SafeCTabFolderRenderer.java
new file mode 100644
index 0000000000..7d5a3830cc
--- /dev/null
+++ b/ui/src/main/java/org/apache/hop/ui/core/SafeCTabFolderRenderer.java
@@ -0,0 +1,85 @@
+/*
+ * 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.ui.core;
+
+import org.eclipse.swt.custom.CTabFolder;
+import org.eclipse.swt.custom.CTabFolderRenderer;
+import org.eclipse.swt.custom.CTabItem;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Rectangle;
+
+/**
+ * A safe CTabFolderRenderer that catches IllegalArgumentException when 
drawing tab images. This
+ * prevents crashes on some Linux desktop environments where images may be 
invalid or disposed
+ * during rendering. When an image drawing error occurs, the image is skipped 
but the text is still
+ * rendered.
+ *
+ * <p>This class is in a separate file to avoid loading CTabFolderRenderer 
(which doesn't exist in
+ * RAP/web mode) when PropsUi is loaded. The class is only instantiated on 
Linux desktop
+ * environments where it's needed.
+ */
+public class SafeCTabFolderRenderer extends CTabFolderRenderer {
+  private final CTabFolder parentFolder;
+
+  public SafeCTabFolderRenderer(CTabFolder parent) {
+    super(parent);
+    this.parentFolder = parent;
+  }
+
+  @Override
+  protected void draw(int part, int state, Rectangle bounds, GC gc) {
+    if (bounds != null && (bounds.width <= 0 || bounds.height <= 0)) {
+      return;
+    }
+    try {
+      super.draw(part, state, bounds, gc);
+    } catch (IllegalArgumentException e) {
+      // If image drawing fails, temporarily remove images from tab items and 
redraw
+      // This allows text to be rendered even when images are invalid
+      if (parentFolder != null && !parentFolder.isDisposed()) {
+        CTabItem[] items = parentFolder.getItems();
+        Image[] savedImages = new Image[items.length];
+        boolean hadImages = false;
+
+        // Save and temporarily clear images
+        for (int i = 0; i < items.length; i++) {
+          savedImages[i] = items[i].getImage();
+          if (savedImages[i] != null) {
+            items[i].setImage(null);
+            hadImages = true;
+          }
+        }
+
+        if (hadImages) {
+          try {
+            // Redraw without images - this should succeed and render the text
+            super.draw(part, state, bounds, gc);
+          } finally {
+            // Restore images (even if they're invalid, we'll catch the 
exception next time)
+            for (int i = 0; i < items.length; i++) {
+              if (savedImages[i] != null) {
+                items[i].setImage(savedImages[i]);
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}

Reply via email to