/* * $Id: BeanNode.java,v 1.2 2006/09/25 08:52:36 acaproni Exp $ * * $Date: 2006/09/25 08:52:36 $ * $Revision: 1.2 $ * $Author: acaproni $ * * Copyright CERN, All Rights Reserved. */ package cern.gp.nodes.impl; import java.beans.BeanInfo; import java.beans.Customizer; import java.beans.IndexedPropertyDescriptor; import java.beans.IntrospectionException; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.beans.PropertyDescriptor; import javax.swing.Action; import org.openide.nodes.Children; import org.openide.nodes.CookieSet; import org.openide.nodes.IndexedPropertySupport; import org.openide.nodes.Node; import org.openide.nodes.PropertySupport; import org.openide.nodes.Sheet; import org.openide.util.Utilities; import org.openide.util.actions.SystemAction; import cern.gp.beans.BeanTagger; import cern.gp.beans.BeanUtils; import cern.gp.beans.PropertyInfo; import cern.gp.beans.impl.IntrospectionBasedNodeUpdater; import cern.gp.beans.impl.NodeUpdaterListener; import cern.gp.capabilities.Capability; import cern.gp.capabilities.CapabilityProvider; import cern.gp.nodes.cache.Cacheable; import cern.gp.nodes.cache.CachingStrategy; import cern.gp.nodes.cache.NoCachingStrategy; import cern.gp.nodes.cache.StickyCachingStrategy; import cern.gp.nodes.cache.TimeLimitedCachingStrategy; import cern.gp.util.GPManager; /** * <i><font size="-1" color="#FF0000">**For internal use only** </font></i> * Represents one JavaBean in the nodes hierarchy. * It provides all methods that are needed for communication between * the NetBeans platform and the bean. * <p> * You may use this node type for an already-existing JavaBean * in order for its JavaBean properties to be reflected * as corresponding node properties. Thus, it serves as a compatibility wrapper. * </p> * <p> * This BeanNode is based on a <code>NodeUpdater</code> that provides it the information * it needs. BeanNode builds a <code>NodeUpdater</code> based on the introspection of the bean. * Then it tries to register itself as <code>PropertyChangeListener</code> provided that the * bean implements the method <code>addPropertyChangeListener</code>. In such a case, * the BeanNode will receive the <code>PropertyChangeEvent</code> and update itself * when needed. * </p><p> * Whether or not the BeanNode can register itself as <code>PropertyChangeListener</code> or * receives any <code>PropertyChangeEvent</code>, it will still try to use the getters * available through introspection to initialize itself. When a given getter is not available * for a property needed by the node, the node will try to find a default value in the * <code>BeanInfo</code>. * </p><p> * To see the property this node is interested in, see the class * <code>cern.gp.beans.IntrospectionBasedNodeUpdater</code>. * </p> * * @see cern.gp.beans.impl.NodeUpdater * @see cern.gp.beans.impl.NodeUpdaterProvider * @see cern.gp.beans.impl.IntrospectionBasedNodeUpdater * * @author Lionel Mestre */ public class BeanNode extends org.openide.nodes.AbstractNode implements Cacheable { // register GP standard PropertyEditor static { BeanUtils.registerEditorSearchPaths(new String[] {"cern.gp.beans.editors" }); } // // -- STATIC VARIABLES ----------------------------------------------- // private static final CachingStrategy DEFAULT_CACHING_STRATEGY = new NoCachingStrategy(); /** Icon base for bean nodes */ private static final String ICON_BASE = "org/openide/resources/beans"; private static final Node.Property[] EMPTY_NODE_PROPERTY_ARRAY = new Node.Property[0]; // // -- MEMBER VARIABLES ----------------------------------------------- // /** cached value of the icon when changed */ private java.awt.Image _iconColor16; /** bean */ private Object _bean; /** bean info for the bean */ private BeanInfo _beanInfo; private GuiUpdaterIntrospector _guiUpdater; private Boolean _nodePropertiesCacheable; private SystemAction _defaultAction; // // -- CONSTRUCTORS ----------------------------------------------- // /** * 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 the children of this node * @throws IntrospectionException if the bean cannot be analyzed */ protected BeanNode(Object bean, Children children) throws IntrospectionException { super(children); this._bean = bean; initialization(); } // // -- PUBLIC METHODS ----------------------------------------------- // /* (non-Javadoc) * @see org.openide.nodes.Node#getPreferredAction() */ public Action getPreferredAction() { if (_defaultAction == null) { return super.getPreferredAction(); } return _defaultAction; } public Object getBean() { return _bean; } /** * Detaches all listeners from the bean and destroys it. * @throws IOException if there was a problem */ public void destroy() throws java.io.IOException { _guiUpdater.removeNodeUpdaterListener(null); super.destroy(); } /** * 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 java.awt.Image getIcon(int type) { if ((type == java.beans.BeanInfo.ICON_COLOR_16x16 || type == java.beans.BeanInfo.ICON_MONO_16x16) && _iconColor16 != null) { return _iconColor16; } java.awt.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(int)}. */ public java.awt.Image getOpenedIcon(int type) { return getIcon(type); } /** 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) { exception(e); return null; } catch (IllegalAccessException e) { exception(e); return null; } if (!(o instanceof Customizer)) return null; Customizer cust = ((java.beans.Customizer) o); 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 = createDialog(o); } if (comp == null) { // no component provided return null; } cust.setObject(_bean); if (!_guiUpdater.hasRegisteredListenerInternal()) { cust.addPropertyChangeListener(new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent e) { firePropertyChange(e.getPropertyName(), e.getOldValue(), e.getNewValue()); } }); } return comp; } // // -- implements Cacheable interface ------------------------------------------------- // /** * reset the cache for this Node. As a result the node will reset the cache of all * its properties. */ public void resetCache() { Sheet.Set regularProperties = getSheet().get(Sheet.PROPERTIES); Sheet.Set expertProperties = getSheet().get(Sheet.EXPERT); if (regularProperties != null) resetCache(regularProperties); if (expertProperties != null) resetCache(expertProperties); } // // -- PROTECTED METHODS ----------------------------------------------- // /** * 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 org.openide.nodes.BeanNode#computeProperties * @param bean bean to compute properties for * @param info information about the bean * @param propertyInfo extra information of some properties (possibly null) */ protected void createProperties(Object bean, BeanInfo info, PropertyInfo[] propertyInfo) { Descriptor d = computeProperties(bean, _beanInfo, true, _nodePropertiesCacheable, propertyInfo); Sheet sets = getSheet(); Sheet.Set pset = Sheet.createPropertiesSet(); pset.put(d.property); sets.put(pset); if (d.expert.length != 0) { Sheet.Set eset = Sheet.createExpertSet(); eset.put(d.expert); sets.put(eset); } } /** * Returns the actions that shall be displayed in the pop-up menu for this node. * This method is called by Netbeans Explorer to build the pop-up menu for this node. * It uses the BeanInfo associated with the contained bean for finding the actions. */ protected SystemAction[] createActions() { String[] actions = _guiUpdater.getNodeActions(); if (actions == null) return null; SystemAction[] sysActions = new SystemAction[actions.length]; for (int i = 0; i < actions.length; i++) { if (actions[i] != null) { try { Class actionClass = Class.forName(actions[i], true, _bean.getClass().getClassLoader()); sysActions[i] = SystemAction.get(actionClass); } catch (ClassNotFoundException e) { warning(e); } } } return sysActions; } // // -- PRIVATE METHODS ----------------------------------------------- // private void resetCache(String propertyName) { Sheet.Set regularProperties = getSheet().get(Sheet.PROPERTIES); Sheet.Set expertProperties = getSheet().get(Sheet.EXPERT); if (regularProperties != null) resetCache(regularProperties, propertyName); if (expertProperties != null) resetCache(expertProperties, propertyName); } private void resetCache(Sheet.Set sheetSet) { Property[] properties = sheetSet.getProperties(); for (int i = 0; i < properties.length; i++) { if (properties[i] instanceof Cacheable) { ((Cacheable) properties[i]).resetCache(); } } } private void resetCache(Sheet.Set sheetSet, String propertyName) { Property property = sheetSet.get(propertyName); if (property != null && property instanceof Cacheable) { ((Cacheable) property).resetCache(); } } private void hideUnwantedPropertyDescriptors() { PropertyDescriptor[] propertyDescriptor = _beanInfo.getPropertyDescriptors(); int k = propertyDescriptor.length; for (int i = 0; i < k; i++) { if (_guiUpdater.isPropertyHidden(propertyDescriptor[i])) { propertyDescriptor[i].setHidden(true); } } } /** * Performs initalization of the node */ private void initialization() throws IntrospectionException { setIconBase(ICON_BASE); Class clazz = BeanUtils.findTargetClass(_bean); _beanInfo = Utilities.getBeanInfo(clazz); _guiUpdater = new GuiUpdaterIntrospector(_bean, _beanInfo, clazz); // set the node properties from the bean / BeanInfo String name = _guiUpdater.getName(); if (name != null && name.length() > 0) setName(name); String displayName = _guiUpdater.getDisplayName(); if (displayName != null && displayName.length() > 0) setDisplayName(displayName); String shortDescription = _guiUpdater.getShortDescription(); if (shortDescription != null && shortDescription.length() > 0) setShortDescription(shortDescription); _iconColor16 = _guiUpdater.getNodeIcon(); _defaultAction = getDefaultActionInstance(_guiUpdater.getNodeDefaultAction()); _nodePropertiesCacheable = _guiUpdater.getNodePropertiesCacheable(); hideUnwantedPropertyDescriptors(); createProperties(_bean, _beanInfo, _guiUpdater.getPropertyInfo()); for (java.util.Enumeration e = _beanInfo.getBeanDescriptor().attributeNames(); e.hasMoreElements();) { String aname = (String) e.nextElement(); setValue(aname, _beanInfo.getBeanDescriptor().getValue(aname)); } Node.Cookie instanceCookie = createInstanceCookie(_bean); if (instanceCookie != null) { getCookieSet().add(instanceCookie); } // add capabilities from the bean to the cookieSet initializeCookieSet(_bean, getCookieSet()); } private SystemAction getDefaultActionInstance(String defaultAction) { if (defaultAction == null) return SystemAction.get(cern.gp.actions.PropertiesAction.class); try { return SystemAction.get(Class.forName(defaultAction, true, _bean.getClass().getClassLoader())); } catch (ClassNotFoundException e) { warning(e); return SystemAction.get(cern.gp.actions.PropertiesAction.class); } } // // -- PRIVATE STATIC METHODS : properties ----------------------------------------------- // private static Node.Property createIndexedNodeProperty( Object bean, IndexedPropertyDescriptor p, CachingStrategy strategy) { 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 return null; } IndexedPropertySupport support = new CacheableIndexedPropertySupport( bean, p.getPropertyType(), p.getIndexedPropertyType(), p.getReadMethod(), p.getWriteMethod(), p.getIndexedReadMethod(), p.getIndexedWriteMethod(), strategy); support.setName(p.getName()); support.setDisplayName(p.getDisplayName()); support.setShortDescription(p.getShortDescription()); for (java.util.Enumeration e = p.attributeNames(); e.hasMoreElements();) { String aname = (String) e.nextElement(); support.setValue(aname, p.getValue(aname)); } return support; } private static Node.Property createNodeProperty(Object bean, PropertyDescriptor p, CachingStrategy strategy) { // 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 CacheablePropertySupport(bean, p.getPropertyType(), p.getReadMethod(), p.getWriteMethod(), strategy); support.setName(p.getName()); support.setDisplayName(p.getDisplayName()); support.setShortDescription(p.getShortDescription()); support.setPropertyEditorClass(p.getPropertyEditorClass()); for (java.util.Enumeration e = p.attributeNames(); e.hasMoreElements();) { String aname = (String) e.nextElement(); support.setValue(aname, p.getValue(aname)); } return support; } private static CachingStrategy createCachingStrategy(Boolean beanCacheable, Boolean propertyCacheable) { boolean shouldCache = false; if (propertyCacheable == null) { // unspecified at the property level : we use bean level shouldCache = beanCacheable != null && beanCacheable.booleanValue(); } else { // specified at the property level : override bean level shouldCache = propertyCacheable.booleanValue(); } if (shouldCache) { return new StickyCachingStrategy(); } else { return new TimeLimitedCachingStrategy(500); } } /** * Computes a descriptor for properties from a bean info. * @param bean bean to create properties for * @param info about the bean * @param ignoreHiddenProperties true if hidden property should be ignored completely * @param propertyInfo extra information of some properties (possibly null) * @return three property lists */ private static Descriptor computeProperties( Object bean, BeanInfo info, boolean ignoreHiddenProperties, Boolean nodePropertiesCacheable, PropertyInfo[] propertyInfo) { java.util.List property = null; java.util.List expert = null; java.util.List hidden = null; PropertyDescriptor[] propertyDescriptor = info.getPropertyDescriptors(); int k = propertyDescriptor.length; for (int i = 0; i < k; i++) { if (propertyDescriptor[i].getPropertyType() == null) continue; String propName = propertyDescriptor[i].getName(); // we first update the PropertyDescriptor with the PropertyInfo // that update may make non hidden a property that was hidden PropertyInfo propInfo = findPropertyInfoByName(propertyInfo, propName); if (propInfo != null) propInfo.updatePropertyDescriptor(propertyDescriptor[i]); // after the update we can test whether the property is hidden or not if (ignoreHiddenProperties && propertyDescriptor[i].isHidden()) { continue; } CachingStrategy strategy = createCachingStrategy(nodePropertiesCacheable, BeanTagger.isCacheable(propertyDescriptor[i])); Node.Property prop; if (propertyDescriptor[i] instanceof IndexedPropertyDescriptor) { prop = createIndexedNodeProperty(bean, (IndexedPropertyDescriptor) propertyDescriptor[i], strategy); } else { prop = createNodeProperty(bean, propertyDescriptor[i], strategy); } if (prop == null) continue; // Add to right category. if (propertyDescriptor[i].isHidden()) { if (hidden == null) hidden = new java.util.ArrayList(); hidden.add(prop); } else if (propertyDescriptor[i].isExpert()) { if (expert == null) expert = new java.util.ArrayList(); expert.add(prop); } else { if (property == null) property = new java.util.ArrayList(); property.add(prop); } } // for return new Descriptor(property, expert, hidden); } private static PropertyInfo findPropertyInfoByName(PropertyInfo[] propertyInfo, String propName) { if (propertyInfo == null) return null; for (int i = 0; i < propertyInfo.length; i++) { if (propName.equals(propertyInfo[i].getName())) return propertyInfo[i]; } return null; } // // -- PRIVATE STATIC METHODS : Helpers ----------------------------------------------- // private static void exception(Throwable e) { GPManager.notify(GPManager.INFORMATIONAL, e); } private static void warning(Throwable e) { GPManager.notify(GPManager.WARNING, e); } /** * Initializes the CookieSet with the corresponding Capability available * from the bean directly or through a CapabilityProvider * @param bean the bean from which to find the capabilities * @param cookieSet the cookie set to update */ private static void initializeCookieSet(Object bean, CookieSet cookieSet) { if (bean instanceof CapabilityProvider) { // the bean is a provides a method to get capabilities CapabilityProvider provider = (CapabilityProvider) bean; Capability[] capabilities = provider.getCapabilities(); if (capabilities == null || capabilities.length == 0) return; for (int i = 0; i < capabilities.length; i++) { cookieSet.add(capabilities[i]); } } else { // we introspect to get the capabilities Class[] interfaces = bean.getClass().getInterfaces(); for (int i = 0; i < interfaces.length; i++) { if (Capability.class.isAssignableFrom(interfaces[i])) { cookieSet.add((Capability) bean); } } } } /** Creates InstanceCookie, if available. * @param bean the object to create cookie for * @return Node.Cookie or null */ private static final Node.Cookie createInstanceCookie(final Object o) { return new org.openide.cookies.InstanceCookie() { public String instanceName() { return o.getClass().getName(); } public Class instanceClass() { return o.getClass(); } public Object instanceCreate() { return o; } }; } /** Checks whether an object is instance of DialogDescriptor and if * so it used top manager to create its instance. * @param maybeDialogDescriptor an object * @return a dialog or null */ private static final java.awt.Dialog createDialog(Object o) { if (o instanceof org.openide.DialogDescriptor) { return GPManager.createDialog((org.openide.DialogDescriptor) o); } return null; } /** Attaches a customizer to given node. * @param node the bean node * @param cust customizer to attach */ private static void attachCustomizer(Node node, java.beans.Customizer cust) { if (cust instanceof org.openide.explorer.propertysheet.editors.NodeCustomizer) { ((org.openide.explorer.propertysheet.editors.NodeCustomizer) cust).attach(node); } } // // -- INNER CLASSES ----------------------------------------------- // /** * Descriptor of three types of properties. Regular, expert and hidden. */ private static final class Descriptor { /** Regular properties. */ public final Node.Property[] property; /** Expert properties. */ public final Node.Property[] expert; /** Hidden properties. */ public final Node.Property[] hidden; /** private constructor */ Descriptor(java.util.List property, java.util.List expert, java.util.List hidden) { if (property == null || property.size() == 0) { this.property = EMPTY_NODE_PROPERTY_ARRAY; } else { this.property = new Node.Property[property.size()]; property.toArray(this.property); } if (expert == null || expert.size() == 0) { this.expert = EMPTY_NODE_PROPERTY_ARRAY; } else { this.expert = new Node.Property[expert.size()]; expert.toArray(this.expert); } if (hidden == null || hidden.size() == 0) { this.hidden = EMPTY_NODE_PROPERTY_ARRAY; } else { this.hidden = new Node.Property[hidden.size()]; hidden.toArray(this.hidden); } } } /** * Inherits from <code>IntrospectionBasedNodeUpdater</code> in order to hook * the abstract <code>fire***Change</code> to the properties of this node. */ private final class GuiUpdaterIntrospector extends IntrospectionBasedNodeUpdater { public GuiUpdaterIntrospector(Object bean, BeanInfo beanInfo, Class targetClass) throws IntrospectionException { super(bean, beanInfo, targetClass); } boolean hasRegisteredListenerInternal() { return hasRegisteredListener(); } // // -- implements NodeUpdater ----------------------------------------------- // public void addNodeUpdaterListener(NodeUpdaterListener listener) { } public void removeNodeUpdaterListener(NodeUpdaterListener listener) { removePropertyChangeListener(); } // // -- PROTECTED METHODS ----------------------------------------------- // protected void firePropertyChange(String propertyName, Object oldValue, Object newValue) { if (propertyName == null) { resetCache(); } else { resetCache(propertyName); } BeanNode.this.firePropertyChange(propertyName, oldValue, newValue); } protected void fireNameChange(String newName) { setName(newName); } protected void fireDisplayNameChange(String newDisplayName) { setDisplayName(newDisplayName); } protected void fireShortDescriptionChange(String newShortDescription) { setShortDescription(newShortDescription); } protected void fireNodeIconChange(java.awt.Image newIcon) { _iconColor16 = newIcon; BeanNode.this.fireIconChange(); } protected void fireNodeDefaultActionChange(String newDefaultAction) { _defaultAction = getDefaultActionInstance(newDefaultAction); } } // end inner class GuiUpdaterIntrospector }