15.9 Manipulating Beans
The ShowBean class of Chapter 11
is a simple beanbox for
displaying and experimenting with individual beans. The
ShowBean code listed in Example 11-30 is concerned primarily with the creation of a
GUI, and the key bean-manipulation methods are handled by a separate
Bean class. Now that we've seen
how to write a bean and its auxiliary classes, we're
ready to tackle this Bean class itself; it is
listed in Example 15-10.An instance of the Bean class represents a single
bean and its associated BeanInfo.
Bean defines methods for querying and setting bean
properties and for querying and invoking bean commands. (It defines a
command as a method with no arguments and no return values.) In some
ways, Bean can be considered a simplified
interface to the BeanInfo class. Note that the
java.beans package does not define any class named
Bean: JavaBeans are not required to implement any
Bean interface or extend any
Bean superclass, so we've
appropriated this class name for our own use here.The Bean class has a public constructor that uses
the
java.beans.Introspector class to obtain
BeanInfo
for the bean object you pass to it. Bean also
defines three static factory methods that you can use to instantiate
the bean object instead of creating it yourself:
forClassName( ) instantiates a named class to create the
bean; fromSerializedStream(
) reads a serialized bean object from a
java.io.ObjectInputStream
(see Chapter 10); and
fromPersistentStream(
) uses the JavaBeans persistence
mechanism to read a bean instance from a stream using
java.beans.XMLDecoder.
XMLDecoder and the corresponding
XMLEncoderExample 11-30) are new in Java 1.4 and are usually a better
choice for saving the persistent state of beans: although the storage
format is XML-based, it is usually more compact than the binary
serialization format, and it is based on the public API of the bean
rather than the private implementation, which is subject to
versioning problems.Pay attention to the ways Bean sets named
properties. The setPropertyValue(
) method is passed a property name and
value as strings. It checks whether the type of the named property is
one that it knows how to convert a string to, and, if so, it converts
the string and sets the property. If it does not know the type of the
property, it attempts to find and use a
PropertyEditor for that type, but this does not
work for editors that implement getCustomEditor( )
instead of setAsText( ).ShowBean uses setPropertyValue(
) to set property values specified on the command line. It
does not use this method to set properties from its
Properties menu, however; in this case,
ShowBean calls getPropertyEditor(
). getPropertyEditor( ) does not return
a PropertyEditor object directlyas we noted
when implementing property editors, the
PropertyEditor interface is confusing and hard to
work with. Instead, getPropertyEditor( ) looks for
a PropertyEditor for the named property and, if it
finds one, returns a Component that is hooked up
to the PropertyEditor. The
Component is suitable for display in a dialog box
or panel: when the user interacts with the returned component, the
component interacts with the PropertyEditor to set
the property.
Example 15-10. Bean.java
package je3.beans;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;
import java.beans.*;
import java.lang.reflect.*;
import java.util.*;
import java.util.List;
// explicit import to disambiguate from java.awt.List
import java.io.*;
/**
* This class encapsulates a bean object and its BeanInfo.
* It is a key part of the ShowBean "beanbox" program,
and demonstrates
* how to instantiate and instrospect beans,
and how to use reflection to
* set properties and invoke methods.
It also illustrates how to work with
* PropertyEditor classes.
*/
public class Bean {
Object bean; // The bean object we encapsulate
BeanInfo info; // Information about beans of that type
Map properties; // Map property names to PropertyDescriptor objects
Map commands; // Map command names to MethodDescriptor objects
boolean expert; // Whether to include "expert" properties and commands
// Utility object used when invoking no-arg methods
static final Object[ ] NOARGS = new Object[0];
// This constructor introspects the specified component.
// Typically you'll use one of the static factory methods instead.
public Bean(Object bean, boolean expert) throws IntrospectionException {
this.bean = bean; // The object to instrospect
this.expert = expert; // Is the end-user an expert?
// Introspect to get BeanInfo for the bean
info = Introspector.getBeanInfo(bean.getClass( ));
// Now create a map of property names to PropertyDescriptor objects
properties = new HashMap( );
PropertyDescriptor[ ] props = info.getPropertyDescriptors( );
for(int i = 0; i < props.length; i++) {
// Skip hidden properties, indexed properties, and expert
// properties unless the end-user is an expert.
if (props[i].isHidden( )) continue;
if (props[i] instanceof IndexedPropertyDescriptor) continue;
if (!expert && props[i].isExpert( )) continue;
properties.put(props[i].getDisplayName( ), props[i]);
}
// Create a map of command names to MethodDescriptor objects
// Commands are methods with no arguments and no return value.
// We skip commands defined in Object, Component, Container, and
// JComponent because they contain methods that meet this definition
// but are not intended for end-users.
commands = new HashMap( );
MethodDescriptor[ ] methods = info.getMethodDescriptors( );
for(int i = 0; i < methods.length; i++) {
// Skip it if it is hidden or expert (unless user is expert)
if (methods[i].isHidden( )) continue;
if (!expert && methods[i].isExpert( )) continue;
Method m = methods[i].getMethod( );
// Skip it if it has arguments or a return value
if (m.getParameterTypes( ).length > 0) continue;
if (m.getReturnType( ) != Void.TYPE) continue;
// Check the declaring class and skip useless superclasses
Class c = m.getDeclaringClass( );
if (c==JComponent.class || c==Component.class ||
c==Container.class || c==Object.class) continue;
// Get the unqualifed classname to prefix method name with
String classname = c.getName( );
classname = classname.substring(classname.lastIndexOf('.')+1);
// Otherwise, this is a valid command, so add it to the list
commands.put(classname + "." + m.getName( ), methods[i]);
}
}
// Factory method to instantiate a bean from a named class
public static Bean forClassName(String className, boolean expert)
throws ClassNotFoundException, InstantiationException,
IllegalAccessException, IntrospectionException
{
// Load the named bean class
Class c = Class.forName(className);
// Instantiate it to create the component instance
Object bean = c.newInstance( );
return new Bean(bean, expert);
}
// Factory method to read a serialized bean
public static Bean fromSerializedStream(ObjectInputStream in,
boolean expert)
throws IOException, ClassNotFoundException, IntrospectionException
{
return new Bean(in.readObject( ), expert);
}
// Factory method to read a persistent XMLEncoded bean from a stream.
public static Bean fromPersistentStream(InputStream in, boolean expert)
throws IntrospectionException
{
return new Bean(new XMLDecoder(in).readObject( ), expert);
}
// Return the bean object itself.
public Object getBean( ) { return bean; }
// Return the name of the bean
public String getDisplayName( ) {
return info.getBeanDescriptor( ).getDisplayName( );
}
// Return an icon for the bean
public Image getIcon( ) {
Image icon = info.getIcon(BeanInfo.ICON_COLOR_32x32);
if (icon != null) return icon;
else return info.getIcon(BeanInfo.ICON_COLOR_16x16);
}
// Return a short description for the bean
public String getShortDescription( ) {
return info.getBeanDescriptor( ).getShortDescription( );
}
// Return an alphabetized list of property names for the bean
// Note the elegant use of the Collections Framework
public List getPropertyNames( ) {
// Make a List from a Set (from a Map), and sort it before returning.
List names = new ArrayList(properties.keySet( ));
Collections.sort(names);
return names;
}
// Return an alphabetized list of command names for the bean.
public List getCommandNames( ) {
List names = new ArrayList(commands.keySet( ));
Collections.sort(names);
return names;
}
// Get a description of a property; useful for tooltips
public String getPropertyDescription(String name) {
PropertyDescriptor p = (PropertyDescriptor) properties.get(name);
if (p == null) throw new IllegalArgumentException(name);
return p.getShortDescription( );
}
// Get a description of a command; useful for tooltips
public String getCommandDescription(String name) {
MethodDescriptor m = (MethodDescriptor) commands.get(name);
if (m == null) throw new IllegalArgumentException(name);
return m.getShortDescription( );
}
// Return true if the named property is read-only
public boolean isReadOnly(String name) {
PropertyDescriptor p = (PropertyDescriptor) properties.get(name);
if (p == null) throw new IllegalArgumentException(name);
return p.getWriteMethod( ) == null;
}
// Invoke the named (no-arg) method of the bean
public void invokeCommand(String name)
throws IllegalAccessException, InvocationTargetException
{
MethodDescriptor method = (MethodDescriptor) commands.get(name);
if (method == null) throw new IllegalArgumentException(name);
Method m = method.getMethod( );
m.invoke(bean, NOARGS);
}
// Return the value of the named property as a string
// This method relies on the toString( ) method of the returned value.
// A more robust implementation might use a PropertyEditor.
public String getPropertyValue(String name)
throws IllegalAccessException, InvocationTargetException
{
PropertyDescriptor p = (PropertyDescriptor) properties.get(name);
if (p == null) throw new IllegalArgumentException(name);
Method m = p.getReadMethod( ); // property accessor method
Object value = m.invoke(bean, NOARGS); // invoke it to get value
if (value == null) return "null";
return value.toString( ); // use the toString method( )
}
// Set the named property to the named value, if possible.
// This method knows how to convert a handful of well-known types. It
// attempts to use a PropertyEditor for types it does not know about but
// this only works for editors that have working setAsText( ) methods.
public void setPropertyValue(String name, String value)
throws IllegalAccessException, InvocationTargetException
{
// Get the descriptor for the named property
PropertyDescriptor p = (PropertyDescriptor) properties.get(name);
if (p == null || isReadOnly(name)) // Make sure we can set it
throw new IllegalArgumentException(name);
Object v; // Store the converted string value here.
Class type = p.getPropertyType( );
// Convert common types in well-known ways
if (type == String.class) v = value;
else if (type == boolean.class) v = Boolean.valueOf(value);
else if (type == byte.class) v = Byte.valueOf(value);
else if (type == char.class) v = new Character(value.charAt(0));
else if (type == short.class) v = Short.valueOf(value);
else if (type == int.class) v = Integer.valueOf(value);
else if (type == long.class) v = Long.valueOf(value);
else if (type == float.class) v = Float.valueOf(value);
else if (type == double.class) v = Double.valueOf(value);
else if (type == Color.class) v = Color.decode(value);
else if (type == Font.class) v = Font.decode(value);
else {
// Try to find a property editor for unknown types
PropertyEditor editor = PropertyEditorManager.findEditor(type);
if (editor != null) {
editor.setAsText(value);
v = editor.getValue( );
}
// Otherwise, give up.
else throw new UnsupportedOperationException("Can't set " +
"properties of type "+
type.getName( ));
}
// Now get the Method object for the property setter method and
// invoke it on the bean object, passing the converted value.
Method setter = p.getWriteMethod( );
setter.invoke(bean, new Object[ ] { v });
}
// Return a component that allows the user to edit the property value.
// The component is live and changes the property value in real time;
// there is no need to call setPropertyValue( ).
public Component getPropertyEditor(final String name)
throws IllegalAccessException, InvocationTargetException,
InstantiationException
{
// Get the descriptor for the named property; final for inner classes.
final PropertyDescriptor p = (PropertyDescriptor) properties.get(name);
if (p == null || isReadOnly(name)) // Make sure we can edit it.
throw new IllegalArgumentException(name);
// Find a PropertyEditor for the property
final PropertyEditor editor; // final for inner class use
if (p.getPropertyEditorClass( ) != null) {
// If there is a custom editor for this property, instantiate one.
editor = (PropertyEditor)p.getPropertyEditorClass( ).newInstance( );
}
else {
// Otherwise, look up an editor based on the property type
Class type = p.getPropertyType( );
editor = PropertyEditorManager.findEditor(type);
// If there is no editor, give up
if (editor == null)
throw new UnsupportedOperationException("Can't set " +
"properties of type " +
type.getName( ));
}
// Get the property accessor methods for this property so we can
// query the initial value and set the edited value
final Method getter = p.getReadMethod( );
final Method setter = p.getWriteMethod( );
// Use Java reflection to find the current property value. Then tell
// the property editor about it.
Object currentValue = getter.invoke(bean, NOARGS);
editor.setValue(currentValue);
// If the PropertyEditor has a custom editor, then we'll just return
// that custom editor component from this method. User changes to the
// component change the value in the PropertyEditor, which generates
// a PropertyChangeEvent. We register a listener so that these changes
// set the property on the bean as well.
if (editor.supportsCustomEditor( )) {
final Component editComponent = editor.getCustomEditor( );
// Note that we register the listener on the PropertyEditor, not
// on its custom editor Component.
editor.addPropertyChangeListener(new PropertyChangeListener( ) {
public void propertyChange(PropertyChangeEvent e) {
try {
// Pass edited value to property setter
Object editedValue = editor.getValue( );
setter.invoke(bean, new Object[ ] { editedValue});
}
catch(Exception ex) {
JOptionPane.showMessageDialog(editComponent,
ex, ex.getClass( ).getName( ),
JOptionPane.ERROR_MESSAGE);
}
}
});
return editComponent;
}
// Otherwise, if the PropertyEditor is for an enumerated type based
// on a fixed list of possible values, then return a JComboBox
// component that allows the user to select one of the values.
final String[ ] tags = editor.getTags( );
if (tags != null) {
// Create the component
final JComboBox combobox = new JComboBox(tags);
// Use the current value of the property as the currently selected
// item in the combo box.
combobox.setSelectedItem(editor.getAsText( ));
// Add a listener to hook the combo box up to the property. When
// the user selects an item, set the property value.
combobox.addItemListener(new ItemListener( ) {
public void itemStateChanged(ItemEvent e) {
// Ignore deselect events
if (e.getStateChange( ) == ItemEvent.DESELECTED) return;
try {
// Get the user's selected string from combo box
String selectedTag =
(String)combobox.getSelectedItem( );
// Tell the editor about this string value
editor.setAsText(selectedTag);
// Ask the editor to convert to the property type
Object editedValue = editor.getValue( );
// Pass this value to the property setter method
setter.invoke(bean, new Object[ ] { editedValue });
}
catch(Exception ex) {
JOptionPane.showMessageDialog(combobox,
ex, ex.getClass( ).getName( ),
JOptionPane.ERROR_MESSAGE);
}
}
});
return combobox;
}
// Otherwise, property type is not enumerated, and we use a JTextField
// to allow the user to enter arbitrary text for conversion by the
// setAsText( ) method of the PropertyEditor
final JTextField textfield = new JTextField( );
// Display the current value of the property in the field
textfield.setText(editor.getAsText( ));
// Hook the JTextField up to the PropertyEditor.
textfield.addActionListener(new ActionListener( ) {
// This is called when the user strikes the Enter key
public void actionPerformed(ActionEvent e) {
try {
// Get the user's input from the text field
String newText = textfield.getText( );
// Tell the editor about it
editor.setAsText(newText);
// Ask the editor to convert to the property type
Object editedValue = editor.getValue( );
// Pass this value to the property setter method
setter.invoke(bean, new Object[ ] { editedValue });
}
catch(Exception ex) {
JOptionPane.showMessageDialog(textfield,
ex, ex.getClass( ).getName( ),
JOptionPane.ERROR_MESSAGE);
}
}
});
return textfield;
}
}