Skip to main content

Dreams Beans (Part 1)

Posted by fabriziogiudici on February 1, 2008 at 7:58 AM PST

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.


style="font-style: italic;">Now, if you're already
complaining about that style="font-family: monospace;">extends style="font-style: italic;">, 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.


style="font-style: italic;">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
style="font-style: italic;">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 href="http://cglib.sourceforge.net">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 style="font-family: monospace;">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 style="font-family: monospace;">iptcContent).

src="https://bluemarine.dev.java.net/nonav/Blog/20080201/Matisse1.png">



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.



src="https://bluemarine.dev.java.net/nonav/Blog/20080201/Matisse2.png">



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.



src="https://bluemarine.dev.java.net/nonav/Blog/20080201/Matisse3.png">



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:



src="https://bluemarine.dev.java.net/nonav/Blog/20080201/Matisse4.png">



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.


style="font-style: italic;">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 style="font-family: monospace;">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.