Fixes generics, only creates one constructor, adds some tests, adds java compatibility options, rework some classes, fixes some problems

This commit is contained in:
Guillaume Tâche
2024-12-13 21:20:49 +01:00
parent 7ec52cd175
commit d63188e8ee
172 changed files with 3955 additions and 17340 deletions

276
README.md
View File

@@ -1 +1,275 @@
# FXML Compiler
# FXML Compiler
## Introduction
This projects aims at generating Java code from FXML files.
## Requirements
- Java 21 (at least for the plugin, the generated code can be compatible with older Java versions)
- Maven 3.8.0
## Installation
Add the plugin to your project:
```xml
<build>
<plugins>
<plugin>
<groupId>com.github.gtache</groupId>
<artifactId>fxml-compiler-maven-plugin</artifactId>
<version>1.0.0</version>
<executions>
<execution>
<goals>
<goal>compile</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
```
Optionally add dependencies to the plugin (e.g. when using MediaView and controlsfx):
```xml
<build>
<plugins>
<plugin>
<groupId>com.github.gtache</groupId>
<artifactId>fxml-compiler-maven-plugin</artifactId>
<version>1.0.0</version>
<executions>
<execution>
<goals>
<goal>compile</goal>
</goals>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-media</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>org.controlsfx</groupId>
<artifactId>controlsfx</artifactId>
<version>${controlsfx.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
```
## Advantages
- Compile-time validation
- Faster startup speed for the application
- Possibility to use controller factories to instantiate controllers with final fields
- Easier time with JPMS
- No need to open the controllers packages to javafx.fxml
- No need to open the resources packages when using use-image-inputstream-constructor (if images or resource bundles
are in the project resources)
## Disadvantages
- Possible bugs (file an issue if you see one)
- Expression binding is limited
- Probably not fully compatible with all FXML features (file an issue if you need one in specific)
## Parameters
### Field injection
There are four ways to inject fields into a controller:
- `REFLECTION`: Inject fields using reflection (like FXMLLoader)
-
- ```java
try {
final var field = controller.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(controller, object);
} catch (final NoSuchFieldException | IllegalAccessException e) {
throw new RuntimeException("Error using reflection on " + fieldName, e);
}
```
-
- Slowest method
-
- Fully compatible with FXMLLoader, so this allows easy switching between the two.
-
- This is the default injection method (for compatibility reasons).
- `ASSIGN`: variable assignment
-
- `controller.field = value`
-
- This means that the field must be accessible from the view (e.g. package-private).
- `SETTERS`: controller setters methods
-
- `controller.setField(value)`
- `FACTORY`: controller factory
-
- `controller = factory.create(fieldMap)`
-
- `factory` is a `ControllerFactory` instance that is created at runtime and passed to the view.
-
- `fieldMap` is a map of field name (String) to value (Object) that is computed during the view `load` method.
-
- This allows the controller to have final fields.
### Method injections
There are two ways to inject methods (meaning use them as event handlers) into a controller:
- `REFLECTION`: Inject methods using reflection (like FXMLLoader)
-
- ```java
try {
final java.lang.reflect.Method method;
final var methods = java.util.Arrays.stream(controller.getClass().getDeclaredMethods())
.filter(m -> m.getName().equals(methodName)).toList();
if (methods.size() > 1) {
final var eventMethods = methods.stream().filter(m ->
m.getParameterCount() == 1 && javafx.event.Event.class.isAssignableFrom(m.getParameterTypes()[0])).toList();
if (eventMethods.size() == 1) {
method = eventMethods.getFirst();
} else {
final var emptyMethods = methods.stream().filter(m -> m.getParameterCount() == 0).toList();
if (emptyMethods.size() == 1) {
method = emptyMethods.getFirst();
} else {
throw new IllegalArgumentException("Multiple matching methods for " + methodName);
}
}
} else if (methods.size() == 1) {
method = methods.getFirst();
} else {
throw new IllegalArgumentException("No matching method for " + methodName);
}
method.setAccessible(true);
if (method.getParameterCount() == 0) {
method.invoke(controller);
} else {
method.invoke(controller, event);
}
} catch (final IllegalAccessException | java.lang.reflect.InvocationTargetException ex) {
throw new RuntimeException("Error using reflection on " + methodName, ex);
}
```
-
- Slowest method
-
- Fully compatible with FXMLLoader, so this allows easy switching between the two.
-
- This is the default injection method (for compatibility reasons).
- `REFERENCE`: Directly reference the method
-
- `controller.method(event)`
-
- This means that the method must be accessible from the view (e.g. package-private).
### Resource bundle injection
There are three ways to inject resource bundles into a controller:
- `CONSTRUCTOR`: Inject resource bundle in the view constructor
-
- ```java
view = new View(controller, resourceBundle);
```
-
- This is the default injection method because it is the most similar to FXMLLoader (
`FXMLLoader.setResources(resourceBundle)`).
- `CONSTRUCTOR_FUNCTION`: Injects a function in the view constructor
-
- `bundleFunction.apply(key)`
-
- The function takes a string (the key) and returns a string (the value)
- This allows using another object than a resource bundle for example
- `GETTER`: Retrieves the resource bundle using a controller getter method
-
- `controller.resources()`
-
- The method name (resources) was chosen because it matches the name of the field injected by FXMLLoader.
-
- The method must be accessible from the view (e.g. package-private).
- `GET-BUNDLE`: Injects the bundle name in the view constructor and retrieves it using
`ResourceBundle.getBundle(bundleName)`
-
- `ResourceBundle.getBundle(bundleName)`
- Also used when fx:include specifies a resource attribute to pass it to the included view.
## View creation
The views are generated in the same packages as the FXML files.
The name of the class is generated from the name of the FXML file.
The constructor of the view is generated depending on the parameters of the plugin.
The constructor will have as many arguments as the number of controllers in the FXML tree (recursive fx:include) +
potentially the resource bundle if necessary. If no resource reference (`%key.to.resource`) is found in the FXML tree or
if all the includes using references specify a resources attribute, the argument is not created.
The type of the constructor arguments will either be the controller instance or the controller factory (a function of
fields map -> controller).
The resource bundle argument will either be the resource bundle instance, the resource bundle name or a function of
string ->
string.
The smallest constructor will have only one argument: The controller (or controller factory).
## Maven Plugin
### Parameters
- output-directory
-
- The output directory of the generated classes
-
- default: `${project.build.directory}/generated-sources/java`)
- target-version
-
- The target Java version for the generated code
- default: `21`
- minimum: `8`
- File an issue if the generated code is not compatible with the target version
- use-image-inputstream-constructor
-
- Use the InputStream constructor for Image instead of the String (URL) one.
-
- default: `true`
- Disables background loading
- field-injection
-
- The type of field injections to use (see [Field injection](#field-injection))
- default: `REFLECTION`
- method-injection
-
- The type of method injections to use (see [Method injection](#method-injection))
- default: `REFLECTION`
- bundle-injection
-
- The type of resource bundle injection to use (see [Resource bundle injection](#resource-bundle-injection))
- default: `CONSTRUCTOR`
- bundle-map
-
- A map of resource bundle name to resource bundle path
- Used with `GET-BUNDLE` injection
- default: `{}`
### Limitations
- Given that the plugin operates during the `generate-sources` phase, it doesn't have access to the classes of the
application.
-
- The controller info (fields, methods) is obtained from the source file and may therefore be inaccurate.
-
- Custom classes instantiated in the FXML files are not available during generation and may therefore cause it to
fail.
- If the application uses e.g. WebView, the javafx-web dependency must be added to the plugin dependencies.