Initial commit

This commit is contained in:
Guillaume Tâche
2024-11-15 19:17:27 +01:00
commit f583c9942b
65 changed files with 7069 additions and 0 deletions

View File

@@ -0,0 +1,26 @@
package com.github.gtache.fxml.compiler.impl;
import com.github.gtache.fxml.compiler.ControllerInjection;
import com.github.gtache.fxml.compiler.InjectionType;
/**
* Base field {@link InjectionType}s for {@link ControllerInjection}
*/
public enum ControllerFieldInjectionTypes implements InjectionType {
/**
* Inject using variable assignment
*/
ASSIGN,
/**
* Inject using a factory
*/
FACTORY,
/**
* Inject using reflection
*/
REFLECTION,
/**
* Inject using setters
*/
SETTERS
}

View File

@@ -0,0 +1,21 @@
package com.github.gtache.fxml.compiler.impl;
import com.github.gtache.fxml.compiler.ControllerInfo;
import java.util.List;
import java.util.Map;
/**
* Implementation of {@link ControllerInfo}
*
* @param handlerHasArgument The mapping of method name to true if the method has an argument
* @param propertyGenericTypes The mapping of property name to generic types
*/
public record ControllerInfoImpl(Map<String, Boolean> handlerHasArgument,
Map<String, List<String>> propertyGenericTypes) implements ControllerInfo {
public ControllerInfoImpl {
handlerHasArgument = Map.copyOf(handlerHasArgument);
propertyGenericTypes = Map.copyOf(propertyGenericTypes);
}
}

View File

@@ -0,0 +1,22 @@
package com.github.gtache.fxml.compiler.impl;
import com.github.gtache.fxml.compiler.ControllerInjection;
import com.github.gtache.fxml.compiler.InjectionType;
import java.util.Objects;
/**
* Implementation of {@link ControllerInjection}
*
* @param fieldInjectionType The field injection type
* @param methodInjectionType The method injection type
* @param injectionClass The injection class name
*/
public record ControllerInjectionImpl(InjectionType fieldInjectionType, InjectionType methodInjectionType,
String injectionClass) implements ControllerInjection {
public ControllerInjectionImpl {
Objects.requireNonNull(fieldInjectionType);
Objects.requireNonNull(methodInjectionType);
Objects.requireNonNull(injectionClass);
}
}

View File

@@ -0,0 +1,18 @@
package com.github.gtache.fxml.compiler.impl;
import com.github.gtache.fxml.compiler.ControllerInjection;
import com.github.gtache.fxml.compiler.InjectionType;
/**
* Base methods {@link InjectionType}s for {@link ControllerInjection}
*/
public enum ControllerMethodsInjectionType implements InjectionType {
/**
* Inject using visible methods
*/
REFERENCE,
/**
* Inject using reflection
*/
REFLECTION,
}

View File

@@ -0,0 +1,30 @@
package com.github.gtache.fxml.compiler.impl;
import com.github.gtache.fxml.compiler.ControllerInjection;
import com.github.gtache.fxml.compiler.GenerationParameters;
import com.github.gtache.fxml.compiler.ResourceBundleInjection;
import java.util.Map;
import java.util.Objects;
/**
* Implementation of {@link GenerationParameters}
*
* @param controllerInjections The mapping of controller class name to controller injection
* @param sourceToGeneratedClassName The mapping of fx:include source to generated class name
* @param sourceToControllerName The mapping of fx:include source to controller class name
* @param resourceBundleInjection The resource bundle injection
*/
public record GenerationParametersImpl(Map<String, ControllerInjection> controllerInjections,
Map<String, String> sourceToGeneratedClassName,
Map<String, String> sourceToControllerName,
ResourceBundleInjection resourceBundleInjection) implements GenerationParameters {
public GenerationParametersImpl {
controllerInjections = Map.copyOf(controllerInjections);
sourceToGeneratedClassName = Map.copyOf(sourceToGeneratedClassName);
sourceToControllerName = Map.copyOf(sourceToControllerName);
Objects.requireNonNull(resourceBundleInjection);
}
}

View File

@@ -0,0 +1,26 @@
package com.github.gtache.fxml.compiler.impl;
import com.github.gtache.fxml.compiler.ControllerInfo;
import com.github.gtache.fxml.compiler.GenerationParameters;
import com.github.gtache.fxml.compiler.GenerationRequest;
import com.github.gtache.fxml.compiler.parsing.ParsedObject;
import java.util.Objects;
/**
* Implementation of {@link GenerationRequest}
*
* @param parameters The generation parameters
* @param rootObject The root object
* @param outputClassName The output class name
*/
public record GenerationRequestImpl(GenerationParameters parameters, ControllerInfo controllerInfo,
ParsedObject rootObject,
String outputClassName) implements GenerationRequest {
public GenerationRequestImpl {
Objects.requireNonNull(parameters);
Objects.requireNonNull(controllerInfo);
Objects.requireNonNull(rootObject);
Objects.requireNonNull(outputClassName);
}
}

View File

@@ -0,0 +1,998 @@
package com.github.gtache.fxml.compiler.impl;
import com.github.gtache.fxml.compiler.ControllerInjection;
import com.github.gtache.fxml.compiler.GenerationRequest;
import com.github.gtache.fxml.compiler.Generator;
import com.github.gtache.fxml.compiler.parsing.ParsedInclude;
import com.github.gtache.fxml.compiler.parsing.ParsedObject;
import com.github.gtache.fxml.compiler.parsing.ParsedProperty;
import javafx.beans.NamedArg;
import javafx.event.EventHandler;
import javafx.scene.Node;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.SequencedMap;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import static java.util.Objects.requireNonNull;
/**
* Implementation of {@link Generator}
*/
public class GeneratorImpl implements Generator {
private static final Map<Class<?>, Boolean> HAS_VALUE_OF = new ConcurrentHashMap<>();
private static final Map<Class<?>, Boolean> IS_GENERIC = new ConcurrentHashMap<>();
private static final Map<Class<?>, Map<String, Method>> METHODS = new ConcurrentHashMap<>();
private static final Map<Class<?>, Map<String, Method>> STATIC_METHODS = new ConcurrentHashMap<>();
private final Collection<String> controllerFactoryPostAction;
private final Map<String, AtomicInteger> variableNameCounters;
/**
* Instantiates a new generator
*/
public GeneratorImpl() {
this.controllerFactoryPostAction = new ArrayList<>();
this.variableNameCounters = new ConcurrentHashMap<>();
}
@Override
public String generate(final GenerationRequest request) {
controllerFactoryPostAction.clear();
variableNameCounters.clear();
final var className = request.outputClassName();
final var pkgName = className.substring(0, className.lastIndexOf('.'));
final var simpleClassName = className.substring(className.lastIndexOf('.') + 1);
final var loadMethod = getLoadMethod(request);
final var controllerInjection = getControllerInjection(request);
final var controllerInjectionType = controllerInjection.fieldInjectionType();
final String constructorArgument;
final String constructorControllerJavadoc;
final String controllerArgumentType;
final String controllerMapType;
final var controllerInjectionClass = controllerInjection.injectionClass();
final var imports = getImports(request);
if (controllerInjectionType == ControllerFieldInjectionTypes.FACTORY) {
constructorArgument = "controllerFactory";
constructorControllerJavadoc = "controller factory";
controllerArgumentType = "ControllerFactory<" + controllerInjectionClass + ">";
controllerMapType = "ControllerFactory<?>";
} else {
constructorArgument = "controller";
constructorControllerJavadoc = "controller";
controllerArgumentType = controllerInjectionClass;
controllerMapType = "Object";
}
final var helperMethods = getHelperMethods(request);
return """
package %1$s;
%9$s
/**
* Generated code, not thread-safe
*/
public final class %2$s {
private final Map<Class<?>, %7$s> controllersMap;
private final Map<Class<?>, ResourceBundle> resourceBundlesMap;
private boolean loaded;
private %3$s controller;
/**
* Instantiates a new %2$s with no nested controllers and no resource bundle
* @param %4$s The %5$s
*/
public %2$s(final %8$s %4$s) {
this(Map.of(%3$s.class, %4$s), Map.of());
}
/**
* Instantiates a new %2$s with no nested controllers
* @param %4$s The %5$s
* @param resourceBundle The resource bundle
*/
public %2$s(final %8$s %4$s, final ResourceBundle resourceBundle) {
this(Map.of(%3$s.class, %4$s), Map.of(%3$s.class, resourceBundle));
}
/**
* Instantiates a new %2$s with nested controllers
* @param controllersMap The map of controller class to %5$s
* @param resourceBundlesMap The map of controller class to resource bundle
*/
public %2$s(final Map<Class<?>, %7$s> controllersMap, final Map<Class<?>, ResourceBundle> resourceBundlesMap) {
this.controllersMap = Map.copyOf(controllersMap);
this.resourceBundlesMap = Map.copyOf(resourceBundlesMap);
}
%6$s
%10$s
/**
* @return The controller
*/
public %3$s controller() {
if (loaded) {
return controller;
} else {
throw new IllegalStateException("Not loaded");
}
}
}
""".formatted(pkgName, simpleClassName, controllerInjectionClass, constructorArgument, constructorControllerJavadoc,
loadMethod, controllerMapType, controllerArgumentType, imports, helperMethods);
}
/**
* Gets helper methods string for the given generation request
*
* @param request The generation request
* @return The helper methods
*/
private static String getHelperMethods(final GenerationRequest request) {
final var injection = getControllerInjection(request);
final var methodInjectionType = injection.methodInjectionType();
final var sb = new StringBuilder();
if (methodInjectionType == ControllerMethodsInjectionType.REFLECTION) {
sb.append("""
private <T extends Event> void callMethod(final String methodName, final T event) {
try {
final Method method;
final var methods = Arrays.stream(controller.getClass().getDeclaredMethods())
.filter(m -> m.getName().equals(methodName)).toList();
if (methods.size() > 1) {
final var eventMethods = methods.stream().filter(m ->
m.getParameterCount() == 1 && Event.class.isAssignableFrom(m.getParameterTypes()[0])).toList();
if (eventMethods.size() == 1) {
method = eventMethods.getFirst();
} else {
final var emptyMethods = methods.stream().filter(m -> m.getParameterCount() == 0).toList();
if (emptyMethods.size() == 1) {
method = emptyMethods.getFirst();
} else {
throw new IllegalArgumentException("Multiple matching methods for " + methodName);
}
}
} else if (methods.size() == 1) {
method = methods.getFirst();
} else {
throw new IllegalArgumentException("No matching method for " + methodName);
}
method.setAccessible(true);
if (method.getParameterCount() == 0) {
method.invoke(controller);
} else {
method.invoke(controller, event);
}
} catch (final IllegalAccessException | InvocationTargetException ex) {
throw new RuntimeException("Error using reflection on " + methodName, ex);
}
}
""");
}
if (injection.fieldInjectionType() == ControllerFieldInjectionTypes.REFLECTION) {
sb.append("""
private <T> void injectField(final String fieldName, final T object) {
try {
final var field = controller.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(controller, object);
} catch (final NoSuchFieldException | IllegalAccessException e) {
throw new RuntimeException("Error using reflection on " + fieldName, e);
}
}""");
}
return sb.toString();
}
/**
* Gets imports for the given generation request
*
* @param request The generation request
* @return The imports
*/
private static String getImports(final GenerationRequest request) {
final var injection = getControllerInjection(request);
final var fieldInjectionType = injection.fieldInjectionType();
final var sb = new StringBuilder("import java.util.Map;\nimport java.util.ResourceBundle;\nimport java.util.HashMap;\n");
if (fieldInjectionType == ControllerFieldInjectionTypes.FACTORY) {
sb.append("import com.github.gtache.fxml.compiler.ControllerFactory;\n");
}
final var methodInjectionType = injection.methodInjectionType();
if (methodInjectionType == ControllerMethodsInjectionType.REFLECTION) {
sb.append("import java.lang.reflect.InvocationTargetException;\n");
sb.append("import java.util.Arrays;\n");
sb.append("import javafx.event.Event;\n");
sb.append("import java.lang.reflect.Method;\n");
}
return sb.toString();
}
/**
* Computes the load method
*
* @param request The generation request
* @return The load method
*/
private String getLoadMethod(final GenerationRequest request) {
final var rootObject = request.rootObject();
final var controllerInjection = getControllerInjection(request);
final var controllerInjectionType = controllerInjection.fieldInjectionType();
final var controllerClass = controllerInjection.injectionClass();
final var sb = new StringBuilder("public javafx.scene.Parent load() {\n");
sb.append(" if (loaded) {\n");
sb.append(" throw new IllegalStateException(\"Already loaded\");\n");
sb.append(" }\n");
final var resourceBundleInjection = request.parameters().resourceBundleInjection();
if (resourceBundleInjection.injectionType() == ResourceBundleInjectionTypes.GET_BUNDLE) {
sb.append(" final var bundle = ResourceBundle.getBundle(\"").append(resourceBundleInjection.bundleName()).append("\");\n");
} else if (resourceBundleInjection.injectionType() == ResourceBundleInjectionTypes.CONSTRUCTOR) {
sb.append(" final var bundle = resourceBundlesMap.get(").append(controllerClass).append(".class);\n");
}
if (controllerInjectionType == ControllerFieldInjectionTypes.FACTORY) {
sb.append(" final var fieldMap = new HashMap<String, Object>();\n");
} else {
sb.append(" controller = (").append(controllerClass).append(") controllersMap.get(").append(controllerClass).append(".class);\n");
}
final var variableName = getNextVariableName("object");
format(request, rootObject, variableName, sb);
if (controllerInjectionType == ControllerFieldInjectionTypes.FACTORY) {
sb.append(" final var controllerFactory = controllersMap.get(").append(controllerClass).append(".class);\n");
sb.append(" controller = (").append(controllerClass).append(") controllerFactory.create(fieldMap);\n");
controllerFactoryPostAction.forEach(sb::append);
}
if (controllerInjection.methodInjectionType() == ControllerMethodsInjectionType.REFLECTION) {
sb.append(" try {\n");
sb.append(" final var initialize = controller.getClass().getDeclaredMethod(\"initialize\");\n");
sb.append(" initialize.setAccessible(true);\n");
sb.append(" initialize.invoke(controller);\n");
sb.append(" } catch (final InvocationTargetException | IllegalAccessException e) {\n");
sb.append(" throw new RuntimeException(\"Error using reflection\", e);\n");
sb.append(" } catch (final NoSuchMethodException ignored) {\n");
sb.append(" }\n");
} else {
sb.append(" controller.initialize();\n");
}
sb.append(" loaded = true;\n");
sb.append(" return ").append(variableName).append(";\n");
sb.append("}");
return sb.toString();
}
/**
* Formats an object
*
* @param request The generation request
* @param parsedObject The parsed object to format
* @param variableName The variable name for the object
* @param sb The string builder
*/
private void format(final GenerationRequest request, final ParsedObject parsedObject, final String variableName, final StringBuilder sb) {
if (parsedObject instanceof final ParsedInclude include) {
formatInclude(request, include, variableName, sb);
} else {
final var clazz = parsedObject.clazz();
final var constructors = clazz.getConstructors();
final var allPropertyNames = new HashSet<>(parsedObject.properties().keySet());
allPropertyNames.addAll(parsedObject.children().keySet().stream().map(ParsedProperty::name).collect(Collectors.toSet()));
final var constructorArgs = getMatchingConstructorArgs(constructors, allPropertyNames);
if (constructorArgs == null) {
if (allPropertyNames.size() == 1 && allPropertyNames.iterator().next().equals("fx:constant")) {
final var property = parsedObject.properties().get("fx:constant");
sb.append(" final var ").append(variableName).append(" = ").append(clazz.getCanonicalName()).append(".").append(property.value()).append(";\n");
} else {
throw new IllegalStateException("Cannot find constructor for " + clazz.getCanonicalName());
}
} else {
final var args = getListConstructorArgs(constructorArgs, parsedObject);
final var genericTypes = getGenericTypes(request, parsedObject);
sb.append(" final var ").append(variableName).append(" = new ").append(clazz.getCanonicalName())
.append(genericTypes).append("(").append(String.join(", ", args)).append(");\n");
parsedObject.properties().entrySet().stream().filter(e -> !constructorArgs.namedArgs().containsKey(e.getKey())).forEach(e -> {
final var p = e.getValue();
formatProperty(request, p, parsedObject, variableName, sb);
});
parsedObject.children().entrySet().stream().filter(e -> !constructorArgs.namedArgs().containsKey(e.getKey().name())).forEach(e -> {
final var p = e.getKey();
final var o = e.getValue();
formatChild(request, parsedObject, p, o, variableName, sb);
});
}
}
}
private static String getGenericTypes(final GenerationRequest request, final ParsedObject parsedObject) {
final var clazz = parsedObject.clazz();
if (isGeneric(clazz)) {
final var idProperty = parsedObject.properties().get("fx:id");
if (idProperty == null) {
return "<>";
} else {
final var id = idProperty.value();
final var genericTypes = request.controllerInfo().propertyGenericTypes(id);
if (genericTypes == null) { //Raw
return "";
} else {
return "<" + String.join(", ", genericTypes) + ">";
}
}
}
return "";
}
/**
* Checks if the given class is generic
* The result is cached
*
* @param clazz The class
* @return True if the class is generic
*/
private static boolean isGeneric(final Class<?> clazz) {
return IS_GENERIC.computeIfAbsent(clazz, c -> c.getTypeParameters().length > 0);
}
/**
* Formats an include object
*
* @param request The generation request
* @param include The include object
* @param subNodeName The sub node name
* @param sb The string builder
*/
private void formatInclude(final GenerationRequest request, final ParsedInclude include, final String subNodeName, final StringBuilder sb) {
final var subViewVariable = getNextVariableName("view");
final var source = include.source();
final var resources = include.resources();
final var subControllerClass = request.parameters().sourceToControllerName().get(source);
final var subClassName = request.parameters().sourceToGeneratedClassName().get(source);
if (subClassName == null) {
throw new IllegalArgumentException("Unknown include source : " + source);
}
if (resources == null) {
sb.append(" final var ").append(subViewVariable).append(" = new ").append(subClassName).append("(controllersMap, resourceBundlesMap);\n");
} else {
final var subResourceBundlesMapVariable = getNextVariableName("map");
final var subBundleVariable = getNextVariableName("bundle");
sb.append(" final var ").append(subResourceBundlesMapVariable).append(" = new HashMap<>(resourceBundlesMap);\n");
sb.append(" final var ").append(subBundleVariable).append(" = ResourceBundle.getBundle(\"").append(resources).append("\");\n");
sb.append(" ").append(subResourceBundlesMapVariable).append(".put(").append(subControllerClass).append(", ").append(subBundleVariable).append(");\n");
sb.append(" final var ").append(subViewVariable).append(" = new ").append(subClassName).append("(controllersMap, ").append(subResourceBundlesMapVariable).append(");\n");
}
sb.append(" final var ").append(subNodeName).append(" = ").append(subViewVariable).append(".load();\n");
final var id = include.controllerId();
if (id != null) {
final var subControllerVariable = getNextVariableName("controller");
sb.append(" final var ").append(subControllerVariable).append(" = ").append(subViewVariable).append(".controller();\n");
injectControllerField(request, id, subControllerVariable, sb);
}
}
/**
* Formats a property
*
* @param request The generation request
* @param property The property to format
* @param parent The property's parent object
* @param parentVariable The parent variable
* @param sb The string builder
*/
private void formatProperty(final GenerationRequest request, final ParsedProperty property, final ParsedObject parent, final String parentVariable, final StringBuilder sb) {
final var propertyName = property.name();
final var setMethod = getSetMethod(propertyName);
if (propertyName.equals("fx:id")) {
final var id = property.value();
injectControllerField(request, id, parentVariable, sb);
} else if (propertyName.equals("fx:controller")) {
if (parent != request.rootObject()) {
throw new IllegalStateException("Invalid nested controller");
}
} else if (property.sourceType() == EventHandler.class) {
injectControllerMethod(request, property, parentVariable, sb);
} else if (property.sourceType() != null) {
if (hasStaticMethod(property.sourceType(), setMethod)) {
final var method = getStaticMethod(property.sourceType(), setMethod);
final var parameterType = method.getParameterTypes()[1];
final var arg = getArg(request, property.value(), parameterType);
sb.append(" ").append(property.sourceType().getName()).append(".").append(setMethod).append("(").append(parentVariable).append(", ").append(arg).append(");\n");
} else {
throw new IllegalStateException("Cannot set " + propertyName + " on " + property.sourceType().getCanonicalName());
}
} else {
final var getMethod = getGetMethod(propertyName);
if (hasMethod(parent.clazz(), setMethod)) {
final var method = getMethod(parent.clazz(), setMethod);
final var parameterType = method.getParameterTypes()[0];
final var arg = getArg(request, property.value(), parameterType);
sb.append(" ").append(parentVariable).append(".").append(setMethod).append("(").append(arg).append(");\n");
} else if (hasMethod(parent.clazz(), getMethod)) {
final var method = getMethod(parent.clazz(), getMethod);
final var returnType = method.getReturnType();
if (hasMethod(returnType, "addAll")) {
final var arg = getArg(request, property.value(), String.class);
sb.append(" ").append(parentVariable).append(".").append(getMethod).append("().addAll(").append(arg).append(");\n");
}
} else {
throw new IllegalStateException("Cannot set " + propertyName + " on " + parent.clazz().getCanonicalName());
}
}
}
/**
* Injects a controller method
*
* @param request The generation request
* @param property The property to inject
* @param parentVariable The parent variable
* @param sb The string builder
*/
private void injectControllerMethod(final GenerationRequest request, final ParsedProperty property, final String parentVariable, final StringBuilder sb) {
final var injection = getControllerInjection(request);
final var methodInjection = getMethodInjection(request, property, parentVariable, sb);
if (injection.fieldInjectionType() instanceof final ControllerFieldInjectionTypes fieldTypes) {
switch (fieldTypes) {
case FACTORY -> controllerFactoryPostAction.add(methodInjection);
case ASSIGN, SETTERS, REFLECTION -> sb.append(methodInjection);
}
} else {
throw new IllegalArgumentException("Unknown injection type : " + injection.fieldInjectionType());
}
}
/**
* Computes the method injection
*
* @param request The generation request
* @param property The property
* @param parentVariable The parent variable
* @param sb The string builder
* @return The method injection
*/
private static String getMethodInjection(final GenerationRequest request, final ParsedProperty property, final String parentVariable, final StringBuilder sb) {
final var setMethod = getSetMethod(property.name());
final var injection = getControllerInjection(request);
final var controllerMethod = property.value().replace("#", "");
if (injection.methodInjectionType() instanceof final ControllerMethodsInjectionType methodTypes) {
return switch (methodTypes) {
case REFERENCE -> {
final var hasArgument = request.controllerInfo().handlerHasArgument(controllerMethod);
if (hasArgument) {
yield " " + parentVariable + "." + setMethod + "(controller::" + controllerMethod + ");\n";
} else {
yield " " + parentVariable + "." + setMethod + "(e -> controller." + controllerMethod + "());\n";
}
}
case REFLECTION ->
" " + parentVariable + "." + setMethod + "(e -> callMethod(\"" + controllerMethod + "\", e));\n";
};
} else {
throw new IllegalArgumentException("Unknown injection type : " + injection.methodInjectionType());
}
}
/**
* Formats an argument to a method
*
* @param request The generation request
* @param value The value
* @param parameterType The parameter type
* @return The formatted value
*/
private static String getArg(final GenerationRequest request, final String value, final Class<?> parameterType) {
if (parameterType == String.class && value.startsWith("%")) {
return getBundleValue(request, value.substring(1));
} else {
return toString(value, parameterType);
}
}
/**
* Injects the given variable into the controller
*
* @param request The generation request
* @param id The object id
* @param variable The object variable
* @param sb The string builder
*/
private static void injectControllerField(final GenerationRequest request, final String id, final String variable, final StringBuilder sb) {
final var controllerInjection = getControllerInjection(request);
final var controllerInjectionType = controllerInjection.fieldInjectionType();
if (controllerInjectionType instanceof final ControllerFieldInjectionTypes types) {
switch (types) {
case FACTORY ->
sb.append(" fieldMap.put(\"").append(id).append("\", ").append(variable).append(");\n");
case ASSIGN -> sb.append(" controller.").append(id).append(" = ").append(variable).append(";\n");
case SETTERS -> {
final var setMethod = getSetMethod(id);
sb.append(" controller.").append(setMethod).append("(").append(variable).append(");\n");
}
case REFLECTION -> {
sb.append(" injectField(\"").append(id).append("\", ").append(variable).append(");\n");
}
}
} else {
throw new IllegalArgumentException("Unknown controller injection type : " + controllerInjectionType);
}
}
/**
* Gets the controller injection object from the generation request
*
* @param request The generation request
* @return The controller injection
*/
private static ControllerInjection getControllerInjection(final GenerationRequest request) {
final var property = request.rootObject().properties().get("fx:controller");
if (property == null) {
throw new IllegalArgumentException("Root object must have a controller property");
} else {
final var id = property.value();
return request.parameters().controllerInjections().get(id);
}
}
/**
* Formats the children objects of a property
*
* @param request The generation request
* @param parent The parent object
* @param property The parent property
* @param objects The child objects
* @param parentVariable The parent object variable
* @param sb The string builder
*/
private void formatChild(final GenerationRequest request, final ParsedObject parent, final ParsedProperty property,
final Collection<? extends ParsedObject> objects, final String parentVariable, final StringBuilder sb) {
final var propertyName = property.name();
final var variables = objects.stream().map(go -> {
final var vn = getNextVariableName("object");
format(request, go, vn, sb);
return vn;
}).toList();
if (variables.size() > 1) {
formatMultipleChildren(variables, propertyName, parent, parentVariable, sb);
} else if (variables.size() == 1) {
final var vn = variables.getFirst();
formatSingleChild(vn, property, parent, parentVariable, sb);
}
}
/**
* Formats children objects given that they are more than one
*
* @param variables The children variables
* @param propertyName The property name
* @param parent The parent object
* @param parentVariable The parent object variable
* @param sb The string builder
*/
private static void formatMultipleChildren(final Iterable<String> variables, final String propertyName, final ParsedObject parent,
final String parentVariable, final StringBuilder sb) {
final var getMethod = getGetMethod(propertyName);
if (hasMethod(parent.clazz(), getMethod)) {
sb.append(" ").append(parentVariable).append(".").append(getMethod).append("().addAll(").append(String.join(", ", variables)).append(");\n");
}
}
/**
* Formats a single child object
*
* @param variableName The child's variable name
* @param property The parent property
* @param parent The parent object
* @param parentVariable The parent object variable
* @param sb The string builder
*/
private static void formatSingleChild(final String variableName, final ParsedProperty property, final ParsedObject parent,
final String parentVariable, final StringBuilder sb) {
if (property.sourceType() == null) {
formatSingleChildInstance(variableName, property, parent, parentVariable, sb);
} else {
formatSingleChildStatic(variableName, property, parentVariable, sb);
}
}
/**
* Formats a single child object using an instance method on the parent object
*
* @param variableName The child's variable name
* @param property The parent property
* @param parent The parent object
* @param parentVariable The parent object variable
* @param sb The string builder
*/
private static void formatSingleChildInstance(final String variableName, final ParsedProperty property, final ParsedObject parent,
final String parentVariable, final StringBuilder sb) {
final var setMethod = getSetMethod(property);
final var getMethod = getGetMethod(property);
if (hasMethod(parent.clazz(), setMethod)) {
sb.append(" ").append(parentVariable).append(".").append(setMethod).append("(").append(variableName).append(");\n");
} else if (hasMethod(parent.clazz(), getMethod)) {
//Probably a list method that has only one element
sb.append(" ").append(parentVariable).append(".").append(getMethod).append("().addAll(").append(variableName).append(");\n");
} else {
throw new IllegalStateException("Cannot set " + property.name() + " on " + parent.clazz().getCanonicalName());
}
}
/**
* Formats a child object using a static method
*
* @param variableName The child's variable name
* @param property The parent property
* @param parentVariable The parent variable
* @param sb The string builder
*/
private static void formatSingleChildStatic(final String variableName, final ParsedProperty property, final String parentVariable, final StringBuilder sb) {
final var setMethod = getSetMethod(property);
if (hasStaticMethod(property.sourceType(), setMethod)) {
sb.append(" ").append(property.sourceType().getName()).append(".").append(setMethod).append("(").append(parentVariable).append(", ").append(variableName).append(");\n");
} else {
throw new IllegalStateException("Cannot set " + property.name() + " on " + property.sourceType().getCanonicalName());
}
}
/**
* Returns the getter method name for the given property
*
* @param property The property
* @return The getter method name
*/
private static String getGetMethod(final ParsedProperty property) {
return getGetMethod(property.name());
}
/**
* Returns the getter method name for the given property name
*
* @param propertyName The property name
* @return The getter method name
*/
private static String getGetMethod(final String propertyName) {
return "get" + propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1);
}
/**
* Returns the setter method name for the given property
*
* @param property The property
* @return The setter method name
*/
private static String getSetMethod(final ParsedProperty property) {
return getSetMethod(property.name());
}
/**
* Returns the setter method name for the given property name
*
* @param propertyName The property name
* @return The setter method name
*/
private static String getSetMethod(final String propertyName) {
return "set" + propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1);
}
/**
* 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
* @return True if the class has a method with the given name
*/
private static boolean hasMethod(final Class<?> clazz, final String methodName) {
final var methodMap = METHODS.computeIfAbsent(clazz, c -> new ConcurrentHashMap<>());
final var method = methodMap.computeIfAbsent(methodName, m -> computeMethod(clazz, m));
return method != null;
}
/**
* Gets the method corresponding to the given class and name
* The result is cached
*
* @param clazz The class
* @param methodName The method name
* @return The method
*/
private static Method getMethod(final Class<?> clazz, final String methodName) {
final var methodMap = METHODS.computeIfAbsent(clazz, c -> new ConcurrentHashMap<>());
return methodMap.computeIfAbsent(methodName, m -> computeMethod(clazz, m));
}
/**
* Checks if the given class has a method with the given name
*
* @param clazz The class
* @param methodName The method name
* @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 {
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;
}
}
/**
* Checks if the given class has a static method with the given name
* The result is cached
*
* @param clazz The class
* @param methodName The method name
* @return True if the class has a static method with the given name
*/
private static boolean hasStaticMethod(final Class<?> clazz, final String methodName) {
final var methodMap = STATIC_METHODS.computeIfAbsent(clazz, c -> new ConcurrentHashMap<>());
final var method = methodMap.computeIfAbsent(methodName, m -> computeStaticMethod(clazz, m));
return method != null;
}
/**
* Gets the static method corresponding to the given class and name
* The result is cached
*
* @param clazz The class
* @param methodName The method name
* @return The method
*/
private static Method getStaticMethod(final Class<?> clazz, final String methodName) {
final var methodMap = STATIC_METHODS.computeIfAbsent(clazz, c -> new ConcurrentHashMap<>());
return methodMap.computeIfAbsent(methodName, m -> computeStaticMethod(clazz, m));
}
/**
* Gets the static method corresponding to the given class and name
*
* @param clazz The class name
* @param methodName The method name
* @return The method, or null if not found
*/
private static Method computeStaticMethod(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 parameterTypes.length > 1 && parameterTypes[0] == Node.class;
} else {
return false;
}
}).toList();
if (matching.size() > 1) {
throw new UnsupportedOperationException("Multiple matching methods not supported yet : " + clazz + " - " + methodName);
} else if (matching.size() == 1) {
return matching.getFirst();
} else {
return null;
}
}
private static String getBundleValue(final GenerationRequest request, final String value) {
final var resourceBundleInjectionType = request.parameters().resourceBundleInjection().injectionType();
if (resourceBundleInjectionType instanceof final ResourceBundleInjectionTypes types) {
return switch (types) {
case CONSTRUCTOR, GET_BUNDLE -> "bundle.getString(\"" + value + "\")";
case GETTER -> {
if (getControllerInjection(request).fieldInjectionType() == ControllerFieldInjectionTypes.FACTORY) {
throw new UnsupportedOperationException("Factory injection with bundle getter not supported yet");
} else {
yield "controller.resources().getString(\"" + value + "\")";
}
}
};
} else {
throw new IllegalArgumentException("Unknown resource bundle injection type : " + resourceBundleInjectionType);
}
}
/**
* Gets the constructor arguments as a list of strings
*
* @param constructorArgs The constructor arguments
* @param parsedObject The parsed object
* @return The list of constructor arguments
*/
private static List<String> getListConstructorArgs(final ConstructorArgs constructorArgs, final ParsedObject parsedObject) {
final var args = new ArrayList<String>(constructorArgs.namedArgs().size());
for (final var entry : constructorArgs.namedArgs().entrySet()) {
final var type = entry.getValue().type();
final var p = parsedObject.properties().get(entry.getKey());
if (p == null) {
final var c = parsedObject.children().entrySet().stream().filter(e ->
e.getKey().name().equals(entry.getKey())).findFirst().orElse(null);
if (c == null) {
args.add(toString(entry.getValue().defaultValue(), type));
} else {
throw new UnsupportedOperationException("Constructor using complex property not supported yet");
}
} else {
args.add(toString(p.value(), type));
}
}
return args;
}
/**
* Gets the constructor arguments that best match the given property names
*
* @param constructors The constructors
* @param allPropertyNames The property names
* @return The matching constructor arguments, or null if no constructor matches and no default constructor exists
*/
private static ConstructorArgs getMatchingConstructorArgs(final Constructor<?>[] constructors, final Set<String> allPropertyNames) {
ConstructorArgs matchingConstructorArgs = null;
for (final var constructor : constructors) {
final var constructorArgs = getConstructorArgs(constructor);
final var matchingArgsCount = getMatchingArgsCount(constructorArgs, allPropertyNames);
if (matchingConstructorArgs == null ? matchingArgsCount > 0 : matchingArgsCount > getMatchingArgsCount(matchingConstructorArgs, allPropertyNames)) {
matchingConstructorArgs = constructorArgs;
}
}
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;
}
}
/**
* Checks how many arguments of the given constructor match the given property names
*
* @param constructorArgs The constructor arguments
* @param allPropertyNames The property names
* @return The number of matching arguments
*/
private static long getMatchingArgsCount(final ConstructorArgs constructorArgs, final Set<String> allPropertyNames) {
return constructorArgs.namedArgs().keySet().stream().filter(allPropertyNames::contains).count();
}
/**
* Computes the constructor arguments for the given constructor
*
* @param constructor The constructor
* @return The constructor arguments
*/
private static ConstructorArgs getConstructorArgs(final Constructor<?> constructor) {
final var namedArgs = new LinkedHashMap<String, Parameter>();
final var annotationsArray = constructor.getParameterAnnotations();
for (var i = 0; i < annotationsArray.length; i++) {
final var annotations = annotationsArray[i];
final var getNamedArg = Arrays.stream(annotations).filter(NamedArg.class::isInstance).findFirst().orElse(null);
if (getNamedArg != null) {
final var namedArg = (NamedArg) getNamedArg;
final var name = namedArg.value();
final var clazz = constructor.getParameterTypes()[i];
namedArgs.put(name, new Parameter(name, constructor.getParameterTypes()[i], namedArg.defaultValue().isEmpty() ?
getDefaultValue(clazz) : namedArg.defaultValue()));
}
}
return new ConstructorArgs(constructor, namedArgs);
}
/**
* Computes the default value for the given class
*
* @param clazz The class
* @return The value
*/
private static String getDefaultValue(final Class<?> clazz) {
final var primitiveWrappers = Set.of(Integer.class, Byte.class, Short.class, Long.class, Float.class, Double.class);
if (clazz == char.class || clazz == Character.class) {
return "\u0000";
} else if (clazz == boolean.class || clazz == Boolean.class) {
return "false";
} else if (clazz.isPrimitive() || primitiveWrappers.contains(clazz)) {
return "0";
} else {
return "null";
}
}
/**
* Computes the string value to use in the generated code
*
* @param value The value
* @param clazz The value class
* @return The computed string value
*/
private static String toString(final String value, final Class<?> clazz) {
final var primitiveWrappers = Set.of(Integer.class, Byte.class, Short.class, Long.class, Float.class, Double.class, Boolean.class);
if (clazz == String.class) {
return "\"" + value.replace("\"", "\\\"") + "\"";
} else if (clazz == char.class || clazz == Character.class) {
return "'" + value + "'";
} else if (clazz.isPrimitive() || primitiveWrappers.contains(clazz)) {
return value;
} else if (hasValueOf(clazz)) {
if (clazz.isEnum()) {
return clazz.getCanonicalName() + "." + value;
} else {
return clazz.getCanonicalName() + ".valueOf(\"" + value + "\")";
}
} else {
return value;
}
}
/**
* Checks if the given class has a valueOf(String) method
* The result is cached
*
* @param clazz The class
* @return True if the class has a valueOf(String)
*/
private static boolean hasValueOf(final Class<?> clazz) {
return HAS_VALUE_OF.computeIfAbsent(clazz, GeneratorImpl::computeHasValueOf);
}
/**
* Computes if the given class has a valueOf(String) method
*
* @param clazz The class
* @return True if the class has a valueOf(String)
*/
private static boolean computeHasValueOf(final Class<?> clazz) {
try {
clazz.getMethod("valueOf", String.class);
return true;
} catch (final NoSuchMethodException ignored) {
return false;
}
}
/**
* Computes the next available variable name for the given prefix
*
* @param prefix The variable name prefix
* @return The next available variable name
*/
private String getNextVariableName(final String prefix) {
final var counter = variableNameCounters.computeIfAbsent(prefix, k -> new AtomicInteger(0));
return prefix + counter.getAndIncrement();
}
private record ConstructorArgs(Constructor<?> constructor,
SequencedMap<String, GeneratorImpl.Parameter> namedArgs) {
private ConstructorArgs {
requireNonNull(constructor);
namedArgs = new LinkedHashMap<>(namedArgs);
}
}
private record Parameter(String name, Class<?> type, String defaultValue) {
private Parameter {
requireNonNull(name);
requireNonNull(type);
requireNonNull(defaultValue);
}
}
}

View File

@@ -0,0 +1,21 @@
package com.github.gtache.fxml.compiler.impl;
import com.github.gtache.fxml.compiler.InjectionType;
import com.github.gtache.fxml.compiler.ResourceBundleInjection;
import java.util.Objects;
/**
* Implementation of {@link ResourceBundleInjection}
*
* @param injectionType The injection type
* @param bundleName The bundle name
*/
public record ResourceBundleInjectionImpl(InjectionType injectionType,
String bundleName) implements ResourceBundleInjection {
public ResourceBundleInjectionImpl {
Objects.requireNonNull(injectionType);
Objects.requireNonNull(bundleName);
}
}

View File

@@ -0,0 +1,22 @@
package com.github.gtache.fxml.compiler.impl;
import com.github.gtache.fxml.compiler.InjectionType;
import com.github.gtache.fxml.compiler.ResourceBundleInjection;
/**
* Base {@link InjectionType}s for {@link ResourceBundleInjection}
*/
public enum ResourceBundleInjectionTypes implements InjectionType {
/**
* Resource bundle is injected in the constructor
*/
CONSTRUCTOR,
/**
* Resource bundle is loaded using getBundle
*/
GET_BUNDLE,
/**
* Resource bundle is retrieved from controller using getter
*/
GETTER
}

View File

@@ -0,0 +1,19 @@
package com.github.gtache.fxml.compiler.parsing.impl;
import com.github.gtache.fxml.compiler.parsing.ParsedInclude;
import com.github.gtache.fxml.compiler.parsing.ParsedProperty;
import java.util.LinkedHashMap;
import java.util.SequencedMap;
/**
* Implementation of {@link ParsedInclude}
*
* @param properties The object properties
*/
public record ParsedIncludeImpl(SequencedMap<String, ParsedProperty> properties) implements ParsedInclude {
public ParsedIncludeImpl {
properties = new LinkedHashMap<>(properties);
}
}

View File

@@ -0,0 +1,90 @@
package com.github.gtache.fxml.compiler.parsing.impl;
import com.github.gtache.fxml.compiler.parsing.ParsedObject;
import com.github.gtache.fxml.compiler.parsing.ParsedProperty;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.Objects;
import java.util.SequencedCollection;
import java.util.SequencedMap;
/**
* Implementation of {@link ParsedObject}
*
* @param clazz The object class
* @param properties The object properties
* @param children The object children (complex properties)
*/
public record ParsedObjectImpl(Class<?> clazz, SequencedMap<String, ParsedProperty> properties,
SequencedMap<ParsedProperty, SequencedCollection<ParsedObject>> children) implements ParsedObject {
public ParsedObjectImpl {
Objects.requireNonNull(clazz);
properties = new LinkedHashMap<>(properties);
children = new LinkedHashMap<>(children);
}
/**
* Builder for {@link ParsedObjectImpl}
*/
public static class Builder {
private Class<?> clazz;
private final SequencedMap<String, ParsedProperty> properties;
private final SequencedMap<ParsedProperty, SequencedCollection<ParsedObject>> children;
/**
* Creates a new builder
*/
public Builder() {
this.properties = new LinkedHashMap<>();
this.children = new LinkedHashMap<>();
}
/**
* Sets the object class
*
* @param clazz The object class
* @return The builder
*/
public Builder clazz(final Class<?> clazz) {
this.clazz = clazz;
return this;
}
/**
* Adds a property
*
* @param property The property
* @return The builder
*/
public Builder addProperty(final ParsedProperty property) {
properties.put(property.name(), property);
return this;
}
/**
* Adds a child
*
* @param property The property
* @param child The child
* @return The builder
*/
public Builder addChild(final ParsedProperty property, final ParsedObject child) {
final var sequence = children.computeIfAbsent(property, k -> new ArrayList<>());
sequence.add(child);
children.put(property, sequence);
return this;
}
/**
* Builds the object
*
* @return The object
*/
public ParsedObjectImpl build() {
return new ParsedObjectImpl(clazz, properties, children);
}
}
}

View File

@@ -0,0 +1,19 @@
package com.github.gtache.fxml.compiler.parsing.impl;
import com.github.gtache.fxml.compiler.parsing.ParsedProperty;
import java.util.Objects;
/**
* Implementation of {@link ParsedProperty}
*
* @param name The property name
* @param sourceType The property source type
* @param value The property value
*/
public record ParsedPropertyImpl(String name, Class<?> sourceType, String value) implements ParsedProperty {
public ParsedPropertyImpl {
Objects.requireNonNull(name);
}
}

View File

@@ -0,0 +1,65 @@
package com.github.gtache.fxml.compiler.impl;
import com.github.gtache.fxml.compiler.ControllerInfo;
import org.junit.jupiter.api.Test;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
class TestControllerInfoImpl {
private final Map<String, Boolean> handlerHasArgument;
private final Map<String, List<String>> propertyGenericTypes;
private final ControllerInfo info;
TestControllerInfoImpl() {
this.handlerHasArgument = new HashMap<>(Map.of("one", true, "two", false));
this.propertyGenericTypes = new HashMap<>(Map.of("one", List.of("a", "b"), "two", List.of()));
this.info = new ControllerInfoImpl(handlerHasArgument, propertyGenericTypes);
}
@Test
void testHandlerHasArgument() {
assertEquals(handlerHasArgument, info.handlerHasArgument());
assertTrue(info.handlerHasArgument("one"));
assertFalse(info.handlerHasArgument("two"));
assertTrue(info.handlerHasArgument("three"));
}
@Test
void testPropertyGenericTypes() {
assertEquals(propertyGenericTypes, info.propertyGenericTypes());
assertEquals(List.of("a", "b"), info.propertyGenericTypes("one"));
assertEquals(List.of(), info.propertyGenericTypes("two"));
}
@Test
void testMapsCopied() {
final var originalHandler = Map.copyOf(handlerHasArgument);
final var originalPropertyTypes = Map.copyOf(propertyGenericTypes);
assertEquals(originalHandler, info.handlerHasArgument());
assertEquals(originalPropertyTypes, info.propertyGenericTypes());
handlerHasArgument.clear();
propertyGenericTypes.clear();
assertEquals(originalHandler, info.handlerHasArgument());
assertEquals(originalPropertyTypes, info.propertyGenericTypes());
}
@Test
void testUnmodifiable() {
final var infoHandler = info.handlerHasArgument();
final var infoProperty = info.propertyGenericTypes();
assertThrows(UnsupportedOperationException.class, infoHandler::clear);
assertThrows(UnsupportedOperationException.class, infoProperty::clear);
}
@Test
void testIllegal() {
assertThrows(NullPointerException.class, () -> new ControllerInfoImpl(null, propertyGenericTypes));
assertThrows(NullPointerException.class, () -> new ControllerInfoImpl(handlerHasArgument, null));
}
}

View File

@@ -0,0 +1,43 @@
package com.github.gtache.fxml.compiler.impl;
import com.github.gtache.fxml.compiler.ControllerInjection;
import com.github.gtache.fxml.compiler.InjectionType;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Objects;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
@ExtendWith(MockitoExtension.class)
class TestControllerInjectionImpl {
private final InjectionType fieldInjectionType;
private final InjectionType methodInjectionType;
private final String injectionClass;
private final ControllerInjection controllerInjection;
TestControllerInjectionImpl(@Mock final InjectionType fieldInjectionType, @Mock final InjectionType methodInjectionType) {
this.fieldInjectionType = Objects.requireNonNull(fieldInjectionType);
this.methodInjectionType = Objects.requireNonNull(methodInjectionType);
this.injectionClass = "class";
this.controllerInjection = new ControllerInjectionImpl(fieldInjectionType, methodInjectionType, injectionClass);
}
@Test
void testGetters() {
assertEquals(fieldInjectionType, controllerInjection.fieldInjectionType());
assertEquals(methodInjectionType, controllerInjection.methodInjectionType());
assertEquals(injectionClass, controllerInjection.injectionClass());
}
@Test
void testIllegal() {
assertThrows(NullPointerException.class, () -> new ControllerInjectionImpl(null, methodInjectionType, injectionClass));
assertThrows(NullPointerException.class, () -> new ControllerInjectionImpl(fieldInjectionType, null, injectionClass));
assertThrows(NullPointerException.class, () -> new ControllerInjectionImpl(fieldInjectionType, methodInjectionType, null));
}
}

View File

@@ -0,0 +1,49 @@
package com.github.gtache.fxml.compiler.impl;
import com.github.gtache.fxml.compiler.ControllerInjection;
import com.github.gtache.fxml.compiler.GenerationParameters;
import com.github.gtache.fxml.compiler.ResourceBundleInjection;
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.Map;
import java.util.Objects;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
@ExtendWith(MockitoExtension.class)
class TestGenerationParametersImpl {
private final Map<String, ControllerInjection> controllerInjections;
private final Map<String, String> sourceToGeneratedClassName;
private final Map<String, String> sourceToControllerName;
private final ResourceBundleInjection resourceBundleInjection;
private final GenerationParameters parameters;
TestGenerationParametersImpl(@Mock final ControllerInjection controllerInjection, @Mock final ResourceBundleInjection resourceBundleInjection) {
this.controllerInjections = Map.of("class", controllerInjection);
this.sourceToGeneratedClassName = Map.of("source", "generated");
this.sourceToControllerName = Map.of("source", "class");
this.resourceBundleInjection = Objects.requireNonNull(resourceBundleInjection);
this.parameters = new GenerationParametersImpl(controllerInjections, sourceToGeneratedClassName, sourceToControllerName, resourceBundleInjection);
}
@Test
void testGetters() {
assertEquals(controllerInjections, parameters.controllerInjections());
assertEquals(sourceToGeneratedClassName, parameters.sourceToGeneratedClassName());
assertEquals(sourceToControllerName, parameters.sourceToControllerName());
assertEquals(resourceBundleInjection, parameters.resourceBundleInjection());
}
@Test
void testIllegal() {
assertThrows(NullPointerException.class, () -> new GenerationParametersImpl(null, sourceToGeneratedClassName, sourceToControllerName, resourceBundleInjection));
assertThrows(NullPointerException.class, () -> new GenerationParametersImpl(controllerInjections, null, sourceToControllerName, resourceBundleInjection));
assertThrows(NullPointerException.class, () -> new GenerationParametersImpl(controllerInjections, sourceToGeneratedClassName, null, resourceBundleInjection));
assertThrows(NullPointerException.class, () -> new GenerationParametersImpl(controllerInjections, sourceToGeneratedClassName, sourceToControllerName, null));
}
}

View File

@@ -0,0 +1,49 @@
package com.github.gtache.fxml.compiler.impl;
import com.github.gtache.fxml.compiler.ControllerInfo;
import com.github.gtache.fxml.compiler.GenerationParameters;
import com.github.gtache.fxml.compiler.GenerationRequest;
import com.github.gtache.fxml.compiler.parsing.ParsedObject;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Objects;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
@ExtendWith(MockitoExtension.class)
class TestGenerationRequestImpl {
private final GenerationParameters parameters;
private final ControllerInfo controllerInfo;
private final ParsedObject rootObject;
private final String outputClassName;
private final GenerationRequest request;
TestGenerationRequestImpl(@Mock final GenerationParameters parameters, @Mock final ControllerInfo controllerInfo, @Mock final ParsedObject rootObject) {
this.parameters = Objects.requireNonNull(parameters);
this.controllerInfo = Objects.requireNonNull(controllerInfo);
this.rootObject = Objects.requireNonNull(rootObject);
this.outputClassName = "class";
this.request = new GenerationRequestImpl(parameters, controllerInfo, rootObject, outputClassName);
}
@Test
void testGetters() {
assertEquals(parameters, request.parameters());
assertEquals(controllerInfo, request.controllerInfo());
assertEquals(rootObject, request.rootObject());
assertEquals(outputClassName, request.outputClassName());
}
@Test
void testIllegal() {
assertThrows(NullPointerException.class, () -> new GenerationRequestImpl(null, controllerInfo, rootObject, outputClassName));
assertThrows(NullPointerException.class, () -> new GenerationRequestImpl(parameters, null, rootObject, outputClassName));
assertThrows(NullPointerException.class, () -> new GenerationRequestImpl(parameters, controllerInfo, null, outputClassName));
assertThrows(NullPointerException.class, () -> new GenerationRequestImpl(parameters, controllerInfo, rootObject, null));
}
}

View File

@@ -0,0 +1,4 @@
package com.github.gtache.fxml.compiler.impl;
class TestGeneratorImpl {
}

View File

@@ -0,0 +1,39 @@
package com.github.gtache.fxml.compiler.impl;
import com.github.gtache.fxml.compiler.InjectionType;
import com.github.gtache.fxml.compiler.ResourceBundleInjection;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Objects;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
@ExtendWith(MockitoExtension.class)
class TestResourceBundleInjectionImpl {
private final InjectionType injectionType;
private final String bundleName;
private final ResourceBundleInjection resourceBundleInjection;
TestResourceBundleInjectionImpl(@Mock final InjectionType injectionType) {
this.injectionType = Objects.requireNonNull(injectionType);
this.bundleName = "bundle";
this.resourceBundleInjection = new ResourceBundleInjectionImpl(injectionType, bundleName);
}
@Test
void testGetters() {
assertEquals(injectionType, resourceBundleInjection.injectionType());
assertEquals(bundleName, resourceBundleInjection.bundleName());
}
@Test
void testIllegal() {
assertThrows(NullPointerException.class, () -> new ResourceBundleInjectionImpl(null, bundleName));
assertThrows(NullPointerException.class, () -> new ResourceBundleInjectionImpl(injectionType, null));
}
}

View File

@@ -0,0 +1,4 @@
package com.github.gtache.fxml.compiler.parsing.impl;
public class TestParsedIncludeImpl {
}

View File

@@ -0,0 +1,66 @@
package com.github.gtache.fxml.compiler.parsing.impl;
import com.github.gtache.fxml.compiler.parsing.ParsedObject;
import com.github.gtache.fxml.compiler.parsing.ParsedProperty;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.SequencedCollection;
import java.util.SequencedMap;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
@ExtendWith(MockitoExtension.class)
class TestParsedObjectImpl {
private final Class<?> clazz;
private final SequencedMap<String, ParsedProperty> properties;
private final SequencedMap<ParsedProperty, SequencedCollection<ParsedObject>> children;
private final ParsedObject parsedObject;
TestParsedObjectImpl(@Mock final ParsedProperty property, @Mock final ParsedObject object) {
this.clazz = Object.class;
this.properties = new LinkedHashMap<>();
this.properties.put("name", property);
this.children = new LinkedHashMap<>();
this.children.put(property, List.of(object));
this.parsedObject = new ParsedObjectImpl(clazz, properties, children);
}
@Test
void testGetters() {
assertEquals(clazz, parsedObject.clazz());
assertEquals(properties, parsedObject.properties());
assertEquals(children, parsedObject.children());
}
@Test
void testCopyMap() {
final var originalProperties = parsedObject.properties();
final var originalChildren = parsedObject.children();
properties.clear();
children.clear();
assertEquals(originalProperties, parsedObject.properties());
assertEquals(originalChildren, parsedObject.children());
}
@Test
void testUnmodifiable() {
final var objectProperties = parsedObject.properties();
final var objectChildren = parsedObject.children();
assertThrows(UnsupportedOperationException.class, objectProperties::clear);
assertThrows(UnsupportedOperationException.class, objectChildren::clear);
}
@Test
void testIllegal() {
assertThrows(NullPointerException.class, () -> new ParsedObjectImpl(null, properties, children));
assertThrows(NullPointerException.class, () -> new ParsedObjectImpl(clazz, null, children));
assertThrows(NullPointerException.class, () -> new ParsedObjectImpl(clazz, properties, null));
}
}

View File

@@ -0,0 +1,108 @@
package com.github.gtache.fxml.compiler.parsing.impl;
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.List;
import java.util.Map;
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.when;
@ExtendWith(MockitoExtension.class)
class TestParsedObjectImplBuilder {
private final Class<?> clazz1;
private final Class<?> clazz2;
private final ParsedProperty property1;
private final ParsedProperty property2;
private final ParsedObject object1;
private final ParsedObject object2;
private final ParsedObjectImpl.Builder builder;
TestParsedObjectImplBuilder(@Mock final ParsedProperty property1, @Mock final ParsedProperty property2,
@Mock final ParsedObject object1, @Mock final ParsedObject object2) {
this.clazz1 = Object.class;
this.clazz2 = String.class;
this.property1 = Objects.requireNonNull(property1);
this.property2 = Objects.requireNonNull(property2);
this.object1 = Objects.requireNonNull(object1);
this.object2 = Objects.requireNonNull(object2);
this.builder = new ParsedObjectImpl.Builder();
}
@BeforeEach
void beforeEach() {
when(property1.name()).thenReturn("property1");
when(property2.name()).thenReturn("property2");
}
@Test
void testBuildNullClass() {
assertThrows(NullPointerException.class, builder::build);
}
@Test
void testClazz() {
builder.clazz(clazz1);
final var built = builder.build();
assertEquals(clazz1, built.clazz());
assertEquals(Map.of(), built.properties());
assertEquals(Map.of(), built.children());
}
@Test
void testOverwriteClazz() {
builder.clazz(clazz1);
builder.clazz(clazz2);
final var built = builder.build();
assertEquals(clazz2, built.clazz());
assertEquals(Map.of(), built.properties());
assertEquals(Map.of(), built.children());
}
@Test
void testAddProperty() {
builder.clazz(clazz1);
builder.addProperty(property1);
final var built = builder.build();
assertEquals(Map.of(property1.name(), property1), built.properties());
assertEquals(Map.of(), built.children());
}
@Test
void testAddMultipleProperties() {
builder.clazz(clazz1);
builder.addProperty(property1);
builder.addProperty(property2);
final var built = builder.build();
assertEquals(Map.of(property1.name(), property1, property2.name(), property2), built.properties());
assertEquals(Map.of(), built.children());
}
@Test
void testAddChild() {
builder.clazz(clazz1);
builder.addChild(property1, object1);
final var built = builder.build();
assertEquals(Map.of(), built.properties());
assertEquals(Map.of(property1, List.of(object1)), built.children());
}
@Test
void testAddMultipleChildren() {
builder.clazz(clazz1);
builder.addChild(property1, object1);
builder.addChild(property2, object2);
final var built = builder.build();
assertEquals(Map.of(), built.properties());
assertEquals(Map.of(property1, List.of(object1), property2, List.of(object2)), built.children());
}
}

View File

@@ -0,0 +1,35 @@
package com.github.gtache.fxml.compiler.parsing.impl;
import com.github.gtache.fxml.compiler.parsing.ParsedProperty;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class TestParsedPropertyImpl {
private final String name;
private final Class<?> sourceType;
private final String value;
private final ParsedProperty property;
TestParsedPropertyImpl() {
this.name = "name";
this.sourceType = Object.class;
this.value = "value";
this.property = new ParsedPropertyImpl(name, sourceType, value);
}
@Test
void testGetters() {
assertEquals(name, property.name());
assertEquals(sourceType, property.sourceType());
assertEquals(value, property.value());
}
@Test
void testIllegal() {
assertThrows(NullPointerException.class, () -> new ParsedPropertyImpl(null, sourceType, value));
assertDoesNotThrow(() -> new ParsedPropertyImpl(name, null, value));
assertDoesNotThrow(() -> new ParsedPropertyImpl(name, sourceType, null));
}
}