Compare commits

...

10 Commits

Author SHA1 Message Date
Guillaume Tâche 1fc5a11956 Updates minimum maven version 2025-01-07 08:22:12 +01:00
Guillaume Tâche a26f78cbab Fixes typo in README 2025-01-05 19:22:51 +01:00
Guillaume Tâche 0fc244de88 Adds info about release status to README 2025-01-05 19:20:38 +01:00
Guillaume Tâche 6735d9aa8c Changes sign keyname 2025-01-04 19:51:51 +01:00
Guillaume Tâche f59863c628 Adds gpg, sonatype, changes groupId 2025-01-03 22:08:50 +01:00
Guillaume Tâche c3d9e72e42 Adds BindingFormatter, ExpressionFormatter, ValueClassGuesser, fixes constructor args matching, fixes factory methods 2024-12-30 23:22:33 +01:00
Guillaume Tâche b5c38bea54 Finishes testing, cleanup, adds license 2024-12-28 17:25:33 +01:00
Guillaume Tâche 58fbbff7cb Adds maven wrapper ; rework internal a bit ; Adds all tests for core 2024-12-25 22:40:01 +01:00
Guillaume Tâche 38056c43f7 Adds HelperProvider, starts testing internal classes 2024-12-14 22:02:07 +01:00
Guillaume Tâche d63188e8ee Fixes generics, only creates one constructor, adds some tests, adds java compatibility options, rework some classes, fixes some problems 2024-12-13 21:20:49 +01:00
285 changed files with 13050 additions and 20382 deletions
+19
View File
@@ -0,0 +1,19 @@
# 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.
wrapperVersion=3.3.2
distributionType=only-script
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip
+9
View File
@@ -0,0 +1,9 @@
The MIT License (MIT)
Copyright © 2024 <copyright holders>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+223 -1
View File
@@ -1 +1,223 @@
# FXML Compiler
# FXML Compiler
## The project is not yet published to Maven Central.
## Introduction
This project aims at generating Java code from FXML files.
## Requirements
- Java 21+ for the plugin
- The generated code can be compatible with older java versions.
- Maven 3.9.9+ (Wrapper included)
## Installation
Add the plugin to your project:
```xml
<build>
<plugins>
<plugin>
<groupId>ch.gtache.fxml-compiler</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>ch.gtache.fxml-compiler</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
- Very basic support for bidirectional bindings
- 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
- `fx:script` is not supported
- Expect some bugs (file an issue if you see one)
- Expression binding support is very basic
- Probably not fully compatible with all FXML features (file an issue if you need one in specific)
- All fxml files must have a `fx:controller` attribute
## Parameters
### Controller injection
There are two ways to inject controllers into a view:
- `INSTANCE`: Inject the controller instance
- This is the default injection method
- `FACTORY`: Inject the controller factory
- This injection method is required if the FXML tree contains multiple times the same controller class.
- By default, the factory is a `Supplier<Controller>`, but if used in conjunction with `field-injection`set to
`FACTORY`, the factory is a `Function<Map<String, Object>, Controller>`.
### Field injection
There are four ways to inject fields into a controller:
- `REFLECTION`: Inject fields using reflection (like FXMLLoader)
- 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.apply(fieldMap)`
- `factory` is a `Function<Map<String, Object>, Controller>` 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.
- This also forces the `controller-injection` method to be `FACTORY`.
### Method injection
There are two ways to inject methods (meaning use them as event handlers) into a controller:
- `REFLECTION`: Inject methods using reflection (like FXMLLoader)
- 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 five ways to inject resource bundles into a controller:
- `CONSTRUCTOR`: Inject resource bundle in the view constructor
- `view = new View(controller, resourceBundle)`
- This is the default injection method.
- `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.
- `CONSTRUCTOR_NAME`: Injects the resource bundle name in the view constructor
- `ResourceBundle.getBundle(bundleName)`
- Just for the convenience of not having to create the resource bundle instance outside the view.
- `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).
- **This ignores the `resources` attribute of fx:include.**
- `GET-BUNDLE`: Retrieves the resource bundle using a resource path
- The resource path is passed to the generator (see [Maven Plugin](#maven-plugin)).
- The resource path will therefore be a constant in the view class.
- **This ignores the `resources` attribute of fx:include.**
## 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) plus
potentially the resource bundle if necessary. If no resource reference (`%key.to.resource`) is found in the FXML tree or
if all the fx: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.
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
- controller-injection
- The type of controller injections to use (see [Controller injection](#controller-injection))
- default: `INSTANCE`
- 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: `{}`
- parallelism
- The number of threads to use for compilation
- default: `1` (no multithreading)
- if `<1`, the number of available cores will be used
### 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.
- These classes must therefore be in a separate dependency.
- If the application uses e.g. WebView, the javafx-web dependency must be added to the plugin dependencies.
+3 -1
View File
@@ -4,11 +4,13 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.github.gtache</groupId>
<groupId>ch.gtache.fxml-compiler</groupId>
<artifactId>fxml-compiler</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>fxml-compiler-api</artifactId>
<name>fxml-compiler-api</name>
<description>API module for the fxml-compiler project</description>
</project>
@@ -1,4 +1,4 @@
package com.github.gtache.fxml.compiler;
package ch.gtache.fxml.compiler;
import java.util.List;
@@ -28,5 +28,5 @@ public interface ControllerFieldInfo {
*
* @return The generic types as a list, empty if not generic or raw
*/
List<String> genericTypes();
List<GenericTypes> genericTypes();
}
@@ -0,0 +1,23 @@
package ch.gtache.fxml.compiler;
/**
* Base field {@link InjectionType}s
*/
public enum ControllerFieldInjectionType implements InjectionType {
/**
* Inject using variable assignment
*/
ASSIGN,
/**
* Inject using a factory
*/
FACTORY,
/**
* Inject using reflection
*/
REFLECTION,
/**
* Inject using setters
*/
SETTERS
}
@@ -1,4 +1,4 @@
package com.github.gtache.fxml.compiler;
package ch.gtache.fxml.compiler;
import java.util.Map;
@@ -7,10 +7,17 @@ import java.util.Map;
*/
public interface ControllerInfo {
/**
* Returns the controller class name
*
* @return The name
*/
String className();
/**
* Returns a mapping of event handler method name -> boolean
*
* @return A mapping of method name to true if the method has an argument
* @return A mapping of method name to true if the method has an argument (event handler may not have an argument)
*/
Map<String, Boolean> handlerHasArgument();
@@ -18,7 +25,7 @@ public interface ControllerInfo {
* Returns whether the given event handler method has an argument
*
* @param methodName The method name
* @return A mapping of method name to true if the method has an event
* @return True if the method has an argument
*/
default boolean handlerHasArgument(final String methodName) {
return handlerHasArgument().getOrDefault(methodName, true);
@@ -40,4 +47,11 @@ public interface ControllerInfo {
default ControllerFieldInfo fieldInfo(final String property) {
return fieldInfo().get(property);
}
/**
* Returns whether the controller has an initialize method
*
* @return True if the controller has an initialize method
*/
boolean hasInitialize();
}
@@ -0,0 +1,15 @@
package ch.gtache.fxml.compiler;
/**
* Base controller {@link InjectionType}s
*/
public enum ControllerInjectionType implements InjectionType {
/**
* Inject the controller instance
*/
INSTANCE,
/**
* Inject a controller factory
*/
FACTORY
}
@@ -0,0 +1,15 @@
package ch.gtache.fxml.compiler;
/**
* Base methods {@link InjectionType}s
*/
public enum ControllerMethodsInjectionType implements InjectionType {
/**
* Inject using visible methods
*/
REFERENCE,
/**
* Inject using reflection
*/
REFLECTION,
}
@@ -1,12 +1,13 @@
package com.github.gtache.fxml.compiler;
package ch.gtache.fxml.compiler;
/**
* Exception thrown when a generation error occurs
*/
@SuppressWarnings("serial")
public class GenerationException extends Exception {
/**
* Instantiates a new GenerationException
* Instantiates a new exception
*
* @param message The message
*/
@@ -15,7 +16,7 @@ public class GenerationException extends Exception {
}
/**
* Instantiates a new GenerationException
* Instantiates a new exception
*
* @param message The message
* @param cause The cause
@@ -25,7 +26,7 @@ public class GenerationException extends Exception {
}
/**
* Instantiates a new GenerationException
* Instantiates a new exception
*
* @param cause The cause
*/
@@ -0,0 +1,61 @@
package ch.gtache.fxml.compiler;
import ch.gtache.fxml.compiler.compatibility.GenerationCompatibility;
import java.util.Map;
/**
* Parameters for FXML generation
*/
public interface GenerationParameters {
/**
* Returns the compatibility information
*
* @return The compatibility
*/
GenerationCompatibility compatibility();
/**
* Returns whether to use Image InputStream constructor instead of the String (url) one.
* This allows avoiding opening some packages with JPMS
*
* @return True if the constructor should be used
*/
boolean useImageInputStreamConstructor();
/**
* Returns the mapping of controller class to resource bundle path (in case of GET-BUNDLE injection)
*
* @return The map
*/
Map<String, String> bundleMap();
/**
* Returns the controller injection to use
*
* @return The injection
*/
ControllerInjectionType controllerInjectionType();
/**
* Returns the field injection to use
*
* @return The injection
*/
ControllerFieldInjectionType fieldInjectionType();
/**
* Returns the method injection to use
*
* @return The injection
*/
ControllerMethodsInjectionType methodInjectionType();
/**
* Returns the resource injection to use
*
* @return The injection
*/
ResourceBundleInjectionType resourceInjectionType();
}
@@ -1,6 +1,6 @@
package com.github.gtache.fxml.compiler;
package ch.gtache.fxml.compiler;
import com.github.gtache.fxml.compiler.parsing.ParsedObject;
import ch.gtache.fxml.compiler.parsing.ParsedObject;
/**
* Represents a request for a code generation
@@ -21,6 +21,13 @@ public interface GenerationRequest {
*/
GenerationParameters parameters();
/**
* Returns the info about the main source file
*
* @return The info
*/
SourceInfo sourceInfo();
/**
* Returns the object to generate code for
*
@@ -1,4 +1,4 @@
package com.github.gtache.fxml.compiler;
package ch.gtache.fxml.compiler;
/**
* Generates compiled FXML code
@@ -0,0 +1,23 @@
package ch.gtache.fxml.compiler;
import java.util.List;
/**
* Represents generic types for a field
*/
public interface GenericTypes {
/**
* Returns the name of the type
*
* @return The name
*/
String name();
/**
* Returns the possible subtypes of the type
*
* @return The list of subtypes, empty if no subtypes
*/
List<GenericTypes> subTypes();
}
@@ -1,4 +1,4 @@
package com.github.gtache.fxml.compiler;
package ch.gtache.fxml.compiler;
/**
* A type of injection for controllers
@@ -0,0 +1,27 @@
package ch.gtache.fxml.compiler;
/**
* Base {@link InjectionType}s for resource bundles
*/
public enum ResourceBundleInjectionType implements InjectionType {
/**
* Resource bundle is injected in the constructor
*/
CONSTRUCTOR,
/**
* Resource bundle is injected as a function in the constructor
*/
CONSTRUCTOR_FUNCTION,
/**
* Resource bundle name is injected in the constructor
*/
CONSTRUCTOR_NAME,
/**
* Resource bundle is loaded using getBundle
*/
GET_BUNDLE,
/**
* Resource bundle is retrieved from controller using getter
*/
GETTER
}
@@ -0,0 +1,54 @@
package ch.gtache.fxml.compiler;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
/**
* Info about a source file
*/
public interface SourceInfo {
/**
* Returns the generated view class name
*
* @return The class name
*/
String generatedClassName();
/**
* Returns the controller class name
*
* @return The class name
*/
String controllerClassName();
/**
* Returns the source file
*
* @return The file
*/
Path sourceFile();
/**
* Returns the included sources.
* Note that there can be multiple times the same source.
*
* @return The sources
*/
List<SourceInfo> includedSources();
/**
* Returns the mapping of source value to source info
*
* @return The mapping
*/
Map<String, SourceInfo> sourceToSourceInfo();
/**
* Returns whether the source or its children requires a resource bundle
*
* @return True if the subtree requires a resource bundle
*/
boolean requiresResourceBundle();
}
@@ -0,0 +1,57 @@
package ch.gtache.fxml.compiler.compatibility;
/**
* Compatibility information for generated code
*/
@FunctionalInterface
public interface GenerationCompatibility {
/**
* Returns the minimum supported Java version
*
* @return The version
*/
int javaVersion();
/**
* Returns whether to use var for object declaration
*
* @return True if var should be used
*/
default boolean useVar() {
return javaVersion() >= 10;
}
/**
* Returns the type of list collector to use
*
* @return The collector
*/
default ListCollector listCollector() {
if (javaVersion() >= 16) {
return ListCollector.TO_LIST;
} else if (javaVersion() >= 10) {
return ListCollector.COLLECT_TO_UNMODIFIABLE_LIST;
} else {
return ListCollector.COLLECT_TO_LIST;
}
}
/**
* Returns whether to use List.of() (or Set.of() etc.) instead of Arrays.asList()
*
* @return True if List.of() should be used
*/
default boolean useCollectionsOf() {
return javaVersion() >= 9;
}
/**
* Returns whether to use getFirst() or get(0)
*
* @return True if getFirst() should be used
*/
default boolean useGetFirst() {
return javaVersion() >= 21;
}
}
@@ -0,0 +1,22 @@
package ch.gtache.fxml.compiler.compatibility;
/**
* Type of list collector to use for generated code
*/
public enum ListCollector {
/**
* Use .toList()
*/
TO_LIST,
/**
* Use .collect(Collectors.toUnmodifiableList())
*/
COLLECT_TO_UNMODIFIABLE_LIST,
/**
* Use .collect(Collectors.toList())
*/
COLLECT_TO_LIST
}
@@ -1,4 +1,4 @@
package com.github.gtache.fxml.compiler.parsing;
package ch.gtache.fxml.compiler.parsing;
import java.io.IOException;
import java.nio.file.Files;
@@ -1,12 +1,13 @@
package com.github.gtache.fxml.compiler.parsing;
package ch.gtache.fxml.compiler.parsing;
/**
* Exception thrown when a parsing error occurs
*/
@SuppressWarnings("serial")
public class ParseException extends Exception {
/**
* Instantiates a new ParseException
* Instantiates a new exception
*
* @param message The message
*/
@@ -15,7 +16,7 @@ public class ParseException extends Exception {
}
/**
* Instantiates a new ParseException
* Instantiates a new exception
*
* @param message The message
* @param cause The cause
@@ -25,7 +26,7 @@ public class ParseException extends Exception {
}
/**
* Instantiates a new ParseException
* Instantiates a new exception
*
* @param cause The cause
*/
@@ -1,4 +1,4 @@
package com.github.gtache.fxml.compiler.parsing;
package ch.gtache.fxml.compiler.parsing;
import java.util.LinkedHashMap;
import java.util.List;
@@ -1,4 +1,4 @@
package com.github.gtache.fxml.compiler.parsing;
package ch.gtache.fxml.compiler.parsing;
import java.util.LinkedHashMap;
import java.util.List;
@@ -1,5 +1,6 @@
package com.github.gtache.fxml.compiler.parsing;
package ch.gtache.fxml.compiler.parsing;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.SequencedCollection;
import java.util.SequencedMap;
@@ -10,30 +11,18 @@ import java.util.SequencedMap;
@FunctionalInterface
public interface ParsedDefine extends ParsedObject {
/**
* Returns the object defined by this fx:define
*
* @return The object
*/
ParsedObject object();
@Override
default String className() {
return object().className();
return ParsedDefine.class.getName();
}
@Override
default Map<String, ParsedProperty> attributes() {
return object().attributes();
return Map.of();
}
@Override
default SequencedMap<ParsedProperty, SequencedCollection<ParsedObject>> properties() {
return object().properties();
}
@Override
default SequencedCollection<ParsedObject> children() {
return object().children();
return new LinkedHashMap<>();
}
}
@@ -1,4 +1,4 @@
package com.github.gtache.fxml.compiler.parsing;
package ch.gtache.fxml.compiler.parsing;
import java.util.LinkedHashMap;
import java.util.SequencedCollection;
@@ -29,7 +29,8 @@ public interface ParsedFactory extends ParsedObject {
}
/**
* Returns the arguments for the factory
* Returns the arguments for the factory.
* Different from {@link ParsedObject#children()} (in practice, children should only contain fx:define)
*
* @return The arguments
*/
@@ -1,4 +1,4 @@
package com.github.gtache.fxml.compiler.parsing;
package ch.gtache.fxml.compiler.parsing;
import java.util.LinkedHashMap;
import java.util.List;
@@ -1,4 +1,4 @@
package com.github.gtache.fxml.compiler.parsing;
package ch.gtache.fxml.compiler.parsing;
import java.util.Map;
import java.util.SequencedCollection;
@@ -10,7 +10,7 @@ import java.util.SequencedMap;
public interface ParsedObject {
/**
* The type of the object
* Returns the type of the object
*
* @return The class name
*/
@@ -1,4 +1,4 @@
package com.github.gtache.fxml.compiler.parsing;
package ch.gtache.fxml.compiler.parsing;
/**
* Parsed property/attribute from FXML
@@ -1,4 +1,4 @@
package com.github.gtache.fxml.compiler.parsing;
package ch.gtache.fxml.compiler.parsing;
import java.util.LinkedHashMap;
import java.util.List;
@@ -1,4 +1,4 @@
package com.github.gtache.fxml.compiler.parsing;
package ch.gtache.fxml.compiler.parsing;
import java.util.LinkedHashMap;
import java.util.List;
@@ -21,7 +21,7 @@ public interface ParsedText extends ParsedObject {
@Override
default String className() {
return "java.lang.String";
return String.class.getName();
}
@Override
@@ -1,4 +1,4 @@
package com.github.gtache.fxml.compiler.parsing;
package ch.gtache.fxml.compiler.parsing;
import java.util.LinkedHashMap;
import java.util.List;
@@ -1,20 +0,0 @@
package com.github.gtache.fxml.compiler;
import java.util.Map;
/**
* Factory for creating controllers
*
* @param <T> The type of the controller
*/
@FunctionalInterface
public interface ControllerFactory<T> {
/**
* Creates a controller
*
* @param fieldMap The assignment of field name to value
* @return The created controller
*/
T create(final Map<String, Object> fieldMap);
}
@@ -1,28 +0,0 @@
package com.github.gtache.fxml.compiler;
/**
* Represents a controller injection to use for generated code
*/
public interface ControllerInjection {
/**
* Returns the injection type of class fields
*
* @return The injection type for fields
*/
InjectionType fieldInjectionType();
/**
* Returns the injection type for event handlers methods
*
* @return The injection type for event handlers
*/
InjectionType methodInjectionType();
/**
* The name of the controller class
*
* @return The class
*/
String injectionClass();
}
@@ -1,38 +0,0 @@
package com.github.gtache.fxml.compiler;
import java.util.Map;
/**
* Parameters for FXML generation
*/
public interface GenerationParameters {
/**
* Returns the mapping of controller class name to controller injection
*
* @return The mapping
*/
Map<String, ControllerInjection> controllerInjections();
/**
* Returns the mapping of fx:include source to generated class name
*
* @return The mapping
*/
Map<String, String> sourceToGeneratedClassName();
/**
* Returns the mapping of fx:include source to controller class name
*
* @return The mapping
*/
Map<String, String> sourceToControllerName();
/**
* Returns the resource bundle injection to use
*
* @return The injection
*/
ResourceBundleInjection resourceBundleInjection();
}
@@ -1,21 +0,0 @@
package com.github.gtache.fxml.compiler;
/**
* Represents a controller injection to use for generated code
*/
public interface ResourceBundleInjection {
/**
* Returns the injection type for the resource bundle
*
* @return The injection type
*/
InjectionType injectionType();
/**
* Returns the resource bundle name
*
* @return The path
*/
String bundleName();
}
+4 -3
View File
@@ -1,7 +1,8 @@
/**
* API module for FXML compiler
*/
module com.github.gtache.fxml.compiler.api {
exports com.github.gtache.fxml.compiler;
exports com.github.gtache.fxml.compiler.parsing;
module ch.gtache.fxml.compiler.api {
exports ch.gtache.fxml.compiler;
exports ch.gtache.fxml.compiler.compatibility;
exports ch.gtache.fxml.compiler.parsing;
}
@@ -1,4 +1,4 @@
package com.github.gtache.fxml.compiler;
package ch.gtache.fxml.compiler;
import org.junit.jupiter.api.Test;
@@ -6,8 +6,7 @@ import java.util.List;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.*;
class TestControllerFieldInfo {
@@ -25,7 +24,7 @@ class TestControllerFieldInfo {
@Test
void testIsGenericTrue() {
when(info.genericTypes()).thenReturn(List.of("A", "B", "C"));
when(info.genericTypes()).thenReturn(List.of(mock(GenericTypes.class)));
assertTrue(info.isGeneric());
}
}
@@ -1,4 +1,4 @@
package com.github.gtache.fxml.compiler;
package ch.gtache.fxml.compiler;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -0,0 +1,53 @@
package ch.gtache.fxml.compiler;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Objects;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class TestGenerationException {
private final String message;
private final Throwable throwable;
private final String throwableString;
TestGenerationException(@Mock final Throwable throwable) {
this.message = "message";
this.throwable = Objects.requireNonNull(throwable);
this.throwableString = "throwable";
}
@BeforeEach
void beforeEach() {
when(throwable.toString()).thenReturn(throwableString);
}
@Test
void testOnlyMessage() {
final var exception = new GenerationException(message);
assertEquals(message, exception.getMessage());
assertNull(exception.getCause());
}
@Test
void testOnlyCause() {
final var exception = new GenerationException(throwable);
assertEquals(throwableString, exception.getMessage());
assertEquals(throwable, exception.getCause());
}
@Test
void testMessageAndCause() {
final var exception = new GenerationException(message, throwable);
assertEquals(message, exception.getMessage());
assertEquals(throwable, exception.getCause());
}
}
@@ -0,0 +1,72 @@
package ch.gtache.fxml.compiler.compatibility;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
class TestGenerationCompatibility {
private final GenerationCompatibility compatibility;
TestGenerationCompatibility() {
this.compatibility = spy(GenerationCompatibility.class);
}
@Test
void testUseVar() {
when(compatibility.javaVersion()).thenReturn(10);
assertTrue(compatibility.useVar());
}
@Test
void testDontUseVar() {
when(compatibility.javaVersion()).thenReturn(9);
assertFalse(compatibility.useVar());
}
@Test
void testToListCollector() {
when(compatibility.javaVersion()).thenReturn(16);
assertEquals(ListCollector.TO_LIST, compatibility.listCollector());
}
@Test
void testCollectToUnmodifiableListCollector() {
when(compatibility.javaVersion()).thenReturn(15);
assertEquals(ListCollector.COLLECT_TO_UNMODIFIABLE_LIST, compatibility.listCollector());
when(compatibility.javaVersion()).thenReturn(10);
assertEquals(ListCollector.COLLECT_TO_UNMODIFIABLE_LIST, compatibility.listCollector());
}
@Test
void testCollectToListCollector() {
when(compatibility.javaVersion()).thenReturn(9);
assertEquals(ListCollector.COLLECT_TO_LIST, compatibility.listCollector());
}
@Test
void testUseCollectionsOf() {
when(compatibility.javaVersion()).thenReturn(9);
assertTrue(compatibility.useCollectionsOf());
}
@Test
void testDontUseCollectionsOf() {
when(compatibility.javaVersion()).thenReturn(8);
assertFalse(compatibility.useCollectionsOf());
}
@Test
void testUseGetFirst() {
when(compatibility.javaVersion()).thenReturn(21);
assertTrue(compatibility.useGetFirst());
}
@Test
void testDontUseGetFirst() {
when(compatibility.javaVersion()).thenReturn(20);
assertFalse(compatibility.useGetFirst());
}
}
@@ -1,4 +1,4 @@
package com.github.gtache.fxml.compiler.parsing;
package ch.gtache.fxml.compiler.parsing;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -7,7 +7,7 @@ import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.Path;
import static java.util.Objects.requireNonNull;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -46,7 +46,7 @@ class TestFXMLParser {
@Test
void testParseIOException() throws Exception {
final var file = Paths.get("whatever");
final var file = Path.of("whatever");
assertThrows(ParseException.class, () -> parser.parse(file));
verify(parser, never()).parse(content);
}
@@ -0,0 +1,53 @@
package ch.gtache.fxml.compiler.parsing;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Objects;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class TestParseException {
private final String message;
private final Throwable throwable;
private final String throwableString;
TestParseException(@Mock final Throwable throwable) {
this.message = "message";
this.throwable = Objects.requireNonNull(throwable);
this.throwableString = "throwable";
}
@BeforeEach
void beforeEach() {
when(throwable.toString()).thenReturn(throwableString);
}
@Test
void testOnlyMessage() {
final var exception = new ParseException(message);
assertEquals(message, exception.getMessage());
assertNull(exception.getCause());
}
@Test
void testOnlyCause() {
final var exception = new ParseException(throwable);
assertEquals(throwableString, exception.getMessage());
assertEquals(throwable, exception.getCause());
}
@Test
void testMessageAndCause() {
final var exception = new ParseException(message, throwable);
assertEquals(message, exception.getMessage());
assertEquals(throwable, exception.getCause());
}
}
@@ -1,4 +1,4 @@
package com.github.gtache.fxml.compiler.parsing;
package ch.gtache.fxml.compiler.parsing;
import org.junit.jupiter.api.BeforeEach;
@@ -1,4 +1,4 @@
package com.github.gtache.fxml.compiler.parsing;
package ch.gtache.fxml.compiler.parsing;
import org.junit.jupiter.api.BeforeEach;
@@ -0,0 +1,34 @@
package ch.gtache.fxml.compiler.parsing;
import org.junit.jupiter.api.Test;
import java.util.LinkedHashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.spy;
class TestParsedDefine {
private final ParsedDefine define;
TestParsedDefine() {
this.define = spy(ParsedDefine.class);
}
@Test
void testClassName() {
assertEquals(ParsedDefine.class.getName(), define.className());
}
@Test
void testAttributes() {
assertEquals(Map.of(), define.attributes());
}
@Test
void testProperties() {
assertEquals(new LinkedHashMap<>(), define.properties());
}
}
@@ -1,4 +1,4 @@
package com.github.gtache.fxml.compiler.parsing;
package ch.gtache.fxml.compiler.parsing;
import org.junit.jupiter.api.BeforeEach;
@@ -1,4 +1,4 @@
package com.github.gtache.fxml.compiler.parsing;
package ch.gtache.fxml.compiler.parsing;
import org.junit.jupiter.api.BeforeEach;
@@ -1,4 +1,4 @@
package com.github.gtache.fxml.compiler.parsing;
package ch.gtache.fxml.compiler.parsing;
import org.junit.jupiter.api.BeforeEach;
@@ -1,4 +1,4 @@
package com.github.gtache.fxml.compiler.parsing;
package ch.gtache.fxml.compiler.parsing;
import org.junit.jupiter.api.Test;
@@ -1,4 +1,4 @@
package com.github.gtache.fxml.compiler.parsing;
package ch.gtache.fxml.compiler.parsing;
import org.junit.jupiter.api.BeforeEach;
@@ -1,80 +0,0 @@
package com.github.gtache.fxml.compiler.parsing;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.SequencedCollection;
import static java.util.Objects.requireNonNull;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class TestParsedDefine {
private final ParsedProperty property;
private final ParsedObject object;
private final String string;
private final ParsedDefine define;
TestParsedDefine(@Mock final ParsedProperty property, @Mock final ParsedObject object) {
this.property = requireNonNull(property);
this.object = requireNonNull(object);
this.string = "str/ing";
this.define = spy(ParsedDefine.class);
}
@BeforeEach
void beforeEach() {
when(property.value()).thenReturn(string);
when(define.object()).thenReturn(object);
when(object.className()).thenReturn(string);
when(object.children()).thenReturn(List.of(define));
final var map = new LinkedHashMap<ParsedProperty, SequencedCollection<ParsedObject>>();
map.put(property, List.of(object));
when(object.properties()).thenReturn(map);
when(object.attributes()).thenReturn(Map.of(string, property));
}
@Test
void testObject() {
assertEquals(object, define.object());
}
@Test
void testClassName() {
assertEquals(string, define.className());
verify(define).object();
verify(object).className();
}
@Test
void testAttributes() {
assertEquals(Map.of(string, property), define.attributes());
verify(define).object();
verify(object).attributes();
}
@Test
void testProperties() {
final var map = new LinkedHashMap<ParsedProperty, SequencedCollection<ParsedObject>>();
map.put(property, List.of(object));
assertEquals(map, define.properties());
verify(define).object();
verify(object).properties();
}
@Test
void testChildren() {
assertEquals(List.of(define), define.children());
verify(define).object();
verify(object).children();
}
}
+5 -2
View File
@@ -4,16 +4,19 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.github.gtache</groupId>
<groupId>ch.gtache.fxml-compiler</groupId>
<artifactId>fxml-compiler</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>fxml-compiler-core</artifactId>
<name>fxml-compiler-core</name>
<description>Core module for the fxml-compiler project</description>
<dependencies>
<dependency>
<groupId>com.github.gtache</groupId>
<groupId>ch.gtache.fxml-compiler</groupId>
<artifactId>fxml-compiler-api</artifactId>
</dependency>
<dependency>
@@ -0,0 +1,22 @@
package ch.gtache.fxml.compiler.compatibility.impl;
import ch.gtache.fxml.compiler.compatibility.GenerationCompatibility;
/**
* Implementation of {@link GenerationCompatibility}
*
* @param javaVersion The minimum supported Java version
*/
public record GenerationCompatibilityImpl(int javaVersion) implements GenerationCompatibility {
/**
* Instantiates a new compatibility
*
* @param javaVersion The minimum supported Java version
*/
public GenerationCompatibilityImpl {
if (javaVersion < 8) {
throw new IllegalArgumentException("Java version must be at least 8");
}
}
}
@@ -1,11 +1,10 @@
package com.github.gtache.fxml.compiler.impl;
package ch.gtache.fxml.compiler.impl;
import java.io.IOException;
import java.net.URI;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
@@ -43,12 +42,12 @@ public final class ClassesFinder {
final var file = resource.getFile();
if (file.contains(".jar!")) {
final var jarFile = file.substring(0, file.indexOf(".jar!") + 4);
try (final var fs = FileSystems.newFileSystem(Paths.get(URI.create(jarFile)), classLoader)) {
try (final var fs = FileSystems.newFileSystem(Path.of(URI.create(jarFile)), classLoader)) {
classes.addAll(findClasses(fs.getPath(path), packageName));
}
} else {
final var filepath = START_FILE_PATTERN.matcher(file).replaceAll("");
classes.addAll(findClasses(Paths.get(filepath), packageName));
classes.addAll(findClasses(Path.of(filepath), packageName));
}
}
return classes;
@@ -1,6 +1,7 @@
package com.github.gtache.fxml.compiler.impl;
package ch.gtache.fxml.compiler.impl;
import com.github.gtache.fxml.compiler.ControllerFieldInfo;
import ch.gtache.fxml.compiler.ControllerFieldInfo;
import ch.gtache.fxml.compiler.GenericTypes;
import java.util.List;
import java.util.Objects;
@@ -11,7 +12,7 @@ import java.util.Objects;
* @param name The field name
* @param genericTypes The generic types
*/
public record ControllerFieldInfoImpl(String name, List<String> genericTypes) implements ControllerFieldInfo {
public record ControllerFieldInfoImpl(String name, List<GenericTypes> genericTypes) implements ControllerFieldInfo {
/**
* Instantiates a new info
@@ -0,0 +1,35 @@
package ch.gtache.fxml.compiler.impl;
import ch.gtache.fxml.compiler.ControllerFieldInfo;
import ch.gtache.fxml.compiler.ControllerInfo;
import java.util.Map;
import java.util.Objects;
/**
* Implementation of {@link ControllerInfo}
*
* @param className The controller class name
* @param handlerHasArgument The mapping of method name to true if the method has an argument
* @param fieldInfo The mapping of property name to controller field info
* @param hasInitialize True if the controller has an initialize method
*/
public record ControllerInfoImpl(String className, Map<String, Boolean> handlerHasArgument,
Map<String, ControllerFieldInfo> fieldInfo,
boolean hasInitialize) implements ControllerInfo {
/**
* Instantiates a new controller info
*
* @param className The controller class name
* @param handlerHasArgument The mapping of method name to true if the method has an argument
* @param fieldInfo The mapping of property name to controller field info
* @param hasInitialize True if the controller has an initialize method
* @throws NullPointerException If any parameter is null
*/
public ControllerInfoImpl {
Objects.requireNonNull(className);
handlerHasArgument = Map.copyOf(handlerHasArgument);
fieldInfo = Map.copyOf(fieldInfo);
}
}
@@ -0,0 +1,53 @@
package ch.gtache.fxml.compiler.impl;
import ch.gtache.fxml.compiler.ControllerFieldInjectionType;
import ch.gtache.fxml.compiler.ControllerInjectionType;
import ch.gtache.fxml.compiler.ControllerMethodsInjectionType;
import ch.gtache.fxml.compiler.GenerationParameters;
import ch.gtache.fxml.compiler.ResourceBundleInjectionType;
import ch.gtache.fxml.compiler.compatibility.GenerationCompatibility;
import java.util.Map;
import static java.util.Objects.requireNonNull;
/**
* Implementation of {@link GenerationParameters}
*
* @param compatibility The compatibility info
* @param useImageInputStreamConstructor True if the InputStream constructor should be used
* @param bundleMap The mapping of controller class to resource bundle path
* @param controllerInjectionType The controller injection type
* @param fieldInjectionType The field injection type
* @param methodInjectionType The method injection type
* @param resourceInjectionType The resource injection type
*/
public record GenerationParametersImpl(GenerationCompatibility compatibility, boolean useImageInputStreamConstructor,
Map<String, String> bundleMap,
ControllerInjectionType controllerInjectionType,
ControllerFieldInjectionType fieldInjectionType,
ControllerMethodsInjectionType methodInjectionType,
ResourceBundleInjectionType resourceInjectionType) implements GenerationParameters {
/**
* Instantiates new parameters
*
* @param compatibility The compatibility info
* @param useImageInputStreamConstructor True if the InputStream constructor should be used
* @param bundleMap The mapping of controller class to resource bundle path
* @param controllerInjectionType The controller injection type
* @param fieldInjectionType The field injection type
* @param methodInjectionType The method injection type
* @param resourceInjectionType The resource injection type
* @throws NullPointerException if any parameter is null
*/
public GenerationParametersImpl {
requireNonNull(compatibility);
bundleMap = Map.copyOf(bundleMap);
requireNonNull(controllerInjectionType);
requireNonNull(fieldInjectionType);
requireNonNull(methodInjectionType);
requireNonNull(resourceInjectionType);
}
}
@@ -0,0 +1,40 @@
package ch.gtache.fxml.compiler.impl;
import ch.gtache.fxml.compiler.ControllerInfo;
import ch.gtache.fxml.compiler.GenerationParameters;
import ch.gtache.fxml.compiler.GenerationRequest;
import ch.gtache.fxml.compiler.SourceInfo;
import ch.gtache.fxml.compiler.parsing.ParsedObject;
import java.util.Objects;
/**
* Implementation of {@link GenerationRequest}
*
* @param parameters The generation parameters
* @param controllerInfo The controller info
* @param sourceInfo The source info
* @param rootObject The root object
* @param outputClassName The output class name
*/
public record GenerationRequestImpl(GenerationParameters parameters, ControllerInfo controllerInfo,
SourceInfo sourceInfo, ParsedObject rootObject,
String outputClassName) implements GenerationRequest {
/**
* Instantiates a new request
*
* @param parameters The generation parameters
* @param controllerInfo The controller info
* @param sourceInfo The source info
* @param rootObject The root object
* @param outputClassName The output class name
* @throws NullPointerException If any parameter is null
*/
public GenerationRequestImpl {
Objects.requireNonNull(parameters);
Objects.requireNonNull(controllerInfo);
Objects.requireNonNull(sourceInfo);
Objects.requireNonNull(rootObject);
Objects.requireNonNull(outputClassName);
}
}
@@ -0,0 +1,78 @@
package ch.gtache.fxml.compiler.impl;
import ch.gtache.fxml.compiler.GenerationException;
import ch.gtache.fxml.compiler.GenerationRequest;
import ch.gtache.fxml.compiler.Generator;
import ch.gtache.fxml.compiler.impl.internal.GenerationProgress;
import ch.gtache.fxml.compiler.impl.internal.HelperProvider;
import java.util.Objects;
import java.util.function.Function;
/**
* Implementation of {@link Generator}
*/
public class GeneratorImpl implements Generator {
private final Function<GenerationProgress, HelperProvider> helperProviderFactory;
/**
* Instantiates a new generator
*/
public GeneratorImpl() {
this(HelperProvider::new);
}
/**
* Used for testing
*
* @param helperProviderFactory The helper provider factory
*/
GeneratorImpl(final Function<GenerationProgress, HelperProvider> helperProviderFactory) {
this.helperProviderFactory = Objects.requireNonNull(helperProviderFactory);
}
@Override
public String generate(final GenerationRequest request) throws GenerationException {
final var progress = new GenerationProgress(request);
final var helperProvider = helperProviderFactory.apply(progress);
final var className = request.outputClassName();
final var pkgName = className.substring(0, className.lastIndexOf('.'));
final var simpleClassName = className.substring(className.lastIndexOf('.') + 1);
final var controllerInjectionClass = request.controllerInfo().className();
final var sb = progress.stringBuilder();
sb.append("package ").append(pkgName).append(";\n");
sb.append("\n");
sb.append("/**\n");
sb.append(" * Generated code\n");
sb.append(" */\n");
sb.append("public final class ").append(simpleClassName).append(" {\n");
sb.append("\n");
helperProvider.getInitializationFormatter().formatFieldsAndConstructor();
sb.append("\n");
helperProvider.getLoadMethodFormatter().formatLoadMethod();
sb.append("\n");
helperProvider.getHelperMethodsFormatter().formatHelperMethods();
sb.append("\n");
formatControllerMethod(progress, controllerInjectionClass);
sb.append("}\n");
return sb.toString();
}
private static void formatControllerMethod(final GenerationProgress progress, final String controllerInjectionClass) {
final var sb = progress.stringBuilder();
sb.append(" /**\n");
sb.append(" * Returns the controller if available\n");
sb.append(" * @return The controller\n");
sb.append(" * @throws IllegalStateException If the view is not loaded\n");
sb.append(" */\n");
sb.append(" public ").append(controllerInjectionClass).append(" controller() {\n");
sb.append(" if (loaded) {\n");
sb.append(" return controller;\n");
sb.append(" } else {\n");
sb.append(" throw new IllegalStateException(\"Not loaded\");\n");
sb.append(" }\n");
sb.append(" }\n");
}
}
@@ -0,0 +1,27 @@
package ch.gtache.fxml.compiler.impl;
import ch.gtache.fxml.compiler.GenericTypes;
import java.util.List;
import java.util.Objects;
/**
* Implementation of {@link GenericTypes}
*
* @param name The name
* @param subTypes The subtypes
*/
public record GenericTypesImpl(String name, List<GenericTypes> subTypes) implements GenericTypes {
/**
* Instantiates a new generic types
*
* @param name The name
* @param subTypes The subtypes
* @throws NullPointerException if any parameter is null
*/
public GenericTypesImpl {
Objects.requireNonNull(name);
subTypes = List.copyOf(subTypes);
}
}
@@ -0,0 +1,43 @@
package ch.gtache.fxml.compiler.impl;
import ch.gtache.fxml.compiler.SourceInfo;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* Implementation of {@link SourceInfo}
*
* @param generatedClassName The generated class name
* @param controllerClassName The controller class name
* @param sourceFile The source file
* @param includedSources The included sources
* @param sourceToSourceInfo The mapping of source value to source info
* @param requiresResourceBundle True if the subtree requires a resource bundle
*/
public record SourceInfoImpl(String generatedClassName, String controllerClassName, Path sourceFile,
List<SourceInfo> includedSources,
Map<String, SourceInfo> sourceToSourceInfo,
boolean requiresResourceBundle) implements SourceInfo {
/**
* Instantiates a new source info
*
* @param generatedClassName The generated class name
* @param controllerClassName The controller class name
* @param sourceFile The source file
* @param includedSources The included sources
* @param sourceToSourceInfo The mapping of source value to source info
* @param requiresResourceBundle True if the subtree requires a resource bundle
* @throws NullPointerException If any parameter is null
*/
public SourceInfoImpl {
Objects.requireNonNull(generatedClassName);
Objects.requireNonNull(controllerClassName);
Objects.requireNonNull(sourceFile);
includedSources = List.copyOf(includedSources);
sourceToSourceInfo = Map.copyOf(sourceToSourceInfo);
}
}
@@ -0,0 +1,111 @@
package ch.gtache.fxml.compiler.impl.internal;
import ch.gtache.fxml.compiler.ControllerFieldInjectionType;
import ch.gtache.fxml.compiler.GenerationException;
import ch.gtache.fxml.compiler.parsing.ParsedObject;
import ch.gtache.fxml.compiler.parsing.ParsedProperty;
import javafx.beans.property.Property;
import javafx.beans.property.ReadOnlyProperty;
import java.util.Arrays;
import java.util.SequencedCollection;
import static ch.gtache.fxml.compiler.impl.internal.GenerationHelper.*;
import static java.util.Objects.requireNonNull;
/**
* Formatter for property bindings
*/
final class BindingFormatter {
private static final String PROPERTY = "Property";
private final HelperProvider helperProvider;
private final ControllerFieldInjectionType fieldInjectionType;
private final StringBuilder sb;
private final SequencedCollection<String> controllerFactoryPostAction;
BindingFormatter(final HelperProvider helperProvider, final ControllerFieldInjectionType fieldInjectionType,
final StringBuilder sb, final SequencedCollection<String> controllerFactoryPostAction) {
this.helperProvider = requireNonNull(helperProvider);
this.fieldInjectionType = requireNonNull(fieldInjectionType);
this.sb = requireNonNull(sb);
this.controllerFactoryPostAction = requireNonNull(controllerFactoryPostAction);
}
/**
* Formats a binding
*
* @param property The property
* @param parent The parent object
* @param parentVariable The parent variable
*/
void formatBinding(final ParsedProperty property, final ParsedObject parent, final String parentVariable) throws GenerationException {
final var value = property.value();
if (value.endsWith("}")) {
if (value.startsWith(BINDING_EXPRESSION_PREFIX)) {
formatSimpleBinding(property, parent, parentVariable);
} else if (value.startsWith(BIDIRECTIONAL_BINDING_PREFIX)) {
formatBidirectionalBinding(property, parent, parentVariable);
} else {
throw new GenerationException("Unknown binding : " + value);
}
} else {
throw new GenerationException("Invalid binding : " + value);
}
}
private void formatSimpleBinding(final ParsedProperty property, final ParsedObject parent, final String parentVariable) throws GenerationException {
formatBinding(property, parent, parentVariable, false);
}
private void formatBidirectionalBinding(final ParsedProperty property, final ParsedObject parent, final String parentVariable) throws GenerationException {
formatBinding(property, parent, parentVariable, true);
}
private void formatBinding(final ParsedProperty property, final ParsedObject parent, final String parentVariable, final boolean bidirectional) throws GenerationException {
final var name = property.name();
final var value = property.value();
final var className = parent.className();
final var methodName = name + PROPERTY;
final var bindMethod = bidirectional ? "bindBidirectional" : "bind";
if (bidirectional ? hasWriteProperty(className, methodName) : hasReadProperty(className, methodName)) {
final var returnType = ReflectionHelper.getReturnType(className, methodName);
final var expression = helperProvider.getExpressionFormatter().format(value, returnType);
final var binding = INDENT_8 + parentVariable + "." + methodName + "()." + bindMethod + "(" + expression + ");\n";
if (isControllerWithFactory(value)) {
controllerFactoryPostAction.add(binding);
} else {
sb.append(binding);
}
} else {
throw new GenerationException("Cannot bind " + name + " on " + className);
}
}
private boolean isControllerWithFactory(final String expression) {
final var cleaned = expression.substring(2, expression.length() - 1).trim();
final var split = Arrays.stream(cleaned.split("\\.")).filter(s -> !s.isEmpty()).toList();
if (split.size() == 2) {
final var referenced = split.getFirst();
if (referenced.equals("controller")) {
return fieldInjectionType == ControllerFieldInjectionType.FACTORY;
}
}
return false;
}
private static boolean hasReadProperty(final String className, final String methodName) throws GenerationException {
return isPropertyReturnType(className, methodName, ReadOnlyProperty.class);
}
private static boolean hasWriteProperty(final String className, final String methodName) throws GenerationException {
return isPropertyReturnType(className, methodName, Property.class);
}
private static boolean isPropertyReturnType(final String className, final String methodName, final Class<?> expectedReturnType) throws GenerationException {
final var returnType = ReflectionHelper.getReturnType(className, methodName);
return expectedReturnType.isAssignableFrom(returnType);
}
}
@@ -1,4 +1,4 @@
package com.github.gtache.fxml.compiler.impl.internal;
package ch.gtache.fxml.compiler.impl.internal;
import java.lang.reflect.Constructor;
import java.util.Collections;
@@ -0,0 +1,166 @@
package ch.gtache.fxml.compiler.impl.internal;
import ch.gtache.fxml.compiler.GenerationException;
import ch.gtache.fxml.compiler.impl.GeneratorImpl;
import ch.gtache.fxml.compiler.parsing.ParsedObject;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Helper methods for {@link GeneratorImpl} to handle objects constructors
*/
final class ConstructorHelper {
private ConstructorHelper() {
}
/**
* Gets the constructor arguments as a list of strings
*
* @param constructorArgs The constructor arguments
* @param parsedObject The parsed object
* @return The list of constructor arguments
* @throws GenerationException if an error occurs
*/
static List<String> getListConstructorArgs(final ConstructorArgs constructorArgs, final ParsedObject parsedObject) throws GenerationException {
final var args = new ArrayList<String>(constructorArgs.namedArgs().size());
for (final var entry : constructorArgs.namedArgs().entrySet()) {
final var parameter = entry.getValue();
final var type = parameter.type();
final var p = parsedObject.attributes().get(entry.getKey());
if (p == null) {
//Check if it is a complex property: If yes, throw an exception
final var c = parsedObject.properties().entrySet().stream().filter(e ->
e.getKey().name().equals(entry.getKey())).findFirst().orElse(null);
if (c == null) {
args.add(ValueFormatter.toString(parameter.defaultValue(), type));
} else {
throw new GenerationException("Constructor using complex property not supported yet");
}
} else {
args.add(ValueFormatter.toString(p.value(), type));
}
}
return args;
}
/**
* Gets the constructor arguments that best match the given properties
*
* @param constructors The constructors
* @param properties The mapping of properties name to possible types
* @return The matching constructor arguments, or null if no constructor matches and no default constructor exists
*/
static ConstructorArgs getMatchingConstructorArgs(final Constructor<?>[] constructors, final Map<String, List<Class<?>>> properties) {
final var argsDistance = getArgsDistance(constructors, properties);
final var distances = argsDistance.keySet().stream().sorted().toList();
for (final var distance : distances) {
final var matching = argsDistance.get(distance);
final var argsTypeDistance = getArgsTypeDistance(matching, properties);
if (!argsTypeDistance.isEmpty()) {
return argsTypeDistance.entrySet().stream().sorted(Map.Entry.comparingByKey()).map(Map.Entry::getValue)
.map(s -> s.iterator().next()).findFirst().orElseThrow(() -> new IllegalStateException("Shouldn't happen"));
}
}
//No matching constructor
return Arrays.stream(constructors).filter(c -> c.getParameterCount() == 0).findFirst()
.map(c -> new ConstructorArgs(c, new LinkedHashMap<>())).orElse(null);
}
/**
* Computes the mapping of distance (difference between number of properties and number of matching arguments) to constructor arguments
*
* @param constructors The constructors
* @param properties The object properties
* @return The mapping
*/
private static Map<Long, Set<ConstructorArgs>> getArgsDistance(final Constructor<?>[] constructors, final Map<String, List<Class<?>>> properties) {
final var argsDistance = HashMap.<Long, Set<ConstructorArgs>>newHashMap(constructors.length);
for (final var constructor : constructors) {
final var constructorArgs = ReflectionHelper.getConstructorArgs(constructor);
final var matchingArgsCount = getMatchingArgsCount(constructorArgs, properties);
if (matchingArgsCount != 0) {
final var difference = Math.abs(constructorArgs.namedArgs().size() - matchingArgsCount);
argsDistance.computeIfAbsent(difference, d -> new HashSet<>()).add(constructorArgs);
}
}
return argsDistance;
}
/**
* Computes the mapping of type distance (the total of difference between best matching property type and constructor argument type) to constructor arguments.
* Also filters out constructors that don't match the properties
*
* @param matching The matching constructor arguments
* @param properties The object properties
* @return The mapping
*/
private static Map<Long, Set<ConstructorArgs>> getArgsTypeDistance(final Collection<ConstructorArgs> matching, final Map<String, ? extends List<Class<?>>> properties) {
final var argsTypeDistance = HashMap.<Long, Set<ConstructorArgs>>newHashMap(matching.size());
for (final var constructorArgs : matching) {
final var typeDistance = getTypeDistance(constructorArgs, properties);
if (typeDistance >= 0) {
//Valid constructor
argsTypeDistance.computeIfAbsent(typeDistance, d -> new HashSet<>()).add(constructorArgs);
}
}
return argsTypeDistance;
}
/**
* Calculates the type distance between the constructor arguments and the properties
*
* @param constructorArgs The constructor arguments
* @param properties The object properties
* @return The type distance
*/
private static long getTypeDistance(final ConstructorArgs constructorArgs, final Map<String, ? extends List<Class<?>>> properties) {
var typeDistance = 0L;
for (final var namedArg : constructorArgs.namedArgs().entrySet()) {
final var name = namedArg.getKey();
final var parameter = namedArg.getValue();
final var type = parameter.type();
final var property = properties.get(name);
if (property != null) {
var distance = -1L;
for (var i = 0; i < property.size(); i++) {
final var clazz = property.get(i);
if (clazz.isAssignableFrom(type)) {
distance = i;
break;
}
}
if (distance < 0) {
return -1;
} else {
typeDistance += distance;
}
}
}
return typeDistance;
}
/**
* Checks how many arguments of the given constructor match the given properties
*
* @param constructorArgs The constructor arguments
* @param properties The mapping of properties name to expected type
* @return The number of matching arguments
*/
private static long getMatchingArgsCount(final ConstructorArgs constructorArgs, final Map<String, List<Class<?>>> properties) {
return constructorArgs.namedArgs().keySet().stream().filter(properties::containsKey).count();
}
}
@@ -0,0 +1,131 @@
package ch.gtache.fxml.compiler.impl.internal;
import ch.gtache.fxml.compiler.ControllerFieldInjectionType;
import ch.gtache.fxml.compiler.ControllerInfo;
import ch.gtache.fxml.compiler.ControllerMethodsInjectionType;
import ch.gtache.fxml.compiler.impl.GeneratorImpl;
import ch.gtache.fxml.compiler.parsing.ParsedProperty;
import java.util.SequencedCollection;
import static ch.gtache.fxml.compiler.impl.internal.GenerationHelper.INDENT_8;
import static java.util.Objects.requireNonNull;
/**
* Various methods to help {@link GeneratorImpl} for injecting controllers
*/
final class ControllerInjector {
private final ControllerInfo controllerInfo;
private final ControllerFieldInjectionType fieldInjectionType;
private final ControllerMethodsInjectionType methodInjectionType;
private final StringBuilder sb;
private final SequencedCollection<String> controllerFactoryPostAction;
ControllerInjector(final ControllerInfo controllerInfo, final ControllerFieldInjectionType fieldInjectionType,
final ControllerMethodsInjectionType methodInjectionType, final StringBuilder sb,
final SequencedCollection<String> controllerFactoryPostAction) {
this.controllerInfo = requireNonNull(controllerInfo);
this.fieldInjectionType = requireNonNull(fieldInjectionType);
this.methodInjectionType = requireNonNull(methodInjectionType);
this.sb = requireNonNull(sb);
this.controllerFactoryPostAction = requireNonNull(controllerFactoryPostAction);
}
/**
* Injects the given variable into the controller
*
* @param id The object id
* @param variable The object variable
*/
void injectControllerField(final String id, final String variable) {
switch (fieldInjectionType) {
case FACTORY ->
sb.append(INDENT_8).append("fieldMap.put(\"").append(id).append("\", ").append(variable).append(");\n");
case ASSIGN ->
sb.append(INDENT_8).append("controller.").append(id).append(" = ").append(variable).append(";\n");
case SETTERS -> {
final var setMethod = GenerationHelper.getSetMethod(id);
sb.append(INDENT_8).append("controller.").append(setMethod).append("(").append(variable).append(");\n");
}
case REFLECTION ->
sb.append(INDENT_8).append("injectField(\"").append(id).append("\", ").append(variable).append(");\n");
}
}
/**
* Injects an event handler controller method
*
* @param property The property to inject
* @param parentVariable The parent variable
*/
void injectEventHandlerControllerMethod(final ParsedProperty property, final String parentVariable) {
injectControllerMethod(getEventHandlerMethodInjection(property, parentVariable));
}
/**
* Injects a callback controller method
*
* @param property The property to inject
* @param parentVariable The parent variable
* @param argumentClazz The argument class
*/
void injectCallbackControllerMethod(final ParsedProperty property, final String parentVariable, final String argumentClazz) {
injectControllerMethod(getCallbackMethodInjection(property, parentVariable, argumentClazz));
}
/**
* Injects a controller method
*
* @param methodInjection The method injection
*/
private void injectControllerMethod(final String methodInjection) {
switch (fieldInjectionType) {
case FACTORY -> controllerFactoryPostAction.add(methodInjection);
case ASSIGN, SETTERS, REFLECTION -> sb.append(methodInjection);
}
}
/**
* Computes the method injection for event handler
*
* @param property The property
* @param parentVariable The parent variable
* @return The method injection
*/
private String getEventHandlerMethodInjection(final ParsedProperty property, final String parentVariable) {
final var setMethod = GenerationHelper.getSetMethod(property.name());
final var controllerMethod = property.value().replace("#", "");
return switch (methodInjectionType) {
case REFERENCE -> {
// Checks if the method has the event as argument
final var hasArgument = controllerInfo.handlerHasArgument(controllerMethod);
if (hasArgument) {
yield INDENT_8 + parentVariable + "." + setMethod + "(controller::" + controllerMethod + ");\n";
} else {
yield INDENT_8 + parentVariable + "." + setMethod + "(e -> controller." + controllerMethod + "());\n";
}
}
case REFLECTION ->
INDENT_8 + parentVariable + "." + setMethod + "(e -> callEventHandlerMethod(\"" + controllerMethod + "\", e));\n";
};
}
/**
* Computes the method injection for callback
*
* @param property The property
* @param parentVariable The parent variable
* @param argumentClazz The argument class
* @return The method injection
*/
private String getCallbackMethodInjection(final ParsedProperty property, final String parentVariable, final String argumentClazz) {
final var setMethod = GenerationHelper.getSetMethod(property.name());
final var controllerMethod = property.value().replace("#", "");
return switch (methodInjectionType) {
case REFERENCE -> INDENT_8 + parentVariable + "." + setMethod + "(controller::" + controllerMethod + ");\n";
case REFLECTION ->
INDENT_8 + parentVariable + "." + setMethod + "(e -> callCallbackMethod(\"" + controllerMethod + "\", e, " + argumentClazz + "));\n";
};
}
}
@@ -0,0 +1,118 @@
package ch.gtache.fxml.compiler.impl.internal;
import ch.gtache.fxml.compiler.ControllerFieldInjectionType;
import ch.gtache.fxml.compiler.GenerationException;
import javafx.beans.property.ReadOnlyProperty;
import java.util.Arrays;
import static java.util.Objects.requireNonNull;
/**
* Formats binding expressions
*/
class ExpressionFormatter {
private static final String PROPERTY_METHOD = "Property()";
private final HelperProvider helperProvider;
private final ControllerFieldInjectionType fieldInjectionType;
private final StringBuilder sb;
/**
* Instantiates a new Expression formatter
*
* @param helperProvider The helper provider
* @param fieldInjectionType The field injection type
* @param sb The string builder
*/
ExpressionFormatter(final HelperProvider helperProvider, final ControllerFieldInjectionType fieldInjectionType, final StringBuilder sb) {
this.helperProvider = requireNonNull(helperProvider);
this.fieldInjectionType = requireNonNull(fieldInjectionType);
this.sb = requireNonNull(sb);
}
/**
* Formats a binding expression
*
* @param expression The expression
* @param returnType The return type
* @return The argument to pass to the bind method (e.g. a variable, a method call...)
* @throws GenerationException If an error occurs
*/
String format(final String expression, final Class<?> returnType) throws GenerationException {
final var cleaned = expression.substring(2, expression.length() - 1).trim();
if (cleaned.contains(".")) {
//Reference to the property of an object
return getDotExpression(expression, returnType);
} else {
//Simple reference to an id
return getNonDotExpression(expression);
}
}
private String getDotExpression(final String expression, final Class<?> returnType) throws GenerationException {
final var cleaned = expression.substring(2, expression.length() - 1).trim();
final var split = Arrays.stream(cleaned.split("\\.")).filter(s -> !s.isEmpty()).toList();
if (split.size() == 2) {
final var referenced = split.get(0);
final var value = split.get(1);
//Checks if it is a reference to the controller
if (referenced.equals("controller")) {
return getControllerExpression(value, returnType);
} else {
return getNonControllerExpression(referenced, value);
}
} else {
//Only supports one level
throw new GenerationException("Unsupported binding : " + expression);
}
}
private String getNonDotExpression(final String expression) throws GenerationException {
final var cleaned = expression.substring(2, expression.length() - 1).trim();
final var info = helperProvider.getVariableProvider().getVariableInfo(cleaned);
if (info == null) {
throw new GenerationException("Unknown variable : " + cleaned);
} else {
return info.variableName();
}
}
private String getControllerExpression(final String value, final Class<?> returnType) {
return switch (fieldInjectionType) {
case REFLECTION -> getControllerReflectionExpression(value, returnType);
case SETTERS, FACTORY -> "controller." + value + PROPERTY_METHOD;
case ASSIGN -> "controller." + value;
};
}
private String getControllerReflectionExpression(final String value, final Class<?> returnType) {
final var startVar = helperProvider.getCompatibilityHelper().getStartVar(returnType.getName());
final var variable = helperProvider.getVariableProvider().getNextVariableName("binding");
sb.append(startVar).append(variable).append(";\n");
sb.append(" try {\n");
sb.append(" ").append(helperProvider.getCompatibilityHelper().getStartVar("java.lang.reflect.Field", 0))
.append("field = controller.getClass().getDeclaredField(\"").append(value).append("\");\n");
sb.append(" field.setAccessible(true);\n");
sb.append(" ").append(variable).append(" = (").append(returnType.getName()).append(") field.get(controller);\n");
sb.append(" } catch (final NoSuchFieldException | IllegalAccessException e) {\n");
sb.append(" throw new RuntimeException(e);\n");
sb.append(" }\n");
return variable;
}
private String getNonControllerExpression(final String referenced, final String value) throws GenerationException {
final var info = helperProvider.getVariableProvider().getVariableInfo(referenced);
if (info == null) {
throw new GenerationException("Unknown variable : " + referenced);
} else {
final var hasReadProperty = ReadOnlyProperty.class.isAssignableFrom(ReflectionHelper.getReturnType(info.className(), value + "Property"));
if (hasReadProperty) {
return info.variableName() + "." + value + PROPERTY_METHOD;
} else {
throw new GenerationException("Cannot read " + value + " on " + info.className());
}
}
}
}
@@ -0,0 +1,122 @@
package ch.gtache.fxml.compiler.impl.internal;
import ch.gtache.fxml.compiler.ControllerFieldInjectionType;
import ch.gtache.fxml.compiler.GenerationException;
import ch.gtache.fxml.compiler.impl.GeneratorImpl;
import ch.gtache.fxml.compiler.parsing.ParsedProperty;
import java.util.Objects;
import java.util.SequencedCollection;
import static ch.gtache.fxml.compiler.impl.internal.GenerationHelper.EXPRESSION_PREFIX;
import static ch.gtache.fxml.compiler.impl.internal.GenerationHelper.INDENT_8;
import static java.util.Objects.requireNonNull;
/**
* Helper methods for {@link GeneratorImpl} to set nodes properties using controller's fields
*/
final class FieldSetter {
private static final String CONTROLLER = "controller";
private final HelperProvider helperProvider;
private final ControllerFieldInjectionType fieldInjectionType;
private final StringBuilder sb;
private final SequencedCollection<String> controllerFactoryPostAction;
FieldSetter(final HelperProvider helperProvider, final ControllerFieldInjectionType fieldInjectionType,
final StringBuilder sb, final SequencedCollection<String> controllerFactoryPostAction) {
this.helperProvider = requireNonNull(helperProvider);
this.fieldInjectionType = requireNonNull(fieldInjectionType);
this.sb = requireNonNull(sb);
this.controllerFactoryPostAction = requireNonNull(controllerFactoryPostAction);
}
/**
* Sets an event handler field
*
* @param property The property to inject
* @param parentVariable The parent variable
* @throws GenerationException if an error occurs
*/
void setEventHandler(final ParsedProperty property, final String parentVariable) throws GenerationException {
setField(property, parentVariable, "javafx.event.EventHandler");
}
/**
* Sets a field
*
* @param property The property to inject
* @param parentVariable The parent variable
* @param fieldType The field type
* @throws GenerationException if an error occurs
*/
void setField(final ParsedProperty property, final String parentVariable, final String fieldType) throws GenerationException {
switch (fieldInjectionType) {
case ASSIGN -> setAssign(property, parentVariable);
case FACTORY -> setFactory(property, parentVariable);
case REFLECTION -> setReflection(property, parentVariable, fieldType);
case SETTERS -> setSetter(property, parentVariable);
}
}
private void setAssign(final ParsedProperty property, final String parentVariable) throws GenerationException {
final var methodName = GenerationHelper.getSetMethod(property);
final var value = property.value().replace(EXPRESSION_PREFIX, "");
final var split = value.split("\\.");
final var holderName = split[0];
if (Objects.equals(holderName, CONTROLLER)) {
sb.append(INDENT_8).append(parentVariable).append(".").append(methodName).append("(").append(value).append(");\n");
} else {
throw unexpectedHolderException(holderName);
}
}
private void setFactory(final ParsedProperty property, final String parentVariable) throws GenerationException {
controllerFactoryPostAction.add(getSetString(property, parentVariable));
}
private void setSetter(final ParsedProperty property, final String parentVariable) throws GenerationException {
sb.append(getSetString(property, parentVariable));
}
private static String getSetString(final ParsedProperty property, final String parentVariable) throws GenerationException {
//If field injection is setter or factory, assume controller has getters
final var methodName = GenerationHelper.getSetMethod(property);
final var value = property.value().replace(EXPRESSION_PREFIX, "");
final var split = value.split("\\.");
final var holderName = split[0];
if (Objects.equals(holderName, CONTROLLER)) {
final var getterName = GenerationHelper.getGetMethod(split[1]);
return INDENT_8 + parentVariable + "." + methodName + "(" + CONTROLLER + "." + getterName + "());\n";
} else {
throw unexpectedHolderException(holderName);
}
}
private void setReflection(final ParsedProperty property, final String parentVariable, final String fieldType) throws GenerationException {
final var methodName = GenerationHelper.getSetMethod(property);
final var value = property.value().replace(EXPRESSION_PREFIX, "");
final var split = value.split("\\.");
final var holderName = split[0];
if (Objects.equals(holderName, CONTROLLER)) {
final var fieldName = split[1];
sb.append(" try {\n");
sb.append(" ").append(helperProvider.getCompatibilityHelper().getStartVar("java.lang.reflect.Field", 0))
.append("field = ").append(CONTROLLER).append(".getClass().getDeclaredField(\"").append(fieldName).append("\");\n");
sb.append(" field.setAccessible(true);\n");
sb.append(" final var value = (").append(fieldType).append(") field.get(").append(CONTROLLER).append(");\n");
sb.append(" ").append(parentVariable).append(".").append(methodName).append("(value);\n");
sb.append(" } catch (final NoSuchFieldException | IllegalAccessException e) {\n");
sb.append(" throw new RuntimeException(e);\n");
sb.append(" }\n");
} else {
throw unexpectedHolderException(holderName);
}
}
private static GenerationException unexpectedHolderException(final String holderName) {
return new GenerationException("Unexpected variable holder : " + holderName + " ; expected : " + CONTROLLER);
}
}
@@ -1,9 +1,9 @@
package com.github.gtache.fxml.compiler.impl.internal;
package ch.gtache.fxml.compiler.impl.internal;
import com.github.gtache.fxml.compiler.GenerationException;
import com.github.gtache.fxml.compiler.impl.GeneratorImpl;
import com.github.gtache.fxml.compiler.parsing.ParsedObject;
import com.github.gtache.fxml.compiler.parsing.ParsedProperty;
import ch.gtache.fxml.compiler.GenerationException;
import ch.gtache.fxml.compiler.impl.GeneratorImpl;
import ch.gtache.fxml.compiler.parsing.ParsedObject;
import ch.gtache.fxml.compiler.parsing.ParsedProperty;
import javafx.scene.text.FontPosture;
import javafx.scene.text.FontWeight;
@@ -11,19 +11,29 @@ import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Objects;
import static com.github.gtache.fxml.compiler.impl.internal.GenerationHelper.*;
import static ch.gtache.fxml.compiler.impl.internal.GenerationHelper.FX_ID;
import static ch.gtache.fxml.compiler.impl.internal.GenerationHelper.getSortedAttributes;
/**
* Helper methods for {@link GeneratorImpl} to format fonts
*/
final class FontFormatter {
private FontFormatter() {
private final HelperProvider helperProvider;
private final StringBuilder sb;
FontFormatter(final HelperProvider helperProvider, final StringBuilder sb) {
this.helperProvider = Objects.requireNonNull(helperProvider);
this.sb = Objects.requireNonNull(sb);
}
static void formatFont(final GenerationProgress progress, final ParsedObject parsedObject, final String variableName) throws GenerationException {
private String getStartFont() {
return helperProvider.getCompatibilityHelper().getStartVar("javafx.scene.text.Font");
}
void formatFont(final ParsedObject parsedObject, final String variableName) throws GenerationException {
if (parsedObject.children().isEmpty() && parsedObject.properties().isEmpty()) {
final var value = parseFontValue(parsedObject);
final var url = value.url();
@@ -31,39 +41,40 @@ final class FontFormatter {
final var fp = value.fontPosture();
final var size = value.size();
final var name = value.name();
if (url != null) {
formatURL(progress, url, size, variableName);
} else if (fw == null && fp == null) {
formatNoStyle(progress, name, size, variableName);
if (url == null) {
if (name == null) {
throw new GenerationException("Font must have a name or url : " + parsedObject);
} else if (fw == null && fp == null) {
formatNoStyle(name, size, variableName);
} else {
formatStyle(fw, fp, size, name, variableName);
}
} else {
formatStyle(progress, fw, fp, size, name, variableName);
formatURL(url, size, variableName);
}
handleId(progress, parsedObject, variableName);
} else {
throw new GenerationException("Font cannot have children or properties : " + parsedObject);
}
}
private static void formatURL(final GenerationProgress progress, final URL url, final double size, final String variableName) {
final var urlVariableName = URLBuilder.formatURL(progress, url.toString());
progress.stringBuilder().append("""
Font %1$s;
try (final var in = %2$s.openStream()) {
%1$s = Font.loadFont(in, %3$s);
} catch (final IOException e) {
throw new RuntimeException(e);
}
""".formatted(variableName, urlVariableName, size));
private void formatURL(final URL url, final double size, final String variableName) {
final var urlVariableName = helperProvider.getURLFormatter().formatURL(url.toString());
sb.append(" final javafx.scene.text.Font ").append(variableName).append(";\n");
sb.append(" try (").append(helperProvider.getCompatibilityHelper().getStartVar("java.io.InputStream", 0)).append("in = ").append(urlVariableName).append(".openStream()) {\n");
sb.append(" ").append(variableName).append(" = javafx.scene.text.Font.loadFont(in, ").append(size).append(");\n");
sb.append(" } catch (final java.io.IOException e) {\n");
sb.append(" throw new RuntimeException(e);\n");
sb.append(" }\n");
}
private static void formatNoStyle(final GenerationProgress progress, final String name, final double size, final String variableName) {
progress.stringBuilder().append(START_VAR).append(variableName).append(" = new javafx.scene.text.Font(\"").append(name).append("\", ").append(size).append(");\n");
private void formatNoStyle(final String name, final double size, final String variableName) {
sb.append(getStartFont()).append(variableName).append(" = new javafx.scene.text.Font(\"").append(name).append("\", ").append(size).append(");\n");
}
private static void formatStyle(final GenerationProgress progress, final FontWeight fw, final FontPosture fp, final double size, final String name, final String variableName) {
private void formatStyle(final FontWeight fw, final FontPosture fp, final double size, final String name, final String variableName) {
final var finalFW = fw == null ? FontWeight.NORMAL : fw;
final var finalFP = fp == null ? FontPosture.REGULAR : fp;
progress.stringBuilder().append(START_VAR).append(variableName).append(" = new javafx.scene.text.Font(\"").append(name)
sb.append(getStartFont()).append(variableName).append(" = new javafx.scene.text.Font(\"").append(name)
.append("\", javafx.scene.text.FontWeight.").append(finalFW.name()).append(", javafx.scene.text.FontPosture.")
.append(finalFP.name()).append(", ").append(size).append(");\n");
}
@@ -78,7 +89,7 @@ final class FontFormatter {
for (final var property : sortedAttributes) {
switch (property.name()) {
case FX_ID -> {
//Do nothing
//Do nothing, handled in ObjectFormatter
}
case "name" -> {
try {
@@ -0,0 +1,62 @@
package ch.gtache.fxml.compiler.impl.internal;
import ch.gtache.fxml.compiler.GenerationException;
import ch.gtache.fxml.compiler.compatibility.GenerationCompatibility;
import ch.gtache.fxml.compiler.impl.GeneratorImpl;
import ch.gtache.fxml.compiler.parsing.ParsedObject;
import java.util.Objects;
/**
* Various helper methods for {@link GeneratorImpl} to handle compatibility with older java versions
*/
final class GenerationCompatibilityHelper {
private final HelperProvider helperProvider;
private final GenerationCompatibility compatibility;
GenerationCompatibilityHelper(final HelperProvider helperProvider, final GenerationCompatibility compatibility) {
this.helperProvider = Objects.requireNonNull(helperProvider);
this.compatibility = Objects.requireNonNull(compatibility);
}
String getStartVar(final ParsedObject parsedObject) throws GenerationException {
return getStartVar(parsedObject.className() + helperProvider.getReflectionHelper().getGenericTypes(parsedObject));
}
String getStartVar(final String className) {
return getStartVar(className, 8);
}
String getStartVar(final String className, final int indent) {
if (compatibility.useVar()) {
return " ".repeat(indent) + "final var ";
} else {
return " ".repeat(indent) + "final " + className + " ";
}
}
String getToList() {
return switch (compatibility.listCollector()) {
case TO_LIST -> ".toList()";
case COLLECT_TO_UNMODIFIABLE_LIST -> ".collect(java.util.stream.Collectors.toUnmodifiableList())";
case COLLECT_TO_LIST -> ".collect(java.util.stream.Collectors.toList())";
};
}
String getGetFirst() {
if (compatibility.useGetFirst()) {
return ".getFirst()";
} else {
return ".get(0)";
}
}
String getListOf() {
if (compatibility.useCollectionsOf()) {
return "java.util.List.of(";
} else {
return "java.util.Arrays.asList(";
}
}
}
@@ -0,0 +1,125 @@
package ch.gtache.fxml.compiler.impl.internal;
import ch.gtache.fxml.compiler.impl.GeneratorImpl;
import ch.gtache.fxml.compiler.parsing.ParsedObject;
import ch.gtache.fxml.compiler.parsing.ParsedProperty;
import java.util.List;
import java.util.Map;
/**
* Various helper methods for {@link GeneratorImpl}
*/
final class GenerationHelper {
static final String INDENT_8 = " ";
static final String FX_ID = "fx:id";
static final String FX_VALUE = "fx:value";
static final String VALUE = "value";
//Taken from FXMLLoader
static final String ESCAPE_PREFIX = "\\";
static final String RELATIVE_PATH_PREFIX = "@";
static final String RESOURCE_KEY_PREFIX = "%";
static final String EXPRESSION_PREFIX = "$";
static final String BINDING_EXPRESSION_PREFIX = "${";
static final String BIDIRECTIONAL_BINDING_PREFIX = "#{";
private GenerationHelper() {
}
/**
* Returns the variable prefix for the given object
*
* @param object The object
* @return The variable prefix
*/
static String getVariablePrefix(final ParsedObject object) {
return getVariablePrefix(object.className());
}
/**
* Returns the variable prefix for the given class name
*
* @param className The class name
* @return The variable prefix
*/
static String getVariablePrefix(final String className) {
return className.substring(className.lastIndexOf('.') + 1).toLowerCase();
}
/**
* Returns the getter method name for the given property
*
* @param property The property
* @return The getter method name
*/
static String getGetMethod(final ParsedProperty property) {
return getGetMethod(property.name());
}
/**
* Returns the getter method name for the given property name
*
* @param propertyName The property name
* @return The getter method name
*/
static String getGetMethod(final String propertyName) {
return getMethod(propertyName, "get");
}
/**
* Returns the getter method name for the given property
*
* @param property The property
* @return The getter method name
*/
static String getIsMethod(final ParsedProperty property) {
return getIsMethod(property.name());
}
/**
* Returns the getter method name for the given property name
*
* @param propertyName The property name
* @return The getter method name
*/
static String getIsMethod(final String propertyName) {
return getMethod(propertyName, "is");
}
/**
* Returns the setter method name for the given property
*
* @param property The property
* @return The setter method name
*/
static String getSetMethod(final ParsedProperty property) {
return getSetMethod(property.name());
}
/**
* Returns the setter method name for the given property name
*
* @param propertyName The property name
* @return The setter method name
*/
static String getSetMethod(final String propertyName) {
return getMethod(propertyName, "set");
}
private static String getMethod(final String propertyName, final String methodPrefix) {
return methodPrefix + propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1);
}
/**
* Returns the sorted attributes of the given object
*
* @param parsedObject The parsed object
* @return The sorted attributes
*/
static List<ParsedProperty> getSortedAttributes(final ParsedObject parsedObject) {
return parsedObject.attributes().entrySet().stream().sorted(Map.Entry.comparingByKey()).map(Map.Entry::getValue).toList();
}
}
@@ -0,0 +1,44 @@
package ch.gtache.fxml.compiler.impl.internal;
import ch.gtache.fxml.compiler.GenerationRequest;
import ch.gtache.fxml.compiler.impl.GeneratorImpl;
import java.util.ArrayList;
import java.util.Objects;
import java.util.SequencedCollection;
/**
* Used by {@link GeneratorImpl} to track the generation progress
*
* @param request The generation request
* @param controllerFactoryPostAction The controller factory post action for factory injection
* @param stringBuilder The string builder
*/
public record GenerationProgress(GenerationRequest request,
SequencedCollection<String> controllerFactoryPostAction,
StringBuilder stringBuilder) {
/**
* Instantiates a new GenerationProgress
*
* @param request The generation request
* @param controllerFactoryPostAction The controller factory post action
* @param stringBuilder The string builder
* @throws NullPointerException if any parameter is null
*/
public GenerationProgress {
Objects.requireNonNull(request);
Objects.requireNonNull(controllerFactoryPostAction);
Objects.requireNonNull(stringBuilder);
}
/**
* Instantiates a new GenerationProgress
*
* @param request The generation request
* @throws NullPointerException if request is null
*/
public GenerationProgress(final GenerationRequest request) {
this(request, new ArrayList<>(), new StringBuilder());
}
}
@@ -0,0 +1,106 @@
package ch.gtache.fxml.compiler.impl.internal;
import ch.gtache.fxml.compiler.ControllerFieldInjectionType;
import ch.gtache.fxml.compiler.ControllerMethodsInjectionType;
import java.util.Objects;
/**
* Formats the helper methods for the generated code
*/
public final class HelperMethodsFormatter {
private final HelperProvider helperProvider;
private final ControllerFieldInjectionType fieldInjectionType;
private final ControllerMethodsInjectionType methodInjectionType;
private final StringBuilder sb;
HelperMethodsFormatter(final HelperProvider helperProvider, final ControllerFieldInjectionType fieldInjectionType,
final ControllerMethodsInjectionType methodInjectionType, final StringBuilder sb) {
this.helperProvider = Objects.requireNonNull(helperProvider);
this.fieldInjectionType = Objects.requireNonNull(fieldInjectionType);
this.methodInjectionType = Objects.requireNonNull(methodInjectionType);
this.sb = Objects.requireNonNull(sb);
}
/**
* Formats the helper methods
*/
public void formatHelperMethods() {
final var compatibilityHelper = helperProvider.getCompatibilityHelper();
if (methodInjectionType == ControllerMethodsInjectionType.REFLECTION) {
final var toList = compatibilityHelper.getToList();
final var getFirst = compatibilityHelper.getGetFirst();
final var startVariableMethodList = compatibilityHelper.getStartVar("java.util.List<java.lang.reflect.Method>", 0);
sb.append(" private <T extends javafx.event.Event> void callEventHandlerMethod(final String methodName, final T event) {\n");
sb.append(" try {\n");
sb.append(" final java.lang.reflect.Method method;\n");
sb.append(" ").append(startVariableMethodList).append("methods = java.util.Arrays.stream(controller.getClass().getDeclaredMethods())\n");
sb.append(" .filter(m -> m.getName().equals(methodName))").append(toList).append(";\n");
sb.append(" if (methods.size() > 1) {\n");
sb.append(" ").append(startVariableMethodList).append("eventMethods = methods.stream().filter(m ->\n");
sb.append(" m.getParameterCount() == 1 && javafx.event.Event.class.isAssignableFrom(m.getParameterTypes()[0]))").append(toList).append(";\n");
sb.append(" if (eventMethods.size() == 1) {\n");
sb.append(" method = eventMethods").append(getFirst).append(";\n");
sb.append(" } else {\n");
sb.append(" ").append(startVariableMethodList).append("emptyMethods = methods.stream().filter(m -> m.getParameterCount() == 0)").append(toList).append(";\n");
sb.append(" if (emptyMethods.size() == 1) {\n");
sb.append(" method = emptyMethods").append(getFirst).append(";\n");
sb.append(" } else {\n");
sb.append(" throw new IllegalArgumentException(\"Multiple matching methods for \" + methodName);\n");
sb.append(" }\n");
sb.append(" }\n");
sb.append(" } else if (methods.size() == 1) {\n");
sb.append(" method = methods").append(getFirst).append(";\n");
sb.append(" } else {\n");
sb.append(" throw new IllegalArgumentException(\"No matching method for \" + methodName);\n");
sb.append(" }\n");
sb.append(" method.setAccessible(true);\n");
sb.append(" if (method.getParameterCount() == 0) {\n");
sb.append(" method.invoke(controller);\n");
sb.append(" } else {\n");
sb.append(" method.invoke(controller, event);\n");
sb.append(" }\n");
sb.append(" } catch (final IllegalAccessException | java.lang.reflect.InvocationTargetException ex) {\n");
sb.append(" throw new RuntimeException(\"Error using reflection on \" + methodName, ex);\n");
sb.append(" }\n");
sb.append(" }\n");
sb.append("\n");
sb.append(" private <T, U> U callCallbackMethod(final String methodName, final T value, final Class<T> clazz) {\n");
sb.append(" try {\n");
sb.append(" final java.lang.reflect.Method method;\n");
sb.append(" ").append(startVariableMethodList).append("methods = java.util.Arrays.stream(controller.getClass().getDeclaredMethods())\n");
sb.append(" .filter(m -> m.getName().equals(methodName))").append(toList).append(";\n");
sb.append(" if (methods.size() > 1) {\n");
sb.append(" ").append(startVariableMethodList).append("eventMethods = methods.stream().filter(m ->\n");
sb.append(" m.getParameterCount() == 2 && clazz.isAssignableFrom(m.getParameterTypes()[1]))").append(toList).append(";\n");
sb.append(" if (eventMethods.size() == 1) {\n");
sb.append(" method = eventMethods").append(getFirst).append(";\n");
sb.append(" } else {\n");
sb.append(" throw new IllegalArgumentException(\"Multiple matching methods for \" + methodName);\n");
sb.append(" }\n");
sb.append(" } else if (methods.size() == 1) {\n");
sb.append(" method = methods").append(getFirst).append(";\n");
sb.append(" } else {\n");
sb.append(" throw new IllegalArgumentException(\"No matching method for \" + methodName);\n");
sb.append(" }\n");
sb.append(" method.setAccessible(true);\n");
sb.append(" return (U) method.invoke(controller, value);\n");
sb.append(" } catch (final IllegalAccessException | java.lang.reflect.InvocationTargetException ex) {\n");
sb.append(" throw new RuntimeException(\"Error using reflection on \" + methodName, ex);\n");
sb.append(" }\n");
sb.append(" }\n");
}
if (fieldInjectionType == ControllerFieldInjectionType.REFLECTION) {
sb.append(" private <T> void injectField(final String fieldName, final T object) {\n");
sb.append(" try {\n");
sb.append(" ").append(compatibilityHelper.getStartVar("java.lang.reflect.Field", 0)).append("field = controller.getClass().getDeclaredField(fieldName);\n");
sb.append(" field.setAccessible(true);\n");
sb.append(" field.set(controller, object);\n");
sb.append(" } catch (final NoSuchFieldException | IllegalAccessException e) {\n");
sb.append(" throw new RuntimeException(\"Error using reflection on \" + fieldName, e);\n");
sb.append(" }\n");
sb.append(" }\n");
}
}
}
@@ -0,0 +1,169 @@
package ch.gtache.fxml.compiler.impl.internal;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
/**
* Provider of helper classes
*/
public class HelperProvider {
private final Map<Class<?>, Object> helpers;
private final GenerationProgress progress;
/**
* Instantiates a new helper provider
*
* @param progress The generation progress
*/
public HelperProvider(final GenerationProgress progress) {
this.progress = Objects.requireNonNull(progress);
this.helpers = new HashMap<>();
}
BindingFormatter getBindingFormatter() {
return (BindingFormatter) helpers.computeIfAbsent(BindingFormatter.class, c -> {
final var fieldInjectionType = progress.request().parameters().fieldInjectionType();
final var sb = progress.stringBuilder();
final var controllerFactoryPostAction = progress.controllerFactoryPostAction();
return new BindingFormatter(this, fieldInjectionType, sb, controllerFactoryPostAction);
});
}
ControllerInjector getControllerInjector() {
return (ControllerInjector) helpers.computeIfAbsent(ControllerInjector.class, c -> {
final var request = progress.request();
final var controllerInfo = request.controllerInfo();
final var parameters = request.parameters();
final var fieldInjectionType = parameters.fieldInjectionType();
final var methodInjectionType = parameters.methodInjectionType();
final var sb = progress.stringBuilder();
final var controllerFactoryPostAction = progress.controllerFactoryPostAction();
return new ControllerInjector(controllerInfo, fieldInjectionType, methodInjectionType, sb, controllerFactoryPostAction);
});
}
ExpressionFormatter getExpressionFormatter() {
return (ExpressionFormatter) helpers.computeIfAbsent(ExpressionFormatter.class, c -> {
final var fieldInjectionType = progress.request().parameters().fieldInjectionType();
final var sb = progress.stringBuilder();
return new ExpressionFormatter(this, fieldInjectionType, sb);
});
}
FieldSetter getFieldSetter() {
return (FieldSetter) helpers.computeIfAbsent(FieldSetter.class, c -> {
final var fieldInjectionType = progress.request().parameters().fieldInjectionType();
final var sb = progress.stringBuilder();
final var controllerFactoryPostAction = progress.controllerFactoryPostAction();
return new FieldSetter(this, fieldInjectionType, sb, controllerFactoryPostAction);
});
}
FontFormatter getFontFormatter() {
return (FontFormatter) helpers.computeIfAbsent(FontFormatter.class, c -> {
final var sb = progress.stringBuilder();
return new FontFormatter(this, sb);
});
}
GenerationCompatibilityHelper getCompatibilityHelper() {
return (GenerationCompatibilityHelper) helpers.computeIfAbsent(GenerationCompatibilityHelper.class, c -> {
final var compatibility = progress.request().parameters().compatibility();
return new GenerationCompatibilityHelper(this, compatibility);
});
}
public HelperMethodsFormatter getHelperMethodsFormatter() {
return (HelperMethodsFormatter) helpers.computeIfAbsent(HelperMethodsFormatter.class, c -> {
final var parameters = progress.request().parameters();
final var fieldInjectionType = parameters.fieldInjectionType();
final var methodInjectionType = parameters.methodInjectionType();
final var sb = progress.stringBuilder();
return new HelperMethodsFormatter(this, fieldInjectionType, methodInjectionType, sb);
});
}
ImageFormatter getImageFormatter() {
return (ImageFormatter) helpers.computeIfAbsent(ImageFormatter.class, c -> {
final var sb = progress.stringBuilder();
final var useImageInputStreamConstructor = progress.request().parameters().useImageInputStreamConstructor();
return new ImageFormatter(this, sb, useImageInputStreamConstructor);
});
}
public InitializationFormatter getInitializationFormatter() {
return (InitializationFormatter) helpers.computeIfAbsent(InitializationFormatter.class, c -> {
final var request = progress.request();
final var sb = progress.stringBuilder();
return new InitializationFormatter(this, request, sb);
});
}
public LoadMethodFormatter getLoadMethodFormatter() {
return (LoadMethodFormatter) helpers.computeIfAbsent(LoadMethodFormatter.class, c -> new LoadMethodFormatter(this, progress));
}
ObjectFormatter getObjectFormatter() {
return (ObjectFormatter) helpers.computeIfAbsent(ObjectFormatter.class, c -> {
final var request = progress.request();
final var sb = progress.stringBuilder();
return new ObjectFormatter(this, request, sb);
});
}
PropertyFormatter getPropertyFormatter() {
return (PropertyFormatter) helpers.computeIfAbsent(PropertyFormatter.class, c -> new PropertyFormatter(this, progress));
}
ReflectionHelper getReflectionHelper() {
return (ReflectionHelper) helpers.computeIfAbsent(ReflectionHelper.class, c -> {
final var controllerInfo = progress.request().controllerInfo();
return new ReflectionHelper(controllerInfo);
});
}
SceneFormatter getSceneFormatter() {
return (SceneFormatter) helpers.computeIfAbsent(SceneFormatter.class, c -> {
final var sb = progress.stringBuilder();
return new SceneFormatter(this, sb);
});
}
TriangleMeshFormatter getTriangleMeshFormatter() {
return (TriangleMeshFormatter) helpers.computeIfAbsent(TriangleMeshFormatter.class, c -> {
final var sb = progress.stringBuilder();
return new TriangleMeshFormatter(this, sb);
});
}
URLFormatter getURLFormatter() {
return (URLFormatter) helpers.computeIfAbsent(URLFormatter.class, c -> {
final var sb = progress.stringBuilder();
return new URLFormatter(this, sb);
});
}
ValueFormatter getValueFormatter() {
return (ValueFormatter) helpers.computeIfAbsent(ValueFormatter.class, c -> {
final var resourceInjectionType = progress.request().parameters().resourceInjectionType();
return new ValueFormatter(this, resourceInjectionType);
});
}
ValueClassGuesser getValueClassGuesser() {
return (ValueClassGuesser) helpers.computeIfAbsent(ValueClassGuesser.class, c -> new ValueClassGuesser(this));
}
VariableProvider getVariableProvider() {
return (VariableProvider) helpers.computeIfAbsent(VariableProvider.class, c -> new VariableProvider());
}
WebViewFormatter getWebViewFormatter() {
return (WebViewFormatter) helpers.computeIfAbsent(WebViewFormatter.class, c -> {
final var sb = progress.stringBuilder();
return new WebViewFormatter(this, sb);
});
}
}
@@ -0,0 +1,88 @@
package ch.gtache.fxml.compiler.impl.internal;
import ch.gtache.fxml.compiler.GenerationException;
import ch.gtache.fxml.compiler.impl.GeneratorImpl;
import ch.gtache.fxml.compiler.parsing.ParsedObject;
import static ch.gtache.fxml.compiler.impl.internal.GenerationHelper.FX_ID;
import static ch.gtache.fxml.compiler.impl.internal.GenerationHelper.getSortedAttributes;
import static java.util.Objects.requireNonNull;
/**
* Helper methods for {@link GeneratorImpl} to format Images
*/
final class ImageFormatter {
private final HelperProvider helperProvider;
private final StringBuilder sb;
private final boolean useImageInputStreamConstructor;
ImageFormatter(final HelperProvider helperProvider, final StringBuilder sb, final boolean useImageInputStreamConstructor) {
this.helperProvider = requireNonNull(helperProvider);
this.sb = requireNonNull(sb);
this.useImageInputStreamConstructor = useImageInputStreamConstructor;
}
void formatImage(final ParsedObject parsedObject, final String variableName) throws GenerationException {
if (parsedObject.children().isEmpty() && parsedObject.properties().isEmpty()) {
doFormatImage(parsedObject, variableName);
} else {
throw new GenerationException("Image cannot have children or properties : " + parsedObject);
}
}
private void doFormatImage(final ParsedObject parsedObject, final String variableName) throws GenerationException {
final var sortedAttributes = getSortedAttributes(parsedObject);
String url = null;
var requestedWidth = 0.0;
var requestedHeight = 0.0;
var preserveRatio = false;
var smooth = false;
var backgroundLoading = false;
for (final var property : sortedAttributes) {
switch (property.name()) {
case FX_ID -> {
//Do nothing, handled in ObjectFormatter
}
case "url" -> url = helperProvider.getURLFormatter().formatURL(property.value());
case "requestedWidth" -> requestedWidth = Double.parseDouble(property.value());
case "requestedHeight" -> requestedHeight = Double.parseDouble(property.value());
case "preserveRatio" -> preserveRatio = Boolean.parseBoolean(property.value());
case "smooth" -> smooth = Boolean.parseBoolean(property.value());
case "backgroundLoading" -> backgroundLoading = Boolean.parseBoolean(property.value());
default -> throw new GenerationException("Unknown image attribute : " + property.name());
}
}
if (url == null) {
throw new GenerationException("Image must have a url attribute : " + parsedObject);
}
if (useImageInputStreamConstructor) {
formatInputStream(url, requestedWidth, requestedHeight, preserveRatio, smooth, variableName);
} else {
formatURL(url, requestedWidth, requestedHeight, preserveRatio, smooth, backgroundLoading, variableName);
}
}
private void formatInputStream(final String url, final double requestedWidth,
final double requestedHeight, final boolean preserveRatio, final boolean smooth, final String variableName) {
final var inputStream = helperProvider.getVariableProvider().getNextVariableName("inputStream");
sb.append(" final javafx.scene.image.Image ").append(variableName).append(";\n");
sb.append(" try (").append(helperProvider.getCompatibilityHelper().getStartVar("java.io.InputStream", 0)).append(inputStream).append(" = ").append(url).append(".openStream()) {\n");
sb.append(" ").append(variableName).append(" = new javafx.scene.image.Image(").append(inputStream);
sb.append(", ").append(requestedWidth).append(", ").append(requestedHeight).append(", ").append(preserveRatio).append(", ").append(smooth).append(");\n");
sb.append(" } catch (final java.io.IOException e) {\n");
sb.append(" throw new RuntimeException(e);\n");
sb.append(" }\n");
}
private void formatURL(final String url, final double requestedWidth,
final double requestedHeight, final boolean preserveRatio, final boolean smooth,
final boolean backgroundLoading, final String variableName) {
final var urlString = helperProvider.getVariableProvider().getNextVariableName("urlStr");
final var compatibilityHelper = helperProvider.getCompatibilityHelper();
sb.append(compatibilityHelper.getStartVar("String")).append(urlString).append(" = ").append(url).append(".toString();\n");
sb.append(compatibilityHelper.getStartVar("javafx.scene.image.Image")).append(variableName).append(" = new javafx.scene.image.Image(").append(urlString)
.append(", ").append(requestedWidth).append(", ").append(requestedHeight).append(", ")
.append(preserveRatio).append(", ").append(smooth).append(", ").append(backgroundLoading).append(");\n");
}
}
@@ -0,0 +1,222 @@
package ch.gtache.fxml.compiler.impl.internal;
import ch.gtache.fxml.compiler.ControllerFieldInjectionType;
import ch.gtache.fxml.compiler.ControllerInjectionType;
import ch.gtache.fxml.compiler.GenerationException;
import ch.gtache.fxml.compiler.GenerationRequest;
import ch.gtache.fxml.compiler.InjectionType;
import ch.gtache.fxml.compiler.SourceInfo;
import ch.gtache.fxml.compiler.parsing.ParsedInclude;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import static java.util.Objects.requireNonNull;
/**
* Utility class to provide the view's constructor and fields
*/
public final class InitializationFormatter {
private static final String RESOURCE_BUNDLE_TYPE = "java.util.ResourceBundle";
private static final String RESOURCE_BUNDLE = "resourceBundle";
private final HelperProvider helperProvider;
private final GenerationRequest request;
private final StringBuilder sb;
private final Map<String, String> controllerClassToVariable;
InitializationFormatter(final HelperProvider helperProvider, final GenerationRequest request, final StringBuilder sb) {
this(helperProvider, request, sb, new HashMap<>());
}
InitializationFormatter(final HelperProvider helperProvider, final GenerationRequest request, final StringBuilder sb, final Map<String, String> controllerClassToVariable) {
this.helperProvider = requireNonNull(helperProvider);
this.request = requireNonNull(request);
this.sb = requireNonNull(sb);
this.controllerClassToVariable = requireNonNull(controllerClassToVariable);
}
/**
* Formats the class initialization (fields and constructor)
*
* @throws GenerationException if an error occurs
*/
public void formatFieldsAndConstructor() throws GenerationException {
if (!controllerClassToVariable.isEmpty()) {
throw new GenerationException("Method has already been called");
}
final var className = request.outputClassName();
final var simpleClassName = className.substring(className.lastIndexOf('.') + 1);
final var mainControllerClass = request.controllerInfo().className();
final var parameters = request.parameters();
final var controllerInjectionType = parameters.controllerInjectionType();
final var fieldInjectionType = parameters.fieldInjectionType();
final var isFactory = controllerInjectionType == ControllerInjectionType.FACTORY;
if (hasDuplicateControllerClass() && !isFactory) {
throw new GenerationException("Some controllers in the view tree have the same class ; Factory controller injection is required");
}
fillControllers();
final var sortedControllersKeys = controllerClassToVariable.keySet().stream().sorted().toList();
final var controllerArg = getVariableName("controller", isFactory);
final var controllerArgClass = getType(mainControllerClass, controllerInjectionType, fieldInjectionType);
final var resourceBundleInfo = getResourceBundleInfo();
final var resourceBundleType = resourceBundleInfo.type();
final var resourceBundleArg = resourceBundleInfo.variableName();
if (isFactory) {
sb.append(" private final ").append(controllerArgClass).append(" ").append(controllerArg).append(";\n");
sortedControllersKeys.forEach(e -> sb.append(" private final ").append(getType(e, controllerInjectionType, fieldInjectionType)).append(" ").append(controllerClassToVariable.get(e)).append("Factory").append(";\n"));
sb.append(" private ").append(mainControllerClass).append(" controller;\n");
} else {
sb.append(" private final ").append(mainControllerClass).append(" controller;\n");
sortedControllersKeys.forEach(e -> sb.append(" private final ").append(getType(e, controllerInjectionType, fieldInjectionType)).append(" ").append(controllerClassToVariable.get(e)).append(";\n"));
}
if (resourceBundleType != null) {
sb.append(" private final ").append(resourceBundleType).append(" ").append(resourceBundleArg).append(";\n");
}
sb.append(" private boolean loaded;\n");
sb.append("\n");
sb.append(" /**\n");
sb.append(" * Instantiates a new ").append(simpleClassName).append("\n");
sb.append(" * @param ").append(controllerArg).append(" The controller ").append(isFactory ? "factory" : "instance").append("\n");
sortedControllersKeys.forEach(e -> sb.append(" * @param ").append(getVariableName(controllerClassToVariable.get(e), isFactory))
.append(" The subcontroller ").append(isFactory ? "factory" : "instance").append(" for ").append(e).append("\n"));
if (resourceBundleType != null) {
sb.append(" * @param ").append(resourceBundleArg).append(" The resource bundle\n");
}
sb.append(" */\n");
final var arguments = "final " + controllerArgClass + " " + controllerArg +
((sortedControllersKeys.isEmpty()) ? "" : ", ") +
sortedControllersKeys.stream().map(e -> "final " + getType(e, controllerInjectionType, fieldInjectionType) + " " + getVariableName(controllerClassToVariable.get(e), isFactory))
.sorted().collect(Collectors.joining(", "))
+ (resourceBundleType == null ? "" : ", final " + resourceBundleType + " " + resourceBundleArg);
sb.append(" public ").append(simpleClassName).append("(").append(arguments).append(") {\n");
sb.append(" this.").append(controllerArg).append(" = java.util.Objects.requireNonNull(").append(controllerArg).append(");\n");
sortedControllersKeys.forEach(s -> {
final var variableName = getVariableName(controllerClassToVariable.get(s), isFactory);
sb.append(" this.").append(variableName).append(" = java.util.Objects.requireNonNull(").append(variableName).append(");\n");
});
if (resourceBundleType != null) {
sb.append(" this.").append(resourceBundleArg).append(" = java.util.Objects.requireNonNull(").append(resourceBundleArg).append(");\n");
}
sb.append(" }\n");
}
private ResourceBundleInfo getResourceBundleInfo() {
final var injectionType = request.parameters().resourceInjectionType();
return switch (injectionType) {
case CONSTRUCTOR -> new ResourceBundleInfo(RESOURCE_BUNDLE_TYPE, RESOURCE_BUNDLE);
case CONSTRUCTOR_FUNCTION ->
new ResourceBundleInfo("java.util.function.Function<String, String>", "resourceBundleFunction");
case CONSTRUCTOR_NAME -> new ResourceBundleInfo("String", "resourceBundleName");
case GETTER, GET_BUNDLE -> new ResourceBundleInfo(null, null);
};
}
private record ResourceBundleInfo(String type, String variableName) {
}
private static String getType(final String controllerClass, final InjectionType controllerInjectionTypes, final InjectionType fieldInjectionTypes) {
if (fieldInjectionTypes == ControllerFieldInjectionType.FACTORY) {
return "java.util.function.Function<java.util.Map<String, Object>, " + controllerClass + ">";
} else if (controllerInjectionTypes == ControllerInjectionType.FACTORY) {
return "java.util.function.Supplier<" + controllerClass + ">";
} else {
return controllerClass;
}
}
private static String getVariableName(final String variableName, final boolean isFactory) {
if (isFactory) {
return variableName + "Factory";
} else {
return variableName;
}
}
private boolean hasDuplicateControllerClass() {
final var set = new HashSet<String>();
return hasDuplicateControllerClass(request.sourceInfo(), set);
}
private static boolean hasDuplicateControllerClass(final SourceInfo info, final Set<? super String> controllers) {
final var controllerClass = info.controllerClassName();
if (controllers.contains(controllerClass)) {
return true;
}
controllers.add(controllerClass);
return info.includedSources().stream().anyMatch(s -> hasDuplicateControllerClass(s, controllers));
}
private void fillControllers() {
request.sourceInfo().includedSources().forEach(this::fillControllers);
}
private void fillControllers(final SourceInfo info) {
controllerClassToVariable.put(info.controllerClassName(), helperProvider.getVariableProvider()
.getNextVariableName(GenerationHelper.getVariablePrefix(info.controllerClassName())));
info.includedSources().forEach(this::fillControllers);
}
private static void fillControllers(final SourceInfo info, final Set<? super String> controllers) {
controllers.add(info.controllerClassName());
info.includedSources().forEach(s -> fillControllers(s, controllers));
}
String formatSubViewConstructorCall(final ParsedInclude include) throws GenerationException {
final var info = request.sourceInfo();
final var subInfo = info.sourceToSourceInfo().get(include.source());
if (subInfo == null) {
throw new GenerationException("Unknown include source : " + include.source());
} else {
final var isFactory = request.parameters().controllerInjectionType() == ControllerInjectionType.FACTORY;
final var subClassName = subInfo.controllerClassName();
final var subControllerVariable = getVariableName(controllerClassToVariable.get(subClassName), isFactory);
final var subControllers = new HashSet<String>();
subInfo.includedSources().forEach(s -> fillControllers(s, subControllers));
final var arguments = subControllers.stream().sorted().map(c -> getVariableName(controllerClassToVariable.get(c), isFactory)).collect(Collectors.joining(", "));
final var bundleVariable = subInfo.requiresResourceBundle() ? getBundleVariable(include) : null;
final var argumentList = subControllerVariable + (arguments.isEmpty() ? "" : ", " + arguments) + (bundleVariable == null ? "" : ", " + bundleVariable);
final var subViewName = subInfo.generatedClassName();
final var variable = helperProvider.getVariableProvider().getNextVariableName(GenerationHelper.getVariablePrefix(subViewName));
sb.append(helperProvider.getCompatibilityHelper().getStartVar(subViewName)).append(variable).append(" = new ").append(subViewName).append("(").append(argumentList).append(");\n");
return variable;
}
}
private String getBundleVariable(final ParsedInclude include) {
final var info = getResourceBundleInfo();
if (info.type() == null) {
return null;
} else if (include.resources() == null) {
return info.variableName();
} else {
final var compatibilityHelper = helperProvider.getCompatibilityHelper();
final var variableProvider = helperProvider.getVariableProvider();
return switch (request.parameters().resourceInjectionType()) {
case GETTER, GET_BUNDLE -> null;
case CONSTRUCTOR_NAME -> {
final var bundleVariable = variableProvider.getNextVariableName("resourceBundleName");
sb.append(compatibilityHelper.getStartVar("String")).append(bundleVariable).append(" = \"").append(include.resources()).append("\";\n");
yield bundleVariable;
}
case CONSTRUCTOR_FUNCTION -> {
final var bundleVariable = variableProvider.getNextVariableName(RESOURCE_BUNDLE);
sb.append(compatibilityHelper.getStartVar(RESOURCE_BUNDLE_TYPE)).append(bundleVariable).append(" = java.util.ResourceBundle.getBundle(\"").append(include.resources()).append("\");\n");
final var bundleFunctionVariable = variableProvider.getNextVariableName("resourceBundleFunction");
sb.append(compatibilityHelper.getStartVar("java.util.function.Function<String, String>")).append(bundleFunctionVariable).append(" = (java.util.function.Function<String, String>) s -> ").append(bundleVariable).append(".getString(s);\n");
yield bundleFunctionVariable;
}
case CONSTRUCTOR -> {
final var bundleVariable = variableProvider.getNextVariableName(RESOURCE_BUNDLE);
sb.append(compatibilityHelper.getStartVar(RESOURCE_BUNDLE_TYPE)).append(bundleVariable).append(" = java.util.ResourceBundle.getBundle(\"").append(include.resources()).append("\");\n");
yield bundleVariable;
}
};
}
}
}
@@ -0,0 +1,82 @@
package ch.gtache.fxml.compiler.impl.internal;
import ch.gtache.fxml.compiler.ControllerFieldInjectionType;
import ch.gtache.fxml.compiler.ControllerInjectionType;
import ch.gtache.fxml.compiler.ControllerMethodsInjectionType;
import ch.gtache.fxml.compiler.GenerationException;
import ch.gtache.fxml.compiler.ResourceBundleInjectionType;
import static java.util.Objects.requireNonNull;
/**
* Formats the load method for the generated code
*/
public final class LoadMethodFormatter {
private final HelperProvider helperProvider;
private final GenerationProgress progress;
LoadMethodFormatter(final HelperProvider helperProvider, final GenerationProgress progress) {
this.helperProvider = requireNonNull(helperProvider);
this.progress = requireNonNull(progress);
}
/**
* Formats the load method
*
* @throws GenerationException if an error occurs
*/
public void formatLoadMethod() throws GenerationException {
final var request = progress.request();
final var rootObject = request.rootObject();
final var parameters = progress.request().parameters();
final var controllerInjectionType = parameters.controllerInjectionType();
final var fieldInjectionType = parameters.fieldInjectionType();
final var controllerClass = progress.request().controllerInfo().className();
final var sb = progress.stringBuilder();
sb.append(" /**\n");
sb.append(" * Loads the view. Can only be called once.\n");
sb.append(" *\n");
sb.append(" * @return The view parent\n");
sb.append(" */\n");
sb.append(" public <T> T load() {\n");
sb.append(" if (loaded) {\n");
sb.append(" throw new IllegalStateException(\"Already loaded\");\n");
sb.append(" }\n");
final var resourceBundleInjection = parameters.resourceInjectionType();
final var generationCompatibilityHelper = helperProvider.getCompatibilityHelper();
if (resourceBundleInjection == ResourceBundleInjectionType.CONSTRUCTOR_NAME) {
sb.append(generationCompatibilityHelper.getStartVar("java.util.ResourceBundle")).append("resourceBundle = java.util.ResourceBundle.getBundle(resourceBundleName);\n");
} else if (resourceBundleInjection == ResourceBundleInjectionType.GET_BUNDLE && parameters.bundleMap().containsKey(controllerClass)) {
sb.append(generationCompatibilityHelper.getStartVar("java.util.ResourceBundle")).append("resourceBundle = java.util.ResourceBundle.getBundle(\"")
.append(parameters.bundleMap().get(controllerClass)).append("\");\n");
}
if (fieldInjectionType == ControllerFieldInjectionType.FACTORY) {
sb.append(generationCompatibilityHelper.getStartVar("java.util.Map<String, Object>")).append("fieldMap = new java.util.HashMap<String, Object>();\n");
} else if (controllerInjectionType == ControllerInjectionType.FACTORY) {
sb.append(" controller = controllerFactory.get();\n");
}
final var variableName = helperProvider.getVariableProvider().getNextVariableName(GenerationHelper.getVariablePrefix(rootObject));
helperProvider.getObjectFormatter().format(rootObject, variableName);
if (fieldInjectionType == ControllerFieldInjectionType.FACTORY) {
sb.append(" controller = controllerFactory.apply(fieldMap);\n");
progress.controllerFactoryPostAction().forEach(sb::append);
}
if (request.controllerInfo().hasInitialize()) {
if (parameters.methodInjectionType() == ControllerMethodsInjectionType.REFLECTION) {
sb.append(" try {\n");
sb.append(" ").append(generationCompatibilityHelper.getStartVar("java.lang.reflect.Method", 0)).append("initialize = controller.getClass().getDeclaredMethod(\"initialize\");\n");
sb.append(" initialize.setAccessible(true);\n");
sb.append(" initialize.invoke(controller);\n");
sb.append(" } catch (final java.lang.reflect.InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {\n");
sb.append(" throw new RuntimeException(\"Error using reflection\", e);\n");
sb.append(" }\n");
} else {
sb.append(" controller.initialize();\n");
}
}
sb.append(" loaded = true;\n");
sb.append(" return (T) ").append(variableName).append(";\n");
sb.append(" }\n");
}
}
@@ -0,0 +1,472 @@
package ch.gtache.fxml.compiler.impl.internal;
import ch.gtache.fxml.compiler.GenerationException;
import ch.gtache.fxml.compiler.GenerationRequest;
import ch.gtache.fxml.compiler.impl.GeneratorImpl;
import ch.gtache.fxml.compiler.parsing.ParsedConstant;
import ch.gtache.fxml.compiler.parsing.ParsedCopy;
import ch.gtache.fxml.compiler.parsing.ParsedDefine;
import ch.gtache.fxml.compiler.parsing.ParsedFactory;
import ch.gtache.fxml.compiler.parsing.ParsedInclude;
import ch.gtache.fxml.compiler.parsing.ParsedObject;
import ch.gtache.fxml.compiler.parsing.ParsedReference;
import ch.gtache.fxml.compiler.parsing.ParsedText;
import ch.gtache.fxml.compiler.parsing.ParsedValue;
import ch.gtache.fxml.compiler.parsing.impl.ParsedPropertyImpl;
import javafx.scene.Node;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.SequencedCollection;
import java.util.Set;
import static ch.gtache.fxml.compiler.impl.internal.GenerationHelper.*;
import static java.util.Objects.requireNonNull;
/**
* Helper methods for {@link GeneratorImpl} to format properties
*/
final class ObjectFormatter {
private static final Logger logger = LogManager.getLogger(ObjectFormatter.class);
private static final String NEW_ASSIGN = " = new ";
private static final Set<String> BUILDER_CLASSES = Set.of(
"javafx.scene.Scene",
"javafx.scene.text.Font",
"javafx.scene.image.Image",
"java.net.URL",
"javafx.scene.shape.TriangleMesh",
"javafx.scene.web.WebView"
);
private static final Set<String> SIMPLE_CLASSES = Set.of(
"java.lang.String",
"java.lang.Integer",
"java.lang.Byte",
"java.lang.Short",
"java.lang.Long",
"java.lang.Float",
"java.lang.Double"
);
private final HelperProvider helperProvider;
private final GenerationRequest request;
private final StringBuilder sb;
ObjectFormatter(final HelperProvider helperProvider, final GenerationRequest request, final StringBuilder sb) {
this.helperProvider = requireNonNull(helperProvider);
this.request = requireNonNull(request);
this.sb = requireNonNull(sb);
}
/**
* Formats an object
*
* @param parsedObject The parsed object to format
* @param variableName The variable name for the object
* @throws GenerationException if an error occurs
*/
public void format(final ParsedObject parsedObject, final String variableName) throws GenerationException {
switch (parsedObject) {
case final ParsedConstant constant -> formatConstant(constant, variableName);
case final ParsedCopy copy -> formatCopy(copy, variableName);
case final ParsedDefine define -> formatDefine(define);
case final ParsedFactory factory -> formatFactory(factory, variableName);
case final ParsedInclude include -> formatInclude(include, variableName);
case final ParsedReference reference -> formatReference(reference, variableName);
case final ParsedValue value -> formatValue(value, variableName);
case final ParsedText text -> formatText(text, variableName);
default -> formatObject(parsedObject, variableName);
}
handleId(parsedObject, variableName);
}
/**
* Handles the fx:id attribute of an object
*
* @param parsedObject The parsed object
* @param variableName The variable name
* @throws GenerationException if an error occurs
*/
private void handleId(final ParsedObject parsedObject, final String variableName) throws GenerationException {
final var id = parsedObject.attributes().get(FX_ID);
if (id == null) {
handleIdProperty(parsedObject, variableName);
} else {
final var idValue = id.value();
handleId(parsedObject, variableName, idValue);
}
}
private void handleIdProperty(final ParsedObject parsedObject, final String variableName) throws GenerationException {
final var property = parsedObject.properties().entrySet().stream().filter(
e -> e.getKey().name().equals(FX_ID) && e.getKey().sourceType() == null).findFirst().orElse(null);
if (property != null) {
final var values = property.getValue();
formatDefines(values);
final var notDefinedChildren = getNotDefines(values);
if (notDefinedChildren.size() == 1) {
final var object = notDefinedChildren.getFirst();
if (object instanceof final ParsedText text) {
final var idValue = text.text();
handleId(parsedObject, variableName, idValue);
} else {
throw new GenerationException("Malformed fx:id property : " + parsedObject);
}
} else {
throw new GenerationException("Malformed fx:id property : " + parsedObject);
}
}
}
private static SequencedCollection<ParsedDefine> getDefines(final SequencedCollection<ParsedObject> objects) {
return objects.stream().filter(ParsedDefine.class::isInstance).map(ParsedDefine.class::cast).toList();
}
private static SequencedCollection<ParsedObject> getNotDefines(final SequencedCollection<ParsedObject> objects) {
return objects.stream().filter(c -> !(c instanceof ParsedDefine)).toList();
}
private void handleId(final ParsedObject parsedObject, final String variableName, final String value) throws GenerationException {
final String className;
if (request.controllerInfo().fieldInfo(value) == null) {
className = parsedObject.className();
logger.debug("Not injecting {} because it is not found in controller", value);
} else {
final var reflectionHelper = helperProvider.getReflectionHelper();
if (ReflectionHelper.isGeneric(ReflectionHelper.getClass(parsedObject.className()))) {
className = parsedObject.className() + reflectionHelper.getGenericTypes(parsedObject);
} else {
className = parsedObject.className();
}
helperProvider.getControllerInjector().injectControllerField(value, variableName);
}
helperProvider.getVariableProvider().addVariableInfo(value, new VariableInfo(value, parsedObject, variableName, className));
}
/**
* Formats a simple text
*
* @param text The parsed text
* @param variableName The variable name
*/
private void formatText(final ParsedText text, final String variableName) {
sb.append(helperProvider.getCompatibilityHelper().getStartVar("String"))
.append(variableName).append(" = \"").append(text.text()).append("\";\n");
}
/**
* Formats a basic object
*
* @param parsedObject The parsed object to format
* @param variableName The variable name for the object
*/
private void formatObject(final ParsedObject parsedObject, final String variableName) throws GenerationException {
if (BUILDER_CLASSES.contains(parsedObject.className())) {
formatBuilderObject(parsedObject, variableName);
} else {
formatNotBuilder(parsedObject, variableName);
}
}
/**
* Formats a builder object
*
* @param parsedObject The parsed object
* @param variableName The variable name
*/
private void formatBuilderObject(final ParsedObject parsedObject, final String variableName) throws GenerationException {
final var className = parsedObject.className();
switch (className) {
case "javafx.scene.Scene" -> helperProvider.getSceneFormatter().formatScene(parsedObject, variableName);
case "javafx.scene.text.Font" -> helperProvider.getFontFormatter().formatFont(parsedObject, variableName);
case "javafx.scene.image.Image" ->
helperProvider.getImageFormatter().formatImage(parsedObject, variableName);
case "javafx.scene.shape.TriangleMesh" ->
helperProvider.getTriangleMeshFormatter().formatTriangleMesh(parsedObject, variableName);
case "java.net.URL" -> helperProvider.getURLFormatter().formatURL(parsedObject, variableName);
case "javafx.scene.web.WebView" ->
helperProvider.getWebViewFormatter().formatWebView(parsedObject, variableName);
default -> throw new IllegalArgumentException("Unknown builder class : " + className);
}
}
private void formatNotBuilder(final ParsedObject parsedObject, final String variableName) throws GenerationException {
if (isSimpleClass(parsedObject)) {
formatSimpleClass(parsedObject, variableName);
} else {
formatComplexClass(parsedObject, variableName);
}
}
private void formatSimpleClass(final ParsedObject parsedObject, final String variableName) throws GenerationException {
if (!parsedObject.properties().isEmpty()) {
throw new GenerationException("Simple class cannot have properties : " + parsedObject);
}
if (parsedObject.attributes().keySet().stream().anyMatch(k -> !k.equals(FX_ID) && !k.equals(VALUE) && !k.equals(FX_VALUE))) {
throw new GenerationException("Invalid attributes for simple class : " + parsedObject);
}
final var value = getSimpleValue(parsedObject);
final var valueStr = ValueFormatter.toString(value, ReflectionHelper.getClass(parsedObject.className()));
sb.append(helperProvider.getCompatibilityHelper().getStartVar(parsedObject)).append(variableName).append(" = ").append(valueStr).append(";\n");
}
private String getSimpleValue(final ParsedObject parsedObject) throws GenerationException {
formatDefines(parsedObject.children());
final var notDefinedChildren = getNotDefines(parsedObject.children());
if (parsedObject.attributes().containsKey(FX_VALUE)) {
return getSimpleFXValue(parsedObject, notDefinedChildren);
} else if (parsedObject.attributes().containsKey(VALUE)) {
return getSimpleValue(parsedObject, notDefinedChildren);
} else {
return getSimpleChild(parsedObject, notDefinedChildren);
}
}
private void formatDefines(final SequencedCollection<ParsedObject> values) throws GenerationException {
final var defines = getDefines(values);
for (final var definedChild : defines) {
format(definedChild, helperProvider.getVariableProvider().getNextVariableName("define"));
}
}
private static String getSimpleFXValue(final ParsedObject parsedObject, final Collection<ParsedObject> notDefinedChildren) throws GenerationException {
if (notDefinedChildren.isEmpty() && !parsedObject.attributes().containsKey(VALUE)) {
return parsedObject.attributes().get(FX_VALUE).value();
} else {
throw new GenerationException("Malformed simple class : " + parsedObject);
}
}
private static String getSimpleValue(final ParsedObject parsedObject, final Collection<ParsedObject> notDefinedChildren) throws GenerationException {
if (notDefinedChildren.isEmpty()) {
return parsedObject.attributes().get(VALUE).value();
} else {
throw new GenerationException("Malformed simple class : " + parsedObject);
}
}
private static String getSimpleChild(final ParsedObject parsedObject, final SequencedCollection<ParsedObject> notDefinedChildren) throws GenerationException {
if (notDefinedChildren.size() == 1) {
final var child = notDefinedChildren.getFirst();
if (child instanceof final ParsedText text) {
return text.text();
} else {
throw new GenerationException("Invalid value for : " + parsedObject);
}
} else {
throw new GenerationException("Value not found for : " + parsedObject);
}
}
private static boolean isSimpleClass(final ParsedObject object) throws GenerationException {
final var className = object.className();
if (SIMPLE_CLASSES.contains(className)) {
return true;
} else {
final var clazz = ReflectionHelper.getClass(className);
return clazz.isEnum();
}
}
private void formatComplexClass(final ParsedObject parsedObject, final String variableName) throws GenerationException {
final var clazz = ReflectionHelper.getClass(parsedObject.className());
final var children = parsedObject.children();
final var notDefinedChildren = getNotDefines(children);
final var allProperties = new HashMap<String, List<Class<?>>>();
for (final var entry : parsedObject.attributes().entrySet()) {
final var possibleTypes = helperProvider.getValueClassGuesser().guess(entry.getValue().value());
allProperties.put(entry.getKey(), possibleTypes);
}
for (final var entry : parsedObject.properties().entrySet()) {
allProperties.put(entry.getKey().name(), List.of(Node.class, Node[].class));
}
formatDefines(children);
if (!notDefinedChildren.isEmpty()) {
final var defaultProperty = ReflectionHelper.getDefaultProperty(parsedObject.className());
if (defaultProperty != null) {
allProperties.put(defaultProperty, List.of(Node.class, Node[].class));
}
}
final var constructors = clazz.getConstructors();
final var constructorArgs = ConstructorHelper.getMatchingConstructorArgs(constructors, allProperties);
if (constructorArgs == null) {
logger.debug("No constructor found for {} with attributes {}", clazz.getCanonicalName(), allProperties);
formatNoConstructor(parsedObject, variableName, allProperties.keySet());
} else {
formatConstructor(parsedObject, variableName, constructorArgs);
}
}
private void formatNoConstructor(final ParsedObject parsedObject, final String variableName, final Collection<String> allAttributesNames) throws GenerationException {
final var clazz = ReflectionHelper.getClass(parsedObject.className());
if (allAttributesNames.contains("fx:constant") && (allAttributesNames.size() == 1 ||
(allAttributesNames.size() == 2 && allAttributesNames.contains("fx:id")))) {
final var property = parsedObject.attributes().get("fx:constant");
sb.append(helperProvider.getCompatibilityHelper().getStartVar(parsedObject)).append(variableName).append(" = ").append(clazz.getCanonicalName()).append(".").append(property.value()).append(";\n");
} else {
throw new GenerationException("Cannot find empty constructor for " + clazz.getCanonicalName());
}
}
private void formatConstructor(final ParsedObject parsedObject, final String variableName, final ConstructorArgs constructorArgs) throws GenerationException {
final var reflectionHelper = helperProvider.getReflectionHelper();
final var args = ConstructorHelper.getListConstructorArgs(constructorArgs, parsedObject);
final var genericTypes = reflectionHelper.getGenericTypes(parsedObject);
sb.append(helperProvider.getCompatibilityHelper().getStartVar(parsedObject)).append(variableName)
.append(NEW_ASSIGN).append(parsedObject.className()).append(genericTypes).append("(")
.append(String.join(", ", args)).append(");\n");
final var sortedAttributes = getSortedAttributes(parsedObject);
for (final var value : sortedAttributes) {
if (!constructorArgs.namedArgs().containsKey(value.name())) {
helperProvider.getPropertyFormatter().formatProperty(value, parsedObject, variableName);
}
}
final var sortedProperties = parsedObject.properties().entrySet().stream().sorted(Comparator.comparing(p -> p.getKey().name())).toList();
for (final var e : sortedProperties) {
if (!constructorArgs.namedArgs().containsKey(e.getKey().name())) {
final var p = e.getKey();
final var o = e.getValue();
helperProvider.getPropertyFormatter().formatProperty(p, o, parsedObject, variableName);
}
}
final var notDefinedChildren = parsedObject.children().stream().filter(c -> !(c instanceof ParsedDefine)).toList();
if (!notDefinedChildren.isEmpty()) {
final var defaultProperty = ReflectionHelper.getDefaultProperty(parsedObject.className());
if (!constructorArgs.namedArgs().containsKey(defaultProperty)) {
final var property = new ParsedPropertyImpl(defaultProperty, null, null);
helperProvider.getPropertyFormatter().formatProperty(property, notDefinedChildren, parsedObject, variableName);
}
}
}
/**
* Formats an include object
*
* @param include The include object
* @param subNodeName The sub node name
*/
private void formatInclude(final ParsedInclude include, final String subNodeName) throws GenerationException {
final var viewVariable = helperProvider.getInitializationFormatter().formatSubViewConstructorCall(include);
sb.append(" final javafx.scene.Parent ").append(subNodeName).append(" = ").append(viewVariable).append(".load();\n");
injectSubController(include, viewVariable);
}
private void injectSubController(final ParsedInclude include, final String subViewVariable) {
final var id = include.controllerId();
if (id != null) {
final var variableProvider = helperProvider.getVariableProvider();
final var subControllerVariable = variableProvider.getNextVariableName("controller");
final var controllerClass = request.sourceInfo().sourceToSourceInfo().get(include.source()).controllerClassName();
sb.append(helperProvider.getCompatibilityHelper().getStartVar(controllerClass)).append(subControllerVariable).append(" = ").append(subViewVariable).append(".controller();\n");
variableProvider.addVariableInfo(id, new VariableInfo(id, include, subControllerVariable, controllerClass));
if (request.controllerInfo().fieldInfo(id) == null) {
logger.debug("Not injecting {} because it is not found in controller", id);
} else {
helperProvider.getControllerInjector().injectControllerField(id, subControllerVariable);
}
}
}
/**
* Formats a fx:define
*
* @param define The parsed define
* @throws GenerationException if an error occurs
*/
private void formatDefine(final ParsedObject define) throws GenerationException {
for (final var child : define.children()) {
format(child, helperProvider.getVariableProvider().getNextVariableName("definedObject"));
}
}
/**
* Formats a fx:reference
*
* @param reference The parsed reference
* @throws GenerationException if an error occurs
*/
private void formatReference(final ParsedReference reference, final String variableName) throws GenerationException {
final var id = reference.source();
final var variableInfo = helperProvider.getVariableProvider().getVariableInfo(id);
if (variableInfo == null) {
throw new GenerationException("Unknown id : " + id);
}
final var referenceName = variableInfo.variableName();
sb.append(helperProvider.getCompatibilityHelper().getStartVar(variableInfo.className())).append(variableName).append(" = ").append(referenceName).append(";\n");
}
/**
* Formats a fx:copy
*
* @param copy The parsed copy
* @param variableName The variable name
* @throws GenerationException if an error occurs
*/
private void formatCopy(final ParsedCopy copy, final String variableName) throws GenerationException {
final var id = copy.source();
final var variableInfo = helperProvider.getVariableProvider().getVariableInfo(id);
if (variableInfo == null) {
throw new GenerationException("Unknown id : " + id);
}
final var copyVariable = variableInfo.variableName();
sb.append(helperProvider.getCompatibilityHelper().getStartVar(variableInfo.className())).append(variableName)
.append(NEW_ASSIGN).append(variableInfo.className()).append("(").append(copyVariable).append(");\n");
}
/**
* Formats a constant object
*
* @param constant The constant
* @param variableName The variable name
*/
private void formatConstant(final ParsedConstant constant, final String variableName) {
sb.append(helperProvider.getCompatibilityHelper().getStartVar(constant.className()))
.append(variableName).append(" = ").append(constant.className()).append(".").append(constant.constant()).append(";\n");
}
/**
* Formats a value object
*
* @param value The value
* @param variableName The variable name
*/
private void formatValue(final ParsedValue value, final String variableName) {
sb.append(helperProvider.getCompatibilityHelper().getStartVar(value.className())).append(variableName).append(" = ").append(value.className()).append(".valueOf(\"").append(value.value()).append("\");\n");
}
/**
* Formats a factory object
*
* @param factory The factory
* @param variableName The variable name
*/
private void formatFactory(final ParsedFactory factory, final String variableName) throws GenerationException {
final var variables = new ArrayList<String>();
for (final var child : factory.children()) {
final var vn = helperProvider.getVariableProvider().getNextVariableName(getVariablePrefix(child));
format(child, vn);
}
for (final var argument : factory.arguments()) {
final var argumentVariable = helperProvider.getVariableProvider().getNextVariableName("arg");
variables.add(argumentVariable);
format(argument, argumentVariable);
}
final var compatibilityHelper = helperProvider.getCompatibilityHelper();
if (request.parameters().compatibility().useVar()) {
sb.append(compatibilityHelper.getStartVar(factory.className())).append(variableName).append(" = ").append(factory.className())
.append(".").append(factory.factory()).append("(").append(String.join(", ", variables)).append(");\n");
} else {
final var returnType = ReflectionHelper.getStaticReturnType(factory.className(), factory.factory()).getName();
sb.append(compatibilityHelper.getStartVar(returnType)).append(variableName).append(" = ").append(factory.className())
.append(".").append(factory.factory()).append("(").append(String.join(", ", variables)).append(");\n");
}
}
}
@@ -1,4 +1,4 @@
package com.github.gtache.fxml.compiler.impl.internal;
package ch.gtache.fxml.compiler.impl.internal;
import static java.util.Objects.requireNonNull;
@@ -19,7 +19,7 @@ record Parameter(String name, Class<?> type, String defaultValue) {
* @param defaultValue The parameter default value
* @throws NullPointerException if any parameter is null
*/
public Parameter {
Parameter {
requireNonNull(name);
requireNonNull(type);
requireNonNull(defaultValue);
@@ -0,0 +1,271 @@
package ch.gtache.fxml.compiler.impl.internal;
import ch.gtache.fxml.compiler.ControllerFieldInjectionType;
import ch.gtache.fxml.compiler.GenerationException;
import ch.gtache.fxml.compiler.ResourceBundleInjectionType;
import ch.gtache.fxml.compiler.impl.GeneratorImpl;
import ch.gtache.fxml.compiler.parsing.ParsedDefine;
import ch.gtache.fxml.compiler.parsing.ParsedObject;
import ch.gtache.fxml.compiler.parsing.ParsedProperty;
import ch.gtache.fxml.compiler.parsing.ParsedText;
import ch.gtache.fxml.compiler.parsing.impl.ParsedPropertyImpl;
import javafx.event.EventHandler;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.SequencedCollection;
import static ch.gtache.fxml.compiler.impl.internal.GenerationHelper.*;
import static java.util.Objects.requireNonNull;
/**
* Helper methods for {@link GeneratorImpl} to format properties
*/
final class PropertyFormatter {
private final HelperProvider helperProvider;
private final GenerationProgress progress;
PropertyFormatter(final HelperProvider helperProvider, final GenerationProgress progress) {
this.helperProvider = requireNonNull(helperProvider);
this.progress = requireNonNull(progress);
}
/**
* Formats a property
*
* @param property The property to format
* @param parent The property's parent object
* @param parentVariable The parent variable
* @throws GenerationException if an error occurs
*/
void formatProperty(final ParsedProperty property, final ParsedObject parent, final String parentVariable) throws GenerationException {
final var value = property.value();
if (value.endsWith("}") && (value.startsWith(BINDING_EXPRESSION_PREFIX) || value.startsWith(BIDIRECTIONAL_BINDING_PREFIX))) {
helperProvider.getBindingFormatter().formatBinding(property, parent, parentVariable);
} else {
final var propertyName = property.name();
if (propertyName.equals(FX_ID)) {
//Do nothing
} else if (propertyName.equals("fx:controller")) {
checkDuplicateController(parent);
} else if (Objects.equals(property.sourceType(), EventHandler.class.getName())) {
handleEventHandler(property, parentVariable);
} else if (property.sourceType() != null) {
handleStaticProperty(property, parentVariable, propertyName);
} else {
handleProperty(property, parent, parentVariable);
}
}
}
/**
* Formats a complex property (containing a list of objects).
* The values should not contain any ParsedDefine
*
* @param property The property to format
* @param values The property's values
* @param parent The property's parent object
* @param parentVariable The parent variable
* @throws GenerationException if an error occurs or if the values contain a ParsedDefine
*/
void formatProperty(final ParsedProperty property, final SequencedCollection<? extends ParsedObject> values, final ParsedObject parent, final String parentVariable) throws GenerationException {
if (values.stream().anyMatch(ParsedDefine.class::isInstance)) {
throw new GenerationException("Values should not contain any ParsedDefine");
} else if (values.size() == 1 && values.getFirst() instanceof final ParsedText text) {
final var newProperty = new ParsedPropertyImpl(property.name(), property.sourceType(), text.text());
formatProperty(newProperty, parent, parentVariable);
} else {
formatChild(parent, property, values, parentVariable);
}
}
private void checkDuplicateController(final ParsedObject parent) throws GenerationException {
if (parent != progress.request().rootObject()) {
throw new GenerationException("Invalid nested controller");
}
}
private void handleEventHandler(final ParsedProperty property, final String parentVariable) throws GenerationException {
if (property.value().startsWith("#")) {
helperProvider.getControllerInjector().injectEventHandlerControllerMethod(property, parentVariable);
} else {
helperProvider.getFieldSetter().setEventHandler(property, parentVariable);
}
}
private void handleStaticProperty(final ParsedProperty property, final String parentVariable, final String propertyName) throws GenerationException {
final var setMethod = getSetMethod(propertyName);
final var propertySourceTypeClass = ReflectionHelper.getClass(property.sourceType());
if (ReflectionHelper.hasStaticMethod(propertySourceTypeClass, setMethod, null, null)) {
final var method = ReflectionHelper.getStaticMethod(propertySourceTypeClass, setMethod, null, null);
final var parameterType = method.getParameterTypes()[1];
final var arg = helperProvider.getValueFormatter().getArg(property.value(), parameterType);
setLaterIfNeeded(property, parameterType, " " + property.sourceType() + "." + setMethod + "(" + parentVariable + ", " + arg + ");\n");
} else {
throw new GenerationException("Cannot set " + propertyName + " on " + property.sourceType());
}
}
private void handleProperty(final ParsedProperty property, final ParsedObject parent, final String parentVariable) throws GenerationException {
final var propertyName = property.name();
final var setMethod = getSetMethod(propertyName);
final var getMethod = getGetMethod(propertyName);
final var parentClass = ReflectionHelper.getClass(parent.className());
if (ReflectionHelper.hasMethod(parentClass, setMethod, (Class<?>) null)) {
handleSetProperty(property, parentClass, parentVariable);
} else if (ReflectionHelper.hasMethod(parentClass, getMethod)) {
handleGetProperty(property, parentClass, parentVariable);
} else {
throw new GenerationException("Cannot set " + propertyName + " on " + parent.className());
}
}
private void handleSetProperty(final ParsedProperty property, final Class<?> parentClass, final String parentVariable) throws GenerationException {
final var setMethod = getSetMethod(property.name());
final var method = ReflectionHelper.getMethod(parentClass, setMethod, (Class<?>) null);
final var parameterType = method.getParameterTypes()[0];
final var arg = helperProvider.getValueFormatter().getArg(property.value(), parameterType);
setLaterIfNeeded(property, parameterType, " " + parentVariable + "." + setMethod + "(" + arg + ");\n");
}
private void handleGetProperty(final ParsedProperty property, final Class<?> parentClass, final String parentVariable) throws GenerationException {
final var getMethod = getGetMethod(property.name());
final var method = ReflectionHelper.getMethod(parentClass, getMethod);
final var returnType = method.getReturnType();
if (ReflectionHelper.hasMethod(returnType, "addAll", List.class)) {
final var arg = helperProvider.getValueFormatter().getArg(property.value(), String.class);
setLaterIfNeeded(property, String.class, " " + parentVariable + "." + getMethod + "().addAll(" +
helperProvider.getCompatibilityHelper().getListOf() + arg + "));\n");
} else {
throw new GenerationException("Cannot set " + property.name() + " on " + parentClass);
}
}
/**
* Saves the text to set after constructor creation if factory injection is used
*
* @param property The property
* @param type The type
* @param arg The argument
*/
private void setLaterIfNeeded(final ParsedProperty property, final Class<?> type, final String arg) {
final var parameters = progress.request().parameters();
if (type == String.class && property.value().startsWith(RESOURCE_KEY_PREFIX) && parameters.resourceInjectionType() == ResourceBundleInjectionType.GETTER
&& parameters.fieldInjectionType() == ControllerFieldInjectionType.FACTORY) {
progress.controllerFactoryPostAction().add(arg);
} else {
progress.stringBuilder().append(arg);
}
}
/**
* Formats the children objects of a property
*
* @param parent The parent object
* @param property The parent property
* @param objects The child objects
* @param parentVariable The parent object variable
*/
private void formatChild(final ParsedObject parent, final ParsedProperty property,
final Iterable<? extends ParsedObject> objects, final String parentVariable) throws GenerationException {
final var propertyName = property.name();
final var variables = new ArrayList<String>();
for (final var object : objects) {
final var vn = helperProvider.getVariableProvider().getNextVariableName(getVariablePrefix(object));
helperProvider.getObjectFormatter().format(object, vn);
variables.add(vn);
}
if (variables.size() > 1) {
formatMultipleChildren(variables, propertyName, parent, parentVariable);
} else if (variables.size() == 1) {
final var vn = variables.getFirst();
formatSingleChild(vn, property, parent, parentVariable);
}
}
/**
* Formats children objects given that they are more than one
*
* @param variables The children variables
* @param propertyName The property name
* @param parent The parent object
* @param parentVariable The parent object variable
*/
private void formatMultipleChildren(final Iterable<String> variables, final String propertyName, final ParsedObject parent,
final String parentVariable) throws GenerationException {
final var getMethod = getGetMethod(propertyName);
if (ReflectionHelper.hasMethod(ReflectionHelper.getClass(parent.className()), getMethod)) {
progress.stringBuilder().append(" ").append(parentVariable).append(".").append(getMethod).append("().addAll(").append(helperProvider.getCompatibilityHelper().getListOf()).append(String.join(", ", variables)).append("));\n");
} else {
throw getCannotSetException(propertyName, parent.className());
}
}
/**
* Formats a single child object
*
* @param variableName The child's variable name
* @param property The parent property
* @param parent The parent object
* @param parentVariable The parent object variable
*/
private void formatSingleChild(final String variableName, final ParsedProperty property, final ParsedObject parent,
final String parentVariable) throws GenerationException {
if (property.sourceType() == null) {
formatSingleChildInstance(variableName, property, parent, parentVariable);
} else {
formatSingleChildStatic(variableName, property, parentVariable);
}
}
/**
* Formats a single child object using an instance method on the parent object
*
* @param variableName The child's variable name
* @param property The parent property
* @param parent The parent object
* @param parentVariable The parent object variable
*/
private void formatSingleChildInstance(final String variableName,
final ParsedProperty property, final ParsedObject parent,
final String parentVariable) throws GenerationException {
final var setMethod = getSetMethod(property);
final var getMethod = getGetMethod(property);
final var parentClass = ReflectionHelper.getClass(parent.className());
final var sb = progress.stringBuilder();
if (ReflectionHelper.hasMethod(parentClass, setMethod, (Class<?>) null)) {
sb.append(" ").append(parentVariable).append(".").append(setMethod).append("(").append(variableName).append(");\n");
} else if (ReflectionHelper.hasMethod(parentClass, getMethod)) {
//Probably a list method that has only one element
sb.append(" ").append(parentVariable).append(".").append(getMethod).append("().addAll(").append(helperProvider.getCompatibilityHelper().getListOf()).append(variableName).append("));\n");
} else {
throw getCannotSetException(property.name(), parent.className());
}
}
/**
* Formats a child object using a static method
*
* @param variableName The child's variable name
* @param property The parent property
* @param parentVariable The parent variable
*/
private void formatSingleChildStatic(final String variableName,
final ParsedProperty property, final String parentVariable) throws GenerationException {
final var setMethod = getSetMethod(property);
final var clazz = ReflectionHelper.getClass(property.sourceType());
if (ReflectionHelper.hasStaticMethod(clazz, setMethod, null, null)) {
progress.stringBuilder().append(" ").append(property.sourceType()).append(".").append(setMethod)
.append("(").append(parentVariable).append(", ").append(variableName).append(");\n");
} else {
throw getCannotSetException(property.name(), property.sourceType());
}
}
private static GenerationException getCannotSetException(final String propertyName, final String className) {
return new GenerationException("Cannot set " + propertyName + " on " + className);
}
}
@@ -0,0 +1,491 @@
package ch.gtache.fxml.compiler.impl.internal;
import ch.gtache.fxml.compiler.ControllerInfo;
import ch.gtache.fxml.compiler.GenerationException;
import ch.gtache.fxml.compiler.GenericTypes;
import ch.gtache.fxml.compiler.parsing.ParsedObject;
import javafx.beans.DefaultProperty;
import javafx.beans.NamedArg;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.lang.invoke.MethodType;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import static ch.gtache.fxml.compiler.impl.internal.GenerationHelper.FX_ID;
/**
* Helper methods for reflection
*/
final class ReflectionHelper {
private static final Logger logger = LogManager.getLogger(ReflectionHelper.class);
private static final Map<String, Class<?>> classMap = new ConcurrentHashMap<>();
private static final Map<Class<?>, Boolean> hasValueOf = new ConcurrentHashMap<>();
private static final Map<Class<?>, Boolean> isGeneric = new ConcurrentHashMap<>();
private static final Map<String, String> defaultProperty = new ConcurrentHashMap<>();
private static final Map<Class<?>, Map<MethodKey, Method>> methods = new ConcurrentHashMap<>();
private static final Map<Class<?>, Map<MethodKey, Method>> staticMethods = new ConcurrentHashMap<>();
private static final Map<Class<?>, Map<MethodKey, Class<?>>> methodsReturnType = new ConcurrentHashMap<>();
private static final Map<String, Class<?>> PRIMITIVE_TYPES = Map.of(
"boolean", boolean.class,
"byte", byte.class,
"char", char.class,
"short", short.class,
"int", int.class,
"long", long.class,
"float", float.class,
"double", double.class
);
private final ControllerInfo controllerInfo;
ReflectionHelper(final ControllerInfo controllerInfo) {
this.controllerInfo = Objects.requireNonNull(controllerInfo);
}
/**
* Checks if the given class is generic
* The result is cached
*
* @param clazz The class
* @return True if the class is generic
*/
static boolean isGeneric(final Class<?> clazz) {
return isGeneric.computeIfAbsent(clazz, c -> c.getTypeParameters().length > 0);
}
/**
* Checks if the given class has a method with the given name.
* The result is cached
*
* @param clazz The class
* @param methodName The method name
* @param parameterTypes The method parameter types (null if any object is allowed)
* @return True if the class has a method with the given name
*/
static boolean hasMethod(final Class<?> clazz, final String methodName, final Class<?>... parameterTypes) {
final var methodMap = methods.computeIfAbsent(clazz, c -> new ConcurrentHashMap<>());
final var methodKey = new MethodKey(methodName, Arrays.asList(parameterTypes));
final var method = methodMap.computeIfAbsent(methodKey, m -> {
try {
return computeMethod(clazz, m);
} catch (final GenerationException ignored) {
return null;
}
});
return method != null;
}
/**
* Gets the method corresponding to the given class and name.
* The result is cached
*
* @param clazz The class
* @param methodName The method name
* @param parameterTypes The method parameter types
* @return The method
*/
static Method getMethod(final Class<?> clazz, final String methodName, final Class<?>... parameterTypes) throws GenerationException {
final var methodMap = methods.computeIfAbsent(clazz, c -> new ConcurrentHashMap<>());
try {
final var methodKey = new MethodKey(methodName, Arrays.asList(parameterTypes));
return methodMap.computeIfAbsent(methodKey, m -> {
try {
return computeMethod(clazz, m);
} catch (final GenerationException e) {
throw new RuntimeException(e);
}
});
} catch (final RuntimeException e) {
throw (GenerationException) e.getCause();
}
}
/**
* Checks if the given class has a method with the given name
*
* @param clazz The class
* @param methodKey The method key
* @return True if the class has a method with the given name
*/
private static Method computeMethod(final Class<?> clazz, final MethodKey methodKey) throws GenerationException {
return computeMethod(clazz, methodKey, false);
}
private static boolean typesMatch(final Class<?>[] types, final List<Class<?>> parameterTypes) {
for (var i = 0; i < types.length; i++) {
final var type = types[i];
final var parameterType = parameterTypes.get(i);
if (parameterType != null && !type.isAssignableFrom(parameterType)) {
return false;
}
}
return true;
}
/**
* Checks if the given class has a static method with the given name
* The result is cached
*
* @param clazz The class
* @param methodName The method name
* @return True if the class has a static method with the given name
*/
static boolean hasStaticMethod(final Class<?> clazz, final String methodName, final Class<?>... parameterTypes) {
final var methodMap = staticMethods.computeIfAbsent(clazz, c -> new ConcurrentHashMap<>());
final var methodKey = new MethodKey(methodName, Arrays.asList(parameterTypes));
final var method = methodMap.computeIfAbsent(methodKey, m -> {
try {
return computeStaticMethod(clazz, m);
} catch (final GenerationException ignored) {
return null;
}
});
return method != null;
}
/**
* Gets the static method corresponding to the given class and name
* The result is cached
*
* @param clazz The class
* @param methodName The method name
* @return The method
*/
static Method getStaticMethod(final Class<?> clazz, final String methodName, final Class<?>... parameterTypes) throws GenerationException {
final var methodMap = staticMethods.computeIfAbsent(clazz, c -> new ConcurrentHashMap<>());
final var methodKey = new MethodKey(methodName, Arrays.asList(parameterTypes));
try {
return methodMap.computeIfAbsent(methodKey, m -> {
try {
return computeStaticMethod(clazz, m);
} catch (final GenerationException e) {
throw new RuntimeException(e);
}
});
} catch (final RuntimeException e) {
throw (GenerationException) e.getCause();
}
}
/**
* Gets the static method corresponding to the given class and name
*
* @param clazz The class name
* @param methodKey The method name
* @return The method, or null if not found
*/
private static Method computeStaticMethod(final Class<?> clazz, final MethodKey methodKey) throws GenerationException {
return computeMethod(clazz, methodKey, true);
}
private static Method computeMethod(final Class<?> clazz, final MethodKey methodKey, final boolean isStatic) throws GenerationException {
final var parameterTypes = methodKey.parameterTypes();
if (parameterTypes.stream().allMatch(Objects::nonNull)) {
return computeExactMethod(clazz, methodKey, isStatic);
} else {
return computeInexactMethod(clazz, methodKey, isStatic);
}
}
private static Method computeExactMethod(final Class<?> clazz, final MethodKey methodKey, final boolean isStatic) throws GenerationException {
final var parameterTypes = methodKey.parameterTypes();
final var methodName = methodKey.methodName();
try {
final var method = clazz.getMethod(methodName, parameterTypes.toArray(new Class<?>[0]));
if (isStatic == Modifier.isStatic(method.getModifiers())) {
return method;
} else {
throw new GenerationException("Method not found : " + clazz + " - " + methodKey + " (found static method)");
}
} catch (final NoSuchMethodException ignored) {
return computeInexactMethod(clazz, methodKey, isStatic);
}
}
private static Method computeInexactMethod(final Class<?> clazz, final MethodKey methodKey, final boolean isStatic) throws GenerationException {
final var parameterTypes = methodKey.parameterTypes();
final var methodName = methodKey.methodName();
final var matching = Arrays.stream(clazz.getMethods()).filter(m -> {
if (m.getName().equals(methodName) && isStatic == Modifier.isStatic(m.getModifiers())) {
final var types = m.getParameterTypes();
return types.length == parameterTypes.size() && typesMatch(types, parameterTypes);
} else {
return false;
}
}).toList();
if (matching.size() == 1) {
return matching.getFirst();
} else if (matching.isEmpty()) {
throw new GenerationException("Method not found : " + clazz + " - " + methodKey);
} else {
throw new GenerationException("Multiple matching methods not supported yet : " + clazz + " - " + methodKey);
}
}
/**
* Checks if the given class has a valueOf(String) method
* The result is cached
*
* @param clazz The class
* @return True if the class has a valueOf(String)
*/
static boolean hasValueOf(final Class<?> clazz) {
return hasValueOf.computeIfAbsent(clazz, ReflectionHelper::computeHasValueOf);
}
/**
* Computes if the given class has a valueOf(String) method
*
* @param clazz The class
* @return True if the class has a valueOf(String)
*/
private static boolean computeHasValueOf(final Class<?> clazz) {
try {
clazz.getMethod("valueOf", String.class);
return true;
} catch (final NoSuchMethodException ignored) {
return false;
}
}
/**
* Computes the constructor arguments for the given constructor
*
* @param constructor The constructor
* @return The constructor arguments
*/
static ConstructorArgs getConstructorArgs(final Constructor<?> constructor) {
final var namedArgs = new LinkedHashMap<String, Parameter>();
final var annotationsArray = constructor.getParameterAnnotations();
var hasNamedArgs = 0;
for (var i = 0; i < annotationsArray.length; i++) {
final var annotations = annotationsArray[i];
final var getNamedArg = Arrays.stream(annotations).filter(NamedArg.class::isInstance).findFirst().orElse(null);
if (getNamedArg != null) {
hasNamedArgs++;
final var namedArg = (NamedArg) getNamedArg;
final var name = namedArg.value();
final var clazz = constructor.getParameterTypes()[i];
namedArgs.put(name, new Parameter(name, constructor.getParameterTypes()[i], namedArg.defaultValue().isEmpty() ?
getDefaultValue(clazz) : namedArg.defaultValue()));
}
}
if (hasNamedArgs != 0 && hasNamedArgs != annotationsArray.length) {
throw new IllegalStateException("Constructor " + constructor + " has both named and unnamed arguments");
} else {
return new ConstructorArgs(constructor, namedArgs);
}
}
/**
* Computes the default property for the given class
*
* @param className The class name
* @return The default property
* @throws GenerationException If the class is not found or no default property is found
*/
static String getDefaultProperty(final String className) throws GenerationException {
if (defaultProperty.containsKey(className)) {
return defaultProperty.get(className);
} else {
final var property = computeDefaultProperty(className);
if (property != null) {
defaultProperty.put(className, property);
}
return property;
}
}
/**
* Gets the wrapper class for the given class
*
* @param clazz The class
* @return The wrapper class (e.g. int.class -> Integer.class) or the original class if it is not a primitive
*/
static String getWrapperClass(final Class<?> clazz) {
final var name = clazz.getName();
if (name.contains(".") || Character.isUpperCase(name.charAt(0))) {
return name;
} else {
return MethodType.methodType(clazz).wrap().returnType().getName();
}
}
/**
* Gets the class for the given class name
*
* @param className The class name
* @return The class
* @throws GenerationException If the class is not found
*/
static Class<?> getClass(final String className) throws GenerationException {
if (classMap.containsKey(className)) {
return classMap.get(className);
} else {
final var clazz = computeClass(className);
classMap.put(className, clazz);
return clazz;
}
}
private static Class<?> computeClass(final String className) throws GenerationException {
if (PRIMITIVE_TYPES.containsKey(className)) {
return PRIMITIVE_TYPES.get(className);
} else {
try {
return Class.forName(className, false, Thread.currentThread().getContextClassLoader());
} catch (final ClassNotFoundException e) {
throw new GenerationException("Cannot find class " + className + " ; Is a dependency missing for the plugin?", e);
}
}
}
private static String computeDefaultProperty(final String className) throws GenerationException {
try {
final var clazz = Class.forName(className, false, Thread.currentThread().getContextClassLoader());
final var annotation = clazz.getAnnotation(DefaultProperty.class);
if (annotation == null) {
return null;
} else {
return annotation.value();
}
} catch (final ClassNotFoundException e) {
throw new GenerationException("Class " + className + " not found ; Either specify the property explicitly or put the class in a dependency", e);
}
}
/**
* Computes the default value for the given class
*
* @param clazz The class
* @return The value
*/
private static String getDefaultValue(final Class<?> clazz) {
final var primitiveWrappers = Set.of(Integer.class, Byte.class, Short.class, Long.class, Float.class, Double.class);
if (clazz == char.class || clazz == Character.class) {
return "\u0000";
} else if (clazz == boolean.class || clazz == Boolean.class) {
return "false";
} else if (clazz.isPrimitive() || primitiveWrappers.contains(clazz)) {
return "0";
} else {
return "null";
}
}
/**
* Gets the generic types for the given object
*
* @param parsedObject The parsed object
* @return The generic types
* @throws GenerationException if an error occurs
*/
String getGenericTypes(final ParsedObject parsedObject) throws GenerationException {
final var clazz = getClass(parsedObject.className());
if (isGeneric(clazz)) {
final var idProperty = parsedObject.attributes().get(FX_ID);
if (idProperty == null) {
logger.warn("No id found for generic class {} ; Using raw", clazz.getName());
return "";
} else {
final var id = idProperty.value();
final var fieldInfo = controllerInfo.fieldInfo(id);
if (fieldInfo == null) { //Not found
logger.warn("No field found for generic class {} (id={}) ; Using raw", clazz.getName(), id);
return "";
} else if (fieldInfo.isGeneric()) {
return formatGenerics(fieldInfo.genericTypes());
}
}
}
return "";
}
private static String formatGenerics(final List<? extends GenericTypes> genericTypes) {
final var sb = new StringBuilder();
sb.append("<");
for (var i = 0; i < genericTypes.size(); i++) {
final var genericType = genericTypes.get(i);
sb.append(genericType.name());
formatGenerics(genericType.subTypes(), sb);
if (i < genericTypes.size() - 1) {
sb.append(", ");
}
}
sb.append(">");
return sb.toString();
}
private static void formatGenerics(final List<? extends GenericTypes> genericTypes, final StringBuilder sb) {
if (!genericTypes.isEmpty()) {
sb.append("<");
for (var i = 0; i < genericTypes.size(); i++) {
final var genericType = genericTypes.get(i);
sb.append(genericType.name());
formatGenerics(genericType.subTypes(), sb);
if (i < genericTypes.size() - 1) {
sb.append(", ");
}
}
sb.append(">");
}
}
/**
* Gets the return type of the given instance method for the given class
*
* @param className The class
* @param methodName The method
* @param parameterTypes The method parameter types (null if any object is allowed)
* @return The return type
* @throws GenerationException if an error occurs
*/
static Class<?> getReturnType(final String className, final String methodName, final Class<?>... parameterTypes) throws GenerationException {
final var clazz = getClass(className);
return getReturnType(clazz, methodName, parameterTypes, false);
}
/**
* Gets the return type of the given static method for the given class
*
* @param className The class
* @param methodName The method
* @param parameterTypes The method parameter types (null if any object is allowed)
* @return The return type
* @throws GenerationException if an error occurs
*/
static Class<?> getStaticReturnType(final String className, final String methodName, final Class<?>... parameterTypes) throws GenerationException {
final var clazz = getClass(className);
return getReturnType(clazz, methodName, parameterTypes, true);
}
private static Class<?> getReturnType(final Class<?> clazz, final String methodName, final Class<?>[] parameterTypes, final boolean isStatic) throws GenerationException {
final var returnTypes = methodsReturnType.computeIfAbsent(clazz, c -> new ConcurrentHashMap<>());
try {
final var methodKey = new MethodKey(methodName, Arrays.asList(parameterTypes));
return returnTypes.computeIfAbsent(methodKey, m -> {
try {
return isStatic ? getStaticMethod(clazz, methodName, parameterTypes).getReturnType() :
getMethod(clazz, methodName, parameterTypes).getReturnType();
} catch (final GenerationException e) {
throw new RuntimeException(e);
}
});
} catch (final RuntimeException e) {
throw (GenerationException) e.getCause();
}
}
private record MethodKey(String methodName, List<Class<?>> parameterTypes) {
}
}
@@ -0,0 +1,121 @@
package ch.gtache.fxml.compiler.impl.internal;
import ch.gtache.fxml.compiler.GenerationException;
import ch.gtache.fxml.compiler.impl.GeneratorImpl;
import ch.gtache.fxml.compiler.parsing.ParsedDefine;
import ch.gtache.fxml.compiler.parsing.ParsedObject;
import ch.gtache.fxml.compiler.parsing.ParsedProperty;
import ch.gtache.fxml.compiler.parsing.impl.ParsedPropertyImpl;
import javafx.scene.paint.Color;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.stream.Stream;
import static ch.gtache.fxml.compiler.impl.internal.GenerationHelper.FX_ID;
import static ch.gtache.fxml.compiler.impl.internal.GenerationHelper.getSortedAttributes;
import static java.util.Objects.requireNonNull;
/**
* Helper methods for {@link GeneratorImpl} to format Scenes
*/
final class SceneFormatter {
private static final ParsedProperty ROOT_PROPERTY = new ParsedPropertyImpl("root", null, null);
private final HelperProvider helperProvider;
private final StringBuilder sb;
SceneFormatter(final HelperProvider helperProvider, final StringBuilder sb) {
this.helperProvider = requireNonNull(helperProvider);
this.sb = requireNonNull(sb);
}
void formatScene(final ParsedObject parsedObject, final String variableName) throws GenerationException {
checkPropertiesAndChildren(parsedObject);
formatDefines(parsedObject);
final var root = findRoot(parsedObject);
final var rootVariableName = helperProvider.getVariableProvider().getNextVariableName("root");
helperProvider.getObjectFormatter().format(root, rootVariableName);
final var sortedAttributes = getSortedAttributes(parsedObject);
double width = -1;
double height = -1;
var paint = Color.WHITE.toString();
final var stylesheets = new ArrayList<String>();
for (final var property : sortedAttributes) {
switch (property.name()) {
case FX_ID -> {
//Do nothing, handled in ObjectFormatter
}
case "width" -> width = Double.parseDouble(property.value());
case "height" -> height = Double.parseDouble(property.value());
case "fill" -> paint = property.value();
case "stylesheets" -> stylesheets.add(property.value());
default -> throw new GenerationException("Unknown font attribute : " + property.name());
}
}
sb.append(helperProvider.getCompatibilityHelper().getStartVar("javafx.scene.Scene")).append(variableName).append(" = new javafx.scene.Scene(").append(rootVariableName).append(", ")
.append(width).append(", ").append(height).append(", javafx.scene.paint.Color.valueOf(\"").append(paint).append("\"));\n");
addStylesheets(variableName, stylesheets);
}
private void formatDefines(final ParsedObject parsedObject) throws GenerationException {
final var objectFormatter = helperProvider.getObjectFormatter();
for (final var define : parsedObject.children()) {
if (define instanceof ParsedDefine) {
objectFormatter.format(define, helperProvider.getVariableProvider().getNextVariableName("define"));
}
}
for (final var define : parsedObject.properties().getOrDefault(ROOT_PROPERTY, List.of())) {
if (define instanceof ParsedDefine) {
objectFormatter.format(define, helperProvider.getVariableProvider().getNextVariableName("define"));
}
}
}
private static ParsedObject findRoot(final ParsedObject parsedObject) throws GenerationException {
final var rootPropertyChildren = parsedObject.properties().get(ROOT_PROPERTY);
if (rootPropertyChildren == null) {
return getNonDefineObjects(parsedObject.children()).findFirst()
.orElseThrow(() -> new GenerationException("Expected only one child for scene : " + parsedObject));
} else {
return getNonDefineObjects(rootPropertyChildren).findFirst()
.orElseThrow(() -> new GenerationException("Expected only one root property child for scene : " + parsedObject));
}
}
private static Stream<ParsedObject> getNonDefineObjects(final Collection<ParsedObject> objects) {
return objects.stream().filter(c -> !(c instanceof ParsedDefine));
}
private static void checkPropertiesAndChildren(final ParsedObject parsedObject) throws GenerationException {
if (parsedObject.properties().keySet().stream().anyMatch(k -> !k.equals(ROOT_PROPERTY))) {
throw new GenerationException("Unsupported scene properties : " + parsedObject);
}
final var nonDefineCount = getNonDefineObjects(parsedObject.children()).count();
final var rootPropertyChildren = parsedObject.properties().get(ROOT_PROPERTY);
if (rootPropertyChildren == null) {
if (nonDefineCount != 1) {
throw new GenerationException("Expected only one child for scene : " + parsedObject);
}
} else {
final var nonDefinePropertyChildren = getNonDefineObjects(rootPropertyChildren).count();
if (nonDefinePropertyChildren != 1) {
throw new GenerationException("Expected only one root property child for scene : " + parsedObject);
} else if (nonDefineCount != 0) {
throw new GenerationException("Expected no children for scene : " + parsedObject);
}
}
}
private void addStylesheets(final String variableName, final Collection<String> stylesheets) {
if (!stylesheets.isEmpty()) {
final var urlVariables = helperProvider.getURLFormatter().formatURL(stylesheets);
final var tmpVariable = helperProvider.getVariableProvider().getNextVariableName("stylesheets");
final var compatibilityHelper = helperProvider.getCompatibilityHelper();
sb.append(compatibilityHelper.getStartVar("java.util.List<String>")).append(tmpVariable).append(" = ").append(variableName).append(".getStyleSheets();\n");
sb.append(" ").append(tmpVariable).append(".addAll(").append(compatibilityHelper.getListOf()).append(String.join(", ", urlVariables)).append("));\n");
}
}
}
@@ -1,9 +1,9 @@
package com.github.gtache.fxml.compiler.impl.internal;
package ch.gtache.fxml.compiler.impl.internal;
import com.github.gtache.fxml.compiler.GenerationException;
import com.github.gtache.fxml.compiler.impl.GeneratorImpl;
import com.github.gtache.fxml.compiler.parsing.ParsedObject;
import com.github.gtache.fxml.compiler.parsing.ParsedProperty;
import ch.gtache.fxml.compiler.GenerationException;
import ch.gtache.fxml.compiler.impl.GeneratorImpl;
import ch.gtache.fxml.compiler.parsing.ParsedObject;
import ch.gtache.fxml.compiler.parsing.ParsedProperty;
import javafx.scene.shape.VertexFormat;
import java.util.ArrayList;
@@ -14,18 +14,23 @@ import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static com.github.gtache.fxml.compiler.impl.internal.GenerationHelper.*;
import static ch.gtache.fxml.compiler.impl.internal.GenerationHelper.*;
import static java.util.Objects.requireNonNull;
/**
* Helper methods for {@link GeneratorImpl} to format TriangleMeshes
*/
final class TriangleMeshBuilder {
final class TriangleMeshFormatter {
private TriangleMeshBuilder() {
private final HelperProvider helperProvider;
private final StringBuilder sb;
TriangleMeshFormatter(final HelperProvider helperProvider, final StringBuilder sb) {
this.helperProvider = requireNonNull(helperProvider);
this.sb = requireNonNull(sb);
}
static void formatTriangleMesh(final GenerationProgress progress, final ParsedObject parsedObject, final String variableName) throws GenerationException {
void formatTriangleMesh(final ParsedObject parsedObject, final String variableName) throws GenerationException {
if (parsedObject.children().isEmpty() && parsedObject.properties().isEmpty()) {
final var sortedAttributes = getSortedAttributes(parsedObject);
final var points = new ArrayList<Float>();
@@ -37,7 +42,7 @@ final class TriangleMeshBuilder {
for (final var property : sortedAttributes) {
switch (property.name().toLowerCase()) {
case FX_ID -> {
//Do nothing
//Do nothing, handled in ObjectFormatter
}
case "points" -> {
points.clear();
@@ -63,15 +68,13 @@ final class TriangleMeshBuilder {
default -> throw new GenerationException("Unknown TriangleMesh attribute : " + property.name());
}
}
final var sb = progress.stringBuilder();
sb.append(START_VAR).append(variableName).append(" = new javafx.scene.shape.TriangleMesh();\n");
setPoints(progress, variableName, points);
setTexCoords(progress, variableName, texCoords);
setNormals(progress, variableName, normals);
setFaces(progress, variableName, faces);
setFaceSmoothingGroups(progress, variableName, faceSmoothingGroups);
setVertexFormat(progress, variableName, vertexFormat);
handleId(progress, parsedObject, variableName);
sb.append(helperProvider.getCompatibilityHelper().getStartVar("javafx.scene.shape.TriangleMesh")).append(variableName).append(" = new javafx.scene.shape.TriangleMesh();\n");
setPoints(variableName, points);
setTexCoords(variableName, texCoords);
setNormals(variableName, normals);
setFaces(variableName, faces);
setFaceSmoothingGroups(variableName, faceSmoothingGroups);
setVertexFormat(variableName, vertexFormat);
} else {
throw new GenerationException("Image cannot have children or properties : " + parsedObject);
}
@@ -87,39 +90,39 @@ final class TriangleMeshBuilder {
}
}
private static void setPoints(final GenerationProgress progress, final String variableName, final Collection<Float> points) {
private void setPoints(final String variableName, final Collection<Float> points) {
if (!points.isEmpty()) {
progress.stringBuilder().append(" ").append(variableName).append(".getPoints().setAll(new float[]{").append(formatList(points)).append("});\n");
sb.append(INDENT_8).append(variableName).append(".getPoints().setAll(new float[]{").append(formatList(points)).append("});\n");
}
}
private static void setTexCoords(final GenerationProgress progress, final String variableName, final Collection<Float> texCoords) {
private void setTexCoords(final String variableName, final Collection<Float> texCoords) {
if (!texCoords.isEmpty()) {
progress.stringBuilder().append(" ").append(variableName).append(".getTexCoords().setAll(new float[]{").append(formatList(texCoords)).append("});\n");
sb.append(INDENT_8).append(variableName).append(".getTexCoords().setAll(new float[]{").append(formatList(texCoords)).append("});\n");
}
}
private static void setNormals(final GenerationProgress progress, final String variableName, final Collection<Float> normals) {
private void setNormals(final String variableName, final Collection<Float> normals) {
if (!normals.isEmpty()) {
progress.stringBuilder().append(" ").append(variableName).append(".getNormals().setAll(new float[]{").append(formatList(normals)).append("});\n");
sb.append(INDENT_8).append(variableName).append(".getNormals().setAll(new float[]{").append(formatList(normals)).append("});\n");
}
}
private static void setFaces(final GenerationProgress progress, final String variableName, final Collection<Integer> faces) {
private void setFaces(final String variableName, final Collection<Integer> faces) {
if (!faces.isEmpty()) {
progress.stringBuilder().append(" ").append(variableName).append(".getFaces().setAll(new int[]{").append(formatList(faces)).append("});\n");
sb.append(INDENT_8).append(variableName).append(".getFaces().setAll(new int[]{").append(formatList(faces)).append("});\n");
}
}
private static void setFaceSmoothingGroups(final GenerationProgress progress, final String variableName, final Collection<Integer> faceSmoothingGroups) {
private void setFaceSmoothingGroups(final String variableName, final Collection<Integer> faceSmoothingGroups) {
if (!faceSmoothingGroups.isEmpty()) {
progress.stringBuilder().append(" ").append(variableName).append(".getFaceSmoothingGroups().setAll(new int[]{").append(formatList(faceSmoothingGroups)).append("});\n");
sb.append(INDENT_8).append(variableName).append(".getFaceSmoothingGroups().setAll(new int[]{").append(formatList(faceSmoothingGroups)).append("});\n");
}
}
private static void setVertexFormat(final GenerationProgress progress, final String variableName, final VertexFormat vertexFormat) {
private void setVertexFormat(final String variableName, final VertexFormat vertexFormat) {
if (vertexFormat != null) {
progress.stringBuilder().append(" ").append(variableName).append(".setVertexFormat(javafx.scene.shape.VertexFormat.").append(vertexFormat).append(");\n");
sb.append(INDENT_8).append(variableName).append(".setVertexFormat(javafx.scene.shape.VertexFormat.").append(vertexFormat).append(");\n");
}
}
@@ -128,9 +131,8 @@ final class TriangleMeshBuilder {
}
private static <T> List<T> parseList(final CharSequence value, final Function<? super String, ? extends T> parser) {
final var splitPattern = Pattern.compile("[\\s+,]");
final var splitPattern = Pattern.compile("\\s*,\\s*|\\s+");
final var split = splitPattern.split(value);
return Arrays.stream(split).map(parser).collect(Collectors.toList());
return Arrays.stream(split).map(String::trim).filter(s -> !s.isEmpty()).map(parser).collect(Collectors.toList());
}
}
@@ -0,0 +1,73 @@
package ch.gtache.fxml.compiler.impl.internal;
import ch.gtache.fxml.compiler.GenerationException;
import ch.gtache.fxml.compiler.impl.GeneratorImpl;
import ch.gtache.fxml.compiler.parsing.ParsedObject;
import java.util.ArrayList;
import java.util.List;
import static ch.gtache.fxml.compiler.impl.internal.GenerationHelper.*;
import static java.util.Objects.requireNonNull;
/**
* Helper methods for {@link GeneratorImpl} to format URLs
*/
final class URLFormatter {
private final HelperProvider helperProvider;
private final StringBuilder sb;
URLFormatter(final HelperProvider helperProvider, final StringBuilder sb) {
this.helperProvider = requireNonNull(helperProvider);
this.sb = requireNonNull(sb);
}
List<String> formatURL(final Iterable<String> stylesheets) {
final var ret = new ArrayList<String>();
for (final var styleSheet : stylesheets) {
ret.add(formatURL(styleSheet));
}
return ret;
}
String formatURL(final String url) {
final var variableName = helperProvider.getVariableProvider().getNextVariableName("url");
if (url.startsWith(RELATIVE_PATH_PREFIX)) {
sb.append(getStartURL()).append(variableName).append(" = getClass().getResource(\"").append(url.substring(1)).append("\");\n");
} else {
sb.append(" final java.net.URL ").append(variableName).append(";\n");
sb.append(" try {\n");
sb.append(" ").append(variableName).append(" = new java.net.URI(\"").append(url).append("\").toURL();\n");
sb.append(" } catch (final java.net.MalformedURLException | java.net.URISyntaxException e) {\n");
sb.append(" throw new RuntimeException(\"Couldn't parse url : ").append(url).append("\", e);\n");
sb.append(" }\n");
}
return variableName;
}
void formatURL(final ParsedObject parsedObject, final String variableName) throws GenerationException {
if (parsedObject.children().isEmpty() && parsedObject.properties().isEmpty()) {
final var sortedAttributes = getSortedAttributes(parsedObject);
String value = null;
for (final var property : sortedAttributes) {
switch (property.name()) {
case FX_ID -> {
//Do nothing, handled in ObjectFormatter
}
case "value" -> value = property.value();
default -> throw new GenerationException("Unknown URL attribute : " + property.name());
}
}
//FIXME only relative path (@) ?
sb.append(getStartURL()).append(variableName).append(" = getClass().getResource(\"").append(value).append("\");\n");
} else {
throw new GenerationException("URL cannot have children or properties : " + parsedObject);
}
}
private String getStartURL() {
return helperProvider.getCompatibilityHelper().getStartVar("java.net.URL");
}
}
@@ -0,0 +1,81 @@
package ch.gtache.fxml.compiler.impl.internal;
import ch.gtache.fxml.compiler.GenerationException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;
/**
* Guesses the class of a value
*/
class ValueClassGuesser {
private final HelperProvider helperProvider;
ValueClassGuesser(final HelperProvider helperProvider) {
this.helperProvider = Objects.requireNonNull(helperProvider);
}
List<Class<?>> guess(final String value) throws GenerationException {
if (value.startsWith("$")) {
return getPossibleVariableTypes(value.substring(1));
} else {
return getPossibleTypes(value);
}
}
private List<Class<?>> getPossibleVariableTypes(final String value) throws GenerationException {
if (value.contains(".")) {
throw new GenerationException("Unsupported variable : " + value);
} else {
final var variableInfo = helperProvider.getVariableProvider().getVariableInfo(value);
if (variableInfo == null) {
throw new GenerationException("Unknown variable : " + value);
} else {
return List.of(ReflectionHelper.getClass(variableInfo.className()));
}
}
}
private static List<Class<?>> getPossibleTypes(final String value) {
final var ret = new ArrayList<Class<?>>();
ret.add(String.class);
ret.addAll(tryParse(value, LocalDateTime::parse, LocalDateTime.class));
ret.addAll(tryParse(value, LocalDate::parse, LocalDate.class));
ret.addAll(tryParse(value, ValueClassGuesser::parseBoolean, Boolean.class, boolean.class));
ret.addAll(tryParse(value, BigDecimal::new, BigDecimal.class));
ret.addAll(tryParse(value, Double::parseDouble, Double.class, double.class));
ret.addAll(tryParse(value, Float::parseFloat, Float.class, float.class));
ret.addAll(tryParse(value, BigInteger::new, BigInteger.class));
ret.addAll(tryParse(value, Long::parseLong, Long.class, long.class));
ret.addAll(tryParse(value, Integer::parseInt, Integer.class, int.class));
ret.addAll(tryParse(value, Short::parseShort, Short.class, short.class));
ret.addAll(tryParse(value, Byte::parseByte, Byte.class, byte.class));
return ret.reversed();
}
private static boolean parseBoolean(final String value) {
if (!value.equals("true") && !value.equals("false")) {
throw new RuntimeException("Invalid boolean value : " + value);
} else {
return Boolean.parseBoolean(value);
}
}
private static <T> Collection<Class<?>> tryParse(final String value, final Function<? super String, T> parseFunction, final Class<?>... classes) {
try {
parseFunction.apply(value);
return Arrays.asList(classes);
} catch (final RuntimeException ignored) {
//Do nothing
return List.of();
}
}
}
@@ -0,0 +1,137 @@
package ch.gtache.fxml.compiler.impl.internal;
import ch.gtache.fxml.compiler.GenerationException;
import ch.gtache.fxml.compiler.ResourceBundleInjectionType;
import ch.gtache.fxml.compiler.impl.GeneratorImpl;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.regex.Pattern;
import static ch.gtache.fxml.compiler.impl.internal.GenerationHelper.*;
import static java.util.Objects.requireNonNull;
/**
* Helper methods for {@link GeneratorImpl} to format values
*/
final class ValueFormatter {
private static final Pattern INT_PATTERN = Pattern.compile("\\d+");
private static final Pattern DECIMAL_PATTERN = Pattern.compile("\\d+(?:\\.\\d+)?");
private static final Pattern START_BACKSLASH_PATTERN = Pattern.compile("^\\\\");
private final HelperProvider helperProvider;
private final ResourceBundleInjectionType resourceInjectionType;
ValueFormatter(final HelperProvider helperProvider, final ResourceBundleInjectionType resourceInjectionType) {
this.helperProvider = requireNonNull(helperProvider);
this.resourceInjectionType = requireNonNull(resourceInjectionType);
}
/**
* Formats an argument to a method
*
* @param value The value
* @param parameterType The parameter type
* @return The formatted value
* @throws GenerationException if an error occurs
*/
String getArg(final String value, final Class<?> parameterType) throws GenerationException {
if (parameterType == String.class && value.startsWith(RESOURCE_KEY_PREFIX)) {
return getBundleValue(value.substring(1));
} else if (value.startsWith(RELATIVE_PATH_PREFIX)) {
final var subpath = value.substring(1);
return getResourceValue(subpath);
} else if (value.startsWith(BINDING_EXPRESSION_PREFIX)) {
throw new GenerationException("Should be handled by BindingFormatter");
} else if (value.startsWith(BIDIRECTIONAL_BINDING_PREFIX)) {
throw new GenerationException("Should be handled by BindingFormatter");
} else if (value.startsWith(EXPRESSION_PREFIX)) {
final var variable = helperProvider.getVariableProvider().getVariableInfo(value.substring(1));
if (variable == null) {
throw new GenerationException("Unknown variable : " + value.substring(1));
}
return variable.variableName();
} else {
return toString(value, parameterType);
}
}
private static String getResourceValue(final String subpath) {
return "getClass().getResource(\"" + subpath + "\").toString()";
}
/**
* Gets the resource bundle value for the given value
*
* @param value The value
* @return The resource bundle value
*/
private String getBundleValue(final String value) {
return switch (resourceInjectionType) {
case CONSTRUCTOR, GET_BUNDLE, CONSTRUCTOR_NAME -> "resourceBundle.getString(\"" + value + "\")";
case GETTER -> "controller.resources().getString(\"" + value + "\")";
case CONSTRUCTOR_FUNCTION -> "resourceBundleFunction.apply(\"" + value + "\")";
};
}
/**
* Computes the string value to use in the generated code
*
* @param value The value
* @param clazz The value class
* @return The computed string value
*/
static String toString(final String value, final Class<?> clazz) {
if (clazz == String.class) {
return "\"" + START_BACKSLASH_PATTERN.matcher(value).replaceAll("").replace("\\", "\\\\")
.replace("\"", "\\\"") + "\"";
} else if (clazz == char.class || clazz == Character.class) {
return "'" + value + "'";
} else if (clazz == boolean.class || clazz == Boolean.class) {
return value;
} else if (clazz == byte.class || clazz == Byte.class || clazz == short.class || clazz == Short.class ||
clazz == int.class || clazz == Integer.class || clazz == long.class || clazz == Long.class) {
return intToString(value, clazz);
} else if (clazz == float.class || clazz == Float.class || clazz == double.class || clazz == Double.class) {
return decimalToString(value, clazz);
} else if (clazz == LocalDate.class) {
return "LocalDate.parse(\"" + value + "\")";
} else if (clazz == LocalDateTime.class) {
return "LocalDateTime.parse(\"" + value + "\")";
} else if (ReflectionHelper.hasValueOf(clazz)) {
return valueOfToString(value, clazz);
} else {
return value;
}
}
private static String intToString(final String value, final Class<?> clazz) {
if (INT_PATTERN.matcher(value).matches()) {
return value;
} else {
return getValueOf(ReflectionHelper.getWrapperClass(clazz), value);
}
}
private static String decimalToString(final String value, final Class<?> clazz) {
if (DECIMAL_PATTERN.matcher(value).matches()) {
return value;
} else {
return getValueOf(ReflectionHelper.getWrapperClass(clazz), value);
}
}
private static String valueOfToString(final String value, final Class<?> clazz) {
if (clazz.isEnum()) {
return clazz.getCanonicalName() + "." + value;
} else {
return getValueOf(clazz.getCanonicalName(), value);
}
}
private static String getValueOf(final String clazz, final String value) {
return clazz + ".valueOf(\"" + value + "\")";
}
}
@@ -0,0 +1,32 @@
package ch.gtache.fxml.compiler.impl.internal;
import ch.gtache.fxml.compiler.parsing.ParsedObject;
import java.util.Objects;
/**
* Info about a variable
*
* @param id The fx:id of the variable
* @param parsedObject The parsed object of the variable
* @param variableName The variable name
* @param className The class name of the variable
*/
record VariableInfo(String id, ParsedObject parsedObject, String variableName, String className) {
/**
* Instantiates a new variable info
*
* @param id The fx:id of the variable
* @param parsedObject The parsed object of the variable
* @param variableName The variable name
* @param className The class name of the variable
* @throws NullPointerException if any parameter is null
*/
VariableInfo {
Objects.requireNonNull(id);
Objects.requireNonNull(parsedObject);
Objects.requireNonNull(variableName);
Objects.requireNonNull(className);
}
}
@@ -0,0 +1,53 @@
package ch.gtache.fxml.compiler.impl.internal;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Provider of variable names and info
*/
class VariableProvider {
private final Map<String, AtomicInteger> variableNameCounters;
private final Map<String, VariableInfo> idToVariableInfo;
/**
* Instantiates a new provider
*/
VariableProvider() {
this.variableNameCounters = new HashMap<>();
this.idToVariableInfo = new HashMap<>();
}
/**
* Gets the next available variable name for the given prefix
*
* @param prefix The variable name prefix
* @return The next available variable name
*/
String getNextVariableName(final String prefix) {
final var counter = variableNameCounters.computeIfAbsent(prefix, k -> new AtomicInteger(0));
return prefix + counter.getAndIncrement();
}
/**
* Adds a variable info
*
* @param id The variable id
* @param variableInfo The variable info
*/
void addVariableInfo(final String id, final VariableInfo variableInfo) {
idToVariableInfo.put(id, variableInfo);
}
/**
* Gets the variable info
*
* @param id The variable id
* @return The variable info
*/
VariableInfo getVariableInfo(final String id) {
return idToVariableInfo.get(id);
}
}
@@ -0,0 +1,109 @@
package ch.gtache.fxml.compiler.impl.internal;
import ch.gtache.fxml.compiler.GenerationException;
import ch.gtache.fxml.compiler.impl.GeneratorImpl;
import ch.gtache.fxml.compiler.parsing.ParsedObject;
import ch.gtache.fxml.compiler.parsing.ParsedProperty;
import static ch.gtache.fxml.compiler.impl.internal.GenerationHelper.FX_ID;
import static ch.gtache.fxml.compiler.impl.internal.GenerationHelper.getSortedAttributes;
import static java.util.Objects.requireNonNull;
/**
* Helper methods for {@link GeneratorImpl} to format WebViews
*/
final class WebViewFormatter {
private final HelperProvider helperProvider;
private final StringBuilder sb;
WebViewFormatter(final HelperProvider helperProvider, final StringBuilder sb) {
this.helperProvider = requireNonNull(helperProvider);
this.sb = requireNonNull(sb);
}
/**
* Formats a WebView object
*
* @param parsedObject The parsed object
* @param variableName The variable name
* @throws GenerationException if an error occurs
*/
void formatWebView(final ParsedObject parsedObject, final String variableName) throws GenerationException {
if (parsedObject.children().isEmpty() && parsedObject.properties().isEmpty()) {
final var sortedAttributes = getSortedAttributes(parsedObject);
final var compatibilityHelper = helperProvider.getCompatibilityHelper();
sb.append(compatibilityHelper.getStartVar("javafx.scene.web.WebView")).append(variableName).append(" = new javafx.scene.web.WebView();\n");
final var engineVariable = helperProvider.getVariableProvider().getNextVariableName("engine");
sb.append(compatibilityHelper.getStartVar("javafx.scene.web.WebEngine")).append(engineVariable).append(" = ").append(variableName).append(".getEngine();\n");
for (final var value : sortedAttributes) {
formatAttribute(value, parsedObject, variableName, engineVariable);
}
} else {
throw new GenerationException("WebView cannot have children or properties : " + parsedObject);
}
}
private void formatAttribute(final ParsedProperty value, final ParsedObject parsedObject,
final String variableName, final String engineVariable) throws GenerationException {
switch (value.name()) {
case FX_ID -> {
//Do nothing, handled in ObjectFormatter
}
case "confirmHandler" -> injectConfirmHandler(value, engineVariable);
case "createPopupHandler" -> injectCreatePopupHandler(value, engineVariable);
case "onAlert", "onResized", "onStatusChanged", "onVisibilityChanged" ->
injectEventHandler(value, engineVariable);
case "promptHandler" -> injectPromptHandler(value, engineVariable);
case "location" -> injectLocation(value, engineVariable);
default -> helperProvider.getPropertyFormatter().formatProperty(value, parsedObject, variableName);
}
}
private void injectConfirmHandler(final ParsedProperty value, final String engineVariable) throws GenerationException {
if (value.value().startsWith("#")) {
helperProvider.getControllerInjector().injectCallbackControllerMethod(value, engineVariable, "String.class");
} else {
setCallback(value, engineVariable);
}
}
private void injectCreatePopupHandler(final ParsedProperty value, final String engineVariable) throws GenerationException {
if (value.value().startsWith("#")) {
helperProvider.getControllerInjector().injectCallbackControllerMethod(value, engineVariable, "javafx.scene.web.PopupFeatures.class");
} else {
setCallback(value, engineVariable);
}
}
private void injectEventHandler(final ParsedProperty value, final String engineVariable) throws GenerationException {
if (value.value().startsWith("#")) {
helperProvider.getControllerInjector().injectEventHandlerControllerMethod(value, engineVariable);
} else {
helperProvider.getFieldSetter().setEventHandler(value, engineVariable);
}
}
private void injectPromptHandler(final ParsedProperty value, final String engineVariable) throws GenerationException {
if (value.value().startsWith("#")) {
helperProvider.getControllerInjector().injectCallbackControllerMethod(value, engineVariable, "javafx.scene.web.PromptData.class");
} else {
setCallback(value, engineVariable);
}
}
private void injectLocation(final ParsedProperty value, final String engineVariable) {
sb.append(" ").append(engineVariable).append(".load(\"").append(value.value()).append("\");\n");
}
/**
* Sets a callback field
*
* @param property The property to inject
* @param parentVariable The parent variable
*/
private void setCallback(final ParsedProperty property, final String parentVariable) throws GenerationException {
helperProvider.getFieldSetter().setField(property, parentVariable, "javafx.util.Callback");
}
}
@@ -0,0 +1,45 @@
package ch.gtache.fxml.compiler.parsing.impl;
import ch.gtache.fxml.compiler.parsing.ParsedConstant;
import ch.gtache.fxml.compiler.parsing.ParsedProperty;
import java.util.Map;
import java.util.Objects;
/**
* Implementation of {@link ParsedConstant}
*
* @param className The constant class
* @param attributes The constant attributes
*/
public record ParsedConstantImpl(String className, Map<String, ParsedProperty> attributes) implements ParsedConstant {
private static final String FX_CONSTANT = "fx:constant";
/**
* Instantiates the constant
*
* @param className The constant class
* @param attributes The constant attributes
* @throws NullPointerException if any argument is null
* @throws IllegalArgumentException If the attributes do not contain fx:constant
*/
public ParsedConstantImpl {
Objects.requireNonNull(className);
if (!attributes.containsKey(FX_CONSTANT)) {
throw new IllegalArgumentException("Missing " + FX_CONSTANT);
}
attributes = Map.copyOf(attributes);
}
/**
* Instantiates the constant
*
* @param className The constant class
* @param value The constant value
* @throws NullPointerException if any argument is null
*/
public ParsedConstantImpl(final String className, final String value) {
this(className, Map.of(FX_CONSTANT, new ParsedPropertyImpl(FX_CONSTANT, null, value)));
}
}
@@ -0,0 +1,40 @@
package ch.gtache.fxml.compiler.parsing.impl;
import ch.gtache.fxml.compiler.parsing.ParsedCopy;
import ch.gtache.fxml.compiler.parsing.ParsedProperty;
import java.util.Map;
/**
* Implementation of {@link ParsedCopy}
*
* @param attributes The copy attributes
*/
public record ParsedCopyImpl(Map<String, ParsedProperty> attributes) implements ParsedCopy {
private static final String SOURCE = "source";
/**
* Instantiates the copy
*
* @param attributes The copy attributes
* @throws NullPointerException If the attributes are null
* @throws IllegalArgumentException If the attributes don't contain source
*/
public ParsedCopyImpl {
attributes = Map.copyOf(attributes);
if (!attributes.containsKey(SOURCE)) {
throw new IllegalArgumentException("Missing " + SOURCE);
}
}
/**
* Instantiates the copy
*
* @param source The source
* @throws NullPointerException If the source is null
*/
public ParsedCopyImpl(final String source) {
this(Map.of(SOURCE, new ParsedPropertyImpl(SOURCE, null, source)));
}
}
@@ -0,0 +1,24 @@
package ch.gtache.fxml.compiler.parsing.impl;
import ch.gtache.fxml.compiler.parsing.ParsedDefine;
import ch.gtache.fxml.compiler.parsing.ParsedObject;
import java.util.List;
/**
* Implementation of {@link ParsedObject}
*
* @param children The objects in this define
*/
public record ParsedDefineImpl(List<ParsedObject> children) implements ParsedDefine {
/**
* Instantiates the define
*
* @param children The children
* @throws NullPointerException If the children are null
*/
public ParsedDefineImpl {
children = List.copyOf(children);
}
}
@@ -1,8 +1,8 @@
package com.github.gtache.fxml.compiler.parsing.impl;
package ch.gtache.fxml.compiler.parsing.impl;
import com.github.gtache.fxml.compiler.parsing.ParsedFactory;
import com.github.gtache.fxml.compiler.parsing.ParsedObject;
import com.github.gtache.fxml.compiler.parsing.ParsedProperty;
import ch.gtache.fxml.compiler.parsing.ParsedFactory;
import ch.gtache.fxml.compiler.parsing.ParsedObject;
import ch.gtache.fxml.compiler.parsing.ParsedProperty;
import java.util.List;
import java.util.Map;
@@ -13,13 +13,23 @@ import java.util.SequencedCollection;
* Implementation of {@link ParsedFactory}
*
* @param className The factory class
* @param attributes The factory properties
* @param attributes The factory attributes
* @param arguments The factory arguments
* @param children The factory children
*/
public record ParsedFactoryImpl(String className, Map<String, ParsedProperty> attributes,
SequencedCollection<ParsedObject> arguments,
SequencedCollection<ParsedObject> children) implements ParsedFactory {
/**
* Instantiates the factory
*
* @param className The factory class
* @param attributes The factory attributes
* @param arguments The factory arguments
* @param children The factory children
* @throws NullPointerException if any argument is null
*/
public ParsedFactoryImpl {
Objects.requireNonNull(className);
attributes = Map.copyOf(attributes);
@@ -0,0 +1,58 @@
package ch.gtache.fxml.compiler.parsing.impl;
import ch.gtache.fxml.compiler.parsing.ParsedInclude;
import ch.gtache.fxml.compiler.parsing.ParsedProperty;
import java.util.HashMap;
import java.util.Map;
import static java.util.Objects.requireNonNull;
/**
* Implementation of {@link ParsedInclude}
*
* @param attributes The include attributes
*/
public record ParsedIncludeImpl(Map<String, ParsedProperty> attributes) implements ParsedInclude {
private static final String SOURCE = "source";
/**
* Instantiates an include
*
* @param attributes The include attributes
* @throws NullPointerException If attributes is null
* @throws IllegalArgumentException If attributes does not contain source
*/
public ParsedIncludeImpl {
if (!attributes.containsKey(SOURCE)) {
throw new IllegalArgumentException("Missing " + SOURCE);
}
attributes = Map.copyOf(attributes);
}
/**
* Instantiates an include
*
* @param source The source
* @param resources The resources
* @param fxId The fx:id
* @throws NullPointerException If source is null
*/
public ParsedIncludeImpl(final String source, final String resources, final String fxId) {
this(createAttributes(source, resources, fxId));
}
private static Map<String, ParsedProperty> createAttributes(final String source, final String resources, final String fxId) {
requireNonNull(source);
final var map = HashMap.<String, ParsedProperty>newHashMap(3);
map.put(SOURCE, new ParsedPropertyImpl(SOURCE, null, source));
if (resources != null) {
map.put("resources", new ParsedPropertyImpl("resources", null, resources));
}
if (fxId != null) {
map.put("fx:id", new ParsedPropertyImpl("fx:id", null, fxId));
}
return map;
}
}
@@ -1,7 +1,7 @@
package com.github.gtache.fxml.compiler.parsing.impl;
package ch.gtache.fxml.compiler.parsing.impl;
import com.github.gtache.fxml.compiler.parsing.ParsedObject;
import com.github.gtache.fxml.compiler.parsing.ParsedProperty;
import ch.gtache.fxml.compiler.parsing.ParsedObject;
import ch.gtache.fxml.compiler.parsing.ParsedProperty;
import java.util.Collections;
import java.util.LinkedHashMap;
@@ -15,13 +15,23 @@ import java.util.SequencedMap;
* Implementation of {@link ParsedObject}
*
* @param className The object class
* @param attributes The object properties
* @param properties The object children (complex properties)
* @param attributes The object attributes
* @param properties The object properties
* @param children The object children
*/
public record ParsedObjectImpl(String className, Map<String, ParsedProperty> attributes,
SequencedMap<ParsedProperty, SequencedCollection<ParsedObject>> properties,
SequencedCollection<ParsedObject> children) implements ParsedObject {
/**
* Instantiates a new object
*
* @param className The object class
* @param attributes The object attributes
* @param properties The object properties
* @param children The object children
* @throws NullPointerException if any parameter is null
*/
public ParsedObjectImpl {
Objects.requireNonNull(className);
attributes = Map.copyOf(attributes);
@@ -0,0 +1,27 @@
package ch.gtache.fxml.compiler.parsing.impl;
import ch.gtache.fxml.compiler.parsing.ParsedProperty;
import static java.util.Objects.requireNonNull;
/**
* Implementation of {@link ParsedProperty}
*
* @param name The property name
* @param sourceType The property source type
* @param value The property value
*/
public record ParsedPropertyImpl(String name, String sourceType, String value) implements ParsedProperty {
/**
* Instantiates a property
*
* @param name The property name
* @param sourceType The property source type
* @param value The property value
* @throws NullPointerException If the name is null
*/
public ParsedPropertyImpl {
requireNonNull(name);
}
}
@@ -0,0 +1,40 @@
package ch.gtache.fxml.compiler.parsing.impl;
import ch.gtache.fxml.compiler.parsing.ParsedProperty;
import ch.gtache.fxml.compiler.parsing.ParsedReference;
import java.util.Map;
/**
* Implementation of {@link ParsedReference}
*
* @param attributes The reference attributes
*/
public record ParsedReferenceImpl(Map<String, ParsedProperty> attributes) implements ParsedReference {
private static final String SOURCE = "source";
/**
* Instantiates a new reference
*
* @param attributes The reference attributes
* @throws NullPointerException If the attributes are null
* @throws IllegalArgumentException If the attributes do not contain source
*/
public ParsedReferenceImpl {
if (!attributes.containsKey(SOURCE)) {
throw new IllegalArgumentException("Missing " + SOURCE);
}
attributes = Map.copyOf(attributes);
}
/**
* Instantiates a new reference
*
* @param source The reference source
* @throws NullPointerException If the source is null
*/
public ParsedReferenceImpl(final String source) {
this(Map.of(SOURCE, new ParsedPropertyImpl(SOURCE, null, source)));
}
}
@@ -1,6 +1,6 @@
package com.github.gtache.fxml.compiler.parsing.impl;
package ch.gtache.fxml.compiler.parsing.impl;
import com.github.gtache.fxml.compiler.parsing.ParsedText;
import ch.gtache.fxml.compiler.parsing.ParsedText;
import java.util.Objects;
@@ -0,0 +1,45 @@
package ch.gtache.fxml.compiler.parsing.impl;
import ch.gtache.fxml.compiler.parsing.ParsedProperty;
import ch.gtache.fxml.compiler.parsing.ParsedValue;
import java.util.Map;
import java.util.Objects;
/**
* Implementation of {@link ParsedValue}
*
* @param className The value class
* @param attributes The value properties
*/
public record ParsedValueImpl(String className, Map<String, ParsedProperty> attributes) implements ParsedValue {
private static final String FX_VALUE = "fx:value";
/**
* Instantiates a new value
*
* @param className The value class
* @param attributes The value properties
* @throws NullPointerException If any parameter is null
* @throws IllegalArgumentException If the attributes don't contain fx:value
*/
public ParsedValueImpl {
Objects.requireNonNull(className);
if (!attributes.containsKey(FX_VALUE)) {
throw new IllegalArgumentException("Missing " + FX_VALUE);
}
attributes = Map.copyOf(attributes);
}
/**
* Instantiates a new value
*
* @param className The value class
* @param value The value
* @throws NullPointerException If any parameter is null
*/
public ParsedValueImpl(final String className, final String value) {
this(className, Map.of(FX_VALUE, new ParsedPropertyImpl(FX_VALUE, null, value)));
}
}
@@ -1,26 +0,0 @@
package com.github.gtache.fxml.compiler.impl;
import com.github.gtache.fxml.compiler.ControllerInjection;
import com.github.gtache.fxml.compiler.InjectionType;
/**
* Base field {@link InjectionType}s for {@link ControllerInjection}
*/
public enum ControllerFieldInjectionTypes implements InjectionType {
/**
* Inject using variable assignment
*/
ASSIGN,
/**
* Inject using a factory
*/
FACTORY,
/**
* Inject using reflection
*/
REFLECTION,
/**
* Inject using setters
*/
SETTERS
}
@@ -1,21 +0,0 @@
package com.github.gtache.fxml.compiler.impl;
import com.github.gtache.fxml.compiler.ControllerFieldInfo;
import com.github.gtache.fxml.compiler.ControllerInfo;
import java.util.Map;
/**
* Implementation of {@link ControllerInfo}
*
* @param handlerHasArgument The mapping of method name to true if the method has an argument
* @param fieldInfo The mapping of property name to controller field info
*/
public record ControllerInfoImpl(Map<String, Boolean> handlerHasArgument,
Map<String, ControllerFieldInfo> fieldInfo) implements ControllerInfo {
public ControllerInfoImpl {
handlerHasArgument = Map.copyOf(handlerHasArgument);
fieldInfo = Map.copyOf(fieldInfo);
}
}

Some files were not shown because too many files have changed in this diff Show More