apache / netbeans

Apache NetBeans
https://netbeans.apache.org/
Apache License 2.0
2.65k stars 850 forks source link

Not honoring containerDelegate in BeanInfo #5774

Open rapastranac opened 1 year ago

rapastranac commented 1 year ago

Apache NetBeans version

Apache NetBeans 17

What happened

Please find attached two minimum reproducible examples. One uses maven and the other one uses ant.

When creating a BeanInfo using netbeans, in the getBdescriptor() method, we can set the value "containerDelegate" using the following line of code to delegate to another container.

beanDescriptor.setValue("containerDelegate", "getInnerPanel");

These examples work with all previous JDK versions, except for JDK 19 I also tested JDK 13, 14, 15, 16, 17 and 18 and they all work fine.

DummyAnt.zip DummyMaven.zip

How to reproduce

Using the Dummy Projects.

With any new JPanel, or using MainPanel:

SectionPanel should be recognized as a Container, therefore the widget should get added into the SectionPanel instance

Did this work correctly in an earlier version?

Did not test JDK19 with previous NetBeans versions

Operating System

Windows 11

JDK

19.0.2

Apache NetBeans packaging

Apache NetBeans platform

Anything else

Are you willing to submit a pull request?

No

matthiasblaesing commented 1 year ago

I think, that you are seeing a bug in the Introspector in the JDK, that just showed up now by "luck".

Stacktrace ``` [exec] INFO: Cannot invoke "java.beans.PropertyDescriptor.getName()" because "pd" is null [exec] java.lang.NullPointerException: Cannot invoke "java.beans.PropertyDescriptor.getName()" because "pd" is null [exec] at java.desktop/java.beans.Introspector.addPropertyDescriptor(Introspector.java:533) [exec] at java.desktop/java.beans.Introspector.addPropertyDescriptors(Introspector.java:570) [exec] at java.desktop/java.beans.Introspector.getTargetPropertyInfo(Introspector.java:496) [exec] at java.desktop/java.beans.Introspector.getBeanInfo(Introspector.java:448) [exec] at java.desktop/java.beans.Introspector.getBeanInfo(Introspector.java:195) [exec] at org.openide.util.Utilities.getBeanInfo(Utilities.java:312) [exec] [catch] at org.netbeans.modules.form.FormUtils.getBeanInfo(FormUtils.java:1907) [exec] at org.netbeans.modules.form.RADComponent.createBeanInfo(RADComponent.java:456) [exec] at org.netbeans.modules.form.RADComponent.getBeanInfo(RADComponent.java:436) [exec] at org.netbeans.modules.form.RADComponent.initInstance(RADComponent.java:167) [exec] at org.netbeans.modules.form.GandalfPersistenceManager.restoreComponent(GandalfPersistenceManager.java:761) [exec] at org.netbeans.modules.form.GandalfPersistenceManager.loadComponent(GandalfPersistenceManager.java:949) [exec] at org.netbeans.modules.form.GandalfPersistenceManager.loadForm(GandalfPersistenceManager.java:484) [exec] at org.netbeans.modules.form.GandalfPersistenceManager.loadForm(GandalfPersistenceManager.java:260) [exec] at org.netbeans.modules.form.FormEditor$2.run(FormEditor.java:327) [exec] at org.netbeans.modules.form.FormLAF$2.run(FormLAF.java:268) [exec] at org.netbeans.modules.openide.util.NbMutexEventProvider$Event.doEventAccess(NbMutexEventProvider.java:123) [exec] at org.netbeans.modules.openide.util.NbMutexEventProvider$Event.readAccess(NbMutexEventProvider.java:77) [exec] at org.netbeans.modules.openide.util.LazyMutexImplementation.readAccess(LazyMutexImplementation.java:71) [exec] at org.openide.util.Mutex.readAccess(Mutex.java:232) [exec] at org.netbeans.modules.form.FormLAF.executeWithLookAndFeel(FormLAF.java:251) [exec] at org.netbeans.modules.form.FormEditor.loadFormData(FormEditor.java:324) [exec] at org.netbeans.modules.nbform.FormEditorSupport.loadOpeningForm(FormEditorSupport.java:436) [exec] at org.netbeans.modules.nbform.FormDesignerTC.loadForm(FormDesignerTC.java:256) [exec] at org.netbeans.modules.nbform.FormDesignerTC.access$300(FormDesignerTC.java:64) [exec] at org.netbeans.modules.nbform.FormDesignerTC$PreLoadTask$1.run(FormDesignerTC.java:245) [exec] at java.desktop/java.awt.event.InvocationEvent.dispatch(InvocationEvent.java:318) [exec] at java.desktop/java.awt.EventQueue.dispatchEventImpl(EventQueue.java:773) [exec] at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:720) [exec] at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:714) [exec] at java.base/java.security.AccessController.doPrivileged(AccessController.java:399) [exec] at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:86) [exec] at java.desktop/java.awt.EventQueue.dispatchEvent(EventQueue.java:742) [exec] at org.netbeans.core.TimableEventQueue.dispatchEvent(TimableEventQueue.java:136) [exec] at java.desktop/java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:203) [exec] at java.desktop/java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:124) [exec] at java.desktop/java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:113) [exec] at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:109) [exec] at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:101) [exec] at java.desktop/java.awt.EventDispatchThread.run(EventDispatchThread.java:90) [exec] java.beans.IntrospectionException: Method not found: setUI [exec] at java.desktop/java.beans.PropertyDescriptor.(PropertyDescriptor.java:114) [exec] at com.mycompany.dummy.SectionPanelBeanInfo.getPdescriptor(SectionPanelBeanInfo.java:226) [exec] at com.mycompany.dummy.SectionPanelBeanInfo.getPropertyDescriptors(SectionPanelBeanInfo.java:779) [exec] at java.desktop/java.beans.Introspector.getPropertyDescriptors(Introspector.java:576) [exec] at java.desktop/java.beans.Introspector.getTargetPropertyInfo(Introspector.java:482) [exec] at java.desktop/java.beans.Introspector.getBeanInfo(Introspector.java:448) [exec] at java.desktop/java.beans.Introspector.getBeanInfo(Introspector.java:195) [exec] at org.openide.util.Utilities.getBeanInfo(Utilities.java:312) [exec] at org.netbeans.modules.form.FormUtils.getBeanInfo(FormUtils.java:1907) [exec] at org.netbeans.modules.form.RADComponent.createBeanInfo(RADComponent.java:456) [exec] at org.netbeans.modules.form.RADComponent.getBeanInfo(RADComponent.java:436) [exec] at org.netbeans.modules.form.RADComponent.initInstance(RADComponent.java:167) [exec] at org.netbeans.modules.form.GandalfPersistenceManager.restoreComponent(GandalfPersistenceManager.java:761) [exec] at org.netbeans.modules.form.GandalfPersistenceManager.loadComponent(GandalfPersistenceManager.java:949) [exec] at org.netbeans.modules.form.GandalfPersistenceManager.loadForm(GandalfPersistenceManager.java:484) [exec] at org.netbeans.modules.form.GandalfPersistenceManager.loadForm(GandalfPersistenceManager.java:260) [exec] at org.netbeans.modules.form.FormEditor$2.run(FormEditor.java:327) [exec] at org.netbeans.modules.form.FormLAF$2.run(FormLAF.java:268) [exec] at org.netbeans.modules.openide.util.NbMutexEventProvider$Event.doEventAccess(NbMutexEventProvider.java:123) [exec] at org.netbeans.modules.openide.util.NbMutexEventProvider$Event.readAccess(NbMutexEventProvider.java:77) [exec] at org.netbeans.modules.openide.util.LazyMutexImplementation.readAccess(LazyMutexImplementation.java:71) [exec] at org.openide.util.Mutex.readAccess(Mutex.java:232) [exec] at org.netbeans.modules.form.FormLAF.executeWithLookAndFeel(FormLAF.java:251) [exec] at org.netbeans.modules.form.FormEditor.loadFormData(FormEditor.java:324) [exec] at org.netbeans.modules.nbform.FormEditorSupport.loadOpeningForm(FormEditorSupport.java:436) [exec] at org.netbeans.modules.nbform.FormDesignerTC.loadForm(FormDesignerTC.java:256) [exec] at org.netbeans.modules.nbform.FormDesignerTC.access$300(FormDesignerTC.java:64) [exec] at org.netbeans.modules.nbform.FormDesignerTC$PreLoadTask$1.run(FormDesignerTC.java:245) [exec] at java.desktop/java.awt.event.InvocationEvent.dispatch(InvocationEvent.java:318) [exec] at java.desktop/java.awt.EventQueue.dispatchEventImpl(EventQueue.java:773) [exec] at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:720) [exec] at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:714) [exec] at java.base/java.security.AccessController.doPrivileged(AccessController.java:399) [exec] at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:86) [exec] at java.desktop/java.awt.EventQueue.dispatchEvent(EventQueue.java:742) [exec] at org.netbeans.core.TimableEventQueue.dispatchEvent(TimableEventQueue.java:136) [exec] at java.desktop/java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:203) [exec] at java.desktop/java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:124) [exec] at java.desktop/java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:113) [exec] at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:109) [exec] at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:101) [exec] at java.desktop/java.awt.EventDispatchThread.run(EventDispatchThread.java:90) ```

I traced it into the com.sun.beans.introspect.ClassInfo. See this:

matthias@enterprise:~/src/netbeans$ ~/bin/jdk-17/bin/jshell --add-exports java.desktop/com.sun.beans.introspect=ALL-UNNAMED                                                              
|  Welcome to JShell -- Version 17.0.4.1
|  For an introduction type: /help intro

jshell> com.sun.beans.introspect.ClassInfo.get(javax.swing.JPanel.class).getMethods().forEach(System.out::println)
public javax.accessibility.AccessibleContext javax.swing.JPanel.getAccessibleContext()
public javax.swing.plaf.PanelUI javax.swing.JPanel.getUI()
public javax.swing.plaf.ComponentUI javax.swing.JPanel.getUI()
public java.lang.String javax.swing.JPanel.getUIClassID()
public void javax.swing.JPanel.setUI(javax.swing.plaf.PanelUI)
public void javax.swing.JPanel.updateUI()

jshell> 
matthias@enterprise:~/src/netbeans$ ~/bin/jdk-19/bin/jshell --add-exports java.desktop/com.sun.beans.introspect=ALL-UNNAMED
|  Willkommen bei JShell - Version 19
|  Geben Sie für eine Einführung Folgendes ein: /help intro

jshell> com.sun.beans.introspect.ClassInfo.get(javax.swing.JPanel.class).getMethods().forEach(System.out::println)
public javax.accessibility.AccessibleContext javax.swing.JPanel.getAccessibleContext()
public javax.swing.plaf.ComponentUI javax.swing.JPanel.getUI()
public javax.swing.plaf.PanelUI javax.swing.JPanel.getUI()
public java.lang.String javax.swing.JPanel.getUIClassID()
public void javax.swing.JPanel.setUI(javax.swing.plaf.PanelUI)
public void javax.swing.JPanel.updateUI()

jshell> 

In java.beans.PropertyDescriptor#getReadMethod is this comment:

      // Since there can be multiple write methods but only one getter
      // method, find the getter method first so that you know what the
      // property type is.  For booleans, there can be "is" and "get"
      // methods.  If an "is" method exists, this is the official
      // reader method so look for this one first.

The first sentence is not true (as visible above). The claim overlooks, that java supports covariant returns, which is implemented by synthesized bridge methods, that are added by the compiler.

ClassInfo iterates the methods and uses the first matching method. Then the return type of that method is assumed to find the setter. The problem on JDK 19 public javax.swing.plaf.ComponentUI javax.swing.JPanel.getUI() is used, but there is no public void javax.swing.JPanel.setUI(javax.swing.plaf.ComponentUI).

I think the JDK introspector needs to improved to first consider non-synthesized methods and non-bridge methods.

Please consider to report this upstream against OpenJDK.

mrserb commented 1 year ago

The order of methods returned by the "ClassInfo" should not affect the selection of "read" and "write" methods. It is just a "stable" wrapper over "Class.getMethods()" since the returned order is not specified and is random for that method. The next code shows the correct read/write methods for the JPanel class on JDK 18 and JDK 19.

        BeanInfo beanInfo = Introspector.getBeanInfo(JPanel.class);
        PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
        for (PropertyDescriptor pd : propertyDescriptors) {
            Class<?> propertyType = pd.getPropertyType();
            if (propertyType !=null && propertyType.toString().contains("UI")) {
                System.out.println("pd = " + pd);
            }
        }

readMethod=public javax.swing.plaf.PanelUI javax.swing.JPanel.getUI(); writeMethod=public void javax.swing.JPanel.setUI(javax.swing.plaf.PanelUI)

mrserb commented 1 year ago

The bug can be reproduced by the next small code when the property descriptor is created w/o usage of Introspector new PropertyDescriptor("UI", JPanel.class, "getUI", "setUI");

matthiasblaesing commented 1 year ago

@mrserb I reread your comment and still don't get it.

The order of methods returned by the "ClassInfo" should not affect the selection of "read" and "write" methods. It is just a "stable" wrapper over "Class.getMethods()" since the returned order is not specified and is random for that method.

The order varies between JDK 17 and JDK 19:

17:

public javax.swing.plaf.PanelUI javax.swing.JPanel.getUI()
public javax.swing.plaf.ComponentUI javax.swing.JPanel.getUI()

19:

public javax.swing.plaf.ComponentUI javax.swing.JPanel.getUI()
public javax.swing.plaf.PanelUI javax.swing.JPanel.getUI()

For both cases in the constructor of PropertyDescriptor getReadMethod is invoked, which more or less directly delegates to Introspector#findMethod which delegates to Introspector#internalFindMethod:

https://github.com/openjdk/jdk/blob/a92363461dbe67d8736a6b0c3cbe1c3ad7aa28ae/src/java.desktop/share/classes/java/beans/Introspector.java#L1156-L1184

The first method, that has the right name and arguments is returned. And here is the difference between JDK 17 and 19 (see order above). For 17 you will get the getter, that returns javax.swing.plaf.PanelUI and for 19 you will get javax.swing.plaf.ComponentUI.

Now execution returns to the PropertyDescriptor constructor and invokes getWriteMethod. It will first deduce the property type from the read method (the return type discussed in the previous paragraph) and now it tries to find the write method, that takes the property type as parameter.

So for 17 it looks for setUI(javax.swing.plaf.PanelUI) and for 19 it will look for setUI(javax.swing.plaf.ComponentUI). Only the former can be satisfied:

public void javax.swing.JPanel.setUI(javax.swing.plaf.PanelUI)

I still think, that the introspector is wrong.

mrserb commented 1 year ago

My point is that the Instospector via public API(Introspector.getBeanInfo) returns the correct beanifo and property descriptors, but the bug is reproduced if the PropertyDescriptor is created directly. The bug is not in the order of methods returned by the "ClassInfo" but in the internalFindMethod which depends on the order, it should select most specific read method based on the return type, this is already done in the Introspector.getBeanInfo

digiovinazzo commented 1 year ago

We ended up fixing the problem creating the PropertyDescriptor of the UI property using the constructor that takes Methods, not method names. This way we can get the PanelUI getUI() method and not the ComponentUI getUI() method:

try {
    Method readMethod = JPanel.class.getDeclaredMethod("getUI"); // NOI18N
    Class<?> returnType = readMethod.getReturnType();
    if (!returnType.equals(PanelUI.class)) {
        throw new RuntimeException("getUI() returns " + returnType + " instead of PanelUI"); // NOI18N
    }

    Method writeMethod = JPanel.class.getDeclaredMethod("setUI", PanelUI.class); // NOI18N
    properties[PROPERTY_UI] = new PropertyDescriptor("UI", readMethod, writeMethod); // NOI18N

} catch (IntrospectionException | NoSuchMethodException | SecurityException e) {
    // XXX: this will not work as it will find the method getUI() returning ComponentUI, not PanelUI
    // see https://github.com/apache/netbeans/issues/5774
    properties[PROPERTY_UI] = new PropertyDescriptor("UI", com.streamsim.commonsgui.SectionPanel.class, "getUI", "setUI"); // NOI18N
}