Adds BindingFormatter, ExpressionFormatter, ValueClassGuesser, fixes constructor args matching, fixes factory methods

This commit is contained in:
Guillaume Tâche
2024-12-30 23:22:33 +01:00
parent b5c38bea54
commit c3d9e72e42
32 changed files with 1378 additions and 166 deletions

View File

@@ -73,6 +73,7 @@ Optionally add dependencies to the plugin (e.g. when using MediaView and control
- Compile-time validation - Compile-time validation
- Faster startup speed for the application - Faster startup speed for the application
- Possibility to use controller factories to instantiate controllers with final fields - Possibility to use controller factories to instantiate controllers with final fields
- Very basic support for bidirectional bindings
- Easier time with JPMS - Easier time with JPMS
- No need to open the controllers packages to javafx.fxml - 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 - No need to open the resources packages when using use-image-inputstream-constructor (if images or resource bundles
@@ -81,8 +82,8 @@ Optionally add dependencies to the plugin (e.g. when using MediaView and control
## Disadvantages ## Disadvantages
- `fx:script` is not supported - `fx:script` is not supported
- Possible bugs (file an issue if you see one) - Expect some bugs (file an issue if you see one)
- Expression binding is (very) limited - Expression binding support is very basic
- Probably not fully compatible with all FXML features (file an issue if you need one in specific) - 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 - All fxml files must have a `fx:controller` attribute

View File

@@ -0,0 +1,110 @@
package com.github.gtache.fxml.compiler.impl.internal;
import com.github.gtache.fxml.compiler.ControllerFieldInjectionType;
import com.github.gtache.fxml.compiler.GenerationException;
import com.github.gtache.fxml.compiler.parsing.ParsedObject;
import com.github.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 com.github.gtache.fxml.compiler.impl.internal.GenerationHelper.*;
import static java.util.Objects.requireNonNull;
/**
* Formatter for property bindings
*/
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);
if (isControllerWithFactory(value)) {
controllerFactoryPostAction.add(INDENT_8 + parentVariable + "." + methodName + "()." + bindMethod + "(" + expression + ");\n");
} else {
sb.append(INDENT_8).append(parentVariable).append(".").append(methodName).append("().").append(bindMethod).append("(").append(expression).append(");\n");
}
} 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);
}
}

View File

@@ -7,8 +7,12 @@ import com.github.gtache.fxml.compiler.parsing.ParsedObject;
import java.lang.reflect.Constructor; import java.lang.reflect.Constructor;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
/** /**
@@ -16,6 +20,7 @@ import java.util.Set;
*/ */
final class ConstructorHelper { final class ConstructorHelper {
private ConstructorHelper() { private ConstructorHelper() {
} }
@@ -50,41 +55,111 @@ final class ConstructorHelper {
} }
/** /**
* Gets the constructor arguments that best match the given property names * Gets the constructor arguments that best match the given properties
* *
* @param constructors The constructors * @param constructors The constructors
* @param allPropertyNames The property names * @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 * @return The matching constructor arguments, or null if no constructor matches and no default constructor exists
*/ */
static ConstructorArgs getMatchingConstructorArgs(final Constructor<?>[] constructors, final Set<String> allPropertyNames) { static ConstructorArgs getMatchingConstructorArgs(final Constructor<?>[] constructors, final Map<String, List<Class<?>>> properties) {
ConstructorArgs matchingConstructorArgs = null; final var argsDistance = getArgsDistance(constructors, properties);
for (final var constructor : constructors) { final var distances = argsDistance.keySet().stream().sorted().toList();
final var constructorArgs = ReflectionHelper.getConstructorArgs(constructor); for (final var distance : distances) {
final var matchingArgsCount = getMatchingArgsCount(constructorArgs, allPropertyNames); final var matching = argsDistance.get(distance);
if (matchingConstructorArgs == null ? matchingArgsCount > 0 : matchingArgsCount > getMatchingArgsCount(matchingConstructorArgs, allPropertyNames)) { final var argsTypeDistance = getArgsTypeDistance(matching, properties);
matchingConstructorArgs = constructorArgs; 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"));
} }
} }
if (matchingConstructorArgs == null) { //No matching constructor
return Arrays.stream(constructors).filter(c -> c.getParameterCount() == 0).findFirst() return Arrays.stream(constructors).filter(c -> c.getParameterCount() == 0).findFirst()
.map(c -> new ConstructorArgs(c, new LinkedHashMap<>())).orElse(null); .map(c -> new ConstructorArgs(c, new LinkedHashMap<>())).orElse(null);
} else {
return matchingConstructorArgs;
}
} }
/** /**
* Checks how many arguments of the given constructor match the given property names * Computes the mapping of distance (difference between number of properties and number of matching arguments) to constructor arguments
* *
* @param constructorArgs The constructor arguments * @param constructors The constructors
* @param allPropertyNames The property names * @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 * @return The number of matching arguments
*/ */
private static long getMatchingArgsCount(final ConstructorArgs constructorArgs, final Set<String> allPropertyNames) { private static long getMatchingArgsCount(final ConstructorArgs constructorArgs, final Map<String, List<Class<?>>> properties) {
if (constructorArgs.namedArgs().keySet().stream().anyMatch(s -> !allPropertyNames.contains(s))) { return constructorArgs.namedArgs().keySet().stream().filter(properties::containsKey).count();
return 0;
} else {
return constructorArgs.namedArgs().keySet().stream().filter(allPropertyNames::contains).count();
}
} }
} }

View File

@@ -0,0 +1,114 @@
package com.github.gtache.fxml.compiler.impl.internal;
import com.github.gtache.fxml.compiler.ControllerFieldInjectionType;
import com.github.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(".")) {
return getDotExpression(expression, returnType);
} else {
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);
if (referenced.equals("controller")) {
return getControllerExpression(value, returnType);
} else {
return getNonControllerExpression(referenced, value);
}
} else {
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());
}
}
}
}

View File

@@ -13,7 +13,6 @@ import java.util.Map;
final class GenerationHelper { final class GenerationHelper {
static final String INDENT_8 = " "; static final String INDENT_8 = " ";
static final String INDENT_12 = " ";
static final String FX_ID = "fx:id"; static final String FX_ID = "fx:id";
static final String FX_VALUE = "fx:value"; static final String FX_VALUE = "fx:value";
static final String VALUE = "value"; static final String VALUE = "value";
@@ -24,7 +23,7 @@ final class GenerationHelper {
static final String RESOURCE_KEY_PREFIX = "%"; static final String RESOURCE_KEY_PREFIX = "%";
static final String EXPRESSION_PREFIX = "$"; static final String EXPRESSION_PREFIX = "$";
static final String BINDING_EXPRESSION_PREFIX = "${"; static final String BINDING_EXPRESSION_PREFIX = "${";
static final String BI_DIRECTIONAL_BINDING_PREFIX = "#{"; static final String BIDIRECTIONAL_BINDING_PREFIX = "#{";
private GenerationHelper() { private GenerationHelper() {
@@ -67,7 +66,27 @@ final class GenerationHelper {
* @return The getter method name * @return The getter method name
*/ */
static String getGetMethod(final String propertyName) { static String getGetMethod(final String propertyName) {
return "get" + propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1); 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");
} }
/** /**
@@ -87,7 +106,11 @@ final class GenerationHelper {
* @return The setter method name * @return The setter method name
*/ */
static String getSetMethod(final String propertyName) { static String getSetMethod(final String propertyName) {
return "set" + propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1); return getMethod(propertyName, "set");
}
private static String getMethod(final String propertyName, final String methodPrefix) {
return methodPrefix + propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1);
} }
/** /**

View File

@@ -22,6 +22,15 @@ public class HelperProvider {
this.helpers = new HashMap<>(); 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() { ControllerInjector getControllerInjector() {
return (ControllerInjector) helpers.computeIfAbsent(ControllerInjector.class, c -> { return (ControllerInjector) helpers.computeIfAbsent(ControllerInjector.class, c -> {
final var request = progress.request(); final var request = progress.request();
@@ -35,6 +44,14 @@ public class HelperProvider {
}); });
} }
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() { FieldSetter getFieldSetter() {
return (FieldSetter) helpers.computeIfAbsent(FieldSetter.class, c -> { return (FieldSetter) helpers.computeIfAbsent(FieldSetter.class, c -> {
final var fieldInjectionType = progress.request().parameters().fieldInjectionType(); final var fieldInjectionType = progress.request().parameters().fieldInjectionType();
@@ -135,6 +152,10 @@ public class HelperProvider {
}); });
} }
ValueClassGuesser getValueClassGuesser() {
return (ValueClassGuesser) helpers.computeIfAbsent(ValueClassGuesser.class, c -> new ValueClassGuesser(this));
}
VariableProvider getVariableProvider() { VariableProvider getVariableProvider() {
return (VariableProvider) helpers.computeIfAbsent(VariableProvider.class, c -> new VariableProvider()); return (VariableProvider) helpers.computeIfAbsent(VariableProvider.class, c -> new VariableProvider());
} }

View File

@@ -54,12 +54,12 @@ public final class LoadMethodFormatter {
if (fieldInjectionType == ControllerFieldInjectionType.FACTORY) { if (fieldInjectionType == ControllerFieldInjectionType.FACTORY) {
sb.append(generationCompatibilityHelper.getStartVar("java.util.Map<String, Object>")).append("fieldMap = new java.util.HashMap<String, Object>();\n"); sb.append(generationCompatibilityHelper.getStartVar("java.util.Map<String, Object>")).append("fieldMap = new java.util.HashMap<String, Object>();\n");
} else if (controllerInjectionType == ControllerInjectionType.FACTORY) { } else if (controllerInjectionType == ControllerInjectionType.FACTORY) {
sb.append(" controller = controllerFactory.create();\n"); sb.append(" controller = controllerFactory.get();\n");
} }
final var variableName = helperProvider.getVariableProvider().getNextVariableName(GenerationHelper.getVariablePrefix(rootObject)); final var variableName = helperProvider.getVariableProvider().getNextVariableName(GenerationHelper.getVariablePrefix(rootObject));
helperProvider.getObjectFormatter().format(rootObject, variableName); helperProvider.getObjectFormatter().format(rootObject, variableName);
if (fieldInjectionType == ControllerFieldInjectionType.FACTORY) { if (fieldInjectionType == ControllerFieldInjectionType.FACTORY) {
sb.append(" controller = controllerFactory.create(fieldMap);\n"); sb.append(" controller = controllerFactory.apply(fieldMap);\n");
progress.controllerFactoryPostAction().forEach(sb::append); progress.controllerFactoryPostAction().forEach(sb::append);
} }
if (request.controllerInfo().hasInitialize()) { if (request.controllerInfo().hasInitialize()) {

View File

@@ -3,18 +3,27 @@ package com.github.gtache.fxml.compiler.impl.internal;
import com.github.gtache.fxml.compiler.GenerationException; import com.github.gtache.fxml.compiler.GenerationException;
import com.github.gtache.fxml.compiler.GenerationRequest; import com.github.gtache.fxml.compiler.GenerationRequest;
import com.github.gtache.fxml.compiler.impl.GeneratorImpl; import com.github.gtache.fxml.compiler.impl.GeneratorImpl;
import com.github.gtache.fxml.compiler.parsing.*; import com.github.gtache.fxml.compiler.parsing.ParsedConstant;
import com.github.gtache.fxml.compiler.parsing.ParsedCopy;
import com.github.gtache.fxml.compiler.parsing.ParsedDefine;
import com.github.gtache.fxml.compiler.parsing.ParsedFactory;
import com.github.gtache.fxml.compiler.parsing.ParsedInclude;
import com.github.gtache.fxml.compiler.parsing.ParsedObject;
import com.github.gtache.fxml.compiler.parsing.ParsedReference;
import com.github.gtache.fxml.compiler.parsing.ParsedText;
import com.github.gtache.fxml.compiler.parsing.ParsedValue;
import com.github.gtache.fxml.compiler.parsing.impl.ParsedPropertyImpl; import com.github.gtache.fxml.compiler.parsing.impl.ParsedPropertyImpl;
import javafx.scene.Node;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashSet; import java.util.HashMap;
import java.util.List;
import java.util.SequencedCollection; import java.util.SequencedCollection;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors;
import static com.github.gtache.fxml.compiler.impl.internal.GenerationHelper.*; import static com.github.gtache.fxml.compiler.impl.internal.GenerationHelper.*;
import static java.util.Objects.requireNonNull; import static java.util.Objects.requireNonNull;
@@ -271,19 +280,26 @@ final class ObjectFormatter {
final var clazz = ReflectionHelper.getClass(parsedObject.className()); final var clazz = ReflectionHelper.getClass(parsedObject.className());
final var children = parsedObject.children(); final var children = parsedObject.children();
final var notDefinedChildren = getNotDefines(children); final var notDefinedChildren = getNotDefines(children);
final var constructors = clazz.getConstructors(); final var allProperties = new HashMap<String, List<Class<?>>>();
final var allAttributesNames = new HashSet<>(parsedObject.attributes().keySet()); for (final var entry : parsedObject.attributes().entrySet()) {
allAttributesNames.addAll(parsedObject.properties().keySet().stream().map(ParsedProperty::name).collect(Collectors.toSet())); 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); formatDefines(children);
if (!notDefinedChildren.isEmpty()) { if (!notDefinedChildren.isEmpty()) {
final var defaultProperty = ReflectionHelper.getDefaultProperty(parsedObject.className()); final var defaultProperty = ReflectionHelper.getDefaultProperty(parsedObject.className());
if (defaultProperty != null) { if (defaultProperty != null) {
allAttributesNames.add(defaultProperty); allProperties.put(defaultProperty, List.of(Node.class, Node[].class));
} }
} }
final var constructorArgs = ConstructorHelper.getMatchingConstructorArgs(constructors, allAttributesNames); final var constructors = clazz.getConstructors();
final var constructorArgs = ConstructorHelper.getMatchingConstructorArgs(constructors, allProperties);
if (constructorArgs == null) { if (constructorArgs == null) {
formatNoConstructor(parsedObject, variableName, allAttributesNames); logger.debug("No constructor found for {} with attributes {}", clazz.getCanonicalName(), allProperties);
formatNoConstructor(parsedObject, variableName, allProperties.keySet());
} else { } else {
formatConstructor(parsedObject, variableName, constructorArgs); formatConstructor(parsedObject, variableName, constructorArgs);
} }
@@ -296,7 +312,7 @@ final class ObjectFormatter {
final var property = parsedObject.attributes().get("fx:constant"); 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"); sb.append(helperProvider.getCompatibilityHelper().getStartVar(parsedObject)).append(variableName).append(" = ").append(clazz.getCanonicalName()).append(".").append(property.value()).append(";\n");
} else { } else {
throw new GenerationException("Cannot find constructor for " + clazz.getCanonicalName()); throw new GenerationException("Cannot find empty constructor for " + clazz.getCanonicalName());
} }
} }
@@ -338,10 +354,9 @@ final class ObjectFormatter {
* @param subNodeName The sub node name * @param subNodeName The sub node name
*/ */
private void formatInclude(final ParsedInclude include, final String subNodeName) throws GenerationException { private void formatInclude(final ParsedInclude include, final String subNodeName) throws GenerationException {
final var subViewVariable = helperProvider.getVariableProvider().getNextVariableName("view");
final var viewVariable = helperProvider.getInitializationFormatter().formatSubViewConstructorCall(include); final var viewVariable = helperProvider.getInitializationFormatter().formatSubViewConstructorCall(include);
sb.append(" final javafx.scene.Parent ").append(subNodeName).append(" = ").append(viewVariable).append(".load();\n"); sb.append(" final javafx.scene.Parent ").append(subNodeName).append(" = ").append(viewVariable).append(".load();\n");
injectSubController(include, subViewVariable); injectSubController(include, viewVariable);
} }
private void injectSubController(final ParsedInclude include, final String subViewVariable) { private void injectSubController(final ParsedInclude include, final String subViewVariable) {
@@ -449,7 +464,7 @@ final class ObjectFormatter {
sb.append(compatibilityHelper.getStartVar(factory.className())).append(variableName).append(" = ").append(factory.className()) sb.append(compatibilityHelper.getStartVar(factory.className())).append(variableName).append(" = ").append(factory.className())
.append(".").append(factory.factory()).append("(").append(String.join(", ", variables)).append(");\n"); .append(".").append(factory.factory()).append("(").append(String.join(", ", variables)).append(");\n");
} else { } else {
final var returnType = ReflectionHelper.getReturnType(factory.className(), factory.factory()); final var returnType = ReflectionHelper.getStaticReturnType(factory.className(), factory.factory()).getName();
sb.append(compatibilityHelper.getStartVar(returnType)).append(variableName).append(" = ").append(factory.className()) sb.append(compatibilityHelper.getStartVar(returnType)).append(variableName).append(" = ").append(factory.className())
.append(".").append(factory.factory()).append("(").append(String.join(", ", variables)).append(");\n"); .append(".").append(factory.factory()).append("(").append(String.join(", ", variables)).append(");\n");
} }

View File

@@ -12,6 +12,7 @@ import com.github.gtache.fxml.compiler.parsing.impl.ParsedPropertyImpl;
import javafx.event.EventHandler; import javafx.event.EventHandler;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.SequencedCollection; import java.util.SequencedCollection;
@@ -41,17 +42,22 @@ final class PropertyFormatter {
* @throws GenerationException if an error occurs * @throws GenerationException if an error occurs
*/ */
void formatProperty(final ParsedProperty property, final ParsedObject parent, final String parentVariable) throws GenerationException { void formatProperty(final ParsedProperty property, final ParsedObject parent, final String parentVariable) throws GenerationException {
final var propertyName = property.name(); final var value = property.value();
if (propertyName.equals(FX_ID)) { if (value.endsWith("}") && (value.startsWith(BINDING_EXPRESSION_PREFIX) || value.startsWith(BIDIRECTIONAL_BINDING_PREFIX))) {
//Do nothing helperProvider.getBindingFormatter().formatBinding(property, parent, parentVariable);
} 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 { } else {
handleProperty(property, parent, parentVariable); 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);
}
} }
} }
@@ -93,8 +99,8 @@ final class PropertyFormatter {
private void handleStaticProperty(final ParsedProperty property, final String parentVariable, final String propertyName) throws GenerationException { private void handleStaticProperty(final ParsedProperty property, final String parentVariable, final String propertyName) throws GenerationException {
final var setMethod = getSetMethod(propertyName); final var setMethod = getSetMethod(propertyName);
final var propertySourceTypeClass = ReflectionHelper.getClass(property.sourceType()); final var propertySourceTypeClass = ReflectionHelper.getClass(property.sourceType());
if (ReflectionHelper.hasStaticMethod(propertySourceTypeClass, setMethod)) { if (ReflectionHelper.hasStaticMethod(propertySourceTypeClass, setMethod, null, null)) {
final var method = ReflectionHelper.getStaticMethod(propertySourceTypeClass, setMethod); final var method = ReflectionHelper.getStaticMethod(propertySourceTypeClass, setMethod, null, null);
final var parameterType = method.getParameterTypes()[1]; final var parameterType = method.getParameterTypes()[1];
final var arg = helperProvider.getValueFormatter().getArg(property.value(), parameterType); final var arg = helperProvider.getValueFormatter().getArg(property.value(), parameterType);
setLaterIfNeeded(property, parameterType, " " + property.sourceType() + "." + setMethod + "(" + parentVariable + ", " + arg + ");\n"); setLaterIfNeeded(property, parameterType, " " + property.sourceType() + "." + setMethod + "(" + parentVariable + ", " + arg + ");\n");
@@ -108,7 +114,7 @@ final class PropertyFormatter {
final var setMethod = getSetMethod(propertyName); final var setMethod = getSetMethod(propertyName);
final var getMethod = getGetMethod(propertyName); final var getMethod = getGetMethod(propertyName);
final var parentClass = ReflectionHelper.getClass(parent.className()); final var parentClass = ReflectionHelper.getClass(parent.className());
if (ReflectionHelper.hasMethod(parentClass, setMethod)) { if (ReflectionHelper.hasMethod(parentClass, setMethod, (Class<?>) null)) {
handleSetProperty(property, parentClass, parentVariable); handleSetProperty(property, parentClass, parentVariable);
} else if (ReflectionHelper.hasMethod(parentClass, getMethod)) { } else if (ReflectionHelper.hasMethod(parentClass, getMethod)) {
handleGetProperty(property, parentClass, parentVariable); handleGetProperty(property, parentClass, parentVariable);
@@ -119,7 +125,7 @@ final class PropertyFormatter {
private void handleSetProperty(final ParsedProperty property, final Class<?> parentClass, final String parentVariable) throws GenerationException { private void handleSetProperty(final ParsedProperty property, final Class<?> parentClass, final String parentVariable) throws GenerationException {
final var setMethod = getSetMethod(property.name()); final var setMethod = getSetMethod(property.name());
final var method = ReflectionHelper.getMethod(parentClass, setMethod); final var method = ReflectionHelper.getMethod(parentClass, setMethod, (Class<?>) null);
final var parameterType = method.getParameterTypes()[0]; final var parameterType = method.getParameterTypes()[0];
final var arg = helperProvider.getValueFormatter().getArg(property.value(), parameterType); final var arg = helperProvider.getValueFormatter().getArg(property.value(), parameterType);
setLaterIfNeeded(property, parameterType, " " + parentVariable + "." + setMethod + "(" + arg + ");\n"); setLaterIfNeeded(property, parameterType, " " + parentVariable + "." + setMethod + "(" + arg + ");\n");
@@ -129,7 +135,7 @@ final class PropertyFormatter {
final var getMethod = getGetMethod(property.name()); final var getMethod = getGetMethod(property.name());
final var method = ReflectionHelper.getMethod(parentClass, getMethod); final var method = ReflectionHelper.getMethod(parentClass, getMethod);
final var returnType = method.getReturnType(); final var returnType = method.getReturnType();
if (ReflectionHelper.hasMethod(returnType, "addAll")) { if (ReflectionHelper.hasMethod(returnType, "addAll", List.class)) {
final var arg = helperProvider.getValueFormatter().getArg(property.value(), String.class); final var arg = helperProvider.getValueFormatter().getArg(property.value(), String.class);
setLaterIfNeeded(property, String.class, " " + parentVariable + "." + getMethod + "().addAll(" + setLaterIfNeeded(property, String.class, " " + parentVariable + "." + getMethod + "().addAll(" +
helperProvider.getCompatibilityHelper().getListOf() + arg + "));\n"); helperProvider.getCompatibilityHelper().getListOf() + arg + "));\n");
@@ -155,7 +161,6 @@ final class PropertyFormatter {
} }
} }
/** /**
* Formats the children objects of a property * Formats the children objects of a property
* *
@@ -231,7 +236,7 @@ final class PropertyFormatter {
final var getMethod = getGetMethod(property); final var getMethod = getGetMethod(property);
final var parentClass = ReflectionHelper.getClass(parent.className()); final var parentClass = ReflectionHelper.getClass(parent.className());
final var sb = progress.stringBuilder(); final var sb = progress.stringBuilder();
if (ReflectionHelper.hasMethod(parentClass, setMethod)) { if (ReflectionHelper.hasMethod(parentClass, setMethod, (Class<?>) null)) {
sb.append(" ").append(parentVariable).append(".").append(setMethod).append("(").append(variableName).append(");\n"); sb.append(" ").append(parentVariable).append(".").append(setMethod).append("(").append(variableName).append(");\n");
} else if (ReflectionHelper.hasMethod(parentClass, getMethod)) { } else if (ReflectionHelper.hasMethod(parentClass, getMethod)) {
//Probably a list method that has only one element //Probably a list method that has only one element
@@ -251,7 +256,8 @@ final class PropertyFormatter {
private void formatSingleChildStatic(final String variableName, private void formatSingleChildStatic(final String variableName,
final ParsedProperty property, final String parentVariable) throws GenerationException { final ParsedProperty property, final String parentVariable) throws GenerationException {
final var setMethod = getSetMethod(property); final var setMethod = getSetMethod(property);
if (ReflectionHelper.hasStaticMethod(ReflectionHelper.getClass(property.sourceType()), setMethod)) { final var clazz = ReflectionHelper.getClass(property.sourceType());
if (ReflectionHelper.hasStaticMethod(clazz, setMethod, null, null)) {
progress.stringBuilder().append(" ").append(property.sourceType()).append(".").append(setMethod) progress.stringBuilder().append(" ").append(property.sourceType()).append(".").append(setMethod)
.append("(").append(parentVariable).append(", ").append(variableName).append(");\n"); .append("(").append(parentVariable).append(", ").append(variableName).append(");\n");
} else { } else {

View File

@@ -6,7 +6,6 @@ import com.github.gtache.fxml.compiler.GenericTypes;
import com.github.gtache.fxml.compiler.parsing.ParsedObject; import com.github.gtache.fxml.compiler.parsing.ParsedObject;
import javafx.beans.DefaultProperty; import javafx.beans.DefaultProperty;
import javafx.beans.NamedArg; import javafx.beans.NamedArg;
import javafx.scene.Node;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
@@ -15,7 +14,6 @@ import java.lang.reflect.Constructor;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.lang.reflect.Modifier; import java.lang.reflect.Modifier;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -30,12 +28,13 @@ import static com.github.gtache.fxml.compiler.impl.internal.GenerationHelper.FX_
*/ */
final class ReflectionHelper { final class ReflectionHelper {
private static final Logger logger = LogManager.getLogger(ReflectionHelper.class); private static final Logger logger = LogManager.getLogger(ReflectionHelper.class);
private static final Map<String, Class<?>> classMap = new HashMap<>(); private static final Map<String, Class<?>> classMap = new ConcurrentHashMap<>();
private static final Map<Class<?>, Boolean> hasValueOf = new HashMap<>(); private static final Map<Class<?>, Boolean> hasValueOf = new ConcurrentHashMap<>();
private static final Map<Class<?>, Boolean> isGeneric = new HashMap<>(); private static final Map<Class<?>, Boolean> isGeneric = new ConcurrentHashMap<>();
private static final Map<String, String> defaultProperty = new HashMap<>(); private static final Map<String, String> defaultProperty = new ConcurrentHashMap<>();
private static final Map<Class<?>, Map<String, Method>> methods = new HashMap<>(); private static final Map<Class<?>, Map<MethodKey, Method>> methods = new ConcurrentHashMap<>();
private static final Map<Class<?>, Map<String, Method>> staticMethods = new HashMap<>(); 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( private static final Map<String, Class<?>> PRIMITIVE_TYPES = Map.of(
"boolean", boolean.class, "boolean", boolean.class,
@@ -65,60 +64,72 @@ final class ReflectionHelper {
} }
/** /**
* Checks if the given class has a method with the given name * Checks if the given class has a method with the given name.
* The result is cached * The result is cached
* *
* @param clazz The class * @param clazz The class
* @param methodName The method name * @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 * @return True if the class has a method with the given name
*/ */
static boolean hasMethod(final Class<?> clazz, final String methodName) { static boolean hasMethod(final Class<?> clazz, final String methodName, final Class<?>... parameterTypes) {
final var methodMap = methods.computeIfAbsent(clazz, c -> new ConcurrentHashMap<>()); final var methodMap = methods.computeIfAbsent(clazz, c -> new ConcurrentHashMap<>());
final var method = methodMap.computeIfAbsent(methodName, m -> computeMethod(clazz, m)); 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; return method != null;
} }
/** /**
* Gets the method corresponding to the given class and name * Gets the method corresponding to the given class and name.
* The result is cached * The result is cached
* *
* @param clazz The class * @param clazz The class
* @param methodName The method name * @param methodName The method name
* @param parameterTypes The method parameter types
* @return The method * @return The method
*/ */
static Method getMethod(final Class<?> clazz, final String methodName) { static Method getMethod(final Class<?> clazz, final String methodName, final Class<?>... parameterTypes) throws GenerationException {
final var methodMap = methods.computeIfAbsent(clazz, c -> new ConcurrentHashMap<>()); final var methodMap = methods.computeIfAbsent(clazz, c -> new ConcurrentHashMap<>());
return methodMap.computeIfAbsent(methodName, m -> computeMethod(clazz, m)); 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 * Checks if the given class has a method with the given name
* *
* @param clazz The class * @param clazz The class
* @param methodName The method name * @param methodKey The method key
* @return True if the class has a method with the given name * @return True if the class has a method with the given name
*/ */
private static Method computeMethod(final Class<?> clazz, final String methodName) { private static Method computeMethod(final Class<?> clazz, final MethodKey methodKey) throws GenerationException {
final var matching = Arrays.stream(clazz.getMethods()).filter(m -> { return computeMethod(clazz, methodKey, false);
if (m.getName().equals(methodName) && !Modifier.isStatic(m.getModifiers())) { }
final var parameterTypes = m.getParameterTypes();
return methodName.startsWith("get") ? parameterTypes.length == 0 : parameterTypes.length >= 1; //TODO not very clean private static boolean typesMatch(final Class<?>[] types, final List<Class<?>> parameterTypes) {
} else { 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 false;
} }
}).toList();
if (matching.size() > 1) {
final var varargsFilter = matching.stream().filter(Method::isVarArgs).toList();
if (varargsFilter.size() == 1) {
return varargsFilter.getFirst();
} else {
throw new UnsupportedOperationException("Multiple matching methods not supported yet : " + clazz + " - " + methodName);
}
} else if (matching.size() == 1) {
return matching.getFirst();
} else {
return null;
} }
return true;
} }
/** /**
@@ -129,9 +140,16 @@ final class ReflectionHelper {
* @param methodName The method name * @param methodName The method name
* @return True if the class has a static method with the given name * @return True if the class has a static method with the given name
*/ */
static boolean hasStaticMethod(final Class<?> clazz, final String methodName) { static boolean hasStaticMethod(final Class<?> clazz, final String methodName, final Class<?>... parameterTypes) {
final var methodMap = staticMethods.computeIfAbsent(clazz, c -> new ConcurrentHashMap<>()); final var methodMap = staticMethods.computeIfAbsent(clazz, c -> new ConcurrentHashMap<>());
final var method = methodMap.computeIfAbsent(methodName, m -> computeStaticMethod(clazz, m)); 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; return method != null;
} }
@@ -143,33 +161,74 @@ final class ReflectionHelper {
* @param methodName The method name * @param methodName The method name
* @return The method * @return The method
*/ */
static Method getStaticMethod(final Class<?> clazz, final String methodName) { 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 methodMap = staticMethods.computeIfAbsent(clazz, c -> new ConcurrentHashMap<>());
return methodMap.computeIfAbsent(methodName, m -> computeStaticMethod(clazz, m)); 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 * Gets the static method corresponding to the given class and name
* *
* @param clazz The class name * @param clazz The class name
* @param methodName The method name * @param methodKey The method name
* @return The method, or null if not found * @return The method, or null if not found
*/ */
private static Method computeStaticMethod(final Class<?> clazz, final String methodName) { 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 -> { final var matching = Arrays.stream(clazz.getMethods()).filter(m -> {
if (m.getName().equals(methodName) && Modifier.isStatic(m.getModifiers())) { if (m.getName().equals(methodName) && isStatic == Modifier.isStatic(m.getModifiers())) {
final var parameterTypes = m.getParameterTypes(); final var types = m.getParameterTypes();
return parameterTypes.length > 1 && parameterTypes[0] == Node.class; return types.length == parameterTypes.size() && typesMatch(types, parameterTypes);
} else { } else {
return false; return false;
} }
}).toList(); }).toList();
if (matching.size() > 1) { if (matching.size() == 1) {
throw new UnsupportedOperationException("Multiple matching methods not supported yet : " + clazz + " - " + methodName);
} else if (matching.size() == 1) {
return matching.getFirst(); return matching.getFirst();
} else if (matching.isEmpty()) {
throw new GenerationException("Method not found : " + clazz + " - " + methodKey);
} else { } else {
return null; throw new GenerationException("Multiple matching methods not supported yet : " + clazz + " - " + methodKey);
} }
} }
@@ -383,17 +442,50 @@ final class ReflectionHelper {
} }
/** /**
* Gets the return type of the given method for the given class * Gets the return type of the given instance method for the given class
* *
* @param className The class * @param className The class
* @param methodName The method * @param methodName The method
* @param parameterTypes The method parameter types (null if any object is allowed)
* @return The return type * @return The return type
* @throws GenerationException if an error occurs * @throws GenerationException if an error occurs
*/ */
static String getReturnType(final String className, final String methodName) throws GenerationException { static Class<?> getReturnType(final String className, final String methodName, final Class<?>... parameterTypes) throws GenerationException {
final var clazz = getClass(className); final var clazz = getClass(className);
final var method = Arrays.stream(clazz.getMethods()).filter(m -> m.getName().equals(methodName)) return getReturnType(clazz, methodName, parameterTypes, false);
.findFirst().orElseThrow(() -> new GenerationException("Method " + methodName + " not found in class " + className)); }
return method.getReturnType().getName();
/**
* 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) {
} }
} }

View File

@@ -0,0 +1,81 @@
package com.github.gtache.fxml.compiler.impl.internal;
import com.github.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();
}
}
}

View File

@@ -4,6 +4,8 @@ import com.github.gtache.fxml.compiler.GenerationException;
import com.github.gtache.fxml.compiler.ResourceBundleInjectionType; import com.github.gtache.fxml.compiler.ResourceBundleInjectionType;
import com.github.gtache.fxml.compiler.impl.GeneratorImpl; import com.github.gtache.fxml.compiler.impl.GeneratorImpl;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import static com.github.gtache.fxml.compiler.impl.internal.GenerationHelper.*; import static com.github.gtache.fxml.compiler.impl.internal.GenerationHelper.*;
@@ -41,7 +43,9 @@ final class ValueFormatter {
final var subpath = value.substring(1); final var subpath = value.substring(1);
return getResourceValue(subpath); return getResourceValue(subpath);
} else if (value.startsWith(BINDING_EXPRESSION_PREFIX)) { } else if (value.startsWith(BINDING_EXPRESSION_PREFIX)) {
throw new GenerationException("Not implemented yet"); 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)) { } else if (value.startsWith(EXPRESSION_PREFIX)) {
final var variable = helperProvider.getVariableProvider().getVariableInfo(value.substring(1)); final var variable = helperProvider.getVariableProvider().getVariableInfo(value.substring(1));
if (variable == null) { if (variable == null) {
@@ -92,6 +96,10 @@ final class ValueFormatter {
return intToString(value, clazz); return intToString(value, clazz);
} else if (clazz == float.class || clazz == Float.class || clazz == double.class || clazz == Double.class) { } else if (clazz == float.class || clazz == Float.class || clazz == double.class || clazz == Double.class) {
return decimalToString(value, clazz); 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)) { } else if (ReflectionHelper.hasValueOf(clazz)) {
return valueOfToString(value, clazz); return valueOfToString(value, clazz);
} else { } else {

View File

@@ -5,6 +5,7 @@ module com.github.gtache.fxml.compiler.core {
requires transitive com.github.gtache.fxml.compiler.api; requires transitive com.github.gtache.fxml.compiler.api;
requires transitive javafx.graphics; requires transitive javafx.graphics;
requires org.apache.logging.log4j; requires org.apache.logging.log4j;
requires java.sql;
exports com.github.gtache.fxml.compiler.impl; exports com.github.gtache.fxml.compiler.impl;
exports com.github.gtache.fxml.compiler.parsing.impl; exports com.github.gtache.fxml.compiler.parsing.impl;

View File

@@ -0,0 +1,131 @@
package com.github.gtache.fxml.compiler.impl.internal;
import com.github.gtache.fxml.compiler.ControllerFieldInjectionType;
import com.github.gtache.fxml.compiler.GenerationException;
import com.github.gtache.fxml.compiler.parsing.ParsedObject;
import com.github.gtache.fxml.compiler.parsing.ParsedProperty;
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.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.SequencedCollection;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class TestBindingFormatter {
private final HelperProvider helperProvider;
private final ExpressionFormatter expressionFormatter;
private final ControllerFieldInjectionType fieldInjectionType;
private final StringBuilder sb;
private final SequencedCollection<String> controllerFactoryPostAction;
private final ParsedProperty property;
private final ParsedObject parent;
private final String parentVariable;
private final BindingFormatter bindingFormatter;
TestBindingFormatter(@Mock final HelperProvider helperProvider, @Mock final ExpressionFormatter expressionFormatter,
@Mock final ControllerFieldInjectionType fieldInjectionType, @Mock final ParsedProperty property, @Mock final ParsedObject parent) {
this.helperProvider = Objects.requireNonNull(helperProvider);
this.expressionFormatter = Objects.requireNonNull(expressionFormatter);
this.fieldInjectionType = Objects.requireNonNull(fieldInjectionType);
this.property = Objects.requireNonNull(property);
this.parent = Objects.requireNonNull(parent);
this.parentVariable = "parentVariable";
this.sb = new StringBuilder();
this.controllerFactoryPostAction = new ArrayList<>();
this.bindingFormatter = new BindingFormatter(helperProvider, fieldInjectionType, sb, controllerFactoryPostAction);
}
@BeforeEach
void beforeEach() throws GenerationException {
when(helperProvider.getExpressionFormatter()).thenReturn(expressionFormatter);
when(expressionFormatter.format(anyString(), any())).then(i -> i.getArgument(0) + "-" + i.getArgument(1));
}
@Test
void testFormatDoesntEndValid() {
when(property.value()).thenReturn("${value");
assertThrows(GenerationException.class, () -> bindingFormatter.formatBinding(property, parent, parentVariable));
}
@Test
void testFormatDoesntStartValid() {
when(property.value()).thenReturn("value}");
assertThrows(GenerationException.class, () -> bindingFormatter.formatBinding(property, parent, parentVariable));
}
@Test
void testFormatSimpleNoReadProperty() {
when(property.name()).thenReturn("abc");
when(property.value()).thenReturn("${value}");
when(parent.className()).thenReturn("javafx.scene.control.Label");
assertThrows(GenerationException.class, () -> bindingFormatter.formatBinding(property, parent, parentVariable));
}
@Test
void testFormatSimple() throws GenerationException {
when(property.name()).thenReturn("text");
when(property.value()).thenReturn("${value}");
when(parent.className()).thenReturn("javafx.scene.control.Label");
bindingFormatter.formatBinding(property, parent, parentVariable);
final var expected = """
parentVariable.textProperty().bind(${value}-class javafx.beans.property.StringProperty);
""";
assertEquals(expected, sb.toString());
assertEquals(List.of(), controllerFactoryPostAction);
}
@Test
void testFormatBidirectional() throws GenerationException {
when(property.name()).thenReturn("text");
when(property.value()).thenReturn("#{value}");
when(parent.className()).thenReturn("javafx.scene.control.Label");
bindingFormatter.formatBinding(property, parent, parentVariable);
final var expected = """
parentVariable.textProperty().bindBidirectional(#{value}-class javafx.beans.property.StringProperty);
""";
assertEquals(expected, sb.toString());
assertEquals(List.of(), controllerFactoryPostAction);
}
@Test
void testFormatSimpleControllerFactory() throws GenerationException {
final var factoryFormatter = new BindingFormatter(helperProvider, ControllerFieldInjectionType.FACTORY, sb, controllerFactoryPostAction);
when(property.name()).thenReturn("text");
when(property.value()).thenReturn("${controller.value}");
when(parent.className()).thenReturn("javafx.scene.control.Label");
factoryFormatter.formatBinding(property, parent, parentVariable);
final var expected = """
parentVariable.textProperty().bind(${controller.value}-class javafx.beans.property.StringProperty);
""";
assertEquals("", sb.toString());
assertEquals(List.of(expected), controllerFactoryPostAction);
}
@Test
void testFormatBidirectionalNoWriteProperty() {
when(property.name()).thenReturn("labelPadding");
when(property.value()).thenReturn("#{value}");
when(parent.className()).thenReturn("javafx.scene.control.Label");
assertThrows(GenerationException.class, () -> bindingFormatter.formatBinding(property, parent, parentVariable));
}
@Test
void testIllegal() {
assertThrows(NullPointerException.class, () -> new BindingFormatter(null, fieldInjectionType, sb, controllerFactoryPostAction));
assertThrows(NullPointerException.class, () -> new BindingFormatter(helperProvider, null, sb, controllerFactoryPostAction));
assertThrows(NullPointerException.class, () -> new BindingFormatter(helperProvider, fieldInjectionType, null, controllerFactoryPostAction));
assertThrows(NullPointerException.class, () -> new BindingFormatter(helperProvider, fieldInjectionType, sb, null));
}
}

View File

@@ -5,6 +5,7 @@ import com.github.gtache.fxml.compiler.parsing.ParsedObject;
import com.github.gtache.fxml.compiler.parsing.ParsedProperty; import com.github.gtache.fxml.compiler.parsing.ParsedProperty;
import com.github.gtache.fxml.compiler.parsing.impl.ParsedPropertyImpl; import com.github.gtache.fxml.compiler.parsing.impl.ParsedPropertyImpl;
import javafx.beans.NamedArg; import javafx.beans.NamedArg;
import javafx.scene.control.Spinner;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock; import org.mockito.Mock;
@@ -12,6 +13,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
import java.lang.annotation.Annotation; import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor; import java.lang.reflect.Constructor;
import java.time.LocalDate;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
@@ -89,7 +91,93 @@ class TestConstructorHelper {
new NamedArgImpl("p2", "value2")}}); new NamedArgImpl("p2", "value2")}});
when(constructors[1].getParameterTypes()).thenReturn(new Class[]{int.class, String.class}); when(constructors[1].getParameterTypes()).thenReturn(new Class[]{int.class, String.class});
final var expectedArgs = new ConstructorArgs(constructors[1], namedArgs); final var expectedArgs = new ConstructorArgs(constructors[1], namedArgs);
assertEquals(expectedArgs, ConstructorHelper.getMatchingConstructorArgs(constructors, propertyNames)); assertEquals(expectedArgs, ConstructorHelper.getMatchingConstructorArgs(constructors, Map.of("p1", List.of(double.class, int.class), "p2", List.of(String.class))));
}
@Test
void testGetMatchingConstructorArgsSpinnerIntFull() throws NoSuchMethodException {
final var spinnerProperties = Map.of("min", List.<Class<?>>of(int.class, double.class, LocalDate.class),
"max", List.<Class<?>>of(int.class, double.class, LocalDate.class), "initialValue",
List.<Class<?>>of(int.class, double.class, LocalDate.class), "amountToStepBy", List.<Class<?>>of(int.class, double.class, LocalDate.class));
final var namedArgs = new LinkedHashMap<String, Parameter>();
namedArgs.put("min", new Parameter("min", int.class, "0"));
namedArgs.put("max", new Parameter("max", int.class, "0"));
namedArgs.put("initialValue", new Parameter("initialValue", int.class, "0"));
namedArgs.put("amountToStepBy", new Parameter("amountToStepBy", int.class, "0"));
final var spinnerConstructors = Spinner.class.getConstructors();
final var constructor = Spinner.class.getConstructor(int.class, int.class, int.class, int.class);
final var expectedArgs = new ConstructorArgs(constructor, namedArgs);
assertEquals(expectedArgs, ConstructorHelper.getMatchingConstructorArgs(spinnerConstructors, spinnerProperties));
}
@Test
void testGetMatchingConstructorArgsSpinnerMismatch() throws NoSuchMethodException {
final var spinnerProperties = Map.of("min", List.<Class<?>>of(double.class),
"max", List.<Class<?>>of(int.class), "initialValue",
List.<Class<?>>of(double.class, LocalDate.class));
final var spinnerConstructors = Spinner.class.getConstructors();
final var constructor = Spinner.class.getConstructor();
final var expectedArgs = new ConstructorArgs(constructor, new LinkedHashMap<>());
assertEquals(expectedArgs, ConstructorHelper.getMatchingConstructorArgs(spinnerConstructors, spinnerProperties));
}
@Test
void testGetMatchingConstructorArgsSpinnerDouble2() throws NoSuchMethodException {
final var spinnerProperties = Map.of("min", List.<Class<?>>of(int.class, double.class, LocalDate.class),
"max", List.<Class<?>>of(int.class, double.class), "initialValue",
List.<Class<?>>of(double.class, LocalDate.class));
final var namedArgs = new LinkedHashMap<String, Parameter>();
namedArgs.put("min", new Parameter("min", double.class, "0"));
namedArgs.put("max", new Parameter("max", double.class, "0"));
namedArgs.put("initialValue", new Parameter("initialValue", double.class, "0"));
final var spinnerConstructors = Spinner.class.getConstructors();
final var constructor = Spinner.class.getConstructor(double.class, double.class, double.class);
final var expectedArgs = new ConstructorArgs(constructor, namedArgs);
assertEquals(expectedArgs, ConstructorHelper.getMatchingConstructorArgs(spinnerConstructors, spinnerProperties));
}
@Test
void testGetMatchingConstructorArgsSpinnerInt() throws NoSuchMethodException {
final var spinnerProperties = Map.of("min", List.<Class<?>>of(int.class, double.class, LocalDate.class),
"max", List.<Class<?>>of(int.class, double.class, LocalDate.class), "initialValue",
List.<Class<?>>of(int.class, double.class, LocalDate.class));
final var namedArgs = new LinkedHashMap<String, Parameter>();
namedArgs.put("min", new Parameter("min", int.class, "0"));
namedArgs.put("max", new Parameter("max", int.class, "0"));
namedArgs.put("initialValue", new Parameter("initialValue", int.class, "0"));
final var spinnerConstructors = Spinner.class.getConstructors();
final var constructor = Spinner.class.getConstructor(int.class, int.class, int.class);
final var expectedArgs = new ConstructorArgs(constructor, namedArgs);
assertEquals(expectedArgs, ConstructorHelper.getMatchingConstructorArgs(spinnerConstructors, spinnerProperties));
}
@Test
void testGetMatchingConstructorArgsSpinnerDouble() throws NoSuchMethodException {
final var spinnerProperties = Map.of("min", List.<Class<?>>of(double.class, LocalDate.class),
"max", List.<Class<?>>of(double.class, LocalDate.class), "initialValue",
List.<Class<?>>of(double.class, LocalDate.class));
final var namedArgs = new LinkedHashMap<String, Parameter>();
namedArgs.put("min", new Parameter("min", double.class, "0"));
namedArgs.put("max", new Parameter("max", double.class, "0"));
namedArgs.put("initialValue", new Parameter("initialValue", double.class, "0"));
final var spinnerConstructors = Spinner.class.getConstructors();
final var constructor = Spinner.class.getConstructor(double.class, double.class, double.class);
final var expectedArgs = new ConstructorArgs(constructor, namedArgs);
assertEquals(expectedArgs, ConstructorHelper.getMatchingConstructorArgs(spinnerConstructors, spinnerProperties));
}
@Test
void testGetMatchingConstructorArgsSpinnerPartial() throws NoSuchMethodException {
final var spinnerProperties = Map.of("min", List.<Class<?>>of(int.class, double.class, LocalDate.class));
final var namedArgs = new LinkedHashMap<String, Parameter>();
namedArgs.put("min", new Parameter("min", int.class, "0"));
namedArgs.put("max", new Parameter("max", int.class, "0"));
namedArgs.put("initialValue", new Parameter("initialValue", int.class, "0"));
final var spinnerConstructors = Spinner.class.getConstructors();
final var constructor = Spinner.class.getConstructor(int.class, int.class, int.class);
final var expectedArgs = new ConstructorArgs(constructor, namedArgs);
assertEquals(expectedArgs, ConstructorHelper.getMatchingConstructorArgs(spinnerConstructors, spinnerProperties));
} }
@Test @Test
@@ -101,7 +189,7 @@ class TestConstructorHelper {
when(constructors[0].getParameterCount()).thenReturn(0); when(constructors[0].getParameterCount()).thenReturn(0);
when(constructors[1].getParameterCount()).thenReturn(1); when(constructors[1].getParameterCount()).thenReturn(1);
final var expectedArgs = new ConstructorArgs(constructors[0], namedArgs); final var expectedArgs = new ConstructorArgs(constructors[0], namedArgs);
assertEquals(expectedArgs, ConstructorHelper.getMatchingConstructorArgs(constructors, propertyNames)); assertEquals(expectedArgs, ConstructorHelper.getMatchingConstructorArgs(constructors, Map.of("p1", List.of(int.class), "p2", List.of(int.class))));
} }
@Test @Test
@@ -110,10 +198,9 @@ class TestConstructorHelper {
when(constructors[1].getParameterAnnotations()).thenReturn(EMPTY_ANNOTATIONS); when(constructors[1].getParameterAnnotations()).thenReturn(EMPTY_ANNOTATIONS);
when(constructors[0].getParameterCount()).thenReturn(1); when(constructors[0].getParameterCount()).thenReturn(1);
when(constructors[1].getParameterCount()).thenReturn(1); when(constructors[1].getParameterCount()).thenReturn(1);
assertNull(ConstructorHelper.getMatchingConstructorArgs(constructors, propertyNames)); assertNull(ConstructorHelper.getMatchingConstructorArgs(constructors, Map.of("p1", List.of(int.class), "p2", List.of(int.class))));
} }
private record NamedArgImpl(String value, String defaultValue) implements NamedArg { private record NamedArgImpl(String value, String defaultValue) implements NamedArg {
@Override @Override

View File

@@ -0,0 +1,136 @@
package com.github.gtache.fxml.compiler.impl.internal;
import com.github.gtache.fxml.compiler.ControllerFieldInjectionType;
import com.github.gtache.fxml.compiler.GenerationException;
import javafx.beans.property.StringProperty;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
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.assertThrows;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class TestExpressionFormatter {
private final HelperProvider helperProvider;
private final GenerationCompatibilityHelper compatibilityHelper;
private final VariableProvider variableProvider;
private final VariableInfo variableInfo;
private final ControllerFieldInjectionType fieldInjectionType;
private final StringBuilder sb;
private final ExpressionFormatter formatter;
TestExpressionFormatter(@Mock final HelperProvider helperProvider, @Mock final GenerationCompatibilityHelper compatibilityHelper,
@Mock final VariableProvider variableProvider, @Mock final ControllerFieldInjectionType fieldInjectionType,
@Mock final VariableInfo variableInfo) {
this.helperProvider = Objects.requireNonNull(helperProvider);
this.compatibilityHelper = Objects.requireNonNull(compatibilityHelper);
this.variableProvider = Objects.requireNonNull(variableProvider);
this.fieldInjectionType = Objects.requireNonNull(fieldInjectionType);
this.variableInfo = Objects.requireNonNull(variableInfo);
this.sb = new StringBuilder();
this.formatter = new ExpressionFormatter(helperProvider, fieldInjectionType, sb);
}
@BeforeEach
void beforeEach() {
when(helperProvider.getCompatibilityHelper()).thenReturn(compatibilityHelper);
when(helperProvider.getVariableProvider()).thenReturn(variableProvider);
when(variableProvider.getVariableInfo(anyString())).thenReturn(variableInfo);
when(variableProvider.getNextVariableName(anyString())).then(i -> i.getArgument(0));
when(compatibilityHelper.getStartVar(anyString())).then(i -> i.getArgument(0));
when(compatibilityHelper.getStartVar(anyString(), anyInt())).then(i -> i.getArgument(0));
}
@Test
void testFormatNonDotNoInfo() {
when(variableProvider.getVariableInfo(anyString())).thenReturn(null);
assertThrows(GenerationException.class, () -> formatter.format("${value}", StringProperty.class));
}
@Test
void testFormatNonDot() throws GenerationException {
when(variableInfo.variableName()).thenReturn("variableName");
assertEquals("variableName", formatter.format("${value}", StringProperty.class));
assertEquals("", sb.toString());
}
@ParameterizedTest
@ValueSource(strings = {"${controller.}", "${.value}", "${controller.value.value}"})
void testGetDotExpressionBadSplit(final String expression) {
assertThrows(GenerationException.class, () -> formatter.format(expression, StringProperty.class));
}
@Test
void testFormatNonControllerNoInfo() {
when(variableProvider.getVariableInfo(anyString())).thenReturn(null);
assertThrows(GenerationException.class, () -> formatter.format("${other.text}", StringProperty.class));
}
@Test
void testFormatNonControllerCantRead() {
when(variableInfo.className()).thenReturn("javafx.scene.control.ComboBox");
assertThrows(GenerationException.class, () -> formatter.format("${other.text}", StringProperty.class));
}
@Test
void testFormatNonController() throws GenerationException {
when(variableInfo.variableName()).thenReturn("variableName");
when(variableInfo.className()).thenReturn("javafx.scene.control.TextField");
formatter.format("${other.text}", StringProperty.class);
assertEquals("variableName.textProperty()", formatter.format("${other.text}", StringProperty.class));
assertEquals("", sb.toString());
}
@Test
void testFormatControllerReflection() throws GenerationException {
final var reflectionFormatter = new ExpressionFormatter(helperProvider, ControllerFieldInjectionType.REFLECTION, sb);
final var expected = """
javafx.beans.property.StringPropertybinding;
try {
java.lang.reflect.Fieldfield = controller.getClass().getDeclaredField("text");
field.setAccessible(true);
binding = (javafx.beans.property.StringProperty) field.get(controller);
} catch (final NoSuchFieldException | IllegalAccessException e) {
throw new RuntimeException(e);
}
""";
assertEquals("binding", reflectionFormatter.format("${controller.text}", StringProperty.class));
assertEquals(expected, sb.toString());
}
@Test
void testFormatControllerSetters() throws GenerationException {
final var settersFormatter = new ExpressionFormatter(helperProvider, ControllerFieldInjectionType.SETTERS, sb);
assertEquals("controller.textProperty()", settersFormatter.format("${controller.text}", StringProperty.class));
}
@Test
void testFormatControllerFactory() throws GenerationException {
final var factoryFormatter = new ExpressionFormatter(helperProvider, ControllerFieldInjectionType.FACTORY, sb);
assertEquals("controller.textProperty()", factoryFormatter.format("${controller.text}", StringProperty.class));
}
@Test
void testFormatControllerAssign() throws GenerationException {
final var assignFormatter = new ExpressionFormatter(helperProvider, ControllerFieldInjectionType.ASSIGN, sb);
assertEquals("controller.text", assignFormatter.format("${controller.text}", StringProperty.class));
}
@Test
void testIllegal() {
assertThrows(NullPointerException.class, () -> new ExpressionFormatter(null, fieldInjectionType, sb));
assertThrows(NullPointerException.class, () -> new ExpressionFormatter(helperProvider, null, sb));
assertThrows(NullPointerException.class, () -> new ExpressionFormatter(helperProvider, fieldInjectionType, null));
}
}

View File

@@ -58,7 +58,17 @@ class TestGenerationHelper {
@Test @Test
void testGetGetMethod() { void testGetGetMethod() {
assertEquals("getSomething", GenerationHelper.getGetMethod("Something")); assertEquals("getSomething", GenerationHelper.getGetMethod("something"));
}
@Test
void testGetIsMethodProperty() {
assertEquals("isProperty", GenerationHelper.getIsMethod(property));
}
@Test
void testGetIsMethod() {
assertEquals("isSomething", GenerationHelper.getIsMethod("something"));
} }
@Test @Test
@@ -68,7 +78,7 @@ class TestGenerationHelper {
@Test @Test
void testGetSetMethod() { void testGetSetMethod() {
assertEquals("setSomething", GenerationHelper.getSetMethod("Something")); assertEquals("setSomething", GenerationHelper.getSetMethod("something"));
} }
@Test @Test

View File

@@ -65,12 +65,24 @@ class TestHelperProvider {
this.helperProvider = new HelperProvider(progress); this.helperProvider = new HelperProvider(progress);
} }
@Test
void testGetBindingFormatter() {
final var bindingFormatter = helperProvider.getBindingFormatter();
assertSame(bindingFormatter, helperProvider.getBindingFormatter());
}
@Test @Test
void testControllerInjector() { void testControllerInjector() {
final var injector = helperProvider.getControllerInjector(); final var injector = helperProvider.getControllerInjector();
assertSame(injector, helperProvider.getControllerInjector()); assertSame(injector, helperProvider.getControllerInjector());
} }
@Test
void testGetExpressionFormatter() {
final var expressionFormatter = helperProvider.getExpressionFormatter();
assertSame(expressionFormatter, helperProvider.getExpressionFormatter());
}
@Test @Test
void testGetFieldSetter() { void testGetFieldSetter() {
final var fieldSetter = helperProvider.getFieldSetter(); final var fieldSetter = helperProvider.getFieldSetter();
@@ -155,6 +167,12 @@ class TestHelperProvider {
assertSame(valueFormatter, helperProvider.getValueFormatter()); assertSame(valueFormatter, helperProvider.getValueFormatter());
} }
@Test
void testGetValueClassGuesser() {
final var valueClassGuesser = helperProvider.getValueClassGuesser();
assertSame(valueClassGuesser, helperProvider.getValueClassGuesser());
}
@Test @Test
void testGetVariableProvider() { void testGetVariableProvider() {
final var variableProvider = helperProvider.getVariableProvider(); final var variableProvider = helperProvider.getVariableProvider();

View File

@@ -119,7 +119,7 @@ class TestLoadMethodFormatter {
throw new IllegalStateException("Already loaded"); throw new IllegalStateException("Already loaded");
} }
java.util.ResourceBundleresourceBundle = java.util.ResourceBundle.getBundle(resourceBundleName); java.util.ResourceBundleresourceBundle = java.util.ResourceBundle.getBundle(resourceBundleName);
controller = controllerFactory.create(); controller = controllerFactory.get();
object-class controller.initialize(); object-class controller.initialize();
loaded = true; loaded = true;
return (T) class; return (T) class;
@@ -150,7 +150,7 @@ class TestLoadMethodFormatter {
} }
java.util.ResourceBundleresourceBundle = java.util.ResourceBundle.getBundle("bundle"); java.util.ResourceBundleresourceBundle = java.util.ResourceBundle.getBundle("bundle");
java.util.Map<String, Object>fieldMap = new java.util.HashMap<String, Object>(); java.util.Map<String, Object>fieldMap = new java.util.HashMap<String, Object>();
object-class controller = controllerFactory.create(fieldMap); object-class controller = controllerFactory.apply(fieldMap);
try { try {
java.lang.reflect.Methodinitialize = controller.getClass().getDeclaredMethod("initialize"); java.lang.reflect.Methodinitialize = controller.getClass().getDeclaredMethod("initialize");
initialize.setAccessible(true); initialize.setAccessible(true);
@@ -185,7 +185,7 @@ class TestLoadMethodFormatter {
throw new IllegalStateException("Already loaded"); throw new IllegalStateException("Already loaded");
} }
java.util.Map<String, Object>fieldMap = new java.util.HashMap<String, Object>(); java.util.Map<String, Object>fieldMap = new java.util.HashMap<String, Object>();
object-class controller = controllerFactory.create(fieldMap); object-class controller = controllerFactory.apply(fieldMap);
loaded = true; loaded = true;
return (T) class; return (T) class;
} }

View File

@@ -38,6 +38,7 @@ class TestObjectFormatter {
private final GenerationCompatibilityHelper compatibilityHelper; private final GenerationCompatibilityHelper compatibilityHelper;
private final InitializationFormatter initializationFormatter; private final InitializationFormatter initializationFormatter;
private final ReflectionHelper reflectionHelper; private final ReflectionHelper reflectionHelper;
private final ValueClassGuesser valueClassGuesser;
private final VariableProvider variableProvider; private final VariableProvider variableProvider;
private final GenerationRequest request; private final GenerationRequest request;
private final ControllerInfo controllerInfo; private final ControllerInfo controllerInfo;
@@ -49,7 +50,7 @@ class TestObjectFormatter {
TestObjectFormatter(@Mock final HelperProvider helperProvider, @Mock final GenerationCompatibilityHelper compatibilityHelper, TestObjectFormatter(@Mock final HelperProvider helperProvider, @Mock final GenerationCompatibilityHelper compatibilityHelper,
@Mock final InitializationFormatter initializationFormatter, @Mock final ReflectionHelper reflectionHelper, @Mock final InitializationFormatter initializationFormatter, @Mock final ReflectionHelper reflectionHelper,
@Mock final VariableProvider variableProvider, @Mock final GenerationRequest request, @Mock final VariableProvider variableProvider, @Mock final ValueClassGuesser valueClassGuesser, @Mock final GenerationRequest request,
@Mock final ControllerInfo controllerInfo, @Mock final ControllerInjector controllerInjector, @Mock final ControllerInfo controllerInfo, @Mock final ControllerInjector controllerInjector,
@Mock final SourceInfo sourceInfo) { @Mock final SourceInfo sourceInfo) {
this.helperProvider = Objects.requireNonNull(helperProvider); this.helperProvider = Objects.requireNonNull(helperProvider);
@@ -57,6 +58,7 @@ class TestObjectFormatter {
this.compatibilityHelper = Objects.requireNonNull(compatibilityHelper); this.compatibilityHelper = Objects.requireNonNull(compatibilityHelper);
this.initializationFormatter = Objects.requireNonNull(initializationFormatter); this.initializationFormatter = Objects.requireNonNull(initializationFormatter);
this.reflectionHelper = Objects.requireNonNull(reflectionHelper); this.reflectionHelper = Objects.requireNonNull(reflectionHelper);
this.valueClassGuesser = Objects.requireNonNull(valueClassGuesser);
this.variableProvider = Objects.requireNonNull(variableProvider); this.variableProvider = Objects.requireNonNull(variableProvider);
this.request = Objects.requireNonNull(request); this.request = Objects.requireNonNull(request);
this.controllerInfo = Objects.requireNonNull(controllerInfo); this.controllerInfo = Objects.requireNonNull(controllerInfo);
@@ -72,6 +74,7 @@ class TestObjectFormatter {
when(helperProvider.getControllerInjector()).thenReturn(controllerInjector); when(helperProvider.getControllerInjector()).thenReturn(controllerInjector);
when(helperProvider.getInitializationFormatter()).thenReturn(initializationFormatter); when(helperProvider.getInitializationFormatter()).thenReturn(initializationFormatter);
when(helperProvider.getReflectionHelper()).thenReturn(reflectionHelper); when(helperProvider.getReflectionHelper()).thenReturn(reflectionHelper);
when(helperProvider.getValueClassGuesser()).thenReturn(valueClassGuesser);
when(helperProvider.getVariableProvider()).thenReturn(variableProvider); when(helperProvider.getVariableProvider()).thenReturn(variableProvider);
when(compatibilityHelper.getStartVar(anyString())).then(i -> i.getArgument(0)); when(compatibilityHelper.getStartVar(anyString())).then(i -> i.getArgument(0));
when(compatibilityHelper.getStartVar(anyString(), anyInt())).then(i -> i.getArgument(0)); when(compatibilityHelper.getStartVar(anyString(), anyInt())).then(i -> i.getArgument(0));
@@ -301,7 +304,7 @@ class TestObjectFormatter {
void testFormatIncludeOnlySource() throws GenerationException { void testFormatIncludeOnlySource() throws GenerationException {
final var include = new ParsedIncludeImpl("source", null, null); final var include = new ParsedIncludeImpl("source", null, null);
objectFormatter.format(include, variableName); objectFormatter.format(include, variableName);
final var expected = "include(source, null) final javafx.scene.Parent variable = view.load();\n"; final var expected = "include(source, null) final javafx.scene.Parent variable = view.load();\n";
assertEquals(expected, sb.toString()); assertEquals(expected, sb.toString());
verify(initializationFormatter).formatSubViewConstructorCall(include); verify(initializationFormatter).formatSubViewConstructorCall(include);
} }
@@ -316,7 +319,7 @@ class TestObjectFormatter {
final var include = new ParsedIncludeImpl("source", "resources", "id"); final var include = new ParsedIncludeImpl("source", "resources", "id");
objectFormatter.format(include, variableName); objectFormatter.format(include, variableName);
final var expected = """ final var expected = """
include(source, resources) final javafx.scene.Parent variable = view.load(); include(source, resources) final javafx.scene.Parent variable = view.load();
controllerClassNamecontroller = view.controller(); controllerClassNamecontroller = view.controller();
"""; """;
assertEquals(expected, sb.toString()); assertEquals(expected, sb.toString());
@@ -335,7 +338,7 @@ class TestObjectFormatter {
final var include = new ParsedIncludeImpl(source, "resources", "id"); final var include = new ParsedIncludeImpl(source, "resources", "id");
objectFormatter.format(include, variableName); objectFormatter.format(include, variableName);
final var expected = """ final var expected = """
include(source, resources) final javafx.scene.Parent variable = view.load(); include(source, resources) final javafx.scene.Parent variable = view.load();
controllerClassNamecontroller = view.controller(); controllerClassNamecontroller = view.controller();
inject(idController, controller)inject(id, variable)"""; inject(idController, controller)inject(id, variable)""";
assertEquals(expected, sb.toString()); assertEquals(expected, sb.toString());
@@ -563,6 +566,10 @@ class TestObjectFormatter {
@Test @Test
void testFormatConstructorNamedArgs(@Mock final PropertyFormatter propertyFormatter) throws GenerationException { void testFormatConstructorNamedArgs(@Mock final PropertyFormatter propertyFormatter) throws GenerationException {
when(helperProvider.getPropertyFormatter()).thenReturn(propertyFormatter); when(helperProvider.getPropertyFormatter()).thenReturn(propertyFormatter);
when(valueClassGuesser.guess("1")).thenReturn(List.of(int.class));
when(valueClassGuesser.guess("2")).thenReturn(List.of(int.class));
when(valueClassGuesser.guess("3")).thenReturn(List.of(int.class));
when(valueClassGuesser.guess("false")).thenReturn(List.of(boolean.class));
doAnswer(i -> sb.append("property")).when(propertyFormatter).formatProperty(any(ParsedProperty.class), any(), any()); doAnswer(i -> sb.append("property")).when(propertyFormatter).formatProperty(any(ParsedProperty.class), any(), any());
final var className = "javafx.scene.control.Spinner"; final var className = "javafx.scene.control.Spinner";
final var attributes = Map.<String, ParsedProperty>of("min", new ParsedPropertyImpl("min", null, "1"), final var attributes = Map.<String, ParsedProperty>of("min", new ParsedPropertyImpl("min", null, "1"),
@@ -585,6 +592,21 @@ class TestObjectFormatter {
verify(reflectionHelper).getGenericTypes(parsedObject); verify(reflectionHelper).getGenericTypes(parsedObject);
} }
@Test
void testFormatConstructorNamedArgsPartial(@Mock final PropertyFormatter propertyFormatter) throws GenerationException {
when(helperProvider.getPropertyFormatter()).thenReturn(propertyFormatter);
when(valueClassGuesser.guess("2")).thenReturn(List.of(double.class));
final var className = "javafx.geometry.Insets";
final var attributes = Map.<String, ParsedProperty>of("left", new ParsedPropertyImpl("left", null, "2"));
final var properties = new LinkedHashMap<ParsedProperty, SequencedCollection<ParsedObject>>();
final var parsedObject = new ParsedObjectImpl(className, attributes, properties, List.of());
when(reflectionHelper.getGenericTypes(parsedObject)).thenReturn("");
objectFormatter.format(parsedObject, variableName);
final var expected = "startVarvariable = new javafx.geometry.Insets(0, 0, 0, 2);\n";
assertEquals(expected, sb.toString());
verify(compatibilityHelper).getStartVar(parsedObject);
verify(reflectionHelper).getGenericTypes(parsedObject);
}
@Test @Test
void testFormatConstructorDefault(@Mock final PropertyFormatter propertyFormatter) throws GenerationException { void testFormatConstructorDefault(@Mock final PropertyFormatter propertyFormatter) throws GenerationException {

View File

@@ -34,6 +34,7 @@ import static org.mockito.Mockito.*;
class TestPropertyFormatter { class TestPropertyFormatter {
private final HelperProvider helperProvider; private final HelperProvider helperProvider;
private final BindingFormatter bindingFormatter;
private final VariableProvider variableProvider; private final VariableProvider variableProvider;
private final GenerationCompatibilityHelper compatibilityHelper; private final GenerationCompatibilityHelper compatibilityHelper;
private final ControllerInjector controllerInjector; private final ControllerInjector controllerInjector;
@@ -49,12 +50,13 @@ class TestPropertyFormatter {
private final List<String> controllerFactoryPostAction; private final List<String> controllerFactoryPostAction;
private final PropertyFormatter propertyFormatter; private final PropertyFormatter propertyFormatter;
TestPropertyFormatter(@Mock final HelperProvider helperProvider, @Mock final VariableProvider variableProvider, TestPropertyFormatter(@Mock final HelperProvider helperProvider, @Mock final BindingFormatter bindingFormatter, @Mock final VariableProvider variableProvider,
@Mock final GenerationCompatibilityHelper compatibilityHelper, @Mock final ControllerInjector controllerInjector, @Mock final GenerationCompatibilityHelper compatibilityHelper, @Mock final ControllerInjector controllerInjector,
@Mock final FieldSetter fieldSetter, @Mock final ValueFormatter valueFormatter, @Mock final GenerationProgress progress, @Mock final FieldSetter fieldSetter, @Mock final ValueFormatter valueFormatter, @Mock final GenerationProgress progress,
@Mock final GenerationRequest request, @Mock final GenerationParameters parameters, @Mock final GenerationRequest request, @Mock final GenerationParameters parameters,
@Mock final ParsedObject rootObject, @Mock final ParsedProperty property) { @Mock final ParsedObject rootObject, @Mock final ParsedProperty property) {
this.helperProvider = Objects.requireNonNull(helperProvider); this.helperProvider = Objects.requireNonNull(helperProvider);
this.bindingFormatter = Objects.requireNonNull(bindingFormatter);
this.variableProvider = Objects.requireNonNull(variableProvider); this.variableProvider = Objects.requireNonNull(variableProvider);
this.compatibilityHelper = Objects.requireNonNull(compatibilityHelper); this.compatibilityHelper = Objects.requireNonNull(compatibilityHelper);
this.controllerInjector = Objects.requireNonNull(controllerInjector); this.controllerInjector = Objects.requireNonNull(controllerInjector);
@@ -73,6 +75,7 @@ class TestPropertyFormatter {
@BeforeEach @BeforeEach
void beforeEach() throws GenerationException { void beforeEach() throws GenerationException {
when(helperProvider.getBindingFormatter()).thenReturn(bindingFormatter);
when(helperProvider.getCompatibilityHelper()).thenReturn(compatibilityHelper); when(helperProvider.getCompatibilityHelper()).thenReturn(compatibilityHelper);
when(helperProvider.getControllerInjector()).thenReturn(controllerInjector); when(helperProvider.getControllerInjector()).thenReturn(controllerInjector);
when(helperProvider.getFieldSetter()).thenReturn(fieldSetter); when(helperProvider.getFieldSetter()).thenReturn(fieldSetter);
@@ -86,13 +89,35 @@ class TestPropertyFormatter {
when(progress.controllerFactoryPostAction()).thenReturn(controllerFactoryPostAction); when(progress.controllerFactoryPostAction()).thenReturn(controllerFactoryPostAction);
when(compatibilityHelper.getListOf()).thenReturn("listof("); when(compatibilityHelper.getListOf()).thenReturn("listof(");
when(valueFormatter.getArg(anyString(), any())).then(i -> i.getArgument(0) + "-" + i.getArgument(1)); when(valueFormatter.getArg(anyString(), any())).then(i -> i.getArgument(0) + "-" + i.getArgument(1));
doAnswer(i -> sb.append(i.getArgument(0) + "-" + i.getArgument(2))).when(bindingFormatter).formatBinding(any(), any(), anyString());
doAnswer(i -> sb.append(i.getArgument(0) + "-" + i.getArgument(1))).when(controllerInjector).injectEventHandlerControllerMethod(any(), anyString()); doAnswer(i -> sb.append(i.getArgument(0) + "-" + i.getArgument(1))).when(controllerInjector).injectEventHandlerControllerMethod(any(), anyString());
doAnswer(i -> sb.append(i.getArgument(0) + "-" + i.getArgument(1))).when(fieldSetter).setEventHandler(any(), anyString()); doAnswer(i -> sb.append(i.getArgument(0) + "-" + i.getArgument(1))).when(fieldSetter).setEventHandler(any(), anyString());
} }
@Test
void testFormatSimpleBinding() throws GenerationException {
when(property.name()).thenReturn("text");
when(property.value()).thenReturn("${value}");
propertyFormatter.formatProperty(property, rootObject, variableName);
final var expected = property + "-" + variableName;
assertEquals(expected, sb.toString());
verify(bindingFormatter).formatBinding(property, rootObject, variableName);
}
@Test
void testFormatBidirectionalBinding() throws GenerationException {
when(property.name()).thenReturn("text");
when(property.value()).thenReturn("#{value}");
propertyFormatter.formatProperty(property, rootObject, variableName);
final var expected = property + "-" + variableName;
assertEquals(expected, sb.toString());
verify(bindingFormatter).formatBinding(property, rootObject, variableName);
}
@Test @Test
void testFormatPropertyId() throws GenerationException { void testFormatPropertyId() throws GenerationException {
when(property.name()).thenReturn("fx:id"); when(property.name()).thenReturn("fx:id");
when(property.value()).thenReturn("value");
propertyFormatter.formatProperty(property, rootObject, variableName); propertyFormatter.formatProperty(property, rootObject, variableName);
assertEquals("", sb.toString()); assertEquals("", sb.toString());
} }
@@ -100,12 +125,14 @@ class TestPropertyFormatter {
@Test @Test
void testFormatControllerSame() { void testFormatControllerSame() {
when(property.name()).thenReturn("fx:controller"); when(property.name()).thenReturn("fx:controller");
when(property.value()).thenReturn(variableName);
assertDoesNotThrow(() -> propertyFormatter.formatProperty(property, rootObject, variableName)); assertDoesNotThrow(() -> propertyFormatter.formatProperty(property, rootObject, variableName));
} }
@Test @Test
void testFormatControllerDifferent() { void testFormatControllerDifferent() {
when(property.name()).thenReturn("fx:controller"); when(property.name()).thenReturn("fx:controller");
when(property.value()).thenReturn("value");
assertThrows(GenerationException.class, () -> propertyFormatter.formatProperty(property, mock(ParsedObject.class), variableName)); assertThrows(GenerationException.class, () -> propertyFormatter.formatProperty(property, mock(ParsedObject.class), variableName));
} }

View File

@@ -7,6 +7,7 @@ import com.github.gtache.fxml.compiler.impl.GenericTypesImpl;
import com.github.gtache.fxml.compiler.parsing.ParsedObject; import com.github.gtache.fxml.compiler.parsing.ParsedObject;
import com.github.gtache.fxml.compiler.parsing.ParsedProperty; import com.github.gtache.fxml.compiler.parsing.ParsedProperty;
import com.github.gtache.fxml.compiler.parsing.impl.ParsedPropertyImpl; import com.github.gtache.fxml.compiler.parsing.impl.ParsedPropertyImpl;
import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.control.ComboBox; import javafx.scene.control.ComboBox;
@@ -21,6 +22,7 @@ import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
@@ -67,23 +69,57 @@ class TestReflectionHelper {
@Test @Test
void testHasMethod() { void testHasMethod() {
assertFalse(ReflectionHelper.hasMethod(String.class, "bla")); assertFalse(ReflectionHelper.hasMethod(String.class, "bla"));
assertTrue(ReflectionHelper.hasMethod(String.class, "charAt")); assertTrue(ReflectionHelper.hasMethod(String.class, "charAt", int.class));
assertTrue(ReflectionHelper.hasMethod(String.class, "charAt", (Class<?>) null));
assertTrue(ReflectionHelper.hasMethod(StackPane.class, "getChildren")); assertTrue(ReflectionHelper.hasMethod(StackPane.class, "getChildren"));
} }
@Test @Test
void testGetMethod() throws NoSuchMethodException { void testHasMethodStatic() {
assertEquals(String.class.getMethod("charAt", int.class), ReflectionHelper.getMethod(String.class, "charAt")); assertFalse(ReflectionHelper.hasMethod(String.class, "valueOf", char.class));
}
@Test
void testGetMethod() throws Exception {
assertEquals(String.class.getMethod("codePointAt", int.class), ReflectionHelper.getMethod(String.class, "codePointAt", int.class));
assertEquals(String.class.getMethod("codePointAt", int.class), ReflectionHelper.getMethod(String.class, "codePointAt", (Class<?>) null));
}
@Test
void testGetMethodAmbiguous() {
assertThrows(GenerationException.class, () -> ReflectionHelper.getStaticMethod(String.class, "valueOf", (Class<?>) null));
}
@Test
void testGetMethodInexactNotFound() {
assertThrows(GenerationException.class, () -> ReflectionHelper.getStaticMethod(String.class, "abc", (Class<?>) null));
}
@Test
void testGetMethodStatic() {
assertThrows(GenerationException.class, () -> ReflectionHelper.getMethod(String.class, "valueOf", int.class));
} }
@Test @Test
void testHasStaticMethod() { void testHasStaticMethod() {
assertTrue(ReflectionHelper.hasStaticMethod(HBox.class, "setHgrow")); assertTrue(ReflectionHelper.hasStaticMethod(HBox.class, "setHgrow", Node.class, Priority.class));
assertTrue(ReflectionHelper.hasStaticMethod(HBox.class, "setHgrow", null, null));
} }
@Test @Test
void testGetStaticMethod() throws NoSuchMethodException { void testHasStaticMethodInstance() {
assertEquals(HBox.class.getMethod("setHgrow", Node.class, Priority.class), ReflectionHelper.getStaticMethod(HBox.class, "setHgrow")); assertFalse(ReflectionHelper.hasStaticMethod(String.class, "codePointAt", int.class));
}
@Test
void testGetStaticMethod() throws Exception {
assertEquals(HBox.class.getMethod("setMargin", Node.class, Insets.class), ReflectionHelper.getStaticMethod(HBox.class, "setMargin", Node.class, Insets.class));
assertEquals(HBox.class.getMethod("setMargin", Node.class, Insets.class), ReflectionHelper.getStaticMethod(HBox.class, "setMargin", null, null));
}
@Test
void testGetStaticMethodNotStatic() {
assertThrows(GenerationException.class, () -> ReflectionHelper.getStaticMethod(String.class, "charAt", int.class));
} }
@Test @Test
@@ -182,6 +218,11 @@ class TestReflectionHelper {
assertEquals(int.class, ReflectionHelper.getClass("int")); assertEquals(int.class, ReflectionHelper.getClass("int"));
} }
@Test
void testGetClassNotFound() {
assertThrows(GenerationException.class, () -> ReflectionHelper.getClass("java.lang.ABC"));
}
@Test @Test
void testGetGenericTypesNotGeneric() throws GenerationException { void testGetGenericTypesNotGeneric() throws GenerationException {
when(parsedObject.className()).thenReturn("java.lang.String"); when(parsedObject.className()).thenReturn("java.lang.String");
@@ -226,7 +267,14 @@ class TestReflectionHelper {
@Test @Test
void testGetReturnType() throws GenerationException { void testGetReturnType() throws GenerationException {
assertEquals(String.class.getName(), ReflectionHelper.getReturnType("java.lang.String", "valueOf")); assertEquals(String.class, ReflectionHelper.getReturnType("java.lang.String", "substring", int.class));
}
@Test
void testHasMethodAssignable() {
assertTrue(ReflectionHelper.hasMethod(List.class, "addAll", Collection.class));
assertTrue(ReflectionHelper.hasMethod(List.class, "addAll", List.class));
assertFalse(ReflectionHelper.hasMethod(List.class, "addAll", Object.class));
} }
@Test @Test

View File

@@ -0,0 +1,103 @@
package com.github.gtache.fxml.compiler.impl.internal;
import com.github.gtache.fxml.compiler.GenerationException;
import com.github.gtache.fxml.compiler.parsing.ParsedObject;
import javafx.scene.Node;
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.math.BigDecimal;
import java.math.BigInteger;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Objects;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class TestValueClassGuesser {
private final HelperProvider helperProvider;
private final VariableProvider variableProvider;
private final ValueClassGuesser valueClassGuesser;
TestValueClassGuesser(@Mock final HelperProvider helperProvider, @Mock final VariableProvider variableProvider) {
this.helperProvider = Objects.requireNonNull(helperProvider);
this.variableProvider = Objects.requireNonNull(variableProvider);
this.valueClassGuesser = new ValueClassGuesser(helperProvider);
}
@BeforeEach
void beforeEach() {
when(helperProvider.getVariableProvider()).thenReturn(variableProvider);
}
@Test
void testGuessVariableUnsupported() {
assertThrows(GenerationException.class, () -> valueClassGuesser.guess("$controller.value"));
}
@Test
void testGuessVariableNotFound() {
assertThrows(GenerationException.class, () -> valueClassGuesser.guess("$value"));
}
@Test
void testGuessVariable() throws GenerationException {
final var object = mock(ParsedObject.class);
when(variableProvider.getVariableInfo("value")).thenReturn(new VariableInfo("value", object, "value", "javafx.scene.Node"));
assertEquals(List.of(Node.class), valueClassGuesser.guess("$value"));
}
@Test
void testGuessNotDecimal() throws GenerationException {
assertEquals(List.of(String.class), valueClassGuesser.guess("value"));
}
@Test
void testGuessDouble() throws GenerationException {
assertEquals(List.of(float.class, Float.class, double.class, Double.class, BigDecimal.class, String.class), valueClassGuesser.guess("1.0"));
assertEquals(List.of(float.class, Float.class, double.class, Double.class, String.class), valueClassGuesser.guess("-Infinity"));
assertEquals(List.of(float.class, Float.class, double.class, Double.class, String.class), valueClassGuesser.guess("Infinity"));
assertEquals(List.of(float.class, Float.class, double.class, Double.class, String.class), valueClassGuesser.guess("NaN"));
}
@Test
void testGuessInteger() throws GenerationException {
assertEquals(List.of(byte.class, Byte.class, short.class, Short.class, int.class, Integer.class, long.class, Long.class, BigInteger.class,
float.class, Float.class, double.class, Double.class, BigDecimal.class, String.class), valueClassGuesser.guess("1"));
}
@Test
void testGuessNotByte() throws GenerationException {
assertEquals(List.of(short.class, Short.class, int.class, Integer.class, long.class, Long.class, BigInteger.class, float.class, Float.class, double.class, Double.class, BigDecimal.class, String.class), valueClassGuesser.guess("256"));
}
@Test
void testGuessLocalDate() throws GenerationException {
assertEquals(List.of(LocalDate.class, String.class), valueClassGuesser.guess("2022-01-01"));
}
@Test
void testGuessLocalDateTime() throws GenerationException {
assertEquals(List.of(LocalDateTime.class, String.class), valueClassGuesser.guess("2022-01-01T00:00:00"));
}
@Test
void testGuessBoolean() throws GenerationException {
assertEquals(List.of(boolean.class, Boolean.class, String.class), valueClassGuesser.guess("true"));
assertEquals(List.of(boolean.class, Boolean.class, String.class), valueClassGuesser.guess("false"));
}
@Test
void testIllegal() {
assertThrows(NullPointerException.class, () -> new ValueClassGuesser(null));
}
}

View File

@@ -22,7 +22,7 @@ import java.util.Set;
*/ */
public record CompilationInfo(Path inputFile, Path outputFile, String outputClass, Path controllerFile, public record CompilationInfo(Path inputFile, Path outputFile, String outputClass, Path controllerFile,
String controllerClass, Set<FieldInfo> injectedFields, Set<String> injectedMethods, String controllerClass, Set<FieldInfo> injectedFields, Set<String> injectedMethods,
Map<String, Path> includes, boolean requiresResourceBundle) { Map<String, Inclusion> includes, boolean requiresResourceBundle) {
/** /**
* Instantiates a new info * Instantiates a new info
@@ -62,7 +62,7 @@ public record CompilationInfo(Path inputFile, Path outputFile, String outputClas
private boolean requiresResourceBundle; private boolean requiresResourceBundle;
private final Set<FieldInfo> injectedFields; private final Set<FieldInfo> injectedFields;
private final Set<String> injectedMethods; private final Set<String> injectedMethods;
private final Map<String, Path> includes; private final Map<String, Inclusion> includes;
Builder() { Builder() {
this.injectedFields = new HashSet<>(); this.injectedFields = new HashSet<>();
@@ -110,7 +110,14 @@ public record CompilationInfo(Path inputFile, Path outputFile, String outputClas
} }
Builder addInclude(final String key, final Path value) { Builder addInclude(final String key, final Path value) {
this.includes.put(key, value); final var current = includes.get(key);
final Inclusion newInclusion;
if (current == null) {
newInclusion = new Inclusion(value, 1);
} else {
newInclusion = new Inclusion(value, current.count() + 1);
}
this.includes.put(key, newInclusion);
return this; return this;
} }

View File

@@ -48,11 +48,11 @@ final class ControllerInfoProvider {
final var name = fieldInfo.name(); final var name = fieldInfo.name();
final var type = fieldInfo.type(); final var type = fieldInfo.type();
if (fillFieldInfo(type, name, content, imports, propertyGenericTypes)) { if (fillFieldInfo(type, name, content, imports, propertyGenericTypes)) {
logger.debug("Found injected field {} of type {} with generic types {} in controller {}", name, type, propertyGenericTypes.get(name), info.controllerFile()); logger.debug("Found injected field {} of type {} with generic types {} in controller {}", name, type, propertyGenericTypes.get(name).genericTypes(), info.controllerFile());
} else if (type.contains(".")) { } else if (type.contains(".")) {
final var simpleName = type.substring(type.lastIndexOf('.') + 1); final var simpleName = type.substring(type.lastIndexOf('.') + 1);
if (fillFieldInfo(simpleName, name, content, imports, propertyGenericTypes)) { if (fillFieldInfo(simpleName, name, content, imports, propertyGenericTypes)) {
logger.debug("Found injected field {} of type {} with generic types {} in controller {}", name, simpleName, propertyGenericTypes.get(name), info.controllerFile()); logger.debug("Found injected field {} of type {} with generic types {} in controller {}", name, simpleName, propertyGenericTypes.get(name).genericTypes(), info.controllerFile());
} }
} else { } else {
logger.info("Field {}({}) not found in controller {}", name, type, info.controllerFile()); logger.info("Field {}({}) not found in controller {}", name, type, info.controllerFile());

View File

@@ -0,0 +1,28 @@
package com.github.gtache.fxml.compiler.maven.internal;
import java.nio.file.Path;
import java.util.Objects;
/**
* Represents an fx:include info
*
* @param path The path to the included file
* @param count The number of times the file is included
*/
record Inclusion(Path path, int count) {
/**
* Instantiates a new Inclusion
*
* @param path The path to the included file
* @param count The number of times the file is included
* @throws NullPointerException if path is null
* @throws IllegalArgumentException if count < 1
*/
Inclusion {
Objects.requireNonNull(path);
if (count < 1) {
throw new IllegalArgumentException("count must be >= 1");
}
}
}

View File

@@ -5,8 +5,8 @@ import com.github.gtache.fxml.compiler.impl.SourceInfoImpl;
import com.github.gtache.fxml.compiler.maven.FXMLCompilerMojo; import com.github.gtache.fxml.compiler.maven.FXMLCompilerMojo;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
/** /**
@@ -31,8 +31,13 @@ final class SourceInfoProvider {
final var includes = info.includes(); final var includes = info.includes();
final var requiresResourceBundle = info.requiresResourceBundle(); final var requiresResourceBundle = info.requiresResourceBundle();
final var includesMapping = new HashMap<String, SourceInfo>(); final var includesMapping = new HashMap<String, SourceInfo>();
includes.forEach((k, v) -> includesMapping.put(k, getSourceInfo(mapping.get(v), mapping))); includes.forEach((k, v) -> includesMapping.put(k, getSourceInfo(mapping.get(v.path()), mapping)));
//FIXME mutliple same includes final var includesSources = new ArrayList<SourceInfo>();
return new SourceInfoImpl(outputClass, controllerClass, inputFile, List.copyOf(includesMapping.values()), includesMapping, requiresResourceBundle); includes.forEach((key, value) -> {
for (var i = 0; i < value.count(); ++i) {
includesSources.add(includesMapping.get(key));
}
});
return new SourceInfoImpl(outputClass, controllerClass, inputFile, includesSources, includesMapping, requiresResourceBundle);
} }
} }

View File

@@ -24,11 +24,11 @@ class TestCompilationInfo {
private final String controllerClass; private final String controllerClass;
private final Set<FieldInfo> injectedFields; private final Set<FieldInfo> injectedFields;
private final Set<String> injectedMethods; private final Set<String> injectedMethods;
private final Map<String, Path> includes; private final Map<String, Inclusion> includes;
private final boolean requiresResourceBundle; private final boolean requiresResourceBundle;
private final CompilationInfo info; private final CompilationInfo info;
TestCompilationInfo(@Mock final Path inputFile, @Mock final Path outputFile, @Mock final Path controllerFile, @Mock final FieldInfo fieldInfo) { TestCompilationInfo(@Mock final Path inputFile, @Mock final Path outputFile, @Mock final Path controllerFile, @Mock final Inclusion inclusion, @Mock final FieldInfo fieldInfo) {
this.inputFile = Objects.requireNonNull(inputFile); this.inputFile = Objects.requireNonNull(inputFile);
this.outputFile = Objects.requireNonNull(outputFile); this.outputFile = Objects.requireNonNull(outputFile);
this.outputClass = "outputClass"; this.outputClass = "outputClass";
@@ -36,7 +36,7 @@ class TestCompilationInfo {
this.controllerClass = "controllerClass"; this.controllerClass = "controllerClass";
this.injectedFields = new HashSet<>(Set.of(fieldInfo)); this.injectedFields = new HashSet<>(Set.of(fieldInfo));
this.injectedMethods = new HashSet<>(Set.of("one", "two")); this.injectedMethods = new HashSet<>(Set.of("one", "two"));
this.includes = new HashMap<>(Map.of("one", Objects.requireNonNull(inputFile))); this.includes = new HashMap<>(Map.of("one", Objects.requireNonNull(inclusion)));
this.requiresResourceBundle = true; this.requiresResourceBundle = true;
this.info = new CompilationInfo(inputFile, outputFile, outputClass, controllerFile, controllerClass, injectedFields, injectedMethods, includes, requiresResourceBundle); this.info = new CompilationInfo(inputFile, outputFile, outputClass, controllerFile, controllerClass, injectedFields, injectedMethods, includes, requiresResourceBundle);
} }

View File

@@ -21,10 +21,10 @@ class TestCompilationInfoBuilder {
private final String controllerClass; private final String controllerClass;
private final Set<FieldInfo> injectedFields; private final Set<FieldInfo> injectedFields;
private final Set<String> injectedMethods; private final Set<String> injectedMethods;
private final Map<String, Path> includes; private final Map<String, Inclusion> includes;
private final CompilationInfo info; private final CompilationInfo info;
TestCompilationInfoBuilder(@Mock final Path inputFile, @Mock final Path outputFile, @Mock final Path controllerFile, @Mock final FieldInfo fieldInfo) { TestCompilationInfoBuilder(@Mock final Path inputFile, @Mock final Path outputFile, @Mock final Path controllerFile) {
this.inputFile = Objects.requireNonNull(inputFile); this.inputFile = Objects.requireNonNull(inputFile);
this.outputFile = Objects.requireNonNull(outputFile); this.outputFile = Objects.requireNonNull(outputFile);
this.outputClass = "outputClass"; this.outputClass = "outputClass";
@@ -32,7 +32,7 @@ class TestCompilationInfoBuilder {
this.controllerClass = "controllerClass"; this.controllerClass = "controllerClass";
this.injectedFields = Set.of(new FieldInfo("type", "name")); this.injectedFields = Set.of(new FieldInfo("type", "name"));
this.injectedMethods = Set.of("one", "two"); this.injectedMethods = Set.of("one", "two");
this.includes = Map.of("one", Objects.requireNonNull(inputFile)); this.includes = Map.of("one", new Inclusion(inputFile, 1));
this.info = new CompilationInfo(inputFile, outputFile, outputClass, controllerFile, controllerClass, injectedFields, injectedMethods, includes, true); this.info = new CompilationInfo(inputFile, outputFile, outputClass, controllerFile, controllerClass, injectedFields, injectedMethods, includes, true);
} }
@@ -47,9 +47,14 @@ class TestCompilationInfoBuilder {
builder.controllerClass(controllerClass); builder.controllerClass(controllerClass);
injectedFields.forEach(f -> builder.addInjectedField(f.name(), f.type())); injectedFields.forEach(f -> builder.addInjectedField(f.name(), f.type()));
injectedMethods.forEach(builder::addInjectedMethod); injectedMethods.forEach(builder::addInjectedMethod);
includes.forEach(builder::addInclude); builder.addInclude("one", inputFile);
builder.requiresResourceBundle(); builder.requiresResourceBundle();
final var actual = builder.build(); final var actual = builder.build();
assertEquals(info, actual); assertEquals(info, actual);
builder.addInclude("one", inputFile);
final var newIncludes = Map.of("one", new Inclusion(inputFile, 2));
final var newInfo = new CompilationInfo(inputFile, outputFile, outputClass, controllerFile, controllerClass, injectedFields, injectedMethods, newIncludes, true);
assertEquals(newInfo, builder.build());
} }
} }

View File

@@ -40,7 +40,7 @@ class TestCompilationInfoProvider {
"com.github.gtache.fxml.compiler.maven.internal.InfoView", controllerPath, controllerClass, "com.github.gtache.fxml.compiler.maven.internal.InfoView", controllerPath, controllerClass,
Set.of(new FieldInfo("javafx.event.EventHandler", "onContextMenuRequested"), new FieldInfo("Button", "button"), Set.of(new FieldInfo("javafx.event.EventHandler", "onContextMenuRequested"), new FieldInfo("Button", "button"),
new FieldInfo("com.github.gtache.fxml.compiler.maven.internal.IncludeController", "includeViewController")), new FieldInfo("com.github.gtache.fxml.compiler.maven.internal.IncludeController", "includeViewController")),
Set.of("onAction"), Map.of("includeView.fxml", path.getParent().resolve("includeView.fxml")), true); Set.of("onAction"), Map.of("includeView.fxml", new Inclusion(path.getParent().resolve("includeView.fxml"), 1)), true);
final var compilationInfoProvider = new CompilationInfoProvider(project, tempDir); final var compilationInfoProvider = new CompilationInfoProvider(project, tempDir);
final var actual = compilationInfoProvider.getCompilationInfo(tempDir, path, Map.of(includedPath, "com.github.gtache.fxml.compiler.maven.internal.IncludeController")); final var actual = compilationInfoProvider.getCompilationInfo(tempDir, path, Map.of(includedPath, "com.github.gtache.fxml.compiler.maven.internal.IncludeController"));
assertEquals(expected, actual); assertEquals(expected, actual);
@@ -115,7 +115,7 @@ class TestCompilationInfoProvider {
"com.github.gtache.fxml.compiler.maven.internal.NoResourceBundle", controllerPath, controllerClass, "com.github.gtache.fxml.compiler.maven.internal.NoResourceBundle", controllerPath, controllerClass,
Set.of(new FieldInfo("javafx.event.EventHandler", "onContextMenuRequested"), new FieldInfo("Button", "button"), Set.of(new FieldInfo("javafx.event.EventHandler", "onContextMenuRequested"), new FieldInfo("Button", "button"),
new FieldInfo("com.github.gtache.fxml.compiler.maven.internal.IncludeController", "includeViewController")), new FieldInfo("com.github.gtache.fxml.compiler.maven.internal.IncludeController", "includeViewController")),
Set.of("onAction"), Map.of("includeView.fxml", path.getParent().resolve("includeView.fxml")), false); Set.of("onAction"), Map.of("includeView.fxml", new Inclusion(path.getParent().resolve("includeView.fxml"), 1)), false);
final var compilationInfoProvider = new CompilationInfoProvider(project, tempDir); final var compilationInfoProvider = new CompilationInfoProvider(project, tempDir);
final var actual = compilationInfoProvider.getCompilationInfo(tempDir, path, Map.of(includedPath, "com.github.gtache.fxml.compiler.maven.internal.IncludeController")); final var actual = compilationInfoProvider.getCompilationInfo(tempDir, path, Map.of(includedPath, "com.github.gtache.fxml.compiler.maven.internal.IncludeController"));
assertEquals(expected, actual); assertEquals(expected, actual);

View File

@@ -0,0 +1,38 @@
package com.github.gtache.fxml.compiler.maven.internal;
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.nio.file.Path;
import java.util.Objects;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
@ExtendWith(MockitoExtension.class)
class TestInclusion {
private final Path path;
private final int count;
private final Inclusion inclusion;
TestInclusion(@Mock final Path path) {
this.path = Objects.requireNonNull(path);
this.count = 1;
this.inclusion = new Inclusion(path, count);
}
@Test
void testGetters() {
assertEquals(path, inclusion.path());
assertEquals(count, inclusion.count());
}
@Test
void testIllegal() {
assertThrows(NullPointerException.class, () -> new Inclusion(null, count));
assertThrows(IllegalArgumentException.class, () -> new Inclusion(path, 0));
}
}

View File

@@ -23,7 +23,7 @@ class TestSourceInfoProvider {
final var controllerClass = "controllerClass"; final var controllerClass = "controllerClass";
final var inputFile = Path.of("inputFile"); final var inputFile = Path.of("inputFile");
final var includeFile = Path.of("includeFile"); final var includeFile = Path.of("includeFile");
final var includes = Map.of("one", includeFile); final var includes = Map.of("one", new Inclusion(includeFile, 3));
when(compilationInfo.outputClass()).thenReturn(outputClass); when(compilationInfo.outputClass()).thenReturn(outputClass);
when(compilationInfo.controllerClass()).thenReturn(controllerClass); when(compilationInfo.controllerClass()).thenReturn(controllerClass);
when(compilationInfo.inputFile()).thenReturn(inputFile); when(compilationInfo.inputFile()).thenReturn(inputFile);
@@ -42,7 +42,7 @@ class TestSourceInfoProvider {
final var expectedIncludeSourceInfo = new SourceInfoImpl(includeOutputClass, includeControllerClass, includeInputFile, List.of(), Map.of(), false); final var expectedIncludeSourceInfo = new SourceInfoImpl(includeOutputClass, includeControllerClass, includeInputFile, List.of(), Map.of(), false);
assertEquals(expectedIncludeSourceInfo, SourceInfoProvider.getSourceInfo(includeCompilationInfo, mapping)); assertEquals(expectedIncludeSourceInfo, SourceInfoProvider.getSourceInfo(includeCompilationInfo, mapping));
final var expected = new SourceInfoImpl(outputClass, controllerClass, inputFile, List.of(expectedIncludeSourceInfo), final var expected = new SourceInfoImpl(outputClass, controllerClass, inputFile, List.of(expectedIncludeSourceInfo, expectedIncludeSourceInfo, expectedIncludeSourceInfo),
Map.of("one", expectedIncludeSourceInfo), true); Map.of("one", expectedIncludeSourceInfo), true);
assertEquals(expected, SourceInfoProvider.getSourceInfo(compilationInfo, mapping)); assertEquals(expected, SourceInfoProvider.getSourceInfo(compilationInfo, mapping));
} }