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) {
+ }
+}