teaching an old dog updated languages

For reasons I won’t get into just yet, I’ve been looking at cleaning up my moldering Python skills. It’s been years since I coded anything of significance in Python, and “back in the day” I did it because I had few other options, one of them being Perl. I refuse to write anything in Perl anymore, even though I started my long journey into interpreted languages with the pink Perl book published in 1991.

In the early days I fell upon Perl like a man dying of thirst falls upon an oasis in a desert. In my case, the desert was a mix of bourne and AWK scripting (and I came away positively loathing TCL). With Perl 4 I could build highly sophisticated tooling completely in Perl, and just use my shell scripts to launch my more sophisticated Perl scripts. I started with Perl sometime around 1993 and kept using it until 2000 when I upgraded to Perl 5. With Perl 5, Perl fell out of favor with me, and Python began to rise in its place. I found that Python, in spite of its FORTRAN-like use of spaces to determine scope, was close to C++ and Java than Perl. I found it easier to user Python, both as wrapper for Java and C++ applications as well as being a powerful development language in its own right.

But time moved on and the reasons for using Python fell away. I stuck with C++ and Java, with occasional forays into JavaScript.

And then, for reasons as I said I won’t go into just yet, I had a need to brush up on my Python skills. Except this time, rather than staying in Python 2, I decided to jump whole sale into Python 3, or 3.4.0. I grabbed a copy of the source tarball off the Python website, unpacked it in my Ubuntu 14.04 VM, built it with the configure/make/make install kabuki dance, and started to work with it.

To refresh my skills I started with simple apps, to get a feel for the basics in Python 3. I quickly put together a factorial application, the source and output of which follows:

#!/usr/bin/env python3import sysdef fact(n):a = 1print( n, end="! = ")while n > 0:a, n = a * n, n - 1print(a)if __name__ == '__main__':if len(sys.argv) <= 1:print("Factorial calculator. Must call with at least one number as argument.")sys.exit()for i in range(1, len(sys.argv)):fact(int(sys.argv[i]))

There’s not much to say about this application. It has one function (fact()). It can test for command line arguments and print out a message with no arguments. It has a tight, simple iteration in main to walk through any and all arguments. The output shows that Python can easily handle arbitrarily large numbers without invoking special features. That alone is one reason it’s used in scientific computing.

While I don’t like how spacing is used to delineate scope, it does remove the need for curly braces used in other languages such as C++, Java, and Ruby, to name but three. What follows is the same application, this time written in Java 8.

import java.math.BigInteger;public class Fact {public static void fact(String num) {System.out.print(num + "! = ");BigInteger a = new BigInteger("1");BigInteger n = new BigInteger(num);while (n.compareTo(BigInteger.ZERO) > 0) {a = a.multiply(n);n = n.subtract(BigInteger.ONE);}System.out.println(a.toString());}public static void main (String[] args) {if (args.length < 1) {System.out.println("Factorial calculator. Must call with at least one number as argument.");System.exit(1);}for (String number : args) {fact(number);}}}

The syntax is radically different between the two, which is to be expected. Of significant difference is the use of java.math.BigInteger to match Python’s ability to use numbers of arbitrary size and precision. When I first wrote this I used regular integers (int) and could only calculate factorial 11 (11!) before the integer product became negative (sign overflow) and then went to zero. Using BigInteger solved that problem, but introduced a far more complex coding method. While the Java application is only six lines longer, its fact() function is a lot more complex than the fact() function in the Python example. Whereas the Python code can handle the numbers with the same old syntax, special functions within the BigInteger class have to be used to perform basic math (multiplication and subtraction) and the while loop comparison is a somewhat obscure big class function in Java, while with Python its the straightforward comparison we’ve all come to know and love.

In this specific example, the Python code wipes the floor with the Java code, or at least it does for me. There’s a certain sparse (dare I say beautiful?) elegance with the Python code not needing all the matching curly braces that Java requires (and C++, too, for that matter). From a purely coding standpoint, and with this very very narrow example, I find I prefer writing in Python. Maybe larger, more complex coding projects will continue to favor Python over Java, and then again, maybe not. While I want to continue working this kind of duality and comparison, it’s going to reach a point where I’ll need to make a decision when to use which, and stick to it.

I’m surprised, however, how much I like coding in Python. Have I reached a point in my coding life where I should do the majority of my work in Python, moving on from C++ and Java? Maybe even from JavaScript as well.

further refinements to tabbed table java application

I’ve been working with this personal project on and off, in the evenings after work, for several more weeks now. I’ve reached a point of diminishing returns (meaning that all I’m doing now is twiddling a few lines of code) so it’s time to ship it.

As a lead-in to the post I decided to be a bit classier (pun intended) than usual and toss out a modest UML diagram illustrating the overall OO design of the application. To draw it up I grabbed a copy of Violet, the UML editor, and sketched up a very basic UML diagram showing the relationship of all the classes in this small test application. I kept the diagramming to an absolute bare minimum, showing only the classes I wrote (and none of the JFC classes I extended), as well as just the public class methods. If you want more details the class sources are at the end of the post.

It’s been a long while since I cobbled together a formal UML diagram (other than the informal ones on a white board). In the process I was, once again, a bit torn between the usage of the aggregation (open diamond end) versus composition (filled-in diamond end). I go through this internal mental argument every time. In the end composition notation won out (yet again) because this is all about what the application is composed of. All I wanted to show was the has-a relationship of all the various classes versus the is-a relationship of inheritance, which I show between the TableDataProvider interface and the StaticTestData implementation. And for those who are interested, the dashed arrow between TableModel and StaticTestData shows the dependency for data that TableModel has on StaticTestData.

Ignoring the levels of inheritance within Java’s Foundation Classes (Swing), the design is extremely flat, with the interface the solution I came up with to separate out the test data I originally had slapped into the TabbedTables main class. It also gives me a better way to add other data sources without having to re-write any other classes. As long as the TableDataProvider implementation can provide the column names and data to be loaded into the table model, then a TableDataProvider can get its data from any source.

Some of the young turks (and old farts) will argue that UML is a waste of their time. Nobody will read it they say. Others argue that it’s a waste of time because changes to code are not reflected automatically in the design (specifically the UML diagram(s)) or vice versa. To the latter I say cry me a river.

If you can’t describe the design in a high-level understandable manner, with just enough detail to be useful, then you really don’t understand what you’re attempting to code. As for not keeping changes in code and design documents aligned, I have two responses; (1) you can always buy tools that allow for round trip engineering between UML design and code (and there are too many cheap/lazy coders who won’t do that), and (2) you should always start at the design to think about your overall changes, then work down towards the code implementation/changes. That’s a matter of discipline.

Anything worth working on should always start with a reasonably clear idea leading to a small set of succinct needs (requirements), in turn leading to a reasonable high-level design. If you can’t do that, then you’re wasting time and money and creating software that can be exploited against you by external actors if you create exploitable opportunities due to poor design, let alone poor implementation.

I’m no fan of design-it-all-up-front. You don’t have to design the world, just a reasonable first start. Whittle the big ideas down into smaller chunks, then prioritize the chunks. Execute in small enough steps to find the mistakes (in assumptions, design, and implementation) when they occur (and trust me, they will occur) and correct them. Monolithic big-bang designs, either in commercial or military projects, are epic failures just waiting to happen.

Changes since the last time I took a screen grab include setting the JTable so that it doesn’t automatically fill the viewport horizontally and locking the columns in place. As for the latter “feature”, I’m not sure who came up with the idea we needed to move our columns around. The ability to sort a column is far more useful, and simply clicking a column header to sort also has the column briefly moving, which drives me crazy. So I locked columns in place with “table.getTableHeader().setReorderingAllowed(false)” in CustomTable. I also tweaked the colors for both the cell borders and alternating rows to be the same, and the way the close button is drawn.

This screen grab shows what happens when any of the table rows are double clicked. It opens a simple detail panel on the right. Again you can look at the source code for CustomTable to see how this is done. The CloseControl was enhanced to work with both the TabbedContainer’s JTabbedPane pane tab, as well as the detail view that is opened. It changes color to red and white when hovered over (as seen above) and clicking the close button closes the detail pane and restores the look to the view in the first screen capture. While the view is primitive in this example, it proves the point that I can click any row and get the data associated with that row, even if the rows have been sorted. In a more sophisticated application I would use the data to actually reach back out and get detailed data associated with cell data in that row rather than just simply display the cells. But this vets that the logic up to this point is working and proves the basic design to be reasonably correct.

What follows are all seven source files in this project.

package tabbedtables;import java.awt.Color;import java.util.Enumeration;import javax.swing.JFrame;import javax.swing.SwingUtilities;import javax.swing.UIDefaults;import javax.swing.UIManager;import javax.swing.UnsupportedLookAndFeelException;public class TabbedTables {private static void dumpLookAndFeelDefaults() {UIDefaults defaults = UIManager.getLookAndFeelDefaults();Enumeration enums = UIManager.getLookAndFeelDefaults().keys();while (enums.hasMoreElements()) {Object key = enums.nextElement();Object val = defaults.get(key);System.out.println(key.toString() + " = " + (val != null ? val.toString() : "NULL"));}}private static void createGUI() {try {// Set system Java L&F (Windows for Windows, Gnome for Java...)//UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());}catch (UnsupportedLookAndFeelException |ClassNotFoundException |InstantiationException |IllegalAccessException exception) {// silently handle exception}// Turn off the dashed line around the active tab. Make it transparent.//UIManager.put("TabbedPane.focus", new Color(0,0,0,0));//dumpLookAndFeelDefaults();TabbedContainer tabbedContainer = new TabbedContainer();tabbedContainer.addTable("Tab 1", new CustomTable(new StaticTestData()));tabbedContainer.addTable("Tab 2", new CustomTable(new StaticTestData()));tabbedContainer.addTable("Tab 3", new CustomTable(new StaticTestData()));tabbedContainer.addTable("Tab 4", new CustomTable(new StaticTestData()));JFrame frame = new JFrame("Test tabs and tables");frame.setContentPane(tabbedContainer);frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);frame.setSize(800, 600);frame.setVisible(true);}public static void main(String[] args) {SwingUtilities.invokeLater(new Runnable() {@Overridepublic void run() {createGUI();}});}}
package tabbedtables;import javax.swing.JComponent;import javax.swing.JTabbedPane;import javax.swing.event.ChangeEvent;import javax.swing.event.ChangeListener;public class TabbedContainer extends JTabbedPane {private int lastActiveTab = 0;public TabbedContainer() {addChangeListener( new ChangeListener() {   @Override   public void stateChanged(ChangeEvent event) {   JTabbedPane source = (JTabbedPane)event.getSource();   if (getTabCount() == 0) return;   if (lastActiveTab >= getTabCount()) lastActiveTab = getTabCount() - 1;   if (source.getTabComponentAt(lastActiveTab) == null) return;   ((CloseControl)source.getTabComponentAt(lastActiveTab)).setButtonEnabled(false);   lastActiveTab = source.getSelectedIndex();   ((CloseControl)source.getTabComponentAt(lastActiveTab)).setButtonEnabled(true);   }});}public void addTable(String tabTitle, JComponent table) {int lastTabCount = getTabCount();addTab(tabTitle, table);CloseControl ctc = new CloseControl(this);setTabComponentAt(getTabCount() - 1, ctc);setSelectedIndex(getTabCount() - 1);if (lastTabCount > 0) {lastActiveTab = lastTabCount;}}}
package tabbedtables;import java.awt.BorderLayout;import java.awt.Color;import java.awt.Component;import java.awt.GridLayout;import java.awt.event.MouseAdapter;import java.awt.event.MouseEvent;import javax.swing.JLabel;import javax.swing.JPanel;import javax.swing.JScrollPane;import javax.swing.JSplitPane;import javax.swing.JTable;import javax.swing.ListSelectionModel;import javax.swing.SwingConstants;import javax.swing.table.TableCellRenderer;public class CustomTable extends JPanel {private JSplitPane splitPane;private JPanel rightDetail = null;private JLabel rightText;public CustomTable(TableDataProvider tableData) {super(new GridLayout(1,0,4,4));JTable table = new JTable(new TableModel(tableData.getColumnNames(), tableData.getTableData())) {//// A simple renderer to render every odd row in the table as// a color other than white, or the default background color.//@Overridepublic Component prepareRenderer(TableCellRenderer renderer, int row, int col) {Component component = super.prepareRenderer(renderer, row, col);//// Alternate row color//if (!isRowSelected(row))component.setBackground(row % 2 == 0 ? getBackground() : Color.decode("0xEED8AE"));return component;}};table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);table.setFillsViewportHeight(true);table.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);table.setAutoCreateRowSorter(true);table.getTableHeader().setReorderingAllowed(false);table.setGridColor(Color.decode("0xEED8AE"));// Look for a double mouse click to select a given cell in the table.//table.addMouseListener(new MouseAdapter() {@Overridepublic void mouseClicked(MouseEvent event) {if (event.getClickCount() == 2) {JTable table = (JTable)event.getSource();int row = table.getSelectedRow();int col = table.getSelectedColumn();// If we haven't added the right detail pane yet, add it// and make the split pane's divider wide enough to// easily drag. This will honor the user's arbitrary// width setting for any following detail clicks.//if (splitPane.getRightComponent() == null) {splitPane.setRightComponent(rightDetail);splitPane.setDividerSize(4);}// Simple detailed view of a given row.// Don't do like I did the first time and get cell values// from the table model, especially after a sort.// The table view is where you want to get detailed data.//rightText.setText("<html><h1>Detailed Information</h1>" +"<h2>Row clicked:" + Integer.toString(row) + "</h2>" +"<h2>Column clicked: " + Integer.toString(col) + "</h2>" +"<h3>Column 1: " + table.getValueAt(row, 0) + "</h3>" +"<h3>Column 2: " + table.getValueAt(row, 1) + "</h3>" +"<h3>Column 3: " + table.getValueAt(row, 2) + "</h3>" +"<h3>Column 4: " + table.getValueAt(row, 3) + "</h3>" +"</html>");}}});//JTableHeader tableHeader = table.getTableHeader();//tableHeader.setReorderingAllowed(false);// Ceate the split pane, but DON'T ADD the right detail panel. We'll// add it when someone first double-clicks on a table row.//splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT,new JScrollPane(table, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED), null);splitPane.setDividerSize(0);add(splitPane);rightDetail = new JPanel(new BorderLayout());JPanel closeRightTop = new CloseControl("Placeholder text...", splitPane);rightDetail.add(closeRightTop, BorderLayout.PAGE_START);rightText = new JLabel();rightText.setVerticalAlignment(SwingConstants.TOP);rightDetail.add(rightText, BorderLayout.CENTER);}}
package tabbedtables;import java.awt.BasicStroke;import java.awt.BorderLayout;import java.awt.Color;import java.awt.Dimension;import java.awt.FlowLayout;import java.awt.Graphics;import java.awt.Graphics2D;import java.awt.RenderingHints;import java.awt.event.ActionEvent;import java.awt.event.ActionListener;import javax.swing.BorderFactory;import javax.swing.Box;import javax.swing.JButton;import javax.swing.JLabel;import javax.swing.JPanel;import javax.swing.JSplitPane;import javax.swing.JTabbedPane;import javax.swing.UIManager;import javax.swing.plaf.basic.BasicButtonUI;public class CloseControl extends JPanel {private JTabbedPane parentTabbedPane;private JSplitPane parentSplitPane;private CloseButton closeButton;private boolean repaintButton = false;public CloseControl(final JTabbedPane pane) {super(new FlowLayout(FlowLayout.LEFT,0,0));if (pane == null) throw new NullPointerException("JTabbedPane is null");parentTabbedPane = pane;setOpaque(false);JLabel label = new JLabel() {@Overridepublic String getText() {int index = pane.indexOfTabComponent(CloseControl.this);if (index != -1) return pane.getTitleAt(index);return null;}};label.setBorder(BorderFactory.createEmptyBorder(0,0,0,5));add(label);add(Box.createHorizontalStrut(10));closeButton = new CloseButton();closeButton.addActionListener(new ActionListener() {@Overridepublic void actionPerformed(ActionEvent e) {int index = parentTabbedPane.indexOfTabComponent(CloseControl.this);if (index != -1) parentTabbedPane.remove(index);}});add(closeButton);setBorder(BorderFactory.createEmptyBorder(1,2,1,2));}public CloseControl(final String title, final JSplitPane component) {super(new BorderLayout());parentSplitPane = component;setOpaque(false);JLabel label = new JLabel() {@Overridepublic String getText() {return title;}};label.setBorder(BorderFactory.createEmptyBorder(0,0,0,5));add(label, BorderLayout.LINE_START);closeButton = new CloseButton();closeButton.addActionListener(new ActionListener() {@Overridepublic void actionPerformed(ActionEvent event) {parentSplitPane.setRightComponent(null);parentSplitPane.setDividerSize(0);}});add(closeButton, BorderLayout.LINE_END);setBorder(BorderFactory.createEmptyBorder(2,2,2,2));repaintButton = true;}public void setButtonEnabled(boolean enable) {closeButton.setEnabled(enable);}private class CloseButton extends JButton {public CloseButton() {setPreferredSize(new Dimension(18, 18));setToolTipText("Close");setUI(new BasicButtonUI());setContentAreaFilled(false);setOpaque(true);setFocusable(false);setBorder(BorderFactory.createEmptyBorder());setBorderPainted(false);setRolloverEnabled(true);}@Overridepublic void updateUI() {}@Overrideprotected void paintComponent(Graphics graphics) {Graphics2D g2 = (Graphics2D) graphics.create();g2.setRenderingHints(new RenderingHints(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON));g2.setStroke(new BasicStroke(3, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_ROUND));if (getModel().isRollover()) {g2.setColor(Color.decode("0xCF0000"));g2.fillRect(0, 0, getWidth(), getHeight());g2.setColor(Color.WHITE);}else {if (repaintButton == true) {g2.setColor(UIManager.getColor("Button.background"));g2.fillRect(0, 0, getWidth(), getHeight());}g2.setColor(Color.decode("0x707070"));}// Draw an 'X'. Firt stroke is from upper left to lower right.// Second stroke is from lower left to upper right.//int cornerOffset = 4;g2.drawLine(cornerOffset-1, cornerOffset-1, getWidth() - cornerOffset, getHeight() - cornerOffset);g2.drawLine(getWidth() - cornerOffset, cornerOffset-1, cornerOffset-1, getHeight() - cornerOffset);g2.dispose();}}}
package tabbedtables;import javax.swing.table.AbstractTableModel;public class TableModel extends AbstractTableModel {private final String[] columnNames;private Object[][] data = null;public TableModel(final String[] columnNames, final Object[][] initialData) {this.columnNames = columnNames;data = initialData;}@Overridepublic int getColumnCount() {return columnNames.length;}@Overridepublic int getRowCount() {return data != null ? data.length : 0 ;}@Overridepublic String getColumnName(int column) {return columnNames[column];}@Overridepublic Object getValueAt(int row, int col) {return data != null ? data[row][col] : null ;}@Overridepublic Class getColumnClass(int col) {return getValueAt(0, col).getClass();}@Overridepublic boolean isCellEditable(int row, int col) {return false;}@Overridepublic void setValueAt(Object value, int row, int col) {if (data != null) {data[row][col] = value;fireTableCellUpdated(row, col);}}}
package tabbedtables;public interface TableDataProvider {String[] getColumnNames();Object[][] getTableData();}
package tabbedtables;public class StaticTestData implements TableDataProvider {public static String[] columnNames = {"Column 1", "Column 2", "Column 3", "Column 4"};public static Object[][] sampleData ={{ "sample 1", "sample e", "sample", "sample" },{ "sample 2", "sample d", "sample", "sample" },{ "sample 3", "sample c", "sample", "sample" },{ "sample 4", "sample b", "sample", "sample" },{ "sample 5", "sample a", "sample", "sample" },{ "sample", "sample x", "sample", "sample" },{ "sample", "sample y", "sample", "sample" },{ "sample", "sample z", "sample", "sample" },{ "sample", "sample", "sample", "sample" },{ "sample", "sample", "sample", "sample" },{ "sample", "sample", "sample", "sample" },{ "sample", "sample", "sample", "sample" },{ "sample", "sample", "sample", "sample" },{ "sample", "sample", "sample", "sample" },{ "sample", "sample", "sample", "sample" },{ "sample", "sample", "sample", "sample" },{ "sample", "sample", "sample", "sample" },{ "sample", "sample", "sample", "sample" },{ "sample", "sample", "sample", "sample" },{ "sample", "sample", "sample", "sample" },{ "sample", "sample", "sample", "sample" },{ "sample", "sample", "sample", "sample" },{ "sample", "sample", "sample", "sample" },{ "sample", "sample", "sample", "sample" },{ "sample", "sample", "sample", "sample" },{ "sample", "sample", "sample", "sample" },{ "sample", "sample", "sample", "sample" },{ "sample", "sample", "sample", "sample" },{ "sample", "sample", "sample", "sample" },{ "sample", "sample", "sample", "sample" },{ "sample", "sample", "sample", "sample" },{ "sample", "sample", "sample", "sample" },{ "sample", "sample", "sample", "sample" },{ "sample", "sample", "sample", "sample" },{ "sample", "sample", "sample", "sample" },{ "sample", "sample", "sample", "sample" },{ "sample", "sample", "sample", "sample" },{ "sample", "sample", "sample", "sample" },{ "sample", "sample", "sample", "sample" },{ "sample", "sample", "sample", "sample" },};@Overridepublic String[] getColumnNames() {return columnNames;}@Overridepublic Object[][] getTableData() {return sampleData;}}