Removes loader, adds some missing features, refactors for easier comprehension and maintenance

This commit is contained in:
Guillaume Tâche
2024-11-30 17:47:47 +01:00
parent 102927b040
commit 17d112fb41
177 changed files with 17395 additions and 31206 deletions

View File

@@ -1,83 +1,59 @@
package com.github.gtache.fxml.compiler.impl;
import com.github.gtache.fxml.compiler.ControllerInjection;
import com.github.gtache.fxml.compiler.GenerationException;
import com.github.gtache.fxml.compiler.GenerationRequest;
import com.github.gtache.fxml.compiler.Generator;
import com.github.gtache.fxml.compiler.parsing.ParsedConstant;
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.event.EventHandler;
import com.github.gtache.fxml.compiler.impl.internal.GenerationProgress;
import com.github.gtache.fxml.compiler.impl.internal.HelperMethodsProvider;
import java.lang.reflect.Constructor;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static com.github.gtache.fxml.compiler.impl.internal.GenerationHelper.getControllerInjection;
import static com.github.gtache.fxml.compiler.impl.internal.GenerationHelper.getVariablePrefix;
import static com.github.gtache.fxml.compiler.impl.internal.ObjectFormatter.format;
import static com.github.gtache.fxml.compiler.impl.ReflectionHelper.*;
//TODO handle binding (${})
/**
* Implementation of {@link Generator}
*/
public class GeneratorImpl implements Generator {
private static final Pattern INT_PATTERN = Pattern.compile("\\d+");
private static final Pattern DECIMAL_PATTERN = Pattern.compile("\\d+(?:\\.\\d+)?");
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) throws GenerationException {
controllerFactoryPostAction.clear();
variableNameCounters.clear();
final var progress = new GenerationProgress(request);
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 loadMethod = getLoadMethod(progress);
final var controllerInjection = getControllerInjection(progress);
final var controllerInjectionType = controllerInjection.fieldInjectionType();
final var controllerInjectionClass = controllerInjection.injectionClass();
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<?>";
controllerArgumentType = "com.github.gtache.fxml.compiler.ControllerFactory<" + controllerInjectionClass + ">";
controllerMapType = "com.github.gtache.fxml.compiler.ControllerFactory<?>";
} else {
constructorArgument = "controller";
constructorControllerJavadoc = "controller";
controllerArgumentType = controllerInjectionClass;
controllerMapType = "Object";
}
final var helperMethods = getHelperMethods(request);
final var helperMethods = HelperMethodsProvider.getHelperMethods(progress);
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 final java.util.Map<Class<?>, %7$s> controllersMap;
private final java.util.Map<Class<?>, java.util.ResourceBundle> resourceBundlesMap;
private boolean loaded;
private %3$s controller;
@@ -86,7 +62,7 @@ public class GeneratorImpl implements Generator {
* @param %4$s The %5$s
*/
public %2$s(final %8$s %4$s) {
this(Map.of(%3$s.class, %4$s), Map.of());
this(java.util.Map.of(%3$s.class, %4$s), java.util.Map.of());
}
/**
@@ -94,8 +70,8 @@ public class GeneratorImpl implements Generator {
* @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));
public %2$s(final %8$s %4$s, final java.util.ResourceBundle resourceBundle) {
this(java.util.Map.of(%3$s.class, %4$s), java.util.Map.of(%3$s.class, resourceBundle));
}
/**
@@ -103,9 +79,9 @@ public class GeneratorImpl implements Generator {
* @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);
public %2$s(final java.util.Map<Class<?>, %7$s> controllersMap, final java.util.Map<Class<?>, java.util.ResourceBundle> resourceBundlesMap) {
this.controllersMap = java.util.Map.copyOf(controllersMap);
this.resourceBundlesMap = java.util.Map.copyOf(resourceBundlesMap);
}
/**
@@ -115,7 +91,7 @@ public class GeneratorImpl implements Generator {
*/
%6$s
%10$s
%9$s
/**
* @return The controller
@@ -129,113 +105,30 @@ public class GeneratorImpl implements Generator {
}
}
""".formatted(pkgName, simpleClassName, controllerInjectionClass, constructorArgument, constructorControllerJavadoc,
loadMethod, controllerMapType, controllerArgumentType, imports, helperMethods);
loadMethod, controllerMapType, controllerArgumentType, 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) throws GenerationException {
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) throws GenerationException {
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
* @param progress The generation progress
* @return The load method
*/
private String getLoadMethod(final GenerationRequest request) throws GenerationException {
private static String getLoadMethod(final GenerationProgress progress) throws GenerationException {
final var request = progress.request();
final var rootObject = request.rootObject();
final var controllerInjection = getControllerInjection(request);
final var controllerInjection = getControllerInjection(progress);
final var controllerInjectionType = controllerInjection.fieldInjectionType();
final var controllerClass = controllerInjection.injectionClass();
final var sb = new StringBuilder("public <T> T load() {\n");
final var sb = progress.stringBuilder();
sb.append("public <T> T load() {\n");
sb.append(" if (loaded) {\n");
sb.append(" throw new IllegalStateException(\"Already loaded\");\n");
sb.append(" }\n");
final var resourceBundleInjection = request.parameters().resourceBundleInjection();
if (resourceBundleInjection.injectionType() == ResourceBundleInjectionTypes.GET_BUNDLE) {
sb.append(" final var bundle = ResourceBundle.getBundle(\"").append(resourceBundleInjection.bundleName()).append("\");\n");
sb.append(" final var bundle = java.util.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");
}
@@ -244,19 +137,19 @@ public class GeneratorImpl implements Generator {
} else {
sb.append(" controller = (").append(controllerClass).append(") controllersMap.get(").append(controllerClass).append(".class);\n");
}
final var variableName = getNextVariableName("object");
format(request, rootObject, variableName, sb);
final var variableName = progress.getNextVariableName(getVariablePrefix(rootObject));
format(progress, rootObject, variableName);
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);
progress.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(" } catch (final java.lang.reflect.InvocationTargetException | IllegalAccessException e) {\n");
sb.append(" throw new RuntimeException(\"Error using reflection\", e);\n");
sb.append(" } catch (final NoSuchMethodException ignored) {\n");
sb.append(" }\n");
@@ -264,584 +157,10 @@ public class GeneratorImpl implements Generator {
sb.append(" controller.initialize();\n");
}
sb.append(" loaded = true;\n");
sb.append(" return ").append(variableName).append(";\n");
sb.append(" return (T) ").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) throws GenerationException {
switch (parsedObject) {
case final ParsedInclude include -> formatInclude(request, include, variableName, sb);
case final ParsedConstant constant -> formatConstant(constant, variableName, sb);
default -> {
final var clazz = getClass(parsedObject.className());
final var constructors = clazz.getConstructors();
final var allPropertyNames = new HashSet<>(parsedObject.attributes().keySet());
allPropertyNames.addAll(parsedObject.properties().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.attributes().get("fx:constant");
sb.append(" final var ").append(variableName).append(" = ").append(clazz.getCanonicalName()).append(".").append(property.value()).append(";\n");
} else {
throw new GenerationException("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");
final var sortedAttributes = parsedObject.attributes().entrySet().stream().sorted(Map.Entry.comparingByKey()).toList();
for (final var e : sortedAttributes) {
if (!constructorArgs.namedArgs().containsKey(e.getKey())) {
final var p = e.getValue();
formatProperty(request, p, parsedObject, variableName, sb);
}
}
final var sortedProperties = parsedObject.properties().entrySet().stream().sorted(Comparator.comparing(e -> e.getKey().name())).toList();
for (final var e : sortedProperties) {
if (!constructorArgs.namedArgs().containsKey(e.getKey().name())) {
final var p = e.getKey();
final var o = e.getValue();
formatChild(request, parsedObject, p, o, variableName, sb);
}
}
}
}
}
}
private static String getGenericTypes(final GenerationRequest request, final ParsedObject parsedObject) throws GenerationException {
final var clazz = getClass(parsedObject.className());
if (isGeneric(clazz)) {
final var idProperty = parsedObject.attributes().get("fx:id");
if (idProperty == null) {
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 "";
}
/**
* 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) throws GenerationException {
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 GenerationException("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 constant object
*
* @param constant The constant
* @param variableName The variable name
* @param sb The string builder
*/
private static void formatConstant(final ParsedConstant constant, final String variableName, final StringBuilder sb) {
sb.append(" final var ").append(variableName).append(" = ").append(constant.className()).append(".").append(constant.constant()).append(";\n");
}
/**
* 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) throws GenerationException {
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 GenerationException("Invalid nested controller");
}
} else if (Objects.equals(property.sourceType(), EventHandler.class.getName())) {
injectControllerMethod(request, property, parentVariable, sb);
} else if (property.sourceType() != null) {
final var propertySourceTypeClass = getClass(property.sourceType());
if (hasStaticMethod(propertySourceTypeClass, setMethod)) {
final var method = getStaticMethod(propertySourceTypeClass, setMethod);
final var parameterType = method.getParameterTypes()[1];
final var arg = getArg(request, property.value(), parameterType);
setLaterIfNeeded(request, property, parameterType, " " + property.sourceType() + "." + setMethod + "(" + parentVariable + ", " + arg + ");\n", sb);
} else {
throw new GenerationException("Cannot set " + propertyName + " on " + property.sourceType());
}
} else {
final var getMethod = getGetMethod(propertyName);
final var parentClass = getClass(parent.className());
if (hasMethod(parentClass, setMethod)) {
final var method = getMethod(parentClass, setMethod);
final var parameterType = method.getParameterTypes()[0];
final var arg = getArg(request, property.value(), parameterType);
setLaterIfNeeded(request, property, parameterType, " " + parentVariable + "." + setMethod + "(" + arg + ");\n", sb);
} else if (hasMethod(parentClass, getMethod)) {
final var method = getMethod(parentClass, getMethod);
final var returnType = method.getReturnType();
if (hasMethod(returnType, "addAll")) {
final var arg = getArg(request, property.value(), String.class);
setLaterIfNeeded(request, property, String.class, " " + parentVariable + "." + getMethod + "().addAll(" + arg + ");\n", sb);
}
} else {
throw new GenerationException("Cannot set " + propertyName + " on " + parent.className());
}
}
}
private void setLaterIfNeeded(final GenerationRequest request, final ParsedProperty property, final Class<?> type, final String arg, final StringBuilder sb) throws GenerationException {
if (type == String.class && property.value().startsWith("%") && request.parameters().resourceBundleInjection().injectionType() == ResourceBundleInjectionTypes.GETTER
&& getControllerInjection(request).fieldInjectionType() == ControllerFieldInjectionTypes.FACTORY) {
controllerFactoryPostAction.add(arg);
} else {
sb.append(arg);
}
}
/**
* 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) throws GenerationException {
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 GenerationException("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) throws GenerationException {
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 GenerationException("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) throws GenerationException {
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) throws GenerationException {
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 GenerationException("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) throws GenerationException {
final var property = request.rootObject().attributes().get("fx:controller");
if (property == null) {
throw new GenerationException("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) throws GenerationException {
final var propertyName = property.name();
final var variables = new ArrayList<String>();
for (final var object : objects) {
final var vn = getNextVariableName("object");
format(request, object, vn, sb);
variables.add(vn);
}
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) throws GenerationException {
final var getMethod = getGetMethod(propertyName);
if (hasMethod(getClass(parent.className()), getMethod)) {
sb.append(" ").append(parentVariable).append(".").append(getMethod).append("().addAll(").append(String.join(", ", variables)).append(");\n");
} else {
throw new GenerationException("Cannot set " + propertyName + " on " + parent.className());
}
}
/**
* Formats a single child object
*
* @param variableName The child's variable name
* @param property The parent property
* @param parent The parent object
* @param parentVariable The parent object variable
* @param sb The string builder
*/
private static void formatSingleChild(final String variableName, final ParsedProperty property, final ParsedObject parent,
final String parentVariable, final StringBuilder sb) throws GenerationException {
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) throws GenerationException {
final var setMethod = getSetMethod(property);
final var getMethod = getGetMethod(property);
final var parentClass = getClass(parent.className());
if (hasMethod(parentClass, setMethod)) {
sb.append(" ").append(parentVariable).append(".").append(setMethod).append("(").append(variableName).append(");\n");
} else if (hasMethod(parentClass, 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 GenerationException("Cannot set " + property.name() + " on " + parent.className());
}
}
/**
* Formats a child object using a static method
*
* @param variableName The child's variable name
* @param property The parent property
* @param parentVariable The parent variable
* @param sb The string builder
*/
private static void formatSingleChildStatic(final String variableName, final ParsedProperty property, final String parentVariable, final StringBuilder sb) throws GenerationException {
final var setMethod = getSetMethod(property);
if (hasStaticMethod(getClass(property.sourceType()), setMethod)) {
sb.append(" ").append(property.sourceType()).append(".").append(setMethod).append("(").append(parentVariable).append(", ").append(variableName).append(");\n");
} else {
throw new GenerationException("Cannot set " + property.name() + " on " + property.sourceType());
}
}
/**
* 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);
}
private static String getBundleValue(final GenerationRequest request, final String value) throws GenerationException {
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 -> "controller.resources().getString(\"" + value + "\")";
};
} else {
throw new GenerationException("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) throws GenerationException {
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.attributes().get(entry.getKey());
if (p == null) {
final var c = parsedObject.properties().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 GenerationException("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 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) {
if (clazz == String.class) {
return "\"" + value.replace("\"", "\\\"") + "\"";
} else if (clazz == char.class || clazz == Character.class) {
return "'" + value + "'";
} else if (clazz == boolean.class || clazz == Boolean.class) {
return value;
} else if (clazz == byte.class || clazz == Byte.class || clazz == short.class || clazz == Short.class ||
clazz == int.class || clazz == Integer.class || clazz == long.class || clazz == Long.class) {
if (INT_PATTERN.matcher(value).matches()) {
return value;
} else {
return getWrapperClass(clazz) + ".valueOf(\"" + value + "\")";
}
} else if (clazz == float.class || clazz == Float.class || clazz == double.class || clazz == Double.class) {
if (DECIMAL_PATTERN.matcher(value).matches()) {
return value;
} else {
return getWrapperClass(clazz) + ".valueOf(\"" + value + "\")";
}
} else if (hasValueOf(clazz)) {
if (clazz.isEnum()) {
return clazz.getCanonicalName() + "." + value;
} else {
return clazz.getCanonicalName() + ".valueOf(\"" + value + "\")";
}
} else {
return value;
}
}
private static String getWrapperClass(final Class<?> clazz) {
final var name = clazz.getName();
if (name.contains(".") || Character.isUpperCase(name.charAt(0))) {
return name;
} else {
return name.substring(0, 1).toUpperCase() + name.substring(1);
}
}
/**
* 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 static Class<?> getClass(final String className) throws GenerationException {
try {
return Class.forName(className, false, Thread.currentThread().getContextClassLoader());
} catch (final ClassNotFoundException e) {
throw new GenerationException("Cannot find class " + className + " ; Is a dependency missing for the plugin?", e);
}
}
}

View File

@@ -1,4 +1,4 @@
package com.github.gtache.fxml.compiler.impl;
package com.github.gtache.fxml.compiler.impl.internal;
import java.lang.reflect.Constructor;
import java.util.Collections;
@@ -15,6 +15,14 @@ import static java.util.Objects.requireNonNull;
*/
record ConstructorArgs(Constructor<?> constructor,
SequencedMap<String, Parameter> namedArgs) {
/**
* Instantiates new args
*
* @param constructor The constructor
* @param namedArgs The named args
* @throws NullPointerException if any argument is null
*/
ConstructorArgs {
requireNonNull(constructor);
namedArgs = Collections.unmodifiableSequencedMap(new LinkedHashMap<>(namedArgs));

View File

@@ -0,0 +1,84 @@
package com.github.gtache.fxml.compiler.impl.internal;
import com.github.gtache.fxml.compiler.GenerationException;
import com.github.gtache.fxml.compiler.impl.GeneratorImpl;
import com.github.gtache.fxml.compiler.parsing.ParsedObject;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Set;
import static com.github.gtache.fxml.compiler.impl.internal.ReflectionHelper.getConstructorArgs;
/**
* Helper methods for {@link GeneratorImpl} to handle constructors
*/
final class ConstructorHelper {
private ConstructorHelper() {
}
/**
* Gets the constructor arguments as a list of strings
*
* @param constructorArgs The constructor arguments
* @param parsedObject The parsed object
* @return The list of constructor arguments
* @throws GenerationException if an error occurs
*/
static List<String> getListConstructorArgs(final ConstructorArgs constructorArgs, final ParsedObject parsedObject) throws GenerationException {
final var args = new ArrayList<String>(constructorArgs.namedArgs().size());
for (final var entry : constructorArgs.namedArgs().entrySet()) {
final var type = entry.getValue().type();
final var p = parsedObject.attributes().get(entry.getKey());
if (p == null) {
final var c = parsedObject.properties().entrySet().stream().filter(e ->
e.getKey().name().equals(entry.getKey())).findFirst().orElse(null);
if (c == null) {
args.add(ValueFormatter.toString(entry.getValue().defaultValue(), type));
} else {
throw new GenerationException("Constructor using complex property not supported yet");
}
} else {
args.add(ValueFormatter.toString(p.value(), type));
}
}
return args;
}
/**
* Gets the constructor arguments that best match the given 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
*/
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();
}
}

View File

@@ -0,0 +1,154 @@
package com.github.gtache.fxml.compiler.impl.internal;
import com.github.gtache.fxml.compiler.GenerationException;
import com.github.gtache.fxml.compiler.InjectionType;
import com.github.gtache.fxml.compiler.impl.ControllerFieldInjectionTypes;
import com.github.gtache.fxml.compiler.impl.ControllerMethodsInjectionType;
import com.github.gtache.fxml.compiler.impl.GeneratorImpl;
import com.github.gtache.fxml.compiler.parsing.ParsedProperty;
import static com.github.gtache.fxml.compiler.impl.internal.GenerationHelper.getControllerInjection;
import static com.github.gtache.fxml.compiler.impl.internal.GenerationHelper.getSetMethod;
/**
* Various methods to help {@link GeneratorImpl} for injecting controllers
*/
final class ControllerInjector {
private ControllerInjector() {
}
/**
* Injects the given variable into the controller
*
* @param progress The generation progress
* @param id The object id
* @param variable The object variable
* @throws GenerationException if an error occurs
*/
static void injectControllerField(final GenerationProgress progress, final String id, final String variable) throws GenerationException {
final var controllerInjection = getControllerInjection(progress);
final var controllerInjectionType = controllerInjection.fieldInjectionType();
if (controllerInjectionType instanceof final ControllerFieldInjectionTypes types) {
final var sb = progress.stringBuilder();
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 GenerationException("Unknown controller injection type : " + controllerInjectionType);
}
}
/**
* Injects an event handler controller method
*
* @param progress The generation progress
* @param property The property to inject
* @param parentVariable The parent variable
* @throws GenerationException if an error occurs
*/
static void injectEventHandlerControllerMethod(final GenerationProgress progress, final ParsedProperty property, final String parentVariable) throws GenerationException {
injectControllerMethod(progress, getEventHandlerMethodInjection(progress, property, parentVariable));
}
/**
* Injects a callback controller method
*
* @param progress The generation progress
* @param property The property to inject
* @param parentVariable The parent variable
* @param argumentClazz The argument class
* @throws GenerationException if an error occurs
*/
static void injectCallbackControllerMethod(final GenerationProgress progress, final ParsedProperty property, final String parentVariable, final String argumentClazz) throws GenerationException {
injectControllerMethod(progress, getCallbackMethodInjection(progress, property, parentVariable, argumentClazz));
}
/**
* Injects a controller method
*
* @param progress The generation progress
* @param methodInjection The method injection
* @throws GenerationException if an error occurs
*/
private static void injectControllerMethod(final GenerationProgress progress, final String methodInjection) throws GenerationException {
final var injection = getControllerInjection(progress);
if (injection.fieldInjectionType() instanceof final ControllerFieldInjectionTypes fieldTypes) {
switch (fieldTypes) {
case FACTORY -> progress.controllerFactoryPostAction().add(methodInjection);
case ASSIGN, SETTERS, REFLECTION -> progress.stringBuilder().append(methodInjection);
}
} else {
throw getUnknownInjectionException(injection.fieldInjectionType());
}
}
/**
* Computes the method injection for event handler
*
* @param progress The generation progress
* @param property The property
* @param parentVariable The parent variable
* @return The method injection
* @throws GenerationException if an error occurs
*/
private static String getEventHandlerMethodInjection(final GenerationProgress progress, final ParsedProperty property, final String parentVariable) throws GenerationException {
final var setMethod = getSetMethod(property.name());
final var injection = getControllerInjection(progress);
final var controllerMethod = property.value().replace("#", "");
if (injection.methodInjectionType() instanceof final ControllerMethodsInjectionType methodTypes) {
return switch (methodTypes) {
case REFERENCE -> {
final var hasArgument = progress.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 -> callEventHandlerMethod(\"" + controllerMethod + "\", e));\n";
};
} else {
throw getUnknownInjectionException(injection.methodInjectionType());
}
}
/**
* Computes the method injection for callback
*
* @param progress The generation progress
* @param property The property
* @param parentVariable The parent variable
* @param argumentClazz The argument class
* @return The method injection
* @throws GenerationException if an error occurs
*/
private static String getCallbackMethodInjection(final GenerationProgress progress, final ParsedProperty property, final String parentVariable, final String argumentClazz) throws GenerationException {
final var setMethod = getSetMethod(property.name());
final var injection = getControllerInjection(progress);
final var controllerMethod = property.value().replace("#", "");
if (injection.methodInjectionType() instanceof final ControllerMethodsInjectionType methodTypes) {
return switch (methodTypes) {
case REFERENCE ->
" " + parentVariable + "." + setMethod + "(controller::" + controllerMethod + ");\n";
case REFLECTION ->
" " + parentVariable + "." + setMethod + "(e -> callCallbackMethod(\"" + controllerMethod + "\", e, " + argumentClazz + "));\n";
};
} else {
throw getUnknownInjectionException(injection.methodInjectionType());
}
}
private static GenerationException getUnknownInjectionException(final InjectionType type) {
return new GenerationException("Unknown injection type : " + type);
}
}

View File

@@ -0,0 +1,94 @@
package com.github.gtache.fxml.compiler.impl.internal;
import com.github.gtache.fxml.compiler.GenerationException;
import com.github.gtache.fxml.compiler.impl.ControllerFieldInjectionTypes;
import com.github.gtache.fxml.compiler.impl.GeneratorImpl;
import com.github.gtache.fxml.compiler.parsing.ParsedProperty;
import static com.github.gtache.fxml.compiler.impl.internal.GenerationHelper.*;
/**
* Helper methods for {@link GeneratorImpl} to set fields
*/
final class FieldSetter {
private FieldSetter() {
}
/**
* Sets an event handler field
*
* @param progress The generation progress
* @param property The property to inject
* @param parentVariable The parent variable
* @throws GenerationException if an error occurs@
*/
static void setEventHandler(final GenerationProgress progress, final ParsedProperty property, final String parentVariable) throws GenerationException {
setField(progress, property, parentVariable, "javafx.event.EventHandler");
}
/**
* Sets a field
*
* @param progress The generation progress
* @param property The property to inject
* @param parentVariable The parent variable
* @param fieldType The field type
* @throws GenerationException if an error occurs
*/
static void setField(final GenerationProgress progress, final ParsedProperty property, final String parentVariable, final String fieldType) throws GenerationException {
final var injection = getControllerInjection(progress);
if (injection.fieldInjectionType() instanceof final ControllerFieldInjectionTypes fieldTypes) {
switch (fieldTypes) {
case ASSIGN -> setAssign(progress, property, parentVariable);
case FACTORY -> setFactory(progress, property, parentVariable);
case SETTERS -> setSetter(progress, property, parentVariable);
case REFLECTION -> setReflection(progress, property, parentVariable, fieldType);
}
} else {
throw new GenerationException("Unknown injection type : " + injection.fieldInjectionType());
}
}
private static void setAssign(final GenerationProgress progress, final ParsedProperty property, final String parentVariable) {
final var methodName = getSetMethod(property);
final var value = property.value().replace("$", "");
progress.stringBuilder().append(" ").append(parentVariable).append(".").append(methodName).append("(").append(value).append(");\n");
}
private static void setFactory(final GenerationProgress progress, final ParsedProperty property, final String parentVariable) {
progress.controllerFactoryPostAction().add(getSetString(property, parentVariable));
}
private static void setSetter(final GenerationProgress progress, final ParsedProperty property, final String parentVariable) {
progress.stringBuilder().append(getSetString(property, parentVariable));
}
private static String getSetString(final ParsedProperty property, final String parentVariable) {
final var methodName = getSetMethod(property);
final var value = property.value().replace("$", "");
final var split = value.split("\\.");
final var getterName = getGetMethod(split[1]);
return " " + parentVariable + "." + methodName + "(" + split[0] + "." + getterName + ");\n";
}
private static void setReflection(final GenerationProgress progress, final ParsedProperty property, final String parentVariable, final String fieldType) {
final var methodName = getSetMethod(property);
final var value = property.value().replace("$", "");
final var split = value.split("\\.");
final var fieldName = split[1];
progress.stringBuilder().append("""
try {
final var field = controller.getClass().getDeclaredField("%s");
field.setAccessible(true);
final var value = (%s) field.get(controller);
%s.%s(value);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new RuntimeException(e);
}
""".formatted(fieldName, fieldType, parentVariable, methodName));
}
}

View File

@@ -0,0 +1,138 @@
package com.github.gtache.fxml.compiler.impl.internal;
import com.github.gtache.fxml.compiler.GenerationException;
import com.github.gtache.fxml.compiler.impl.GeneratorImpl;
import com.github.gtache.fxml.compiler.parsing.ParsedObject;
import com.github.gtache.fxml.compiler.parsing.ParsedProperty;
import javafx.scene.text.FontPosture;
import javafx.scene.text.FontWeight;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import static com.github.gtache.fxml.compiler.impl.internal.GenerationHelper.*;
/**
* Helper methods for {@link GeneratorImpl} to format fonts
*/
final class FontFormatter {
private FontFormatter() {
}
static void formatFont(final GenerationProgress progress, final ParsedObject parsedObject, final String variableName) throws GenerationException {
if (parsedObject.children().isEmpty() && parsedObject.properties().isEmpty()) {
final var value = parseFontValue(parsedObject);
final var url = value.url();
final var fw = value.fontWeight();
final var fp = value.fontPosture();
final var size = value.size();
final var name = value.name();
if (url != null) {
formatURL(progress, url, size, variableName);
} else if (fw == null && fp == null) {
formatNoStyle(progress, name, size, variableName);
} else {
formatStyle(progress, fw, fp, size, name, variableName);
}
handleId(progress, parsedObject, variableName);
} else {
throw new GenerationException("Font cannot have children or properties : " + parsedObject);
}
}
private static void formatURL(final GenerationProgress progress, final URL url, final double size, final String variableName) {
final var urlVariableName = URLBuilder.formatURL(progress, url.toString());
progress.stringBuilder().append("""
Font %1$s;
try (final var in = %2$s.openStream()) {
%1$s = Font.loadFont(in, %3$s);
} catch (final IOException e) {
throw new RuntimeException(e);
}
""".formatted(variableName, urlVariableName, size));
}
private static void formatNoStyle(final GenerationProgress progress, final String name, final double size, final String variableName) {
progress.stringBuilder().append(START_VAR).append(variableName).append(" = new javafx.scene.text.Font(\"").append(name).append("\", ").append(size).append(");\n");
}
private static void formatStyle(final GenerationProgress progress, final FontWeight fw, final FontPosture fp, final double size, final String name, final String variableName) {
final var finalFW = fw == null ? FontWeight.NORMAL : fw;
final var finalFP = fp == null ? FontPosture.REGULAR : fp;
progress.stringBuilder().append(START_VAR).append(variableName).append(" = new javafx.scene.text.Font(\"").append(name)
.append("\", javafx.scene.text.FontWeight.").append(finalFW.name()).append(", javafx.scene.text.FontPosture.")
.append(finalFP.name()).append(", ").append(size).append(");\n");
}
private static FontValue parseFontValue(final ParsedObject parsedObject) throws GenerationException {
URL url = null;
String name = null;
double size = 12;
FontWeight fw = null;
FontPosture fp = null;
final var sortedAttributes = getSortedAttributes(parsedObject);
for (final var property : sortedAttributes) {
switch (property.name()) {
case FX_ID -> {
//Do nothing
}
case "name" -> {
try {
url = new URI(property.value()).toURL();
} catch (final MalformedURLException | URISyntaxException | IllegalArgumentException ignored) {
name = property.value();
}
}
case "size" -> size = Double.parseDouble(property.value());
case "style" -> {
final var style = getFontStyle(property);
if (style.fontWeight() != null) {
fw = style.fontWeight();
}
if (style.fontPosture() != null) {
fp = style.fontPosture();
}
}
case "url" -> url = getURL(property);
default -> throw new GenerationException("Unknown font attribute : " + property.name());
}
}
return new FontValue(url, name, size, fw, fp);
}
private static URL getURL(final ParsedProperty property) throws GenerationException {
try {
return new URI(property.value()).toURL();
} catch (final MalformedURLException | URISyntaxException e) {
throw new GenerationException("Couldn't parse url : " + property.value(), e);
}
}
private static FontStyle getFontStyle(final ParsedProperty property) {
final var split = property.value().split(" ");
FontWeight fw = null;
FontPosture fp = null;
for (final var s : split) {
final var fontWeight = FontWeight.findByName(s);
final var fontPosture = FontPosture.findByName(s);
if (fontWeight != null) {
fw = fontWeight;
} else if (fontPosture != null) {
fp = fontPosture;
}
}
return new FontStyle(fw, fp);
}
private record FontValue(URL url, String name, double size, FontWeight fontWeight,
FontPosture fontPosture) {
}
private record FontStyle(FontWeight fontWeight, FontPosture fontPosture) {
}
}

View File

@@ -0,0 +1,124 @@
package com.github.gtache.fxml.compiler.impl.internal;
import com.github.gtache.fxml.compiler.ControllerInjection;
import com.github.gtache.fxml.compiler.GenerationException;
import com.github.gtache.fxml.compiler.impl.GeneratorImpl;
import com.github.gtache.fxml.compiler.parsing.ParsedObject;
import com.github.gtache.fxml.compiler.parsing.ParsedProperty;
import java.util.List;
import java.util.Map;
import static com.github.gtache.fxml.compiler.impl.internal.ControllerInjector.injectControllerField;
/**
* Various helper methods for {@link GeneratorImpl}
*/
public final class GenerationHelper {
static final String FX_ID = "fx:id";
static final String FX_VALUE = "fx:value";
static final String VALUE = "value";
static final String START_VAR = " final var ";
private GenerationHelper() {
}
/**
* Returns the variable prefix for the given object
*
* @param object The object
* @return The variable prefix
*/
public static String getVariablePrefix(final ParsedObject object) {
final var className = object.className();
return className.substring(className.lastIndexOf('.') + 1).toLowerCase();
}
/**
* Gets the controller injection object from the generation request
*
* @param progress The generation progress
* @return The controller injection
* @throws GenerationException If the controller is not found
*/
public static ControllerInjection getControllerInjection(final GenerationProgress progress) throws GenerationException {
final var request = progress.request();
final var property = request.rootObject().attributes().get("fx:controller");
if (property == null) {
throw new GenerationException("Root object must have a controller property");
} else {
final var id = property.value();
return request.parameters().controllerInjections().get(id);
}
}
/**
* Returns the getter method name for the given property
*
* @param property The property
* @return The getter method name
*/
static String getGetMethod(final ParsedProperty property) {
return getGetMethod(property.name());
}
/**
* Returns the getter method name for the given property name
*
* @param propertyName The property name
* @return The getter method name
*/
static String getGetMethod(final String propertyName) {
return "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
*/
static String getSetMethod(final ParsedProperty property) {
return getSetMethod(property.name());
}
/**
* Returns the setter method name for the given property name
*
* @param propertyName The property name
* @return The setter method name
*/
static String getSetMethod(final String propertyName) {
return "set" + propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1);
}
/**
* Handles the fx:id attribute of an object
*
* @param progress The generation progress
* @param parsedObject The parsed object
* @param variableName The variable name
* @throws GenerationException if an error occurs
*/
static void handleId(final GenerationProgress progress, final ParsedObject parsedObject, final String variableName) throws GenerationException {
final var id = parsedObject.attributes().get(FX_ID);
if (id != null) {
progress.idToVariableName().put(id.value(), variableName);
progress.idToObject().put(id.value(), parsedObject);
//TODO Don't inject if variable doesn't exist
injectControllerField(progress, id.value(), variableName);
}
}
/**
* Returns the sorted attributes of the given object
*
* @param parsedObject The parsed object
* @return The sorted attributes
*/
static List<ParsedProperty> getSortedAttributes(final ParsedObject parsedObject) {
return parsedObject.attributes().entrySet().stream().sorted(Map.Entry.comparingByKey()).map(Map.Entry::getValue).toList();
}
}

View File

@@ -0,0 +1,70 @@
package com.github.gtache.fxml.compiler.impl.internal;
import com.github.gtache.fxml.compiler.GenerationRequest;
import com.github.gtache.fxml.compiler.impl.GeneratorImpl;
import com.github.gtache.fxml.compiler.parsing.ParsedObject;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.SequencedCollection;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Used by {@link GeneratorImpl} to track the generation progress
*
* @param request The generation request
* @param idToVariableName The id to variable name mapping
* @param idToObject The id to parsed object mapping
* @param variableNameCounters The variable name counters for variable name generation
* @param controllerFactoryPostAction The controller factory post action for factory injection
* @param stringBuilder The string builder
*/
public record GenerationProgress(GenerationRequest request, Map<String, String> idToVariableName,
Map<String, ParsedObject> idToObject,
Map<String, AtomicInteger> variableNameCounters,
SequencedCollection<String> controllerFactoryPostAction,
StringBuilder stringBuilder) {
/**
* Instantiates a new GenerationProgress
*
* @param request The generation request
* @param idToVariableName The id to variable name mapping
* @param idToObject The id to parsed object mapping
* @param variableNameCounters The variable name counters
* @param controllerFactoryPostAction The controller factory post action
* @param stringBuilder The string builder
* @throws NullPointerException if any parameter is null
*/
public GenerationProgress {
Objects.requireNonNull(request);
Objects.requireNonNull(idToVariableName);
Objects.requireNonNull(idToObject);
Objects.requireNonNull(variableNameCounters);
Objects.requireNonNull(controllerFactoryPostAction);
Objects.requireNonNull(stringBuilder);
}
/**
* Instantiates a new GenerationProgress
*
* @param request The generation request
* @throws NullPointerException if request is null
*/
public GenerationProgress(final GenerationRequest request) {
this(request, new HashMap<>(), new HashMap<>(), new HashMap<>(), new ArrayList<>(), new StringBuilder());
}
/**
* Gets the next available variable name for the given prefix
*
* @param prefix The variable name prefix
* @return The next available variable name
*/
public String getNextVariableName(final String prefix) {
final var counter = variableNameCounters.computeIfAbsent(prefix, k -> new AtomicInteger(0));
return prefix + counter.getAndIncrement();
}
}

View File

@@ -0,0 +1,106 @@
package com.github.gtache.fxml.compiler.impl.internal;
import com.github.gtache.fxml.compiler.GenerationException;
import com.github.gtache.fxml.compiler.impl.ControllerFieldInjectionTypes;
import com.github.gtache.fxml.compiler.impl.ControllerMethodsInjectionType;
import static com.github.gtache.fxml.compiler.impl.internal.GenerationHelper.getControllerInjection;
/**
* Provides the helper methods for the generated code
*/
public final class HelperMethodsProvider {
private HelperMethodsProvider() {
}
/**
* Gets helper methods string for the given generation progress
*
* @param progress The generation progress
* @return The helper methods
* @throws GenerationException if an error occurs
*/
public static String getHelperMethods(final GenerationProgress progress) throws GenerationException {
final var injection = getControllerInjection(progress);
final var methodInjectionType = injection.methodInjectionType();
final var sb = new StringBuilder();
if (methodInjectionType == ControllerMethodsInjectionType.REFLECTION) {
sb.append("""
private <T extends javafx.event.Event> void callEventHandlerMethod(final String methodName, final T event) {
try {
final java.lang.reflect.Method method;
final var methods = java.util.Arrays.stream(controller.getClass().getDeclaredMethods())
.filter(m -> m.getName().equals(methodName)).toList();
if (methods.size() > 1) {
final var eventMethods = methods.stream().filter(m ->
m.getParameterCount() == 1 && javafx.event.Event.class.isAssignableFrom(m.getParameterTypes()[0])).toList();
if (eventMethods.size() == 1) {
method = eventMethods.getFirst();
} else {
final var emptyMethods = methods.stream().filter(m -> m.getParameterCount() == 0).toList();
if (emptyMethods.size() == 1) {
method = emptyMethods.getFirst();
} else {
throw new IllegalArgumentException("Multiple matching methods for " + methodName);
}
}
} else if (methods.size() == 1) {
method = methods.getFirst();
} else {
throw new IllegalArgumentException("No matching method for " + methodName);
}
method.setAccessible(true);
if (method.getParameterCount() == 0) {
method.invoke(controller);
} else {
method.invoke(controller, event);
}
} catch (final IllegalAccessException | java.lang.reflect.InvocationTargetException ex) {
throw new RuntimeException("Error using reflection on " + methodName, ex);
}
}
private <T, U> U callCallbackMethod(final String methodName, final T value, final Class<T> clazz) {
try {
final java.lang.reflect.Method method;
final var methods = java.util.Arrays.stream(controller.getClass().getDeclaredMethods())
.filter(m -> m.getName().equals(methodName)).toList();
if (methods.size() > 1) {
final var eventMethods = methods.stream().filter(m ->
m.getParameterCount() == 1 && clazz.isAssignableFrom(m.getParameterTypes()[0])).toList();
if (eventMethods.size() == 1) {
method = eventMethods.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);
return (U) method.invoke(controller, value);
} catch (final IllegalAccessException | java.lang.reflect.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();
}
}

View File

@@ -0,0 +1,52 @@
package com.github.gtache.fxml.compiler.impl.internal;
import com.github.gtache.fxml.compiler.GenerationException;
import com.github.gtache.fxml.compiler.impl.GeneratorImpl;
import com.github.gtache.fxml.compiler.parsing.ParsedObject;
import static com.github.gtache.fxml.compiler.impl.internal.GenerationHelper.*;
import static com.github.gtache.fxml.compiler.impl.internal.URLBuilder.formatURL;
/**
* Helper methods for {@link GeneratorImpl} to format Images
*/
final class ImageBuilder {
private ImageBuilder() {
}
static void formatImage(final GenerationProgress progress, final ParsedObject parsedObject, final String variableName) throws GenerationException {
if (parsedObject.children().isEmpty() && parsedObject.properties().isEmpty()) {
final var sortedAttributes = getSortedAttributes(parsedObject);
String url = null;
var requestedWidth = 0.0;
var requestedHeight = 0.0;
var preserveRatio = false;
var smooth = false;
var backgroundLoading = false;
for (final var property : sortedAttributes) {
switch (property.name()) {
case FX_ID -> {
//Do nothing
}
case "url" -> url = formatURL(progress, property.value());
case "requestedWidth" -> requestedWidth = Double.parseDouble(property.value());
case "requestedHeight" -> requestedHeight = Double.parseDouble(property.value());
case "preserveRatio" -> preserveRatio = Boolean.parseBoolean(property.value());
case "smooth" -> smooth = Boolean.parseBoolean(property.value());
case "backgroundLoading" -> backgroundLoading = Boolean.parseBoolean(property.value());
default -> throw new GenerationException("Unknown image attribute : " + property.name());
}
}
final var urlString = progress.getNextVariableName("urlStr");
progress.stringBuilder().append(START_VAR).append(urlString).append(" = ").append(url).append(".toString();\n");
progress.stringBuilder().append(START_VAR).append(variableName).append(" = new javafx.scene.image.Image(").append(urlString)
.append(", ").append(requestedWidth).append(", ").append(requestedHeight).append(", ")
.append(preserveRatio).append(", ").append(smooth).append(", ").append(backgroundLoading).append(");\n");
handleId(progress, parsedObject, variableName);
} else {
throw new GenerationException("Image cannot have children or properties : " + parsedObject);
}
}
}

View File

@@ -0,0 +1,511 @@
package com.github.gtache.fxml.compiler.impl.internal;
import com.github.gtache.fxml.compiler.GenerationException;
import com.github.gtache.fxml.compiler.impl.GeneratorImpl;
import com.github.gtache.fxml.compiler.parsing.*;
import com.github.gtache.fxml.compiler.parsing.impl.ParsedPropertyImpl;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashSet;
import java.util.SequencedCollection;
import java.util.Set;
import java.util.stream.Collectors;
import static com.github.gtache.fxml.compiler.impl.internal.ConstructorHelper.getListConstructorArgs;
import static com.github.gtache.fxml.compiler.impl.internal.ConstructorHelper.getMatchingConstructorArgs;
import static com.github.gtache.fxml.compiler.impl.internal.ControllerInjector.injectControllerField;
import static com.github.gtache.fxml.compiler.impl.internal.FontFormatter.formatFont;
import static com.github.gtache.fxml.compiler.impl.internal.GenerationHelper.*;
import static com.github.gtache.fxml.compiler.impl.internal.ImageBuilder.formatImage;
import static com.github.gtache.fxml.compiler.impl.internal.PropertyFormatter.formatProperty;
import static com.github.gtache.fxml.compiler.impl.internal.ReflectionHelper.*;
import static com.github.gtache.fxml.compiler.impl.internal.SceneBuilder.formatScene;
import static com.github.gtache.fxml.compiler.impl.internal.TriangleMeshBuilder.formatTriangleMesh;
import static com.github.gtache.fxml.compiler.impl.internal.URLBuilder.formatURL;
import static com.github.gtache.fxml.compiler.impl.internal.WebViewBuilder.formatWebView;
/**
* Helper methods for {@link GeneratorImpl} to format properties
*/
public final class ObjectFormatter {
private static final String NEW_ASSIGN = " = new ";
private static final Set<String> BUILDER_CLASSES = Set.of(
"javafx.scene.Scene",
"javafx.scene.text.Font",
"javafx.scene.image.Image",
"java.net.URL",
"javafx.scene.shape.TriangleMesh",
"javafx.scene.web.WebView"
);
private static final Set<String> SIMPLE_CLASSES = Set.of(
"java.lang.String",
"java.lang.Integer",
"java.lang.Byte",
"java.lang.Short",
"java.lang.Long",
"java.lang.Float",
"java.lang.Double"
);
private ObjectFormatter() {
}
/**
* Formats an object
*
* @param progress The generation progress
* @param parsedObject The parsed object to format
* @param variableName The variable name for the object
* @throws GenerationException if an error occurs
*/
public static void format(final GenerationProgress progress, final ParsedObject parsedObject, final String variableName) throws GenerationException {
switch (parsedObject) {
case final ParsedConstant constant -> formatConstant(progress, constant, variableName);
case final ParsedCopy copy -> formatCopy(progress, copy, variableName);
case final ParsedDefine define -> formatDefine(progress, define, variableName);
case final ParsedFactory factory -> formatFactory(progress, factory, variableName);
case final ParsedInclude include -> formatInclude(progress, include, variableName);
case final ParsedReference reference -> formatReference(progress, reference, variableName);
case final ParsedValue value -> formatValue(progress, value, variableName);
case final ParsedText text -> formatText(progress, text, variableName);
default -> formatObject(progress, parsedObject, variableName);
}
}
/**
* Formats a simple text
*
* @param progress The generation progress
* @param text The parsed text
* @param variableName The variable name
*/
private static void formatText(final GenerationProgress progress, final ParsedText text, final String variableName) {
progress.stringBuilder().append(START_VAR).append(variableName).append(" = \"").append(text.text()).append("\";\n");
}
/**
* Formats a basic object
*
* @param progress The generation progress
* @param parsedObject The parsed object to format
* @param variableName The variable name for the object
*/
private static void formatObject(final GenerationProgress progress, final ParsedObject parsedObject, final String variableName) throws GenerationException {
if (BUILDER_CLASSES.contains(parsedObject.className())) {
formatBuilderObject(progress, parsedObject, variableName);
} else {
formatNotBuilder(progress, parsedObject, variableName);
}
}
/**
* Formats a builder object
*
* @param progress The generation progress
* @param parsedObject The parsed object
* @param variableName The variable name
*/
private static void formatBuilderObject(final GenerationProgress progress, final ParsedObject parsedObject, final String variableName) throws GenerationException {
final var className = parsedObject.className();
switch (className) {
case "javafx.scene.Scene" -> formatScene(progress, parsedObject, variableName);
case "javafx.scene.text.Font" -> formatFont(progress, parsedObject, variableName);
case "javafx.scene.image.Image" -> formatImage(progress, parsedObject, variableName);
case "java.net.URL" -> formatURL(progress, parsedObject, variableName);
case "javafx.scene.shape.TriangleMesh" -> formatTriangleMesh(progress, parsedObject, variableName);
case "javafx.scene.web.WebView" -> formatWebView(progress, parsedObject, variableName);
default -> throw new IllegalArgumentException("Unknown builder class : " + className);
}
}
private static void formatNotBuilder(final GenerationProgress progress, final ParsedObject parsedObject, final String variableName) throws GenerationException {
if (isSimpleClass(parsedObject)) {
formatSimpleClass(progress, parsedObject, variableName);
} else {
formatComplexClass(progress, parsedObject, variableName);
}
}
private static void formatSimpleClass(final GenerationProgress progress, final ParsedObject parsedObject, final String variableName) throws GenerationException {
if (!parsedObject.properties().isEmpty()) {
throw new GenerationException("Simple class cannot have properties : " + parsedObject);
}
if (parsedObject.attributes().keySet().stream().anyMatch(k -> !k.equals(FX_ID) && !k.equals(VALUE) && !k.equals(FX_VALUE))) {
throw new GenerationException("Invalid attributes for simple class : " + parsedObject);
}
final var value = getSimpleValue(progress, parsedObject);
final var valueStr = ValueFormatter.toString(value, ReflectionHelper.getClass(parsedObject.className()));
progress.stringBuilder().append(START_VAR).append(variableName).append(" = ").append(valueStr).append(";\n");
handleId(progress, parsedObject, variableName);
}
private static String getSimpleValue(final GenerationProgress progress, final ParsedObject parsedObject) throws GenerationException {
final var definedChildren = parsedObject.children().stream().filter(ParsedDefine.class::isInstance).toList();
for (final var definedChild : definedChildren) {
formatObject(progress, definedChild, progress.getNextVariableName("define"));
}
final var notDefinedChildren = parsedObject.children().stream().filter(c -> !(c instanceof ParsedDefine)).toList();
if (parsedObject.attributes().containsKey(FX_VALUE)) {
return getSimpleFXValue(parsedObject, notDefinedChildren);
} else if (parsedObject.attributes().containsKey(VALUE)) {
return getSimpleValue(parsedObject, notDefinedChildren);
} else {
return getSimpleChild(parsedObject, notDefinedChildren);
}
}
private static String getSimpleFXValue(final ParsedObject parsedObject, final Collection<ParsedObject> notDefinedChildren) throws GenerationException {
if (notDefinedChildren.isEmpty() && !parsedObject.attributes().containsKey(VALUE)) {
return parsedObject.attributes().get(FX_VALUE).value();
} else {
throw new GenerationException("Malformed simple class : " + parsedObject);
}
}
private static String getSimpleValue(final ParsedObject parsedObject, final Collection<ParsedObject> notDefinedChildren) throws GenerationException {
if (notDefinedChildren.isEmpty()) {
return parsedObject.attributes().get(VALUE).value();
} else {
throw new GenerationException("Malformed simple class : " + parsedObject);
}
}
private static String getSimpleChild(final ParsedObject parsedObject, final SequencedCollection<ParsedObject> notDefinedChildren) throws GenerationException {
if (notDefinedChildren.size() == 1) {
final var child = notDefinedChildren.getFirst();
if (child instanceof final ParsedText text) {
return text.text();
} else {
throw new GenerationException("Invalid value for : " + parsedObject);
}
} else {
throw new GenerationException("Value not found for : " + parsedObject);
}
}
private static boolean isSimpleClass(final ParsedObject object) throws GenerationException {
final var className = object.className();
if (SIMPLE_CLASSES.contains(className)) {
return true;
} else {
final var clazz = ReflectionHelper.getClass(className);
return clazz.isEnum();
}
}
private static void formatComplexClass(final GenerationProgress progress, final ParsedObject parsedObject, final String variableName) throws GenerationException {
final var clazz = ReflectionHelper.getClass(parsedObject.className());
final var children = parsedObject.children();
final var definedChildren = children.stream().filter(ParsedDefine.class::isInstance).toList();
final var notDefinedChildren = children.stream().filter(c -> !(c instanceof ParsedDefine)).toList();
final var constructors = clazz.getConstructors();
final var allPropertyNames = new HashSet<>(parsedObject.attributes().keySet());
allPropertyNames.addAll(parsedObject.properties().keySet().stream().map(ParsedProperty::name).collect(Collectors.toSet()));
if (!definedChildren.isEmpty()) {
for (final var definedChild : definedChildren) {
format(progress, definedChild, progress.getNextVariableName("define"));
}
}
if (!notDefinedChildren.isEmpty()) {
final var defaultProperty = getDefaultProperty(parsedObject.className());
if (defaultProperty != null) {
allPropertyNames.add(defaultProperty);
}
}
final var constructorArgs = getMatchingConstructorArgs(constructors, allPropertyNames);
if (constructorArgs == null) {
formatNoConstructor(progress, parsedObject, variableName, allPropertyNames);
} else {
formatConstructor(progress, parsedObject, variableName, constructorArgs);
}
}
private static void formatNoConstructor(final GenerationProgress progress, final ParsedObject parsedObject, final String variableName, final Collection<String> allPropertyNames) throws GenerationException {
final var clazz = ReflectionHelper.getClass(parsedObject.className());
if (allPropertyNames.size() == 1 && allPropertyNames.iterator().next().equals("fx:constant")) {
final var property = parsedObject.attributes().get("fx:constant");
progress.stringBuilder().append(START_VAR).append(variableName).append(" = ").append(clazz.getCanonicalName()).append(".").append(property.value()).append(";\n");
} else {
throw new GenerationException("Cannot find constructor for " + clazz.getCanonicalName());
}
}
private static void formatConstructor(final GenerationProgress progress, final ParsedObject parsedObject, final String variableName, final ConstructorArgs constructorArgs) throws GenerationException {
final var args = getListConstructorArgs(constructorArgs, parsedObject);
final var genericTypes = getGenericTypes(progress, parsedObject);
progress.stringBuilder().append(START_VAR).append(variableName).append(NEW_ASSIGN).append(parsedObject.className())
.append(genericTypes).append("(").append(String.join(", ", args)).append(");\n");
final var sortedAttributes = getSortedAttributes(parsedObject);
for (final var value : sortedAttributes) {
if (!constructorArgs.namedArgs().containsKey(value.name())) {
formatProperty(progress, value, parsedObject, variableName);
}
}
final var sortedProperties = parsedObject.properties().entrySet().stream().sorted(Comparator.comparing(e -> e.getKey().name())).toList();
for (final var e : sortedProperties) {
if (!constructorArgs.namedArgs().containsKey(e.getKey().name())) {
final var p = e.getKey();
final var o = e.getValue();
formatChild(progress, parsedObject, p, o, variableName);
}
}
final var notDefinedChildren = parsedObject.children().stream().filter(c -> !(c instanceof ParsedDefine)).toList();
if (!notDefinedChildren.isEmpty()) {
final var defaultProperty = getDefaultProperty(parsedObject.className());
if (!constructorArgs.namedArgs().containsKey(defaultProperty)) {
final var property = new ParsedPropertyImpl(defaultProperty, null, null);
formatChild(progress, parsedObject, property, notDefinedChildren, variableName);
}
}
}
/**
* Formats an include object
*
* @param progress The generation progress
* @param include The include object
* @param subNodeName The sub node name
*/
private static void formatInclude(final GenerationProgress progress, final ParsedInclude include, final String subNodeName) throws GenerationException {
final var subViewVariable = progress.getNextVariableName("view");
final var source = include.source();
final var resources = include.resources();
final var request = progress.request();
final var subControllerClass = request.parameters().sourceToControllerName().get(source);
final var subClassName = request.parameters().sourceToGeneratedClassName().get(source);
if (subClassName == null) {
throw new GenerationException("Unknown include source : " + source);
}
final var sb = progress.stringBuilder();
if (resources == null) {
sb.append(START_VAR).append(subViewVariable).append(NEW_ASSIGN).append(subClassName).append("(controllersMap, resourceBundlesMap);\n");
} else {
final var subResourceBundlesMapVariable = progress.getNextVariableName("map");
final var subBundleVariable = progress.getNextVariableName("bundle");
sb.append(START_VAR).append(subResourceBundlesMapVariable).append(" = new HashMap<>(resourceBundlesMap);\n");
sb.append(START_VAR).append(subBundleVariable).append(" = java.util.ResourceBundle.getBundle(\"").append(resources).append("\");\n");
sb.append(" ").append(subResourceBundlesMapVariable).append(".put(").append(subControllerClass).append(", ").append(subBundleVariable).append(");\n");
sb.append(START_VAR).append(subViewVariable).append(NEW_ASSIGN).append(subClassName).append("(controllersMap, ").append(subResourceBundlesMapVariable).append(");\n");
}
sb.append(" final javafx.scene.Parent ").append(subNodeName).append(" = ").append(subViewVariable).append(".load();\n");
injectSubController(progress, include, subViewVariable);
}
private static void injectSubController(final GenerationProgress progress, final ParsedInclude include, final String subViewVariable) throws GenerationException {
final var id = include.controllerId();
if (id != null) {
final var subControllerVariable = progress.getNextVariableName("controller");
progress.stringBuilder().append(START_VAR).append(subControllerVariable).append(" = ").append(subViewVariable).append(".controller();\n");
progress.idToVariableName().put(id, subControllerVariable);
progress.idToObject().put(id, include);
//TODO Don't inject if variable doesn't exist
injectControllerField(progress, id, subControllerVariable);
}
}
/**
* Formats a fx:define
*
* @param progress The generation progress
* @param define The parsed define
* @param variableName The variable name
* @throws GenerationException if an error occurs
*/
private static void formatDefine(final GenerationProgress progress, final ParsedDefine define, final String variableName) throws GenerationException {
formatObject(progress, define.object(), variableName);
}
/**
* Formats a fx:reference
*
* @param progress The generation progress
* @param reference The parsed reference
* @throws GenerationException if an error occurs
*/
private static void formatReference(final GenerationProgress progress, final ParsedReference reference, final String variableName) throws GenerationException {
final var id = reference.source();
final var variable = progress.idToVariableName().get(id);
if (variable == null) {
throw new GenerationException("Unknown id : " + id);
}
progress.stringBuilder().append(START_VAR).append(variableName).append(" = ").append(variable).append(";\n");
}
/**
* Formats a fx:copy
*
* @param progress The generation progress
* @param copy The parsed copy
* @param variableName The variable name
* @throws GenerationException if an error occurs
*/
private static void formatCopy(final GenerationProgress progress, final ParsedCopy copy, final String variableName) throws GenerationException {
final var id = copy.source();
final var variable = progress.idToVariableName().get(id);
final var object = progress.idToObject().get(id);
if (variable == null || object == null) {
throw new GenerationException("Unknown id : " + id);
}
progress.stringBuilder().append(START_VAR).append(variableName).append(NEW_ASSIGN).append(object.className()).append("(").append(variable).append(");\n");
}
/**
* Formats a constant object
*
* @param progress The generation progress
* @param constant The constant
* @param variableName The variable name
*/
private static void formatConstant(final GenerationProgress progress, final ParsedConstant constant, final String variableName) {
progress.stringBuilder().append(START_VAR).append(variableName).append(" = ").append(constant.className()).append(".").append(constant.constant()).append(";\n");
}
/**
* Formats a value object
*
* @param progress The generation progress
* @param value The value
* @param variableName The variable name
*/
private static void formatValue(final GenerationProgress progress, final ParsedValue value, final String variableName) {
progress.stringBuilder().append(START_VAR).append(variableName).append(" = ").append(value.className()).append(".valueOf(\"").append(value.value()).append("\");\n");
}
/**
* Formats a factory object
*
* @param progress The generation progress
* @param factory The factory
* @param variableName The variable name
*/
private static void formatFactory(final GenerationProgress progress, final ParsedFactory factory, final String variableName) throws GenerationException {
final var variables = new ArrayList<String>();
for (final var argument : factory.arguments()) {
final var argumentVariable = progress.getNextVariableName("arg");
variables.add(argumentVariable);
format(progress, argument, argumentVariable);
}
progress.stringBuilder().append(START_VAR).append(variableName).append(" = ").append(factory.className())
.append(".").append(factory.factory()).append("(").append(String.join(", ", variables)).append(");\n");
}
/**
* Formats the children objects of a property
*
* @param progress The generation progress
* @param parent The parent object
* @param property The parent property
* @param objects The child objects
* @param parentVariable The parent object variable
*/
private static void formatChild(final GenerationProgress progress, final ParsedObject parent, final ParsedProperty property,
final Iterable<? extends ParsedObject> objects, final String parentVariable) throws GenerationException {
final var propertyName = property.name();
final var variables = new ArrayList<String>();
for (final var object : objects) {
final var vn = progress.getNextVariableName(getVariablePrefix(object));
format(progress, object, vn);
if (!(object instanceof ParsedDefine)) {
variables.add(vn);
}
}
if (variables.size() > 1) {
formatMultipleChildren(progress, variables, propertyName, parent, parentVariable);
} else if (variables.size() == 1) {
final var vn = variables.getFirst();
formatSingleChild(progress, vn, property, parent, parentVariable);
}
}
/**
* Formats children objects given that they are more than one
*
* @param progress The generation progress
* @param variables The children variables
* @param propertyName The property name
* @param parent The parent object
* @param parentVariable The parent object variable
*/
private static void formatMultipleChildren(final GenerationProgress progress, final Iterable<String> variables, final String propertyName, final ParsedObject parent,
final String parentVariable) throws GenerationException {
final var getMethod = getGetMethod(propertyName);
if (hasMethod(ReflectionHelper.getClass(parent.className()), getMethod)) {
progress.stringBuilder().append(" ").append(parentVariable).append(".").append(getMethod).append("().addAll(java.util.List.of(").append(String.join(", ", variables)).append("));\n");
} else {
throw getCannotSetException(propertyName, parent.className());
}
}
/**
* Formats a single child object
*
* @param progress The generation progress
* @param variableName The child's variable name
* @param property The parent property
* @param parent The parent object
* @param parentVariable The parent object variable
*/
private static void formatSingleChild(final GenerationProgress progress, final String variableName, final ParsedProperty property, final ParsedObject parent,
final String parentVariable) throws GenerationException {
if (property.sourceType() == null) {
formatSingleChildInstance(progress, variableName, property, parent, parentVariable);
} else {
formatSingleChildStatic(progress, variableName, property, parentVariable);
}
}
/**
* Formats a single child object using an instance method on the parent object
*
* @param progress The generation progress
* @param variableName The child's variable name
* @param property The parent property
* @param parent The parent object
* @param parentVariable The parent object variable
*/
private static void formatSingleChildInstance(final GenerationProgress progress, final String variableName,
final ParsedProperty property, final ParsedObject parent,
final String parentVariable) throws GenerationException {
final var setMethod = getSetMethod(property);
final var getMethod = getGetMethod(property);
final var parentClass = ReflectionHelper.getClass(parent.className());
final var sb = progress.stringBuilder();
if (hasMethod(parentClass, setMethod)) {
sb.append(" ").append(parentVariable).append(".").append(setMethod).append("(").append(variableName).append(");\n");
} else if (hasMethod(parentClass, getMethod)) {
//Probably a list method that has only one element
sb.append(" ").append(parentVariable).append(".").append(getMethod).append("().addAll(java.util.List.of(").append(variableName).append("));\n");
} else {
throw getCannotSetException(property.name(), parent.className());
}
}
/**
* Formats a child object using a static method
*
* @param progress The generation progress
* @param variableName The child's variable name
* @param property The parent property
* @param parentVariable The parent variable
*/
private static void formatSingleChildStatic(final GenerationProgress progress, final String variableName,
final ParsedProperty property, final String parentVariable) throws GenerationException {
final var setMethod = getSetMethod(property);
if (hasStaticMethod(ReflectionHelper.getClass(property.sourceType()), setMethod)) {
progress.stringBuilder().append(" ").append(property.sourceType()).append(".").append(setMethod)
.append("(").append(parentVariable).append(", ").append(variableName).append(");\n");
} else {
throw getCannotSetException(property.name(), property.sourceType());
}
}
private static GenerationException getCannotSetException(final String propertyName, final String className) {
return new GenerationException("Cannot set " + propertyName + " on " + className);
}
}

View File

@@ -1,4 +1,4 @@
package com.github.gtache.fxml.compiler.impl;
package com.github.gtache.fxml.compiler.impl.internal;
import static java.util.Objects.requireNonNull;
@@ -11,7 +11,15 @@ import static java.util.Objects.requireNonNull;
*/
record Parameter(String name, Class<?> type, String defaultValue) {
Parameter {
/**
* Instantiates a new Parameter
*
* @param name The parameter name
* @param type The parameter type
* @param defaultValue The parameter default value
* @throws NullPointerException if any parameter is null
*/
public Parameter {
requireNonNull(name);
requireNonNull(type);
requireNonNull(defaultValue);

View File

@@ -0,0 +1,127 @@
package com.github.gtache.fxml.compiler.impl.internal;
import com.github.gtache.fxml.compiler.GenerationException;
import com.github.gtache.fxml.compiler.impl.ControllerFieldInjectionTypes;
import com.github.gtache.fxml.compiler.impl.GeneratorImpl;
import com.github.gtache.fxml.compiler.impl.ResourceBundleInjectionTypes;
import com.github.gtache.fxml.compiler.parsing.ParsedObject;
import com.github.gtache.fxml.compiler.parsing.ParsedProperty;
import javafx.event.EventHandler;
import java.util.Objects;
import static com.github.gtache.fxml.compiler.impl.internal.ControllerInjector.injectEventHandlerControllerMethod;
import static com.github.gtache.fxml.compiler.impl.internal.FieldSetter.setEventHandler;
import static com.github.gtache.fxml.compiler.impl.internal.GenerationHelper.*;
import static com.github.gtache.fxml.compiler.impl.internal.ReflectionHelper.*;
import static com.github.gtache.fxml.compiler.impl.internal.ValueFormatter.getArg;
/**
* Helper methods for {@link GeneratorImpl} to format properties
*/
final class PropertyFormatter {
private PropertyFormatter() {
}
/**
* Formats a property
*
* @param progress The generation progress
* @param property The property to format
* @param parent The property's parent object
* @param parentVariable The parent variable
* @throws GenerationException if an error occurs
*/
static void formatProperty(final GenerationProgress progress, final ParsedProperty property, final ParsedObject parent, final String parentVariable) throws GenerationException {
final var propertyName = property.name();
if (propertyName.equals(FX_ID)) {
handleId(progress, parent, parentVariable);
} else if (propertyName.equals("fx:controller")) {
checkDuplicateController(progress, parent);
} else if (Objects.equals(property.sourceType(), EventHandler.class.getName())) {
handleEventHandler(progress, property, parentVariable);
} else if (property.sourceType() != null) {
handleStaticProperty(progress, property, parentVariable, propertyName);
} else {
handleProperty(progress, property, parent, parentVariable);
}
}
private static void checkDuplicateController(final GenerationProgress progress, final ParsedObject parent) throws GenerationException {
if (parent != progress.request().rootObject()) {
throw new GenerationException("Invalid nested controller");
}
}
private static void handleEventHandler(final GenerationProgress progress, final ParsedProperty property, final String parentVariable) throws GenerationException {
if (property.value().startsWith("#")) {
injectEventHandlerControllerMethod(progress, property, parentVariable);
} else {
setEventHandler(progress, property, parentVariable);
}
}
private static void handleStaticProperty(final GenerationProgress progress, final ParsedProperty property, final String parentVariable, final String propertyName) throws GenerationException {
final var setMethod = getSetMethod(propertyName);
final var propertySourceTypeClass = ReflectionHelper.getClass(property.sourceType());
if (hasStaticMethod(propertySourceTypeClass, setMethod)) {
final var method = getStaticMethod(propertySourceTypeClass, setMethod);
final var parameterType = method.getParameterTypes()[1];
final var arg = getArg(progress, property.value(), parameterType);
setLaterIfNeeded(progress, property, parameterType, " " + property.sourceType() + "." + setMethod + "(" + parentVariable + ", " + arg + ");\n");
} else {
throw new GenerationException("Cannot set " + propertyName + " on " + property.sourceType());
}
}
private static void handleProperty(final GenerationProgress progress, final ParsedProperty property, final ParsedObject parent, final String parentVariable) throws GenerationException {
final var propertyName = property.name();
final var setMethod = getSetMethod(propertyName);
final var getMethod = getGetMethod(propertyName);
final var parentClass = ReflectionHelper.getClass(parent.className());
if (hasMethod(parentClass, setMethod)) {
handleSetProperty(progress, property, parentClass, parentVariable);
} else if (hasMethod(parentClass, getMethod)) {
handleGetProperty(progress, property, parentClass, parentVariable);
} else {
throw new GenerationException("Cannot set " + propertyName + " on " + parent.className());
}
}
private static void handleSetProperty(final GenerationProgress progress, final ParsedProperty property, final Class<?> parentClass, final String parentVariable) throws GenerationException {
final var setMethod = getSetMethod(property.name());
final var method = getMethod(parentClass, setMethod);
final var parameterType = method.getParameterTypes()[0];
final var arg = getArg(progress, property.value(), parameterType);
setLaterIfNeeded(progress, property, parameterType, " " + parentVariable + "." + setMethod + "(" + arg + ");\n");
}
private static void handleGetProperty(final GenerationProgress progress, final ParsedProperty property, final Class<?> parentClass, final String parentVariable) throws GenerationException {
final var getMethod = getGetMethod(property.name());
final var method = getMethod(parentClass, getMethod);
final var returnType = method.getReturnType();
if (hasMethod(returnType, "addAll")) {
final var arg = getArg(progress, property.value(), String.class);
setLaterIfNeeded(progress, property, String.class, " " + parentVariable + "." + getMethod + "().addAll(java.util.List.of(" + arg + "));\n");
}
}
/**
* Saves the text to set after constructor creation if factory injection is used
*
* @param progress The generation progress
* @param property The property
* @param type The type
* @param arg The argument
* @throws GenerationException if an error occurs
*/
private static void setLaterIfNeeded(final GenerationProgress progress, final ParsedProperty property, final Class<?> type, final String arg) throws GenerationException {
if (type == String.class && property.value().startsWith("%") && progress.request().parameters().resourceBundleInjection().injectionType() == ResourceBundleInjectionTypes.GETTER
&& getControllerInjection(progress).fieldInjectionType() == ControllerFieldInjectionTypes.FACTORY) {
progress.controllerFactoryPostAction().add(arg);
} else {
progress.stringBuilder().append(arg);
}
}
}

View File

@@ -1,5 +1,8 @@
package com.github.gtache.fxml.compiler.impl;
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.beans.DefaultProperty;
import javafx.beans.NamedArg;
import javafx.scene.Node;
@@ -12,12 +15,15 @@ import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import static com.github.gtache.fxml.compiler.impl.internal.GenerationHelper.FX_ID;
/**
* Helper methods for reflection
*/
final class ReflectionHelper {
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<String, String> DEFAULT_PROPERTY = 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<>();
@@ -199,6 +205,69 @@ final class ReflectionHelper {
}
}
/**
* Computes the default property for the given class
*
* @param className The class name
* @return The default property
* @throws GenerationException If the class is not found or no default property is found
*/
static String getDefaultProperty(final String className) throws GenerationException {
if (DEFAULT_PROPERTY.containsKey(className)) {
return DEFAULT_PROPERTY.get(className);
} else {
final var defaultProperty = computeDefaultProperty(className);
if (defaultProperty != null) {
DEFAULT_PROPERTY.put(className, defaultProperty);
}
return defaultProperty;
}
}
/**
* Gets the wrapper class for the given class
*
* @param clazz The class
* @return The wrapper class (e.g. int.class -> Integer.class) or the original class if it is not a primitive
*/
static String getWrapperClass(final Class<?> clazz) {
final var name = clazz.getName();
if (name.contains(".") || Character.isUpperCase(name.charAt(0))) {
return name;
} else {
return name.substring(0, 1).toUpperCase() + name.substring(1);
}
}
/**
* Gets the class for the given class name
*
* @param className The class name
* @return The class
* @throws GenerationException If the class is not found
*/
public static Class<?> getClass(final String className) throws GenerationException {
try {
return Class.forName(className, false, Thread.currentThread().getContextClassLoader());
} catch (final ClassNotFoundException e) {
throw new GenerationException("Cannot find class " + className + " ; Is a dependency missing for the plugin?", e);
}
}
private static String computeDefaultProperty(final String className) throws GenerationException {
try {
final var clazz = Class.forName(className, false, Thread.currentThread().getContextClassLoader());
final var annotation = clazz.getAnnotation(DefaultProperty.class);
if (annotation == null) {
return null;
} else {
return annotation.value();
}
} catch (final ClassNotFoundException e) {
throw new GenerationException("Class " + className + " not found ; Either specify the property explicitly or put the class in a dependency", e);
}
}
/**
* Computes the default value for the given class
*
@@ -217,4 +286,31 @@ final class ReflectionHelper {
return "null";
}
}
/**
* Gets the generic types for the given object
*
* @param progress The generation progress
* @param parsedObject The parsed object
* @return The generic types
* @throws GenerationException if an error occurs
*/
static String getGenericTypes(final GenerationProgress progress, final ParsedObject parsedObject) throws GenerationException {
final var clazz = getClass(parsedObject.className());
if (isGeneric(clazz)) {
final var idProperty = parsedObject.attributes().get(FX_ID);
if (idProperty == null) {
return "<>";
} else {
final var id = idProperty.value();
final var genericTypes = progress.request().controllerInfo().propertyGenericTypes(id);
if (genericTypes == null) { //Raw
return "";
} else {
return "<" + String.join(", ", genericTypes) + ">";
}
}
}
return "";
}
}

View File

@@ -0,0 +1,77 @@
package com.github.gtache.fxml.compiler.impl.internal;
import com.github.gtache.fxml.compiler.GenerationException;
import com.github.gtache.fxml.compiler.impl.GeneratorImpl;
import com.github.gtache.fxml.compiler.parsing.ParsedObject;
import javafx.scene.paint.Color;
import java.util.ArrayList;
import java.util.Collection;
import static com.github.gtache.fxml.compiler.impl.internal.GenerationHelper.*;
import static com.github.gtache.fxml.compiler.impl.internal.ObjectFormatter.format;
import static com.github.gtache.fxml.compiler.impl.internal.URLBuilder.formatURL;
/**
* Helper methods for {@link GeneratorImpl} to format Scenes
*/
final class SceneBuilder {
private SceneBuilder() {
}
static void formatScene(final GenerationProgress progress, final ParsedObject parsedObject, final String variableName) throws GenerationException {
final var root = findRoot(parsedObject);
final var rootVariableName = progress.getNextVariableName("root");
format(progress, root, rootVariableName);
final var sortedAttributes = getSortedAttributes(parsedObject);
double width = -1;
double height = -1;
var paint = Color.WHITE.toString();
final var stylesheets = new ArrayList<String>();
for (final var property : sortedAttributes) {
switch (property.name()) {
case FX_ID -> {
//Do nothing
}
case "width" -> width = Double.parseDouble(property.value());
case "height" -> height = Double.parseDouble(property.value());
case "fill" -> paint = property.value();
case "stylesheets" -> stylesheets.add(property.value());
default -> throw new GenerationException("Unknown font attribute : " + property.name());
}
}
final var sb = progress.stringBuilder();
sb.append(START_VAR).append(variableName).append(" = new javafx.scene.Scene(").append(rootVariableName).append(", ")
.append(width).append(", ").append(height).append(", javafx.scene.paint.Color.valueOf(\"").append(paint).append("\"));\n");
addStylesheets(progress, variableName, stylesheets);
handleId(progress, parsedObject, variableName);
}
private static ParsedObject findRoot(final ParsedObject parsedObject) throws GenerationException {
final var rootProperty = parsedObject.properties().entrySet().stream().filter(e -> e.getKey().name().equals("root"))
.filter(e -> e.getValue().size() == 1)
.map(e -> e.getValue().getFirst()).findFirst().orElse(null);
if (rootProperty != null) {
return rootProperty;
} else if (parsedObject.children().size() == 1) {
return parsedObject.children().getFirst();
} else {
throw new GenerationException("Scene must have a root");
}
}
private static void addStylesheets(final GenerationProgress progress, final String variableName, final Collection<String> stylesheets) {
if (!stylesheets.isEmpty()) {
final var urlVariables = formatURL(progress, stylesheets);
final var tmpVariable = progress.getNextVariableName("stylesheets");
final var sb = progress.stringBuilder();
sb.append("""
final var %1$s = %2$s.getStylesheets();
%1$s.addAll(java.util.List.of(%3$s));
""".formatted(tmpVariable, variableName, String.join(", ", urlVariables)));
stylesheets.forEach(s -> sb.append(" ").append(variableName).append(".getStyleSheets().add(\"").append(s).append("\");\n"));
}
}
}

View File

@@ -0,0 +1,136 @@
package com.github.gtache.fxml.compiler.impl.internal;
import com.github.gtache.fxml.compiler.GenerationException;
import com.github.gtache.fxml.compiler.impl.GeneratorImpl;
import com.github.gtache.fxml.compiler.parsing.ParsedObject;
import com.github.gtache.fxml.compiler.parsing.ParsedProperty;
import javafx.scene.shape.VertexFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static com.github.gtache.fxml.compiler.impl.internal.GenerationHelper.*;
/**
* Helper methods for {@link GeneratorImpl} to format TriangleMeshes
*/
final class TriangleMeshBuilder {
private TriangleMeshBuilder() {
}
static void formatTriangleMesh(final GenerationProgress progress, final ParsedObject parsedObject, final String variableName) throws GenerationException {
if (parsedObject.children().isEmpty() && parsedObject.properties().isEmpty()) {
final var sortedAttributes = getSortedAttributes(parsedObject);
final var points = new ArrayList<Float>();
final var texCoords = new ArrayList<Float>();
final var normals = new ArrayList<Float>();
final var faces = new ArrayList<Integer>();
final var faceSmoothingGroups = new ArrayList<Integer>();
VertexFormat vertexFormat = null;
for (final var property : sortedAttributes) {
switch (property.name().toLowerCase()) {
case FX_ID -> {
//Do nothing
}
case "points" -> {
points.clear();
points.addAll(parseList(property.value(), Float::parseFloat));
}
case "texcoords" -> {
texCoords.clear();
texCoords.addAll(parseList(property.value(), Float::parseFloat));
}
case "normals" -> {
normals.clear();
normals.addAll(parseList(property.value(), Float::parseFloat));
}
case "faces" -> {
faces.clear();
faces.addAll(parseList(property.value(), Integer::parseInt));
}
case "facesmoothinggroups" -> {
faceSmoothingGroups.clear();
faceSmoothingGroups.addAll(parseList(property.value(), Integer::parseInt));
}
case "vertexformat" -> vertexFormat = parseVertexFormat(property);
default -> throw new GenerationException("Unknown TriangleMesh attribute : " + property.name());
}
}
final var sb = progress.stringBuilder();
sb.append(START_VAR).append(variableName).append(" = new javafx.scene.shape.TriangleMesh();\n");
setPoints(progress, variableName, points);
setTexCoords(progress, variableName, texCoords);
setNormals(progress, variableName, normals);
setFaces(progress, variableName, faces);
setFaceSmoothingGroups(progress, variableName, faceSmoothingGroups);
setVertexFormat(progress, variableName, vertexFormat);
handleId(progress, parsedObject, variableName);
} else {
throw new GenerationException("Image cannot have children or properties : " + parsedObject);
}
}
private static VertexFormat parseVertexFormat(final ParsedProperty property) throws GenerationException {
if (property.value().equalsIgnoreCase("point_texcoord")) {
return VertexFormat.POINT_TEXCOORD;
} else if (property.value().equalsIgnoreCase("point_normal_texcoord")) {
return VertexFormat.POINT_NORMAL_TEXCOORD;
} else {
throw new GenerationException("Unknown vertex format : " + property.value());
}
}
private static void setPoints(final GenerationProgress progress, final String variableName, final Collection<Float> points) {
if (!points.isEmpty()) {
progress.stringBuilder().append(" ").append(variableName).append(".getPoints().setAll(new float[]{").append(formatList(points)).append("});\n");
}
}
private static void setTexCoords(final GenerationProgress progress, final String variableName, final Collection<Float> texCoords) {
if (!texCoords.isEmpty()) {
progress.stringBuilder().append(" ").append(variableName).append(".getTexCoords().setAll(new float[]{").append(formatList(texCoords)).append("});\n");
}
}
private static void setNormals(final GenerationProgress progress, final String variableName, final Collection<Float> normals) {
if (!normals.isEmpty()) {
progress.stringBuilder().append(" ").append(variableName).append(".getNormals().setAll(new float[]{").append(formatList(normals)).append("});\n");
}
}
private static void setFaces(final GenerationProgress progress, final String variableName, final Collection<Integer> faces) {
if (!faces.isEmpty()) {
progress.stringBuilder().append(" ").append(variableName).append(".getFaces().setAll(new int[]{").append(formatList(faces)).append("});\n");
}
}
private static void setFaceSmoothingGroups(final GenerationProgress progress, final String variableName, final Collection<Integer> faceSmoothingGroups) {
if (!faceSmoothingGroups.isEmpty()) {
progress.stringBuilder().append(" ").append(variableName).append(".getFaceSmoothingGroups().setAll(new int[]{").append(formatList(faceSmoothingGroups)).append("});\n");
}
}
private static void setVertexFormat(final GenerationProgress progress, final String variableName, final VertexFormat vertexFormat) {
if (vertexFormat != null) {
progress.stringBuilder().append(" ").append(variableName).append(".setVertexFormat(javafx.scene.shape.VertexFormat.").append(vertexFormat).append(");\n");
}
}
private static <T> String formatList(final Collection<T> list) {
return list.stream().map(String::valueOf).collect(Collectors.joining(", "));
}
private static <T> List<T> parseList(final CharSequence value, final Function<? super String, ? extends T> parser) {
final var splitPattern = Pattern.compile("[\\s+,]");
final var split = splitPattern.split(value);
return Arrays.stream(split).map(parser).collect(Collectors.toList());
}
}

View File

@@ -0,0 +1,62 @@
package com.github.gtache.fxml.compiler.impl.internal;
import com.github.gtache.fxml.compiler.GenerationException;
import com.github.gtache.fxml.compiler.impl.GeneratorImpl;
import com.github.gtache.fxml.compiler.parsing.ParsedObject;
import java.util.Collection;
import java.util.List;
import static com.github.gtache.fxml.compiler.impl.internal.GenerationHelper.*;
/**
* Helper methods for {@link GeneratorImpl} to format URLs
*/
final class URLBuilder {
private URLBuilder() {
}
static List<String> formatURL(final GenerationProgress progress, final Collection<String> stylesheets) {
return stylesheets.stream().map(s -> formatURL(progress, s)).toList();
}
static String formatURL(final GenerationProgress progress, final String url) {
final var variableName = progress.getNextVariableName("url");
final var sb = progress.stringBuilder();
if (url.startsWith("@")) {
sb.append(START_VAR).append(variableName).append(" = getClass().getResource(\"").append(url.substring(1)).append("\");\n");
} else {
sb.append("""
final java.net.URL %1$s;
try {
%1$s = new java.net.URI("%2$s").toURL();
} catch (final java.net.MalformedURLException | java.net.URISyntaxException e) {
throw new RuntimeException("Couldn't parse url : %2$s", e);
}
""".formatted(variableName, url));
}
return variableName;
}
static void formatURL(final GenerationProgress progress, final ParsedObject parsedObject, final String variableName) throws GenerationException {
if (parsedObject.children().isEmpty() && parsedObject.properties().isEmpty()) {
final var sortedAttributes = getSortedAttributes(parsedObject);
String value = null;
for (final var property : sortedAttributes) {
switch (property.name()) {
case FX_ID -> {
//Do nothing
}
case "value" -> value = property.value();
default -> throw new GenerationException("Unknown URL attribute : " + property.name());
}
}
progress.stringBuilder().append(START_VAR).append(variableName).append(" = getClass().getResource(\"").append(value).append("\");\n");
handleId(progress, parsedObject, variableName);
} else {
throw new GenerationException("URL cannot have children or properties : " + parsedObject);
}
}
}

View File

@@ -0,0 +1,118 @@
package com.github.gtache.fxml.compiler.impl.internal;
import com.github.gtache.fxml.compiler.GenerationException;
import com.github.gtache.fxml.compiler.impl.GeneratorImpl;
import com.github.gtache.fxml.compiler.impl.ResourceBundleInjectionTypes;
import java.util.regex.Pattern;
import static com.github.gtache.fxml.compiler.impl.internal.ReflectionHelper.getWrapperClass;
import static com.github.gtache.fxml.compiler.impl.internal.ReflectionHelper.hasValueOf;
/**
* Helper methods for {@link GeneratorImpl} to format values
*/
final class ValueFormatter {
private static final Pattern INT_PATTERN = Pattern.compile("\\d+");
private static final Pattern DECIMAL_PATTERN = Pattern.compile("\\d+(?:\\.\\d+)?");
private static final Pattern START_BACKSLASH_PATTERN = Pattern.compile("^\\\\");
private ValueFormatter() {
}
/**
* Formats an argument to a method
*
* @param progress The generation progress
* @param value The value
* @param parameterType The parameter type
* @return The formatted value
* @throws GenerationException if an error occurs
*/
static String getArg(final GenerationProgress progress, final String value, final Class<?> parameterType) throws GenerationException {
if (parameterType == String.class && value.startsWith("%")) {
return getBundleValue(progress, value.substring(1));
} else if (value.startsWith("@")) {
final var subpath = value.substring(1);
return getResourceValue(subpath);
} else if (value.startsWith("${")) {
throw new UnsupportedOperationException("Not implemented yet");
} else if (value.startsWith("$")) {
final var variable = progress.idToVariableName().get(value.substring(1));
if (variable == null) {
throw new GenerationException("Unknown variable : " + value.substring(1));
}
return variable;
} else {
return toString(value, parameterType);
}
}
private static String getResourceValue(final String subpath) {
return "getClass().getResource(\"" + subpath + "\").toString()";
}
/**
* Gets the resource bundle value for the given value
*
* @param progress The generation progress
* @param value The value
* @return The resource bundle value
* @throws GenerationException if an error occurs
*/
private static String getBundleValue(final GenerationProgress progress, final String value) throws GenerationException {
final var resourceBundleInjectionType = progress.request().parameters().resourceBundleInjection().injectionType();
if (resourceBundleInjectionType instanceof final ResourceBundleInjectionTypes types) {
return switch (types) {
case CONSTRUCTOR, GET_BUNDLE -> "bundle.getString(\"" + value + "\")";
case GETTER -> "controller.resources().getString(\"" + value + "\")";
};
} else {
throw new GenerationException("Unknown resource bundle injection type : " + resourceBundleInjectionType);
}
}
/**
* Computes the string value to use in the generated code
*
* @param value The value
* @param clazz The value class
* @return The computed string value
*/
static String toString(final String value, final Class<?> clazz) {
if (clazz == String.class) {
return "\"" + START_BACKSLASH_PATTERN.matcher(value).replaceAll("").replace("\"", "\\\"") + "\"";
} else if (clazz == char.class || clazz == Character.class) {
return "'" + value + "'";
} else if (clazz == boolean.class || clazz == Boolean.class) {
return value;
} else if (clazz == byte.class || clazz == Byte.class || clazz == short.class || clazz == Short.class ||
clazz == int.class || clazz == Integer.class || clazz == long.class || clazz == Long.class) {
if (INT_PATTERN.matcher(value).matches()) {
return value;
} else {
return getValueOf(getWrapperClass(clazz), value);
}
} else if (clazz == float.class || clazz == Float.class || clazz == double.class || clazz == Double.class) {
if (DECIMAL_PATTERN.matcher(value).matches()) {
return value;
} else {
return getValueOf(getWrapperClass(clazz), value);
}
} else if (hasValueOf(clazz)) {
if (clazz.isEnum()) {
return clazz.getCanonicalName() + "." + value;
} else {
return getValueOf(clazz.getCanonicalName(), value);
}
} else {
return value;
}
}
private static String getValueOf(final String clazz, final String value) {
return clazz + ".valueOf(\"" + value + "\")";
}
}

View File

@@ -0,0 +1,111 @@
package com.github.gtache.fxml.compiler.impl.internal;
import com.github.gtache.fxml.compiler.GenerationException;
import com.github.gtache.fxml.compiler.impl.GeneratorImpl;
import com.github.gtache.fxml.compiler.parsing.ParsedObject;
import com.github.gtache.fxml.compiler.parsing.ParsedProperty;
import static com.github.gtache.fxml.compiler.impl.internal.ControllerInjector.injectCallbackControllerMethod;
import static com.github.gtache.fxml.compiler.impl.internal.ControllerInjector.injectEventHandlerControllerMethod;
import static com.github.gtache.fxml.compiler.impl.internal.FieldSetter.setEventHandler;
import static com.github.gtache.fxml.compiler.impl.internal.FieldSetter.setField;
import static com.github.gtache.fxml.compiler.impl.internal.GenerationHelper.*;
import static com.github.gtache.fxml.compiler.impl.internal.PropertyFormatter.formatProperty;
/**
* Helper methods for {@link GeneratorImpl} to format WebViews
*/
final class WebViewBuilder {
private WebViewBuilder() {
}
/**
* Formats a WebView object
*
* @param progress The generation progress
* @param parsedObject The parsed object
* @param variableName The variable name
* @throws GenerationException if an error occurs
*/
static void formatWebView(final GenerationProgress progress, final ParsedObject parsedObject, final String variableName) throws GenerationException {
if (parsedObject.children().isEmpty() && parsedObject.properties().isEmpty()) {
final var sortedAttributes = getSortedAttributes(parsedObject);
final var sb = progress.stringBuilder();
sb.append(START_VAR).append(variableName).append(" = new javafx.scene.web.WebView();\n");
final var engineVariable = progress.getNextVariableName("engine");
sb.append(START_VAR).append(engineVariable).append(" = ").append(variableName).append(".getEngine();\n");
for (final var value : sortedAttributes) {
formatAttribute(progress, value, parsedObject, variableName, engineVariable);
}
handleId(progress, parsedObject, variableName);
} else {
throw new GenerationException("WebView cannot have children or properties : " + parsedObject);
}
}
private static void formatAttribute(final GenerationProgress progress, final ParsedProperty value, final ParsedObject parsedObject,
final String variableName, final String engineVariable) throws GenerationException {
switch (value.name()) {
case FX_ID -> {
//Do nothing
}
case "confirmHandler" -> injectConfirmHandler(progress, value, engineVariable);
case "createPopupHandler" -> injectCreatePopupHandler(progress, value, engineVariable);
case "onAlert", "onResized", "onStatusChanged", "onVisibilityChanged" ->
injectEventHandler(progress, value, engineVariable);
case "promptHandler" -> injectPromptHandler(progress, value, engineVariable);
case "location" -> injectLocation(progress, value, engineVariable);
default -> formatProperty(progress, value, parsedObject, variableName);
}
}
private static void injectConfirmHandler(final GenerationProgress progress, final ParsedProperty value, final String engineVariable) throws GenerationException {
if (value.value().startsWith("#")) {
injectCallbackControllerMethod(progress, value, engineVariable, "String.class");
} else {
setCallback(progress, value, engineVariable);
}
}
private static void injectCreatePopupHandler(final GenerationProgress progress, final ParsedProperty value, final String engineVariable) throws GenerationException {
if (value.value().startsWith("#")) {
injectCallbackControllerMethod(progress, value, engineVariable, "javafx.scene.web.PopupFeatures.class");
} else {
setCallback(progress, value, engineVariable);
}
}
private static void injectEventHandler(final GenerationProgress progress, final ParsedProperty value, final String engineVariable) throws GenerationException {
if (value.value().startsWith("#")) {
injectEventHandlerControllerMethod(progress, value, engineVariable);
} else {
setEventHandler(progress, value, engineVariable);
}
}
private static void injectPromptHandler(final GenerationProgress progress, final ParsedProperty value, final String engineVariable) throws GenerationException {
if (value.value().startsWith("#")) {
injectCallbackControllerMethod(progress, value, engineVariable, "javafx.scene.web.PromptData.class");
} else {
setCallback(progress, value, engineVariable);
}
}
private static void injectLocation(final GenerationProgress progress, final ParsedProperty value, final String engineVariable) {
progress.stringBuilder().append(" ").append(engineVariable).append(".load(\"").append(value.value()).append("\");\n");
}
/**
* Sets a callback field
*
* @param progress The generation progress
* @param property The property to inject
* @param parentVariable The parent variable
*/
private static void setCallback(final GenerationProgress progress, final ParsedProperty property, final String parentVariable) throws GenerationException {
setField(progress, property, parentVariable, "javafx.util.Callback");
}
}

View File

@@ -0,0 +1,18 @@
package com.github.gtache.fxml.compiler.parsing.impl;
import com.github.gtache.fxml.compiler.parsing.ParsedCopy;
import com.github.gtache.fxml.compiler.parsing.ParsedProperty;
import java.util.Map;
/**
* Implementation of {@link ParsedCopy}
*
* @param attributes The reference properties
*/
public record ParsedCopyImpl(Map<String, ParsedProperty> attributes) implements ParsedCopy {
public ParsedCopyImpl {
attributes = Map.copyOf(attributes);
}
}

View File

@@ -0,0 +1,20 @@
package com.github.gtache.fxml.compiler.parsing.impl;
import com.github.gtache.fxml.compiler.parsing.ParsedDefine;
import com.github.gtache.fxml.compiler.parsing.ParsedObject;
import java.util.Objects;
/**
* Implementation of {@link ParsedObject}
*
* @param className The object class
* @param attributes The object properties
* @param properties The object children (complex properties)
*/
public record ParsedDefineImpl(ParsedObject object) implements ParsedDefine {
public ParsedDefineImpl {
Objects.requireNonNull(object);
}
}

View File

@@ -0,0 +1,29 @@
package com.github.gtache.fxml.compiler.parsing.impl;
import com.github.gtache.fxml.compiler.parsing.ParsedFactory;
import com.github.gtache.fxml.compiler.parsing.ParsedObject;
import com.github.gtache.fxml.compiler.parsing.ParsedProperty;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.SequencedCollection;
/**
* Implementation of {@link ParsedFactory}
*
* @param className The factory class
* @param attributes The factory properties
* @param arguments The factory arguments
*/
public record ParsedFactoryImpl(String className, Map<String, ParsedProperty> attributes,
SequencedCollection<ParsedObject> arguments,
SequencedCollection<ParsedObject> children) implements ParsedFactory {
public ParsedFactoryImpl {
Objects.requireNonNull(className);
attributes = Map.copyOf(attributes);
arguments = List.copyOf(arguments);
children = List.copyOf(children);
}
}

View File

@@ -3,10 +3,9 @@ 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.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.SequencedCollection;
@@ -20,74 +19,13 @@ import java.util.SequencedMap;
* @param properties The object children (complex properties)
*/
public record ParsedObjectImpl(String className, Map<String, ParsedProperty> attributes,
SequencedMap<ParsedProperty, SequencedCollection<ParsedObject>> properties) implements ParsedObject {
SequencedMap<ParsedProperty, SequencedCollection<ParsedObject>> properties,
SequencedCollection<ParsedObject> children) implements ParsedObject {
public ParsedObjectImpl {
Objects.requireNonNull(className);
attributes = Map.copyOf(attributes);
properties = Collections.unmodifiableSequencedMap(new LinkedHashMap<>(properties));
}
/**
* Builder for {@link ParsedObjectImpl}
*/
public static class Builder {
private String className;
private final Map<String, ParsedProperty> attributes;
private final SequencedMap<ParsedProperty, SequencedCollection<ParsedObject>> properties;
/**
* Creates a new builder
*/
public Builder() {
this.attributes = new HashMap<>();
this.properties = new LinkedHashMap<>();
}
/**
* Sets the object class
*
* @param className The object class
* @return The builder
*/
public Builder className(final String className) {
this.className = className;
return this;
}
/**
* Adds an attribute
*
* @param attribute The attribute
* @return The builder
*/
public Builder addAttribute(final ParsedProperty attribute) {
attributes.put(attribute.name(), attribute);
return this;
}
/**
* Adds a property
*
* @param property The property
* @param child The property element
* @return The builder
*/
public Builder addProperty(final ParsedProperty property, final ParsedObject child) {
final var sequence = properties.computeIfAbsent(property, k -> new ArrayList<>());
sequence.add(child);
properties.put(property, sequence);
return this;
}
/**
* Builds the object
*
* @return The object
*/
public ParsedObjectImpl build() {
return new ParsedObjectImpl(className, attributes, properties);
}
children = List.copyOf(children);
}
}

View File

@@ -10,6 +10,7 @@ import java.util.Objects;
* @param name The property name
* @param sourceType The property source type
* @param value The property value
* @param defines The property defines
*/
public record ParsedPropertyImpl(String name, String sourceType, String value) implements ParsedProperty {

View File

@@ -0,0 +1,18 @@
package com.github.gtache.fxml.compiler.parsing.impl;
import com.github.gtache.fxml.compiler.parsing.ParsedProperty;
import com.github.gtache.fxml.compiler.parsing.ParsedReference;
import java.util.Map;
/**
* Implementation of {@link ParsedReference}
*
* @param attributes The reference properties
*/
public record ParsedReferenceImpl(Map<String, ParsedProperty> attributes) implements ParsedReference {
public ParsedReferenceImpl {
attributes = Map.copyOf(attributes);
}
}

View File

@@ -0,0 +1,23 @@
package com.github.gtache.fxml.compiler.parsing.impl;
import com.github.gtache.fxml.compiler.parsing.ParsedText;
import java.util.Objects;
/**
* Implementation of {@link ParsedText}
*
* @param text The text
*/
public record ParsedTextImpl(String text) implements ParsedText {
/**
* Instantiates a new parsed text
*
* @param text The text
* @throws NullPointerException if the text is null
*/
public ParsedTextImpl {
Objects.requireNonNull(text);
}
}

View File

@@ -0,0 +1,21 @@
package com.github.gtache.fxml.compiler.parsing.impl;
import com.github.gtache.fxml.compiler.parsing.ParsedProperty;
import com.github.gtache.fxml.compiler.parsing.ParsedValue;
import java.util.Map;
import java.util.Objects;
/**
* Implementation of {@link ParsedValue}
*
* @param className The value class
* @param attributes The value properties
*/
public record ParsedValueImpl(String className, Map<String, ParsedProperty> attributes) implements ParsedValue {
public ParsedValueImpl {
Objects.requireNonNull(className);
attributes = Map.copyOf(attributes);
}
}

View File

@@ -13,10 +13,14 @@ class TestClassesFinder {
void testGetClassesCurrent() throws IOException {
final var expected = Set.of(
"com.github.gtache.fxml.compiler.parsing.impl.TestParsedConstantImpl",
"com.github.gtache.fxml.compiler.parsing.impl.TestParsedCopyImpl",
"com.github.gtache.fxml.compiler.parsing.impl.TestParsedFactoryImpl",
"com.github.gtache.fxml.compiler.parsing.impl.TestParsedIncludeImpl",
"com.github.gtache.fxml.compiler.parsing.impl.TestParsedObjectImpl",
"com.github.gtache.fxml.compiler.parsing.impl.TestParsedObjectImplBuilder",
"com.github.gtache.fxml.compiler.parsing.impl.TestParsedPropertyImpl");
"com.github.gtache.fxml.compiler.parsing.impl.TestParsedPropertyImpl",
"com.github.gtache.fxml.compiler.parsing.impl.TestParsedReferenceImpl",
"com.github.gtache.fxml.compiler.parsing.impl.TestParsedTextImpl",
"com.github.gtache.fxml.compiler.parsing.impl.TestParsedValueImpl");
final var actual = ClassesFinder.getClasses("com.github.gtache.fxml.compiler.parsing.impl");
assertEquals(expected, actual);
}

View File

@@ -0,0 +1,121 @@
package com.github.gtache.fxml.compiler.impl;
import com.github.gtache.fxml.compiler.GenerationRequest;
import com.github.gtache.fxml.compiler.impl.internal.GenerationProgress;
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.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.SequencedCollection;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.mock;
@ExtendWith(MockitoExtension.class)
class TestGenerationProgress {
private final GenerationRequest request;
private final Map<String, String> idToVariableName;
private final Map<String, ParsedObject> idToObject;
private final Map<String, AtomicInteger> variableNameCounters;
private final SequencedCollection<String> controllerFactoryPostAction;
private final StringBuilder sb;
private final GenerationProgress progress;
TestGenerationProgress(@Mock final GenerationRequest request, @Mock final ParsedObject object) {
this.request = Objects.requireNonNull(request);
this.idToVariableName = new HashMap<>();
idToVariableName.put("var1", "var2");
this.idToObject = new HashMap<>();
idToObject.put("var1", object);
this.variableNameCounters = new HashMap<>();
variableNameCounters.put("var", new AtomicInteger(0));
this.controllerFactoryPostAction = new ArrayList<>();
controllerFactoryPostAction.add("bla");
this.sb = new StringBuilder("test");
this.progress = new GenerationProgress(request, idToVariableName, idToObject, variableNameCounters, controllerFactoryPostAction, sb);
}
@Test
void testGetters() {
assertEquals(request, progress.request());
assertEquals(idToVariableName, progress.idToVariableName());
assertEquals(variableNameCounters, progress.variableNameCounters());
assertEquals(controllerFactoryPostAction, progress.controllerFactoryPostAction());
assertEquals(sb, progress.stringBuilder());
}
@Test
void testConstructorDoesntCopy() {
idToVariableName.clear();
assertEquals(idToVariableName, progress.idToVariableName());
idToObject.clear();
assertEquals(idToObject, progress.idToObject());
variableNameCounters.clear();
assertEquals(variableNameCounters, progress.variableNameCounters());
controllerFactoryPostAction.clear();
assertEquals(controllerFactoryPostAction, progress.controllerFactoryPostAction());
sb.setLength(0);
assertEquals(sb, progress.stringBuilder());
}
@Test
void testCanModify() {
progress.idToVariableName().put("var3", "var4");
assertEquals(idToVariableName, progress.idToVariableName());
progress.idToObject().put("var3", mock(ParsedObject.class));
assertEquals(idToObject, progress.idToObject());
progress.variableNameCounters().put("var5", new AtomicInteger(0));
assertEquals(variableNameCounters, progress.variableNameCounters());
progress.controllerFactoryPostAction().add("bla2");
assertEquals(controllerFactoryPostAction, progress.controllerFactoryPostAction());
progress.stringBuilder().append("test2");
assertEquals(sb, progress.stringBuilder());
}
@Test
void testOtherConstructor() {
final var progress2 = new GenerationProgress(request);
assertEquals(request, progress2.request());
assertEquals(Map.of(), progress2.idToVariableName());
assertEquals(Map.of(), progress2.variableNameCounters());
assertEquals(List.of(), progress2.controllerFactoryPostAction());
assertEquals("", progress2.stringBuilder().toString());
}
@Test
void testGetNextVariableName() {
assertEquals("var0", progress.getNextVariableName("var"));
assertEquals("var1", progress.getNextVariableName("var"));
assertEquals("var2", progress.getNextVariableName("var"));
assertEquals("bla0", progress.getNextVariableName("bla"));
}
@Test
void testIllegal() {
assertThrows(NullPointerException.class, () -> new GenerationProgress(null, idToVariableName, idToObject, variableNameCounters, controllerFactoryPostAction, sb));
assertThrows(NullPointerException.class, () -> new GenerationProgress(request, null, idToObject, variableNameCounters, controllerFactoryPostAction, sb));
assertThrows(NullPointerException.class, () -> new GenerationProgress(request, idToVariableName, null, variableNameCounters, controllerFactoryPostAction, sb));
assertThrows(NullPointerException.class, () -> new GenerationProgress(request, idToVariableName, idToObject, null, controllerFactoryPostAction, sb));
assertThrows(NullPointerException.class, () -> new GenerationProgress(request, idToVariableName, idToObject, variableNameCounters, null, sb));
assertThrows(NullPointerException.class, () -> new GenerationProgress(request, idToVariableName, idToObject, variableNameCounters, controllerFactoryPostAction, null));
}
}

View File

@@ -1,4 +1,4 @@
package com.github.gtache.fxml.compiler.impl;
package com.github.gtache.fxml.compiler.impl.internal;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

View File

@@ -1,4 +1,4 @@
package com.github.gtache.fxml.compiler.impl;
package com.github.gtache.fxml.compiler.impl.internal;
import org.junit.jupiter.api.Test;

View File

@@ -1,5 +1,7 @@
package com.github.gtache.fxml.compiler.impl;
package com.github.gtache.fxml.compiler.impl.internal;
import com.github.gtache.fxml.compiler.GenerationException;
import com.github.gtache.fxml.compiler.impl.WholeConstructorArgs;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.ComboBox;
@@ -110,4 +112,19 @@ class TestReflectionHelper {
final var constructor = Arrays.stream(WholeConstructorArgs.class.getConstructors()).filter(c -> c.getParameterCount() == 3).findFirst().orElseThrow();
assertThrows(IllegalStateException.class, () -> ReflectionHelper.getConstructorArgs(constructor));
}
@Test
void testGetDefaultPropertyClassNotFound() {
assertThrows(GenerationException.class, () -> ReflectionHelper.getDefaultProperty("bla.bla"));
}
@Test
void testGetDefaultPropertyNoDefaultProperty() throws GenerationException {
assertNull(ReflectionHelper.getDefaultProperty("java.lang.String"));
}
@Test
void testGetDefaultProperty() throws GenerationException {
assertEquals("items", ReflectionHelper.getDefaultProperty("javafx.scene.control.ListView"));
}
}

View File

@@ -0,0 +1,52 @@
package com.github.gtache.fxml.compiler.parsing.impl;
import com.github.gtache.fxml.compiler.parsing.ParsedCopy;
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.SequencedMap;
import static org.junit.jupiter.api.Assertions.*;
@ExtendWith(MockitoExtension.class)
class TestParsedCopyImpl {
private final SequencedMap<String, ParsedProperty> properties;
private final ParsedCopy copy;
TestParsedCopyImpl(@Mock final ParsedProperty property) {
this.properties = new LinkedHashMap<>();
this.properties.put("name", property);
this.copy = new ParsedCopyImpl(properties);
}
@Test
void testGetters() {
assertEquals(properties, copy.attributes());
assertEquals(ParsedCopy.class.getName(), copy.className());
assertEquals(new LinkedHashMap<>(), copy.properties());
}
@Test
void testCopyMap() {
final var originalProperties = copy.attributes();
properties.clear();
assertEquals(originalProperties, copy.attributes());
assertNotEquals(properties, copy.attributes());
}
@Test
void testUnmodifiable() {
final var objectProperties = copy.attributes();
assertThrows(UnsupportedOperationException.class, objectProperties::clear);
}
@Test
void testIllegal() {
assertThrows(NullPointerException.class, () -> new TestParsedCopyImpl(null));
}
}

View File

@@ -0,0 +1,90 @@
package com.github.gtache.fxml.compiler.parsing.impl;
import com.github.gtache.fxml.compiler.parsing.ParsedDefine;
import com.github.gtache.fxml.compiler.parsing.ParsedFactory;
import com.github.gtache.fxml.compiler.parsing.ParsedObject;
import com.github.gtache.fxml.compiler.parsing.ParsedProperty;
import 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.HashMap;
import java.util.List;
import java.util.Map;
import java.util.SequencedCollection;
import static org.junit.jupiter.api.Assertions.*;
@ExtendWith(MockitoExtension.class)
class TestParsedFactoryImpl {
private final String className;
private final Map<String, ParsedProperty> attributes;
private final SequencedCollection<ParsedObject> arguments;
private final SequencedCollection<ParsedObject> children;
private final ParsedFactory factory;
TestParsedFactoryImpl(@Mock final ParsedObject object1, @Mock final ParsedObject object2, @Mock final ParsedDefine define) {
this.className = "test";
this.attributes = new HashMap<>(Map.of("fx:factory", new ParsedPropertyImpl("fx:factory", String.class.getName(), "value")));
this.arguments = new ArrayList<>(List.of(object1, object2));
this.children = new ArrayList<>(List.of(define));
this.factory = new ParsedFactoryImpl(className, attributes, arguments, children);
}
@Test
void testGetters() {
assertEquals(className, factory.className());
assertEquals(attributes, factory.attributes());
assertEquals(attributes.get("fx:factory").value(), factory.factory());
assertEquals(arguments, factory.arguments());
assertEquals(children, factory.children());
assertEquals(Map.of(), factory.properties());
}
@Test
void testCopyMap() {
final var originalAttributes = factory.attributes();
attributes.clear();
assertEquals(originalAttributes, factory.attributes());
assertNotEquals(attributes, factory.attributes());
}
@Test
void testCopyArguments() {
final var originalArguments = factory.arguments();
arguments.clear();
assertEquals(originalArguments, factory.arguments());
assertNotEquals(arguments, factory.arguments());
}
@Test
void testCopyDefines() {
final var originalDefines = factory.children();
children.clear();
assertEquals(originalDefines, factory.children());
assertNotEquals(children, factory.children());
}
@Test
void testUnmodifiable() {
final var objectProperties = factory.attributes();
assertThrows(UnsupportedOperationException.class, objectProperties::clear);
final var objectArguments = factory.arguments();
assertThrows(UnsupportedOperationException.class, objectArguments::clear);
final var objectDefines = factory.children();
assertThrows(UnsupportedOperationException.class, objectDefines::clear);
}
@Test
void testIllegal() {
assertThrows(NullPointerException.class, () -> new ParsedFactoryImpl(null, attributes, arguments, children));
assertThrows(NullPointerException.class, () -> new ParsedFactoryImpl(className, null, arguments, children));
assertThrows(NullPointerException.class, () -> new ParsedFactoryImpl(className, attributes, null, children));
assertThrows(NullPointerException.class, () -> new ParsedFactoryImpl(className, attributes, arguments, null));
}
}

View File

@@ -1,5 +1,6 @@
package com.github.gtache.fxml.compiler.parsing.impl;
import com.github.gtache.fxml.compiler.parsing.ParsedDefine;
import com.github.gtache.fxml.compiler.parsing.ParsedObject;
import com.github.gtache.fxml.compiler.parsing.ParsedProperty;
import org.junit.jupiter.api.Test;
@@ -7,6 +8,7 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.SequencedCollection;
@@ -19,48 +21,57 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
class TestParsedObjectImpl {
private final String clazz;
private final SequencedMap<String, ParsedProperty> properties;
private final SequencedMap<ParsedProperty, SequencedCollection<ParsedObject>> children;
private final SequencedMap<String, ParsedProperty> attributes;
private final SequencedMap<ParsedProperty, SequencedCollection<ParsedObject>> properties;
private final SequencedCollection<ParsedObject> objects;
private final ParsedObject parsedObject;
TestParsedObjectImpl(@Mock final ParsedProperty property, @Mock final ParsedObject object) {
TestParsedObjectImpl(@Mock final ParsedProperty property, @Mock final ParsedObject object, @Mock final ParsedDefine define) {
this.clazz = Object.class.getName();
this.attributes = new LinkedHashMap<>();
this.attributes.put("name", property);
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);
this.properties.put(property, List.of(object));
this.objects = new ArrayList<>(List.of(define));
this.parsedObject = new ParsedObjectImpl(clazz, attributes, properties, objects);
}
@Test
void testGetters() {
assertEquals(clazz, parsedObject.className());
assertEquals(properties, parsedObject.attributes());
assertEquals(children, parsedObject.properties());
assertEquals(attributes, parsedObject.attributes());
assertEquals(properties, parsedObject.properties());
assertEquals(objects, parsedObject.children());
}
@Test
void testCopyMap() {
final var originalProperties = parsedObject.attributes();
final var originalChildren = parsedObject.properties();
final var originalAttributes = parsedObject.attributes();
final var originalProperties = parsedObject.properties();
final var originalChildren = parsedObject.children();
attributes.clear();
properties.clear();
children.clear();
assertEquals(originalProperties, parsedObject.attributes());
assertEquals(originalChildren, parsedObject.properties());
objects.clear();
assertEquals(originalAttributes, parsedObject.attributes());
assertEquals(originalProperties, parsedObject.properties());
assertEquals(originalChildren, parsedObject.children());
}
@Test
void testUnmodifiable() {
final var objectProperties = parsedObject.attributes();
final var objectChildren = parsedObject.properties();
final var objectAttributes = parsedObject.attributes();
final var objectProperties = parsedObject.properties();
final var objectChildren = parsedObject.children();
assertThrows(UnsupportedOperationException.class, objectAttributes::clear);
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));
assertThrows(NullPointerException.class, () -> new ParsedObjectImpl(null, attributes, properties, objects));
assertThrows(NullPointerException.class, () -> new ParsedObjectImpl(clazz, null, properties, objects));
assertThrows(NullPointerException.class, () -> new ParsedObjectImpl(clazz, attributes, null, objects));
assertThrows(NullPointerException.class, () -> new ParsedObjectImpl(clazz, attributes, properties, null));
}
}

View File

@@ -1,108 +0,0 @@
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 String clazz1;
private final String 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.getName();
this.clazz2 = String.class.getName();
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 testClassName() {
builder.className(clazz1);
final var built = builder.build();
assertEquals(clazz1, built.className());
assertEquals(Map.of(), built.attributes());
assertEquals(Map.of(), built.properties());
}
@Test
void testOverwriteClassName() {
builder.className(clazz1);
builder.className(clazz2);
final var built = builder.build();
assertEquals(clazz2, built.className());
assertEquals(Map.of(), built.attributes());
assertEquals(Map.of(), built.properties());
}
@Test
void testAddAttribute() {
builder.className(clazz1);
builder.addAttribute(property1);
final var built = builder.build();
assertEquals(Map.of(property1.name(), property1), built.attributes());
assertEquals(Map.of(), built.properties());
}
@Test
void testAddMultipleAttributes() {
builder.className(clazz1);
builder.addAttribute(property1);
builder.addAttribute(property2);
final var built = builder.build();
assertEquals(Map.of(property1.name(), property1, property2.name(), property2), built.attributes());
assertEquals(Map.of(), built.properties());
}
@Test
void testAddProperty() {
builder.className(clazz1);
builder.addProperty(property1, object1);
final var built = builder.build();
assertEquals(Map.of(), built.attributes());
assertEquals(Map.of(property1, List.of(object1)), built.properties());
}
@Test
void testAddMultipleProperties() {
builder.className(clazz1);
builder.addProperty(property1, object1);
builder.addProperty(property2, object2);
final var built = builder.build();
assertEquals(Map.of(), built.attributes());
assertEquals(Map.of(property1, List.of(object1), property2, List.of(object2)), built.properties());
}
}

View File

@@ -0,0 +1,52 @@
package com.github.gtache.fxml.compiler.parsing.impl;
import com.github.gtache.fxml.compiler.parsing.ParsedProperty;
import com.github.gtache.fxml.compiler.parsing.ParsedReference;
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.SequencedMap;
import static org.junit.jupiter.api.Assertions.*;
@ExtendWith(MockitoExtension.class)
class TestParsedReferenceImpl {
private final SequencedMap<String, ParsedProperty> properties;
private final ParsedReference reference;
TestParsedReferenceImpl(@Mock final ParsedProperty property) {
this.properties = new LinkedHashMap<>();
this.properties.put("name", property);
this.reference = new ParsedReferenceImpl(properties);
}
@Test
void testGetters() {
assertEquals(properties, reference.attributes());
assertEquals(ParsedReference.class.getName(), reference.className());
assertEquals(new LinkedHashMap<>(), reference.properties());
}
@Test
void testCopyMap() {
final var originalProperties = reference.attributes();
properties.clear();
assertEquals(originalProperties, reference.attributes());
assertNotEquals(properties, reference.attributes());
}
@Test
void testUnmodifiable() {
final var objectProperties = reference.attributes();
assertThrows(UnsupportedOperationException.class, objectProperties::clear);
}
@Test
void testIllegal() {
assertThrows(NullPointerException.class, () -> new ParsedReferenceImpl(null));
}
}

View File

@@ -0,0 +1,28 @@
package com.github.gtache.fxml.compiler.parsing.impl;
import com.github.gtache.fxml.compiler.parsing.ParsedText;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
class TestParsedTextImpl {
private final String text;
private final ParsedText parsedText;
TestParsedTextImpl() {
this.text = "text";
this.parsedText = new ParsedTextImpl(text);
}
@Test
void testText() {
assertEquals(text, parsedText.text());
}
@Test
void testIllegal() {
assertThrows(NullPointerException.class, () -> new ParsedTextImpl(null));
}
}

View File

@@ -0,0 +1,51 @@
package com.github.gtache.fxml.compiler.parsing.impl;
import com.github.gtache.fxml.compiler.parsing.ParsedProperty;
import com.github.gtache.fxml.compiler.parsing.ParsedValue;
import org.junit.jupiter.api.Test;
import java.util.HashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
class TestParsedValueImpl {
private final String className;
private final Map<String, ParsedProperty> attributes;
private final ParsedValue value;
TestParsedValueImpl() {
this.className = "test";
this.attributes = new HashMap<>(Map.of("fx:value", new ParsedPropertyImpl("fx:value", String.class.getName(), "value")));
this.value = new ParsedValueImpl(className, attributes);
}
@Test
void testGetters() {
assertEquals(className, value.className());
assertEquals(attributes, value.attributes());
assertEquals(attributes.get("fx:value").value(), value.value());
assertEquals(Map.of(), value.properties());
}
@Test
void testCopyMap() {
final var originalAttributes = value.attributes();
attributes.clear();
assertEquals(originalAttributes, value.attributes());
assertNotEquals(attributes, value.attributes());
}
@Test
void testUnmodifiable() {
final var objectProperties = value.attributes();
assertThrows(UnsupportedOperationException.class, objectProperties::clear);
}
@Test
void testIllegal() {
assertThrows(NullPointerException.class, () -> new ParsedValueImpl(null, attributes));
assertThrows(NullPointerException.class, () -> new ParsedValueImpl(className, null));
}
}