You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by pa...@apache.org on 2026/09/07 20:46:58 UTC

(groovy) branch master updated: GROOVY-12376: LookAndFeelHelper: check the Metal theme type before instantiating

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

paulk-asert pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git


The following commit(s) were added to refs/heads/master by this push:
     new 81b415227d GROOVY-12376: LookAndFeelHelper: check the Metal theme type before instantiating
81b415227d is described below

commit 81b415227d3d5cd381349624bb7712e04b23add5
Author: Paul King <pa...@asert.com.au>
AuthorDate: Mon Sep 7 15:38:24 2026 +1000

    GROOVY-12376: LookAndFeelHelper: check the Metal theme type before instantiating
    
    The Metal look-and-feel 'theme' attribute treated an unrecognized string as a
    class name and did Class.forName(name).getConstructor().newInstance() -- so it
    resolved, initialized and instantiated an arbitrary class named by the theme
    value before finding out whether it was even a MetalTheme (the cast failed only
    afterwards). It also resolved via this class's loader, so an application- or
    @Grab-supplied theme class was not found.
    
    Resolve without initializing via the thread context classloader (falling back to
    this class's loader when none is set), confirm the class is a MetalTheme before
    constructing it, and throw IllegalArgumentException otherwise. A non-theme class
    is now never initialized or constructed, and custom or @Grab-supplied theme
    classes resolve.
---
 .../groovy/groovy/swing/LookAndFeelHelper.groovy   |  19 +++-
 .../groovy/swing/LookAndFeelHelperThemeTest.groovy | 109 +++++++++++++++++++++
 2 files changed, 127 insertions(+), 1 deletion(-)

diff --git a/subprojects/groovy-swing/src/main/groovy/groovy/swing/LookAndFeelHelper.groovy b/subprojects/groovy-swing/src/main/groovy/groovy/swing/LookAndFeelHelper.groovy
index 1de431f95b..a9eacacfc2 100644
--- a/subprojects/groovy-swing/src/main/groovy/groovy/swing/LookAndFeelHelper.groovy
+++ b/subprojects/groovy-swing/src/main/groovy/groovy/swing/LookAndFeelHelper.groovy
@@ -18,6 +18,7 @@
  */
 package groovy.swing
 
+import java.lang.reflect.Modifier
 import javax.swing.*
 import javax.swing.plaf.metal.DefaultMetalTheme
 import javax.swing.plaf.metal.MetalLookAndFeel
@@ -94,7 +95,23 @@ class LookAndFeelHelper {
                     } else if (theme == 'steel') {
                         theme = new DefaultMetalTheme();
                     } else {
-                        theme = Class.forName(theme as String).getConstructor().newInstance()
+                        // resolve without initializing, confirm it is a MetalTheme before
+                        // constructing it, and prefer the context classloader so application
+                        // and @Grab-supplied theme classes resolve; the context loader may be
+                        // null, so fall back to this class's loader (the pre-existing behaviour)
+                        def loader = Thread.currentThread().contextClassLoader ?: LookAndFeelHelper.classLoader
+                        def themeClass = Class.forName(theme as String, false, loader)
+                        if (!MetalTheme.isAssignableFrom(themeClass)) {
+                            throw new IllegalArgumentException("Metal theme class ${themeClass.name} is not a ${MetalTheme.name}")
+                        }
+                        if (Modifier.isAbstract(themeClass.modifiers)) {
+                            throw new IllegalArgumentException("Metal theme class ${themeClass.name} is abstract and cannot be instantiated")
+                        }
+                        try {
+                            theme = themeClass.getConstructor().newInstance()
+                        } catch (NoSuchMethodException e) {
+                            throw new IllegalArgumentException("Metal theme class ${themeClass.name} must provide a public no-argument constructor", e)
+                        }
                     }
                 };
                 MetalLookAndFeel.currentTheme = theme
diff --git a/subprojects/groovy-swing/src/test/groovy/groovy/swing/LookAndFeelHelperThemeTest.groovy b/subprojects/groovy-swing/src/test/groovy/groovy/swing/LookAndFeelHelperThemeTest.groovy
new file mode 100644
index 0000000000..8d2119a602
--- /dev/null
+++ b/subprojects/groovy-swing/src/test/groovy/groovy/swing/LookAndFeelHelperThemeTest.groovy
@@ -0,0 +1,109 @@
+/*
+ *  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 groovy.swing
+
+import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+
+import javax.swing.plaf.metal.DefaultMetalTheme
+import javax.swing.plaf.metal.MetalLookAndFeel
+import javax.swing.plaf.metal.MetalTheme
+
+import static groovy.test.GroovyAssert.shouldFail
+
+class LookAndFeelHelperThemeTest {
+
+    // the handler sets MetalLookAndFeel.currentTheme (a global static) as a side effect;
+    // save and restore it so the tests do not leak state into one another
+    private MetalTheme originalTheme
+
+    @BeforeEach
+    void rememberTheme() {
+        originalTheme = MetalLookAndFeel.currentTheme
+    }
+
+    @AfterEach
+    void restoreTheme() {
+        MetalLookAndFeel.currentTheme = originalTheme
+    }
+
+    // the Metal 'theme' attribute handler; invoked directly so the test does not
+    // touch UIManager and therefore runs headless
+    private static metalThemeHandler() {
+        LookAndFeelHelper.instance.@extendedAttributes['javax.swing.plaf.metal.MetalLookAndFeel'].theme
+    }
+
+    @Test
+    void rejectsNonMetalThemeClassWithoutConstructingIt() {
+        ThemeProbeFlag.constructed = false
+        def handler = metalThemeHandler()
+        shouldFail(IllegalArgumentException) {
+            handler(null, 'groovy.swing.NonMetalThemeProbe')
+        }
+        assert !ThemeProbeFlag.constructed: 'a non-MetalTheme class must not be constructed'
+    }
+
+    @Test
+    void acceptsMetalThemeSubclass() {
+        def handler = metalThemeHandler()
+        // the handler returns the theme it set, so assert on that rather than the global
+        def result = handler(null, 'groovy.swing.CustomMetalThemeProbe')
+        assert result instanceof CustomMetalThemeProbe
+    }
+
+    @Test
+    void rejectsAbstractMetalThemeSubclass() {
+        def handler = metalThemeHandler()
+        def err = shouldFail(IllegalArgumentException) {
+            handler(null, 'groovy.swing.AbstractMetalThemeProbe')
+        }
+        assert err.message.contains('abstract')
+    }
+
+    @Test
+    void rejectsMetalThemeWithoutNoArgConstructor() {
+        def handler = metalThemeHandler()
+        def err = shouldFail(IllegalArgumentException) {
+            handler(null, 'groovy.swing.NoDefaultCtorThemeProbe')
+        }
+        assert err.message.contains('no-argument constructor')
+    }
+}
+
+class ThemeProbeFlag {
+    static boolean constructed = false
+}
+
+class NonMetalThemeProbe {
+    NonMetalThemeProbe() {
+        ThemeProbeFlag.constructed = true
+    }
+}
+
+class CustomMetalThemeProbe extends DefaultMetalTheme {
+}
+
+abstract class AbstractMetalThemeProbe extends DefaultMetalTheme {
+}
+
+class NoDefaultCtorThemeProbe extends DefaultMetalTheme {
+    NoDefaultCtorThemeProbe(String required) {
+    }
+}