/*
* Sun Public License Notice
*
* The contents of this file are subject to the Sun Public License
* Version 1.0 (the "License"). You may not use this file except in
* compliance with the License. A copy of the License is available at
* http://www.sun.com/
*
* The Original Code is NetBeans. The Initial Developer of the Original
* Code is Sun Microsystems, Inc. Portions Copyright 1997-2003 Sun
* Microsystems, Inc. All Rights Reserved.
*/
package org.openide.nodes;
import java.awt.Image;
import java.beans.*;
import java.beans.beancontext.BeanContext;
import java.beans.beancontext.BeanContextChild;
import java.beans.beancontext.BeanContextProxy;
import java.io.IOException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Enumeration;
import org.openide.ErrorManager;
import org.openide.util.HelpCtx;
import org.openide.util.Utilities;
import org.openide.util.WeakListener;
import org.openide.util.actions.SystemAction;
/** Represents one JavaBean in the nodes hierarchy.
* It provides all methods that are needed for communication between
* the IDE and the bean.
* <p>You may use this node type for an already-existing JavaBean (possibly
* using BeanContext) in order for its JavaBean properties to be reflected
* as corresponding node properties. Thus, it serves as a compatibility wrapper.
*
* @author Jan Jancura, Ian Formanek, Jaroslav Tulach
*/
public class BeanNode extends AbstractNode {
// static ..................................................................................................................
/** Icon base for bean nodes */
private static final String ICON_BASE = "org/openide/resources/beans"; // NOI18N
private static Children getChildren (Object bean) {
if (bean instanceof BeanContext)
return new BeanChildren ((BeanContext)bean);
if (bean instanceof BeanContextProxy) {
BeanContextChild bch = ((BeanContextProxy)bean).getBeanContextProxy();
if (bch instanceof BeanContext)
return new BeanChildren ((BeanContext)bch);
}
return Children.LEAF;
}
// variables .............................................................................................................
/** bean */
private Object bean;
/** bean info for the bean */
private BeanInfo beanInfo;
/** functions to operate on beans */
private Method nameGetter = null;
private Method nameSetter = null;
/** remove PropertyChangeListener method */
private Method removePCLMethod = null;
/** listener for properties */
private PropL propertyChangeListener = null;
/** is synchronization of name in progress */
private boolean synchronizeName;
// init ..................................................................................................................
/**
* Constructs a node for a JavaBean. If the bean is a {@link BeanContext},
* creates a child list as well.
*
* @param bean the bean this node will be based on
* @throws IntrospectionException if the bean cannot be analyzed
*/
public BeanNode (Object bean) throws IntrospectionException {
this (
bean,
getChildren (bean)
);
}
/** Constructs a node for a JavaBean with a defined child list.
* Intended for use by subclasses with different strategies for computing the children.
* @param bean the bean this node will be based on
* @param children children for the node (default if null)
* @throws IntrospectionException if the bean cannot be analyzed
*/
protected BeanNode (Object bean, Children children) throws IntrospectionException {
super (children == null ? getChildren(bean) : children);
this.bean = bean;
try {
initialization ();
} catch (IntrospectionException ie) {
throw ie;
} catch (RuntimeException re) {
throw mkie(re);
} catch (LinkageError le) {
throw mkie(le);
}
}
private static IntrospectionException mkie(Throwable t) {
IntrospectionException ie = new IntrospectionException(t.toString());
ErrorManager.getDefault().annotate(ie, t);
return ie;
}
/** Set whether or not to keep the node name and Bean name synchronized automatically.
* If enabled, the node will listen to changes in the name of the bean
* and update the (system) name of the node appropriately. The name of the bean can
* be obtained by calling <code>getName ()</code>, <code>getDisplayName ()</code> or from {@link BeanDescriptor#getDisplayName}.
* <p>Also when the (system) name of the node is changing, the change propagates if possible to
* methods <code>setName (String)</code> or <code>setDisplayName (String)</code>. (This
* does not apply to setting the display name of the node, however.)
* <P>
* By default this feature is turned on.
*
* @param watch <code>true</code> if the name of the node should be synchronized with
* the name of the bean, <code>false</code> if the name of the node should be independent
* or manually updated
*
*/
protected void setSynchronizeName (boolean watch) {
synchronizeName = watch;
}
/** Provides access to the bean represented by this BeanNode.
* @return instance of the bean represented by this BeanNode
*/
protected Object getBean () {
return bean;
}
/** Detaches all listeners from the bean and destroys it.
* @throws IOException if there was a problem
*/
public void destroy () throws IOException {
if (removePCLMethod != null) {
try {
Object o = Beans.getInstanceOf (bean, removePCLMethod.getDeclaringClass ());
removePCLMethod.invoke (o, new Object[] {propertyChangeListener});
} catch (Exception e) {
NodeOp.exception (e);
}
}
super.destroy ();
}
/** Can this node be removed?
* @return <CODE>true</CODE> in this implementation
*/
public boolean canDestroy () {
return true;
}
/** Set the node name.
* Also may attempt to change the name of the bean,
* according to {@link #setSynchronizeName}.
* @param s the new name
*/
public void setName (String s) {
if (synchronizeName) {
Method m = nameSetter;
if (m != null) {
try {
m.invoke (bean, new Object[] {s});
} catch (Exception e) {
NodeOp.exception (e);
}
}
}
super.setName (s);
}
/** Can this node be renamed?
* @return <code>true</code> if there is no name synchronization, or there is
* a valid setter method for the name
*/
public boolean canRename () {
return ! synchronizeName || nameSetter != null;
}
/** Get an icon for this node in the closed state.
* Uses the Bean's icon if possible.
*
* @param type constant from {@link java.beans.BeanInfo}
* @return icon to use
*/
public Image getIcon (int type) {
Image image = beanInfo.getIcon (type);
if (image != null) return image;
return super.getIcon(type);
}
/** Get an icon for this node in the open state.
*
* @param type type constants
* @return icon to use. The default implementation just uses {@link #getIcon}.
*/
public Image getOpenedIcon (int type) {
return getIcon(type);
}
public HelpCtx getHelpCtx () {
HelpCtx test = TMUtil.findHelp (this);
if (test != null)
return test;
else
return new HelpCtx (BeanNode.class);
}
/** Prepare node properties based on the bean, storing them into the current property sheet.
* Called when the bean info is ready.
* This implementation always creates a set for standard properties
* and may create a set for expert ones if there are any.
* @see #computeProperties
* @param bean bean to compute properties for
* @param info information about the bean
*/
protected void createProperties (Object bean, BeanInfo info) {
Descriptor d = computeProperties (bean, beanInfo);
Sheet sets = getSheet ();
Sheet.Set pset = Sheet.createPropertiesSet ();
pset.put (d.property);
BeanDescriptor bd = info.getBeanDescriptor();
if ( bd != null && bd.getValue( "propertiesHelpID" ) != null ) { // NOI18N
pset.setValue( "helpID", bd.getValue( "propertiesHelpID" ) ); // NOI18N
}
sets.put (pset);
if (d.expert.length != 0) {
Sheet.Set eset = Sheet.createExpertSet ();
eset.put (d.expert);
if ( bd != null && bd.getValue( "expertHelpID" ) != null ) { // NOI18N
eset.setValue( "helpID", bd.getValue( "expertHelpID" ) ); // NOI18N
}
sets.put (eset);
}
}
/** Can this node be copied?
* @return <code>true</code> in the default implementation
*/
public boolean canCopy () {
return true;
}
/** Can this node be cut?
* @return <code>false</code> in the default implementation
*/
public boolean canCut () {
return false;
}
/* Getter for set of actions that should be present in the
* popup menu of this node. This set is used in construction of
* menu returned from getContextMenu and specially when a menu for
* more nodes is constructed.
*
* @return array of system actions that should be in popup menu
*/
protected SystemAction[] createActions () {
return NodeOp.createFromNames (new String[] {
"CustomizeBean", null, "Copy", null, "Tools", "Properties" // NOI18N
});
}
/* Test if there is a customizer for this node. If <CODE>true</CODE>
* the customizer can be obtained via <CODE>getCustomizer</CODE> method.
*
* @return <CODE>true</CODE> if there is a customizer.
*/
public boolean hasCustomizer () {
// true if we have already computed beanInfo and it has customizer class
return beanInfo.getBeanDescriptor ().getCustomizerClass () != null;
}
/* Returns the customizer component.
* @return the component or <CODE>null</CODE> if there is no customizer
*/
public java.awt.Component getCustomizer () {
Class clazz = beanInfo.getBeanDescriptor ().getCustomizerClass ();
if (clazz == null) return null;
Object o;
try {
o = clazz.newInstance ();
} catch (InstantiationException e) {
NodeOp.exception (e);
return null;
} catch (IllegalAccessException e) {
NodeOp.exception (e);
return null;
}
if (! (o instanceof Customizer) ) {
// no customizer => no fun
// [PENDING] this ought to perform some sort of notification!
return null;
}
Customizer cust = ((java.beans.Customizer)o);
TMUtil.attachCustomizer (this, cust);
// looking for the component
java.awt.Component comp = null;
if (o instanceof java.awt.Component) {
comp = (java.awt.Component)o;
} else {
// create the dialog from descriptor
comp = TMUtil.createDialog (o);
}
if (comp == null) {
// no component provided
return null;
}
cust.setObject (bean);
if (removePCLMethod == null) {
cust.addPropertyChangeListener (
new PropertyChangeListener () {
public void propertyChange(PropertyChangeEvent e) {
firePropertyChange (
e.getPropertyName (), e.getOldValue (), e.getNewValue ()
);
}
});
}
return comp;
}
/** Computes a descriptor for properties from a bean info.
* <p>Property code names are taken from the property descriptor names
* according to the JavaBeans specification. For example, a pair of
* methods <code>getFoo</code> and <code>setFoo</code> would result in
* a node property with code name <code>foo</code>. If you call
* <code>MyBean.setFoo(...)</code>, this should result in a property
* change event with name <code>foo</code>; if you are using these
* properties in some other context (attached to something other than
* a <code>BeanNode</code>) then be careful to fire changes with the correct
* name, or there may be problems with refreshing display of the property etc.
* @param bean bean to create properties for
* @param info about the bean
* @return three property lists
*/
public static Descriptor computeProperties (Object bean, BeanInfo info) {
ArrayList property = new ArrayList ();
ArrayList expert = new ArrayList ();
ArrayList hidden = new ArrayList ();
PropertyDescriptor[] propertyDescriptor = info.getPropertyDescriptors ();
int k = propertyDescriptor.length;
for (int i = 0; i < k; i ++) {
if (propertyDescriptor[i].getPropertyType() == null)
continue;
Node.Property prop;
if (propertyDescriptor[i] instanceof IndexedPropertyDescriptor) {
IndexedPropertyDescriptor p = (IndexedPropertyDescriptor) propertyDescriptor [i];
if ((p.getReadMethod() != null) && (!p.getReadMethod().getReturnType().isArray())) {
// this is fix for #17728. This situation should never happen
// But if the BeanInfo (IndexedPropertyDescriptor) is wrong
// we will ignore this property
continue;
}
IndexedPropertySupport support = new IndexedPropertySupport (
bean, p.getPropertyType (),
p.getIndexedPropertyType(), p.getReadMethod (), p.getWriteMethod (),
p.getIndexedReadMethod (), p.getIndexedWriteMethod ()
);
support.setName (p.getName ());
support.setDisplayName (p.getDisplayName ());
support.setShortDescription (p.getShortDescription ());
for (Enumeration e = p.attributeNames(); e.hasMoreElements();) {
String aname = (String)e.nextElement();
support.setValue(aname, p.getValue(aname));
}
prop = support;
} else {
PropertyDescriptor p = propertyDescriptor [i];
// Note that PS.R sets the method accessible even if it is e.g.
// defined as public in a package-accessible superclass.
PropertySupport.Reflection support = new PropertySupport.Reflection (
bean, p.getPropertyType (),
p.getReadMethod (), p.getWriteMethod ()
);
support.setName (p.getName ());
support.setDisplayName (p.getDisplayName ());
support.setShortDescription (p.getShortDescription ());
support.setPropertyEditorClass (p.getPropertyEditorClass ());
for (Enumeration e = p.attributeNames(); e.hasMoreElements();) {
String aname = (String)e.nextElement();
support.setValue(aname, p.getValue(aname));
}
prop = support;
}
// Propagate helpID's.
Object help = propertyDescriptor[i].getValue ("helpID"); // NOI18N
if (help != null && (help instanceof String)) {
prop.setValue ("helpID", help); // NOI18N
}
// Add to right category.
if (propertyDescriptor[i].isHidden ()) {
// hidden property
hidden.add (prop);
} else {
if (propertyDescriptor[i].isExpert ()) {
expert.add (prop);
} else {
property.add (prop);
}
}
}// for
return new Descriptor (property, expert, hidden);
}
//
//
// Initialization methods
//
//
/** Performs initalization of the node
*/
private void initialization () throws IntrospectionException {
setIconBase (ICON_BASE);
// default action is org.openide.actions.PropertiesAction
SystemAction[] arr = NodeOp.createFromNames (new String[] { "Properties" }); // NOI18N
if (arr.length != 0) {
setDefaultAction (arr[0]);
}
setSynchronizeName (true);
// Find the first public superclass of the actual class.
// Should not introspect on a private class, because then the method objects
// used for the property descriptors will not be callable without an
// IllegalAccessException, even if overriding a public method from a public superclass.
Class clazz = bean.getClass ();
while (! Modifier.isPublic (clazz.getModifiers ()) && !hasExplicitBeanInfo (clazz)) {
clazz = clazz.getSuperclass ();
if (clazz == null) clazz = Object.class; // in case it was an interface
}
beanInfo = Utilities.getBeanInfo (clazz);
// resolving the name of this bean
registerName ();
setNameSilently (getNameForBean ());
BeanDescriptor descriptor = beanInfo.getBeanDescriptor ();
String sd = descriptor.getShortDescription ();
if (! Utilities.compareObjects (sd, descriptor.getDisplayName ()))
setShortDescription (sd);
// add propertyChangeListener
EventSetDescriptor[] eventSetDescriptors = beanInfo.getEventSetDescriptors();
int i, k = eventSetDescriptors.length;
Method method = null;
for (i = 0; i < k; i++) {
method = eventSetDescriptors [i].getAddListenerMethod ();
if (method != null &&
method.getName().equals("addPropertyChangeListener") && // NOI18N
// Possible for a public class to extend a package-private class,
// where the private class defines addPropertyChangeListener, in which
// case the introspector lists an inaccessible method in the event
// set descriptor. In such a case, do not try to add a listener.
Modifier.isPublic(method.getModifiers())) {
break;
}
}
if (i != k) {
try {
Object o = Beans.getInstanceOf (bean, method.getDeclaringClass ());
propertyChangeListener = new PropL ();
method.invoke (o, new Object[] { WeakListener.propertyChange (propertyChangeListener, o) });
removePCLMethod = eventSetDescriptors [i].getRemoveListenerMethod ();
} catch (Exception e) {
// Warning, not info: likely to call e.g. getters or other things used
// during startup of the bean, so it is not good to swallow errors here
// (e.g. SharedClassObject.initialize throws RuntimeException -> it is
// caught here and probably someone wants to know).
ErrorManager.getDefault().annotate(e, ErrorManager.UNKNOWN, "Trying to invoke " + method + " where introspected class is " + clazz.getName(), null, null, null); // NOI18N
NodeOp.warning (e);
}
}
createProperties (bean, beanInfo);
for (Enumeration e = beanInfo.getBeanDescriptor().attributeNames(); e.hasMoreElements();) {
String aname = (String)e.nextElement();
setValue(aname, beanInfo.getBeanDescriptor().getValue(aname));
}
Node.Cookie instanceCookie = TMUtil.createInstanceCookie (bean);
if (instanceCookie != null) {
getCookieSet ().add (instanceCookie);
}
}
/** Checks whether there is an explicit bean info for given class.
* @param clazz the class to test
* @return true if explicit bean info exists
*/
private boolean hasExplicitBeanInfo (Class clazz) {
String className = clazz.getName ();
int indx = className.lastIndexOf('.');
className = className.substring (indx + 1);
String[] paths = Introspector.getBeanInfoSearchPath();
for (int i = 0; i < paths.length; i++) {
String s = paths[i] + '.' + className + "BeanInfo"; // NOI18N
try {
// test if such class exists
Class.forName (s);
return true;
} catch (ClassNotFoundException ex) {
// OK, this is normal.
}
}
return false;
}
// name resolving methods
/**
* Finds setter and getter methods for the name of the bean. Resisters listener
* for changing of name.
*/
private void registerName () {
// [PENDING] ought to use introspection, rather than look up the methods by name --jglick
Class clazz = bean.getClass ();
// Do not want to use getName, even if public, on a private class:
while (! Modifier.isPublic (clazz.getModifiers ())) {
clazz = clazz.getSuperclass ();
if (clazz == null) clazz = Object.class;
}
Class[] param = new Class [0];
// find getter for the name
try {
try {
nameGetter = clazz.getMethod ("getName", param); // NOI18N
if (nameGetter.getReturnType () != String.class) throw new NoSuchMethodException ();
} catch (NoSuchMethodException e) {
try {
nameGetter = clazz.getMethod ("getDisplayName", param); // NOI18N
if (nameGetter.getReturnType () != String.class) throw new NoSuchMethodException ();
} catch (NoSuchMethodException ee) {
nameGetter = null;
return;
}
}
} catch (SecurityException se) {
NodeOp.exception (se);
nameGetter = null;
return;
}
// this code tests wheter everything is fine and the getter is
// invokable
try {
// make sure this is cast to String too:
String result = (String) nameGetter.invoke (bean, null);
} catch (Exception e) {
ErrorManager em = ErrorManager.getDefault ();
em.annotate (e, ErrorManager.WARNING,
"Bad method: " + clazz.getName () + "." + nameGetter.getName (), //NOI18N
null, null, null);
em.notify ( ErrorManager.WARNING, e);
nameGetter = null;
return;
}
// find the setter for the name
param = new Class[] {String.class};
try {
try {
// tries to find method setName (String)
nameSetter = clazz.getMethod ("setName", param); // NOI18N
if (nameSetter.getReturnType () != Void.TYPE) throw new NoSuchMethodException ();
} catch (NoSuchMethodException e) {
try {
nameSetter = clazz.getMethod ("setDisplayName", param); // NOI18N
if (nameSetter.getReturnType () != Void.TYPE) throw new NoSuchMethodException ();
} catch (NoSuchMethodException ee) {
nameSetter = null;
}
}
} catch (SecurityException se) {
NodeOp.exception (se);
}
}
/**
* Returns name of the bean.
*/
private String getNameForBean () {
if (nameGetter != null) {
try {
String name = (String) nameGetter.invoke (bean, null);
return name != null ? name : ""; // NOI18N
} catch (Exception ex) {
NodeOp.warning (ex);
}
}
BeanDescriptor descriptor = beanInfo.getBeanDescriptor ();
return descriptor.getDisplayName ();
}
/** To allow innerclasses to access the super.setName method.
*/
void setNameSilently (String name) {
super.setName (name);
}
/** Descriptor of three types of properties. Regular,
* expert and hidden.
*/
public static final class Descriptor extends Object {
/** Regular properties. */
public final Node.Property[] property;
/** Expert properties. */
public final Node.Property[] expert;
/** Hidden properties. */
public final Node.Property[] hidden;
/** private constructor */
Descriptor (ArrayList p, ArrayList e, ArrayList h) {
property = new Node.Property[p.size ()];
p.toArray (property);
expert = new Node.Property[e.size ()];
e.toArray (expert);
hidden = new Node.Property[h.size ()];
h.toArray (hidden);
}
}
/** Property change listener to update the properties of the node and
* also the name of the node (sometimes)
*/
private final class PropL extends Object implements PropertyChangeListener {
PropL() {}
public void propertyChange(PropertyChangeEvent e) {
firePropertyChange (e.getPropertyName (), e.getOldValue (), e.getNewValue ());
if (synchronizeName) {
String name = e.getPropertyName ();
if (name == null || name.equals ("name") || name.equals ("displayName")) { // NOI18N
String newName = getNameForBean ();
if (!newName.equals (getName ())) {
setNameSilently (newName);
}
}
}
}
}
}