Hello,

I would like to report a bug in FXMLLoader that affects any Node subclass 
whose
constructor carries a @NamedArg annotation.


PROBLEM
=======

When a class has a @NamedArg-annotated constructor, JavaFXBuilderFactory 
returns a
ProxyBuilder for it. Due to how ProxyBuilder intercepts containsKey() and 
get(), the
FXML <properties> child element — which the specification explicitly 
documents as a
standard read-only map property — fails to load with one of the following 
exceptions
depending on the syntax used:

  Nested element (<properties><myKey>...</myKey></properties>):
    UnsupportedOperationException: Cannot determine type for property.

  Attribute (<properties myKey="$ref"/>):
    PropertyNotFoundException: Property "myKey" does not exist or is read-
only.

The same FXML works correctly when the class does NOT have a @NamedArg 
constructor.
Both the @NamedArg value and the <properties> entry are valid in isolation; 
the failure
only occurs when they are combined.


MINIMAL REPRODUCER
==================

A self-contained Maven project is attached (javafx_bug_report.tar.gz).

Requirements: JDK 21+, JavaFX 25 (fetched automatically by Maven), Maven 3.9
+

To reproduce:

    tar xzf javafx_bug_report.tar.gz
    cd javafx_bug_report
    mvn compile exec:exec

Expected output:

    Bug: ProxyBuilder breaks <properties> for @NamedArg classes
    =============================================================
    SimpleButton — no @NamedArg                   (expected: PASS)
    CustomButton — @NamedArg("label") constructor  (expected: FAIL)

    [ Nested   <properties><tag>...</tag></properties> ]
      FXML load threw LoadException: ...
        Caused by UnsupportedOperationException: Cannot determine type for 
property.

    [ Attribute <properties tag="$ref"/> ]
      FXML load threw LoadException: ...
        Caused by PropertyNotFoundException: Property "tag" does not exist 
or is read-only.

    =============================================================
    Nested element approach : FAILED  <-- BUG
    Attribute approach      : FAILED  <-- BUG
    =============================================================

The project loads two buttons from each FXML:
  SimpleButton — no @NamedArg constructor, passes for both syntaxes
  CustomButton — @NamedArg("label") constructor present, fails for both 
syntaxes

Both the @NamedArg value (label="Test Label") and the <properties> entry are
valid in
isolation; the failure only occurs when they are combined.


ROOT CAUSE
==========

ProxyBuilder overrides containsKey() and get() via getTemporaryContainer(), 
which
calls getReadOnlyProperty(). That method was designed to provide a temporary
ArrayListWrapper for read-only List properties (e.g. styleClass, children) 
during the
pre-build phase. The problem is that it does so unconditionally, for every 
property name:

    // ProxyBuilder.java
    private Object getReadOnlyProperty(String propName) {
        // return ArrayListWrapper now and convert it to proper type later
        return new ArrayListWrapper<>();   // <- no check on propName 
whatsoever
    }

    @Override
    public boolean containsKey(Object key) {
        return (getTemporaryContainer(key.toString()) != null); // ALWAYS 
true
    }

    @Override
    public Object get(Object key) {
        return getTemporaryContainer(key.toString()); // ALWAYS returns 
ArrayListWrapper
    }

FXMLLoader's PropertyElement for <properties> calls
parent.getProperties().containsKey("properties") on the ProxyBuilder (which 
is a Map),
gets true, then calls get("properties") and receives an ArrayListWrapper. It
then treats
<properties> as a read-only list element rather than the node's 
ObservableMap. All
subsequent child property resolution fails against ArrayList instead of 
ObservableMap.


RELATION TO EXISTING BUGS
==========================

This is closely related to JDK-8203870 ("ProxyBuilder cannot handle Read-
Only List
Properties properly", open since 2018-05-26), which covers the same 
ArrayListWrapper
mechanism breaking read-only List properties. The root cause is identical; 
this report
covers the read-only Map case (Node.getProperties()), which has not been 
filed before.

Both issues were introduced by the fix for JDK-8134600 (2016), which added
getTemporaryContainer().


SUGGESTED FIX
=============

ProxyBuilder.getReadOnlyProperty(propName) should introspect the target type
and return
new ArrayListWrapper<>() only when the property's getter actually returns a 
Collection.
For all other cases (including read-only Map properties like getProperties()
) it should
return null, signalling to FXMLLoader that the property is not a read-only 
list container.

    private Object getReadOnlyProperty(String propName) {
        // Only treat as a read-only list container if the getter actually 
returns a Collection
        Method getter = findGetter(type, propName);
        if (getter != null && Collection.class.isAssignableFrom(getter.
getReturnType())) {
            return new ArrayListWrapper<>();
        }
        return null;
    }

This fix would also resolve JDK-8203870 for the list case, since it limits 
the
interception to properties that are genuinely read-only collections on the 
target type.




The bug report was generated using AI, but the bug itself was found 
manually.


Thank you for your time.




Regards Petr Štechmüller

Reply via email to