From d6aea491c54b75e2c2891a4829dd13640a9f08db Mon Sep 17 00:00:00 2001 From: Luis <1105281+lpenap@users.noreply.github.com> Date: Wed, 16 Sep 2026 13:05:17 -0300 Subject: [PATCH] Remove sun.misc.Unsafe from tests and switch to constructor injection GuiAppenderTests allocated a MainWindow through sun.misc.Unsafe to skip the JFrame constructor on headless CI and then set private fields by reflection, producing "internal proprietary API" compiler warnings on every build. GuiAppender now depends on a small OutputSink interface that MainWindow implements, so the test passes a plain lambda instead. All Spring beans in the app package now use Lombok's @RequiredArgsConstructor with final fields instead of @Autowired field injection. MainWindow is reformatted to consistent four-space indentation while being rewritten. Co-Authored-By: Claude Fable 5.1 --- .../constructs/app/AppCommandLineRunner.java | 11 +- .../app/ExamplesCommandLineRunner.java | 15 +- .../constructs/app/ui/GuiAppender.java | 8 +- .../example/constructs/app/ui/MainWindow.java | 290 +++++++++--------- .../example/constructs/app/ui/OutputSink.java | 10 + .../constructs/app/ui/GuiAppenderTests.java | 26 +- 6 files changed, 173 insertions(+), 187 deletions(-) create mode 100644 src/main/java/com/penapereira/example/constructs/app/ui/OutputSink.java diff --git a/src/main/java/com/penapereira/example/constructs/app/AppCommandLineRunner.java b/src/main/java/com/penapereira/example/constructs/app/AppCommandLineRunner.java index 83b4e9d..cc671c3 100644 --- a/src/main/java/com/penapereira/example/constructs/app/AppCommandLineRunner.java +++ b/src/main/java/com/penapereira/example/constructs/app/AppCommandLineRunner.java @@ -4,22 +4,21 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.CommandLineRunner; import org.springframework.stereotype.Component; import com.penapereira.example.constructs.app.properties.Messages; import com.penapereira.example.constructs.app.ui.MainWindow; +import lombok.RequiredArgsConstructor; + @Component +@RequiredArgsConstructor public class AppCommandLineRunner implements CommandLineRunner { private static Logger log = LoggerFactory.getLogger(AppCommandLineRunner.class); - @Autowired - MainWindow main; - - @Autowired - Messages msg; + private final MainWindow main; + private final Messages msg; @Override public void run(String... args) throws Exception { diff --git a/src/main/java/com/penapereira/example/constructs/app/ExamplesCommandLineRunner.java b/src/main/java/com/penapereira/example/constructs/app/ExamplesCommandLineRunner.java index 42946c9..dd3b13d 100644 --- a/src/main/java/com/penapereira/example/constructs/app/ExamplesCommandLineRunner.java +++ b/src/main/java/com/penapereira/example/constructs/app/ExamplesCommandLineRunner.java @@ -5,7 +5,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.CommandLineRunner; import org.springframework.context.ApplicationContext; import org.springframework.stereotype.Component; @@ -13,19 +12,17 @@ import com.penapereira.example.constructs.app.properties.ApplicationProperties; import com.penapereira.example.constructs.app.properties.Messages; +import lombok.RequiredArgsConstructor; + @Component +@RequiredArgsConstructor public class ExamplesCommandLineRunner implements CommandLineRunner { private static final Logger log = LoggerFactory.getLogger(ExamplesCommandLineRunner.class); - @Autowired - private ApplicationContext ctx; - - @Autowired - Messages msg; - - @Autowired - ApplicationProperties props; + private final ApplicationContext ctx; + private final Messages msg; + private final ApplicationProperties props; @Override public void run(String... args) throws Exception { diff --git a/src/main/java/com/penapereira/example/constructs/app/ui/GuiAppender.java b/src/main/java/com/penapereira/example/constructs/app/ui/GuiAppender.java index 1056fe7..e5e8535 100644 --- a/src/main/java/com/penapereira/example/constructs/app/ui/GuiAppender.java +++ b/src/main/java/com/penapereira/example/constructs/app/ui/GuiAppender.java @@ -5,17 +5,17 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.InitializingBean; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.AppenderBase; +import lombok.RequiredArgsConstructor; @Component +@RequiredArgsConstructor public class GuiAppender extends AppenderBase implements InitializingBean { - @Autowired - private MainWindow mainWindow; + private final OutputSink output; @Override public void afterPropertiesSet() throws Exception { @@ -25,6 +25,6 @@ public void afterPropertiesSet() throws Exception { @Override protected void append(ILoggingEvent eventObject) { - EventQueue.invokeLater(() -> mainWindow.appendOutput(eventObject.getFormattedMessage() + System.lineSeparator())); + EventQueue.invokeLater(() -> output.appendOutput(eventObject.getFormattedMessage() + System.lineSeparator())); } } diff --git a/src/main/java/com/penapereira/example/constructs/app/ui/MainWindow.java b/src/main/java/com/penapereira/example/constructs/app/ui/MainWindow.java index aed7bfe..2573b9f 100644 --- a/src/main/java/com/penapereira/example/constructs/app/ui/MainWindow.java +++ b/src/main/java/com/penapereira/example/constructs/app/ui/MainWindow.java @@ -1,190 +1,186 @@ package com.penapereira.example.constructs.app.ui; +import java.awt.BorderLayout; import java.awt.Color; import java.awt.Cursor; import java.awt.Font; -import java.awt.BorderLayout; import java.awt.GridLayout; import java.io.IOException; +import java.util.Map; import javax.imageio.ImageIO; +import javax.swing.BorderFactory; import javax.swing.BoxLayout; +import javax.swing.JButton; +import javax.swing.JCheckBox; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JSeparator; import javax.swing.JTextArea; -import javax.swing.JCheckBox; import javax.swing.SwingConstants; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.stereotype.Component; +import com.penapereira.example.constructs.app.ExampleRunnerInterface; import com.penapereira.example.constructs.app.properties.ApplicationProperties; import com.penapereira.example.constructs.app.properties.Messages; -import com.penapereira.example.constructs.app.ExampleRunnerInterface; + +import lombok.RequiredArgsConstructor; @Component -public class MainWindow extends JFrame { +@RequiredArgsConstructor +public class MainWindow extends JFrame implements OutputSink { - private static final Logger log = LoggerFactory.getLogger(MainWindow.class); + private static final Logger log = LoggerFactory.getLogger(MainWindow.class); - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1L; + + private final Messages msg; + private final ApplicationProperties props; + private final ApplicationContext ctx; private JTextArea outputArea; private JCheckBox preserveLogCheck; + private Map examples; + + public void initializeFrame() { + prepareWindow(); + setContentPane(getMainComponent()); + adjustWindow(); + } + + private void adjustWindow() { + pack(); + setLocationRelativeTo(null); + setSize(getWidth() + 50, getHeight() + 50); + } + + private void prepareWindow() { + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + setTitle(msg.getWindowTitle()); + loadIcon(); + } + + private void loadIcon() { + try { + var icon = ImageIO.read(props.getAppIcon().getInputStream()); + setIconImage(icon); + } catch (IOException e) { + log.warn("Could not load app icon", e); + } + } + + private JPanel getMainComponent() { + JPanel mainPanel = new JPanel(new BorderLayout()); + + JPanel infoPanel = new JPanel(new GridLayout(4, 1)); + createCenteredTitle(msg.getGreeting(), infoPanel); + createCenteredLabelOnPanel(msg.getInfo(), infoPanel); + createCenteredHyperlink(msg.getHomeUrl(), infoPanel); + mainPanel.add(infoPanel, BorderLayout.NORTH); + + mainPanel.add(createExamplesPanel(), BorderLayout.WEST); + + outputArea = new JTextArea(10, 40); + outputArea.setEditable(false); + JScrollPane scrollPane = new JScrollPane(outputArea); + scrollPane.setBorder(BorderFactory.createTitledBorder(msg.getOutputTitle())); + + preserveLogCheck = new JCheckBox(msg.getPreserveLog()); + + JPanel outputPanel = new JPanel(new BorderLayout()); + outputPanel.add(preserveLogCheck, BorderLayout.NORTH); + outputPanel.add(scrollPane, BorderLayout.CENTER); + + mainPanel.add(outputPanel, BorderLayout.CENTER); + + return mainPanel; + } + + private void createCenteredTitle(String text, JPanel panel) { + JLabel title = new JLabel(text, JLabel.CENTER); + Font current = title.getFont(); + title.setFont(new Font(current.getName(), current.getStyle(), current.getSize() + 4)); + panel.add(title); + panel.add(new JSeparator(SwingConstants.HORIZONTAL)); + } + + private void createCenteredHyperlink(String text, JPanel panel) { + JLabel hyperlink = new JLabel(text, JLabel.CENTER); + hyperlink.setForeground(Color.decode(props.getLinkColor())); + hyperlink.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + hyperlink.addMouseListener(new HyperlinkMouseListener(props)); + panel.add(hyperlink); + } + + private void createCenteredLabelOnPanel(String text, JPanel panel) { + JLabel label = new JLabel(text); + label.setHorizontalAlignment(JLabel.CENTER); + panel.add(label); + } + + @Override + public void appendOutput(String text) { + if (outputArea != null) { + outputArea.append(text); + outputArea.setCaretPosition(outputArea.getDocument().getLength()); + } + } - @Autowired - Messages msg; - - @Autowired - ApplicationProperties props; - - @Autowired - ApplicationContext ctx; - - private java.util.Map examples; - - public MainWindow() { - super(); - } - - public void initializeFrame() { - prepareWindow(); - setContentPane(getMainComponent()); - adjustWindow(); - } - - private void adjustWindow() { - pack(); - setLocationRelativeTo(null); - setSize(getWidth() + 50, getHeight() + 50); - } - - private void prepareWindow() { - setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); - setTitle(msg.getWindowTitle()); - loadIcon(); - } - - private void loadIcon() { - try { - var icon = ImageIO.read(props.getAppIcon().getInputStream()); - setIconImage(icon); - } catch (IOException e) { - log.warn("Could not load app icon", e); - } - } - - private JPanel getMainComponent() { - JPanel mainPanel = new JPanel(new BorderLayout()); - - JPanel infoPanel = new JPanel(new GridLayout(4, 1)); - createCenteredTitle(msg.getGreeting(), infoPanel); - createCenteredLabelOnPanel(msg.getInfo(), infoPanel); - createCenteredHyperlink(msg.getHomeUrl(), infoPanel); - mainPanel.add(infoPanel, BorderLayout.NORTH); - - mainPanel.add(createExamplesPanel(), BorderLayout.WEST); + private JPanel createExamplesPanel() { + examples = ctx.getBeansOfType(ExampleRunnerInterface.class); - outputArea = new JTextArea(10, 40); - outputArea.setEditable(false); - JScrollPane scrollPane = new JScrollPane(outputArea); - scrollPane.setBorder(javax.swing.BorderFactory.createTitledBorder(msg.getOutputTitle())); + JPanel panel = new JPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + panel.setBorder(BorderFactory.createTitledBorder(msg.getExamplesFound())); - preserveLogCheck = new JCheckBox(msg.getPreserveLog()); + JButton allButton = new JButton("Run All"); + allButton.addActionListener(e -> runAllExamples()); + panel.add(allButton); - JPanel outputPanel = new JPanel(new BorderLayout()); - outputPanel.add(preserveLogCheck, BorderLayout.NORTH); - outputPanel.add(scrollPane, BorderLayout.CENTER); + examples.forEach((name, runner) -> { + String clean = name.replaceFirst("ExampleRunner", ""); + JButton btn = new JButton(clean); + btn.addActionListener(e -> runExample(runner)); + panel.add(btn); + }); - mainPanel.add(outputPanel, BorderLayout.CENTER); + return panel; + } - return mainPanel; + private void runExample(ExampleRunnerInterface runner) { + if (!preserveLogCheck.isSelected()) { + outputArea.setText(""); } - - private void createCenteredTitle(String text, JPanel panel) { - JLabel title = new JLabel(text, JLabel.CENTER); - Font current = title.getFont(); - title.setFont(new Font(current.getName(), current.getStyle(), current.getSize() + 4)); - panel.add(title); - panel.add(new JSeparator(SwingConstants.HORIZONTAL)); - } - - private void createCenteredHyperlink(String text, JPanel panel) { - JLabel hyperlink = new JLabel(text, JLabel.CENTER); - hyperlink.setForeground(Color.decode(props.getLinkColor())); - hyperlink.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); - hyperlink.addMouseListener(new HyperlinkMouseListener(props)); - panel.add(hyperlink); - } - - private void createCenteredLabelOnPanel(String text, JPanel panel) { - JLabel label = new JLabel(text); - label.setHorizontalAlignment(JLabel.CENTER); - panel.add(label); + new Thread(() -> { + try { + log.trace(msg.getSeparator()); + runner.runExample(); + } catch (Exception e) { + log.error("Error executing example", e); + } + }).start(); + } + + private void runAllExamples() { + if (!preserveLogCheck.isSelected()) { + outputArea.setText(""); } - - public void appendOutput(String text) { - if (outputArea != null) { - outputArea.append(text); - outputArea.setCaretPosition(outputArea.getDocument().getLength()); + new Thread(() -> { + examples.values().forEach(r -> { + try { + log.trace(msg.getSeparator()); + r.runExample(); + } catch (Exception e) { + log.error("Error executing example", e); } - } - - private JPanel createExamplesPanel() { - examples = ctx.getBeansOfType(ExampleRunnerInterface.class); - - JPanel panel = new JPanel(); - panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); - panel.setBorder(javax.swing.BorderFactory.createTitledBorder(msg.getExamplesFound())); - - javax.swing.JButton allButton = new javax.swing.JButton("Run All"); - allButton.addActionListener(e -> runAllExamples()); - panel.add(allButton); - - examples.forEach((name, runner) -> { - String clean = name.replaceFirst("ExampleRunner", ""); - javax.swing.JButton btn = new javax.swing.JButton(clean); - btn.addActionListener(e -> runExample(runner)); - panel.add(btn); - }); - - return panel; - } - - private void runExample(ExampleRunnerInterface runner) { - if (!preserveLogCheck.isSelected()) { - outputArea.setText(""); - } - new Thread(() -> { - try { - log.trace(msg.getSeparator()); - runner.runExample(); - } catch (Exception e) { - log.error("Error executing example", e); - } - }).start(); - } - - private void runAllExamples() { - if (!preserveLogCheck.isSelected()) { - outputArea.setText(""); - } - new Thread(() -> { - examples.values().forEach(r -> { - try { - log.trace(msg.getSeparator()); - r.runExample(); - } catch (Exception e) { - log.error("Error executing example", e); - } - }); - }).start(); - } + }); + }).start(); + } } diff --git a/src/main/java/com/penapereira/example/constructs/app/ui/OutputSink.java b/src/main/java/com/penapereira/example/constructs/app/ui/OutputSink.java new file mode 100644 index 0000000..8c28dea --- /dev/null +++ b/src/main/java/com/penapereira/example/constructs/app/ui/OutputSink.java @@ -0,0 +1,10 @@ +package com.penapereira.example.constructs.app.ui; + +/** + * Destination for text produced by the application, such as log output + * forwarded by {@link GuiAppender}. + */ +public interface OutputSink { + + void appendOutput(String text); +} diff --git a/src/test/java/com/penapereira/example/constructs/app/ui/GuiAppenderTests.java b/src/test/java/com/penapereira/example/constructs/app/ui/GuiAppenderTests.java index 598d2d9..4d58ec2 100644 --- a/src/test/java/com/penapereira/example/constructs/app/ui/GuiAppenderTests.java +++ b/src/test/java/com/penapereira/example/constructs/app/ui/GuiAppenderTests.java @@ -1,11 +1,8 @@ package com.penapereira.example.constructs.app.ui; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; import java.awt.EventQueue; -import java.lang.reflect.Field; - -import javax.swing.JTextArea; import org.junit.jupiter.api.Test; @@ -14,22 +11,9 @@ class GuiAppenderTests { @Test - void appendsMessageToTextArea() throws Exception { - // Allocate a MainWindow instance without triggering the JFrame constructor - Field theUnsafeField = sun.misc.Unsafe.class.getDeclaredField("theUnsafe"); - theUnsafeField.setAccessible(true); - sun.misc.Unsafe unsafe = (sun.misc.Unsafe) theUnsafeField.get(null); - - MainWindow window = (MainWindow) unsafe.allocateInstance(MainWindow.class); - JTextArea area = new JTextArea(); - Field f = MainWindow.class.getDeclaredField("outputArea"); - f.setAccessible(true); - f.set(window, area); - - GuiAppender appender = new GuiAppender(); - Field mw = GuiAppender.class.getDeclaredField("mainWindow"); - mw.setAccessible(true); - mw.set(appender, window); + void appendsMessageToOutputSink() throws Exception { + StringBuilder output = new StringBuilder(); + GuiAppender appender = new GuiAppender(output::append); appender.start(); LoggingEvent event = new LoggingEvent(); @@ -39,6 +23,6 @@ void appendsMessageToTextArea() throws Exception { appender.doAppend(event); EventQueue.invokeAndWait(() -> {}); - assertEquals("test message" + System.lineSeparator(), area.getText()); + assertEquals("test message" + System.lineSeparator(), output.toString()); } }