/home/projects

Gumby

Overview

Gumby is a XUL-like framework that allows creating Swing GUIs through XML. Gumby uses Java's introspection capabilities and the Confix API to create objects from XML tags. Gumby does not impose an abstract markup language independent from the underlying UI implementation; Gumby IS Swing with an XML touch and some cool features such as scripting and databinding that allow quickly building forms such as this:.

There are mutiple examples that come with the source distribution. Having a look at them will greatly help. The XML configuration files are in the etc/test directory, and the corresponding Java source files are in the java/intg directory.

Goals

Gumby's first goal is to elimitate the hassles that consume so much time when building UIs in Java: the tedious code-compile-test cycle, the harcodedness and verbose syntax (casting, static typing), the wiring Java code that links UI components with business logic and that often becomes spaghettified...

Another goal is to eliminate the mandatory browser-based webapp - at least in some scenarios: despite the myriad of web frameworks that are attemping to address the problems stemming from building web applications, none can really overcome the HTTP protocol's limitations when it comes to managing complex user interfaces.

Features

Here are Gumby's features, at a glance:

  • XML markup that is tightly bound to Swing: you use XML elements and attributes to instantiate objects and call methods on these objects.
  • As part of the toolkit, a couple of classes and interfaces that helps structure applications and can easily be integrated with existing lightweight containers.
  • Scripting integration through BeanShell and Pnuts
  • Support for databinding: build forms in a snap through a clean MVC implementation.
  • Some goodies such as the TablePanel class that puts an HTML-ish face on the dreaded GridBagLayout.

Non-Features

Here's what Gumby IS NOT:

  • A generic XUL markup that allows transparently replacing the underlying UI toolkit: we simply don't believe in this approach, Swing is too different from SWT ('cause we know you want SWT, don't you?). Over-generalizing would mean loosing the advantages and features of the underlying toolkit. Plus, we think Swing can do the job, so no need to plan for alternatives. Furthermore, I work with Eclipe+Linux on a daily basis, and let me tell you SWT does not perform better on Linux than Swing. Too bad Netbeans does not yet have what I want...
  • A kit that creates UIs with XML and does just that: what about integration with business logic? what about databinding?

Example

Create the XML markup

The following corresponds to the Swing Tutorial's GridBagLayout example.

<gumby:TablePanel xmlns:swing="java:swing"
                  xmlns:gumby="gumby:swing">
                  
 <tr>
   <td weight="0.5">
     <swing:JButton>
       <text>Button 1</text>
     </swing:JButton>
   </td>
   <td weight="0.5">
     <swing:JButton>
       <text>Button 2</text>
     </swing:JButton>
   </td>   
   <td weight="0.5">
     <swing:JButton>
       <text>Button 3</text>
     </swing:JButton>
   </td>   
 </tr>
 
 <tr>
   <td colspan="3" weight="0.0" pady="40">
     <swing:JButton>
       <text>Long-Named Button 4</text>
     </swing:JButton> 
   </td>
 </tr>
 
 <tr weight="1.0">
   <td></td>
   <td colspan="2" 
       weight="0.0" 
       pady="0" 
       valign="bottom" 
       align="right">
     <swing:Insets top="10" 
                   left="0" 
                   bottom="0" 
                   right="0"/>       
     <swing:JButton>
       <text>Button 5</text>
     </swing:JButton> 
   </td>
 </tr> 
</gumby:TablePanel>

Instantiate

...
      
import java.io.File;
import javax.swing.JFrame;
import javax.swing.JPanel;
import org.sapia.gumby.RenderContextFactory;

...
      
public static void main(String[] args) {
  try {
    JFrame.setDefaultLookAndFeelDecorated(true);
    JFrame frame = new JFrame(name);
    JPanel panel =  
    (JPanel)RenderContextFactory
        .newInstance()
        .render(new File("etc/test/tablepanel.xml"));
    frame.setContentPane(panel);
    frame.pack();
    frame.setVisible(true);    
  } catch(Exception e) {
    e.printStackTrace();
  }
}

...

Gumby From 30,000 Feet

The Example Explained

As the example above shows, Gumby creates objects corresponding to an XML description file. As was mentioned, Gumby uses Confix to achieve such a task. Thus, your XML should respect a certain format (the one expected by Confix). More specifically:

  • XML elements that are associated to a namespace (through a prefix) have their corresponding objects created by pre-registered "object factories" - we will show you in a minute how this is done.
  • XML elements that are not associated to a namespace (with no prefix) are always nested within the ones that are "namespaced" (or, at least, the first non-namespaced element in a hierarchy always as a namespaced element as a parent. The non-namespaced elements correspond to the creator, adder or setter methods that have the corresponding name on the parent object (corresponding to their immediate ancestor's XML element) - more on that later, but you should read the Confix doc really, this is not a plug.
  • Architecture

    Internally, there's not much to it: everything is built around Confix. From an application programming point of view though, there are a few classes and interfaces to be familiar with:

  • The GuiEnv interface models an "environment"; it represents a mapping of scoped values - an instance of the interface holds values in terms of Scopes. In addition, as the Javadoc explains, implementations of the interface are expected to respect a delegation model where a given instance might have another instance as a parent, and so on. When looking up a value, an implementation will search its own scopes before delegating to the parent. The interface is intended to allow integration between Gumby and your applications.
  • A Scope is a kind of reduced java.util.Map; in fact, an implementation of the Scope interface that is part of Gumby relies on the HashMap class. As you might guess, scopes are meant to be held in GuiEnv instances.
  • The RenderContext class is what you use to "render" Gumby XML configurations - that is, create an object (Swing component) from a Gumby configuration. A RenderContext encapsulates a GuiEnv. If you eventually implement your own Gumby tags, they can have access to their context by implementing the ContextAware interface. Then, your tags can have access to the "environment", and lookup arbitratry, application-specific objects that are made available through that environment - this is one way to integrate Gumby with your application. A RenderContext context can have another context instance as a child; such a child as its own environment, but in addition inherits the one from its parent. This has been introduced so that specific sections of a UI can have their own protected area, without loosing touch with the global environment.
  • The RenderContextFactory is, well, a factory of RenderContext. One good strategy is to create the first RenderContext with the factory, and then create other contexts with that first context instance - the other contexts thus become children of that first context, which holds global values that all sub-contexts can potentially access.
  • The View class is a special kind of Scope: it corresponds to the view in MVC. A view holds an arbitrary object that represents the model in MVC, as well as Bindings (see next) that associate the model (or parts of it) to the view (more on MVC with Gumby later on).
  • The Binding interface specifies callbacks that are use to synchronize a model with its view. The callbacks correspond to the different events that may occur in the model/view association. The code that handles these callbacks (part of which may be generalized, part of which may be application specific) consist of the controller role in the MVC pattern.
  • Gumby and XML

    Tags in General

    Gumby allows using XML to instantiate objects. Really, that's what it is. As was mentioned, the Confix API is used to fullfill that task. The question is: how does Gumby create objects from given tags - i.e.: how does the tag-to-object mapping works? Simple: internally, Gumby in fact maps XML namespace prefixes to ObjectFactory instances. When looking at the example further above, you sometimes see the gumby prefix, and sometimes the swing prefix. Both of these prefixes have been pre-registered with the framework (that is, they are hard-coded). But nothing stops you from creating your own tags and configure their so called "object definitions" in a configuration file that you can load within Gumby; your definitions could be map to the prefix of your choosing. Thus, instantiating the objects corresponding to your definitions could later be done through XML, in the following way: myPrefix:myTag.

    Gumby comes bundled with default tags. They are documented in the tag reference.

    Now, you might ask, how exactly are such tags configured? Default Gumby tags are kept in the gumby_defs.xml file that is Gumby's jar, under the org.sapia.gumby package. The file is loaded as part of a static initializer when you first use the RenderContextFactory. An excerpt of the file is given below:

    <gumby:config xmlns:gumby="gumby:swing">
    
      <gumby:namespace prefix="gumby">
        <def class="org.sapia.gumby.widgets.Icon" 
    		     name="Icon" />    
        <def class="org.sapia.gumby.tags.Import" 
    		     name="Import" />    
        <def class="org.sapia.gumby.tags.Constant" 
    		     name="Constant" />        
        <def class="org.sapia.gumby.tags.Register" 
    		     name="Register" />        
        <def class="org.sapia.gumby.tags.Ref" 
    		     name="Ref" />
        ...   
      </gumby:namespace>    
    </gumby:config>

    Thus, similarly as above, configuring your own tags is straightforward:

    <gumby:config xmlns:gumby="gumby:swing">
    
      <gumby:namespace prefix="myPrefix">
        <def class="org.foobar.MyTag" name="myTag" />
        ...   
      </gumby:namespace>    
    </gumby:config>

    Then you would simply use your tag in XML as follows:

    <gumby:config xmlns:myPrefix="my:namespace"
                  xmlns:swing="java:swing">
      <swing:JPanel>
        <component>
          <myPrefix:myTag someAttribute="someValue"/>
        </component>
      </swing:JPanel>
      ...
    </gumby:config>

    Of course, it would be tedious to have to open Gumby's jar each time you need to configure your own object definitions. Thus, new definitions can be loaded through the RenderContextFactory, using its loadDefinitions() method. When you load your definitions this way, they become available to all RenderContext instances that have been created with the factory.

    If the above scope is too large (if you want to limit visibility of some definitions to some given parts of your application), then you might use the loadDefinitions() method of the RenderContext from which these definitions should be visible - the delegation model explained earlier on will then come into play.

    Swing Tags

    For all tags bound to the swing prefix, Gumby internally uses a special object factory implementation. For any XML element passed to it, it will look within its registered definitions if a tag has been defined; if not, it will try to load the class under the javax.swing package (not child packages of that one) that has the name corresponding to the XML element passed to it, and create an instance of it through Java reflection.

    Of course, the above only works for classes that have a no-args constructor. For creating objects of classes that have no such constructor, a New tag is provided.

    Gumby Tags

    In addition to Swing widgets that can be loaded by using the proper XML tags, Gumby comes built-in with specialized tags for different purposes. The example seen further above demonstrates the use of the gumby:TablePanel tag - which creates a TablePanel instance. Other tags are provided with Gumby to facilate handling views, databinding, and so on.

    Using Gumby

    Using Gumby can be summed up as follows:

    1. Configure the XML corresponding to the Swing component you want to use.
    2. Create a RenderContext
    3. Set up the context with appropriate (app-specific) environment parameters.
    4. "Render" your XML using the context.
    5. Use the rendered object (usually a Swing component) in any way you please.

    Normally, you render Swing components that you add to some container (a JFrame, JPanel, etc. ). The container might also have been created with Confix, or not. It's up to you; that's the beauty of it.

    Scripting

    To accelerate development, Swing supports two scripting languages: Beanshell and Pnut (the latter is an interesting one that sports great performance). A convenient use of scripts is to implement listeners, such as java.awt.event.ActionListeners. Implementing them in Java can be a tedious task, since they mostly serve as wiring widgets with backend code (they have a role to play in MVC, as we'll see later on). The example below demonstrates how to add action listener to a button:

    ...
    
    <swing:JButton text="Click !!!">
      <actionCommand>click</actionCommand>
      <actionListener>
        <gumby:Expr>
          import javax.swing.JOptionPane;
          import java.awt.event.ActionListener;
          import java.awt.event.ActionEvent;
            
          new ActionListener(){
           
            public void actionPerformed(ActionEvent e){
              frame = 
                RenderContext
                  .getEnv().acquire("Frame", 
                                    "application");
              JOptionPane.showMessageDialog(
                frame, 
                "Action command: " + 
                  e.getActionCommand()
              );
            }
          }
        </gumby:Expr>
      </actionListener>
    </swing:JButton>
    
    ...

    If you look at the example above, we use the gumby:Expr tag assign a listener to our button. This built-in tag is used to evaluate expressions; an expression is a script that evaluates to an object. In the example, the returned object will be an ActionListener. In the actionPerformed() method, you can see that the scripts uses a RenderContext; as you might guess, this variable corresponds to the RenderContext instance that was used to render the XML of which the script is part. In the above case, the script is assumed to be in Beanshell; to indicate that you use Pnuts, use the following notation:

    ...
        <gumby:Expr lang="pnuts">
        ...
        </gumby:Expr>
    ...

    The above shows how to access the environment of the script's context. In this case, the script acquires the Swing frame of the application - from the "application" scope. That is exactly why scopes exist: to provide the snippets of code that make up your application with a handle to the context in which they evolve. Scopes are further dealt with next.

    Scopes

    Scopes are act as isolated maps of values. Multiple hosts can coexist, and scopes have no kwownledge of each other. Scopes are bound to the environment - a GuiEnv instance. Each scope has a given name in that environment, therefore one must generally indicate in which scope to lookup in order to retrieve a given value, or in which scope to bind a given value, using the name of the target scope.

    Since Scope is an interface, it can be implemented in all sorts of ways. One could implement a scope over JNDI's Context interface, and so on.

    Thus, you use the environmenent to make "external" objects available to your Swing application, and you categorize these external objects by scope within the environment, in order to avoid naming conflicts, and also because environments (GuiEnv instances) in Gumby are hierarchical: as a consequence, scopes can therefore be present at different levels: you could decide to create an "application" scope as part of the environment of the root context; all children of that context would therefore see the "application" scope. The following shows how this is done:

    ...
    
    RenderContext root = 
      RenderContextFactory.newInstance();
    root.getEnv().put(
      "someGlobalObject", 
      someGlobalObj, 
      "application"
    );
    
    // menuBarContext will see the "application" scope
    
    RenderContext menuBarContext = 
      root.newChildInstance();
    	
    jFrame.getContentPane().setJMenuBar(
      (JMenuBar)menuBarContext.
        render("foo/bar/myApp/menubar.xml"));
        
    ...

    To add a custom scope implementation to the environment:

    ...
    
    RenderContext root = 
      RenderContextFactory.newInstance();
    root.getEnv()
        .addScope("someCustomScope", someScopeImpl);
    
    ...

    One convenient usage of scopes is to bind arbitrary objects to them from within the XML configuration, or to retrieve objects from them - also from within the config, potentially. Gumby provides the Register and Ref tags to do this, respectively. For example, consider the following snippet:

    ...
    
    RenderContext root =
      RenderContextFactory.newInstance();
    	
    root.getEnv().put("someActionListener", 
                       new someActionListener(), 
                       "listeners");
                       
    // here render and use
    
    ...

    And then:

    ...
            
    <swing:JButton text="Click once more!!!">
      <actionCommand>click</actionCommand>
      <actionListener>
        <gumby:Ref id="someActionListener:listeners" />
      </actionListener>
    </swing:JButton>
    
    ...

    You get it? the Ref tag allows retrieving any object from any arbitrary scope from within the XML configuration. In the above case, we retrieve some global action listener that can thus be reused for different Swing components. The tag expects an ID attribute whose content will have the following format: id:scope. The scope part is optional; if omitted, the environment instance will search through all existing scopes - in the order in which they were added.

    Now, imagine that you want to bind a given Swing widget in order to retrieve it and manipulate it from Java:

    ...
            
    <swing:JButton text="Click once more!!!">
      <actionCommand>click</actionCommand>
      <gumby:Register id="someButton:widgets" />
    </swing:JButton>
    
    ...

    And then:

    ...
    
    RenderContext root = 
      RenderContextFactory.newInstance();
    
    JPanel panel = (JPanel)root
                     .render("some/resource.xml");
    								 
    JButton button = (JPanel)root
                     .get("someButton", "widgets");
    								 
    button.addActionListener(new SomeActionListener());
    
    ...

    As you can see, the Register tag binds the button to the specified scope (from the XML configuration). That button can be retrieved and manipulated from Java, by using the appropriate identifier and scope.

    Mine you, you can put any object you want in a scope. Gumby makes no assumptions as to what type of objects are in scopes. It is all up to you. But very often, what you'll put in a scope are widgets that correspond to a view; that's part of Gumby MVC, which we'll see next.

    MVC

    Model and View

    Gumby offers you a base on which to build UIs that follow the MVC pattern. We have explained scopes in the previous section; there is a special kind of scope implementation in Gumby, which is the View class. The class, in addition to the methods required by the interface it implements, offers other ones that deal with handling models and so-called "bindings". The class' signature is as follows:

    ...
      
    public synchronized void setModel(Object model)
    public synchronized Object getModel();
    public synchronized void addBinding(Binding binding);
    public synchronized void removeBinding(String id);
    public synchronized void fireUpdated(String id);
    public synchronized void fireUpdated();
    public synchronized void fireUpdateModel(String id);
    public synchronized void fireUpdateModel();
      
    ...

    All the methods above have been added for the context of MVC. So concretely, what is a View? A View holds a model (an arbitrary object) and a collection of Bindings. Each binding must provide an identifier that should be unique in the context of a given view. A binding handles synchonization between the model (or parts of the model) and the user interface (i.e.: the widgets that are part of the view) - we will see later on where widgets come into play.

    The model of a view has a set of events that can occur during its lifetime. The framework categorizes these events as followings:

    • The model is first bound (associated) to the view: this occurs when no model is currently bound, and the setModel() method is called on the view with the actual model object being passed in. When that occurs, the view calls the onBound() method on all the bindings it holds. At that point, bindings should initialize the widgets they manage with the corresponding data in the model.
    • The current model is replaced by another instance: this occurs when a model is currently bound, and the setModel() method is called. The view then calls the onChanged() method on all the bindings it holds. Bindings should then update the widgets they manage with the corresponding data in the new model.
    • The current model has its state modified, but not in the context of the view - for example, a background thread could synchronize the model with a database periodically, even as the user is editing it. Of course, it is up to applications to detect such a condition and notify the view accordingly, so that the widgets can be updated to reflect the current state of the model. There are two fireUpdate() methods for this: one that will internally call onUpdated() on all the bindings, and one that will invoke that callback only on the binding whose identifier was given.
    • Another event that can occur is when the user has entered some data in the UI, and then performs an operation that tells the UI that the model should be updated with the data that the widgets hold. This is typical in the case of forms, where an "OK" or "Submit" button is provided. In Swing parlance, an ActionListener would be responsible for detecting the button click and updating the model. In Gumby, the listener would actually call the fireUpdateModel() method. Internally, the method calls updateModel() on all the registered bindings. Bindings are then responsible for updating their part of the model with the data held by the UI widgets they handle. In this scenario, listeners thus delegate the bulk of their work to bindings - which we delve further into in the next section.

    Bindings as Controllers, and the Rest

    As explained in the previous section, Bindings handle synchronizing the state of the model with the state of the UI - or, more precisely, with the state of widgets in the UI. Bindings are thus the "C" in MVC (or at least part of it). On their own, bindings are passive; that is: they can't detect what happens to the model; and they can't detect what happens to the UI. To that end, your Swing listeners and the rest of your application code come into play - your code has to notify the view, as explained in the previous section.

    At this point, what you're probably interested in is how, concretely, all of this works. Here are the main steps - we'll take the example of a data entry form:

    1. You design your form (writing the XML config, running your usual test to adjust your UI).
    2. Next, you have implement your model.
    3. Then you implement your bindings.
    4. In your XML, you make sure that all widgets that will hold data intended for the model are registered with a scope (you use the Register) tag to do so.
    5. Next, you implement the code that will actually relate the whole thing:
    6. You create a RenderContext;
    7. you create a view in the context: you call createView(String) on the context; this method internally registers a View instance with it. In fact to Gumby, remember that views are scopes like any other. So the method actually registers the view as a scope, using the parameter that you pass in as the scope name. Now, this is a very important step: the name of the scope here should correspond to the one under which you have registered your widgets in the XML configuration. This actually how your bindings will be able to retrieve their widgets. The createView() returns a View instance that you use in the next steps.
    8. You register your bindings with the view;
    9. you render the XML;
    10. you create an instance of your model and set it on the view.

    So here's how it looks:

    The XML
    ...
            
    <gumby:TablePanel xmlns:swing="java:swing"
                      xmlns:gumby="gumby:swing">
                      
     <tr>
       <td weight="0.5">
         <swing:Insets top="10" 
                       left="10" 
                       bottom="10" 
                       right="0"/>          
         <swing:JLabel>
           <text>User Account</text>
         </swing:JLabel>
       </td>
     </tr>
     
     <tr>
       <td weight="0.5">
         <swing:Insets top="3" 
                       left="30" 
                       bottom="3" 
                       right="5"/>             
         <swing:JLabel>
           <text>First Name: </text>
         </swing:JLabel> 
       </td>
       <td weight="0.5">
         <swing:Insets top="3" 
                       left="0" 
                       bottom="3" 
                       right="0"/>
       <swing:JTextField columns="15">
       
       <! -- *** Registering widgets with 
                 'forms/UserAccount' scope;
                 that is our view!!! *** -- >
          
           <gumby:Register 
               id="firstName:forms/UserAccount" />   
         </swing:JTextField>       
       </td>   
     </tr>
     
     <tr>
       <td weight="0.5">
         <swing:Insets top="3" 
                       left="30" 
                       bottom="3" 
                       right="5"/>
       <swing:JLabel>
           <text>Last Name: </text>
         </swing:JLabel> 
       </td>
       <td weight="0.5">
         <swing:Insets top="3" 
                       left="0" 
                       bottom="3" 
                       right="0"/>
       <swing:JTextField columns="15">
           <gumby:Register 
               id="lastName:forms/UserAccount" />    
       </swing:JTextField>
       </td>   
     </tr> 
     
     <!-- ========== Bindings ========== -->
     
     <gumby:View scope="forms/UserAccount">
       <binding>
         <gumby:Expr>
           import org.sapia.gumby.view.Binding;
           import org.sapia.gumby.view.View;
    
           new Binding(){       
             public String getId(){
               return "UserAccount";
             }
             public void onBound(View v, 
                                 Object model){
               firstName = v.get("firstName");
               lastName = v.get("lastName");
               firstName.setText(model.getFirstName());
               lastName.setText(model.getLastName());          
             }
             public void onUpdated(View v, 
                                   Object model){
               onBound(v, model);
             }
             public void onChanged(View v, 
                                   Object model){
               onBound(v, model);
             }
             public void updateModel(View v, 
                                     Object model){
               firstName = v.get("firstName");
               lastName = v.get("lastName");
               model.setFirstName(firstName.getText());
               model.setLastName(lastName.getText());          
             }
           };
         </gumby:Expr>
       </binding>
     </gumby:View>
    </gumby:TablePanel>  
    
    ...

    We could have implemented our bindings in Java, register them with the view programmatically, from Java also. But we wanted to demonstrate the use of the Gumby View tag. As can be seen, the tag allows registering bindings with the view straight from the configuration (here, we use Gumby's scripting feature to dynamically create a Binding instance).
    The Model
    package org.foobar;
    
    public class User{
    
      private String firstName, lastName;
      
      public String getFirstName(){
        return firstName;
      }
      
      public void setFirstName(String firstName){
        this.firstName = firstName;
      }
      
      public String getLastName(){
        return lastName;
      }
      
      public void setLastName(String lastName){
        this.lastName = lastName;
      } 
    }
    Putting it together
    ...
    
    public static void main(String[] args) {
      try {
        JFrame frame = new JFrame();
        RenderContext ctx = 
          RenderContextFactory.newInstance();
        ctx.getEnv().put("Frame", frame, "application");
        View userAccountForm = 
          ctx.createView("forms/UserAccount");
        JPanel panel =  
          (JPanel)ctx.render(
            new File("path/to/config.xml"));
        frame.getContentPane()
          .setLayout(new FlowLayout(FlowLayout.LEFT));
        frame.getContentPane().add(panel);
        frame.setSize(400, 400);
        frame.setVisible(true);
        User user = new User();
        user.setFirstName("John");
        userAccountForm.setModel(user);
        
      } catch(Exception e) {
        e.printStackTrace();
      }
    }
      
    ...

    Note that the View class gives you access to the RenderContext; therefore, bindings have access to the environment.

    Databinding with JXPath

    Hard-coding your own bindings for handling JTextFields and other such conventional widgets could be tedious. That's why Gumby comes built in with binding implementations that rely on JXPath. Here's how our example above could be adjusted:

    ...
    
      <gumby:View scope="forms/UserAccount">
         <binding>
           <gumby:BindText id="firstName" 
                           attribute="firstName" />
         </binding>   
         <binding>
           <gumby:BindText id="lastName" 
                           attribute="lastName" />
         </binding>     
      </gumby:View>
      
    ...

    In the above BindText elements, attribute attributes consist of a JXPath expression that should evaluate to a getter of the model (or of a sub-object thereof). The id attribute identifies the widget to which the tag is related.

    There are other reusable bindings that come with Gumby and handle JLists, JComboBoxes, etc. See the tag reference manual for more details.

    Conclusion

    So that's it. For further details, having a look at the examples that come with the source distribution is certainly a good idea. The tag reference manual is also a logical stop.