From c3d9e72e42f2df1809acbee0129c393dcedd42c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillaume=20T=C3=A2che?= Date: Mon, 30 Dec 2024 23:22:33 +0100 Subject: [PATCH] Adds BindingFormatter, ExpressionFormatter, ValueClassGuesser, fixes constructor args matching, fixes factory methods --- README.md | 5 +- .../impl/internal/BindingFormatter.java | 110 +++++++++ .../impl/internal/ConstructorHelper.java | 125 ++++++++--- .../impl/internal/ExpressionFormatter.java | 114 ++++++++++ .../impl/internal/GenerationHelper.java | 31 ++- .../impl/internal/HelperProvider.java | 21 ++ .../impl/internal/LoadMethodFormatter.java | 4 +- .../impl/internal/ObjectFormatter.java | 43 ++-- .../impl/internal/PropertyFormatter.java | 42 ++-- .../impl/internal/ReflectionHelper.java | 210 +++++++++++++----- .../impl/internal/ValueClassGuesser.java | 81 +++++++ .../impl/internal/ValueFormatter.java | 10 +- core/src/main/java/module-info.java | 1 + .../impl/internal/TestBindingFormatter.java | 131 +++++++++++ .../impl/internal/TestConstructorHelper.java | 95 +++++++- .../internal/TestExpressionFormatter.java | 136 ++++++++++++ .../impl/internal/TestGenerationHelper.java | 14 +- .../impl/internal/TestHelperProvider.java | 18 ++ .../internal/TestLoadMethodFormatter.java | 6 +- .../impl/internal/TestObjectFormatter.java | 30 ++- .../impl/internal/TestPropertyFormatter.java | 29 ++- .../impl/internal/TestReflectionHelper.java | 62 +++++- .../impl/internal/TestValueClassGuesser.java | 103 +++++++++ .../maven/internal/CompilationInfo.java | 13 +- .../internal/ControllerInfoProvider.java | 4 +- .../compiler/maven/internal/Inclusion.java | 28 +++ .../maven/internal/SourceInfoProvider.java | 13 +- .../maven/internal/TestCompilationInfo.java | 6 +- .../internal/TestCompilationInfoBuilder.java | 13 +- .../internal/TestCompilationInfoProvider.java | 4 +- .../maven/internal/TestInclusion.java | 38 ++++ .../internal/TestSourceInfoProvider.java | 4 +- 32 files changed, 1378 insertions(+), 166 deletions(-) create mode 100644 core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/BindingFormatter.java create mode 100644 core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/ExpressionFormatter.java create mode 100644 core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/ValueClassGuesser.java create mode 100644 core/src/test/java/com/github/gtache/fxml/compiler/impl/internal/TestBindingFormatter.java create mode 100644 core/src/test/java/com/github/gtache/fxml/compiler/impl/internal/TestExpressionFormatter.java create mode 100644 core/src/test/java/com/github/gtache/fxml/compiler/impl/internal/TestValueClassGuesser.java create mode 100644 maven-plugin/src/main/java/com/github/gtache/fxml/compiler/maven/internal/Inclusion.java create mode 100644 maven-plugin/src/test/java/com/github/gtache/fxml/compiler/maven/internal/TestInclusion.java diff --git a/README.md b/README.md index de30281..d7652d0 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,7 @@ Optionally add dependencies to the plugin (e.g. when using MediaView and control - Compile-time validation - Faster startup speed for the application - Possibility to use controller factories to instantiate controllers with final fields +- Very basic support for bidirectional bindings - Easier time with JPMS - No need to open the controllers packages to javafx.fxml - No need to open the resources packages when using use-image-inputstream-constructor (if images or resource bundles @@ -81,8 +82,8 @@ Optionally add dependencies to the plugin (e.g. when using MediaView and control ## Disadvantages - `fx:script` is not supported -- Possible bugs (file an issue if you see one) -- Expression binding is (very) limited +- Expect some bugs (file an issue if you see one) +- Expression binding support is very basic - Probably not fully compatible with all FXML features (file an issue if you need one in specific) - All fxml files must have a `fx:controller` attribute diff --git a/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/BindingFormatter.java b/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/BindingFormatter.java new file mode 100644 index 0000000..a5bf357 --- /dev/null +++ b/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/BindingFormatter.java @@ -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 controllerFactoryPostAction; + + BindingFormatter(final HelperProvider helperProvider, final ControllerFieldInjectionType fieldInjectionType, + final StringBuilder sb, final SequencedCollection 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); + } +} diff --git a/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/ConstructorHelper.java b/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/ConstructorHelper.java index af846aa..19351d6 100644 --- a/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/ConstructorHelper.java +++ b/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/ConstructorHelper.java @@ -7,8 +7,12 @@ import com.github.gtache.fxml.compiler.parsing.ParsedObject; import java.lang.reflect.Constructor; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Set; /** @@ -16,6 +20,7 @@ import java.util.Set; */ final class 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 allPropertyNames The property names + * @param constructors The constructors + * @param properties The mapping of properties name to possible types * @return The matching constructor arguments, or null if no constructor matches and no default constructor exists */ - static ConstructorArgs getMatchingConstructorArgs(final Constructor[] constructors, final Set allPropertyNames) { - ConstructorArgs matchingConstructorArgs = null; - for (final var constructor : constructors) { - final var constructorArgs = ReflectionHelper.getConstructorArgs(constructor); - final var matchingArgsCount = getMatchingArgsCount(constructorArgs, allPropertyNames); - if (matchingConstructorArgs == null ? matchingArgsCount > 0 : matchingArgsCount > getMatchingArgsCount(matchingConstructorArgs, allPropertyNames)) { - matchingConstructorArgs = constructorArgs; + static ConstructorArgs getMatchingConstructorArgs(final Constructor[] constructors, final Map>> properties) { + final var argsDistance = getArgsDistance(constructors, properties); + final var distances = argsDistance.keySet().stream().sorted().toList(); + for (final var distance : distances) { + final var matching = argsDistance.get(distance); + final var argsTypeDistance = getArgsTypeDistance(matching, properties); + if (!argsTypeDistance.isEmpty()) { + return argsTypeDistance.entrySet().stream().sorted(Map.Entry.comparingByKey()).map(Map.Entry::getValue) + .map(s -> s.iterator().next()).findFirst().orElseThrow(() -> new IllegalStateException("Shouldn't happen")); } } - if (matchingConstructorArgs == null) { - return Arrays.stream(constructors).filter(c -> c.getParameterCount() == 0).findFirst() - .map(c -> new ConstructorArgs(c, new LinkedHashMap<>())).orElse(null); - } else { - return matchingConstructorArgs; - } + //No matching constructor + return Arrays.stream(constructors).filter(c -> c.getParameterCount() == 0).findFirst() + .map(c -> new ConstructorArgs(c, new LinkedHashMap<>())).orElse(null); } /** - * 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 allPropertyNames The property names + * @param constructors The constructors + * @param properties The object properties + * @return The mapping + */ + private static Map> getArgsDistance(final Constructor[] constructors, final Map>> properties) { + final var argsDistance = HashMap.>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> getArgsTypeDistance(final Collection matching, final Map>> properties) { + final var argsTypeDistance = HashMap.>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>> properties) { + var typeDistance = 0L; + for (final var namedArg : constructorArgs.namedArgs().entrySet()) { + final var name = namedArg.getKey(); + final var parameter = namedArg.getValue(); + final var type = parameter.type(); + final var property = properties.get(name); + if (property != null) { + var distance = -1L; + for (var i = 0; i < property.size(); i++) { + final var clazz = property.get(i); + if (clazz.isAssignableFrom(type)) { + distance = i; + break; + } + } + if (distance < 0) { + return -1; + } else { + typeDistance += distance; + } + } + } + return typeDistance; + } + + /** + * Checks how many arguments of the given constructor match the given properties + * + * @param constructorArgs The constructor arguments + * @param properties The mapping of properties name to expected type * @return The number of matching arguments */ - private static long getMatchingArgsCount(final ConstructorArgs constructorArgs, final Set allPropertyNames) { - if (constructorArgs.namedArgs().keySet().stream().anyMatch(s -> !allPropertyNames.contains(s))) { - return 0; - } else { - return constructorArgs.namedArgs().keySet().stream().filter(allPropertyNames::contains).count(); - } + private static long getMatchingArgsCount(final ConstructorArgs constructorArgs, final Map>> properties) { + return constructorArgs.namedArgs().keySet().stream().filter(properties::containsKey).count(); } + + } diff --git a/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/ExpressionFormatter.java b/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/ExpressionFormatter.java new file mode 100644 index 0000000..fe76ec9 --- /dev/null +++ b/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/ExpressionFormatter.java @@ -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()); + } + } + } + +} diff --git a/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/GenerationHelper.java b/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/GenerationHelper.java index f063f72..c26b393 100644 --- a/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/GenerationHelper.java +++ b/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/GenerationHelper.java @@ -13,7 +13,6 @@ import java.util.Map; final class GenerationHelper { static final String INDENT_8 = " "; - static final String INDENT_12 = " "; static final String FX_ID = "fx:id"; static final String FX_VALUE = "fx:value"; static final String VALUE = "value"; @@ -24,7 +23,7 @@ final class GenerationHelper { static final String RESOURCE_KEY_PREFIX = "%"; static final String EXPRESSION_PREFIX = "$"; static final String BINDING_EXPRESSION_PREFIX = "${"; - static final String BI_DIRECTIONAL_BINDING_PREFIX = "#{"; + static final String BIDIRECTIONAL_BINDING_PREFIX = "#{"; private GenerationHelper() { @@ -67,7 +66,27 @@ final class GenerationHelper { * @return The getter method name */ 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 */ 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); } /** diff --git a/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/HelperProvider.java b/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/HelperProvider.java index bf91540..e2101b2 100644 --- a/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/HelperProvider.java +++ b/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/HelperProvider.java @@ -22,6 +22,15 @@ public class HelperProvider { this.helpers = new HashMap<>(); } + BindingFormatter getBindingFormatter() { + return (BindingFormatter) helpers.computeIfAbsent(BindingFormatter.class, c -> { + final var fieldInjectionType = progress.request().parameters().fieldInjectionType(); + final var sb = progress.stringBuilder(); + final var controllerFactoryPostAction = progress.controllerFactoryPostAction(); + return new BindingFormatter(this, fieldInjectionType, sb, controllerFactoryPostAction); + }); + } + ControllerInjector getControllerInjector() { return (ControllerInjector) helpers.computeIfAbsent(ControllerInjector.class, c -> { final var request = progress.request(); @@ -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() { return (FieldSetter) helpers.computeIfAbsent(FieldSetter.class, c -> { 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() { return (VariableProvider) helpers.computeIfAbsent(VariableProvider.class, c -> new VariableProvider()); } diff --git a/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/LoadMethodFormatter.java b/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/LoadMethodFormatter.java index 8ce2685..5d1fb6e 100644 --- a/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/LoadMethodFormatter.java +++ b/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/LoadMethodFormatter.java @@ -54,12 +54,12 @@ public final class LoadMethodFormatter { if (fieldInjectionType == ControllerFieldInjectionType.FACTORY) { sb.append(generationCompatibilityHelper.getStartVar("java.util.Map")).append("fieldMap = new java.util.HashMap();\n"); } 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)); helperProvider.getObjectFormatter().format(rootObject, variableName); if (fieldInjectionType == ControllerFieldInjectionType.FACTORY) { - sb.append(" controller = controllerFactory.create(fieldMap);\n"); + sb.append(" controller = controllerFactory.apply(fieldMap);\n"); progress.controllerFactoryPostAction().forEach(sb::append); } if (request.controllerInfo().hasInitialize()) { diff --git a/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/ObjectFormatter.java b/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/ObjectFormatter.java index 6c3594d..3d5e8fa 100644 --- a/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/ObjectFormatter.java +++ b/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/ObjectFormatter.java @@ -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.GenerationRequest; 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 javafx.scene.Node; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; -import java.util.HashSet; +import java.util.HashMap; +import java.util.List; import java.util.SequencedCollection; import java.util.Set; -import java.util.stream.Collectors; import static com.github.gtache.fxml.compiler.impl.internal.GenerationHelper.*; import static java.util.Objects.requireNonNull; @@ -271,19 +280,26 @@ final class ObjectFormatter { final var clazz = ReflectionHelper.getClass(parsedObject.className()); final var children = parsedObject.children(); final var notDefinedChildren = getNotDefines(children); - final var constructors = clazz.getConstructors(); - final var allAttributesNames = new HashSet<>(parsedObject.attributes().keySet()); - allAttributesNames.addAll(parsedObject.properties().keySet().stream().map(ParsedProperty::name).collect(Collectors.toSet())); + final var allProperties = new HashMap>>(); + for (final var entry : parsedObject.attributes().entrySet()) { + final var possibleTypes = helperProvider.getValueClassGuesser().guess(entry.getValue().value()); + allProperties.put(entry.getKey(), possibleTypes); + } + for (final var entry : parsedObject.properties().entrySet()) { + allProperties.put(entry.getKey().name(), List.of(Node.class, Node[].class)); + } formatDefines(children); if (!notDefinedChildren.isEmpty()) { final var defaultProperty = ReflectionHelper.getDefaultProperty(parsedObject.className()); if (defaultProperty != null) { - 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) { - formatNoConstructor(parsedObject, variableName, allAttributesNames); + logger.debug("No constructor found for {} with attributes {}", clazz.getCanonicalName(), allProperties); + formatNoConstructor(parsedObject, variableName, allProperties.keySet()); } else { formatConstructor(parsedObject, variableName, constructorArgs); } @@ -296,7 +312,7 @@ final class ObjectFormatter { final var property = parsedObject.attributes().get("fx:constant"); sb.append(helperProvider.getCompatibilityHelper().getStartVar(parsedObject)).append(variableName).append(" = ").append(clazz.getCanonicalName()).append(".").append(property.value()).append(";\n"); } else { - throw new GenerationException("Cannot find 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 */ 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); - sb.append(" final javafx.scene.Parent ").append(subNodeName).append(" = ").append(viewVariable).append(".load();\n"); - injectSubController(include, subViewVariable); + sb.append(" final javafx.scene.Parent ").append(subNodeName).append(" = ").append(viewVariable).append(".load();\n"); + injectSubController(include, viewVariable); } private void injectSubController(final ParsedInclude include, final String subViewVariable) { @@ -449,7 +464,7 @@ final class ObjectFormatter { sb.append(compatibilityHelper.getStartVar(factory.className())).append(variableName).append(" = ").append(factory.className()) .append(".").append(factory.factory()).append("(").append(String.join(", ", variables)).append(");\n"); } else { - final var returnType = ReflectionHelper.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()) .append(".").append(factory.factory()).append("(").append(String.join(", ", variables)).append(");\n"); } diff --git a/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/PropertyFormatter.java b/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/PropertyFormatter.java index 1d53055..2f76a7f 100644 --- a/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/PropertyFormatter.java +++ b/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/PropertyFormatter.java @@ -12,6 +12,7 @@ import com.github.gtache.fxml.compiler.parsing.impl.ParsedPropertyImpl; import javafx.event.EventHandler; import java.util.ArrayList; +import java.util.List; import java.util.Objects; import java.util.SequencedCollection; @@ -41,17 +42,22 @@ final class PropertyFormatter { * @throws GenerationException if an error occurs */ void formatProperty(final ParsedProperty property, final ParsedObject parent, final String parentVariable) throws GenerationException { - 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); + final var value = property.value(); + if (value.endsWith("}") && (value.startsWith(BINDING_EXPRESSION_PREFIX) || value.startsWith(BIDIRECTIONAL_BINDING_PREFIX))) { + helperProvider.getBindingFormatter().formatBinding(property, parent, parentVariable); } else { - 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 { final var setMethod = getSetMethod(propertyName); final var propertySourceTypeClass = ReflectionHelper.getClass(property.sourceType()); - if (ReflectionHelper.hasStaticMethod(propertySourceTypeClass, setMethod)) { - final var method = ReflectionHelper.getStaticMethod(propertySourceTypeClass, setMethod); + if (ReflectionHelper.hasStaticMethod(propertySourceTypeClass, setMethod, null, null)) { + final var method = ReflectionHelper.getStaticMethod(propertySourceTypeClass, setMethod, null, null); final var parameterType = method.getParameterTypes()[1]; final var arg = helperProvider.getValueFormatter().getArg(property.value(), parameterType); setLaterIfNeeded(property, parameterType, " " + property.sourceType() + "." + setMethod + "(" + parentVariable + ", " + arg + ");\n"); @@ -108,7 +114,7 @@ final class PropertyFormatter { final var setMethod = getSetMethod(propertyName); final var getMethod = getGetMethod(propertyName); final var parentClass = ReflectionHelper.getClass(parent.className()); - if (ReflectionHelper.hasMethod(parentClass, setMethod)) { + if (ReflectionHelper.hasMethod(parentClass, setMethod, (Class) null)) { handleSetProperty(property, parentClass, parentVariable); } else if (ReflectionHelper.hasMethod(parentClass, getMethod)) { 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 { 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 arg = helperProvider.getValueFormatter().getArg(property.value(), parameterType); setLaterIfNeeded(property, parameterType, " " + parentVariable + "." + setMethod + "(" + arg + ");\n"); @@ -129,7 +135,7 @@ final class PropertyFormatter { final var getMethod = getGetMethod(property.name()); final var method = ReflectionHelper.getMethod(parentClass, getMethod); 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); setLaterIfNeeded(property, String.class, " " + parentVariable + "." + getMethod + "().addAll(" + helperProvider.getCompatibilityHelper().getListOf() + arg + "));\n"); @@ -155,7 +161,6 @@ final class PropertyFormatter { } } - /** * Formats the children objects of a property * @@ -231,7 +236,7 @@ final class PropertyFormatter { final var getMethod = getGetMethod(property); final var parentClass = ReflectionHelper.getClass(parent.className()); 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"); } else if (ReflectionHelper.hasMethod(parentClass, getMethod)) { //Probably a list method that has only one element @@ -251,7 +256,8 @@ final class PropertyFormatter { private void formatSingleChildStatic(final String variableName, final ParsedProperty property, final String parentVariable) throws GenerationException { 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) .append("(").append(parentVariable).append(", ").append(variableName).append(");\n"); } else { diff --git a/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/ReflectionHelper.java b/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/ReflectionHelper.java index d6a1237..6d4acb6 100644 --- a/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/ReflectionHelper.java +++ b/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/ReflectionHelper.java @@ -6,7 +6,6 @@ import com.github.gtache.fxml.compiler.GenericTypes; import com.github.gtache.fxml.compiler.parsing.ParsedObject; import javafx.beans.DefaultProperty; import javafx.beans.NamedArg; -import javafx.scene.Node; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -15,7 +14,6 @@ import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.Arrays; -import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -30,12 +28,13 @@ import static com.github.gtache.fxml.compiler.impl.internal.GenerationHelper.FX_ */ final class ReflectionHelper { private static final Logger logger = LogManager.getLogger(ReflectionHelper.class); - private static final Map> classMap = new HashMap<>(); - private static final Map, Boolean> hasValueOf = new HashMap<>(); - private static final Map, Boolean> isGeneric = new HashMap<>(); - private static final Map defaultProperty = new HashMap<>(); - private static final Map, Map> methods = new HashMap<>(); - private static final Map, Map> staticMethods = new HashMap<>(); + private static final Map> classMap = new ConcurrentHashMap<>(); + private static final Map, Boolean> hasValueOf = new ConcurrentHashMap<>(); + private static final Map, Boolean> isGeneric = new ConcurrentHashMap<>(); + private static final Map defaultProperty = new ConcurrentHashMap<>(); + private static final Map, Map> methods = new ConcurrentHashMap<>(); + private static final Map, Map> staticMethods = new ConcurrentHashMap<>(); + private static final Map, Map>> methodsReturnType = new ConcurrentHashMap<>(); private static final Map> PRIMITIVE_TYPES = Map.of( "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 * - * @param clazz The class - * @param methodName The method name + * @param clazz The class + * @param methodName The method name + * @param parameterTypes The method parameter types (null if any object is allowed) * @return True if the class has a method with the given name */ - static boolean hasMethod(final Class clazz, final String methodName) { + static boolean hasMethod(final Class clazz, final String methodName, final Class... parameterTypes) { 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; } /** - * Gets the method corresponding to the given class and name + * Gets the method corresponding to the given class and name. * The result is cached * - * @param clazz The class - * @param methodName The method name + * @param clazz The class + * @param methodName The method name + * @param parameterTypes The method parameter types * @return The method */ - static Method getMethod(final Class clazz, final String methodName) { + static Method getMethod(final Class clazz, final String methodName, final Class... parameterTypes) throws GenerationException { 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 * - * @param clazz The class - * @param methodName The method name + * @param clazz The class + * @param methodKey The method key * @return True if the class has a method with the given name */ - private static Method computeMethod(final Class clazz, final String methodName) { - final var matching = Arrays.stream(clazz.getMethods()).filter(m -> { - 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 - } else { + private static Method computeMethod(final Class clazz, final MethodKey methodKey) throws GenerationException { + return computeMethod(clazz, methodKey, false); + } + + private static boolean typesMatch(final Class[] types, final List> parameterTypes) { + for (var i = 0; i < types.length; i++) { + final var type = types[i]; + final var parameterType = parameterTypes.get(i); + if (parameterType != null && !type.isAssignableFrom(parameterType)) { return false; } - }).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 * @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 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; } @@ -143,33 +161,74 @@ final class ReflectionHelper { * @param methodName The method name * @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<>()); - 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 * - * @param clazz The class name - * @param methodName The method name + * @param clazz The class name + * @param methodKey The method name * @return The method, or null if not found */ - private static Method computeStaticMethod(final Class clazz, final 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 -> { - if (m.getName().equals(methodName) && Modifier.isStatic(m.getModifiers())) { - final var parameterTypes = m.getParameterTypes(); - return parameterTypes.length > 1 && parameterTypes[0] == Node.class; + if (m.getName().equals(methodName) && isStatic == Modifier.isStatic(m.getModifiers())) { + final var types = m.getParameterTypes(); + return types.length == parameterTypes.size() && typesMatch(types, parameterTypes); } else { return false; } }).toList(); - if (matching.size() > 1) { - throw new UnsupportedOperationException("Multiple matching methods not supported yet : " + clazz + " - " + methodName); - } else if (matching.size() == 1) { + if (matching.size() == 1) { return matching.getFirst(); + } else if (matching.isEmpty()) { + throw new GenerationException("Method not found : " + clazz + " - " + methodKey); } 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 methodName The method + * @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 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 method = Arrays.stream(clazz.getMethods()).filter(m -> m.getName().equals(methodName)) - .findFirst().orElseThrow(() -> new GenerationException("Method " + methodName + " not found in class " + className)); - return method.getReturnType().getName(); + return getReturnType(clazz, methodName, parameterTypes, false); + } + + /** + * Gets the return type of the given static method for the given class + * + * @param className The class + * @param methodName The method + * @param parameterTypes The method parameter types (null if any object is allowed) + * @return The return type + * @throws GenerationException if an error occurs + */ + static Class getStaticReturnType(final String className, final String methodName, final Class... parameterTypes) throws GenerationException { + final var clazz = getClass(className); + return getReturnType(clazz, methodName, parameterTypes, true); + } + + private static Class getReturnType(final Class clazz, final String methodName, final Class[] parameterTypes, final boolean isStatic) throws GenerationException { + final var returnTypes = methodsReturnType.computeIfAbsent(clazz, c -> new ConcurrentHashMap<>()); + try { + final var methodKey = new MethodKey(methodName, Arrays.asList(parameterTypes)); + return returnTypes.computeIfAbsent(methodKey, m -> { + try { + return isStatic ? getStaticMethod(clazz, methodName, parameterTypes).getReturnType() : + getMethod(clazz, methodName, parameterTypes).getReturnType(); + } catch (final GenerationException e) { + throw new RuntimeException(e); + } + }); + } catch (final RuntimeException e) { + throw (GenerationException) e.getCause(); + } + } + + private record MethodKey(String methodName, List> parameterTypes) { } } diff --git a/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/ValueClassGuesser.java b/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/ValueClassGuesser.java new file mode 100644 index 0000000..c3d52d2 --- /dev/null +++ b/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/ValueClassGuesser.java @@ -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> guess(final String value) throws GenerationException { + if (value.startsWith("$")) { + return getPossibleVariableTypes(value.substring(1)); + } else { + return getPossibleTypes(value); + } + } + + private List> 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> getPossibleTypes(final String value) { + final var ret = new ArrayList>(); + 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 Collection> tryParse(final String value, final Function parseFunction, final Class... classes) { + try { + parseFunction.apply(value); + return Arrays.asList(classes); + } catch (final RuntimeException ignored) { + //Do nothing + return List.of(); + } + } +} diff --git a/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/ValueFormatter.java b/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/ValueFormatter.java index 5da8bf8..d2439c3 100644 --- a/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/ValueFormatter.java +++ b/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/ValueFormatter.java @@ -4,6 +4,8 @@ import com.github.gtache.fxml.compiler.GenerationException; import com.github.gtache.fxml.compiler.ResourceBundleInjectionType; import com.github.gtache.fxml.compiler.impl.GeneratorImpl; +import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.regex.Pattern; import static com.github.gtache.fxml.compiler.impl.internal.GenerationHelper.*; @@ -41,7 +43,9 @@ final class ValueFormatter { final var subpath = value.substring(1); return getResourceValue(subpath); } 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)) { final var variable = helperProvider.getVariableProvider().getVariableInfo(value.substring(1)); if (variable == null) { @@ -92,6 +96,10 @@ final class ValueFormatter { return intToString(value, clazz); } else if (clazz == float.class || clazz == Float.class || clazz == double.class || clazz == Double.class) { return decimalToString(value, clazz); + } else if (clazz == LocalDate.class) { + return "LocalDate.parse(\"" + value + "\")"; + } else if (clazz == LocalDateTime.class) { + return "LocalDateTime.parse(\"" + value + "\")"; } else if (ReflectionHelper.hasValueOf(clazz)) { return valueOfToString(value, clazz); } else { diff --git a/core/src/main/java/module-info.java b/core/src/main/java/module-info.java index 579e79a..b55eddf 100644 --- a/core/src/main/java/module-info.java +++ b/core/src/main/java/module-info.java @@ -5,6 +5,7 @@ module com.github.gtache.fxml.compiler.core { requires transitive com.github.gtache.fxml.compiler.api; requires transitive javafx.graphics; requires org.apache.logging.log4j; + requires java.sql; exports com.github.gtache.fxml.compiler.impl; exports com.github.gtache.fxml.compiler.parsing.impl; diff --git a/core/src/test/java/com/github/gtache/fxml/compiler/impl/internal/TestBindingFormatter.java b/core/src/test/java/com/github/gtache/fxml/compiler/impl/internal/TestBindingFormatter.java new file mode 100644 index 0000000..e2d9417 --- /dev/null +++ b/core/src/test/java/com/github/gtache/fxml/compiler/impl/internal/TestBindingFormatter.java @@ -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 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)); + } +} diff --git a/core/src/test/java/com/github/gtache/fxml/compiler/impl/internal/TestConstructorHelper.java b/core/src/test/java/com/github/gtache/fxml/compiler/impl/internal/TestConstructorHelper.java index d2a77c5..6abc468 100644 --- a/core/src/test/java/com/github/gtache/fxml/compiler/impl/internal/TestConstructorHelper.java +++ b/core/src/test/java/com/github/gtache/fxml/compiler/impl/internal/TestConstructorHelper.java @@ -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.impl.ParsedPropertyImpl; import javafx.beans.NamedArg; +import javafx.scene.control.Spinner; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; @@ -12,6 +13,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; +import java.time.LocalDate; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; @@ -89,7 +91,93 @@ class TestConstructorHelper { new NamedArgImpl("p2", "value2")}}); when(constructors[1].getParameterTypes()).thenReturn(new Class[]{int.class, String.class}); 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.>of(int.class, double.class, LocalDate.class), + "max", List.>of(int.class, double.class, LocalDate.class), "initialValue", + List.>of(int.class, double.class, LocalDate.class), "amountToStepBy", List.>of(int.class, double.class, LocalDate.class)); + final var namedArgs = new LinkedHashMap(); + 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.>of(double.class), + "max", List.>of(int.class), "initialValue", + List.>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.>of(int.class, double.class, LocalDate.class), + "max", List.>of(int.class, double.class), "initialValue", + List.>of(double.class, LocalDate.class)); + final var namedArgs = new LinkedHashMap(); + 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.>of(int.class, double.class, LocalDate.class), + "max", List.>of(int.class, double.class, LocalDate.class), "initialValue", + List.>of(int.class, double.class, LocalDate.class)); + final var namedArgs = new LinkedHashMap(); + 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.>of(double.class, LocalDate.class), + "max", List.>of(double.class, LocalDate.class), "initialValue", + List.>of(double.class, LocalDate.class)); + final var namedArgs = new LinkedHashMap(); + 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.>of(int.class, double.class, LocalDate.class)); + final var namedArgs = new LinkedHashMap(); + 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 @@ -101,7 +189,7 @@ class TestConstructorHelper { when(constructors[0].getParameterCount()).thenReturn(0); when(constructors[1].getParameterCount()).thenReturn(1); 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 @@ -110,10 +198,9 @@ class TestConstructorHelper { when(constructors[1].getParameterAnnotations()).thenReturn(EMPTY_ANNOTATIONS); when(constructors[0].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 { @Override diff --git a/core/src/test/java/com/github/gtache/fxml/compiler/impl/internal/TestExpressionFormatter.java b/core/src/test/java/com/github/gtache/fxml/compiler/impl/internal/TestExpressionFormatter.java new file mode 100644 index 0000000..b0bf91b --- /dev/null +++ b/core/src/test/java/com/github/gtache/fxml/compiler/impl/internal/TestExpressionFormatter.java @@ -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)); + } +} diff --git a/core/src/test/java/com/github/gtache/fxml/compiler/impl/internal/TestGenerationHelper.java b/core/src/test/java/com/github/gtache/fxml/compiler/impl/internal/TestGenerationHelper.java index 99ec1cb..02c6a7c 100644 --- a/core/src/test/java/com/github/gtache/fxml/compiler/impl/internal/TestGenerationHelper.java +++ b/core/src/test/java/com/github/gtache/fxml/compiler/impl/internal/TestGenerationHelper.java @@ -58,7 +58,17 @@ class TestGenerationHelper { @Test 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 @@ -68,7 +78,7 @@ class TestGenerationHelper { @Test void testGetSetMethod() { - assertEquals("setSomething", GenerationHelper.getSetMethod("Something")); + assertEquals("setSomething", GenerationHelper.getSetMethod("something")); } @Test diff --git a/core/src/test/java/com/github/gtache/fxml/compiler/impl/internal/TestHelperProvider.java b/core/src/test/java/com/github/gtache/fxml/compiler/impl/internal/TestHelperProvider.java index 9c98c04..50c4833 100644 --- a/core/src/test/java/com/github/gtache/fxml/compiler/impl/internal/TestHelperProvider.java +++ b/core/src/test/java/com/github/gtache/fxml/compiler/impl/internal/TestHelperProvider.java @@ -65,12 +65,24 @@ class TestHelperProvider { this.helperProvider = new HelperProvider(progress); } + @Test + void testGetBindingFormatter() { + final var bindingFormatter = helperProvider.getBindingFormatter(); + assertSame(bindingFormatter, helperProvider.getBindingFormatter()); + } + @Test void testControllerInjector() { final var injector = helperProvider.getControllerInjector(); assertSame(injector, helperProvider.getControllerInjector()); } + @Test + void testGetExpressionFormatter() { + final var expressionFormatter = helperProvider.getExpressionFormatter(); + assertSame(expressionFormatter, helperProvider.getExpressionFormatter()); + } + @Test void testGetFieldSetter() { final var fieldSetter = helperProvider.getFieldSetter(); @@ -155,6 +167,12 @@ class TestHelperProvider { assertSame(valueFormatter, helperProvider.getValueFormatter()); } + @Test + void testGetValueClassGuesser() { + final var valueClassGuesser = helperProvider.getValueClassGuesser(); + assertSame(valueClassGuesser, helperProvider.getValueClassGuesser()); + } + @Test void testGetVariableProvider() { final var variableProvider = helperProvider.getVariableProvider(); diff --git a/core/src/test/java/com/github/gtache/fxml/compiler/impl/internal/TestLoadMethodFormatter.java b/core/src/test/java/com/github/gtache/fxml/compiler/impl/internal/TestLoadMethodFormatter.java index 9582226..10fc2ac 100644 --- a/core/src/test/java/com/github/gtache/fxml/compiler/impl/internal/TestLoadMethodFormatter.java +++ b/core/src/test/java/com/github/gtache/fxml/compiler/impl/internal/TestLoadMethodFormatter.java @@ -119,7 +119,7 @@ class TestLoadMethodFormatter { throw new IllegalStateException("Already loaded"); } java.util.ResourceBundleresourceBundle = java.util.ResourceBundle.getBundle(resourceBundleName); - controller = controllerFactory.create(); + controller = controllerFactory.get(); object-class controller.initialize(); loaded = true; return (T) class; @@ -150,7 +150,7 @@ class TestLoadMethodFormatter { } java.util.ResourceBundleresourceBundle = java.util.ResourceBundle.getBundle("bundle"); java.util.MapfieldMap = new java.util.HashMap(); - object-class controller = controllerFactory.create(fieldMap); + object-class controller = controllerFactory.apply(fieldMap); try { java.lang.reflect.Methodinitialize = controller.getClass().getDeclaredMethod("initialize"); initialize.setAccessible(true); @@ -185,7 +185,7 @@ class TestLoadMethodFormatter { throw new IllegalStateException("Already loaded"); } java.util.MapfieldMap = new java.util.HashMap(); - object-class controller = controllerFactory.create(fieldMap); + object-class controller = controllerFactory.apply(fieldMap); loaded = true; return (T) class; } diff --git a/core/src/test/java/com/github/gtache/fxml/compiler/impl/internal/TestObjectFormatter.java b/core/src/test/java/com/github/gtache/fxml/compiler/impl/internal/TestObjectFormatter.java index 0e89922..3dd9367 100644 --- a/core/src/test/java/com/github/gtache/fxml/compiler/impl/internal/TestObjectFormatter.java +++ b/core/src/test/java/com/github/gtache/fxml/compiler/impl/internal/TestObjectFormatter.java @@ -38,6 +38,7 @@ class TestObjectFormatter { private final GenerationCompatibilityHelper compatibilityHelper; private final InitializationFormatter initializationFormatter; private final ReflectionHelper reflectionHelper; + private final ValueClassGuesser valueClassGuesser; private final VariableProvider variableProvider; private final GenerationRequest request; private final ControllerInfo controllerInfo; @@ -49,7 +50,7 @@ class TestObjectFormatter { TestObjectFormatter(@Mock final HelperProvider helperProvider, @Mock final GenerationCompatibilityHelper compatibilityHelper, @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 SourceInfo sourceInfo) { this.helperProvider = Objects.requireNonNull(helperProvider); @@ -57,6 +58,7 @@ class TestObjectFormatter { this.compatibilityHelper = Objects.requireNonNull(compatibilityHelper); this.initializationFormatter = Objects.requireNonNull(initializationFormatter); this.reflectionHelper = Objects.requireNonNull(reflectionHelper); + this.valueClassGuesser = Objects.requireNonNull(valueClassGuesser); this.variableProvider = Objects.requireNonNull(variableProvider); this.request = Objects.requireNonNull(request); this.controllerInfo = Objects.requireNonNull(controllerInfo); @@ -72,6 +74,7 @@ class TestObjectFormatter { when(helperProvider.getControllerInjector()).thenReturn(controllerInjector); when(helperProvider.getInitializationFormatter()).thenReturn(initializationFormatter); when(helperProvider.getReflectionHelper()).thenReturn(reflectionHelper); + when(helperProvider.getValueClassGuesser()).thenReturn(valueClassGuesser); when(helperProvider.getVariableProvider()).thenReturn(variableProvider); when(compatibilityHelper.getStartVar(anyString())).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 { final var include = new ParsedIncludeImpl("source", null, null); 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()); verify(initializationFormatter).formatSubViewConstructorCall(include); } @@ -316,7 +319,7 @@ class TestObjectFormatter { final var include = new ParsedIncludeImpl("source", "resources", "id"); objectFormatter.format(include, variableName); 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(); """; assertEquals(expected, sb.toString()); @@ -335,7 +338,7 @@ class TestObjectFormatter { final var include = new ParsedIncludeImpl(source, "resources", "id"); objectFormatter.format(include, variableName); 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(); inject(idController, controller)inject(id, variable)"""; assertEquals(expected, sb.toString()); @@ -563,6 +566,10 @@ class TestObjectFormatter { @Test void testFormatConstructorNamedArgs(@Mock final PropertyFormatter propertyFormatter) throws GenerationException { 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()); final var className = "javafx.scene.control.Spinner"; final var attributes = Map.of("min", new ParsedPropertyImpl("min", null, "1"), @@ -585,6 +592,21 @@ class TestObjectFormatter { 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.of("left", new ParsedPropertyImpl("left", null, "2")); + final var properties = new LinkedHashMap>(); + 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 void testFormatConstructorDefault(@Mock final PropertyFormatter propertyFormatter) throws GenerationException { diff --git a/core/src/test/java/com/github/gtache/fxml/compiler/impl/internal/TestPropertyFormatter.java b/core/src/test/java/com/github/gtache/fxml/compiler/impl/internal/TestPropertyFormatter.java index 6657fc3..afc0749 100644 --- a/core/src/test/java/com/github/gtache/fxml/compiler/impl/internal/TestPropertyFormatter.java +++ b/core/src/test/java/com/github/gtache/fxml/compiler/impl/internal/TestPropertyFormatter.java @@ -34,6 +34,7 @@ import static org.mockito.Mockito.*; class TestPropertyFormatter { private final HelperProvider helperProvider; + private final BindingFormatter bindingFormatter; private final VariableProvider variableProvider; private final GenerationCompatibilityHelper compatibilityHelper; private final ControllerInjector controllerInjector; @@ -49,12 +50,13 @@ class TestPropertyFormatter { private final List controllerFactoryPostAction; 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 FieldSetter fieldSetter, @Mock final ValueFormatter valueFormatter, @Mock final GenerationProgress progress, @Mock final GenerationRequest request, @Mock final GenerationParameters parameters, @Mock final ParsedObject rootObject, @Mock final ParsedProperty property) { this.helperProvider = Objects.requireNonNull(helperProvider); + this.bindingFormatter = Objects.requireNonNull(bindingFormatter); this.variableProvider = Objects.requireNonNull(variableProvider); this.compatibilityHelper = Objects.requireNonNull(compatibilityHelper); this.controllerInjector = Objects.requireNonNull(controllerInjector); @@ -73,6 +75,7 @@ class TestPropertyFormatter { @BeforeEach void beforeEach() throws GenerationException { + when(helperProvider.getBindingFormatter()).thenReturn(bindingFormatter); when(helperProvider.getCompatibilityHelper()).thenReturn(compatibilityHelper); when(helperProvider.getControllerInjector()).thenReturn(controllerInjector); when(helperProvider.getFieldSetter()).thenReturn(fieldSetter); @@ -86,13 +89,35 @@ class TestPropertyFormatter { when(progress.controllerFactoryPostAction()).thenReturn(controllerFactoryPostAction); when(compatibilityHelper.getListOf()).thenReturn("listof("); 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(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 void testFormatPropertyId() throws GenerationException { when(property.name()).thenReturn("fx:id"); + when(property.value()).thenReturn("value"); propertyFormatter.formatProperty(property, rootObject, variableName); assertEquals("", sb.toString()); } @@ -100,12 +125,14 @@ class TestPropertyFormatter { @Test void testFormatControllerSame() { when(property.name()).thenReturn("fx:controller"); + when(property.value()).thenReturn(variableName); assertDoesNotThrow(() -> propertyFormatter.formatProperty(property, rootObject, variableName)); } @Test void testFormatControllerDifferent() { when(property.name()).thenReturn("fx:controller"); + when(property.value()).thenReturn("value"); assertThrows(GenerationException.class, () -> propertyFormatter.formatProperty(property, mock(ParsedObject.class), variableName)); } diff --git a/core/src/test/java/com/github/gtache/fxml/compiler/impl/internal/TestReflectionHelper.java b/core/src/test/java/com/github/gtache/fxml/compiler/impl/internal/TestReflectionHelper.java index 2728420..7cbb525 100644 --- a/core/src/test/java/com/github/gtache/fxml/compiler/impl/internal/TestReflectionHelper.java +++ b/core/src/test/java/com/github/gtache/fxml/compiler/impl/internal/TestReflectionHelper.java @@ -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.ParsedProperty; import com.github.gtache.fxml.compiler.parsing.impl.ParsedPropertyImpl; +import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Node; import javafx.scene.control.ComboBox; @@ -21,6 +22,7 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import java.util.Arrays; +import java.util.Collection; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; @@ -67,23 +69,57 @@ class TestReflectionHelper { @Test void testHasMethod() { 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")); } @Test - void testGetMethod() throws NoSuchMethodException { - assertEquals(String.class.getMethod("charAt", int.class), ReflectionHelper.getMethod(String.class, "charAt")); + void testHasMethodStatic() { + 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 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 - void testGetStaticMethod() throws NoSuchMethodException { - assertEquals(HBox.class.getMethod("setHgrow", Node.class, Priority.class), ReflectionHelper.getStaticMethod(HBox.class, "setHgrow")); + void testHasStaticMethodInstance() { + 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 @@ -182,6 +218,11 @@ class TestReflectionHelper { assertEquals(int.class, ReflectionHelper.getClass("int")); } + @Test + void testGetClassNotFound() { + assertThrows(GenerationException.class, () -> ReflectionHelper.getClass("java.lang.ABC")); + } + @Test void testGetGenericTypesNotGeneric() throws GenerationException { when(parsedObject.className()).thenReturn("java.lang.String"); @@ -226,7 +267,14 @@ class TestReflectionHelper { @Test 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 diff --git a/core/src/test/java/com/github/gtache/fxml/compiler/impl/internal/TestValueClassGuesser.java b/core/src/test/java/com/github/gtache/fxml/compiler/impl/internal/TestValueClassGuesser.java new file mode 100644 index 0000000..5168bee --- /dev/null +++ b/core/src/test/java/com/github/gtache/fxml/compiler/impl/internal/TestValueClassGuesser.java @@ -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)); + } +} diff --git a/maven-plugin/src/main/java/com/github/gtache/fxml/compiler/maven/internal/CompilationInfo.java b/maven-plugin/src/main/java/com/github/gtache/fxml/compiler/maven/internal/CompilationInfo.java index 13d7a52..e4b2d77 100644 --- a/maven-plugin/src/main/java/com/github/gtache/fxml/compiler/maven/internal/CompilationInfo.java +++ b/maven-plugin/src/main/java/com/github/gtache/fxml/compiler/maven/internal/CompilationInfo.java @@ -22,7 +22,7 @@ import java.util.Set; */ public record CompilationInfo(Path inputFile, Path outputFile, String outputClass, Path controllerFile, String controllerClass, Set injectedFields, Set injectedMethods, - Map includes, boolean requiresResourceBundle) { + Map includes, boolean requiresResourceBundle) { /** * Instantiates a new info @@ -62,7 +62,7 @@ public record CompilationInfo(Path inputFile, Path outputFile, String outputClas private boolean requiresResourceBundle; private final Set injectedFields; private final Set injectedMethods; - private final Map includes; + private final Map includes; Builder() { 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) { - 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; } diff --git a/maven-plugin/src/main/java/com/github/gtache/fxml/compiler/maven/internal/ControllerInfoProvider.java b/maven-plugin/src/main/java/com/github/gtache/fxml/compiler/maven/internal/ControllerInfoProvider.java index b89d6b0..cfb3d62 100644 --- a/maven-plugin/src/main/java/com/github/gtache/fxml/compiler/maven/internal/ControllerInfoProvider.java +++ b/maven-plugin/src/main/java/com/github/gtache/fxml/compiler/maven/internal/ControllerInfoProvider.java @@ -48,11 +48,11 @@ final class ControllerInfoProvider { final var name = fieldInfo.name(); final var type = fieldInfo.type(); 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(".")) { final var simpleName = type.substring(type.lastIndexOf('.') + 1); 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 { logger.info("Field {}({}) not found in controller {}", name, type, info.controllerFile()); diff --git a/maven-plugin/src/main/java/com/github/gtache/fxml/compiler/maven/internal/Inclusion.java b/maven-plugin/src/main/java/com/github/gtache/fxml/compiler/maven/internal/Inclusion.java new file mode 100644 index 0000000..6bff80f --- /dev/null +++ b/maven-plugin/src/main/java/com/github/gtache/fxml/compiler/maven/internal/Inclusion.java @@ -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"); + } + } +} diff --git a/maven-plugin/src/main/java/com/github/gtache/fxml/compiler/maven/internal/SourceInfoProvider.java b/maven-plugin/src/main/java/com/github/gtache/fxml/compiler/maven/internal/SourceInfoProvider.java index d39d9e8..ffd7cda 100644 --- a/maven-plugin/src/main/java/com/github/gtache/fxml/compiler/maven/internal/SourceInfoProvider.java +++ b/maven-plugin/src/main/java/com/github/gtache/fxml/compiler/maven/internal/SourceInfoProvider.java @@ -5,8 +5,8 @@ import com.github.gtache.fxml.compiler.impl.SourceInfoImpl; import com.github.gtache.fxml.compiler.maven.FXMLCompilerMojo; import java.nio.file.Path; +import java.util.ArrayList; import java.util.HashMap; -import java.util.List; import java.util.Map; /** @@ -31,8 +31,13 @@ final class SourceInfoProvider { final var includes = info.includes(); final var requiresResourceBundle = info.requiresResourceBundle(); final var includesMapping = new HashMap(); - includes.forEach((k, v) -> includesMapping.put(k, getSourceInfo(mapping.get(v), mapping))); - //FIXME mutliple same includes - return new SourceInfoImpl(outputClass, controllerClass, inputFile, List.copyOf(includesMapping.values()), includesMapping, requiresResourceBundle); + includes.forEach((k, v) -> includesMapping.put(k, getSourceInfo(mapping.get(v.path()), mapping))); + final var includesSources = new ArrayList(); + 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); } } diff --git a/maven-plugin/src/test/java/com/github/gtache/fxml/compiler/maven/internal/TestCompilationInfo.java b/maven-plugin/src/test/java/com/github/gtache/fxml/compiler/maven/internal/TestCompilationInfo.java index 80f52cc..04c5171 100644 --- a/maven-plugin/src/test/java/com/github/gtache/fxml/compiler/maven/internal/TestCompilationInfo.java +++ b/maven-plugin/src/test/java/com/github/gtache/fxml/compiler/maven/internal/TestCompilationInfo.java @@ -24,11 +24,11 @@ class TestCompilationInfo { private final String controllerClass; private final Set injectedFields; private final Set injectedMethods; - private final Map includes; + private final Map includes; private final boolean requiresResourceBundle; 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.outputFile = Objects.requireNonNull(outputFile); this.outputClass = "outputClass"; @@ -36,7 +36,7 @@ class TestCompilationInfo { this.controllerClass = "controllerClass"; this.injectedFields = new HashSet<>(Set.of(fieldInfo)); 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.info = new CompilationInfo(inputFile, outputFile, outputClass, controllerFile, controllerClass, injectedFields, injectedMethods, includes, requiresResourceBundle); } diff --git a/maven-plugin/src/test/java/com/github/gtache/fxml/compiler/maven/internal/TestCompilationInfoBuilder.java b/maven-plugin/src/test/java/com/github/gtache/fxml/compiler/maven/internal/TestCompilationInfoBuilder.java index a775974..b855e6f 100644 --- a/maven-plugin/src/test/java/com/github/gtache/fxml/compiler/maven/internal/TestCompilationInfoBuilder.java +++ b/maven-plugin/src/test/java/com/github/gtache/fxml/compiler/maven/internal/TestCompilationInfoBuilder.java @@ -21,10 +21,10 @@ class TestCompilationInfoBuilder { private final String controllerClass; private final Set injectedFields; private final Set injectedMethods; - private final Map includes; + private final Map includes; 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.outputFile = Objects.requireNonNull(outputFile); this.outputClass = "outputClass"; @@ -32,7 +32,7 @@ class TestCompilationInfoBuilder { this.controllerClass = "controllerClass"; this.injectedFields = Set.of(new FieldInfo("type", "name")); 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); } @@ -47,9 +47,14 @@ class TestCompilationInfoBuilder { builder.controllerClass(controllerClass); injectedFields.forEach(f -> builder.addInjectedField(f.name(), f.type())); injectedMethods.forEach(builder::addInjectedMethod); - includes.forEach(builder::addInclude); + builder.addInclude("one", inputFile); builder.requiresResourceBundle(); final var actual = builder.build(); 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()); } } diff --git a/maven-plugin/src/test/java/com/github/gtache/fxml/compiler/maven/internal/TestCompilationInfoProvider.java b/maven-plugin/src/test/java/com/github/gtache/fxml/compiler/maven/internal/TestCompilationInfoProvider.java index 26bdee2..a20b9b8 100644 --- a/maven-plugin/src/test/java/com/github/gtache/fxml/compiler/maven/internal/TestCompilationInfoProvider.java +++ b/maven-plugin/src/test/java/com/github/gtache/fxml/compiler/maven/internal/TestCompilationInfoProvider.java @@ -40,7 +40,7 @@ class TestCompilationInfoProvider { "com.github.gtache.fxml.compiler.maven.internal.InfoView", controllerPath, controllerClass, Set.of(new FieldInfo("javafx.event.EventHandler", "onContextMenuRequested"), new FieldInfo("Button", "button"), 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 actual = compilationInfoProvider.getCompilationInfo(tempDir, path, Map.of(includedPath, "com.github.gtache.fxml.compiler.maven.internal.IncludeController")); assertEquals(expected, actual); @@ -115,7 +115,7 @@ class TestCompilationInfoProvider { "com.github.gtache.fxml.compiler.maven.internal.NoResourceBundle", controllerPath, controllerClass, Set.of(new FieldInfo("javafx.event.EventHandler", "onContextMenuRequested"), new FieldInfo("Button", "button"), 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 actual = compilationInfoProvider.getCompilationInfo(tempDir, path, Map.of(includedPath, "com.github.gtache.fxml.compiler.maven.internal.IncludeController")); assertEquals(expected, actual); diff --git a/maven-plugin/src/test/java/com/github/gtache/fxml/compiler/maven/internal/TestInclusion.java b/maven-plugin/src/test/java/com/github/gtache/fxml/compiler/maven/internal/TestInclusion.java new file mode 100644 index 0000000..625c5b4 --- /dev/null +++ b/maven-plugin/src/test/java/com/github/gtache/fxml/compiler/maven/internal/TestInclusion.java @@ -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)); + } +} diff --git a/maven-plugin/src/test/java/com/github/gtache/fxml/compiler/maven/internal/TestSourceInfoProvider.java b/maven-plugin/src/test/java/com/github/gtache/fxml/compiler/maven/internal/TestSourceInfoProvider.java index 7e0915e..eab9930 100644 --- a/maven-plugin/src/test/java/com/github/gtache/fxml/compiler/maven/internal/TestSourceInfoProvider.java +++ b/maven-plugin/src/test/java/com/github/gtache/fxml/compiler/maven/internal/TestSourceInfoProvider.java @@ -23,7 +23,7 @@ class TestSourceInfoProvider { final var controllerClass = "controllerClass"; final var inputFile = Path.of("inputFile"); 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.controllerClass()).thenReturn(controllerClass); when(compilationInfo.inputFile()).thenReturn(inputFile); @@ -42,7 +42,7 @@ class TestSourceInfoProvider { final var expectedIncludeSourceInfo = new SourceInfoImpl(includeOutputClass, includeControllerClass, includeInputFile, List.of(), Map.of(), false); 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); assertEquals(expected, SourceInfoProvider.getSourceInfo(compilationInfo, mapping)); }