The Source for Java Technology Collaboration
User: Password:



Evan Summers's Blog

June 2006 Archives


Swing and Roundabouts 2: Inside Action

Posted by evanx on June 22, 2006 at 06:06 AM | Permalink | Comments (2)


Prequels
"To err is human, but to really foul things up you need a computer." Paul Ehrlich

See Event DTs, Turn Tables (formerly known as Bean Curd 1), and Panel Beater.

olive2_250.jpg
Introduction
"The best accelerator available for a slow computer is gravity outside the window."

Swing actions are used for buttons, menu items and keystrokes. For example, a "Save" action event might be initiated by the user using a menu item, button or keystroke eg. Ctrl-S.

We need to configure actions centrally, so that consistent labels, icons, mnemonics and keystrokes are associated with a given action, such as "Save." And also so that we can enable and disable an action, without having to attend to every associated button, menu item and keystroke.

So we build an "action framework." Why? Because it's fun, it's useful, and it's legal, so it's a must!

Our action framework is copied from greenscreen, but we will try to improve upon that design here, in particular simplify it, so that anyone can reimplement it from scratch in a few days, or paste and hack it into their project.

Since i redesigned and reimplemented greenscreen some months ago, it's about time to redo that again. So i'm writing this article to present and improve upon that design. So this is an exercise in "design by explanation." Because if it's not easy to explain, then it's not simple enough.


Uncle Vinnie's Application
"Programming today is a race between software engineers striving to build bigger and better idiot-proof programs, and the Universe trying to produce bigger and better idiots." Rich Cook.

Ottidoro_200.jpg Our boss, Uncle Vinnie (from Chicago) wants to modernise our operation. Cos we going legit. And being a legit business, you gotta have these computers and photocopies and fax machines and shredders and stuff like that, you know, to make it look legit.

So we're in the olive oil business. Like everyone else in the neighbourhood. We got different products, like Extra virgin, and some other types and brands.

We gonna build a database front-end application to save our products, and stock quantities and such. Vinnie said we'll leave the supplier and customer records for Phase N. As in, No time soon, probably Never. In the meantime Vinnie uses that little black notebook in his pocket for those. Old habits die hard.

For our application, we prefix class names with Vinnie.

 
public class VinnieProductWorksheet implements ActionListener {

   protected VinnieWorksheetContext context = VinnieWorksheetContext.createContext(this);

   @ActionAnnotation(tooltip = "New product")
   GAction newAction = context.createAction();
   
   @ActionAnnotation(tooltip = "Find product", label = "Find", 
      keystroke = "control F", mnemonic = 'F', icon = "search.png")
   GAction findByNameAction = context.createAction();
   
   @ActionAnnotation(tooltip = "Delete product")
   GAction deleteAction = context.createAction();
   
   @ActionAnnotation(tooltip = "Save product")
   GAction saveAction = context.createAction();
   
   @ActionAnnotation(tooltip = "Exit worksheet")
   GAction exitAction = context.createAction();
      
   ...
   
   public VinnieProductWorksheet() {
      context.configure();      
      configure();
      setEnabled();
      addToolBar(newAction, findByNameAction, deleteAction, saveAction, exitAction);
      ...
   }

   protected void configure() {
      exitAction.setToolTip("Exit worksheet"); // override default
      deleteAction.setKeyStoke(null); // remove default keystroke
      ...
   }
   
   public void actionPerformed(ActionEvent event) {
      context.eventLogger.entering(event);
      if (newAction.isSource(event)) {
         ...
      }
      ...
      setEnabled();
   }
   
   protected void setEnabled() {
      saveAction.setEnabled(productModel.isChanged());
      deleteAction.setEnabled(!productModel.isEmpty());
   }

   protected void addToolBar(GAction ... actions) {
      for (GAction action : actions) {
         JButton button = new JButton();
         button.setAction(action);
         toolBar.add(button);
      }
   }
   
   ...       
}

olive8_crop.jpg Rather than using "dependency injection," we use an approach of exposing all our dependencies via a context object. If you don't like that, then think of it as a shorthand notation to indicate where dependency injection is required. Capice?

In the above code, we have used annotations to configure our actions, and also more direct methods, eg. invoking setToolTip() et al in the configure() method. If you don't like this trick with using annotations for configuration, think of it as a shorthand notation to indicate which setters you gonna invoke in the configure() method. Of course, these settings should be externalised, eg. in a resource bundle, for translation and customisation. But Uncle Vinnie says its OK to hardcode defaults here for rapid prototyping.


Action Configuration Object
"No computer has ever been designed that is ever aware of what it's doing; but most of the time, users aren't either."

Firstly, let's introduce an action configuration object as follows.

 
@XmlRootElement(name="action")
public class ActionConfiguration {    
    @XmlAttribute protected Class worksheetClass; 
        // eg. com.mafia.chicago.vinnie.productworksheet.VinnieProductWorksheet
    @XmlAttribute protected String actionCommand; // eg. "save"
    @XmlAttribute protected String keyStroke; // eg. "control S"
    @XmlAttribute protected Character mnemonic; // eg. 'S'
    @XmlAttribute protected String iconName; // eg. disk.png
    @XmlAttribute protected String label; // "Save"
    @XmlAttribute protected String toolTip; 
    @XmlAttribute protected Integer ordial;
    
    ... // getters and setters
    ... // configure(ActionConfiguration), to overwrite with non-null properties from another instance
    ... // cloneActionConfiguration()
}

We will configure common actions centrally, eg. label, tooltip, keystroke et al, using this class. Also, we will externalise our action configuration by serialising a list of instances of this class. We might use JAXB to persist our action configuration to an XML file. In which case, we can just add JAXB2 annotations into the above class, and Vinnie's your Uncle, woohoo!

Common actions like saveAction would have an null worksheetClass. If a property is not specified, then it will be null, which indicates we should use the default for that action, eg. from the common action configuration.

In the case of common actions, our worksheet's actions typically only override the toolTip eg. "Save product" for the saveAction of VinnieProductWorksheet.


Our Own Actions
"The most reliable components are the ones you leave out." Gordon Bell

Swing has an AbstractAction class with an abstract actionPerformed() method. So we gonna extend this class.

AbstractAction has a Map for its configuration, with keys Action.ACTION_COMMAND_KEY, NAME and SHORT_DESCRIPTION. I was tryna explain to Uncle Vinnie that it might look better to use fields rather than this map. He said, no problem.

 
public class GAction extends AbstractAction {
    
    protected GContext context;

    protected String actionCommand;
    protected String keyStroke;
    protected char mnemonic;
    protected String iconName;
    protected String label;
    protected String toolTip;

    public GAction(GContext context) {
        super();
        this.context = context;
    }
    
    public void setActionCommand(String actionCommand) {
        this.actionCommand = actionCommand;
        super.putValue(Action.ACTION_COMMAND_KEY, actionCommand);
    }
    
    public void setLabel(String label) {
        this.label = label;
        super.putValue(Action.NAME, label);
    }
    
    public String getLabel() {
        return label;
    }
    
    public void setToolTip(String toolTip) {
        this.toolTip = toolTip;
        super.putValue(Action.SHORT_DESCRIPTION, toolTip);
    }

    ... // other getters and setters
 
    public void configure(ActionConfiguration configuration) {
       setActionCommand(configuration.getActionCommand());
       setLabel(configuration.getLabel());
       setKeyStroke(configuration.getKeyStroke());
       ... // other properties
    }

    public void putKeyStrokeAncestor(JComponent component) {
        putKeyStroke(component, JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
    }
    
    public void putKeyStrokeInWindow(JComponent component) {
        putKeyStroke(component, JComponent.WHEN_IN_FOCUSED_WINDOW);
    }
    
    public void putKeyStroke(JComponent component) {
        putKeyStroke(component, JComponent.WHEN_FOCUSED);
    }
    
    /**
     * @see JComponent#WHEN_FOCUSED
     * @see JComponent#WHEN_IN_FOCUSED_WINDOW
     * @see JComponent#WHEN_ANCESTOR_OF_FOCUSED_COMPONENT
     */    
    public void putKeyStroke(JComponent component, int inputMapId) {
        component.getActionMap().put(actionCommand, this);
        component.getInputMap(inputMapId).put(KeyStroke.getKeyStroke(keyStroke), actionCommand);
    }
   
    public void actionPerformed(ActionEvent event) {
        context.eventHelper.actionPerformed(event);
    }
        
}    

olive3_250.jpg The actionPerformed() method delegates to an event helper dependency. Also we introduce some methods for handling keystrokes, because i can never remember the InputMap and ActionMap stuff.

We implement a configure() method to configure an action using an ActionConfiguration.


Event helper
"The price of reliability is the pursuit of the utmost simplicity. It is a price which the very rich find most hard to pay." Edsger Dijkstra

Our event helper is just an event dispatcher to our controller. In effect, it does "automatic event listener registration" by inspecting the controller, In particular it checks if the controller implements the relevant listener interface eg. ActionListener, ie. handles a given event. If so, it forwards the event to the controller, eg. invokes actionPerformed(ActionEvent).

 
public class GEventHelper implements FocusListener, ActionListener, WindowListener {

    protected GContext context;
    protected Object controller = null;
    
    public GEventHelper(GContext context) {
        this.context = context;
    }

    public void setController(Object controller) {
        this.controller = controller;
    }
    
    public void actionPerformed(ActionEvent event) {
        beforeEvent();
        context.actionLogger.entering(event.getSource());
        try {
            if (controller instanceof ActionListener) {
                ActionListener actionListener = (ActionListener) controller;
                actionListener.actionPerformed(event);
            }
        } catch (Throwable e) {
            context.exceptionHelper.severe(e);
        }
        afterEvent(); 
    }

    ... // other listeners
    ... // beforeEvent(), afterEvent() overridable aspects
}

We might extend this class as VinnieEventHelper to override methods eg. the beforeEvent() amd afterEvent() aspects. Then we need to configure our context to use this customised event helper.

olive7_75.jpg
Basic context
"The first 90% of the code accounts for the first 90% of the development time. The remaining 10% of the code accounts for the other 90% of the development time." Tom Cargill

As mentioned earlier, we get into an awful mess by using "customised cloned contexts" rather than IoC dependency injection.

Firstly we have our "framework context" class, which exposes all dependencies required by our framework.

 
public void GContext {

   public static GContext frameworkContext = new GContext();

   public static GLogger eventLogger = new GLogger(this, "eventLogger");
   // logger verbosity set via system properties eg. -DeventLogger.level=FINEST
   // eg. setLevel(Level.parse(System.getProperty(loggerName + ".level", "INFO")));
   
   public static GFormatter formatter = new GFormatter(this);
   
   public GEventHelper eventHelper = new GEventHelper(this);
   
   ...

   protected GContext() {
   }
   
   public static GContext getInstance() {
      return frameworkContext;  
   }
   
   ...
}        

olive4_crop.jpg We don't want null pointer exceptions, so we instantiate references such as eventHelper, with default implementations.

Unfortunately we have to take great care when constructing the above context object. For example, if GLogger references something on this halfbaked context that we pass to its constructor, eg. formatter, then we get a null pointer exception when we construct eventLogger, because formatter is created after eventLogger.


Worksheet context
"When a programming language is created that allows programmers to program in simple English, it will be discovered that programmers cannot speak English."

For event handling, we extend this to include a reference to our worksheet object, eg. VinnieProductWorksheet.

 
public void GWorksheetContext extends GContext {

   public static GActionConfigurator actionConfigurator;

   protected Object worksheet = null;
   
   public GWorksheetContext() {
      super.eventHelper = new GEventHelper(this);
   }
   
   public void setWorksheet(Object worksheet) {
      this.worksheet = worksheet;
      super.eventHelper.setController(worksheet);
   }
   
   public void configure() {
      ... // configure actions using actionConfigurator
      for (Field field : worksheet.getFields()) {
         ... // configure components in the worksheet
      }
   }
   
   ...
}        

When the worksheet is set, we pass this on to the event helper as the controller.

We will introduce our GActionConfigurator later. This is used to configure the common actions of our application in a central, externalisable way. We do this so that our action properties are customisable and translatable.


Custom cloned contexts
"The computer is a stupid machine with the ability to do incredibly smart things, while computer programmers are smart people with the ability to do incredibly stupid things. They are, in short, a perfect match." Bill Bryson

We then extend our framework worksheet context for our application, as follows.

 
public void VinnieWorksheetContext extends GWorksheetContext {

   public static 
   VinnieWorksheetContext applicationContext = new VinnieWorksheetContext();
   ...
   
   protected VinnieWorksheetContext() {
      super();
      super.formatter = new VinnieFormatter(this);
   }

   public static VinnieWorksheetContext createWorksheetContext(Object worksheet) {
      VinnieWorksheetContext worksheetContext = applicationContext.cloneContext();
      worksheetContext.eventHelper = new VinnieEventHelper(this);
      worksheetContext.setWorksheet(worksheet);
      return worksheetContext;
   }

   public VinnieWorksheetContext cloneContext() {
      try {
         return (VinnieWorksheetContext) clone();
      } catch (Exception e) {
         throw new RuntimeException();
      }     
   }
   
   public static void main(String[] args) {
      GContext.frameworkContext = applicationContext;
      ... // launch VinnieApplicationFrame
   }
}        

When we create our application context, we override some of the default dependencies in the framework context with our customised application ones, eg. VinnieFormatter, as in the above constructor.

When we run our application, we customise the framework context instance to the application context instance, as in the above main() method. So when classes invoke GContext.getInstance() they will get a reference to our customised application context.

OliveOil_bottle.jpg When a worksheet creates a context for itself, ie. in createWorksheetContext() above, then the application context is cloned, and the worksheet reference is set on the cloned context, which is now customised for that worksheet.

As i said earlier, it's nasty. But dependency handling is difficult, since there are typically loads of dependencies. Some dependencies are customised for specific classes eg. GEventHelper is created as a peer to a specific worksheet class instance, eg. VinnieProductWorksheet. Others are singletons, but also customisable for our application eg. GFormatter. Others are both customisable for our application, and for a specific instance, eg. GEventHelper is customised as VinnieEventHelper in our application, and is created by the framework for each worksheet instance, eg. in the createWorksheetContext() method above.

As nasty as our "cloned customised context" mechanism is, it is relatively simple compared to a dependency injection framework. And we can avoid XML configurations. Vinnie absolutely insists on this, and otherwise goes flying off the handle.

So we need to pass the relevant context through to our constructors. But we pass one dependency only ie. this context, to expose all the rest of our dependencies. And we need not declare all our dependencies in our client classes, only the context. So Vinnie finds it terribly convenient. Which explains why i do it this way. Because The Boss is always right. Even when he's wrong... Especially when he's wrong, actually.


Central Configuration Station
"Undetectable errors are infinite in variety, in contrast to detectable errors, which by definition are limited."

Vinnie did a stint in jail once, on a trumped up charge of knocking over a cigarette truck. On the inside, when he wasn't pushing weights in the yard, he was in the library learning computers. He came out looking like a jock and talking like a geek. That's when he said we was gonna go legit, and we was gonna get all computerised to prove it.

Vinnie says we want our common actions to be configured centrally in one place as below, just like he learnt in prison.

 
public class VinnieActionConfigurator extends GActionConfigurator {

   @ActionAnnotation(tooltip = "Save", label = "Save", 
      keystroke = "control S", mnemonic = 'S', icon = "disk.png")
   ActionConfiguration saveAction = createActionConfiguration();
  
   ... // many other common actions, eg. new, find, delete, cancel, refresh, next, previous, exit
   
   protected VinnieActionConfigurator() {
      setSmallIconPackage("com.everaldo.icons.crystal.16x16");
      setLargeIconPackage("com.everaldo.icons.crystal.32x32");
      setDefaultIconName("misc");
      configure();
   }      
}   

Where we of course to override these defaults from an externalised configuration file, eg. in the configure() method.


Action configurator
"UNIX is basically a simple operating system, but you have to be a genius to understand the simplicity."

Let's look at our action configuration superclass.

 
public abstract class GActionConfigurator {

   protected Map<String, ActionConfiguration> externalisedActionConfigurationMap = null;
   protected List<ActionConfiguration> commonActionConfigurationList = new ArrayList();
   protected Map<String, ActionConfiguration> commonActionConfigurationMap = new HashMap();

   protected GContext context = GContext.getInstance();
   
   protected String smallIconPackage = "icons.16x16";
   protected String largeIconPackage = "icons.32x32";
   protected String defaultIconName = "misc.png";
   
   protected GActionConfigurator() {
   }

   public void setSmallIconPackage(String iconPackage) {
      this.smallIconPackage = iconPackage;
   }

   ... // setter for largeIconPackage, defaultIconName
   
   public ActionConfiguration createActionConfiguration() {
      ActionConfiguration actionConfiguration = new ActionConfiguration();
      actionConfigurationList.add(actionConfiguration); // configure central action
      return actionConfiguration;
   }

   public void configure() {
      ... // load externalisedActionConfigurationMap eg. from XML file eg. using JAXB
      for (ActionConfiguration commonActionConfiguration : commonActionConfigurationList) {
         configure(field, commonActionConfiguration);
      }
   }

   protected void configure(Field field, ActionConfiguration commonActionConfiguration) {
      String actionCommand = field.getName();
      commonActionConfiguration.setActionCommand(actionCommand);
      ... // configure defaults from ActionAnnotation
      commonActionConfiguration = coalesce(commonActionConfiguration, 
          externalisedActionConfigurationMap.get(actionCommand));
      commonActionConfigurationMap.add(commonActionConfiguration.getActionCommand(), 
          commonActionConfiguration);
   }

   protected ActionConfiguration coalesce(ActionConfiguration actionConfiguration, 
          ActionConfiguration overridingActionConfiguration) {
      if (actionConfiguration == null) return overridingActionConfiguration;
      if (overridingActionConfiguration == null) return actionConfiguration;
      actionConfiguration = actionConfiguration.cloneActionConfiguration();
      actionConfiguration.configure(overridingActionConfiguration);
      return actionConfiguration;
   }   
   
   public void configureWorksheet(Object worksheet) {
      for (Field field : worksheet.getClass().getFields()) {
         field.setAccessible(true);
         if (field.getType() == GAction.class) {
            configure(field, (GAction) field.get(worksheet), worksheet); 
         }
      }
   }

   protected void configure(Field field, GAction action, Object worksheet) {
      String actionCommand = field.getName();
      action.setActionCommand(actionCommand);            
      ... // configure defaults from ActionAnnotation
      ActionConfiguration commonActionConfiguration = commonConfigurationMap.get(action.getActionCommand());
      String key = worksheet.getClass().getName() + "." + action.getActionCommand();
      ActionConfiguration externalisedActionConfiguration = externalisedActionConfigurationMap.get(key);
      ActionConfiguration actionConfiguration = 
          coalesce(commonActionConfiguration, externalisedActionConfiguration);
      if (actionConfiguration != null) {
         action.configure(actionConfiguration);
      } else {
         // this action is not a common one, and its configuration is not externalised (yet)
         ... // maybe generate the externalised XML to cut and paste into our configuration file
      }
      configureIcon(action, smallIconPackage);
   }
      
   protected void configureIcon(GAction action, String iconPackage) {
      if (action.getIconName() == null || action.getIconName().trim().length() == 0) {
         action.setIconName(defaultIconName);
      }
      String iconName = iconPackage + "." + action.getIconName() + ".png";
      URL iconImageUrl = getClass().getResource(iconName);
      try {
         ImageIcon imageIcon = new ImageIcon(iconImageUrl);
         action.setIcon(imageIcon);
      } catch (Exception e) {
         context.fileLogger.warning(e, iconName, iconImageUrl);
      }
   }   
}   

The configure() method gets the list of ActionConfiguration's created in our subclass eg. VinnieActionConfigurator. It configures them using the field name as the action command, extracting the ActionAnnotation, and finally overriding with the externalised configuration, eg. loaded from an XML file to which the actionConfigurationList was previously persisted, eg. using JAXB2.

When we launch our application, we might set GWorksheetContext.actionConfigurator as follows.

 
   public static void main(String[] args) {
      GWorksheetContext.actionConfigurator = new VinnieActionConfigurator();
      GContext.frameworkContext = VinnieWorksheetContext.applicationContext;      
      ... // launch VinnieApplicationFrame
   }

The GWorksheetContext.configure() method will delegate to the configureWorksheet() in the above GActionConfigurator to configure the worksheet's actions appropriately. Defaults are used where these are defined, ie. in common actions. Defaults are overridden with the externalised configuration eg. from an XML configuration file.


Action items
"A printer consists of three main parts: the case, the jammed paper tray and the blinking red light."

Using our actions, we can create buttons and menu items.

 
    public JButton createToolBarIconOnlyButton(GAction action) {
        JButton button = new JButton();
        button.setAction(action);
        button.setText(null);
        return button;
    }

    public JButton createTextOnlyButton(GAction action) {
        JButton button = new JButton();
        button.setAction(action);
        button.setIcon(null);
        return button;
    }

This causes the JButton instances to invoke configurePropertiesFromAction() (in their AbstractButton superclass) to get their label et al from the action. When the button is pressed, the actionPerformed() method in GAction will be invoked, which forwards the event to GEventHelper, which in turn will forward the event to our worksheet, that is if it implements ActionListener.

Menu items are created similarly, as below.

 
    public JMenuItem createMenuItem(GAction action) {
        JMenuItem menuItem = new JMenuItem();
        menuItem.setAction(action);
        return menuItem;
    }    

Finally, keystrokes are activated on components' input maps, using putKeyStroke() methods presented above in GAction.

pizza1_200.jpg
Conclusion
"Computers make very fast, very accurate mistakes."

We continue our Swing adventures from earlier articles Event DTs, Turn Tables and Panel Beater. We present the action framework of greenscreen, with some simplification.

We look at Swing Actions, which are used for buttons, menu items and keystrokes. We configure actions centrally, so that consistent labels, icons, mnemonics and keystrokes are associated with common actions, such as "Save." And also so that we can enable and disable actions, and this will filter through automatically to any buttons, menu items and keystrokes associated to that action.

The configuration of actions is designed to be externalisable eg. in XML configuration files using JAXB2.

Time for pizza... with loads of olive oil!

The next article in this series is Framewarez which will look at building an application frame with menu and tool bars, for housing worksheets, using the actions and worksheet context presented here.

Bean Curd 1F: Swing Panel Beater

Posted by evanx on June 15, 2006 at 07:43 AM | Permalink | Comments (3)


Prequels
"When you think of things, you find sometimes that a Thing which seemed very Thingish inside you is quite different when it gets out into the open and has other people looking at it." Winnie the Pooh

eeyore_sit_crop_150.jpg In Bean Curd 1 we introduced an "explicit properties" approach, where the property descriptors are absorbed into our TableColumn objects, which are declared in our custom TableModel.

In Bean Curd 2: The SQL, we apply the explicit properties approach to object-relational mapping (ORM), to support "native queries," for loading entity beans.

I wrapped up my May blogs in Mayhem Roundup, including the above Bean Curds 1 and 2, in particular the rationale of Bean Curd approach ie. "explicit properties with implicit binding." Mayhem Roundup also presented some of the comments and discussion from other May articles, including Java vs scripting, and GPL vs CDDL.

Another (technical) Swing article I wrote in addition to Bean Curd 1 is Swing and Roundabouts 1: Event DTs.

Health Warning: As always, this is a very noisy article. The noisy bits are italicised, just so you know.


Introduction
"I didn't look around because if you look around and see a Very Fierce Heffalump looking down at you, sometimes you forget what you were going to say. It's hard to be brave, when you're only a Very Small Animal." Piglet

The application archetype I'm mostly interested in is database front-ends. You could call these CURD applications, for Create, Update, Retrieve and Delete. Some other people call them CRUD applications, and that's OK ;)

So we use an ORM to load our "entity beans" from the database. We view and edit these in our Swing application. Finally we'll save them back to the database (using our ORM's entity manager). Or we create new entity beans in Swing, and save those to the database. Or we delete entity beans from the database. It's all the same, urm, crud.

JTable is great for browsing the database. But in this article we look at viewing and editing an entity object using a Swing "form." This is a JPanel with components on it like JTextField, JComboBox, and JCheckbox.

Colleagues and I have (re)implemented a similar approach (ie. Bean Curd 1 plus this) for a few projects. Most recently it is implemented in greenscreen, which is used by aptframework. This paper presents a refined design compared to greenscreen, in particular with respect to validation, which will make it's way into greenscreen in coming weeks (or months).

It is a simple Swing GUI data binding framework, eg. one can implement 80% of it from scratch in a few days. Because 80% of greenscreen took me three days... and nights. I forgot about the nights, actually. The remaining 20% might take a little while longer ;) It has a few downsides I'm sure, but it works for us. I'm sure it breaks a few laws of OO, gravity and what-not. So at least it acheives that objective! ;)

tigger-hacking_out_175.jpg
Entity bean
"And the Small and Sorry Rabbit rushed through the mist at the noise, and it suddenly turned into Tigger; a Friendly Tigger, a Grand Tigger, a Large and Helpful Tigger, a Tigger who bounced, if he bounced at all, in just the beautiful way a Tigger ought to bounce." A. A. Milne

My friend Tigger wrote this bouncy Swing app to keep a handy list of our friends in the forest. Like Roo, Piglet, Eeyore, Rabbit and Owl. Tigger asked me to write this article for him. Because he has a rule about writing documentation. That rule is that he gets me to do it for him.

Let's start with our entity bean. That is to say, let's climb up the tree, and then look down, rather than the other way around.

/**
 * This is a for my friends.
 * @author Tigger
 */
public class FriendEntityBean {
   protected String friendName; // eg. "Pooh"
   protected FriendType friendType; // an enum, eg. POOH_BEAR
   protected ForestFood favouriteFood; // eg. a reference to honey in Pooh's case
   protected Date birthday;
   protected int age; // when the system is first deployed

   ... // getters and setters  
}


Model bean
"It's a funny thing, how everything looks the same in a mist." Piglet

In a simplistic CURD application, the bean we view and edit in our form might be our raw entity bean. But to get flexibility, Tigger likes to wrap his entity beans into more chewy beans, as follows.

public class FriendFormBean {
   protected FriendEntityBean entityBean;
   protected FriendEntityBean originalEntityBean; // for isChanged()
   protected boolean created = false;
   protected boolean deleted = false;
   
   public FriendFormBean() {
      // we are creating a new entity to capture
      entityBean = new FriendEntityBean();      
      originalEntityBean = entityBean.clone();
      created = true;
   }

   public FriendFormBean(FriendEntityBean entityBean) {
      // we are editing an entity we read from the database
      this.entityBean = entityBean;
      originalEntityBean = entityBean.clone();
   }

   public boolean isChanged() {
      return entityBean.compareTo(originalEntityBean) != 0;
   }

   public String getValidationMessage() {
      if (getFriendName() == null) return "Friend's name is null";
      if (getFriendName().trim().length() == 0) return "Friend's name is empty";
      ...
      return null;
   }
   
   public String getFriendName() {
      return entityBean.getFriendName();
   }

   @IntegerRangeValidationAnnotation(minimumValue = 0, maximumValue = 9)
   public void setAge(int age) {
      if (age < 0 || age > 9) {
         throw new GValidationException(
            "They're a funny thing, accidents.\n" + 
            "You never have them til you're having them.\n" +
            "Like now. - Piglet");
      }
      entityBean.setAge(age);
   }

   @NotNullValidationAnnotation()
   public void setFavouriteFood(ForestFood food) {
      entityBean.setFavouriteFood(food);   
   }
   
   @DateNotInFutureValidationAnnotation()
   public void setBirthday(Date birthday) {
      entityBean.setBirthday(birthday);   
   }
   
   ... // other delegating getters and setters, isCreated(), isDeleted()
}

The above bean is a "model bean" representing the data to be displayed in our form. Since this is mostly data in the entity bean, we delegate to the entity bean a lot.

heffalump_125.jpg It isn't really a bean, it's just a POJO. As Owl will tell you. But Tigger likes to call everything a bean. And so I do too.

We show some simple validation mechanisms above. One, we can throw a GValidationException in our mutators, eg. setAge(). Two, we might have a method like getValidationMessage() to check that our data is OK eg. before we save it to the database. Three, we might put annotations on our mutators, eg. IntegerRangeValidationAnnotation. And finally... Oh, that's still to come, in the next section.


Formative Panel
"This writing business. Pencils and what-not. Overrated if you ask me. Silly stuff. Nothing in it." Eeyore

Tigger implements a "form" as a JPanel with a number of "fields" on it. With a whole honey pot of annotations. And ever since Tigger heard of this MVC thing, he tries to label things along those lines. So our form is called a "view." Of the "model bean" we introduced above.

public FriendFormView extends JPanel {

   GFormHelper<FriendFormBean> helper = new GFormHelper(this, FriendFormBean.class);

   @PropertyAnnotation(label = "Name", mnemonic = 'N', displayWidth = 250)
   @LayoutAnnotation(anchor = NORTHWEST)
   @StringValidatorAnnotation(minimumLength = 1)
   @TextFieldAnnotation(maximumLength = 20) // custom annotations for text fields
   JTextField friendName = helper.createTextField();
   
   @PropertyAnnotation(label = "Type of Animal", displayWidth = 200)
   @LayoutAnnotation(fill = HORIZONTAL)
   @ComboBoxAnnotation(selectNoneLabel = "Not specified")
   JComboBox friendType = helper.createComboBox();

   @PropertyAnnotation(label = "Favourite food", displayWidth = 250)
   @LayoutAnnotation()
   @TextFieldAnnotation() // custom annotations for text fields
   JTextField favouriteFood = helper.createTextField(); // lookup field
   
   @PropertyAnnotation(label = "To be deleted")
   @LayoutAnnotation(flow = NEW_LINE, spacer = BOTH)
   @CheckBoxAnnotation(selected = false)
   JCheckBox deleted = helper.createCheckBox();

   ...
   
   public FriendFormView() {   
      super();
      helper.configure();
   }
   
}

As in Bean Curd 1, we let our components get created in the order they are declared, and then finally invoke configure() in the constructor. This reflects on the declared fields, so that they can configure themselves using their own Field, eg. they extract their own field name to be used as the implicit property name for beans binding. And they also extract annotations, which we can use to configure the component, including annotations for validation and layout, as above.

tigger_beer_175.jpg In the example above, we include the default label and display width for each field in the PropertyAnnotation. But we should translate/customise these in a resource bundle. In the case of the display width, we might save and load this using the Preferences API. For example, our fields might listen for a keystroke to increase and decrease their size.

We might also include layout settings in our LayoutAnnotation, eg. gridx, gridy. We try to use GridBagLayout exclusively. But by default we use FlowLayout to flow fields horizontally in a subpanel, and then GridBagLayout to stack subpanels, and throw in a "spacer panel" or two, according to the annotations, eg. fill, anchor, flow, and spacer. We have introduced flow and spacer, to compliment GridBagLayout's fill and anchor, you see.

In general, when writing business apps, enterprise apps, database-front-ends, and such like, we are more interested in keeping it bouncy than anything else. Our "screens" (and reports and what-not), must be quick to implement, and easy for developers to extend and change. So that their weekends are free as in beer. Tiggers are agile and fuzzy creatures, and like to write software that is agile and fuzzy too.

A GUI designer tool makes it easy to build beautiful GUIs, without having to be a Swing expert. Roo prefers GUI designers, and Tigger prefers IDE-agnostic frameworks. Who is right? Owl says they both are right. And that makes them both happy.


Swing honey pot
"It's always useful to know where a friend-or-relation is, whether you want him or whether you don't." Roo

There a few places where we can put our "framework" code. We could have a superclass (which extends JPanel). Or we could put it into a friendly helper class, which we delegate to. This is what Tigger does. That is to say, he prefers composition and delegation to inheritance. In this case, it is possible to have methods in the superclass, that delegate to the helper. And in other superclasses too, although none come to mind immediately.

public class GFormHelper<Bean> implements ActionListener, FocusListener {
   protected JPanel viewObject;
   protected Bean bean;
   protected GBeanInfo beanInfo;
   protected List<GLayoutComponent> layoutComponentList = new ArrayList();
   protected List<GFieldComponent> fieldComponentList = new ArrayList();
   protected boolean ignoreEvents = false;
   protected Object controllerObject = null;
   
   public GFormHelper(JPanel viewObject, Class beanClass) {
      this.viewObject = viewObject;   
      viewObject.setLayout(new GridBagLayout());
      this.beanInfo = new GBeanInfo(beanClass);
   }
   
   public JTextField createTextField() {
      GTextField textField = new GTextField(this);
      configure(textField);
      return textField;
   }

   public void configure(GTextField textField) {
      layoutComponentList.add(textField); // for adding to the panel later in configure()
      fieldComponentList.add(textField); // for bean binding in getModelBean() and setModelBean()
      ... // other custom configuration
   }
      
   public void configure() {
      for (Field field : viewObject.getClass().getFields()) {
         field.setAccessible(true);
         Object object = field.get(viewObject);
         if (object instanceof GFieldComponent) {
            GFieldComponent component = (GFieldComponent) object;
            component.configure(field, beanInfo); // for field name, and annotations
         }
      }
      for (GLayoutComponent component : layoutComponentList) {
         viewObject.add(component, component.getLayoutConstraints());
      }
   }
   
   public void setModelBean(Bean bean) { // update fields from bean
      ignoreEvents = true;
      try {
         for (GFieldComponent component : fieldComponentList) {
            component.setModelBean(bean);
         }
      } finally {
         ignoreEvents = false;
      }   
   }

   public Bean getModelBean() { // update bean from field values
      for (GFieldComponent component : fieldComponentList) {
         component.getModelBean(bean);
      }
      return bean;
   }
   
   public void actionPerformed(ActionEvent event, GFieldComponent fieldComponent) {
      eventLogger.entering(event, ignoreEvents);
      if (!ignoreEvents) {
         ignoreEvents = true;
         try {
            fieldComponent.getModelBean(bean); // update the bean
            if (controllerObject instanceof ActionListener) {
               ActionListener actionListener = (ActionListener) controllerObject;
               actionListener.actionPerformed(event);
            }
            ... // invoke controllerObject.fieldChanged()
         } catch (GParseException pe) {
            showExceptionDialog(pe);
            fieldComponent.requestFocusInWindow();
         } catch (GValidationException ve) {
            showExceptionDialog(ve);
            fieldComponent.requestFocusInWindow();
         } catch (Exception e) {
            showExceptionDialog(e);
         } finally {
            ignoreEvents = false;
         }
      }   
   }
   
   ... // lots of other methods, eg. createComboBox() et al
}

We use a trick for "automatic" event listener registration, where if our controller is an ActionListener, then we forward it action events from its components. Similarly with other types of events, eg. FocusListener.

eeyore_beer_crop.jpg Note that we ignore action events when we are reading from the bean into the form's fields in setModelBean(). In this case, the user is not editing a field, but the developer refreshing the fields from the current values of the bean properties.

When an ActionEvent occurs on the field, we invoke the overloaded actionPerformed() above, with a reference to the relevant field as the second argument. We then update the bean with the new value from the field. At this point, the bean's mutator might throw a GValidationException.

We make sure we catch exceptions that are thrown by event handlers, and display those to the user. Exceptions might be tiny unexpected errors, like null pointers, caused by the developer having too much to do, too soon. Or they might be expected exceptions, like validation exceptions, caused by the user making a big Heffalump mistake like typing the wrong thing at the wrong time, which is totally not allowed of course.

If the user enters a field and we throw a validation exception, we should keep focus on that field, so that the user can get his act together eg. enter a valid value. In the case of a JFormattedTextField, we can use an InputVerifier to nail this too.


One field component
"The most wonderful thing about Tiggers is, I'm the only one!" Tigger

We extend the Swing components JTextField, JFormattedTextField, JComboBox, JPasswordField, JCheckBox etcetera, in a minimal way, in order to implement GFieldComponent so that they all start looking like nails, to be hammered into our form.

public interface GFieldComponent<Value> extends GLayoutComponent {
   public void configure(Field field, GBeanInfo beanInfo);
   public void setFieldValue(Value value);
   public Value getFieldValue();
   public String format(Value value);
   public Value parse(String string); // throws GParseException, GValidationException
   ... // other methods, eg. requestFocusInWindow(), used by GFormHelper
}


Laying it out
"Always watch where you are going. Otherwise, you may step on a piece of the Forest that was left out by mistake."

Components that we are gonna add to the form panel, need to implement the GLayoutComponent interface, ie. our fields, but also buttons and what-not.

public interface GLayoutComponent {
   public GLayoutConstraints getLayoutConstraints();
}

We extend GridBagConstraints to make it chocolaty.

public class GLayoutConstraints extends GridBagConstraints {
   protected int flow; // to mix in flowing sub-panels, ie. using FlowLayout
   protected int spacer; // for adding spacer panels, ie. JPanel with gbc.fill set to this spacer value
   ...
   
   public GLayoutConstraints() {
   }

   public GLayoutConstraints(int gridx, int gridy) {
      super(gridx, gridy, 1, 1, 0., 0., NORTHWEST, NONE, new Insets(0, 0, 0, 0), 0, 0);
   }
   
   public GLayoutConstraints horizontal() {
      super.fill = HORIZONTAL;
      return this;
   }
   
   ...
}

But for the purposes of keeping this article bear-shaped, let's just pretend we use the vanilla GridBagConstraints.

eeyore_rabbit_crop.jpg
Text field example
"It's a funny thing about Tiggers," whispered Tigger to Roo, "how Tiggers never get lost." "Why don't they, Tigger?" "They just don't," explained Tigger. "That's how it is."

Our field components implement the GFieldComponent interface, and delegate to a helper eg. GTextFieldHelper. This helper is a thin customised extension of GFieldComponentHelper which handles the common functionality eg. beans binding, for which it uses GProperty, as we will see later.

It sounds convoluted when put into words, so maybe Tigger lost the plot here. Which can happen when Winnie the Pooh comes over for a 11 o' clock smackerel of something. He disrupts Tigger's train of thought, you see.

public class GTextField<Value> extends JTextField implements GFieldComponent<Value> {

   GTextFieldHelper<Value> helper = new GTextFieldHelper(this);
 
   public GTextField(GFormHelper formHelper) {
      super();
      helper.setFormHelper(formHelper);
   }
   
   public void configure(Field field, GBeanInfo beanInfo) {
      helper.configure(field, beanInfo);
   }   

   public GLayoutConstraints getLayoutConstraints() {
      return helper.getLayoutConstraints();
   }
   
   public void setFieldValue(Value value) {
      super.setText(format(value));
   }

   public Value getFieldValue() {
      return parse(super.getText());
   }

   public String format(Value value) { // in case we wanna override
      return helper.format(value);
   }

   public Value parse(String string) { // in case we wanna override
      return helper.parse(string);
   }   
   
   ... // other methods of GFieldComponent interface, which we delegate out to our helper
}      

eeyore_slouch_100.jpg In the case of GCheckBox, it's getFieldValue() will invoke isSelected() and return a Boolean.

GComboBox's getFieldValue() delegates to its GComboBoxModel to lookup the object value associated with it's getSelected() label, as we will see later.


Field component helpers
'What?' said Piglet, with a jump. And then to show that he hadn't been startled, he jumped up and down once or twice more in an exercising sort of way.

Our GTextFieldHelper keeps a reference to its peer GTextField, and performs any functionality specific to JTextField. But otherwise, the GFieldComponentHelper superclass is the main Heffalump.

public class GTextFieldHelper<Value> extends GFieldComponentHelper<Value> {
   GTextField<Value> textField;
   
   public GTextFieldHelper(GTextField<Value> textField) {
      super(textField);
      this.textField = textField;
   }
   
   public void configure(Field field, GBeanInfo beanInfo) {      
      super.configure(field, beanInfo); // PropertyAnnotation, LayoutAnnotation, and validation annotations
      ... // process TextFieldAnnotation
   }   
   
}   

The configure() methods extract the configuration annotations from the Field.

GComboBoxHelper and GCheckBoxHelper are very much like the above. The Heffalump superclass is implemented as follows.

/**
 * Superclass for GTextFieldHelper, GComboBoxHelper, GCheckBoxHelper, et al.
 * @author Tigger
 */    
public class GFieldComponentHelper<Value> implements ActionListener, FocusListener {
   GFormHelper formHelper;
   GFieldComponent<Value> fieldComponent;
   GLayoutConstraints layoutConstraints;
   GProperty<Value> property;
   
   /**
    * Boa Constructor.
    */    
   public GFieldComponentHelper(GFieldComponent<Value> fieldComponent) {
      this.fieldComponent = fieldComponent;
   }

   /**
    * Get the GProperty from beanInfo, configured using PropertyAnnotation et al
    * Note, we should also invoke setFormHelper() to finally configure this baby.
    * @see GBeanInfo#createProperty(Field)
    */
   public void configure(Field field, GBeanInfo beanInfo) {      
      property = beanInfo.createProperty(field);
      fieldComponent.addActionListener(this); // forwards events to GFormHelper
   }   
   
   /**
    * Forward event to formHelper with reference to source field component.
    * @see GFormHelper#actionPerformed(ActionEvent)
    */
   public void actionPerformed(ActionEvent event) {
      formHelper.actionPerformed(event, fieldComponent); 
   }
   
   /**
    * Format value to viewable string representation, by delegation to GProperty.
    * @see GProperty#format(Value)
    */
   public String format(Value value) {
      return property.format(value);
   }

   /**
    * Convert and validate string, by delegation to GProperty.
    * @see GProperty#parse(String)
    */
   public Value parse(String string) {
      return property.parse(string);
   }   

   /**
    * Push value into field component from bean.
    * @see GProperty#getValue(Bean)
    */
   public void setModelBean(Object bean) { 
      fieldComponent.setFieldValue(property.getValue(bean));
   }

   /**
    * Pull value from field component into bean.
    * @see GProperty#setValue(Bean, Value)
    */
   public void getModelBean(Object bean) { 
      property.setValue(bean, fieldComponent.getFieldValue());
   }
      
   ... // other methods
}      

We use the GProperty property descriptor wrapper, to read and write that property's values to and from our bean, eg. an instance of FriendFormBean. And also to format, parse and validate property values to and from their string representations. Heh heh... Hm... Eh?

eeyore_sit_side_crop3.jpg
Combo box
"I used to believe in forever, but forever is too good to be true." Winnie the Pooh

We implement GComboBox as follows. I don't understand this, so I'm just gonna present it. Any questions, post them. Then my people will get hold of Tigger's people and get back to your people.

public class GComboBox<Value> extends JComboBox implements GFieldComponent<Value> {

   GComboBoxHelper<Value> helper = new GComboBoxHelper(this);
 
   public GComboBox() {
      super();
   }
   
   public void configure(Field field, GBeanInfo beanInfo) {
      helper.configure(field, beanInfo);
   }   
   
   public void setFieldValue(Value value) {
      super.setSelected(format(value));
   }

   public Value getFieldValue() {
      return parse(super.getSelected().toString());
   }

   public String format(Value value) {
      return helper.format(value);
   }

   public Value parse(String string) {
      return helper.parse(string);
   }   
   
   ... // other methods of GFieldComponent interface, which we delegate out to the helper
}      

GComboBoxHelper in turn delegates the format() and parse() to GComboBoxModel. I asked Tigger why so much delegation, from the component to the helper, to the model. He said, "To make it more bouncy, of course!"


Combo box model
"I don't see much sense in that," said Piglet. "No," said Pooh humbly, "there isn't. But there was going to be when I began it. It's just that something happened to it along the way."

public class GComboBoxModel<Value> extends DefaultComboBoxModel {
   GComboBoxHelper<Value> helper;
   List<Value> valueList = new ArrayList();
   Map<String, Value> valueMap = new HashMap();
   String selectNoneLabel = "Select...";
   String selectAllLabel = "The whole honey pot";
   
   ... 
   public void add(String label, Value value) {
      valueMap.put(label, value);
      valueList.add(value);
   }

   public void add(Value value) {
      add(format(value), value);
   }
   
   public void addAll(Value ... values) {
      for (Value value : values) {
         add(value);
      }
   }

   public void addAll(List<Value> valueList) {
      for (Value value : valueList) {
         add(value);
      }
   }
   
   public void addSelectNone(String label) {
      add(label, null);
      selectNoneLabel = label;
   }

   public void addSelectAll(String label) {
      add(label, null);
      selectAllLabel = label;
   }

   public String format(Value value) {
      return helper.comboBox.format(value);
   }

   public Value parse(String label) {
      return valueMap.get(label);
   }
     
   public Object getElementAt(int index) { // implementing ComboBoxModel
      return format(valueList.get(index));
   }

   public int getSize(int index) { // implementing ComboBoxModel
      return valueList.size;
   }
   
   public boolean isSelectNone() {
      return getSelected().equals(selectNoneLabel);
   }

   public boolean isSelectAll() {
      return getSelected().equals(selectAllLabel);
   }
   
}

eeyore_lift_crop.jpg A null value might correspond to a selection of "All" or "None" or both. So we offer methods like isSelectAll() so we can check, ie. when the value is null, and we have "All" and "None" as available selections, when which is it?

There is Another Big Issue. Usually we want to support auto-completion for combo boxes. But we gonna dodge that issue for now.


Filling the combo box
"Company means Food... and Listening-to-Me-Humming and such like." Winnie the Pooh

We might populate a combo box model from an enum type as follows.

   public void addFriendTypes(GComboBox comboBox) {
      for (FriendType friendType : FriendType.values()) {
         comboBox.getComboBoxModel().add(friendType.toString(), friendType);
      }
   }

Another example would be populating from a database table, eg. via a DAO method eg. foodComboBoxModel.addAll(entityManager.food.getEntityList()).

For very large tables, ie. "high volume" combo boxen, we gotta treat those as A Special Case. With auto-completion and reactive fetching and populating of the combo box. And multi-column combo boxes and database lookup-popups. Here's a suggestion, let's seriously forget about all that for now. And leave it for Swing and roundabouts: My big fat geeky combo box.

Hope they bought that? Phew, that was a close one. Tigger better work out how to do all that at some stage!


Bean wiring
"Poetry and Hums aren't things which you get, they're things which get you. And all you can do is go where they can find you." Winnie the Pooh

We should have introduced the GBeanInfo and GProperty wrappers sooner. Here they are, later.

public class GBeanInfo<Bean> {
   protected BeanInfo beanInfo;
   protected Map<String, GProperty> propertyMap;
      
   public GBeanInfo(Class beanClass) {
      this.beanInfo = Introspector.getBeanInfo(beanClass);
      for (PropertyDescriptor propertyDescriptor : beanInfo.getPropertyDescriptors()) {
         GProperty property = new GProperty(this, propertyDescriptor)
         propertyMap.put(propertyDescriptor.getPropertyName(), property);
      }
   }
   
   public GProperty getProperty(String propertyName) {
      return propertyMap.get(propertyName);
   }
   
   public GProperty createProperty(Field field) {
      return propertyMap.get(field.getName()).configure(field);
   }

}

We use GProperty to read and write to the bean, basically.

public class GProperty<Value> {
   protected GBeanInfo beanInfo;
   protected PropertyDescriptor propertyDescriptor;
   protected String label;
   protected GPropertyType propertyType; 
   protected Integer displayWidth;
   protected String format;
   protected List<GValidator> validatorList = new ArrayList();
   protected GFormatter formatter = GContext.getFormatter();
   ...
   
   public GProperty(GBeanInfo beanInfo, PropertyDescriptor propertyDescriptor) {
      this.beanInfo = beanInfo;
      this.propertyDescriptor = propertyDescriptor;
   }

   public GProperty configure(Field field) {
      // extract PropertyAnnotation, to set label, displayWidth, propertyType et al
      // extract validation annotations, to construct validatorList
      ...
      return this;
   }
   
   public Value getValue(Object bean) {
      return (Value) propertyDescriptor.getReadMethod().invoke(bean);
   }
   
   public void setValue(Object bean, Value value) {
      propertyDescriptor.getWriteMethod().invoke(bean, value);
   }   
   
   public String format(Value value) { // delegate to GFormatter
      return formatter.format(value, this);
   }

   public Value parse(String string) { // throws GParseException 
      return validate(formatter.parse(string, this));
   }

   public Value validate(Value value)  { // throws GValidationException  
      for (GPropertyValidator validator : validatorList) {
         validator.validate(value);
      }
      return value;
   }

   ...
}      

We store meta-data extracted from annotations in GProperty, required to perform conversion and validation. So let's introduce GValidator and GFormatter implementations below, to see what we're getting.

Pooh_100.jpg
Validation Annotation Station
Pooh looked at his two paws. He knew that one of them was the right, and he knew that when you had decided which one of them was the right, then the other was the left, but he never could remember how to begin.

Our list of GValidator's in GProperty is constructed from validation annotations, eg. StringValidatorAnnotation, IntegerRangeValidatorAnnotation, DateRangeValidatorAnnotation, and any number of other validation annotations we wish to introduce.

Let's consider the following example.

public class GStringValidator extends GValidator<String> {   
   protected Integer minimumLength;
   protected Integer maximumLength;
   protected boolean nullable = false;
   protected boolean empty = false;
   
   ...
   public void validate(String value) {
      if (value == null) {
         if (!nullable) {
            throw new GValidationException(this, "is null");
         }
         return;
      }         
      if (!empty && value.trim().length() == 0) {
         throw new GValidationException(this, "is empty");
      }
      if (minimumLength != null && value.length() < minimumLength) {
         throw new GValidationException(this, "is shorter than " + minimumLength);
      }
      ...
   }
   
   public void configure(Field field) {
      ... // extract StringValidatorAnnotation to set minimumLength et al
   }         
}

Our GValidator superclass keeps a reference to its GProperty, so that our validation exception messages can report the label of the invalid property to the user, eg. "Friend's name is null".

We might create an InputVerifier for a JFormattedTextField as follows.

public class GPropertyVerifier extends InputVerifier {
   GProperty property;
   
   public GPropertyVerifier(GProperty property) {
      this.property = property;
   }
   
   public boolean verify(JComponent component) {
      if (component instanceof JFormattedTextField) {
         JFormattedTextField textField = (JFormattedTextField) component;
         String text = textField.getText();
         try {
            property.parse(text);
         } catch (Exception e) {
            return false;
         }
      }
      return true;
   }
   
   public boolean shouldYieldFocus(JComponent component) {
      return verify(component);
   }
}                


Annotation Aches with Panes
"Don't underestimate the value of Doing Nothing, of just going along, listening to all the things you can't hear, and not bothering." Roo

eeyore_fallen_small.jpg Unfortunately annotations do not support null values or inheritance, so we work around that, eg. using hacks like default values of -1 and empty strings ie. "" to mimic null values. And cut and paste between annotations like a crazed Heffalump. Very messy stuff.

Tigger wishes that Annotations were POJOs. Where specifying values is like setting bean properties. But they aren't. So we always convert or extract annotations into objects right away, and then conveniently forget about them. Then we can use nulls for properties not specified in the annotation, eg. in the above GStringValidator example, minimumLength is null by default.


Parsing and formatting
'I thought Tiggers were smaller than that.' 'Not the big ones,' said Tigger

Our fields need to "format and parse." That is, they need to convert between objects and strings, and visa versa. Because Pooh and friends read and write strings, but computers are objects and references which are ones and zeroes.

One bouncy way of doing this is having one Heffalump helper to format and parse them all.

public class ForestFriendsFormatter extends GDefaultFormatter {
   ...
   public String format(Object value, GProperty property) {
      if (property.isDateFormat()) return dateFormat.format(value);
      if (value instanceof FriendType) return value.toString();
      if (value instanceof ForestFood) return format((ForestFood) value);
      ...
      return super.format(value, property);
   }
   
   public Object parse(String string, GProperty property) throws GParseException {
      if (property.isDateFormat()) return dateFormat.parse(string);      
      if (property.isTimestampFormat()) return timestampFormat.parse(string);
      if (property.isString()) return string;
      if (property.isInteger()) return new Integer(string);
      ...
      return super.parse(string, property);
   }
   
   ... // custom formatting, eg. format(ForestFood)
}

Actually much of the above would be in the superclass ie. GDefaultFormatter. We extend that to customise it, eg. for our special types eg. FriendType and ForestFood.

eeyore_side_crop.jpg As you can see, we don't buy into anything too fancy here in the forest. We have visiting to do, afternoon naps and things that keep us busy. So when we write software, which we do strictly mornings only, we hope that it works. If it doesn't, we fix it tomoro, time permitting. And anyway... what are cyclic dependencies, by the way?


Custom fields
"Tiggers can't climb downwards, because their tails get in the way, only upwards." Tigger

We might implement custom fields, and overwrite format() and parse(), which are otherwise delegated by the GFieldComponent, firstly to its GFieldComponentHelper helper, then to its GProperty property descriptor, and finally to the GFormatter implementation. Uh Hm. This is what Tigger calls 'whOOPy' programming, all this delegation and inheritance and friends-and-relations, is what makes it so nice and bouncy!

public FriendTypeField extends GComboBoxField<FriendType> {

   public FriendTypeField() {
      super();
      populate();      
   }

   public FriendTypeField(GFormHelper helper) {
      super(helper);
      populate();      
   }
   
   protected void populate() {
      for (FriendType friendType : FriendType.values()) {
         comboBoxModel.add(friendType.toString(), friendType);
      }
   }
   
   public String format(FriendType value) {
      return "A " + value.toString();
   }

   public FriendType parse(String string) {
      return super.parse(string.substring(2));
   }
}   

A Small Problem is that our component factory creates regular text fields and what-not. So we can extend our factory ie. GFormHelper to introduce a factory method like createFriendTypeField(), or we can use the GFormHelper.configure(comboBox) method, as follows.

   FriendTypeField friendTypeField = new FriendTypeField();
   ...
   public void configure() {
      helper.configure(friendTypeField);
      ...
   }

Or we can pass our GFormHelper context through to our custom combo box, so that its GComboBox superclass can invoke helper.configure(this) for us.

So many choices and decisions... Time to have Owl over for tea and a little smackerel of something, shall we?


Form programming
"Bounding up trees is easy for Tiggers. The difficulty is in the climbing down, backwards." Tigger

Our form controller class might be implemented as follows.

public FriendFormController implements ActionListener, GFieldListener {
   
   FriendFormView friendFormView = new FriendFormView();
   
   FriendFormBean friendFormBean = new FriendFormBean();
   
   ... 
   
   public FriendFormController() {      
      friendFormView.helper.setController(this);
      clear();
      ...
   }

   protected void clear() {
      friendFormBean = new FriendFormBean();
      rebind();
   }

   protected void rebind() {
      friendFormView.helper.setModelBean(friendFormBean);
   }
   
   public void actionPerformed(ActionEvent event) {
      eventLogger.entering(event);
      // CURD events
      if (newAction.isSource(event)) {
         if (friendFormBean.isChanged()) {
            if (!guiHelper.showConfirmationDialog("You wanna loose changes?")) {
               return;
            }
         }
         clear();
      } else if (saveAction.isSource(event)) {
         if (!friendFormBean.isChanged()) {
            guiHelper.showMessageDialog("Nothing to save")) {
         } else if (friendFormBean.isDeleted()) {
            entityManager.friend.deleteEntity(friendFormBean.entityBean);
         } else if (friendFormBean.getValidationMessage() != null) {
            guiHelper.showMessageDialog(friendFormBean.getValidationMessage());
         } else {         
            entityManager.friend.saveEntity(friendFormBean.entityBean);
         }
      } else if (findAction.isSource(event)) {
         if (friendFormBean.getFriendName() == null) {
            guiHelper.showMessageDialog("Enter a name first");
         } else {
            FriendEntityBean friendEntity
               = entityManager.friend.getFriendByName(
                  friendFormBean.getFriendName());
            friendFormBean = new FriendFormBean(friendEntity);
            rebind();
         }         
      } else if (deleteAction.isSource(event)) {
         if (friendFormBean.isCreated()) clear();
         else friendFormBean.setDeleted(true);
      }      
   }
   
   public void fieldChanged(GFieldEvent event) {
      eventLogger.entering(event);
      if (friendFormView.friendName.isSource(event)) {
         traceLogger.finer(event.getFieldComponent(), event.getOldValue(), event.getNewValue());
         if (event.isEntered() || event.isFocusLost()) {
            if (friendFormBean.getFriendName().trim().length() == 0) {
               guiHelper.showMessageDialog("Who dat?");
               return;
            }
         }
      }
      // TODO get more bouncy here
   }

   public void documentChanged(GFieldEvent event) {
   }
   
}   

To make it bouncy, we might introduce our own new event specifically for fields, for when the value of a field is changed by the user. When they tab out of the field (which is a "focus lost" FocusEvent), or press Enter (which causes an ActionEvent), we might invoke fieldChanged().

tigger_sit_125.jpg We might throw a documentChanged() event handler into our GFieldListener, which comes into play when the user is editing a text field or what-not, courtesy of our GFormHelper listening for a DocumentEvent and turning that into a GFieldEvent.

OK, let's wrap up. Time for a nap!


Summary
"When late morning rolls around and you're feeling a bit out of sorts, don't worry; you're probably just a little eleven o'clockish." Winnie the Pooh

We continue from Bean Curd 1, to apply "explicit properties" to the Swing JPanel and its fields.

We explicitly declare field components in our form, eg. JTextField, JComboBox et al. Bean binding is performed implicitly, using the field names of the components. Annotations are used to specify layout, validation and stuff. Oh, and we introduce an interface for fields, to make them all look like honey pots. Our field components delegate to helpers and property descriptors to do all the work.

We program to a beautiful bouncy POJO "form model" rather than heavy Swing models. We reference our components as necessary, eg. to invoke setEnabled(), requestFocusInWindow(), et al, and also to identify them as the source of events in our event handlers.

But we don't have to worry about extracting, converting and validating values from fields. We put that into a framework, where it belongs. A very simple framework mind you.

The framework handles events nicely and politely for us too. It does automatic event registration, event filtering (eg. ignoring events when they should be ignored), and introduces a handy custom event for fields, to hide the vagarities of ActionEvent and FocusEvent, which are mixed up with other types of components. Finally, it makes sure that exceptions thrown by event handlers are handled eg. the user is told.

And now for smackerel of something to eat, and then a lovely nap!

bees_and_sunflowers_175.jpg
Coming sooner or later
"Before beginning a Search, it is wise to ask someone what you are looking for before you begin looking for it." Winnie the Pooh

Coming up in the Swing series is "Swing and Roundabouts 2: Lightweights, Canvas, Action!" on Swing Actions (for buttons, toolbars, menus and hotkeys), and "Swing and Roundabouts 3: Framewarez" on building a tabbed application frame for our "worksheets." After that, maybe "Swing and Roundabouts 4: My big fat geeky combo box" on database lookups.

In this Bean Curd series, "Bean Curd 2X: The Xtended Version" will look at applying those native queries to XML, "Bean Curd 3: On Form" will look at binding HTML forms, and a "Bean Curd 4: On the table" series will look at explicit properties for documents, like PDF, HTML and Excel ones.

In the meantime, check out our Featured Prequel...

Thanks: This blog was written using Netbeans, and previewed using Firefox (as hoofed in Netbeans, my weblogging tool). It was started in my sister's house, carried on in my brother's house, continued in my sister's office, and finished in my mom's cottage, on my new Windows XP notebook (as mooted in My Desktop OS: Windows XP), so... now you know! Pictures from yotophoto.com, and flickr.com, notably agnieszka.


Featured Prequel
"You can't always sit in your corner of the forest and wait for people to come to you... you have to go to them sometimes." Eeyore

tigger_honey_pot_125.jpg I'm gonna use this soapblog to punt my previous article, Mayhem Roundup, because if I don't, who will? ;) So this is a roundup of that Roundup article of my May blogs, including Bean Curds 1 and 2, in particular the rationale of their "explicit properties with implicit binding" approach used here.

First things first. My favourite NIH quote in that article is by Homer Simpson. "You couldn’t fool your mother on the foolingest day of your life if you had an electrified fooling machine." Heh heh. And my favourite quote of my own, is "I gotta respect other views that I might disagree strongly with. Not least because they might be right."

Mayhem Roundup presented some of the comments and discussion of my May articles. Especially my own. And especially the discussion related to using Java for "scripting" tasks, and building libraries rather than more scripting languages.

And also exploring the GPL, LGPL, CDDL, and SCA. Because boring legal stuff is well important, innit.

Mayhem roundup

Posted by evanx on June 08, 2006 at 06:13 AM | Permalink | Comments (5)


Prologging
"All my life I've had one dream: to achieve my many goals" Homer Simpson

I've been meaning to start blogging on java.net for a while. To promote myself and my project, and because i really love writing. I go into a whole new world, into a trance with a silly grin on my face. Same as coding.

empty_audience_200.jpg This new era of an interactive web, this so-called Web 2.0 thing, is something special. In the past, you read an article, and then moved on. Now you can post a comment, and engage other readers, and the author. And blog yourself. This is something special. We can participate, and interact. We can do what human beings were made to do!

Typically i mostly listen. This comes from when i was a stutterer at school. You were not gonna catch me opening my mouth if i could possibly help it! One day that all changed, and i starting giving big public talks at every opportunity. Anyway, that's probably why i like writing so much, because it's always been a safe, comfortable environment to express oneself without any risk of stuttering and stammering!

So i'm between contracts. Supposedly working on my project fulltime. But then The Java Posse rode into my iTunes...


Explicit Reflection
"Internet! Is that thing still around? Oh, so they have internet on computers now!" Homer Simpson

posse3.jpg When listening to Java Posse in March one morning, working on my project, I emailed in to "If I was the king of Java..." about "explicit reflection." It was something i had wished there was in the language, because it would have helped me.

I was so excited about my email being lasso'ed by The Posse, that i decided to write my first blog to expand upon the issue.

This started a theme pursued in subsequent articles, like Refactoring Translations and Bean Curds 1 and 2, that string literal references are unwelcome.

Netbeans_125.png
Netbeans Day (South Africa) Trilogy
"Remember as far as anyone knows, we're a nice normal family." Marge Simpson

As it happened, Netbeans Day was taking place that week in South Africa, with James Gosling in attendance.

During the presentation, I decided to take notes of the interesting points. Out of habit. Not because I ever go back and read my notes, but it just feels good to "Save As."

When i got home, I found myself writing a summary of the talks, and those notes came in handy!

Part 1 summarised Geertjan Wielenga intro on Netbeans 5. Preaching to the converted in my case! But there were some very handy tips for me, eg. F2 for bookmarks, "Camel Case Completion" eg. WL for WindowListener, Alt Shift W to enclose with try/catch, and nbextras.org.

Geertjan left a nice comment, and also Chuk Munn Lee (who also presented on the day). I was chuffed!

To quote myself from Part 2

Personally i love the idea of wizards, and sample projects, with integrated documentation, rather than traditional static documentation, like a PDF. I call this "active documentation." Netbeans 5.0 JEE Blueprints are a great example.

and regarding the RCP

I have a happy suspicion that we'll see some great Netbeans RCP apps (and IDE extensions) popping up in future. Personally I'd love to see a whole desktop suite built on RCP - mail client, tabbed browser, and file browser. Then we'll really have a consistent, integrated, and extensible Java desktop!

Part 3 summarises Sang Shin's talk on Netbeans for JEE. I dig his master tutorial index on javapassion.com :)

In part 3, I got gushy...

The golden age is dawning, with fantastic languages, libraries, tools, databases, servers, frameworks, components - all freely available, and cross-platform - with tutorials to rule them all - and with Netbeans, it's all in one little download.

Roman Strobl commented on Derby integration. I was chuffed to find that anyone was reading my blog entry, but Roman Strobl! I have really appreciated his video tutorials, and blogs and podcasts, in the past, so that was great.

I ended the article wishing for a Netbeans weblogger plugin, and Gregg Sporar pointed me to bloged.dev.java.net

Geertjan left a comment that he was gonna link to my summaries of Netbeans Day (South Africa) from his blog. Wow, this blogging thing was really working for me! You start to feel like you are inside this whole community, and not outside looking in, and it feels great!

thinkfreeCalc175.png
Swing trumps... everything!
"Burns releases the hounds on every charity that comes to his door - Feed the Children, Save the Whales, even Release the Hounds."

I was writing a comment to Swing trumps Ajax.

I suggested that Sun will opensource Java because...

"Sun wants developers to be a field of Sunflowers, following the Sun."

Everyone has their favourite things, that they feel passionately about, and other people feel passionately about different things. I wanted to write passionately about my favourite things. At the same time, one doesn't want to offend and upset people. So it's a tight rope. I gotta respect other views that I might disagree strongly with. Not least because they might be right. Certainly they are right, in a situation and perspective other than my own.

I learnt a few tricks at navigating the tight rope. For example, I said, "I think that GNOME is gonna overtake KDE." GNOME people are happy because I'm saying GNOME is best, and KDE people are happy because I'm saying KDE is best. Whereas if i had said, "GNOME is better than KDE" or visa versa, well, the "comments" section would have got messy. Which fortunately it didn't! :)

Lemme paraphrase some thoughts presented there.

My dream for the future of computing, is stateless rich client applications using web data services, where we cache in on rich applications via the web, like Thunderbird, OpenOffice, and of course new Swing and .Net ones too, via WebStart, Google Pack, or whatever, and where your documents and settings follow you around on your GDrive, LiveDrive, Amazon S3, YahooDrive, SunGrid, or whatever.

Personally I think that Sun should reinvent itself as a Web 3.0 software service company in this way, and become the biggest consumer of their own hardware.

Step one is to build SunGrid storage support into OpenOffice (and Firefox and Thunderbird). Why sell CPU time to a few organisations, when you can sell computing to the whole world?

The issue of Java/Swing performance came up in the comments, and i weighed in...

Swing_s.png

If i'm given the choice (and clients too) of delivering something in three months that's gonna need 1Gb of RAM to run, or the same thing in six months, at twice the cost, but that'll only need 512Mb to run, i think you can guess everyone's answer ;)

Considering that Java and C# are languages much like C/C++, any implementation performance issues are solvable engineering problems. This is evident in Hotspot and Mustang. Memory use will be higher (for garbage collection), but apps will be much easier to write and debug. Performance will be comparable, sometimes slightly better (eg. Glassfish's Grizzy using NIO), sometimes worse, than native C/C++ (or Python, or C#) implementations. When it's a lot worse, and that's a big problem, it becomes a priority to fix it, and it gets fixed. It just takes resources allocated - excuse the pun.

You point out that C# is newer and learnt from Java's mistakes - which ones bother you the most, and can't the community do something about them? An (dis)advantage of Java over C# is that there is a community and the JCP (of which Apache, IBM, Google, Oracle, et al, are no small part of) to evolve the language (and standards and libraries), rather than one company. Java 5 has evolved, in response to C# and thanks to the Java community - introducing enums, generics, varargs. Java 5 took away all my big itches.

The point is that every language (and tool and library) needs to evolve in response to new developments, and Java is doing that, and C# is doing that, and they are in healthy competition. In hindsight "mistakes" are made of course (even tho at the time there were the right decisions). When they are identified they should be fixed as soon as possible (eg. aggressive refactoring). Sometimes they can't be fixed right away because that will break backwards compatibility. So lets deprecate them and fix them in five years time. Yes, there are cases where in hindsight, we wish we had done things differently from word go, and would be better off if we had (eg. generics, altho at the time, that was too much effort, and other priorities were more important, probably). Which is why designs and APIs should be, first and foremost, blindingly simple and minimal. Then there's less opportunity to do wrong. And Java got that pretty much right.

Let's make Java (through the JCP), stand the test of time, and get better with age, so we don't have to throw everything out and start again with C#, or the next "best language." Lets make Dolphin etcetera, be the next "best languages."


But enough about you, let's talk about me!
"You couldn’t fool your mother on the foolingest day of your life if you had an electrified fooling machine." Homer Simpson

I had a huge amount of fun and catharsis writing My Desktop OS: Windows XP. I could not wait for my brother to get home from work so that we could giggle hysterically over it.

linux-av.png In case you were wondering, this blog is being written on my new "Windows XP" notebook. I downgraded my cell phone and got a notebook thrown in. Well, a 50% rebate. Notebooks have power saving modes and all types of fancy things, and there is no ways i'm gonna reinstall the thing with Dapper!

I'm embarrassed to say, I do prefer Windows these days. I cannot be arsed to fiddle around with video drivers, and editing repositories, I'd rather be blogging! :) I know Dapper and Easy Ubuntu rocks. So I've made a note to try the Ubuntu after Edgy, ie. April 2007. Until then, my Windows XP notebook, and Mac Mini media center, are gonna keep me very happy.


Bin Bash Java
"I’ve figured out an alternative to giving up my beer. Basically, we become a family of traveling acrobats." Homer Simpson

In his blog, Damien Katz wrote on "Signs you are a crappy programmer, and don't know it." At the top of the list is "Java is all you'll ever need." Since i'm guilty of that, I explained why i choose not to add other languages to my toolbox in Java is all you'll ever need.

Java developers should be programming/scripting in Java (or Groovy), for the same reason that we program in Java and not C... "Give me more scripting power, Scotty McNeally!"

C# programmers are gonna be using that fabulous C# .Net command-line, and make our bash scripts look silly. And make us look silly using Python because Java can't cut it. Lets make Java sharper, not just for big applications, but also for small tasks too.

A reader commented that "anyone thinking he needs only a single tool to do any job is a fool." Since that would be me, I wrote A Fool's Errand to introduce Bin Bash Java 1.

penknife3.jpg

I try to make the point that rather than invent/implement a scripting language, another approach is to implement a support library that achieves similar convenience and functionality, with other advantages, eg. readily toolable using Netbeans and Eclipse, today.

Bash and shell scripting is great if you know it, but not so great for a student who only knows Java. It's also not so great for CIO's when ex-employees leave scripts behind that have grown into unreadable, unmaintainable and unmanageable monsters, on which your organisation's infrastructure now depends, and which keep jumping out of wardrobes to give young programmers nightmares.

I argue that these are legacy solutions, and there is space for a modern Java solution, for the convenience of Java programmers. At this stage it's an academic exercise. A fools errand, as i said ;)

GNU/unixversal utilities like tar et al, are great, but you usually you gotta program them in an extremely limited and highly fragile scripting language (bash).

In general I think that Java/Netbeans/Mattise is a great tool for building front-ends to such utilities, and also wrapping these utilities into a library that is highly programmable, using Java.

AHalsey's comment below resonated with me.

I am tired of the needless language proliferation I see happening today. It fragments our work - partitioning it into incompatible islands. I am a Java programmer. I've spent years mastering the ever growing mountain of Java APIs and have created some myself. Why on earth should I have to learn a new languages' APIs when writing small scripts - it's crazy! It dilutes our effectiveness. Microsoft knows this and will allow programmers to leverage their API knolwedge investment when scripting. I hope Java community has an answer to this - inititives such as Groovy, BeanShell gives me hope.

He pointed me at NailGun which aims to obviate the start time of the JVM, eg. for Java command-line tasks. They say,

Java has an extensive and robust core API and huge number of available open source libraries. It's a great big hammer, making almost any programming project look like a nail.

GlassFishBowl.jpg
CDDL'ing the Duke
"Oh no! What have I done? I smashed open my little boy's piggy bank, and for what? A few measly cents, not even enough to buy one beer. Wait a minute, lemme count and make sure... not even close." Homer Simpson

When the JavaOne DLJ and "opensource Java" news hit the headlines, I wrote Dux in a Tux where I mused about GPL'ing Java as "GlassFishBowl" and rigorously protect the Java trademark, so that Java still means "Java" as in the JCP, TCK, JEE, et cetera.

There were lots of comments. Including one from myself as follows.

My understanding of the GPL and the LGPL is that libraries (eg. GNU libc used in Linux et al) have to be LGPL in order to allow non-GPL'ed programs to link to them. That is that non-GPL'ed programs and GPL'ed cannot be statically or dynamically linked together.

The preamble of the LGPL says

When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library.

Therefore i would assume that the class libraries (to which non-GPLed java programs link as a "shared library" albeit via the JVM) would have to be LGPL'ed.

GNU Classpath for instance is "licensed under the GPL plus a special exception" as follows

Linking this library statically or dynamically with other modules is making a combined work based on this library. Thus, the terms and conditions of the GNU General Public License cover the whole combination.

As a special exception, the copyright holders of this library give you permission to link this library with independent modules to produce an executable, regardless of the license terms of these independent modules...

Since I didn't know much about the CDDL, I explored it in CDDL'ing up to Sun, and summarised the differences between GPL, MPL (of which the CDDL is the state of the art), and BSD (of which the ASL is the state of the art), as follows.

In the BSD/ASL commons, you don't have to contribute your modifications or your derived works back into the commons.

In the MPL/CDDL commons, you have to contribute only your modifications back into the commons.

In the GPL commons, you have to contribute both your modifications and your derived works back into the commons.

My final words were

Opensource promotes common development, where the idea is to engender a community of contributors. But maybe in some cases it's more about distribution. And in others, it's a marketing gimmick. And in most cases, it's some combination of all of the above!

Justice3.jpg On the issue of joint-copyright as required by the "Java Contributors Agreement" (and similarly by FSF and Apache), I commented

I don't see how Contributor Agreements and joint-copyright can be avoided in today's landscape. You wanna have at least one entity that is copyright holder of the entire project to defend the project in court, for one thing. If you don't want to contribute code under a Contributor Agreement, then don't - no one is holding a gun to your head - fork it and fork off.

The way I see it, assigning joint-copyright is pretty much like contributing under a Apache license in addition to the CDDL. This doesn't detract from the contributors rights - it just gives the project group "free use" rights. Which I think they deserve since they donated all their work into the commons to start with, to be benefit of those contributors, and everyone else.

Otherwise the project group lose "free use" of their whole project, as soon as they accept one little patch. That being the case, they could not accept any patches to start with.

I really respect the (Free)BSD attitude, which is, "Here, take this software I wrote - please use it as you wish - make me proud."

Jim Driscoll made the following enlightening comment.

Copyright is not land. If two people own land, they both separately don't have the same rights as if one of them owned it. When two people own copyright, they both have the same rights as if they were a single owner. This is why piracy is not equal to theft.

beans_125.jpg
Bean Curd
"Every time I learn something new, it pushes some old stuff out of my brain. Remember when I took that home winemaking course and forgot how to drive?" Homer Simpson

In Bean Curd 1, I introduced an "explicit properties" approach, where bean properties are declared in an "explicit bean info" class. Actually the bean info class and property descriptors are absorbed into the components bound to the bean, for convenience.

There are a couple of problems with using string literals as references to properties (and fields and methods).

The most serious problem is that they make refactoring fragile. For example, consider that we wish to rename the realName property to just name. The problem is that our IDE doesn't know to change the string reference "realName" as well.

So we can, and probably will, overlook renaming the string reference, since there is no prompting by the IDE or compiler to tell us to do so. This will definitely result in a runtime error. "That's gonna hurt in the morning!" Ok, this problem should be solved using unit tests to make sure that fragile string references are valid.

Hey, I'm lazy. So i don't like string references, because they do not take advantage of the IDE's prompting, auto-completion, and error highlighting. In this sense, they are not readily "toolable."

In Bean Curd 2: The SQL, we apply the "explicit properties" approach to object-relational mapping, and the DAO pattern. This enables us to support "native queries" ie. stringless queries which are toolable, and promote ORM refactoring.

The problem with string queries is highlighted as follows.

So we map our database to nice Java names (in our entity beans). The problem is that as soon as we use the mapped names in string queries (eg. OQL, EJBQL, HQL), then we immediately lose refactorability. It becomes impossible to fix up spelling errors and naming inconsistencies, without breaking our queries. "Thaaat's mentil!" And if you ignore a few broken windows, the next thing the whole building is run down, innit.

locale.png
Refactoring Translations
"Bart, always remember that you’re representing your country. I guess what I’m saying is, don’t mess up France the way you messed up your room."

In Refactoring Translations, I presented an approach I used for "refactoring" strings out of an application, as a first phase in preparing a resource bundle for translation.

In general, I argue that source code should contain no string literals whatsoever! The reason for this is that string literals are typically fragile references, which are not refactorable. This applies to strings that refer to field or method names, as discussed in Explicit Reflection, string references to properties, as discussed in Bean Curd 1, and strings used in OR queries, as discussed in Bean Curd 2.

Clearly strings that are text messages are also undesirable, because they should be externalised for translation (in resource bundles).

And finally string references to externalised messages in resource bundles, are fragile and unable to be unit tested, and consequently dangerous, eg. getString("loginError").


Swing and roundabouts
"Don't eat me! I have a wife and kids! Eat them!" Homer Simpson to the Alien

In my last project, most Swing event handlers kicked off a string of "long tasks" (namely, communicating to a server over a GSM network), and I got into such a tangle with SwingWorkers upon SwingWorkers (so as not to block the EDT), that the code became increasingly difficult to follow.

swings4.jpg In Swing and Roundabouts 1: Event DTs, I presented how I left the event handlers pretty much as is, run them using a SwingWorker, but where they use an "EDT-hardened" GUI helper class, to manipulate the GUI, eg. popup a dialog, request focus for components, and enable/disable components.

I used an event/listener type solution for my nested SwingWorkers at first, but what i didn't like about it, was that i could not easily follow the sequence in the code using Alt-G. So i was losing track and found development and debugging very difficult. So i gave up on that.

Ideally the developer should be able to code naturally, without concern for the EDT, threads, and such plumbing. The developer has a hard enough job as it is implementing the required functionality, without the added strain of worrying about technical plumbing issues.

So the framework (eg. the future JSR 296 one), should, i believe, enable the developer to code relatively naturally, ie. without concern for EDT issues.

An elegant solution that someone proposed (that i came across somewhere in my reading), is to use annotations, eg. @InEdt for methods, that might cause them to be compiled to run within the EDT, eg. taking care of the boiler-plate code like "if not isEventDispatchThread(), then invokeAndWait(new Runnable() {})."


Coming soon
"When will I learn, the answer to life's problems aren't at the bottom of a bottle, they're on TV!" Homer Simpson

Future articles in the "Bean Curd" series might be "Bean Curd 4: On Form", looking at using our "annotated integrated explicit property" approach for HTML forms, for the purpose of capturing parameters for native query servlets, and Bean Curd 5: PH&P - Pdf, Html and Poi for producing reports in PDF, HTML and/or Excel, using annotated property descriptors, native queries and servlets. But first I might do, Bean Curd 3: Swing Form Binder just to mix it up.

Finally, Swing and Roundabouts will delve into the all important Action's, look at automatic registration of event handlers, and also an application shell with a menu and tool bar, to support tabbed application panels.

Netbeans, my weblogging tool

Posted by evanx on June 07, 2006 at 05:31 AM | Permalink | Comments (6)


Hammers and nails
"I write when I'm inspired, and I see to it that I'm inspired at nine o'clock every morning." Peter De Vries

quill_100.jpg I've been using Netbeans to draft my java.net weblogs of late. Because I like rich clients. And i love Netbeans. I loved Eclipse and IntelliJ too. But you gotta pick your tool and stand by it. And go in search of nails.

Java.net weblogs is my newest nail, and i've been hammering it. With Netbeans. In particular for my last two articles, namely "Swing and Roundabouts 1: Event DTs" and "Bean Burd 2: The SQL".

The last article was a long one. I knew it was gonna be. The web browser is killer for reading, but not for writing, not long articles anyway. The "any internet cafe" convenience of gmail and blogger is great. But when I'm at home, I want something rich and fattening!


Taking off the latex gloves
"I never know what I think about something until I read what I've written on it." William Faulkner

In the past I used jEdit to edit my articles in HTML. I like HTML because it reminds of me a LaTeX, which I used for years before there was HTML, and years after HTML came along too. But in the new millenium I switched to HTML with CSS.

HTML and CSS are great because your style and content are separate, so you can focus on your content, just like LaTeX. For example, you can define a style for your code samples as follows.

.code { 
   font-size: 9pt;
   font-family: courier; 
   background: #fcfcfc; 
   padding-top: 0px;
   padding-left: 15px;
   padding-bottom: 15px;
   margin-top: 2px;
   border: 1px dashed green;
   border-style: dashed dashed dashed dashed;
   page-break-inside: never;
   color: black; 
}

Add a few other styles, eg. for document title, subtitle, section headings, subsections, and you're good to go.

You can change the style in one place, and your whole document automagically falls in line. That's how it should be! Blindingly simple, yet incredibly powerful :)


One zip to view them all
"The difference between fiction and reality? Fiction has to make sense." Tom Clancy

yast_zip.png All I wish for is an ODF-type standard format for zipping your HTML, CSS and images into one file that Firefox and Thunderbird (for starters) can understand and view. Like a PDF for the web. Let's call it IDF for Internet Document Format. So it's just a zip file with an index.html. Sweeet. You heard it here first, ladies and gentlemen. Forget all those other people who said it before!


Counting the time
"Only those things are beautiful which are inspired by madness and written by reason." Andre Gide

I've found with long articles, it takes a day to write two pages. I wrote this Swing article last year. The first draft took 5 days, and was 12 pages. I was bored so I decided to extend it. To cut a long story short, it ended up being 45 pages, and took about a month, ie. 22 days.

And this 15 page SQL article took me 8 days. So that's two pages a day, by all accounts.


Bizarre cathedral
"I love being a writer. What I can't stand is the paperwork." Peter De Vries

Cathedral1-150.jpg The bitter moral of the story? Keep it short and sweet. Break it up into multiple parts. That's what's great about blogging. And opensource. "Release often." Don't build cathedrals, build bazaars. No that's not it. Don't build cathedrals in one release, is what it should be. Using bazaars, or something. Ok, I flocked it up, sorry Eric. Send me a signed copy of your book, so i'll never flock it up again, please, please!?

Eric Raymond is really a great guy. How do i know? He stayed with me, in my house! Actually, very small apartment. He was visiting Cape Town, and our LUG appointed me to accomodate him. My impresssions of him before I met him was that he was an arrogant gun-toting guy. Then you meet the chap, have a few lunches with him, and you quickly realise why he is leading our movement. He is a diamond twinkling in the rough. Not arrogant at all. A humble, extremely knowledgeable person that you wish you could marry so that you could listen to him all the time. Because he is so fastinating to listen to, about anything and everything. Besides computers I mean! Throw a dart at wikipedia and you'll enjoy his input on the subject of whatever your dart hits.

OK, lemme focus. It's difficult to stay on track today... well, most days actually.


Public display format
"I have always been a huge admirer of my own work. I'm one of the funniest and most entertaining writers I know." Mel Brooks

So this long 45 page article that I was writing, I edited in jEdit as an HTML document, with a few lines of CSS at the top, and used Firefox to preview it. You know the drill, Ctrl-S Alt-Tab F5. And then I used PDFCreator to print it from Firefox to a PDF file. PDFCreator is the dogs!

font_type1.png The point is that I was using deliberately using HTML to produce a PDF document! Isn't that wikkid cool in a beautifully twisted way? An advantage I guess is that you can readily put the article up on the web. Although I didn't do that with that article, because the company didn't reply to my request to publish it under a creative commons. Doesn't matter, I'll write it again, right here on java.net, piece by piece.

But that wasn't why I was writing it in HTML. I was writing it in HTML because HTML/CSS is my first choice of document formatting language thes