The Role of Argument Maps in text2gui

Argument Maps as a Source of Data at During Conversion

The first purpose of an argument maps is to allow the user to specify values that would be used in a GUI component constructed mainly from resource bundle data. For example, consider and application that displays information laid out in a form. Most of the form can be constructed from static data -- text fields, labels, spaces, etc. This static data can originate from a resource bundle. However, each text field, checkbox, etc. needs to be filled in with a dynamic value (name, address, etc.). But it would be a pain if the caller of the component converter needed to configure these components manually -- after all that is the point of using a component converter. Also we want to leave it up to definition of the component to display the value -- the caller of the component converter shouldn't need to know how the values are displayed. So the solution is to have the caller put the dynamic information into an argument map before passing the map to a component converter -- the component converter then can take values out of the argument map as needed. com.taco.data.INoReturnMap specifies the interface for a basic argument map.

Here's an example of a definition for a text field that takes its text from the argument map:

# Entry point: textField
textField.dispatchType=jtextfield
# Reference the "initialText" key in the argument map
textField.text=$initialText
textField.editable=false
textField.disabledTextColor=GRAY

The text property of the text field is set to $initialText. The $ prefix means that the value comes from the argument map. The following name, initialText, is the key in the argument map that is mapped to the desired value. To create the text field, we would write something like this:

// Get the bundle
bundle = ChainedResourceBundleFactory.DEFAULT_INSTANCE.getBundle("MyResourceBundle",
  locale);


// Create the argument map.
INoReturnMap argMap = new NoReturnMapAdapter();

// Put the initial text in the argument map.
argMap.putNoReturn("initialText", "Howdy!");

// Create the text field.
JTextField textField = (JTextField) DispatchingComponentConverter.DEFAULT_INSTANCE.toObject(
  bundle, "textField", argMap, null);


After these steps, textField is set to a text field with the text "Howdy!". Note the type of the argument map in this case -- NoReturnMapAdapter. This is the most basic implementation of an argument map, which is not observable. The putNoReturn() method does the same thing as Map.put(), but for efficiency, has no return value.

Whenever a reference to an argument map value like $initialText is found, the value corresponding to the initialText key is read from the argument map to use as a property value. This can be disabled by adding a suffix to the reference as in $initialText:w. The lack of an r after the colon (':') means that the value is not read from the argument map initially -- the default value is used.

Updating Created Objects Through Argument Maps

After a component is constructed, the argument map that was used to construct it can also be used to communicate with it via property changes. Communication from the argument map to the component requires that the argument map be observable -- that is, a list of observers can be notified whenever the value mapped to an observed key changes. com.taco.data.INoReturnObservableMap specifies the interface for an observable argument map. During the conversion process, if the argument map is observable, the conversion process automatically adds update listeners to the map so that updates to the map will be reflected in the object created. Each argument map key is bound to the property that it was used to set, so changing the value corresponding to an argument map key will change the property it was used to set.

For example, we could amend the above example as follows:

// Create the argument map.
INoObservableReturnMap argMap = new ObservableMap();

// Put the initial text in the argument map.
argMap.putNoReturn("initialText", "Howdy!");

// Create the text field.
JTextField textField = (JTextField) DispatchingComponentConverter.toObject(bundle,
  "textField", argMap, null);


// Update the text of the text field.
argMap.putNoReturn("initialText", "Doody!");

After the listing above, the text of the text field will be set to "Doody!". The first item to notice is that the type of argument map is now ObservableMap, which is the most basic implementation of an observable argument map. When the conversion occurs during the toObject() call, an update listener is added to the argument map for the initialText key. When the last line executes, the update listener is notified, and updates the text property of the text field with the new map value.

Whenever a reference to an observable argument map value like $initialText is found, an update listener is normally added to the argument map which updates the corresponding property of the created object. To reduce memory allocation, this behavior can be disabled by adding a suffix to the reference as in $initialText:w. The lack of a u after the colon (':') means that the no update listener is added to the map for the corresponding key, so further changes to the mapped value will not affect the created object.

Receiving Notifications of Changes to Created Objects

Swing components are beans, which means that some of their properties are bound. When a bound property of a component is changed, listeners for that property are notified. We can use this mechanism to receive notification of changes in the components that we create. However, notification needs to be done indirectly, or else the GUI state code will depend on the implementation details of the GUI presentation code.

Once again, the argument map is involved in communication with the created object. This time, the argument map receives notification from the the created object. As before, argument map keys are bound to property values of objects. When a property value changes, the value associated with the corresponding argument map key can be updated by a map consistency listener. Because the value in the map is kept up-to-date with the property value of the component, we call the value consistent. This behavior is not the default, but it can be enabled by specifying a suffix to the argument map reference that contains a w.

Not all properties of an object can be made consistent with an argument map. Normally, map consistency for a property requires that the property value is bound. If an object supports other kinds of listeners besides property change listeners, other pseudo-bound properties can be made consistent with the argument map. It's important to check the documented property table for a composite converter to ensure that a property can be made map consistent.

In the example we have been working with, $initialText:ruw could be used as the value for the text property of the text field, like this:

textField.text=$initialText:ruw

Because the w is present, a map consistency listener will be added to the text field. When the user causes the text to change, the map consistency listener will put the new text as the value associated with the initialText key into the argument map.

Passive Input Gathering

There are two ways for an application to deal with changes to a key in the argument map. The first, called passive input gathering, is to use the argument map only for the storage of the corresponding consistent value. When the value is actually needed, the consistent value can be retrieved using get(). This is the way dialog boxes are usually handled, since option values are usually not needed until the dialog box is closed. As an example, we might have the following resource bundle which defines a dialog box which asks the user to select his hero from a list.

dialog.title="Who's your hero?"
dialog.contentPane=%dialogPanel
# Ensure that dialog.show() doesn't return until the dialog is closed.
dialog.modal=true

dialogPanel.layout=box axis=x
dialogPanel.contents=[%heroLabel, {strut}, %heroComboBox]

heroLabel.text=Select your hero:

heroComboBox.items=["Albert Einstein", "Napolean Bonaparte",
"Wilt Chamberlain", "Sun Yat-Sen", "Jean-Luc Picard"]
#The item key in the argument map will be kept up to date with the
#selected hero's name. It is initially read from the argument map too.
heroComboBox.selectedItem=$hero:rw
To create the dialog box, and to retrieve the selected hero's name, we would write the following Java code:

// Get the bundle
...

// Create the argument map
INoReturnMap argMap = new NoReturnMapAdapter();

// Important: Put a default value in for the item key, so get() always has a meaningful
// value to return.

argMap.putNoReturn("hero", "Wilt Chamberlain");

JDialog dialog = (JDialog) DispatchingComponentConverter.DEFAULT_INSTANCE.toObject(bundle,
  "heroDialog", argMap, null);

dialog.pack();

// Since the dialog is modal, this won't return until the dialog is closed.
dialog.show();

// Get the hero selected by the user.
String heroName = (String) argMap.get("hero");

Note that we put a default value of "Wilt Chamberlain" for the hero argument map key before conversion. This is so that even if the user doesn't change the selection, the hero key is still mapped to a valid value. The argument map is only updated when a change to a property value of the created object is made; the initial property value is never put in the map. Thus the application must put in an initial value (and the value must be read during conversion) if the value associated with a key must be valid at all times.

Active Input Gathering

Passive input gathering is useful when changes don't need to be handled immediately. However, some changes do need to be handled immediately, for example, when the user clicks a button. To be notified of a change, an application can add a property change listener to an observable argument map. Then when an observed key has its value updated by the GUI component (via the map consistency listener), the application's property change listener is notified of the change. This way of handling user input is called active input gathering.

For example, the following resource bundle defines a panel with a spinner and a button, which asks for the user's weight:

panel.layout=border
panel.contents=[%upperPanel, {%buttonPanel, {south}}]

upperPanel.layout=box axis=x
upperPanel.contents=[%weightLabel, {strut}, %weightSpinner]

weightLabel.text=Weight (kg):

weightSpinner.value=$weight:ruw

buttonPanel.layout=box
buttonPanel.contents=[{glue}, %button, {glue}]

button.text=OK
button.pressed=$hit:w

An application might create the panel, and receive input events as follows:

// Get the bundle
...

// Create the argument map. This time the map must be observable!
INoObservableReturnMap argMap = new ObservableMap();

// Put the initial weight number in the argument map.
argMap.putNoReturn("weight", new Integer(70));

// Create the panel.
JPanel panel = (JPanel) DispatchingComponentConverter.toObject(bundle,
  "panel", argMap, null);


// Respond to the "weight" property being set
argMap.addPropertyChangeListener("weight", new PropertyChangeListener() {
  public void propertyChange(PropertyChangeEvent event) {
    // The updated value is the new value of the event.
    int weight = ((Integer) event.getNewValue()).intValue();

    if (weight < 50) {
        System.out.println("Eat something, you waif!!");
    } else if (weight > 130) {
        System.out.println("You've been supersized!");

    }
  }
});  

 
// Respond to the "hit" property being set
argMap.addPropertyChangeListener("hit", new PropertyChangeListener() {
  public void propertyChange(PropertyChangeEvent event) {
    // The source of the event is the argument map.
    Map source = (Map) event.getSource();
    int weight = ((Integer) source.get("weight")).intValue();

    if ((weight < 50) || (weight > 130) {
        System.out.println("It's not OK to be satisfied with your weight!");
    } else {
        System.out.println("I guess you're OK.");
    }
  }
});  

Whenever the spinner's value is changed by the user, the first property change listener will be notified immediately. The second property change listener is notified when the user hits the OK button.

Self-Managed Components

Actually, by combining automatic updating and map consistency, components can manage themselves without any involvement from the application. An argument map key's associated value can be kept consistent with a component's property, using the w suffix. Another component can have one of its properties updated automatically when the same argument map key's associated value changes.

Consider a form in which asks the user if he has a pet. If so, a text field is enabled so that the user can fill in the pet's name. The resource bundle definition would look something like this:

panel.layout=grid cols=2 hgap=7 vgap=3
panel.contents=[
  %hasPetLabel, %hasPetCheckBox,
  %petNameLabel,  %petNameTextField
]

hasPetLabel.text=Are you owned by a pet?

hasPetCheckBox.selected=$hasPet:rw
hasPetCheckBox.hAlign=center

#Need double quotes since the string contains a '
petNameLabel.text="Your owner's name:"

petNameTextField.enabled=$hasPet:ru
petNameTextField.text=$petName:rw

The selected property of the check box is made consistent with the hasPet key of argument map by specifying the w suffix. The enabled property of the text field is automatically updated when the hasPet key of the argument map changes by specifying the u suffix. Together, the net effect is that the text field is enabled only when the check box is checked.

To enable self-management, the argument map passed to the converter must be observable. Also, the value associated with the hasPet key of the argument map should be initialized so that the initial state makes sense:

// Get the bundle
...

// Create the argument map. Again, the map must be observable!
INoObservableReturnMap argMap = new ObservableMap();

// Initialize some key / values.
argMap.putNoReturn("hasPet", Boolean.FALSE);
argMap.putNoReturn("petName", "");

// Create the panel.
JPanel panel = (JPanel) DispatchingComponentConverter.toObject(bundle, "panel", argMap,
  null);


Default Values

When creating a component hierarchy written with text2gui code, it is usually easier to test only the components without relying on the application that will create and use the components. The GUI Editor application, which comes with the developer's kit, does just this. The problem is, the GUI Editor application passes in an empty (and observable) argument map to the component converter. Thus reads of the argument map caused by argument map references will fail. To work around this problem, an argument map reference may optionally have a default value associated with it. If an argument map key is not mapped, the default value will be used instead.

A default value is specified by adding a pound ('#') sign to the optionally ruw suffixed argument map reference, then a string specifying the default value, as in $favBook#"Joy of Sets" or $filterSpam:r#true. The default value is converted with the same converter as the one converting the argument map reference. Thus, the default value may itself be a reference.

As an example, a default value could be added to a previous example as follows:

weightSpinner.value=$weight:ruw#70

The value property of a spinner is converted by an instance converter. If the weight key is not defined in the argument map, the instance converter will convert the string "70" into the integer 70, and that will be used as the initial value of the spinner. Note that no value is actually put into the argument map!

Default values are also useful when the definition of a component hierarchy needs to be somewhat independent of the implementation of the code that creates the hierarchy. If a default value is used for an argument map key reference, the code that creates the hierarchy doesn't need to put a value associated with the key, or even know about the key. The default value will be used when a key is missing.

As we shall soon see, additional options can follow the default value. The default value is optional and can be omitted simply by not writing anything after the pound sign.

From and To Map Value Converters

So far, argument map values have been directly usable as component property values. In a previous example, we mapped the argument map key hasPet to a Boolean. The Boolean was used directly as the value of the selected property of a check box. Perhaps we were lucky in this case, but to force every argument map value to be a component property value would imply that the component creation code depended on the details of the component hierarchy. To further decouple the code that creates a component hierarchy and the definition of the hierarchy using a resource bundle, we must provide a way to transform an argument map value into a property value. If a map consistency listener is used, there also needs to be a way to transform a property value into an argument map value.

The conversion between argument map values and property map values is encapsulated in the strategy interface com.taco.util.IObjectMapper. It contains a single method:

/** Convert VALUE to another object. */
Object map(Object value);

Because the method converts objects to objects, com.taco.util.IObjectMapper is a suitable interface for both the argument map to property value converter and the property value to argument map value converter. To specify an argument map value to a property value converter (hereafter called a from map value converter), append a pound sign ('#') after the default value, then append a string specifying an instance of IObjectMapper. The string is converted by an instance converter, so it can either be the name of a class, or a BeanShell script enclosed in curly braces ('{' and '}'). The instance converter is slightly different than default instance converter, in that the interface IObjectMapper is automatically imported into the BeanShell environment for convenience.

For example, the following definition is a defintion text field that uses a from map value converter:

textField.text=$beatsPerMinute#"60"#com.taco.util.DefaultObjectToStringConverter

Presumably, the beatsPerMinute argument map key is mapped to an Integer. However, the text property of a text field has type String, not Integer. Thus a from map value converter is needed to convert the Integer to a String. com.taco.util.DefaultObjectToStringConverter is the fully qualified name of a class which converts an object to a string simply by calling the toString() method of the input object. It implements IObjectMapper, and has a default instance (see InstanceConverter), so it does the trick in this case.

Also, it's important to note that the default value is not converted with the from map value converter. The default value is used directly for the value of conversion when the argument map key isn't mapped.

Here's another example which uses a BeanShell script to do conversion:

label.icon=$color:r##{
  new IObjectMapper() {
    public Object map(Object c) {
      return new Icon() {
        public int getIconHeight() { return 20; }

        public int getIconWidth() { return 60; }

        public void paintIcon(Graphics g, int x, int y) {
          g.setColor(c);
          g.fillRect(x, y, 60, 20);         
        }
      };
    }
  }

In the above example, the color argument map key is expected to be mapped to an instance of Color. The label's icon is converted from a color by a BeanShell script which returns an implementation of IObjectMapper. The interface IObjectMapper is automatically imported, so the script doesn't need to import it again. The value associated with the color argument map key becomes the parameter of map(). map() returns an anonymous implementation of Icon which is then used as the value of the icon property of the label. There is no default value for the icon, so if the color key is not set, the icon won't appear.

From map value converters are optional; if no converter is specified, the argument map value will be used directly as the property value. If a to map value converter string follows (see below), the from map converter can be omitted by simply leaving the string empty.

Now that we've finished with from map value converters, let's tackle the subject of to map value converters. These are the converters that are used to convert property values to argument map values, so that a map consistency listener can update the argument map appropriately when a property value changes. The syntax for specifying a to map value converter is identical to that of specifying a from map value converter, except it follows the from map value converter string. Append a pound sign ('#') after the from map value converter string, then append a string specifying an instance of IObjectMapper. The string is converted by an instance converter, so it can either be the name of a class, or a BeanShell script enclosed in curly braces ('{' and '}'). Again, the interface IObjectMapper is automatically imported into the BeanShell environment.

Here's an example resource bundle that uses a to map value converter to put a command in the argument map, based on which button is pressed:

divideButton.text=Divide
divideButton.pressed=$command:w###{
  new IObjectMapper() {
    public Object map(Object val) {
       return "DIVIDE";
    }
  }
}

conquerButton.text=Conquer
conquerButton.pressed=$command:w###{
  new IObjectMapper() {
    public Object map(Object val) {
       return "CONQUER";
    }
  }
}

Because the pressed property is a pseudo-property that is updated with the value Boolean.TRUE when the button is pressed, there is no need for an initial value. Thus no default value or from map value converter need to be specified. When the Divide button is pressed, the scripted implementation of IObjectMapper simply returns the string "DIVIDE" which is then associated with the key command in the argument map. In this case, the parameter of the map() method is not needed. Similarly, when the Conquer button is pressed, the command argument map key is associated with the string "CONQUER".

Here's an example that illustrates how from and to map converters can be used for i18n applications. In most of the civilized world, temperature is measured using the Celsius scale. For these countries the following resource bundle defines a  panel which presents and gathers a temperature:

panel.layout=grid cols=2
panel.contents=[%tempLabel, %tempField]

tempLabel.text=Body Temperature (C):

# tempField doesn't end with formattedtextfield, so we need specify the dispatch type
tempField.dispatchType=jformattedtextfield
tempField.value=$tempInC:ruw#37

Apparently, the application creating the panel associates the tempInC argument key with an Integer which represents the temperature in Celsius. Unfortunately, we pompous and stubborn Americans still use the Fahrenheit scale to measure temperature. Americans would like to see and enter temperature in Fahrenheit. We can still use the tempInC value if we convert it from and to the map value. A resource bundle specific to the US locale would override two properties:

tempLabel.text=Body Temperature (F):

tempField.value=$tempInC:ruw#99#{
  new IObjectMapper() {
    public Object map(Object val) {
      return (int) Double.round((9.0 / 5.0 * val) + 32.0);
    }
  }
}#{
  new IObjectMapper() {
    public Object map(Object val) {
      return (int) Double.round((5.0 / 9.0) * (val - 32.0));
    }
  }
}


The advantage of this method is that the application can continue to use only temperature in Celsius internally, so it doesn't need to be rewritten. Even if we later add the Kelvin scale for scientists, the application doesn't require changes.

Summary of Argument Map Reference Syntax

Now that we've covered all the options for an argument map reference, we can present the complete syntax for an argument map reference, using Perl-style regular expression syntax:

$ArgumentMapKeyName(:[ruw]+)?(#defaultValue(#fromMapConverter(#toMapConverter)?)?)?#?

In the syntax above, '$', '{''}', and '#'  are literal characters. '[', ']', '+'and '?' have their usual Perl-style regex meaning (see the Javadoc for java.util.regex.Pattern for details).

ArgumentMapKeyName is the name of the argument map key. It may not contains spaces or punctuation except '_'. It may not begin with a number.

If an r appears after the colon (':'), the value corresponding to the argument map key will be used as an initial value.

If a u appears after the colon (':'), a listener will be added to the observable argument map which will update the corresponding property value when the value associated with the argument map key changes.

If a w appears after the colon (':'), if possible, a listener will be added to the created object that will keep the value mapped to the argument map key consistent with the corresponding property value.

defaultValue is converted with same converter as was used for the argument map reference. If the argument map key is not mapped, the the value given by defaultValue will be used as the result of the reference.

fromMapConverter is converted with an instance converter which, in addition to the usual imports, also imports the interface com.taco.util.IObjectMapper. The result of the instance converter should be an instance of IObjectMapper. The IObjectMapper is used to convert argument map values to the result of the reference, and also to update property values of composite objects if the argument map is written later.

toMapConverter
is converted with an instance converter which, in addition to the usual imports, also imports the interface com.taco.util.IObjectMapper. The result of the instance converter should be an instance of IObjectMapper. The IObjectMapper is used to convert property values to argument map values when a map consistency listener is notified of a property change of a composite object.

Argument Map Implementations

How does an application create an argument map suitable for its purposes? By constructing one of the three implementations of argument maps that the text2gui library provides in the com.taco.data package. Starting from least overhead to most overhead, they are NoReturnMapAdapter, ObservableMap, and DelayedResultObservableMap. These maps can also be wrapped to address threading concerns.

NoReturnMapAdapter

NoReturnMapAdapter is the most basic and fastest implementation of an argument map. It is not observable, so it is not suitable for updating a created object through the argument map, and it cannot notify listeners of changes to keys. This kind of map is most suitable for dialog boxes that gather data which doesn't need to be processed immediately.

ObservableMap

ObservableMap is a basic implementation of an observable map. It is good for all-purpose use.

DelayedResultObservableMap

DelayedResultObservableMap is a wrapper over an ObservableMap that allows delayed values to be put and retrieved from the map. A delayed value is a strategy for determining a value at some later time. Often, it is cheaper to create a delayed value rather than to compute the actual value right away. For example, the values property of a JTable represents the entire contents of the table. The values property can be made map consistent, but actually recomputing the entire contents of the table and putting it in the argument map whenever a single element changes would be very inefficient. Instead, when the table's contents change, the map consistency listener puts a delayed value into the map. If the contents are needed immediately, DelayedResultObservableMap will force the delayed value to be computed when the get() method is called on the map. However, if the contents are not needed immediately, many updates can occur without actually recomputing the entire contents each time. Only when the contents are needed are they computed.

Whether delayed values are used is hidden from the text2gui library user. The user retrieves values from the map and is notified of property changes with new values, as usual. The user only needs to use an instance DelayedResourceObservableMap as the argument map to enable delayed values to be stored in the argument map. In addition, the user can put delayed results into argument maps as well. This allows implementations of the component hierarchy that ignore some argument map keys to avoid the cost of constructing the values associated with ignored keys.

DelayedResultObservableMap has all the functionality of ObservableMap, plus the ability to handle delayed values. The disadvantage of DelayedResultObservableMap is that it has more overhead compared to ObservableMap. Use an instance of DelayedResultObservableMap when one or more argument map keys is mapped a property which is expensive to compute.

Making Argument Maps Thread-Safe

The three implementations of argument maps above are not safe for use by multiple threads. Actually, this is not a great concern because Swing component updates occur only on a single thread: the event-dispatch thread.

Without the text2gui library, care must be taken to ensure component properties are only retrieved and set on the event-dispatch thread. If the same care is taken to ensure that argument map operations only occur on the event-dispatch thread, there will be no concurrency problems.

However, to be extra sure that argument maps are only updated on on the event-dispatch thread, the text2gui library provides a proxy of DelayedResultObservableMap, which ensures that all map operations do execute on the event-dispatch thread. When a map operation is invoked on the proxy, the proxy checks if the current thread is the event-dispatch thread. If so, the map operation executes immediately. Otherwise, the map operation is scheduled to be executed on the event-dispatch thread, and the proxy waits until the operation finishes before returning. The proxy adds a significant amount of overhead, but for a moderate number of argument values that are not updated too quickly, the overhead may be worth the assurance of thread-safety. Such a proxy can be created by calling the static method makeSwingInvokeProxyMap() of the class com.taco.swinger.SwingInvokeProxyFactory.

Summary

This document covered how argument maps can be a source of data during object creation, and also how argument maps can be used to communicate with created objects. Also, the options and syntax for argument map references were explained in detail. Finally, the implementations of argument maps provided by the text2gui library were described, so that text2gui library users can select which implementation to use.

Although the mechanisms of communication between argument maps and created objects were described, the motivation for using argument maps for communication was not. Actually, argument maps help to separate GUI code, leading to smaller, cleaner, and more flexible code. See The MVC Architecture and Argument Maps to learn how.