|
|
||
Evan Summers's BlogJune 2006 ArchivesSwing and Roundabouts 2: Inside ActionPosted by evanx on June 22, 2006 at 06:06 AM | Permalink | Comments (2)
See Event DTs, Turn Tables (formerly known as Bean Curd 1), and Panel Beater.
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.
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);
}
}
...
}
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.
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.
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);
}
}
We implement a configure() method to configure an action using an ActionConfiguration.
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.
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;
}
...
}
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.
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.
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.
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.
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.
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.
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.
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 BeaterPosted by evanx on June 15, 2006 at 07:43 AM | Permalink | Comments (3)
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.
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! ;)
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
}
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.
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.
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.
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.
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.
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.
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
}
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.
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
}
GComboBox's getFieldValue() delegates to its GComboBoxModel to lookup the object value associated with it's getSelected() label, as we will see later.
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?
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!"
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);
}
}
There is Another Big Issue. Usually we want to support auto-completion for combo boxes. But we gonna dodge that issue for now.
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!
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.
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);
}
}
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.
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.
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?
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().
OK, let's wrap up. Time for a nap!
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!
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.
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 roundupPosted 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.
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...
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.
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!
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...
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."
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.
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.
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.
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.
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!
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.
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.
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").
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.
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() {})."
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 toolPosted by evanx on June 07, 2006 at 05:31 AM | Permalink | Comments (6)
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!
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 :)
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.
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.
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!
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 | ||