Skip to main content

Dreams Beans (Part 1)

Posted by fabriziogiudici on February 1, 2008 at 10:58 AM EST
One of the most hated things in Swing programming is the management of the infamous Event Dispatcher Thread (a.k.a. AWT Thread). The state-of-the-art solutions such as SwingWorker work fine but are quite verbose and cumbersome to implement. Indeed there are much better solutions, at least for most cases, that are just at hand.

What's the "perfect bean" like in my perspective? It's something like this (taken from a real world case, of course):
package it.tidalwave.metadata.iptc.viewer;

import it.tidalwave.metadata.iptc.IPTCContent;
import it.tidalwave.metadata.viewer.MetadataItemPaneSupport;

public class IPTCContentPane extends MetadataItemPaneSupport<IPTCContent> // might be extends JPanel
  {
    public IPTCContentPane()
      {
        super(IPTCContent.class);
        initComponents();
      }
   
    /***************************************************************************
     *
     * This method is called from within the constructor to
     * initialize the form.
     * WARNING: Do NOT modify this code. The content of this method is
     * always regenerated by the Form Editor.
     *
     **************************************************************************/
    // Matisse-generated code hidden                         
  }
That's all; all the hidden parts are chunks of code that have been generated by the Visual Designer of NetBeans and that I can indirectly edit by means of proper wizards.

Now, if you're already complaining about that extends, well you can avoid it if you wish. Indeed my specific requirements ask for polymorphic behaviour of my panels, hence that inheritance, otherwise I could easily make it by means of composition. Details later.

The above panel matches with this class (a bit long, but if you look at it you'll only see getters and setters):
package it.tidalwave.metadata.iptc;

import java.util.ArrayList;
import java.util.Collection;
import java.util.concurrent.CopyOnWriteArrayList;
import java.io.Serializable;

public class IPTCContent
  {
    private String headline;
    private boolean headlineAvailable;
    private String caption;
    private boolean captionAvailable;
    private Collection<String> iptcSubjectCodes;
    private boolean iptcSubjectCodesAvailable;
    private String descriptionWriter;
    private boolean descriptionWriterAvailable;
    private String category;
    private boolean categoryAvailable;
    private Collection<String> otherCategories;
    private boolean otherCategoriesAvailable;

    public String getHeadline()
      {
        return headline;
      }

    public void setHeadline (final String headline)
      {
        this.headline = headline;
      }

    public boolean isHeadlineAvailable()
      {
        return headlineAvailable;
      }

    public void setHeadlineAvailable (final boolean headlineAvailable)
      {
        this.headlineAvailable = headlineAvailable;
      }

    public String getCaption()
      {
        return caption;
      }

    public void setCaption (final String caption)
      {
        this.caption = caption;
      }

    public boolean isCaptionAvailable()
      {
        return captionAvailable;
      }

    public void setCaptionAvailable (final boolean captionAvailable)
      {
        this.captionAvailable = captionAvailable;
      }

    public Collection<String> getIPTCSubjectCodes()
      {
        return (iptcSubjectCodes == null) ? null : new CopyOnWriteArrayList(iptcSubjectCodes);
      }

    public void setIPTCSubjectCodes (final Collection<String> iptcSubjectCodes)
      {
        if (iptcSubjectCodes == null)
          {
            this.iptcSubjectCodes = null;
          }
       
        else
          {
            if (this.iptcSubjectCodes == null)
              {
                this.iptcSubjectCodes = new ArrayList<String>();
              }
           
            this.iptcSubjectCodes.clear();
            this.iptcSubjectCodes.addAll(iptcSubjectCodes);
          }
      }

    public boolean isIPTCSubjectCodesAvailable()
      {
        return iptcSubjectCodesAvailable;
      }

    public void setIPTCSubjectCodesAvailable (final boolean iptcSubjectCodesAvailable)
      {
        this.iptcSubjectCodesAvailable = iptcSubjectCodesAvailable;
      }

    public String getDescriptionWriter()
      {
        return descriptionWriter;
      }

    public void setDescriptionWriter (final String descriptionWriter)
      {
        this.descriptionWriter = descriptionWriter;
      }

    public boolean isDescriptionWriterAvailable()
      {
        return descriptionWriterAvailable;
      }

    public void setDescriptionWriterAvailable (final boolean descriptionWriterAvailable)
      {
        this.descriptionWriterAvailable = descriptionWriterAvailable;
      }

    public String getCategory()
      {
        return category;
      }

    public void setCategory (final String category)
      {
        this.category = category;
      }

    public boolean isCategoryAvailable()
      {
        return categoryAvailable;
      }

    public void setCategoryAvailable (final boolean categoryAvailable)
      {
        this.categoryAvailable = categoryAvailable;

      }

    public Collection<String> getOtherCategories()
      {
        return (otherCategories == null) ? null : new CopyOnWriteArrayList(otherCategories);
      }

    public void setOtherCategories (final Collection<String> otherCategories)
      {
        if (otherCategories == null)
          {
            this.otherCategories = null;
          }
       
        else
          {
            if (this.otherCategories == null)
              {
                this.otherCategories = new ArrayList<String>();
              }
           
            this.otherCategories.clear();
            this.otherCategories.addAll(otherCategories);
          }
      }

    public boolean isOtherCategoriesAvailable()
      {
        return otherCategoriesAvailable;
      }

    public void setOtherCategoriesAvailable (final boolean otherCategoriesAvailable)
      {
        this.otherCategoriesAvailable = otherCategoriesAvailable;
      }

    @Override
    public boolean equals (final Object obj)
      {
         ...
      }

    @Override
    public int hashCode()
      {
         ...
      }
  }

Now you can just call:
IPTCContentPane contentPane = new IPTCContentPane();
// add contentPane to a Swing component

IPTCContent iptcContent1 = contentPane.getBean();
String headline = iptcContent1.getHeadline();
iptcContent1.setCaption("My caption");

// alternatively

IPTCContent iptcContent2 = new IPTCContent();
contentPane.bind(iptcContent2);
Now, every change made in the UI is automatically reflected in the bean properties, and each change in the bean properties is propagated to the pane, properly delivered by means of the Swing EventDispatcherThread. As you can see, this is just looking like plain Java code, no inner classes, no hassles; and the bean is just a POJO, no event listener stuff and whatever.

How did I make it? Well, mostly with existing pieces of software and tools. When I've understood some residual issues, I could even throw away some code of mine and revert to existing software only.

The fully working code from which I took the above examples is available here:
svn co https://bluemarine.dev.java.net/svn/bluemarine/trunk/src/Metadata/IPTC/IPTC --username guest
svn co https://bluemarine.dev.java.net/svn/bluemarine/trunk/src/Metadata/IPTC/I... --username guest
If you want to compile everything, you need to check out the whole Metadata subproject.


Let's see things in details. There are three problems to address:
  1. We need something compliant with JavaBeans (I mean events, listener, whatever) even if we just write a POJO.
  2. We need a quick way to bind the properties in our bean with the components in the UI.
  3. We need a way to deal with the EventDispatcherThread.

Getting JavaBeans behaviour (almost) for free - and handling the EventDispatcherThread stuff


Turning a POJO in a fully compliant JavaBean just requires a few steps, most notably wrapping the construction of the bean:
JavaBeanEnhancer enhancer = new JavaBeanEnhancer();
IPTCContent content = (IPTCContent)enhancer.createEnhancedBean(new IPTCContent(), JavaBeanEnhancer.EDT_COMPLIANT);
The JavaBeanEnhancer dinamically changes the bytecode by means of the CGLIB library (details below). At this point, the bean can be managed as usual, but it also supports this:
PropertyChangeListener listener = new addPropertyChangeListener() { ... };
((JavaBean)content).addPropertyChangeListener(listener);
Of course, I must accept a trade-off: dynamically changing the code can't change the static definitions by default, so the explicit cast is required. Beyond the explicit use of the listeners, you can use your objects with the BeansBindings API (in this case with no need for casting, as BeansBinding works with plain objects and resolves references by introspection).

The details about how JavaBeanEnhancer has been implemented will go in another post, or I won't ever been able to finish this. In any case, you can look at the code by checking out from here:

svn co https://openbluesky.dev.java.net/svn/openbluesky/trunk/src/OpenBlueSky/B... -r 153 --username guest

The code is a refinement of some base code that was provided by pupmonster and jarppe2 as a response to a previous blog post of mine - thank you guys, you saved me a lot of time.

I must point you to the Spin project (thanks to guette for suggesting it): indeed it does most of the things I need, but in my specific case I'm using CGLIB also for other aspects of my problem. Up to now I've found that I can't run CGLIB to enhance a bean that has already been enhanced by CGLIB. If I am able to fix this, I could be using Spin very soon. In the meantime, it might already be good for you.


Binding Properties

I really don't want to repeat stuff about BeansBinding that is already available in tutorials. I'll only shortly repeats which steps I used for my code:
  1. Use Matisse as usual for designing the form.
  2. Open the Inspector and select the "Other Components" element.
  3. Select the menu "Add From Palette / Beans / Choose Bean".
  4. At this point you have to enter the fully qualified name of the bean that you want to bind (in my case it.tidalwave.metadata.iptc.IPTCContent). Please be aware that you must have compiled at least once the bean, or Matisse won't allow you to go on.
  5. Rename the automatically generated name for the bean reference to something that you like (in my case iptcContent).


Matisse has added your bean to the others already declared and initialized by the auto-generated code. Now you have to open the "Customize Code" menu.



As you can see in the picture below, you can now customize the code for instantiating the bean. Instead of a direct instantiation, we must wrap it with the JavaBeanEnhancer stuff.



There are many ways to do this, I suggest wrapping the thing in a method - getBean()) in my case, which is inherited (but an alternate approach based on composition might be used):
public abstract class MetadataItemPaneSupport<Bean> extends JPanel
  {
    private static final JavaBeanEnhancer JAVA_BEAN_ENHANCER = new JavaBeanEnhancer();
    private final Class<Bean> beanClass;
    private final Bean edtBean;
    private final BindingGroup bindingGroup = new BindingGroup();

    protected MetadataItemPaneSupport (final Class<Bean> beanClass)
      {
        this.beanClass = beanClass;   

        try
          {
            edtBean = (Bean)JAVA_BEAN_ENHANCER.createEnhancedBean(beanClass.newInstance(), JavaBeanEnhancer.EDT_COMPLIANT);
          }
        catch (InstantiationException e)
          {
            throw new RuntimeException(e);
          }
        catch (IllegalAccessException e)
          {
            throw new RuntimeException(e);
          }
      }

    public final Bean getBean()
      {
        return edtBean;   
      }

    public void bind (final Bean externalBean)
      throws IllegalArgumentException
      {
         // see below for details
      }
  }

At this point you can just use Matisse to bind each component to the properties of your beans as shown below:



The bind() method is implemented with a combination of the standard Java introspection capabilities and the BeansBindings API:

    private static final List<String> JAVA_BEAN_ASPECT_PROPERTIES = 
Arrays.asList("callback", "callbacks", "class", "propertyChangeListeners", "vetoableChangeListeners");

public void bind (final Bean externalBean)
throws IllegalArgumentException
{
logger.info(String.format("bind(%s)", externalBean));
validateBean(externalBean);

bindingGroup.unbind();

for (final Binding binding : bindingGroup.getBindings())
{
bindingGroup.removeBinding(binding);
}

try
{
final PropertyDescriptor[] descriptors = Introspector.getBeanInfo(externalBean.getClass()).getPropertyDescriptors();

for (final PropertyDescriptor descriptor : descriptors)
{
if (!JAVA_BEAN_ASPECT_PROPERTIES.contains(descriptor.getName()))
{
final Property property = BeanProperty.create(descriptor.getName());
logger.finest(String.format(">>>> binding %s", property));
final Binding binding = Bindings.createAutoBinding(READ_WRITE, externalBean, property, edtBean, property);
bindingGroup.addBinding(binding);
}
}
}
catch (IntrospectionException e)
{
throw new RuntimeException(e);
}

bindingGroup.bind();
}
It basically perform a "global binding" of all the properties available (with the exclusion of some infrastructural stuff): after a call to the method, the two beans are kept in complete synchrony.

If Shannon Hickey and the other BeansBindings guy are around: what about wrapping the above code (cleaned up, of course) into a method such as:

BindingGroup bindingGroup = Bindings.createAutoBindings(READ_WRITE, bean1, bean2);


That's all for today. Next time I'll show you more details about the JavaBeanEnhancer
Related Topics >>

Comments

Yes, you are right. I was looking at Spin in the context of what I needed, not what you were actually using. I apologize.
That said I've been through this kind of situation before. For me it was table models. I don't understand how the second model of spin can actually work in a highly contested environment. Say you have a property that is index dependent. For this example I'll use and index of 5. An event is triggered in a background thread and flows up through the proxy using invokeAndWait. If this event triggers a repaint that repaint will not be called synchronously. Hence the invokeAndWait will return and the beans propertyChanged() will end. This allows for the next change to occur for the background thread. If the next change makes the index 5 invalid and that happens BEFORE the asynchronous paint comes through trying to paint the item at 5 then you get a ArrayOutOfBounds exception or a NullPointerException. Basically your event goes stale before you can process it. Unless there some other magic going on that I don't know about? I would guess that all of your properties fire events that actually deliver the old and new value and do not depend on going back to the bean for any additional information? Like I said I had issues with table models and multiple threads it my not exactly apply to a regular bean.

Not yet the kiss of death. The quoted sentence is about a different scenario than the one I've talked about in this blog post. In fact, the code I've presented only deals about the capability of modifying the bean from any thread, and having the UI correctly called through the EDT (in the Spin document, it's the second sequence diagram). In this case, I don't see problems: even if you call a lot of changes at the same time from concurrent threads, in the end you'll have a queue of AWT events correctly processed in a serial fashion by the EDT. What you're talking about is the reverse behaviour, which Spin supports (the first sequence diagram) but I've not dealt with yet: what's happen when you change the UI. With the code presented so far, I'm just changing the bean in the AWT thread, and I just started some work yesterday on how to deal with "long running" tasks. Spin does some complex things that I don't like and probably don't need (so if and when I'll resort to using it, I'll only use a part of it). My approach is to decouple the two things: while Spin provides a bound solution for the two cases, I'll let the programmer to choose, by wrapping another virtual proxy, in this case around the bean and not the UI. If you read my previous introductory post about binding, one of the things I want to care of is also the transaction management, so it's clear that there's no "one size fits all" thing in this case.

Spin is a lot like Foxtrot, just dynamically generated.

This though got my attention: Swing developers have expressed their concern about Spin and similar techniques, stating that 'Swing is not completely reentrant'.

This is kind of the kiss of death for your cool superbean. It's really a shame I wish there was a real alternative to the asynchronous pattern of interacting with the EDT. Maybe we should be encouraging the Swing team to take on the reentrant problem and make synchronous solutions viable. Its proabably easier said then done.