Visual programming and Beans

Bruce Eckel’s Thinking in Java Contents | Prev | Next

and
Beans

Inheritance
and polymorphism are essential parts of object-oriented programming, but in the
majority of cases when you’re putting together an application, what you
really want is components that do exactly what you need. You’d like to
drop these parts into your design like the electronic engineer puts together
chips on a circuit board (or even, in the case of Java, onto a Web page). It
seems, too, that there should be some way to accelerate this “modular
assembly” style of programming.

What
is a Bean?

After
the dust settles, then, a
component
is really just a block of code, typically embodied in a class. The key issue is
the ability for the application builder tool to discover the properties and
events for that component. To create a VB component, the programmer had to
write a fairly complicated piece of code following certain conventions to
expose the properties and events. Delphi was a second-generation visual
programming tool and the language was actively designed around visual
programming so it is much easier to create a visual component. However, Java
has brought the creation of visual components to its most advanced state with
Java Beans, because a Bean is just a class. You don’t have to write any
extra code or use special language extensions in order to make something a
Bean. The only thing you need to do, in fact, is slightly modify the way that
you name your methods. It is the method name that tells the application builder
tool whether this is a property, an event, or just an ordinary method.

  1. For
    a property named
    xxx,
    you typically create two methods:
    getXxx( )
    and
    setXxx( ).
    Note that the first letter after get or set is automatically lowercased to
    produce the property name. The type produced by the “get” method is
    the same as the type of the argument to the “set” method. The name
    of the property and the type for the “get” and “set”
    are not related.
  2. For
    a boolean property, you can use the “get” and “set”
    approach above, but you can also use “is” instead of
    “get.”
  3. Ordinary
    methods of the Bean don’t conform to the above naming convention, but
    they’re
    public.
  4. For
    events, you use the “listener” approach. It’s exactly the
    same as you’ve been seeing:
    addFooBarListener(FooBarListener)
    and
    removeFooBarListener(FooBarListener)
    to handle a
    FooBarEvent.
    Most of the time the built-in events and listeners will satisfy your needs, but
    you can also create your own events and listener interfaces.
We
can use these guidelines to create a simple Bean:

//: Frog.java
// A trivial Java Bean
package frogbean;
import java.awt.*;
import java.awt.event.*;
 
class Spots {}
 
public class Frog {
  private int jumps;
  private Color color;
  private Spots spots;
  private boolean jmpr;
  public int getJumps() { return jumps; }
  public void setJumps(int newJumps) {
    jumps = newJumps;
  }
  public Color getColor() { return color; }
  public void setColor(Color newColor) {
    color = newColor;
  }
  public Spots getSpots() { return spots; }
  public void setSpots(Spots newSpots) {
    spots = newSpots;
  }
  public boolean isJumper() { return jmpr; }
  public void setJumper(boolean j) { jmpr = j; }
  public void addActionListener(
      ActionListener l) {
    //...
  }
  public void removeActionListener(
      ActionListener l) {
    // ...
  }
  public void addKeyListener(KeyListener l) {
    // ...
  }
  public void removeKeyListener(KeyListener l) {
    // ...
  }
  // An "ordinary" public method:
  public void croak() {
    System.out.println("Ribbet!");
  }
} ///:~ 

First,
you can see that it’s just a class. Usually, all your fields will be
private,
and accessible only through methods. Following the naming convention, the
properties are
jumps,
color,
spots,
and
jumper
(notice the change in case of the first letter in the property name). Although
the name of the internal identifier is the same as the name of the property in
the first three cases, in
jumper
you can see that the property name does not force you to use any particular
name for internal variables (or, indeed, to even
have
any internal variable for that property).

The
events this Bean handles are
ActionEvent
and
KeyEvent,
based on the naming of the “add” and “remove” methods
for the associated listener. Finally, you can see that the ordinary method
croak( )
is still part of the Bean simply because it’s a
public
method, not because it conforms to any naming scheme.

Extracting
BeanInfo

with
the Introspector

One
of the most critical parts of the Bean scheme occurs when you drag a Bean off a
palette and plop it down on a form. The application builder tool must be able
to create the Bean (which it can do if there’s a default constructor) and
then, without access to the Bean’s source code, extract all the necessary
information to create the property sheet and event handlers.

Usually
you won’t care about any of this – you’ll probably get most
of your Beans off the shelf from vendors, and you don’t need to know all
the magic that’s going on underneath. You’ll simply drag your Beans
onto your form, then configure their properties and write handlers for the
events you’re interested in. However, it’s an interesting and
educational exercise to use the
Introspector
to display information about a Bean, so here’s a tool that does it
(you’ll find it in the
frogbean
subdirectory):

//: BeanDumper.java
// A method to introspect a Bean
import java.beans.*;
import java.lang.reflect.*;
 
public class BeanDumper {
  public static void dump(Class bean){
    BeanInfo bi = null;
    try {
      bi = Introspector.getBeanInfo(
        bean, java.lang.Object.class);
    } catch(IntrospectionException ex) {
      System.out.println("Couldn't introspect " +
        bean.getName());
      System.exit(1);
    }
    PropertyDescriptor[] properties =
      bi.getPropertyDescriptors();
    for(int i = 0; i < properties.length; i++) {
      Class p = properties[i].getPropertyType();
      System.out.println(
        "Property type:n  " + p.getName());
      System.out.println(
        "Property name:n  " +
        properties[i].getName());
      Method readMethod =
        properties[i].getReadMethod();
      if(readMethod != null)
        System.out.println(
          "Read method:n  " +
          readMethod.toString());
      Method writeMethod =
        properties[i].getWriteMethod();
      if(writeMethod != null)
        System.out.println(
          "Write method:n  " +
          writeMethod.toString());
      System.out.println("====================");
    }
    System.out.println("Public methods:");
    MethodDescriptor[] methods =
      bi.getMethodDescriptors();
    for(int i = 0; i < methods.length; i++)
      System.out.println(
        methods[i].getMethod().toString());
    System.out.println("======================");
    System.out.println("Event support:");
    EventSetDescriptor[] events =
      bi.getEventSetDescriptors();
    for(int i = 0; i < events.length; i++) {
      System.out.println("Listener type:n  " +
        events[i].getListenerType().getName());
      Method[] lm =
        events[i].getListenerMethods();
      for(int j = 0; j < lm.length; j++)
        System.out.println(
          "Listener method:n  " +
          lm[j].getName());
      MethodDescriptor[] lmd =
        events[i].getListenerMethodDescriptors();
      for(int j = 0; j < lmd.length; j++)
        System.out.println(
          "Method descriptor:n  " +
          lmd[j].getMethod().toString());
      Method addListener =
        events[i].getAddListenerMethod();
      System.out.println(
          "Add Listener Method:n  " +
        addListener.toString());
      Method removeListener =
        events[i].getRemoveListenerMethod();
      System.out.println(
        "Remove Listener Method:n  " +
        removeListener.toString());
      System.out.println("====================");
    }
  }
  // Dump the class of your choice:
  public static void main(String[] args) {
    if(args.length < 1) {
      System.err.println("usage: n" +
        "BeanDumper fully.qualified.class");
      System.exit(0);
    }
    Class c = null;
    try {
      c = Class.forName(args[0]);
    } catch(ClassNotFoundException ex) {
      System.err.println(
        "Couldn't find " + args[0]);
      System.exit(0);
    }
    dump(c);
  }
} ///:~ 

BeanDumper.dump( )
is the method that does all the work. First it tries to create a
BeanInfo
object, and if successful calls the methods of
BeanInfo
that produce information about properties, methods, and events. In
Introspector.getBeanInfo( ),
you’ll see there is a second argument. This tells the
Introspector
where to stop in the inheritance hierarchy. Here, it stops before it parses all
the methods from
Object,
since we’re not interested in seeing those.

If
you invoke
BeanDumper
on the
Frog
class like this:

java
BeanDumper frogbean.Frog

the
output, after removing extra details that are unnecessary here, is:

class name: Frog
Property type:
  Color
Property name:
  color
Read method:
  public Color getColor()
Write method:
  public void setColor(Color)
====================
Property type:
  Spots
Property name:
  spots
Read method:
  public Spots getSpots()
Write method:
  public void setSpots(Spots)
====================
Property type:
  boolean
Property name:
  jumper
Read method:
  public boolean isJumper()
Write method:
  public void setJumper(boolean)
====================
Property type:
  int
Property name:
  jumps
Read method:
  public int getJumps()
Write method:
  public void setJumps(int)
====================
Public methods:
public void setJumps(int)
public void croak()
public void removeActionListener(ActionListener)
public void addActionListener(ActionListener)
public int getJumps()
public void setColor(Color)
public void setSpots(Spots)
public void setJumper(boolean)
public boolean isJumper()
public void addKeyListener(KeyListener)
public Color getColor()
public void removeKeyListener(KeyListener)
public Spots getSpots()
======================
Event support:
Listener type:
  KeyListener
Listener method:
  keyTyped
Listener method:
  keyPressed
Listener method:
  keyReleased
Method descriptor:
  public void keyTyped(KeyEvent)
Method descriptor:
  public void keyPressed(KeyEvent)
Method descriptor:
  public void keyReleased(KeyEvent)
Add Listener Method:
  public void addKeyListener(KeyListener)
Remove Listener Method:
  public void removeKeyListener(KeyListener)
====================
Listener type:
  ActionListener
Listener method:
  actionPerformed
Method descriptor:
  public void actionPerformed(ActionEvent)
Add Listener Method:
  public void addActionListener(ActionListener)
Remove Listener Method:
  public void removeActionListener(ActionListener)
====================

This
reveals most of what the
Introspector
sees as it produces a
BeanInfo
object from your Bean. You can see that the type of the property and its name
are independent. Notice the lowercasing of the property name. (The only time
this doesn’t occur is when the property name begins with more than one
capital letter in a row.) And remember that the method names you’re
seeing here (such as the read and write methods) are actually produced from a
Method
object that can be used to invoke the associated method on the object.

The
public method list includes the methods that are not associated with a property
or event, such as
croak( ),
as well as those that are. These are all the methods that you can call
programmatically for a Bean, and the application builder tool can choose to
list all of these while you’re making method calls, to ease your task.

Finally,
you can see that the events are fully parsed out into the listener, its
methods, and the add- and remove-listener methods. Basically, once you have the
BeanInfo,
you can find out everything of importance for the Bean. You can also call the
methods for that Bean, even though you don’t have any other information
except the object (again, a feature of reflection).

A
more sophisticated Bean

This
next example is slightly more sophisticated, albeit frivolous. It’s a
canvas that draws a little circle around the mouse whenever the mouse is moved.
When you press the mouse, the word “Bang!” appears in the middle of
the screen, and an action listener is fired.

//: BangBean.java
// A graphical Bean
package bangbean;
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.*;
 
public class BangBean extends Canvas
     implements Serializable {
  protected int xm, ym;
  protected int cSize = 20; // Circle size
  protected String text = "Bang!";
  protected int fontSize = 48;
  protected Color tColor = Color.red;
  protected ActionListener actionListener;
  public BangBean() {
    addMouseListener(new ML());
    addMouseMotionListener(new MML());
  }
  public int getCircleSize() { return cSize; }
  public void setCircleSize(int newSize) {
    cSize = newSize;
  }
  public String getBangText() { return text; }
  public void setBangText(String newText) {
    text = newText;
  }
  public int getFontSize() { return fontSize; }
  public void setFontSize(int newSize) {
    fontSize = newSize;
  }
  public Color getTextColor() { return tColor; }
  public void setTextColor(Color newColor) {
    tColor = newColor;
  }
  public void paint(Graphics g) {
    g.setColor(Color.black);
    g.drawOval(xm - cSize/2, ym - cSize/2,
      cSize, cSize);
  }
  // This is a unicast listener, which is
  // the simplest form of listener management:
  public void addActionListener (
      ActionListener l)
        throws TooManyListenersException {
    if(actionListener != null)
      throw new TooManyListenersException();
    actionListener = l;
  }
  public void removeActionListener(
      ActionListener l) {
    actionListener = null;
  }
  class ML extends MouseAdapter {
    public void mousePressed(MouseEvent e) {
      Graphics g = getGraphics();
      g.setColor(tColor);
      g.setFont(
        new Font(
          "TimesRoman", Font.BOLD, fontSize));
      int width =
        g.getFontMetrics().stringWidth(text);
      g.drawString(text,
        (getSize().width - width) /2,
        getSize().height/2);
      g.dispose();
      // Call the listener's method:
      if(actionListener != null)
        actionListener.actionPerformed(
          new ActionEvent(BangBean.this,
            ActionEvent.ACTION_PERFORMED, null));
    }
  }
  class MML extends MouseMotionAdapter {
    public void mouseMoved(MouseEvent e) {
      xm = e.getX();
      ym = e.getY();
      repaint();
    }
  }
  public Dimension getPreferredSize() {
    return new Dimension(200, 200);
  }
  // Testing the BangBean:
  public static void main(String[] args) {
    BangBean bb = new BangBean();
    try {
      bb.addActionListener(new BBL());
    } catch(TooManyListenersException e) {}
    Frame aFrame = new Frame("BangBean Test");
    aFrame.addWindowListener(
      new WindowAdapter() {
        public void windowClosing(WindowEvent e) {
          System.exit(0);
        }
      });
    aFrame.add(bb, BorderLayout.CENTER);
    aFrame.setSize(300,300);
    aFrame.setVisible(true);
  }
  // During testing, send action information
  // to the console:
  static class BBL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      System.out.println("BangBean action");
    }
  }
} ///:~ 

You
can see that all the fields are
private,
which is what you’ll usually do with a Bean – allow access only
through methods, usually using the “property” scheme.

The
main( )
is added to allow you to test the program from the command line. When a Bean is
in a development environment,
main( )
will not be used, but it’s helpful to have a
main( )
in each of your Beans because it provides for rapid testing.
main( )
creates
a
Frame
and places a
BangBean
within it, attaching a simple
ActionListener
to the
BangBean
to print to the console whenever an
ActionEvent
occurs. Usually, of course, the application builder tool would create most of
the code that uses the Bean.

Packaging
a Bean

Manifest-Version: 1.0
 
Name: bangbean/BangBean.class
Java-Bean: True

The
first line indicates the version of the manifest scheme, which until further
notice from Sun is 1.0. The second line (empty lines are ignored) names the
BangBean.class
file, and the third says, “It’s a Bean.” Without the third
line, the program builder tool will not recognize the class as a Bean.

The
only tricky part is that you must make sure that you get the proper path in the
“Name:” field. If you look back at
BangBean.java,
you’ll see it’s in
package
bangbean
(and
thus in a subdirectory called “bangbean” that’s off of the
classpath), and the name in the manifest file must include this package
information. In addition, you must place the manifest file in the directory
above
the root of your package path, which in this case means placing the file in the
directory above the “bangbean” subdirectory. Then you must invoke
jar
from the same directory as the manifest
file, as follows:

jar
cfm BangBean.jar BangBean.mf bangbean

This
assumes that you want the resulting JAR file to be named
BangBean.jar
and that you’ve put the manifest in a file called
BangBean.mf.

You
might wonder “What about all the other classes that were generated when I
compiled
BangBean.java?”
Well, they all ended up inside the
bangbean
subdirectory, and you’ll see that the last argument for the above
jar
command line is the
bangbean
subdirectory. When you give
jar
the name of a subdirectory, it packages that entire subdirectory into the jar
file (including, in this case, the original
BangBean.java
source-code file – you might not choose to include the source with your
own Beans). In addition, if you turn around and unpack the JAR file
you’ve just created, you’ll discover that your manifest file
isn’t inside, but that
jar
has created its own manifest file (based partly on yours) called
MANIFEST.MF
and
placed it inside the subdirectory
META-INF
(for “meta-information”). If you open this manifest file
you’ll also notice that digital signature information has been added by
jar
for
each file, of the form:

Digest-Algorithms: SHA MD5
SHA-Digest: pDpEAG9NaeCx8aFtqPI4udSX/O0=
MD5-Digest: O4NcS1hE3Smnzlp2hj6qeg==

In
general, you don’t need to worry about any of this, and if you make
changes you can just modify your original manifest file and re-invoke
jar
to create a new JAR file for your Bean. You can also add other Beans to the JAR
file simply by adding their information to your manifest.

One
thing to notice is that you’ll probably want to put each Bean in its own
subdirectory, since when you create a JAR file you hand the
jar
utility the name of a subdirectory and it puts everything in that subdirectory
into the JAR file. You can see that both
Frog
and
BangBean
are in their own subdirectories.

More
complex Bean support

You
can see how remarkably simple it is to make a Bean. But you aren’t
limited to what you’ve seen here. The Java Bean design provides a simple
point of entry but can also scale to more complex situations. These situations
are beyond the scope of this book but they will be briefly introduced here. You
can find more details at
http://java.sun.com/beans.

You
can also change the way your Bean is represented at design time:

  1. You
    can provide a
    custom
    property sheet for your particular Bean. The ordinary property sheet will be
    used for all other Beans, but yours is automatically invoked when your Bean is
    selected.
  2. You
    can create a
    custom
    editor for a particular property, so the ordinary property sheet is used, but
    when your special property is being edited, your editor will automatically be
    invoked.
  3. You
    can provide a
    custom
    BeanInfo
    class for your Bean that produces information that’s different from the
    default created by the
    Introspector.
  4. It’s
    also possible to turn “expert” mode on and off in all
    FeatureDescriptors
    to distinguish between basic features and more complicated ones.

More
to Beans

More by Author

Must Read