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;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.
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
}
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();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.
// 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);
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:
- We need something compliant with JavaBeans (I mean events, listener, whatever) even if we just write a POJO.
- We need a quick way to bind the properties in our bean with the components in the UI.
- 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();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:
IPTCContent content = (IPTCContent)enhancer.createEnhancedBean(new IPTCContent(), JavaBeanEnhancer.EDT_COMPLIANT);
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:
- Use Matisse as usual for designing the form.
- Open the Inspector and select the "Other Components" element.
- Select the menu "Add From Palette / Beans / Choose Bean".
- 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.
- 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 =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.
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();
}
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);
BindingGroup bindingGroup = Bindings.createAutoBindings(READ_WRITE, bean1, bean2);
That's all for today. Next time I'll show you more details about the JavaBeanEnhancer.


