"The People can be depended upon to meet any national crisis. Just bring them the real facts, and beer." Abraham Lincoln See preceeding Swing articles Event DTs, Turn Tables, and Inside Action. gold3_crop.jpg Disclaimer: As always, this is a "noisy" article (because i like writing noisy articles and i wrote this article, so...), but luckily the noise is clearly demarcated using italicalised text this time. So please feel free to skip such text.


"Beer is proof that God loves us and wants us to be happy." Benjamin Franklin Our monastery operates a brewery. Our highly popular "Dosy Abbot" ale has kept us in food and, um, ale, for the past 368 years. So we need to keep track of our ingredients, products, orders, and what-not. Now some of us newer younger monks are interested in computers, in addition to beer of course. And we got the go-ahead from the Abbot to write a stock control system, woohoo! So we wrote this Swing desktop app. And I got nominated to write this article. So our application has of a whole bunch of CURD worksheets, e.g. for editing products, product categories, suppliers, customers, etcetera. Then we have to capture and view transactions, representing stock movements and related financial documents, e.g. purchase orders, invoices, delivery notes, stock transfers, stock takes, etcetera. So we need to assemble all these worksheets into an application framework, with access control. In the first instance, the user should login. Then we display the "menu system" to enable the user to launch "worksheets." Users typically have limited access, i.e. to a specific subset of worksheets. For example, it's not a good idea to give the Abbot access to everything because a little bit of knowledge is a very dangerous thing. But stock transfers to our pantry, and the shrinkage report on our finished products, is for the Abbot's eyes only. The design presented here is an improved sugar-coated redesign of the "access system" of

Worksheet Framewarez

"Beer... Now there's a temporary solution." Homer Simpson Before we wrote our brewery warez, we used a spreadsheet to manage the monastery. (And we still do, to tell the truth.) So when the Abbot OK'ed us writing a computer program to do the same thing, he "requested" that we stick to the spreadsheet metaphor. So we call our "programs" worksheets, and we can open any number of worksheets as tabs at the bottom of the screen, like a spreadsheet program. menu.png Our application framewarez is a JFrame, with a JMenuBar, and a JTabbedPane. Our worksheets are launched from the menu. Worksheets render themselves as JPanel's, which we add to the tabbed pane. And here's a Web Start demo, woohoo! You can read about The Making Of this Web Starter in next week's Trip and Tick 2 article, including why it runs outside of the sandbox, and so is jarsigned, and see a Request for Help on sandboxing these thingymajigs. webstart.small.gif (695k, unsandboxed, Java5)

Defining the Menus

"Without question, the greatest invention in the history of mankind is beer. The wheel does not go nearly as well with pizza." Dave Barry We need to configure our menu items. That is, their labels, icons, keystrokes and the associated worksheet to launch, e.g. "Edit Product" launches ZProductWorksheet. Our framework provides a GMenuConfiguratorfor us to extend as follows.

 package; ... public class ZMenuConfigurator extends GMenuConfigurator { ... // system menu e.g. lock screen, logout, exit @MenuAnnotation( label = "Edit" ) GMenuConfiguration topEdit = createMenu(null); @MenuAnnotation( label = "Edit Product", worksheet = ZProductWorksheet.class, icon = "yast_security", toolTip = "Edit products", ordinal = 2 ) GMenuConfiguration editProduct = createMenu(topEdit); ... // menus for all other worksheets in the application @MenuAnnotation( label = "Help" ) GMenuConfiguration topHelp = createMenu(null); @MenuAnnotation( label = "Online help", icon = "lifesaver", ordinal = 3 ) GMenuConfiguration helpOnlineHelp = createMenu(topHelp); @MenuAnnotation( label = "About", icon = "lightbulb", ordinal = 4 ) GMenuConfiguration helpAbout = createMenu(topHelp); public ZMenuConfigurator() { super(); super.configure(); } } 

where the GMenuConfiguratorsuperclass is implemented as follows.

 package; ... public class GMenuConfigurator { protected List menuList = new ArrayList(); protected GMenuConfigurator() { } protected GMenuConfiguration createMenu(GMenuConfiguration parentMenu) { GMenuConfiguration menu = new GMenuConfiguration(); menu.setParentMenu(parentMenu); menuList.add(menu); return menu; } protected void configure() { ... // read externalised list of MenuConfiguration objects for (Field field : getClass().getFields()) { if (field.getType() == GMenuConfiguration.class) { field.setAccessible(true); configure(field, (GMenuConfiguration) field.get(this)); } } } protected void configure(Field field, GMenuConfiguration menu) { menu.setMenuId(field.getName()); ... // configure using MenuAnnotation ... // override with externalised configuration } public void writeConfigurationFile(File configurationFile) { ... } protected void readConfigurationFile(File configurationFile) { ... } } 

where writeConfigurationFile()is invoked by the developer to generate an externalised configuration file, as presented further below.

Leveraging the IDE for rapid prototyping

"Homer no function well without beer." Homer SimpsonUsing Java code to capture defaults as above, enables us to leverage the IDE, e.g. enjoy auto-completion on the worksheet class names. We can take this further for icons, by generating content for an "icon class" as follows.

 public class GIconClassGenerator { ... public void generate(String iconDirectory) { for (String fileName : getFileNameList(iconDirectory)) { if (!ileName.endsWith(".png")) { String camelCaseFileName = toCamelCase(fileName); StringBuffer buffer = new StringBuffer(); buffer.append("public final GIcon " + camelCaseFileName); buffer.append(" = createIcon(\"" + fileName + "\");"); System.out.println(buffer); } } } } 

where getFileNameList()lists all the files in the given directory.

Externalising the Menu Configuration

"What I like to drink most is beer that belongs to others." Diogenes. We adopt an approach where we code our defaults in the first instance, for rapid prototyping, as in the above ZMenuConfigurator. At any stage, we can then externalise these defaults, e.g. by invoking writeConfigurationFile() to emit content for a resource bundle, and/or XML configuration file, in order to support translation and customisation. When the application starts up, we load the configuration file to override the coded defaults, e.g. using readConfigurationFile() in the above GMenuConfigurator. We read and parse the configuration data into configuration objects, e.g. using JAXB2 to bind the following configuration object to XML.

 @XmlElement(name = "menu") public class GMenuConfiguration { @XmlAttribute protected GMenuConfiguration parentMenu; @XmlAttribute protected Class worksheetClass; @XmlAttribute protected String menuId; @XmlAttribute protected String keyStroke; @XmlAttribute protected Character mnemonic; @XmlAttribute protected String iconName; @XmlAttribute protected String label; @XmlAttribute protected String toolTip; @XmlAttribute protected Integer ordinal; ... // getters and setters ... // configure(GMenuConfiguration), to overwrite with non-null properties from another instance } 

A diatribe on XML follows. I choose Java to program the default configuration, because Java is more programmable, toolable and beautiful than XML. So i love tools like JAXB2, to map XML to Java, using annotations, so that i can program XML in Java. Which, as a Java programmer, i naturally prefer. bottle_150b.jpg Similarly with SQL, OQL et al, as addressed in Bean Curd 2: The SQL.

Users and roles

"All right, brain, let's back to killing you with beer." Homer Simpson Since our worksheets are created by the developer, the menu configuration i.e. what worksheets we have available, is not editable by the system administrator. However, the users, user roles, and menu access control lists, must be editable by an administrative user. So we provide worksheets for that, e.g. ZUserWorksheet, ZUserRoleWorksheet and ZMenuAccessWorksheet. Our "access control lists" indicate which user roles have access to a given menu (or menu item). To keep things tidy, we have DAOs for menus, as well as users and user roles. Our users have a many-to-many relationship to user roles, and our user roles have a many-to-many relationship to our menus. Therefore, we might have "membership" tables, and so DAO's for those too.

 public class ZAccessEntityManager extends MEntityManager { public ZMenuInfo menu = new ZMenuInfo(); public ZMenuMembershipInfo menuMembership = new ZMenuMembershipInfo(); public ZUserInfo user = new ZUserInfo(); public ZUserRoleInfo userRole = new ZUserRoleInfo(); public ZUserRoleMembershipInfo userRoleMembership = new ZUserRoleMembershipInfo(); ... } 

where meme's MEntityManager was introduced in Bean Curd 2: The SQL. We might get a filtered list of those menus available for a given user role by invoking entityManager.menuMembership.getMenuList(userRole)which might be implemented as follows.

 public class ZMenuMembershipInfo extends MEntityBean { ... public List getMenuList(ZUserRole userRole) { List menuList = new ArrayList(); for (ZMenuMembership membership : super.getExtentEntityList()) { if (membership.getUserRole().equals(userRole))) { menuList.add(membership.getMenu()); } } return menuList; } } 

where MEntityBean.getExtentEntityList()gets all the entities, i.e. rows in this database table.

Menu Bar Configurator

"I have feelings too - like I want beer, or I'm going crazy." Homer Simpson When the user logs into the application, we need to build the menu bar, including only those menus and menu items (which mostly correspond to worksheets) to which that user has access. We filter the list of all available menus down to the accessible menus, and then populate the JMenuBaras follows.

 public void configure(JMenuBar menuBar, List menuBeanList) { menuBar.removeAll(); for (ZMenu parentMenuBean : getMenuBeanList(menuBeanList, null)) { JMenu topMenu = createMenu(menuBeanList, parentMenuBean); topMenu.setIcon(null); if (topMenu.getMenuComponentCount() > 0) { menuBar.add(topMenu); } } menuBar.repaint(); } protected List getMenuBeanList(List menuBeanList, ZMenu parentMenuBean) { List list = new ArrayList(); for (ZMenu menuBean : menuBeanList) { if (menuBean.getParentMenu() == parentMenuBean) { list.add(menuBean); } } return list; } protected JMenu createMenu(List menuBeanList, ZMenu parentMenuBean) { JMenu menu = createMenu(parentMenuBean); for (ZMenu menuBean : getMenuBeanList(menuBeanList, parentMenuBean)) { if (getMenuList(menuBeanList, menuBean).size() != 0) { menu.add(createMenu(menuBeanList, menuBean)); } else { menu.add(createMenuItem(menuBean)); } } return menu; } protected JMenuItem createMenuItem(ZMenu menuBean) { GAction action = createMenuAction(menuBean); JMenuItem menuItem = createMenuItem(action); return menuItem; } 

where createMenu(ZMenu) creates a JMenu component from our ZMenu entity, similar to the above createMenuItem(ZMenu). We have an ordinal property on our ZMenuJitem. If this is nonzero, then it indicates that we wish to include this item in our tool bar, using the ordinal value as its order in the tool bar.

 public void configure(JToolBar toolBar, List menuBeanList) { toolBar.removeAll(); Map menuBeanMap = new TreeMap(); for (ZMenu menuBean : menuBeanList) { if (menuBean.getOrdinal() != 0) { menuBeanMap.put(menuBean.getOrdinal(), menuBean); } } for (ZMenu menuBean : menuBeanMap.values()) { GAction action = createMenuAction(menuBean); JButton button = createButton(action); button.setText(null); toolBar.add(button); } toolBar.repaint(); } 

where createMenuAction() creates a Swing Actionusing the menu configuration.

Worksheet Cookie Puncher

"Beer needs sports, and sports needs beer - it has always been thus." Peter RichmondWe introduce an interface for worksheets, so that our framewarez can juggle them.

 public interface ZWorksheet { public void openWorksheet(); public boolean closeWorksheet(); public String getWorksheetLabel(); public GPanel getWorksheetPanel(); public String getWorksheetHelp(); } 

The openWorksheet() method might request focus for some component, so we will invoke it after the worksheet tab is realised, e.g. using SwingUtilities.invokeLater(). beer5_250b.jpg The closeWorksheet() method confirms that the worksheet can be closed, e.g. asks if the user wishes to save changes, if there are any. When the framewarez opens the worksheet, it gets the label for the tab using getWorksheetLabel(), and the content JPanel of the worksheet using getWorksheetPanel(). If the user selects the help menu item, our framewarez might get the help for the currently active worksheet using getWorksheetHelp(). This might be the actual HTML text to display, or it might be a bookmark into the help manual, which makes more sense.

One of Many Worksheets

"Work is the curse of the drinking class." Oscar Wilde We make all of our worksheets implement the above interface. Let's consider our ZProductWorksheet. In MVC-speak, this mashes our MVC three-ball into one class. Additionally, we reference POJO model objects, i.e. instances of ZProductBean, to complete the picture. It would be better to split this class up, e.g. put the table into ZProductTable and the form into ZProductForm. But we aint gonna worry about that right now.

 public class ZProductWorksheet implements ZWorksheet, GActionListener, GFieldListener { @WorksheetContextAnnotation() ZWorksheetContext context = ZWorksheetContext.createWorksheetContext(this); @WorksheetConfigurationAnnotation() ZProductWorksheetConfiguration configuration = new ZProductWorksheetConfiguration(); @ComponentAnnotation(label = "Products") GPanel worksheetPanel = context.createPanel(); @LayoutAnnotation(gridy = 0) GToolBar worksheetToolBar = context.createToolBar(worksheetPanel); @ComponentAnnotation(label = "Close worksheet") GAction closeAction = context.createAction(worksheetToolBar); ... // more actions e.g. new, find, save, undo @LayoutAnnotation(gridy = 1) GTabbedPane tabbedPane = context.createTopTabbedPane(worksheetPanel); @ComponentAnnotation(label = "Products") GTabPanel tableTabPanel = context.createTabPanel(tabbedPane); @ComponentAnnotation(label = "Details") GTabPanel formTabPanel = context.createTabPanel(tabbedPane); @ComponentAnnotation() GTable productForm = context.createTable(tableTabPanel, ZProductBean.class); @ComponentAnnotation(label = "Product Id", width = 100) GField productIdField = context.createField(productForm); ... // other columns @LayoutAnnotation(spacer = Gbc.BOTH, flow = Gbc.HORIZONTAL) GForm productForm = context.createForm(formTabPanel, ZProductBean.class); @ComponentAnnotation(label = "Product Id", width = 100) GTextField productIdColumn = context.createTextField(productForm); ...// other fields List productBeanList = new ArrayList(); public ZProductWorksheet() { context.configure(this); productTable.setDataList(context.entityManager.product.getExtentEntityList()); productForm.setBean(productTable.getBeanList().get(0)); } public void openWorksheet() { productTable.requestFocusInWindow(); } public boolean closeWorksheet() { if (productForm.isChanged()) { if (!context.showConfirmDialog(configuration.confirmCloseWithoutSave)) { return false; } } return true; } public GToolBar getWorksheetToolBar() { return worksheetToolBar; } public GPanel getWorksheetPanel() { return worksheetPanel; } public String getWorksheetHelp() { return configuration.worksheetHelp; } public String getWorksheetLabel() { return configuration.worksheetLabel; } public void actionPerformed(GActionEvent event) { context.traceLogger.entering(event); if (event.getAction().equals(newAction)) { ... } else if (event.getAction().equals(findAction)) { ... } else if (event.getAction().equals(saveAction)) { ... } else if (event.getAction().equals(closeAction)) { context.closeWorksheet(this); } else if (event.getAction() == helpAction) { helpActionPerformed(); } else { context.traceLogger.warning(event); } setEnabled(); } public void helpActionPerformed() { context.framewarez.showHelp(getWorksheetHelp()); } public void setEnabled() { saveAction.setEnabled(productForm.isChanged()); undoAction.setEnabled(productForm.isChanged()); } ... } 

As you can see, we annotate everything, even if we dunno why we would want to (yet). Mmmm, Duff, mmmm, annotations... Note that we use our tabbed framewarez to display our help as a new tab in helpActionPerformed(). In general, we favour opening a tab rather than popping up a JDialog. For one thing, this allows the user to switch between tabs to remind themselves what they are doing. If we really need a modal dialog, then we will use one, but otherwise we go for tabs.

Worksheet Configuration Class

"Beer, the cause and solution to all of life's problems." Homer SimpsonWe distill translatable strings into a worksheet configuration class as follows.

 public class ZProductWorksheetConfiguration { String worksheetLabel = "Products"; String worksheetHelp = "Product catalogue maintenance worksheet \n\n" + "Use this worksheet to maintain our catalogue of beer products."; String confirmCloseWithoutSave = "Close without saving changes?"; ... // other messages, e.g. exception messages, dialog messages } 

This class can be externalised to an XML file for customisation and translation. In addition to the above strings, our configuration bundle implicitly includes the configuration of our components, to override the defaults specified in the annotations, notably the labels and tooltips, e.g. the tooltip for closeAction, tab label for tableTabPanel, column label for productIdColumn, et cetera. The configuration might be generated for a resource bundle as follows.

 productWorksheet.message.worksheetLabel = Products productWorksheet.message.confirmCloseWithoutSave = Close without saving changes? productWorksheet.message.worksheetHelp = Product catalogue maintenance worksheet ... productWorksheet.panel.worksheetPanel.label = Products productWorksheet.action.closeAction.toolTip = Close worksheet = Products = Details productWorksheet.field.productIdField.label = Product Id productWorksheet.column.productIdColumn.label = Product Id 

Alternatively, its XML representation might look like the following.

     Product catalogue maintenance worksheet Use this worksheet to maintain our catalogue of beer products.         

In our ZWorksheetContext.configure() method, we would parse the above into a list of MessageConfiguration, ActionConfiguration, PanelConfiguration, TabConfiguration, FieldConfiguration and ColumnConfiguration objects. And then apply these configurations to our ZProductWorksheetConfiguration instance, and the components of our ZProductWorksheet, to override the defaults e.g. as specified in annotations.


"Without hydrogen and oxygen, there would be no water, a vital ingredient in beer." Dave Barry. Our framewarez is implemented as follows. For starters, we show a login tab. Once the user has logged in, we configure the menu bar appropriately according to the user's access permissions. Then the user can start launching worksheets. login.png

 public class ZAccessFrame implements GFieldListener, ActionListener, GTableListener { @WorksheetContextAnnotation() ZWorksheetContext context = ZWorksheetContext.createWorksheetContext(this); @WorksheetConfigurationAnnotation() ZAccessFrameConfiguration configuration = new ZAccessFrameConfiguration(); @LayoutAnnotation() GFrame mainFrame = context.createFrame(); @LayoutAnnotation() GMenuBar mainMenuBar = context.createMenuBar(mainFrame); @LayoutAnnotation(gridy = 0) GToolBar mainToolBar = context.createToolBar(); @LayoutAnnotation(gridy = 1) GTabbedPane mainTabbedPane = context.createBottomTabbedPane(mainPanel); @LayoutAnnotation() GPanel textPanel = context.createPanel(); @LayoutAnnotation(top = 20, gridy = 2) GTextPane textPane = context.createTextPane(textPanel); @LayoutAnnotation() @ComponentAnnotation(label = "Login") GPanel loginPanel = context.createPanel(); @LayoutAnnotation(gridy = 1) GForm loginForm = context.createForm(loginPanel, ZLoginBean.class); @LayoutAnnotation(gridx = 0, width = 100) @ComponentAnnotation(label = "Username") GTextField usernameField = context.createTextField(loginForm); @LayoutAnnotation(gridx = 1, width = 100) @ComponentAnnotation(label = "Password") GPasswordField passwordField = context.createPasswordField(loginForm); @LayoutAnnotation(gridx = 2) GAction loginAction = context.createButton(loginForm); ZLoginBean loginBean = new ZLoginBean(this); public ZAccessFrame() { context.configure(this); loginForm.setBean(loginBean); setEnabled(); } public void fieldChanged(GFieldEvent event) { context.fieldLogger.entering(event); if (event.getField() == usernameField) { usernameChanged(); } else if (event.getField() == passwordField) { passwordChanged(); } setEnabled(); } protected void usernameChanged() { loginBean.validateUsername(); passwordField.setEnabled(true); passwordField.requestFocusInWindow(); } protected void passwordChanged() { loginForm.getBean(); loginBean.validate(); loginAction.setEnabled(true); loginAction.requestFocusInWindow(); } protected void setEnabled() { loginAction.setEnabled(loginBean.validate()); } public void actionPerformed(ActionEvent event) { ZMenu menu =; context.actionLogger.entering(event, menu); if (menu != null && menu.getWorksheetClass() != null) { openWorksheet(menu.getWorksheetClass()); } else if (loginAction.isSource(event)) { loginActionPerformed(); } else if (context.accessData.systemLogout.isSource(event)) { logoutActionPerformed(); } else if (context.accessData.systemExit.isSource(event)) { exitActionPerformed(); } else if (context.accessData.helpOnlineHelp.isSource(event)) { helpActionPerformed(); } else if (context.accessData.helpAbout.isSource(event)) { aboutActionPerformed(); } else { context.traceLogger.warning(event); } setEnabled(); } public void loginActionPerformed() { loginForm.getBean(); loginBean.validate(); loginUser(); } protected void loginUser() { context.setUser(loginBean.getUser()); showMenu(); } protected void showMenu() { context.configure(mainMenuBar, context.getUser()); context.configure(mainToolBar, context.getUser()); mainMenuBar.requestFocusInWindow(); } protected void logoutActionPerformed() { context.setUser(null); loginBean = new ZLoginBean(this); loginForm.setBean(loginBean); mainTabbedPane.removeAll(); mainTabbedPane.addTab(loginPanel); username.requestFocusInWindow(); } protected void openWorksheet(Class worksheetClass) { try { ZWorksheet worksheet = (ZWorksheet) worksheetClass.newInstance(); openWorksheet(worksheet); } catch (Exception e) { throw new GWrappedRuntimeException(context, e, configuration.openWorksheetError, worksheetClass); } } protected void openWorksheet(final ZWorksheet worksheet) { String tabName = worksheet.getWorksheetLabel(); int index = mainTabbedPane.indexOfTab(tabName); if (mainTabbedPane.indexOfTab(tabName) >= 0) { mainTabbedPane.setSelectedIndex(mainTabbedPane.indexOfTab(tabName)); return; } JPanel worksheetPanel = worksheet.getWorksheetPanel(); mainTabbedPane.addTab(tabName, worksheetPanel); mainTabbedPane.setSelectedComponent(worksheetPanel); worksheet.openWorksheet(); } public void closeWorksheet(ZWorksheet worksheet) { if (worksheet.closeWorksheet()) { mainTabbedPane.remove(worksheet.getWorksheetPanel()); } } ... } 

Not rocket science, just framewarez, so... A handy trick is to enable automatic login, e.g. via -DautoLoginUser=test. Then in development mode, we can press F6 to compile and run the framewarez, and straight-away click on the toolbar icon for the worksheet we are working on, to preview and test it in a tight loop without the niggle of logging in a million times a day. For this reason if nothing else, we need to minimise our application startup time, e.g. by using lazy initialisation, and also not overriding our default configuration from externalised configuration files and preferences, e.g. via -DsuppressExternalisedConfiguration true


"The answers aren't at the bottom of a beer bottle, they're on TV!" Homer Simpson. We present a design of a JFrame with a JTabbedPane for worksheets. We install a JMenuBar on the JFrame for launching worksheets. Menu items are configured in the application, and overridden with the externalised configuration, e.g. using JAXB2 to persist the configuration to an XML file. This enables translation and customisation. leffe.jpgAccess control to the menu items is based on user roles. This is persisted to the database, and maintained by the administrative user using worksheets, i.e. for users and user roles, and the corresponding menu access control lists. This design is implemented in, minus some refinements presented here. If you wish to dive deeper into some undocumented non-sugar-coated code, you can look there. Here's that Web Start demo again... webstart.small.gif (695k, unsandboxed, Java5) "I can resist everything except temptation." Oscar Wilde

Resources - where i will collate these articles and their code.