FileDocCategorySizeDatePackage
Bean.javaAPI DocExample14967Sat Jan 24 10:44:38 GMT 2004je3.beans

Bean.java

/*
 * Copyright (c) 2004 David Flanagan.  All rights reserved.
 * This code is from the book Java Examples in a Nutshell, 3nd Edition.
 * It is provided AS-IS, WITHOUT ANY WARRANTY either expressed or implied.
 * You may study, use, and modify it for any non-commercial purpose,
 * including teaching and use in open-source projects.
 * You may distribute it non-commercially as long as you retain this notice.
 * For a commercial use license, or to purchase the book, 
 * please visit http://www.davidflanagan.com/javaexamples3.
 */
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 to 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 propery 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;
    }
}