1

I'm trying to use the resource tag inside fx:include in a FXML file.

What I don't understand is, that loading the resource bundle manually with ResourceBundle.getBundle() works completely fine. I already tried a lot of variants in the fxml like:

  • "lang_challenges"

  • "wand555/github/io/challengesreworkedgui/lang_challenges"

package wand555.github.io.challengesreworkedgui;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

import java.io.IOException;
import java.util.Enumeration;
import java.util.Locale;
import java.util.ResourceBundle;
import java.util.Set;

public class ChallengeApplication extends Application {
    @Override
    public void start(Stage stage) throws IOException {
        ResourceBundle.clearCache();
        ResourceBundle bundle = new ResourceBundleWrapper(ResourceBundle.getBundle("wand555/github/io/challengesreworkedgui/lang_challenges"));
        System.out.println(bundle.getString("challenge.name")); // outputs "abc"
        FXMLLoader loader = new FXMLLoader(ChallengeApplication.class.getResource("overview.fxml"), bundle);
        Parent root = loader.load();
        Scene scene = new Scene(root, 1000, 1000);
        stage.setScene(scene);
        stage.show();
    }

    public static void main(String[] args) {
        launch();
    }


    // this class effectively does nothing, but it will be loaded by the
    // application class loader
    // instead of the system class loader.
    private static class ResourceBundleWrapper extends ResourceBundle {

        private final ResourceBundle bundle;

        ResourceBundleWrapper(ResourceBundle bundle) {
            this.bundle = bundle;
        }

        @Override
        protected Object handleGetObject(String key) {
            return bundle.getObject(key);
        }

        @Override
        public Enumeration<String> getKeys() {
            return bundle.getKeys();
        }

        @Override
        public boolean containsKey(String key) {
            return bundle.containsKey(key);
        }

        @Override
        public Locale getLocale() {
            return bundle.getLocale();
        }

        @Override
        public Set<String> keySet() {
            return bundle.keySet();
        }

    }
}

overview.fxml:

<?xml version="1.0" encoding="UTF-8"?>

<AnchorPane prefHeight="500.0" xmlns="http://javafx.com/javafx/19" xmlns:fx="http://javafx.com/fxml/1" fx:controller="wand555.github.io.challengesreworkedgui.controllers.OverviewController">
   <children>
      <VBox>
         <children>
            <StackPane alignment="CENTER_LEFT">
               <children>
                  <Button fx:id="exportButton" mnemonicParsing="false" onAction="#onExport" text="Export" />
               </children>
            </StackPane>
            <HBox prefHeight="100.0" prefWidth="200.0" spacing="25.0">
               <children>
                  <fx:include fx:id="challengesOverview" source="challenges/challenges_overview.fxml" resources="lang_challenges" charset="utf-8"/>
                  <Separator orientation="VERTICAL" prefHeight="200.0" />
                  <fx:include source="goals/goal_overview.fxml" />
                  <Separator orientation="VERTICAL" prefHeight="200.0" />
               </children>
            </HBox>
         </children>
      </VBox>
   </children>
</AnchorPane>

The lang_challenges.properties contains a single key-value pair

challenge.name=abc

The `lang_challenges_de.properties' has the same content

challenge.name=abc

And this is the error message I'm getting

Exception in Application start method
java.lang.reflect.InvocationTargetException
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:568)
    at javafx.graphics@19/com.sun.javafx.application.LauncherImpl.launchApplicationWithArgs(LauncherImpl.java:465)
    at javafx.graphics@19/com.sun.javafx.application.LauncherImpl.launchApplication(LauncherImpl.java:364)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:568)
    at java.base/sun.launcher.LauncherHelper$FXHelper.main(LauncherHelper.java:1082)
Caused by: java.lang.RuntimeException: Exception in Application start method
    at javafx.graphics@19/com.sun.javafx.application.LauncherImpl.launchApplication1(LauncherImpl.java:901)
    at javafx.graphics@19/com.sun.javafx.application.LauncherImpl.lambda$launchApplication$2(LauncherImpl.java:196)
    at java.base/java.lang.Thread.run(Thread.java:833)
Caused by: javafx.fxml.LoadException: 
/Users/felixnaumann/Documents/ChallengesReworked/ChallengesReworkedGUI/target/classes/wand555/github/io/challengesreworkedgui/overview.fxml:21

    at javafx.fxml@19/javafx.fxml.FXMLLoader.constructLoadException(FXMLLoader.java:2707)
    at javafx.fxml@19/javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2685)
    at javafx.fxml@19/javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2548)
    at javafx.fxml@19/javafx.fxml.FXMLLoader.load(FXMLLoader.java:2516)
    at challenges.reworked.gui/wand555.github.io.challengesreworkedgui.ChallengeApplication.start(ChallengeApplication.java:22)
    at javafx.graphics@19/com.sun.javafx.application.LauncherImpl.lambda$launchApplication1$9(LauncherImpl.java:847)
    at javafx.graphics@19/com.sun.javafx.application.PlatformImpl.lambda$runAndWait$12(PlatformImpl.java:484)
    at javafx.graphics@19/com.sun.javafx.application.PlatformImpl.lambda$runLater$10(PlatformImpl.java:457)
    at java.base/java.security.AccessController.doPrivileged(AccessController.java:399)
    at javafx.graphics@19/com.sun.javafx.application.PlatformImpl.lambda$runLater$11(PlatformImpl.java:456)
    at javafx.graphics@19/com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:96)
Caused by: java.util.MissingResourceException: Can't find bundle for base name lang_challenges, locale de_DE
    at java.base/java.util.ResourceBundle.throwMissingResourceException(ResourceBundle.java:2045)
    at java.base/java.util.ResourceBundle.getBundleImpl(ResourceBundle.java:1683)
    at java.base/java.util.ResourceBundle.getBundleImpl(ResourceBundle.java:1575)
    at java.base/java.util.ResourceBundle.getBundle(ResourceBundle.java:1280)
    at javafx.fxml@19/javafx.fxml.FXMLLoader$IncludeElement.processAttribute(FXMLLoader.java:1100)
    at javafx.fxml@19/javafx.fxml.FXMLLoader$Element.processStartElement(FXMLLoader.java:230)
    at javafx.fxml@19/javafx.fxml.FXMLLoader$ValueElement.processStartElement(FXMLLoader.java:755)
    at javafx.fxml@19/javafx.fxml.FXMLLoader.processStartElement(FXMLLoader.java:2808)
    at javafx.fxml@19/javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2634)
    ... 9 more
Exception running application wand555.github.io.challengesreworkedgui.ChallengeApplication
wand555
  • 155
  • 6

1 Answers1

3

Whew, finally figured it out after digging deep into the source code of ResourceBundle and ClassLoader.

How to fix it:

Really easy way:

Put your .properties files at the root of the resources folder. Then use it inside fxml

<fx:include source="second.fxml" resources="second_bundle"/>

and then everything should work fine. However this approach is not suitable for larger projects which subdivide the resource bundles into different packages.

Using sub-packages in resources package

Give the fully qualified path name in the resources tag. So for example if your package structure from src is com/example/demo/ (also in the resources folder) then use

<fx:include source="second.fxml" resources="com/example/demo/second_bundle"/>

But we are not done yet. You need to open the package to all modules in the module-info.java, explicitly using

opens com.example.demo to javafx.fxml;

does not work. Instead you need to write

opens com.example.demo;

My two cents

Honestly to me this sounds like a bug. I debugged the entire loading process and when the second_bundle is loaded from inside the fxml file, the caller module is javafx.fxml. And as the java doc for getResourceAsStream (which is internally called when loading) states:

A package name is derived from the resource name. If the package name is a package in the module then the resource can only be located by the caller of this method when the package is open to at least the caller's module. If the resource is not in a package in the module then the resource is not encapsulated.

So in theory opening your package to just javafx.fxml should work, but it doesn't...

Full demo project

This demo project uses sub-packages.

Project structure:

project structure

HelloApplication:

public class HelloApplication extends Application {
    @Override
    public void start(Stage stage) throws IOException {
        ResourceBundle bundle = new ResourceBundleWrapper(ResourceBundle.getBundle("test_bundle"));
        System.out.println(bundle.getString("first.button")); // outputs "English/Deutsch"

        FXMLLoader fxmlLoader = new FXMLLoader(HelloApplication.class.getResource("first.fxml"), bundle);
        Scene scene = new Scene(fxmlLoader.load(), 320, 240);
        stage.setTitle("Hello!");
        stage.setScene(scene);
        stage.show();
    }

    public static void main(String[] args) {
        launch();
    }

    // this class effectively does nothing, but it will be loaded by the
    // application class loader
    // instead of the system class loader.
    private static class ResourceBundleWrapper extends ResourceBundle {

        private final ResourceBundle bundle;

        ResourceBundleWrapper(ResourceBundle bundle) {
            this.bundle = bundle;
        }

        @Override
        protected Object handleGetObject(String key) {
            return bundle.getObject(key);
        }

        @Override
        public Enumeration<String> getKeys() {
            return bundle.getKeys();
        }

        @Override
        public boolean containsKey(String key) {
            return bundle.containsKey(key);
        }

        @Override
        public Locale getLocale() {
            return bundle.getLocale();
        }

        @Override
        public Set<String> keySet() {
            return bundle.keySet();
        }

    }
}

first.fxml:

<VBox alignment="CENTER" spacing="20.0" xmlns:fx="http://javafx.com/fxml"
      fx:controller="com.example.demo.HelloController">
    <padding>
        <Insets bottom="20.0" left="20.0" right="20.0" top="20.0"/>
    </padding>

    <Label fx:id="welcomeText"/>
    <Button text="%first.button" onAction="#onHelloButtonClick"/>
    <fx:include source="second.fxml" resources="com/example/demo/second_bundle"/>
</VBox>

second.fxml:

<VBox alignment="CENTER" spacing="20.0" xmlns:fx="http://javafx.com/fxml">
    <padding>
        <Insets bottom="20.0" left="20.0" right="20.0" top="20.0"/>
    </padding>

    <Label fx:id="welcomeText"/>
    <Button text="%second.button"/>
</VBox>

test_bundle.properties:

first.button=English

second_bundle.properties:

second.button=Hello

module-info.java:

module com.example.demo {
    requires javafx.controls;
    requires javafx.fxml;
    
    opens com.example.demo;
    exports com.example.demo;
}
wand555
  • 155
  • 6
  • *“the caller module is javafx.fxml”* -> From your stack trace, the caller looks to be `java.base` ~> `java.base/java.util.ResourceBundle.getBundleImpl(ResourceBundle.java:1683)`. Though, I didn’t run the debugging or investigation that you did to verify this. – jewelsea Dec 31 '22 at 07:43
  • What I might consider a bug, or at least just do not understand the reasoning behind, is the [weird call](https://github.com/openjdk/jfx/blob/master/modules/javafx.fxml/src/main/java/javafx/fxml/FXMLLoader.java#L1103) to `getClassLoader()` on the given `ResourceBundle`'s class. Not only does that require the outer FXML to have a `ResourceBundle` set even if only the nested FXML needs it, but it completely ignores the class loader semantics used by the rest of `FXMLLoader`. It also requires you to create that wrapper class, because otherwise it returns `null`. Like, why? – Slaw Dec 31 '22 at 09:54
  • Regarding your two cents: The [ResourceBundle](https://docs.oracle.com/en/java/javase/19/docs/api/java.base/java/util/ResourceBundle.html) documentation does mention [ClassLoader.getResourceAsStream](https://docs.oracle.com/en/java/javase/19/docs/api/java.base/java/lang/ClassLoader.html#getResourceAsStream(java.lang.String)) quite a few times, and that method's documentation states, "_Additionally, and except for the special case where the resource has a name ending with ".class", this method will only find resources in packages of named modules when the package is opened unconditionally_". – Slaw Dec 31 '22 at 10:13
  • @jewelsea If you go to the method `loadPropertyResourceBundle` in ResourceBundle (line 3664) and debug the loading, `callerModule` and `module` are both `javafx.fxml` when loading the resource bundle specified in the `resources` tag inside a FXML file. In comparison the `callerModule` and `module` are `com.example.demo` when loading the resource bundle through `ResourceBundle.getBundle()` in Java. Maybe using the word "caller module" is misleading as I meant the variable and not the literal module the error originates from. – wand555 Dec 31 '22 at 11:31
  • @Slaw I dont understand the internals of java enough to fully grasp why this is necessary. They pushed through so hard with the module accessibility yet I have to essentially bypass it to use this "feature". And to the weird call: I had that issue before I had the problem in my post. I found an answer in an oracle forum from like 2011 where someone complained the problem and some other person came up with the wrapper solution. They also said there that they reported the bug but any link to the bug tracker is dead (and I only started with java in 2019 so I dont know how stuff worked back then) – wand555 Dec 31 '22 at 11:44
  • A related question and answer: [How to access resource using class loader in Java 9](https://stackoverflow.com/a/48790851/1155209). The comment on that answer is by Alan Bateman, who I think was part of the Java modularization project. – jewelsea Dec 31 '22 at 12:03
  • Well, I've set up a test application with two modules, `A` and `B`. I put the bundle in `A` and an attempt to load the bundle in `B`. I used `StackWalker` to get the calling module (`A`) and passed it to `ResourceBundle.getBundle(String,Locale,Module)`. Unless module `A` unconditionally `opens` the bundle's package, it doesn't work. Even `opens bundles to java.base, B;` didn't work. So, I'm not sure this is directly related to `FXMLLoader`. The problem seems to be the caller module (of `getBundle`) is not the same as the bundle's module. Don't know if that's a bug or not. – Slaw Dec 31 '22 at 20:47
  • Looking into it more, it appears the intended way for loading resource bundles across named-module boundaries is via [ResourceBundleProvider](https://docs.oracle.com/en/java/javase/19/docs/api/java.base/java/util/spi/ResourceBundleProvider.html). Implementing this is not exactly trivial; see [this article](https://www.morling.dev/blog/resource-bundle-lookups-in-modular-java-applications/). Additionally, I think the `javafx.fxml` module would have to be updated to declare an SPI like `javafx.fxml.spi.FXMLResourcesProvider` and then `uses` that SPI. We can't make those changes, unfortunately. – Slaw Dec 31 '22 at 21:29
  • 1
    Note if you want to minimize the effects of an unconditional `opens`, then you could put your resource bundles in their own bundle-only package(s). – Slaw Dec 31 '22 at 21:52