Finishes implementing, seems to work ; needs to manage define, copy, reference, root

This commit is contained in:
Guillaume Tâche
2024-11-24 20:15:50 +01:00
parent fd145271a0
commit 102927b040
161 changed files with 27870 additions and 9317 deletions

View File

@@ -0,0 +1,75 @@
package com.github.gtache.fxml.compiler.impl;
import java.io.IOException;
import java.net.URI;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
/**
* Helper class to find classes in a package
*/
public final class ClassesFinder {
private static final Pattern START_FILE_PATTERN = Pattern.compile("^(?:file:/)?/");
private ClassesFinder() {
}
/**
* Finds all classes in the given package
*
* @param packageName The package
* @return The classes
* @throws IOException If an error occurs
*/
public static Set<String> getClasses(final String packageName) throws IOException {
return doGetClasses(packageName);
}
private static Set<String> doGetClasses(final String packageName) throws IOException {
final var classLoader = Thread.currentThread().getContextClassLoader();
final var path = packageName.replace('.', '/');
final var resources = classLoader.getResources(path);
final var classes = new HashSet<String>();
while (resources.hasMoreElements()) {
final var resource = resources.nextElement();
final var file = resource.getFile();
if (file.contains(".jar!")) {
final var jarFile = file.substring(0, file.indexOf(".jar!") + 4);
try (final var fs = FileSystems.newFileSystem(Paths.get(URI.create(jarFile)), classLoader)) {
classes.addAll(findClasses(fs.getPath(path), packageName));
}
} else {
final var filepath = START_FILE_PATTERN.matcher(file).replaceAll("");
classes.addAll(findClasses(Paths.get(filepath), packageName));
}
}
return classes;
}
private static List<String> findClasses(final Path directory, final String packageName) throws IOException {
if (Files.isDirectory(directory)) {
final var classes = new ArrayList<String>();
try (final var stream = Files.list(directory)) {
final var files = stream.toList();
for (final var file : files) {
final var filename = file.getFileName().toString();
if (filename.endsWith(".class")) {
final var className = packageName + '.' + filename.substring(0, filename.length() - 6);
classes.add(className);
}
}
}
return classes;
} else {
return List.of();
}
}
}

View File

@@ -0,0 +1,22 @@
package com.github.gtache.fxml.compiler.impl;
import java.lang.reflect.Constructor;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.SequencedMap;
import static java.util.Objects.requireNonNull;
/**
* Used by {@link ReflectionHelper} to store the constructor arguments
*
* @param constructor The constructor
* @param namedArgs The named arguments
*/
record ConstructorArgs(Constructor<?> constructor,
SequencedMap<String, Parameter> namedArgs) {
ConstructorArgs {
requireNonNull(constructor);
namedArgs = Collections.unmodifiableSequencedMap(new LinkedHashMap<>(namedArgs));
}
}

View File

@@ -11,6 +11,7 @@ import java.util.Objects;
* Implementation of {@link GenerationRequest}
*
* @param parameters The generation parameters
* @param controllerInfo The controller info
* @param rootObject The root object
* @param outputClassName The output class name
*/

View File

@@ -1,42 +1,31 @@
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.beans.NamedArg;
import javafx.event.EventHandler;
import javafx.scene.Node;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.SequencedMap;
import java.util.Set;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static java.util.Objects.requireNonNull;
import static com.github.gtache.fxml.compiler.impl.ReflectionHelper.*;
/**
* Implementation of {@link Generator}
*/
public class GeneratorImpl implements Generator {
private static final Map<Class<?>, Boolean> HAS_VALUE_OF = new ConcurrentHashMap<>();
private static final Map<Class<?>, Boolean> IS_GENERIC = new ConcurrentHashMap<>();
private static final Map<Class<?>, Map<String, Method>> METHODS = new ConcurrentHashMap<>();
private static final Map<Class<?>, Map<String, Method>> STATIC_METHODS = new ConcurrentHashMap<>();
private 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;
@@ -50,7 +39,7 @@ public class GeneratorImpl implements Generator {
}
@Override
public String generate(final GenerationRequest request) {
public String generate(final GenerationRequest request) throws GenerationException {
controllerFactoryPostAction.clear();
variableNameCounters.clear();
final var className = request.outputClassName();
@@ -119,6 +108,11 @@ public class GeneratorImpl implements Generator {
this.resourceBundlesMap = Map.copyOf(resourceBundlesMap);
}
/**
* Loads the view. Can only be called once.
*
* @return The view parent
*/
%6$s
%10$s
@@ -144,7 +138,7 @@ public class GeneratorImpl implements Generator {
* @param request The generation request
* @return The helper methods
*/
private static String getHelperMethods(final GenerationRequest request) {
private static String getHelperMethods(final GenerationRequest request) throws GenerationException {
final var injection = getControllerInjection(request);
final var methodInjectionType = injection.methodInjectionType();
final var sb = new StringBuilder();
@@ -207,7 +201,7 @@ public class GeneratorImpl implements Generator {
* @param request The generation request
* @return The imports
*/
private static String getImports(final GenerationRequest request) {
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");
@@ -230,12 +224,12 @@ public class GeneratorImpl implements Generator {
* @param request The generation request
* @return The load method
*/
private String getLoadMethod(final GenerationRequest request) {
private String getLoadMethod(final GenerationRequest request) throws GenerationException {
final var rootObject = request.rootObject();
final var controllerInjection = getControllerInjection(request);
final var controllerInjectionType = controllerInjection.fieldInjectionType();
final var controllerClass = controllerInjection.injectionClass();
final var sb = new StringBuilder("public javafx.scene.Parent load() {\n");
final var sb = new StringBuilder("public <T> T load() {\n");
sb.append(" if (loaded) {\n");
sb.append(" throw new IllegalStateException(\"Already loaded\");\n");
sb.append(" }\n");
@@ -283,44 +277,52 @@ public class GeneratorImpl implements Generator {
* @param variableName The variable name for the object
* @param sb The string builder
*/
private void format(final GenerationRequest request, final ParsedObject parsedObject, final String variableName, final StringBuilder sb) {
if (parsedObject instanceof final ParsedInclude include) {
formatInclude(request, include, variableName, sb);
} else {
final var clazz = parsedObject.clazz();
final var constructors = clazz.getConstructors();
final var allPropertyNames = new HashSet<>(parsedObject.properties().keySet());
allPropertyNames.addAll(parsedObject.children().keySet().stream().map(ParsedProperty::name).collect(Collectors.toSet()));
final var constructorArgs = getMatchingConstructorArgs(constructors, allPropertyNames);
if (constructorArgs == null) {
if (allPropertyNames.size() == 1 && allPropertyNames.iterator().next().equals("fx:constant")) {
final var property = parsedObject.properties().get("fx:constant");
sb.append(" final var ").append(variableName).append(" = ").append(clazz.getCanonicalName()).append(".").append(property.value()).append(";\n");
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 {
throw new IllegalStateException("Cannot find constructor for " + clazz.getCanonicalName());
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);
}
}
}
} else {
final var args = getListConstructorArgs(constructorArgs, parsedObject);
final var genericTypes = getGenericTypes(request, parsedObject);
sb.append(" final var ").append(variableName).append(" = new ").append(clazz.getCanonicalName())
.append(genericTypes).append("(").append(String.join(", ", args)).append(");\n");
parsedObject.properties().entrySet().stream().filter(e -> !constructorArgs.namedArgs().containsKey(e.getKey())).forEach(e -> {
final var p = e.getValue();
formatProperty(request, p, parsedObject, variableName, sb);
});
parsedObject.children().entrySet().stream().filter(e -> !constructorArgs.namedArgs().containsKey(e.getKey().name())).forEach(e -> {
final var p = e.getKey();
final var o = e.getValue();
formatChild(request, parsedObject, p, o, variableName, sb);
});
}
}
}
private static String getGenericTypes(final GenerationRequest request, final ParsedObject parsedObject) {
final var clazz = parsedObject.clazz();
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.properties().get("fx:id");
final var idProperty = parsedObject.attributes().get("fx:id");
if (idProperty == null) {
return "<>";
} else {
@@ -336,16 +338,6 @@ public class GeneratorImpl implements Generator {
return "";
}
/**
* Checks if the given class is generic
* The result is cached
*
* @param clazz The class
* @return True if the class is generic
*/
private static boolean isGeneric(final Class<?> clazz) {
return IS_GENERIC.computeIfAbsent(clazz, c -> c.getTypeParameters().length > 0);
}
/**
* Formats an include object
@@ -355,14 +347,14 @@ public class GeneratorImpl implements Generator {
* @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) {
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 IllegalArgumentException("Unknown include source : " + source);
throw new GenerationException("Unknown include source : " + source);
}
if (resources == null) {
sb.append(" final var ").append(subViewVariable).append(" = new ").append(subClassName).append("(controllersMap, resourceBundlesMap);\n");
@@ -383,6 +375,17 @@ public class GeneratorImpl implements Generator {
}
}
/**
* 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
*
@@ -392,7 +395,7 @@ public class GeneratorImpl implements Generator {
* @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) {
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")) {
@@ -400,40 +403,42 @@ public class GeneratorImpl implements Generator {
injectControllerField(request, id, parentVariable, sb);
} else if (propertyName.equals("fx:controller")) {
if (parent != request.rootObject()) {
throw new IllegalStateException("Invalid nested controller");
throw new GenerationException("Invalid nested controller");
}
} else if (property.sourceType() == EventHandler.class) {
} else if (Objects.equals(property.sourceType(), EventHandler.class.getName())) {
injectControllerMethod(request, property, parentVariable, sb);
} else if (property.sourceType() != null) {
if (hasStaticMethod(property.sourceType(), setMethod)) {
final var method = getStaticMethod(property.sourceType(), setMethod);
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().getName() + "." + setMethod + "(" + parentVariable + ", " + arg + ");\n", sb);
setLaterIfNeeded(request, property, parameterType, " " + property.sourceType() + "." + setMethod + "(" + parentVariable + ", " + arg + ");\n", sb);
} else {
throw new IllegalStateException("Cannot set " + propertyName + " on " + property.sourceType().getCanonicalName());
throw new GenerationException("Cannot set " + propertyName + " on " + property.sourceType());
}
} else {
final var getMethod = getGetMethod(propertyName);
if (hasMethod(parent.clazz(), setMethod)) {
final var method = getMethod(parent.clazz(), setMethod);
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(parent.clazz(), getMethod)) {
final var method = getMethod(parent.clazz(), getMethod);
} 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 IllegalStateException("Cannot set " + propertyName + " on " + parent.clazz().getCanonicalName());
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) {
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);
@@ -450,7 +455,7 @@ public class GeneratorImpl implements Generator {
* @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) {
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) {
@@ -459,7 +464,7 @@ public class GeneratorImpl implements Generator {
case ASSIGN, SETTERS, REFLECTION -> sb.append(methodInjection);
}
} else {
throw new IllegalArgumentException("Unknown injection type : " + injection.fieldInjectionType());
throw new GenerationException("Unknown injection type : " + injection.fieldInjectionType());
}
}
@@ -472,7 +477,7 @@ public class GeneratorImpl implements Generator {
* @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) {
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("#", "");
@@ -490,7 +495,7 @@ public class GeneratorImpl implements Generator {
" " + parentVariable + "." + setMethod + "(e -> callMethod(\"" + controllerMethod + "\", e));\n";
};
} else {
throw new IllegalArgumentException("Unknown injection type : " + injection.methodInjectionType());
throw new GenerationException("Unknown injection type : " + injection.methodInjectionType());
}
}
@@ -502,7 +507,7 @@ public class GeneratorImpl implements Generator {
* @param parameterType The parameter type
* @return The formatted value
*/
private static String getArg(final GenerationRequest request, final String value, final Class<?> parameterType) {
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 {
@@ -518,7 +523,7 @@ public class GeneratorImpl implements Generator {
* @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) {
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) {
@@ -530,12 +535,11 @@ public class GeneratorImpl implements Generator {
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");
}
case REFLECTION ->
sb.append(" injectField(\"").append(id).append("\", ").append(variable).append(");\n");
}
} else {
throw new IllegalArgumentException("Unknown controller injection type : " + controllerInjectionType);
throw new GenerationException("Unknown controller injection type : " + controllerInjectionType);
}
}
@@ -545,10 +549,10 @@ public class GeneratorImpl implements Generator {
* @param request The generation request
* @return The controller injection
*/
private static ControllerInjection getControllerInjection(final GenerationRequest request) {
final var property = request.rootObject().properties().get("fx:controller");
private static ControllerInjection getControllerInjection(final GenerationRequest request) throws GenerationException {
final var property = request.rootObject().attributes().get("fx:controller");
if (property == null) {
throw new IllegalArgumentException("Root object must have a controller property");
throw new GenerationException("Root object must have a controller property");
} else {
final var id = property.value();
return request.parameters().controllerInjections().get(id);
@@ -566,13 +570,14 @@ public class GeneratorImpl implements Generator {
* @param sb The string builder
*/
private void formatChild(final GenerationRequest request, final ParsedObject parent, final ParsedProperty property,
final Collection<? extends ParsedObject> objects, final String parentVariable, final StringBuilder sb) {
final Collection<? extends ParsedObject> objects, final String parentVariable, final StringBuilder sb) throws GenerationException {
final var propertyName = property.name();
final var variables = objects.stream().map(go -> {
final var variables = new ArrayList<String>();
for (final var object : objects) {
final var vn = getNextVariableName("object");
format(request, go, vn, sb);
return vn;
}).toList();
format(request, object, vn, sb);
variables.add(vn);
}
if (variables.size() > 1) {
formatMultipleChildren(variables, propertyName, parent, parentVariable, sb);
} else if (variables.size() == 1) {
@@ -591,10 +596,12 @@ public class GeneratorImpl implements Generator {
* @param sb The string builder
*/
private static void formatMultipleChildren(final Iterable<String> variables, final String propertyName, final ParsedObject parent,
final String parentVariable, final StringBuilder sb) {
final String parentVariable, final StringBuilder sb) throws GenerationException {
final var getMethod = getGetMethod(propertyName);
if (hasMethod(parent.clazz(), getMethod)) {
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());
}
}
@@ -608,7 +615,7 @@ public class GeneratorImpl implements Generator {
* @param sb The string builder
*/
private static void formatSingleChild(final String variableName, final ParsedProperty property, final ParsedObject parent,
final String parentVariable, final StringBuilder sb) {
final String parentVariable, final StringBuilder sb) throws GenerationException {
if (property.sourceType() == null) {
formatSingleChildInstance(variableName, property, parent, parentVariable, sb);
} else {
@@ -626,16 +633,17 @@ public class GeneratorImpl implements Generator {
* @param sb The string builder
*/
private static void formatSingleChildInstance(final String variableName, final ParsedProperty property, final ParsedObject parent,
final String parentVariable, final StringBuilder sb) {
final String parentVariable, final StringBuilder sb) throws GenerationException {
final var setMethod = getSetMethod(property);
final var getMethod = getGetMethod(property);
if (hasMethod(parent.clazz(), setMethod)) {
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(parent.clazz(), getMethod)) {
} 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 IllegalStateException("Cannot set " + property.name() + " on " + parent.clazz().getCanonicalName());
throw new GenerationException("Cannot set " + property.name() + " on " + parent.className());
}
}
@@ -647,12 +655,12 @@ public class GeneratorImpl implements Generator {
* @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) {
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(property.sourceType(), setMethod)) {
sb.append(" ").append(property.sourceType().getName()).append(".").append(setMethod).append("(").append(parentVariable).append(", ").append(variableName).append(");\n");
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 IllegalStateException("Cannot set " + property.name() + " on " + property.sourceType().getCanonicalName());
throw new GenerationException("Cannot set " + property.name() + " on " + property.sourceType());
}
}
@@ -696,116 +704,8 @@ public class GeneratorImpl implements Generator {
return "set" + propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1);
}
/**
* Checks if the given class has a method with the given name
* The result is cached
*
* @param clazz The class
* @param methodName The method name
* @return True if the class has a method with the given name
*/
private static boolean hasMethod(final Class<?> clazz, final String methodName) {
final var methodMap = METHODS.computeIfAbsent(clazz, c -> new ConcurrentHashMap<>());
final var method = methodMap.computeIfAbsent(methodName, m -> computeMethod(clazz, m));
return method != null;
}
/**
* Gets the method corresponding to the given class and name
* The result is cached
*
* @param clazz The class
* @param methodName The method name
* @return The method
*/
private static Method getMethod(final Class<?> clazz, final String methodName) {
final var methodMap = METHODS.computeIfAbsent(clazz, c -> new ConcurrentHashMap<>());
return methodMap.computeIfAbsent(methodName, m -> computeMethod(clazz, m));
}
/**
* Checks if the given class has a method with the given name
*
* @param clazz The class
* @param methodName The method name
* @return True if the class has a method with the given name
*/
private static Method computeMethod(final Class<?> clazz, final String methodName) {
final var matching = Arrays.stream(clazz.getMethods()).filter(m -> {
if (m.getName().equals(methodName) && !Modifier.isStatic(m.getModifiers())) {
final var parameterTypes = m.getParameterTypes();
return methodName.startsWith("get") ? parameterTypes.length == 0 : parameterTypes.length >= 1; //TODO not very clean
} else {
return false;
}
}).toList();
if (matching.size() > 1) {
final var varargsFilter = matching.stream().filter(Method::isVarArgs).toList();
if (varargsFilter.size() == 1) {
return varargsFilter.getFirst();
} else {
throw new UnsupportedOperationException("Multiple matching methods not supported yet : " + clazz + " - " + methodName);
}
} else if (matching.size() == 1) {
return matching.getFirst();
} else {
return null;
}
}
/**
* Checks if the given class has a static method with the given name
* The result is cached
*
* @param clazz The class
* @param methodName The method name
* @return True if the class has a static method with the given name
*/
private static boolean hasStaticMethod(final Class<?> clazz, final String methodName) {
final var methodMap = STATIC_METHODS.computeIfAbsent(clazz, c -> new ConcurrentHashMap<>());
final var method = methodMap.computeIfAbsent(methodName, m -> computeStaticMethod(clazz, m));
return method != null;
}
/**
* Gets the static method corresponding to the given class and name
* The result is cached
*
* @param clazz The class
* @param methodName The method name
* @return The method
*/
private static Method getStaticMethod(final Class<?> clazz, final String methodName) {
final var methodMap = STATIC_METHODS.computeIfAbsent(clazz, c -> new ConcurrentHashMap<>());
return methodMap.computeIfAbsent(methodName, m -> computeStaticMethod(clazz, m));
}
/**
* Gets the static method corresponding to the given class and name
*
* @param clazz The class name
* @param methodName The method name
* @return The method, or null if not found
*/
private static Method computeStaticMethod(final Class<?> clazz, final String methodName) {
final var matching = Arrays.stream(clazz.getMethods()).filter(m -> {
if (m.getName().equals(methodName) && Modifier.isStatic(m.getModifiers())) {
final var parameterTypes = m.getParameterTypes();
return parameterTypes.length > 1 && parameterTypes[0] == Node.class;
} else {
return false;
}
}).toList();
if (matching.size() > 1) {
throw new UnsupportedOperationException("Multiple matching methods not supported yet : " + clazz + " - " + methodName);
} else if (matching.size() == 1) {
return matching.getFirst();
} else {
return null;
}
}
private static String getBundleValue(final GenerationRequest request, final String value) {
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) {
@@ -813,7 +713,7 @@ public class GeneratorImpl implements Generator {
case GETTER -> "controller.resources().getString(\"" + value + "\")";
};
} else {
throw new IllegalArgumentException("Unknown resource bundle injection type : " + resourceBundleInjectionType);
throw new GenerationException("Unknown resource bundle injection type : " + resourceBundleInjectionType);
}
}
@@ -824,18 +724,18 @@ public class GeneratorImpl implements Generator {
* @param parsedObject The parsed object
* @return The list of constructor arguments
*/
private static List<String> getListConstructorArgs(final ConstructorArgs constructorArgs, final ParsedObject parsedObject) {
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.properties().get(entry.getKey());
final var p = parsedObject.attributes().get(entry.getKey());
if (p == null) {
final var c = parsedObject.children().entrySet().stream().filter(e ->
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 UnsupportedOperationException("Constructor using complex property not supported yet");
throw new GenerationException("Constructor using complex property not supported yet");
}
} else {
args.add(toString(p.value(), type));
@@ -878,47 +778,6 @@ public class GeneratorImpl implements Generator {
return constructorArgs.namedArgs().keySet().stream().filter(allPropertyNames::contains).count();
}
/**
* Computes the constructor arguments for the given constructor
*
* @param constructor The constructor
* @return The constructor arguments
*/
private static ConstructorArgs getConstructorArgs(final Constructor<?> constructor) {
final var namedArgs = new LinkedHashMap<String, Parameter>();
final var annotationsArray = constructor.getParameterAnnotations();
for (var i = 0; i < annotationsArray.length; i++) {
final var annotations = annotationsArray[i];
final var getNamedArg = Arrays.stream(annotations).filter(NamedArg.class::isInstance).findFirst().orElse(null);
if (getNamedArg != null) {
final var namedArg = (NamedArg) getNamedArg;
final var name = namedArg.value();
final var clazz = constructor.getParameterTypes()[i];
namedArgs.put(name, new Parameter(name, constructor.getParameterTypes()[i], namedArg.defaultValue().isEmpty() ?
getDefaultValue(clazz) : namedArg.defaultValue()));
}
}
return new ConstructorArgs(constructor, namedArgs);
}
/**
* Computes the default value for the given class
*
* @param clazz The class
* @return The value
*/
private static String getDefaultValue(final Class<?> clazz) {
final var primitiveWrappers = Set.of(Integer.class, Byte.class, Short.class, Long.class, Float.class, Double.class);
if (clazz == char.class || clazz == Character.class) {
return "\u0000";
} else if (clazz == boolean.class || clazz == Boolean.class) {
return "false";
} else if (clazz.isPrimitive() || primitiveWrappers.contains(clazz)) {
return "0";
} else {
return "null";
}
}
/**
* Computes the string value to use in the generated code
@@ -928,13 +787,25 @@ public class GeneratorImpl implements Generator {
* @return The computed string value
*/
private static String toString(final String value, final Class<?> clazz) {
final var primitiveWrappers = Set.of(Integer.class, Byte.class, Short.class, Long.class, Float.class, Double.class, Boolean.class);
if (clazz == String.class) {
return "\"" + value.replace("\"", "\\\"") + "\"";
} else if (clazz == char.class || clazz == Character.class) {
return "'" + value + "'";
} else if (clazz.isPrimitive() || primitiveWrappers.contains(clazz)) {
} 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;
@@ -946,29 +817,12 @@ public class GeneratorImpl implements Generator {
}
}
/**
* Checks if the given class has a valueOf(String) method
* The result is cached
*
* @param clazz The class
* @return True if the class has a valueOf(String)
*/
private static boolean hasValueOf(final Class<?> clazz) {
return HAS_VALUE_OF.computeIfAbsent(clazz, GeneratorImpl::computeHasValueOf);
}
/**
* Computes if the given class has a valueOf(String) method
*
* @param clazz The class
* @return True if the class has a valueOf(String)
*/
private static boolean computeHasValueOf(final Class<?> clazz) {
try {
clazz.getMethod("valueOf", String.class);
return true;
} catch (final NoSuchMethodException ignored) {
return false;
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);
}
}
@@ -983,20 +837,11 @@ public class GeneratorImpl implements Generator {
return prefix + counter.getAndIncrement();
}
private record ConstructorArgs(Constructor<?> constructor,
SequencedMap<String, GeneratorImpl.Parameter> namedArgs) {
private ConstructorArgs {
requireNonNull(constructor);
namedArgs = new LinkedHashMap<>(namedArgs);
}
}
private record Parameter(String name, Class<?> type, String defaultValue) {
private Parameter {
requireNonNull(name);
requireNonNull(type);
requireNonNull(defaultValue);
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

@@ -0,0 +1,19 @@
package com.github.gtache.fxml.compiler.impl;
import static java.util.Objects.requireNonNull;
/**
* Used by {@link ConstructorArgs} to store the constructor arguments
*
* @param name The parameter name
* @param type The parameter type
* @param defaultValue The parameter default value
*/
record Parameter(String name, Class<?> type, String defaultValue) {
Parameter {
requireNonNull(name);
requireNonNull(type);
requireNonNull(defaultValue);
}
}

View File

@@ -0,0 +1,220 @@
package com.github.gtache.fxml.compiler.impl;
import javafx.beans.NamedArg;
import javafx.scene.Node;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
* 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<Class<?>, Map<String, Method>> METHODS = new ConcurrentHashMap<>();
private static final Map<Class<?>, Map<String, Method>> STATIC_METHODS = new ConcurrentHashMap<>();
private ReflectionHelper() {
}
/**
* Checks if the given class is generic
* The result is cached
*
* @param clazz The class
* @return True if the class is generic
*/
static boolean isGeneric(final Class<?> clazz) {
return IS_GENERIC.computeIfAbsent(clazz, c -> c.getTypeParameters().length > 0);
}
/**
* Checks if the given class has a method with the given name
* The result is cached
*
* @param clazz The class
* @param methodName The method name
* @return True if the class has a method with the given name
*/
static boolean hasMethod(final Class<?> clazz, final String methodName) {
final var methodMap = METHODS.computeIfAbsent(clazz, c -> new ConcurrentHashMap<>());
final var method = methodMap.computeIfAbsent(methodName, m -> computeMethod(clazz, m));
return method != null;
}
/**
* Gets the method corresponding to the given class and name
* The result is cached
*
* @param clazz The class
* @param methodName The method name
* @return The method
*/
static Method getMethod(final Class<?> clazz, final String methodName) {
final var methodMap = METHODS.computeIfAbsent(clazz, c -> new ConcurrentHashMap<>());
return methodMap.computeIfAbsent(methodName, m -> computeMethod(clazz, m));
}
/**
* Checks if the given class has a method with the given name
*
* @param clazz The class
* @param methodName The method name
* @return True if the class has a method with the given name
*/
private static Method computeMethod(final Class<?> clazz, final String methodName) {
final var matching = Arrays.stream(clazz.getMethods()).filter(m -> {
if (m.getName().equals(methodName) && !Modifier.isStatic(m.getModifiers())) {
final var parameterTypes = m.getParameterTypes();
return methodName.startsWith("get") ? parameterTypes.length == 0 : parameterTypes.length >= 1; //TODO not very clean
} else {
return false;
}
}).toList();
if (matching.size() > 1) {
final var varargsFilter = matching.stream().filter(Method::isVarArgs).toList();
if (varargsFilter.size() == 1) {
return varargsFilter.getFirst();
} else {
throw new UnsupportedOperationException("Multiple matching methods not supported yet : " + clazz + " - " + methodName);
}
} else if (matching.size() == 1) {
return matching.getFirst();
} else {
return null;
}
}
/**
* Checks if the given class has a static method with the given name
* The result is cached
*
* @param clazz The class
* @param methodName The method name
* @return True if the class has a static method with the given name
*/
static boolean hasStaticMethod(final Class<?> clazz, final String methodName) {
final var methodMap = STATIC_METHODS.computeIfAbsent(clazz, c -> new ConcurrentHashMap<>());
final var method = methodMap.computeIfAbsent(methodName, m -> computeStaticMethod(clazz, m));
return method != null;
}
/**
* Gets the static method corresponding to the given class and name
* The result is cached
*
* @param clazz The class
* @param methodName The method name
* @return The method
*/
static Method getStaticMethod(final Class<?> clazz, final String methodName) {
final var methodMap = STATIC_METHODS.computeIfAbsent(clazz, c -> new ConcurrentHashMap<>());
return methodMap.computeIfAbsent(methodName, m -> computeStaticMethod(clazz, m));
}
/**
* Gets the static method corresponding to the given class and name
*
* @param clazz The class name
* @param methodName The method name
* @return The method, or null if not found
*/
private static Method computeStaticMethod(final Class<?> clazz, final String methodName) {
final var matching = Arrays.stream(clazz.getMethods()).filter(m -> {
if (m.getName().equals(methodName) && Modifier.isStatic(m.getModifiers())) {
final var parameterTypes = m.getParameterTypes();
return parameterTypes.length > 1 && parameterTypes[0] == Node.class;
} else {
return false;
}
}).toList();
if (matching.size() > 1) {
throw new UnsupportedOperationException("Multiple matching methods not supported yet : " + clazz + " - " + methodName);
} else if (matching.size() == 1) {
return matching.getFirst();
} else {
return null;
}
}
/**
* Checks if the given class has a valueOf(String) method
* The result is cached
*
* @param clazz The class
* @return True if the class has a valueOf(String)
*/
static boolean hasValueOf(final Class<?> clazz) {
return HAS_VALUE_OF.computeIfAbsent(clazz, ReflectionHelper::computeHasValueOf);
}
/**
* Computes if the given class has a valueOf(String) method
*
* @param clazz The class
* @return True if the class has a valueOf(String)
*/
private static boolean computeHasValueOf(final Class<?> clazz) {
try {
clazz.getMethod("valueOf", String.class);
return true;
} catch (final NoSuchMethodException ignored) {
return false;
}
}
/**
* Computes the constructor arguments for the given constructor
*
* @param constructor The constructor
* @return The constructor arguments
*/
static ConstructorArgs getConstructorArgs(final Constructor<?> constructor) {
final var namedArgs = new LinkedHashMap<String, Parameter>();
final var annotationsArray = constructor.getParameterAnnotations();
var hasNamedArgs = 0;
for (var i = 0; i < annotationsArray.length; i++) {
final var annotations = annotationsArray[i];
final var getNamedArg = Arrays.stream(annotations).filter(NamedArg.class::isInstance).findFirst().orElse(null);
if (getNamedArg != null) {
hasNamedArgs++;
final var namedArg = (NamedArg) getNamedArg;
final var name = namedArg.value();
final var clazz = constructor.getParameterTypes()[i];
namedArgs.put(name, new Parameter(name, constructor.getParameterTypes()[i], namedArg.defaultValue().isEmpty() ?
getDefaultValue(clazz) : namedArg.defaultValue()));
}
}
if (hasNamedArgs != 0 && hasNamedArgs != annotationsArray.length) {
throw new IllegalStateException("Constructor " + constructor + " has both named and unnamed arguments");
} else {
return new ConstructorArgs(constructor, namedArgs);
}
}
/**
* Computes the default value for the given class
*
* @param clazz The class
* @return The value
*/
private static String getDefaultValue(final Class<?> clazz) {
final var primitiveWrappers = Set.of(Integer.class, Byte.class, Short.class, Long.class, Float.class, Double.class);
if (clazz == char.class || clazz == Character.class) {
return "\u0000";
} else if (clazz == boolean.class || clazz == Boolean.class) {
return "false";
} else if (clazz.isPrimitive() || primitiveWrappers.contains(clazz)) {
return "0";
} else {
return "null";
}
}
}

View File

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

View File

@@ -3,18 +3,16 @@ package com.github.gtache.fxml.compiler.parsing.impl;
import com.github.gtache.fxml.compiler.parsing.ParsedInclude;
import com.github.gtache.fxml.compiler.parsing.ParsedProperty;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.SequencedMap;
import java.util.Map;
/**
* Implementation of {@link ParsedInclude}
*
* @param properties The object properties
* @param attributes The object properties
*/
public record ParsedIncludeImpl(SequencedMap<String, ParsedProperty> properties) implements ParsedInclude {
public record ParsedIncludeImpl(Map<String, ParsedProperty> attributes) implements ParsedInclude {
public ParsedIncludeImpl {
properties = Collections.unmodifiableSequencedMap(new LinkedHashMap<>(properties));
attributes = Map.copyOf(attributes);
}
}

View File

@@ -5,7 +5,9 @@ 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.Map;
import java.util.Objects;
import java.util.SequencedCollection;
import java.util.SequencedMap;
@@ -13,17 +15,17 @@ import java.util.SequencedMap;
/**
* Implementation of {@link ParsedObject}
*
* @param clazz The object class
* @param properties The object properties
* @param children The object children (complex properties)
* @param className The object class
* @param attributes The object properties
* @param properties The object children (complex properties)
*/
public record ParsedObjectImpl(Class<?> clazz, SequencedMap<String, ParsedProperty> properties,
SequencedMap<ParsedProperty, SequencedCollection<ParsedObject>> children) implements ParsedObject {
public record ParsedObjectImpl(String className, Map<String, ParsedProperty> attributes,
SequencedMap<ParsedProperty, SequencedCollection<ParsedObject>> properties) implements ParsedObject {
public ParsedObjectImpl {
Objects.requireNonNull(clazz);
Objects.requireNonNull(className);
attributes = Map.copyOf(attributes);
properties = Collections.unmodifiableSequencedMap(new LinkedHashMap<>(properties));
children = Collections.unmodifiableSequencedMap(new LinkedHashMap<>(children));
}
/**
@@ -31,26 +33,37 @@ public record ParsedObjectImpl(Class<?> clazz, SequencedMap<String, ParsedProper
*/
public static class Builder {
private Class<?> clazz;
private final SequencedMap<String, ParsedProperty> properties;
private final SequencedMap<ParsedProperty, SequencedCollection<ParsedObject>> children;
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<>();
this.children = new LinkedHashMap<>();
}
/**
* Sets the object class
*
* @param clazz The object class
* @param className The object class
* @return The builder
*/
public Builder clazz(final Class<?> clazz) {
this.clazz = clazz;
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;
}
@@ -58,24 +71,13 @@ public record ParsedObjectImpl(Class<?> clazz, SequencedMap<String, ParsedProper
* Adds a property
*
* @param property The property
* @param child The property element
* @return The builder
*/
public Builder addProperty(final ParsedProperty property) {
properties.put(property.name(), property);
return this;
}
/**
* Adds a child
*
* @param property The property
* @param child The child
* @return The builder
*/
public Builder addChild(final ParsedProperty property, final ParsedObject child) {
final var sequence = children.computeIfAbsent(property, k -> new ArrayList<>());
public Builder addProperty(final ParsedProperty property, final ParsedObject child) {
final var sequence = properties.computeIfAbsent(property, k -> new ArrayList<>());
sequence.add(child);
children.put(property, sequence);
properties.put(property, sequence);
return this;
}
@@ -85,7 +87,7 @@ public record ParsedObjectImpl(Class<?> clazz, SequencedMap<String, ParsedProper
* @return The object
*/
public ParsedObjectImpl build() {
return new ParsedObjectImpl(clazz, properties, children);
return new ParsedObjectImpl(className, attributes, properties);
}
}
}

View File

@@ -11,7 +11,7 @@ import java.util.Objects;
* @param sourceType The property source type
* @param value The property value
*/
public record ParsedPropertyImpl(String name, Class<?> sourceType, String value) implements ParsedProperty {
public record ParsedPropertyImpl(String name, String sourceType, String value) implements ParsedProperty {
public ParsedPropertyImpl {
Objects.requireNonNull(name);

View File

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

View File

@@ -0,0 +1,35 @@
package com.github.gtache.fxml.compiler.impl;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.assertEquals;
class TestClassesFinder {
@Test
void testGetClassesCurrent() throws IOException {
final var expected = Set.of(
"com.github.gtache.fxml.compiler.parsing.impl.TestParsedConstantImpl",
"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");
final var actual = ClassesFinder.getClasses("com.github.gtache.fxml.compiler.parsing.impl");
assertEquals(expected, actual);
}
@Test
void testGetClassesJar() throws IOException {
final var expected = Set.of("javafx.beans.DefaultProperty",
"javafx.beans.InvalidationListener",
"javafx.beans.NamedArg",
"javafx.beans.Observable",
"javafx.beans.WeakInvalidationListener",
"javafx.beans.WeakListener");
final var actual = ClassesFinder.getClasses("javafx.beans");
assertEquals(expected, actual);
}
}

View File

@@ -0,0 +1,55 @@
package com.github.gtache.fxml.compiler.impl;
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.lang.reflect.Constructor;
import java.util.LinkedHashMap;
import java.util.Objects;
import java.util.SequencedMap;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
@ExtendWith(MockitoExtension.class)
class TestConstructorArgs {
private final Constructor<?> constructor;
private final SequencedMap<String, Parameter> namedArgs;
private final ConstructorArgs constructorArgs;
TestConstructorArgs(@Mock final Constructor<?> constructor, @Mock final Parameter parameter1, @Mock final Parameter parameter2) {
this.constructor = Objects.requireNonNull(constructor);
this.namedArgs = new LinkedHashMap<>();
namedArgs.put("p1", parameter1);
namedArgs.put("p2", parameter2);
this.constructorArgs = new ConstructorArgs(constructor, namedArgs);
}
@Test
void testGetters() {
assertEquals(constructor, constructorArgs.constructor());
assertEquals(namedArgs, constructorArgs.namedArgs());
}
@Test
void testCopy() {
final var original = constructorArgs.namedArgs();
namedArgs.put("p3", null);
assertEquals(original, constructorArgs.namedArgs());
}
@Test
void testUnmodifiable() {
final var map = constructorArgs.namedArgs();
assertThrows(UnsupportedOperationException.class, map::clear);
}
@Test
void testIllegal() {
assertThrows(NullPointerException.class, () -> new ConstructorArgs(null, namedArgs));
assertThrows(NullPointerException.class, () -> new ConstructorArgs(constructor, null));
}
}

View File

@@ -0,0 +1,35 @@
package com.github.gtache.fxml.compiler.impl;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
class TestParameter {
private final String name;
private final Class<?> type;
private final String defaultValue;
private final Parameter parameter;
TestParameter() {
this.name = "name";
this.type = Object.class;
this.defaultValue = "default";
this.parameter = new Parameter(name, type, defaultValue);
}
@Test
void testGetters() {
assertEquals(name, parameter.name());
assertEquals(type, parameter.type());
assertEquals(defaultValue, parameter.defaultValue());
}
@Test
void testIllegal() {
assertThrows(NullPointerException.class, () -> new Parameter(null, type, defaultValue));
assertThrows(NullPointerException.class, () -> new Parameter(name, null, defaultValue));
assertThrows(NullPointerException.class, () -> new Parameter(name, type, null));
}
}

View File

@@ -0,0 +1,113 @@
package com.github.gtache.fxml.compiler.impl;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.ComboBox;
import javafx.scene.control.TableCell;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.LinkedHashMap;
import static org.junit.jupiter.api.Assertions.*;
class TestReflectionHelper {
@Test
void testIsGeneric() {
assertFalse(ReflectionHelper.isGeneric(String.class));
assertTrue(ReflectionHelper.isGeneric(ComboBox.class));
assertTrue(ReflectionHelper.isGeneric(TableCell.class));
}
@Test
void testHasMethod() {
assertFalse(ReflectionHelper.hasMethod(String.class, "bla"));
assertTrue(ReflectionHelper.hasMethod(String.class, "charAt"));
assertTrue(ReflectionHelper.hasMethod(StackPane.class, "getChildren"));
}
@Test
void testGetMethod() throws NoSuchMethodException {
assertEquals(String.class.getMethod("charAt", int.class), ReflectionHelper.getMethod(String.class, "charAt"));
}
@Test
void testHasStaticMethod() {
assertTrue(ReflectionHelper.hasStaticMethod(HBox.class, "setHgrow"));
}
@Test
void testGetStaticMethod() throws NoSuchMethodException {
assertEquals(HBox.class.getMethod("setHgrow", Node.class, Priority.class), ReflectionHelper.getStaticMethod(HBox.class, "setHgrow"));
}
@Test
void testHasValueOf() {
assertTrue(ReflectionHelper.hasValueOf(Integer.class));
assertTrue(ReflectionHelper.hasValueOf(Pos.class));
assertFalse(ReflectionHelper.hasValueOf(HBox.class));
}
@Test
void testGetConstructorArgsNamedArgsDefault() {
final var parameters = new LinkedHashMap<String, Parameter>();
parameters.put("p1", new Parameter("p1", int.class, "0"));
parameters.put("p2", new Parameter("p2", Integer.class, "0"));
parameters.put("p3", new Parameter("p3", char.class, "\u0000"));
parameters.put("p4", new Parameter("p4", Character.class, "\u0000"));
parameters.put("p5", new Parameter("p5", boolean.class, "false"));
parameters.put("p6", new Parameter("p6", Boolean.class, "false"));
parameters.put("p7", new Parameter("p7", byte.class, "0"));
parameters.put("p8", new Parameter("p8", Byte.class, "0"));
parameters.put("p9", new Parameter("p9", short.class, "0"));
parameters.put("p10", new Parameter("p10", Short.class, "0"));
parameters.put("p11", new Parameter("p11", long.class, "0"));
parameters.put("p12", new Parameter("p12", Long.class, "0"));
parameters.put("p13", new Parameter("p13", float.class, "0"));
parameters.put("p14", new Parameter("p14", Float.class, "0"));
parameters.put("p15", new Parameter("p15", double.class, "0"));
parameters.put("p16", new Parameter("p16", Double.class, "0"));
parameters.put("p17", new Parameter("p17", String.class, "null"));
parameters.put("p18", new Parameter("p18", Object.class, "null"));
final var defaultConstructor = Arrays.stream(WholeConstructorArgs.class.getConstructors()).filter(c -> c.getParameterCount() == 18).findFirst().orElseThrow();
final var expected = new ConstructorArgs(defaultConstructor, parameters);
final var actual = ReflectionHelper.getConstructorArgs(defaultConstructor);
assertEquals(expected, actual);
}
@Test
void testGetConstructorArgsNamedArgs() {
final var parameters = new LinkedHashMap<String, Parameter>();
parameters.put("p1", new Parameter("p1", int.class, "1"));
parameters.put("p3", new Parameter("p3", char.class, "a"));
parameters.put("p5", new Parameter("p5", boolean.class, "true"));
parameters.put("p7", new Parameter("p7", byte.class, "2"));
parameters.put("p9", new Parameter("p9", short.class, "3"));
parameters.put("p11", new Parameter("p11", long.class, "4"));
parameters.put("p13", new Parameter("p13", float.class, "5.5"));
parameters.put("p15", new Parameter("p15", double.class, "6.6"));
parameters.put("p17", new Parameter("p17", String.class, "str"));
final var constructor = Arrays.stream(WholeConstructorArgs.class.getConstructors()).filter(c -> c.getParameterCount() == 9).findFirst().orElseThrow();
final var expected = new ConstructorArgs(constructor, parameters);
final var actual = ReflectionHelper.getConstructorArgs(constructor);
assertEquals(expected, actual);
}
@Test
void testGetConstructorArgsNoNamedArgs() {
final var constructor = Arrays.stream(WholeConstructorArgs.class.getConstructors()).filter(c -> c.getParameterCount() == 2).findFirst().orElseThrow();
final var expected = new ConstructorArgs(constructor, new LinkedHashMap<>());
final var actual = ReflectionHelper.getConstructorArgs(constructor);
assertEquals(expected, actual);
}
@Test
void testGetConstructorArgsMixed() {
final var constructor = Arrays.stream(WholeConstructorArgs.class.getConstructors()).filter(c -> c.getParameterCount() == 3).findFirst().orElseThrow();
assertThrows(IllegalStateException.class, () -> ReflectionHelper.getConstructorArgs(constructor));
}
}

View File

@@ -0,0 +1,35 @@
package com.github.gtache.fxml.compiler.impl;
import javafx.beans.NamedArg;
public class WholeConstructorArgs {
public WholeConstructorArgs(@NamedArg("p1") final int p1, @NamedArg("p2") final Integer p2,
@NamedArg("p3") final char p3, @NamedArg("p4") final Character p4,
@NamedArg("p5") final boolean p5, @NamedArg("p6") final Boolean p6,
@NamedArg("p7") final byte p7, @NamedArg("p8") final Byte p8,
@NamedArg("p9") final short p9, @NamedArg("p10") final Short p10,
@NamedArg("p11") final long p11, @NamedArg("p12") final Long p12,
@NamedArg("p13") final float p13, @NamedArg("p14") final Float p14,
@NamedArg("p15") final double p15, @NamedArg("p16") final Double p16,
@NamedArg("p17") final String p17, @NamedArg("p18") final Object p18) {
}
public WholeConstructorArgs(@NamedArg(value = "p1", defaultValue = "1") final int p1,
@NamedArg(value = "p3", defaultValue = "a") final char p3,
@NamedArg(value = "p5", defaultValue = "true") final boolean p5,
@NamedArg(value = "p7", defaultValue = "2") final byte p7,
@NamedArg(value = "p9", defaultValue = "3") final short p9,
@NamedArg(value = "p11", defaultValue = "4") final long p11,
@NamedArg(value = "p13", defaultValue = "5.5") final float p13,
@NamedArg(value = "p15", defaultValue = "6.6") final double p15,
@NamedArg(value = "p17", defaultValue = "str") final String p17) {
}
public WholeConstructorArgs(final int p1, final char p3) {
}
public WholeConstructorArgs(final int p1, @NamedArg("p3") final char p3, @NamedArg("p5") final boolean p5) {
}
}

View File

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

View File

@@ -26,22 +26,22 @@ class TestParsedIncludeImpl {
@Test
void testGetters() {
assertEquals(properties, include.properties());
assertEquals(ParsedInclude.class, include.clazz());
assertEquals(new LinkedHashMap<>(), include.children());
assertEquals(properties, include.attributes());
assertEquals(ParsedInclude.class.getName(), include.className());
assertEquals(new LinkedHashMap<>(), include.properties());
}
@Test
void testCopyMap() {
final var originalProperties = include.properties();
final var originalProperties = include.attributes();
properties.clear();
assertEquals(originalProperties, include.properties());
assertNotEquals(properties, include.properties());
assertEquals(originalProperties, include.attributes());
assertNotEquals(properties, include.attributes());
}
@Test
void testUnmodifiable() {
final var objectProperties = include.properties();
final var objectProperties = include.attributes();
assertThrows(UnsupportedOperationException.class, objectProperties::clear);
}

View File

@@ -18,13 +18,13 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
@ExtendWith(MockitoExtension.class)
class TestParsedObjectImpl {
private final Class<?> clazz;
private final String clazz;
private final SequencedMap<String, ParsedProperty> properties;
private final SequencedMap<ParsedProperty, SequencedCollection<ParsedObject>> children;
private final ParsedObject parsedObject;
TestParsedObjectImpl(@Mock final ParsedProperty property, @Mock final ParsedObject object) {
this.clazz = Object.class;
this.clazz = Object.class.getName();
this.properties = new LinkedHashMap<>();
this.properties.put("name", property);
this.children = new LinkedHashMap<>();
@@ -34,25 +34,25 @@ class TestParsedObjectImpl {
@Test
void testGetters() {
assertEquals(clazz, parsedObject.clazz());
assertEquals(properties, parsedObject.properties());
assertEquals(children, parsedObject.children());
assertEquals(clazz, parsedObject.className());
assertEquals(properties, parsedObject.attributes());
assertEquals(children, parsedObject.properties());
}
@Test
void testCopyMap() {
final var originalProperties = parsedObject.properties();
final var originalChildren = parsedObject.children();
final var originalProperties = parsedObject.attributes();
final var originalChildren = parsedObject.properties();
properties.clear();
children.clear();
assertEquals(originalProperties, parsedObject.properties());
assertEquals(originalChildren, parsedObject.children());
assertEquals(originalProperties, parsedObject.attributes());
assertEquals(originalChildren, parsedObject.properties());
}
@Test
void testUnmodifiable() {
final var objectProperties = parsedObject.properties();
final var objectChildren = parsedObject.children();
final var objectProperties = parsedObject.attributes();
final var objectChildren = parsedObject.properties();
assertThrows(UnsupportedOperationException.class, objectProperties::clear);
assertThrows(UnsupportedOperationException.class, objectChildren::clear);
}

View File

@@ -19,8 +19,8 @@ import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class TestParsedObjectImplBuilder {
private final Class<?> clazz1;
private final Class<?> clazz2;
private final String clazz1;
private final String clazz2;
private final ParsedProperty property1;
private final ParsedProperty property2;
private final ParsedObject object1;
@@ -29,8 +29,8 @@ class TestParsedObjectImplBuilder {
TestParsedObjectImplBuilder(@Mock final ParsedProperty property1, @Mock final ParsedProperty property2,
@Mock final ParsedObject object1, @Mock final ParsedObject object2) {
this.clazz1 = Object.class;
this.clazz2 = String.class;
this.clazz1 = Object.class.getName();
this.clazz2 = String.class.getName();
this.property1 = Objects.requireNonNull(property1);
this.property2 = Objects.requireNonNull(property2);
this.object1 = Objects.requireNonNull(object1);
@@ -50,59 +50,59 @@ class TestParsedObjectImplBuilder {
}
@Test
void testClazz() {
builder.clazz(clazz1);
void testClassName() {
builder.className(clazz1);
final var built = builder.build();
assertEquals(clazz1, built.clazz());
assertEquals(clazz1, built.className());
assertEquals(Map.of(), built.attributes());
assertEquals(Map.of(), built.properties());
assertEquals(Map.of(), built.children());
}
@Test
void testOverwriteClazz() {
builder.clazz(clazz1);
builder.clazz(clazz2);
void testOverwriteClassName() {
builder.className(clazz1);
builder.className(clazz2);
final var built = builder.build();
assertEquals(clazz2, built.clazz());
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());
assertEquals(Map.of(), built.children());
}
@Test
void testAddProperty() {
builder.clazz(clazz1);
builder.addProperty(property1);
builder.className(clazz1);
builder.addProperty(property1, object1);
final var built = builder.build();
assertEquals(Map.of(property1.name(), property1), built.properties());
assertEquals(Map.of(), built.children());
assertEquals(Map.of(), built.attributes());
assertEquals(Map.of(property1, List.of(object1)), built.properties());
}
@Test
void testAddMultipleProperties() {
builder.clazz(clazz1);
builder.addProperty(property1);
builder.addProperty(property2);
builder.className(clazz1);
builder.addProperty(property1, object1);
builder.addProperty(property2, object2);
final var built = builder.build();
assertEquals(Map.of(property1.name(), property1, property2.name(), property2), built.properties());
assertEquals(Map.of(), built.children());
}
@Test
void testAddChild() {
builder.clazz(clazz1);
builder.addChild(property1, object1);
final var built = builder.build();
assertEquals(Map.of(), built.properties());
assertEquals(Map.of(property1, List.of(object1)), built.children());
}
@Test
void testAddMultipleChildren() {
builder.clazz(clazz1);
builder.addChild(property1, object1);
builder.addChild(property2, object2);
final var built = builder.build();
assertEquals(Map.of(), built.properties());
assertEquals(Map.of(property1, List.of(object1), property2, List.of(object2)), built.children());
assertEquals(Map.of(), built.attributes());
assertEquals(Map.of(property1, List.of(object1), property2, List.of(object2)), built.properties());
}
}

View File

@@ -8,13 +8,13 @@ import static org.junit.jupiter.api.Assertions.*;
class TestParsedPropertyImpl {
private final String name;
private final Class<?> sourceType;
private final String sourceType;
private final String value;
private final ParsedProperty property;
TestParsedPropertyImpl() {
this.name = "name";
this.sourceType = Object.class;
this.sourceType = Object.class.getName();
this.value = "value";
this.property = new ParsedPropertyImpl(name, sourceType, value);
}