Blog-Archiv

Donnerstag, 5. März 2026

Copying a File to Another LINUX Computer at Home

If you want to copy a file from one LINUX machine to another using scp (Secure CoPy), the following is for you. Secure-copy is pre-installed on every LINUX machine, and its syntax is quite similar to normal copy via cp.

Imagine your computer is A, and you want to copy your local myfile to computer B into directory /home/user/. Both machines are up and running and connected to the same router/modem by cable or WIFI (wireless).

  1. Go to computer B. You can find out its IP-address by typing
    ifconfig | awk '/broadcast/ { print $2 }'
    # or: hostname -I
    in a command-line window. Let's assume the IP-address of B is 192.168.0.123.

  2. Then you need the name of the user on B for whom you want to copy the file.
    id | awk '{ print $1 }'
    This gives you the name of the user (in parentheses). Let's assume it is yourusername. Mind that you also need to know the password of that user on B, and that user must have write-privileges in the copy-target directory (/home/user/).

  3. Computer B must have an ssh server (service) installed and running. Check this by typing
    systemctl list-units --type service | grep ssh
    If you see no line like "ssh-service loaded active running ...", type
    sudo apt-get install openssh-server
    and enter the superuser-password, needed to install system-wide software. After that, the service should be up and running, check it again with systemctl.

  4. Now go to computer A. Open a command-line window and change to the directory where myfile is. Then type
    scp myfile yourusername@192.168.0.123:/home/user/
    You will be asked to input the password of yourusername on computer B. Enter it and press RETURN.

  5. A progress indication will be displayed on the command-line for the time of copying. Finally check if the file arrived on computer B by going there and typing
    ls -l /home/user/myfile
    I hope it will work for you as good as it worked for me!

If you prefer to do the copy with the graphical user-interface GNOME-Files (Nautilus?), you can use the input field on bottom of that file-browser:

With all that confusing artificial-intelligence on the web that offer you so many ways to do a file transfer, I felt the need to write down how simple it can be. Moreover it took me a while to find out the shortest way (samba is nice but more complicated!).




Freitag, 27. Februar 2026

JButton in Java/Swing JTable

There are numerous examples on the Internet about Java/Swing JButton instances in a JTable. None convinced me, so "I did it my way":-) Here comes the result.

JTable and JTree are the most complex Swing classes. They require understanding of a design-pattern called "Flyweight". In context of a table-view (which may show just a small part of the data), this pattern tells us that we should not use one object per table cell to draw it, because this could end up in big memory consumption. Better use a single "stamp" to draw any cell, and pass the cell data to that stamp directly before drawing. In other words, the stamp receives all the state it needs before its usage through a mostly big list of parameters. The cell-renderer in Swing implements exactly this concept.

Now lets think of a button in a table. We should not hold one button instance per table cell, the "Flyweight" pattern tells us. Instead we use a cell-renderer that paints all cells using the same button.

But what about the click onto that button? The cell-editor will be called when the user clicks onto a cell, that way we can receive it. The cell-editor is not for painting, it is for editing cell data. Because there are no data for a button, we use AbstractAction instances in the table data instead. In other words, the actions to be performed by the button must be added to the table data. Following the "Flyweight" pattern we use just one AbstractAction instance for all cells, so we always add the same action.

Let me start with these actions.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public abstract class TableAction extends AbstractAction
{
    public static final String ROW_COLUMN_SEPARATOR = "/";
    
    protected JTable table;
    protected DefaultTableModel tableModel;
    protected int row;
    protected int column;
    protected Vector<Object> rowData;

    protected abstract void actionPerformed();
    
    @Override
    public final void actionPerformed(ActionEvent event) {
        table = (JTable) event.getSource();
        
        String actionCommand = event.getActionCommand();
        String[] split = actionCommand.split(TableAction.ROW_COLUMN_SEPARATOR);
        row = Integer.valueOf(split[0].trim());
        column = Integer.valueOf(split[1].trim());
        
        tableModel = (DefaultTableModel) table.getModel();
        rowData = tableModel.getDataVector().get(row);
        
        actionPerformed();
    }
}

This will be the base class for all actions that must be contained in the table's data. It receives an ActionEvent that will be built by the cell-editor. From that event it provides an execution environment for its sub-classes, including clicked row and column of the table (line 17 - 20).

Lines 5 to 9 represent the execution environment. The actionPerformed(event) method on line 14 receives the table and a row/column string in event.getActionCommand(). It unpacks everthing and finally calls the abstract actionPerformed() method on line 25 that will be implemented by sub-classes.

I can derive an edit-action and a delete-action from TableAction.

public class EditCellAction extends TableAction
{
    private final int editingColumn;
    
    public EditCellAction(int editingColumn) {
        this.editingColumn = editingColumn;
    }
    
    @Override
    protected void actionPerformed() {
        String input = JOptionPane.showInputDialog(table, null, rowData.get(editingColumn));
        if (input != null)
            rowData.set(editingColumn, input);
        
        table.revalidate();
        table.repaint();
    }
}

This action receives the column number for its editing content in constructor. On action-performed, it opens an input-dialog for editing the content which must be text. When the dialog was committed, it sets the text back into the table data.

public class DeleteRowAction extends TableAction
{
    @Override
    protected void actionPerformed() {
        tableModel.getDataVector().remove(row);
        table.revalidate();
        table.repaint();
    }
}

These actions anticipate that a data-vector is backing the table. If this is not the case for your table, you may find a way to adapt the source-code. This article is just about how to get buttons into a table.

As you can see, it is easy to write other actions that can work upon the table data in any way. Before showing how to add them to the table data, let me introduce the "Flyweight" cell-editor that drives these actions. It will be just one cell-editor for all kinds of actions.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public class ButtonCellEditor extends AbstractCellEditor implements TableCellEditor
{
    @Override
    public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
        Action action = (Action) value;
        ActionEvent event = new ActionEvent(
                table, 
                ActionEvent.ACTION_PERFORMED, 
                row + TableAction.ROW_COLUMN_SEPARATOR + column);
        action.actionPerformed(event);
        return null;
    }
    
    @Override
    public Object getCellEditorValue() {
        return null;
    }
}

Line 4 is the core of cell-editors, it returns the Component that lets edit the table cell data. But as there are no real data, I return null on line 11. In-between, this method receives the AbstractAction instance (from table data) on line 5. After casting it to Action, which is the AbstractAction interface, it can execute the action on line 10. On lines 6 - 9, the ActionEvent is built together, all from the long "Flyweight" parameter list.

This class derives AbstractCellEditor because that super-class provides default implementations for the big interface TableCellEditor. Important is that it returns true from isCellEditable(), that makes our cell-editor receive the user's click. The getCellEditorValue() method is a duty inherited from interface TableCellEditor, returning null does not cause any damage.

The last thing I need now is the cell-renderer.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class ButtonCellRenderer implements TableCellRenderer
{
    private final JButton button;
    
    public ButtonCellRenderer(String label) {
        this.button = new JButton(label);
        button.setBorder(null); // else label is just "..."
    }
    
    @Override
    public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
        return button;
    }
}

That was easy. The "Flyweight" method on line 11 just returns the button that was built in constructor.

Now I have everything to build together a JTable with buttons. Mind that this is just a demo, and there can be lots of improvements on following code. The table will show just one text column, and two buttons, one to edit the text column, one to delete the table row where it is in.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import java.awt.Component;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.util.Vector;
import javax.swing.*;
import javax.swing.table.*;

public class ButtonsInTableExample
{
    private JTable table;
    
    public ButtonsInTableExample() {
        Vector<Object> columnHeaders = new Vector<>();
        columnHeaders.add("Message");
        columnHeaders.add("Edit");
        columnHeaders.add("Delete");

        // one single action for all rows of a column
        TableAction editAction = new EditCellAction(0); // edit column 0
        TableAction deleteAction = new DeleteRowAction();
        
        Vector<Vector<Object>> data = new Vector<>(); // example rows
        for (int i = 0; i < 3; i++) {
            final Vector<Object> row = new Vector<>();
            row.add("Hello "+i); // column 0
            row.add(editAction);
            row.add(deleteAction);
            data.add(row);
        }
        
        TableModel model = new DefaultTableModel(data, columnHeaders) {
            @Override
            public Class<?> getColumnClass(int columnIndex) {
                if (columnIndex == 1)
                    return EditCellAction.class;
                if (columnIndex == 2)
                    return DeleteRowAction.class;
                return String.class;
            }
        };
        
        this.table = new JTable(model);
        
        // one cell-renderer per button-column
        table.setDefaultRenderer(EditCellAction.class, new ButtonCellRenderer("\u270E"));
        table.setDefaultRenderer(DeleteRowAction.class, new ButtonCellRenderer("\u2715"));
        
        // one generic cell-editor for all button-columns
        table.setDefaultEditor(AbstractAction.class, new ButtonCellEditor());
        
        table.getColumnModel().getColumn(1).setMaxWidth(40);
        table.getColumnModel().getColumn(2).setMaxWidth(40);
    }
}

For try out, you can find the full source-code in just one class on bottom of the article.

On line 10 you see the target of the constructor below, the JTable to build.
Lines 13 - 16 create the column header labels.
Lines 19 and 20 create the action instances that have to be in the table's data.
Lines 22 - 29 build three example rows, so that there is something to try out. Important are lines 26 and 27 where I add actions to the table data.
Lines 31 - 40 create the table-model. I overwrite the getColumnClass() method to return the correct action-classes according to the given column-index. This is important.
Line 42 builds the table from the model.
Lines 45 and 46 set the cell-renderers for the edit- and the delete-action.
Line 49 sets the generic cell-editor for all kinds of AbstractAction cells. Remember that its duty is just to cast the cell-value to Action and then perform it. This is not bound to any class. Mind further that it is important to use AbstractAction and not Action as first parameter in the setDefaultEditor() call, because the getDefaultEditor() method searches for a cell-editor only using super-classes of the column-class, interfaces are ignored!
Lines 51 and 52 just restrict the widths of the button cells.


Here comes the full source-code for trying this out - click left-side arrow to expand!
import java.awt.Component;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.util.Vector;
import javax.swing.*;
import javax.swing.table.*;

public class ButtonsInTableExample
{
    public static void main(String[] args) {
        JFrame frame = new JFrame("JTable button example");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.getContentPane().add(new JScrollPane(new ButtonsInTableExample().table));
        frame.setLocationByPlatform(true);
        frame.setSize(new Dimension(300, 200));
        frame.setVisible(true);
    }
    
    
    // button actions
    
    private static abstract class TableAction extends AbstractAction
    {
        public static final String ROW_COLUMN_SEPARATOR = "/";
        
        protected JTable table;
        protected DefaultTableModel tableModel;
        protected int row;
        protected int column;
        protected Vector<Object> rowData;

        protected abstract void actionPerformed();
        
        @Override
        public void actionPerformed(ActionEvent event) {
            table = (JTable) event.getSource();
            
            String actionCommand = event.getActionCommand();
            String[] split = actionCommand.split(TableAction.ROW_COLUMN_SEPARATOR);
            row = Integer.valueOf(split[0].trim());
            column = Integer.valueOf(split[1].trim());
            
            tableModel = (DefaultTableModel) table.getModel();
            rowData = tableModel.getDataVector().get(row);
            
            actionPerformed();
        }
    }
    
    private static class EditCellAction extends TableAction
    {
        private final int editingColumn;
        
        public EditCellAction(int editingColumn) {
            this.editingColumn = editingColumn;
        }
        
        @Override
        protected void actionPerformed() {
            String input = JOptionPane.showInputDialog(table, null, rowData.get(editingColumn));
            if (input != null)
                rowData.set(editingColumn, input);
            
            table.revalidate();
            table.repaint();
        }
    }
    
    private static class DeleteRowAction extends TableAction
    {
        @Override
        protected void actionPerformed() {
            tableModel.getDataVector().remove(row);
            table.revalidate();
            table.repaint();
        }
    }
    
    
    // cell renderer and editor
    
    private static class ButtonCellRenderer implements TableCellRenderer
    {
        private final JButton button;
        
        public ButtonCellRenderer(String label) {
            this.button = new JButton(label);
            button.setBorder(null); // else label is just "..."
        }
        
        @Override
        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
            return button;
        }
    }
    
    private static class ButtonCellEditor extends AbstractCellEditor implements TableCellEditor
    {
        @Override
        public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
            Action action = (Action) value;
            ActionEvent event = new ActionEvent(
                    table, 
                    ActionEvent.ACTION_PERFORMED, 
                    row + TableAction.ROW_COLUMN_SEPARATOR + column);
            action.actionPerformed(event);
            return null;
        }
        
        @Override
        public Object getCellEditorValue() {
            return null;
        }
    }
    
    
    // table build
    
    private JTable table;
    
    public ButtonsInTableExample() {
        Vector<Object> columnHeaders = new Vector<>();
        columnHeaders.add("Message");
        columnHeaders.add("Edit");
        columnHeaders.add("Delete");

        // one single action for all rows of a column
        TableAction editAction = new EditCellAction(0); // edit column 0
        TableAction deleteAction = new DeleteRowAction();
        
        Vector<Vector<Object>> data = new Vector<>(); // example rows
        for (int i = 0; i < 3; i++) {
            final Vector<Object> row = new Vector<>();
            row.add("Hello "+i); // column 0
            row.add(editAction);
            row.add(deleteAction);
            data.add(row);
        }
        
        TableModel model = new DefaultTableModel(data, columnHeaders) {
            @Override
            public Class<?> getColumnClass(int columnIndex) {
                if (columnIndex == 1)
                    return EditCellAction.class;
                if (columnIndex == 2)
                    return DeleteRowAction.class;
                return String.class;
            }
        };
        
        this.table = new JTable(model);
        
        // one cell-renderer per button-column
        table.setDefaultRenderer(EditCellAction.class, new ButtonCellRenderer("\u270E"));
        table.setDefaultRenderer(DeleteRowAction.class, new ButtonCellRenderer("\u2715"));
        
        // one generic cell-editor for all button-columns
        table.setDefaultEditor(AbstractAction.class, new ButtonCellEditor());
        
        table.getColumnModel().getColumn(1).setMaxWidth(40);
        table.getColumnModel().getColumn(2).setMaxWidth(40);
    }
}



Montag, 23. Februar 2026

Installing Java 17 on 32-bit Platform

After finishing my hiking-emergency-alert project I wanted to drive that application on my old Samsung NC10 Netbook. That was my energy-saving plan for the 2026 hiking summer, because this mail-application has to run all day while you are out hiking. Netbooks are cheap and small computers with low energy consumption. Mine has a 32-bit processor (CPU) and a 1024 x 600 pixel screen, running on Lubuntu LINUX. On LINUX, you can find out your processor and operating-system by launching these command-lines:

  $ lscpu | grep op-mode
  CPU op-mode(s):      32-bit
  
  $ uname -a
  LINUX ..... 4.4.0-145-generic #171-Ubuntu .... 2019 .... i686 GNU/Linux

I haven't used my Netbook since 7 years when I last upgraded its operating system to 4.4.0. Less and less providers still support the 32-bit platform. Launching the Firefox web-browser I found out that web-pages do not work properly, even after I upgraded Firefox to "Quantum" 66.0.3 (last available version for 32-bit?). So I could not even download my JAR file from github, because the github "<> Code" button did not do anything, no dropdown, no reaction to mouse-click, it wasn't even green. I turned to wget, but this downloaded an HTML file, I had to find out what the "raw" URL for my JAR file is. After all I could download it via

  $ wget --no-check-certificate https://github.com/fritzthecap/hiking-emergency-alert/raw/refs/heads/main/hiking-emergency-alert.jar

Now I launched that Swing application via

  $ java -jar hiking-emergency-alert.jar

with the installed very old Java 1.7, which of course gave me a class-version error, because the downloaded JAR had been compiled with Java 21:

  java.lang.UnsupportedClassVersionError: .... Unsupported major.minor version 61.0

I searched for the newest Java supporting 32-bit platforms for a long time on the web. Neither ORACLE nor the backing OpenJDK do support 32-bit anymore since Java 1.8. But there are still people out there that believe in the Java promise "Write once, run everywhere". From Bellsoft you can download "Liberica" Java 17 and even Java 21 for 32-bit platforms:

As the download link in Firefox again did not work, I did it with wget:

  $ wget --no-check-certificate https://download.bell-sw.com/java/17.0.18+10/bellsoft-jdk17.0.18+10-linux-i586.tar.gz

I unpacked the downloaded .tar.gz archive and, without setting PATH, tried to launch

  jdk-17.0.18/bin/java -jar hiking-emergency-alert.jar

And you know what? It worked!


The Java promise "Write once runeverywhere" has crumbled over time. It began when Apple disabled Java virtual-machines 2011 in their web-browser due to security concerns. Next JavaFX did not actually cover the promised mobile platform. With the adoption of Java by ORACLE, the JDK got more and more complex, and the myriads of open-source libraries provided by Maven repositories got big compatibility problems. Dynamic class loading took its toll. The module-system made loading of resource files really complex. Java followers like Spring-Boot ("Java for superheroes") employ developers just for finding out version numbers of libraries that are compatible with each other.

But it's not only the poor documentation and reckless software progress that makes our computer-lifes expensive and time-consuming. Also the browsers are aging fast, and so do the operating-systems and the hardware. Not even Debian LINUX supports 32-bit any more. That's the way all things are going in this Universe: sooner or later they fall apart. Hopefully not poisoning our oceans and soils with precious rare earths.

Freitag, 20. Februar 2026

Redirecting Java Outputs to a Swing Textarea

Most people use a command-line terminal window when they run a Java application. If the application uses stdout and stderr for log messages, one can see these messages on the terminal.

For a Java/Swing application, wouldn't it be nice to render those print streams in a Swing window? This is easier than expected.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import javax.swing.JTextArea;
import javax.swing.SwingUtilities;

/**
 * Redirect System.out and System.err to some UI window.
 */
public final class Log 
{
    private static class RedirectingOutputStream extends ByteArrayOutputStream
    {
        private final JTextArea textArea;
        
        RedirectingOutputStream(JTextArea textArea) {
            this.textArea = textArea;
        }
        
        @Override
        public void flush() throws IOException {
            final String line = toString();
            if (SwingUtilities.isEventDispatchThread())
                textArea.append(line);
            else
                SwingUtilities.invokeLater(() -> textArea.append(line));
            
            reset(); // else toString() would give all output again next time
        }
    }
    
    public static void redirectOut(JTextArea outputArea) {
        final ByteArrayOutputStream outStream = new RedirectingOutputStream(outputArea);
        System.setOut(new PrintStream(outStream, true)); // true: autoFlush on newlines
    }
    public static void redirectErr(JTextArea errorArea) {
        final ByteArrayOutputStream errStream = new RedirectingOutputStream(errorArea);
        System.setErr(new PrintStream(errStream, true)); // true: autoFlush on newlines
    }
    
    private Log() {} // do not instantiate
}

Static implementations are not beautiful, but sometimes you can't get over it. Because Java's System.out and System.err are static, the Log redirection methods above are static too. Because there are utility methods System.setOut() and System.setErr() we can redirect these streams to wherever we want.

The RedirectingOutputStream starting on line 11 does all the work. It extends ByteArrayOutputStream and overrides just the flush() method to send everything to the obtained textarea on line 22. This only works when the wrapping PrintStream was set to auto-flush, which means it calls flush() on every newline it detects. You can see the auto-flush setting on line 34 and 38 ("true") where the ByteArrayOutputStream gets packed into a PrintStream.

Here is a test-main to try this out:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> {
            JButton stdoutButton = new JButton("Write to System.out");
            stdoutButton.addActionListener(event -> System.out.println("Hello System.out at "+new Date()));
            
            JButton stderrButton = new JButton("Write to System.err");
            stderrButton.addActionListener(event -> System.err.println("Hello System.err at "+new Date()));

            JToolBar toolbar = new JToolBar();
            toolbar.add(stdoutButton);
            toolbar.add(stderrButton);
            
            JTextArea textArea = new JTextArea();
            textArea.setLineWrap(true);
            textArea.setWrapStyleWord(true);
            
            Log.redirectOut(textArea);
            Log.redirectErr(textArea);
            
            JPanel panel = new JPanel(new BorderLayout());
            panel.add(new JScrollPane(textArea), BorderLayout.CENTER);
            panel.add(toolbar, BorderLayout.NORTH);
            
            JFrame frame = new JFrame();
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.getContentPane().add(panel);
            frame.setSize(new Dimension(400, 200));
            frame.setLocationRelativeTo(null);
            frame.setVisible(true);
        });
    }

You see the redirection happening on line 17 and 18. Here is a screenshot of the test-application:

Click the buttons on top to generate outputs.


Swing was the first platform-independent object-oriented user-interface system that provided all kinds of components like tables and trees. Meanwhile it is a frozen project, but still contained in the Java runtime environment and thus available everywhere without complicated dependencies. Swing couldn't make it to small devices, but did JavaFX?