All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.openide.nodes.BeanNode Maven / Gradle / Ivy

The newest version!
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.openide.nodes;

import java.awt.Image;
import java.beans.BeanDescriptor;
import java.beans.BeanInfo;
import java.beans.Beans;
import java.beans.Customizer;
import java.beans.EventSetDescriptor;
import java.beans.IndexedPropertyDescriptor;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyDescriptor;
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 java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.Action;
import org.openide.util.Exceptions;
import org.openide.util.HelpCtx;
import org.openide.util.Utilities;
import org.openide.util.Lookup;
import org.openide.util.WeakListeners;
import org.openide.util.actions.SystemAction;

/**
* 

* Represents a JavaBeans component as a node. *

* *

* 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. *

* *

* The bean passed in the constructor will be available in the node's lookup, * though not directly. Instead, the node's Lookup will contain * an InstanceCookie from which you can retrieve the bean instance. *

* * @author Jan Jancura, Ian Formanek, Jaroslav Tulach * @param the type of bean to be represented */ public class BeanNode extends AbstractNode { // static .................................................................................................................. /** Icon base for bean nodes */ private static final String ICON_BASE = "org/openide/nodes/beans.gif"; // NOI18N // variables ............................................................................................................. /** bean */ private final T 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(T bean) throws IntrospectionException { this(bean, null, null); } /** 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(T bean, Children children) throws IntrospectionException { this(bean, children, null); } /** Constructs a node for a JavaBean. * The subclass can provide its own {@link Children} implementation * or leave the default implementation. * It can also provide a Lookup, but if you provide one, please do not call * methods {@link #getCookieSet} and {@link #setCookieSet} they will * throw an exception. *

More info on the correct usage of constructor with Lookup can be found * in the {@link Node#Node(org.openide.nodes.Children, org.openide.util.Lookup)} * javadoc. * * @param bean the bean this node will be based on * @param children children for the node (default if null) * @param lkp the lookup to provide content of {@link #getLookup} * and also {@link #getCookie} * @throws IntrospectionException if the bean cannot be analyzed * @since 6.9 */ protected BeanNode(T bean, Children children, Lookup lkp) throws IntrospectionException { super((children == null) ? getChildren(bean) : children, lkp); if (bean == null) { throw new NullPointerException("cannot make a node for a null bean"); // NOI18N } this.bean = bean; try { initialization(lkp != null); } catch (IntrospectionException ie) { throw ie; } catch (RuntimeException re) { throw mkie(re); } catch (LinkageError le) { throw mkie(le); } } 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; } private static IntrospectionException mkie(Throwable t) { return (IntrospectionException) new IntrospectionException(t.toString()).initCause(t); } /** 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 getName (), getDisplayName () or from {@link BeanDescriptor#getDisplayName}. *

Also when the (system) name of the node is changing, the change propagates if possible to * methods setName (String) or setDisplayName (String). (This * does not apply to setting the display name of the node, however.) *

* By default this feature is turned on. * * @param watch true if the name of the node should be synchronized with * the name of the bean, false 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 T getBean() { return bean; } /** Detaches all listeners from the bean and destroys it. * @throws IOException if there was a problem */ @Override 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 true in this implementation */ @Override 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 */ @Override 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 true if there is no name synchronization, or there is * a valid setter method for the name */ @Override 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 */ @Override 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}. */ @Override public Image getOpenedIcon(int type) { return getIcon(type); } @Override public HelpCtx getHelpCtx() { HelpCtx h = HelpCtx.findHelp(bean); if (h != HelpCtx.DEFAULT_HELP) { return h; } 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(T bean, BeanInfo info) { Descriptor d = computeProperties(bean, info); 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 true in the default implementation */ @Override public boolean canCopy() { return true; } /** Can this node be cut? * @return false in the default implementation */ @Override public boolean canCut() { return false; } @Override public Action[] getActions(boolean context) { return NodeOp.createFromNames( new String[] { "Copy", null, "Tools", "Properties" // NOI18N } ); } /* Test if there is a customizer for this node. If true * the customizer can be obtained via getCustomizer method. * * @return true if there is a customizer. */ @Override 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 null if there is no customizer */ @Override public java.awt.Component getCustomizer() { Class clazz = beanInfo.getBeanDescriptor().getCustomizerClass(); if (clazz == null) { return null; } Object o; try { o = clazz.getDeclaredConstructor().newInstance(); } catch (ReflectiveOperationException 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 = ((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. *

Property code names are taken from the property descriptor names * according to the JavaBeans specification. For example, a pair of * methods getFoo and setFoo would result in * a node property with code name foo. If you call * MyBean.setFoo(...), this should result in a property * change event with name foo; if you are using these * properties in some other context (attached to something other than * a BeanNode) 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) { List property = new ArrayList<>(); List expert = new ArrayList<>(); List 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 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); prop.setExpert(true); } else { property.add(prop); } } } // for return new Descriptor(property, expert, hidden); } // // // Initialization methods // // /** Performs initialization of the node */ private void initialization(boolean hasLookup) throws IntrospectionException { setIconBaseWithExtension(ICON_BASE); 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; int k = eventSetDescriptors.length; Method method = null; for (i = 0; i < k; i++) { method = eventSetDescriptors[i].getAddListenerMethod(); if ( (method != null) && method.getName().equals("addPropertyChangeListener") && // NOI18N Modifier.isPublic(method.getModifiers()) ) { break; } } if (i != k) { try { Object o = Beans.getInstanceOf(bean, method.getDeclaringClass()); propertyChangeListener = new PropL(); method.invoke(o, new Object[] { WeakListeners.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). Exceptions.attachMessage(e, "Trying to invoke " + method + " where introspected class is " + clazz.getName()); // 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)); } if (!hasLookup) { 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; } } // find getter for the name try { try { nameGetter = clazz.getMethod("getName"); // NOI18N if (nameGetter.getReturnType() != String.class) { throw new NoSuchMethodException(); } } catch (NoSuchMethodException e) { try { nameGetter = clazz.getMethod("getDisplayName"); // 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); } catch (Exception e) { Exceptions.attachMessage(e, "Bad method: " + clazz.getName() + "." + nameGetter.getName()); Logger.getLogger(BeanNode.class.getName()).log(Level.WARNING, null, e); nameGetter = null; return; } // find the setter for the name try { try { // tries to find method setName (String) nameSetter = clazz.getMethod("setName", String.class); // NOI18N if (nameSetter.getReturnType() != Void.TYPE) { throw new NoSuchMethodException(); } } catch (NoSuchMethodException e) { try { nameSetter = clazz.getMethod("setDisplayName", String.class); // 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); return (name != null) ? name : ""; // NOI18N } catch (Exception ex) { NodeOp.warning(ex); } } BeanDescriptor descriptor = beanInfo.getBeanDescriptor(); return descriptor.getDisplayName(); } /** To allow inner classes to access the super.setName method. */ void setNameSilently(String name) { super.setName(name); } @Override public Action getPreferredAction() { // default action is org.openide.actions.PropertiesAction SystemAction[] arr = NodeOp.createFromNames(new String[] { "Properties" }); // NOI18N return (arr.length == 1) ? arr[0] : null; } /** 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(List p, List e, List 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) { String name = e.getPropertyName(); if (name == null) { firePropertyChange(null, e.getOldValue(), e.getNewValue()); } else { PropertyDescriptor[] arr = beanInfo.getPropertyDescriptors(); for (int i = 0; i < arr.length; i++) { if (arr[i].isHidden()) { continue; } if (name.equals(arr[i].getName())) { firePropertyChange(e.getPropertyName(), e.getOldValue(), e.getNewValue()); break; } } } if (synchronizeName) { if ((name == null) || name.equals("name") || name.equals("displayName")) { // NOI18N String newName = getNameForBean(); if (!newName.equals(getName())) { setNameSilently(newName); } } } } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy