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

@@ -44,9 +44,18 @@
<dependencies>
<dependency>
<groupId>com.github.gtache</groupId>
<artifactId>fxml-compiler-loader</artifactId>
<artifactId>fxml-compiler-xml</artifactId>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-to-slf4j</artifactId>
</dependency>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-plugin-api</artifactId>

View File

@@ -0,0 +1,105 @@
package com.github.gtache.fxml.compiler.maven;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
/**
* Info about FXML file compilation
*
* @param inputFile The input file
* @param outputFile The output file
* @param outputClass The output class name
* @param controllerFile The controller file
* @param controllerClass The controller class name
* @param injectedFields The injected fields
* @param injectedMethods The injected methods
* @param includes The FXML inclusions
*/
record CompilationInfo(Path inputFile, Path outputFile, String outputClass, Path controllerFile,
String controllerClass,
Set<FieldInfo> injectedFields,
Set<String> injectedMethods, Map<String, Path> includes) {
CompilationInfo {
Objects.requireNonNull(inputFile);
Objects.requireNonNull(outputFile);
Objects.requireNonNull(outputClass);
Objects.requireNonNull(controllerFile);
injectedFields = Set.copyOf(injectedFields);
injectedMethods = Set.copyOf(injectedMethods);
includes = Map.copyOf(includes);
}
/**
* Builder for {@link CompilationInfo}
*/
static class Builder {
private Path inputFile;
private Path outputFile;
private String outputClass;
private Path controllerFile;
private String controllerClass;
private final Set<FieldInfo> injectedFields;
private final Set<String> injectedMethods;
private final Map<String, Path> includes;
Builder() {
this.injectedFields = new HashSet<>();
this.injectedMethods = new HashSet<>();
this.includes = new HashMap<>();
}
Path inputFile() {
return inputFile;
}
Builder inputFile(final Path inputFile) {
this.inputFile = inputFile;
return this;
}
Builder outputFile(final Path outputFile) {
this.outputFile = outputFile;
return this;
}
Builder outputClass(final String outputClassName) {
this.outputClass = outputClassName;
return this;
}
Builder controllerFile(final Path controllerFile) {
this.controllerFile = controllerFile;
return this;
}
Builder controllerClass(final String controllerClass) {
this.controllerClass = controllerClass;
return this;
}
Builder addInjectedField(final String field, final String type) {
injectedFields.add(new FieldInfo(type, field));
return this;
}
Builder addInjectedMethod(final String method) {
injectedMethods.add(method);
return this;
}
Builder addInclude(final String key, final Path value) {
this.includes.put(key, value);
return this;
}
CompilationInfo build() {
return new CompilationInfo(inputFile, outputFile, outputClass, controllerFile, controllerClass, injectedFields, injectedMethods, includes);
}
}
}

View File

@@ -0,0 +1,164 @@
package com.github.gtache.fxml.compiler.maven;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.logging.Log;
import org.apache.maven.project.MavenProject;
import org.w3c.dom.Node;
import org.xml.sax.SAXException;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Map;
import java.util.regex.Pattern;
import static java.util.Objects.requireNonNull;
/**
* Helper class for {@link FXMLCompilerMojo} to provides {@link CompilationInfo}
*/
class CompilationInfoProvider {
private static final DocumentBuilderFactory DOCUMENT_BUILDER_FACTORY = DocumentBuilderFactory.newInstance();
private static final Pattern START_DOT_PATTERN = Pattern.compile("^\\.");
private final MavenProject project;
private final Path outputDirectory;
private final Log logger;
CompilationInfoProvider(final MavenProject project, final Path outputDirectory, final Log logger) {
this.project = requireNonNull(project);
this.outputDirectory = requireNonNull(outputDirectory);
this.logger = requireNonNull(logger);
}
CompilationInfo getCompilationInfo(final Path root, final Path inputPath, final Map<? extends Path, String> controllerMapping) throws MojoExecutionException {
logger.info("Parsing " + inputPath);
try {
final var documentBuilder = DOCUMENT_BUILDER_FACTORY.newDocumentBuilder();
final var document = documentBuilder.parse(inputPath.toFile());
document.getDocumentElement().normalize();
final var builder = new CompilationInfo.Builder();
builder.inputFile(inputPath);
final var inputFilename = inputPath.getFileName().toString();
final var outputFilename = getOutputFilename(inputFilename);
final var outputClass = getOutputClass(root, inputPath, outputFilename);
final var replacedPrefixPath = inputPath.toString().replace(root.toString(), outputDirectory.toString());
final var targetPath = Paths.get(replacedPrefixPath.replace(inputFilename, outputFilename));
builder.outputFile(targetPath);
builder.outputClass(outputClass);
handleNode(document.getDocumentElement(), builder, controllerMapping);
logger.info(inputPath + " will be compiled to " + targetPath);
return builder.build();
} catch (final SAXException | IOException | ParserConfigurationException e) {
throw new MojoExecutionException("Error parsing fxml at " + inputPath, e);
}
}
private static String getOutputClass(final Path root, final Path inputPath, final String outputFilename) {
final var inputFilename = inputPath.getFileName().toString();
final var className = outputFilename.replace(".java", "");
final var replacedPrefixPath = inputPath.toString().replace(root.toString(), "").replace(inputFilename, className);
return START_DOT_PATTERN.matcher(replacedPrefixPath.replace(File.separator, ".")).replaceAll("");
}
private static String getOutputFilename(final CharSequence inputFilename) {
final var builder = new StringBuilder(inputFilename.length());
var nextUppercase = true;
for (var i = 0; i < inputFilename.length(); i++) {
final var c = inputFilename.charAt(i);
if (c == '-' || c == '_') {
nextUppercase = true;
} else if (nextUppercase) {
builder.append(Character.toUpperCase(c));
nextUppercase = false;
} else {
builder.append(c);
}
}
return builder.toString().replace(".fxml", ".java");
}
private void handleNode(final Node node, final CompilationInfo.Builder builder, final Map<? extends Path, String> controllerMapping) throws MojoExecutionException {
if (node.getNodeName().equals("fx:include")) {
handleInclude(node, builder);
}
handleAttributes(node, builder, controllerMapping);
handleChildren(node, builder, controllerMapping);
}
private void handleInclude(final Node node, final CompilationInfo.Builder builder) throws MojoExecutionException {
final var map = node.getAttributes();
if (map == null) {
throw new MojoExecutionException("Missing attributes for include");
} else {
final var sourceAttr = map.getNamedItem("source");
if (sourceAttr == null) {
throw new MojoExecutionException("Missing source for include");
} else {
final var source = sourceAttr.getNodeValue();
final var path = getRelativePath(builder.inputFile(), source);
logger.info("Found include " + source);
builder.addInclude(source, path);
}
}
}
private static Path getRelativePath(final Path base, final String relative) {
return base.getParent().resolve(relative).normalize();
}
private void handleChildren(final Node node, final CompilationInfo.Builder builder, final Map<? extends Path, String> controllerMapping) throws MojoExecutionException {
final var nl = node.getChildNodes();
for (var i = 0; i < nl.getLength(); i++) {
handleNode(nl.item(i), builder, controllerMapping);
}
}
private void handleAttributes(final Node node, final CompilationInfo.Builder builder, final Map<? extends Path, String> controllerMapping) throws MojoExecutionException {
final var map = node.getAttributes();
if (map != null) {
for (var i = 0; i < map.getLength(); i++) {
final var item = map.item(i);
final var name = item.getNodeName();
final var value = item.getNodeValue();
if (name.startsWith("on") && value.startsWith("#")) {
final var methodName = value.replace("#", "");
logger.debug("Found injected method " + methodName);
builder.addInjectedMethod(methodName);
} else if (name.equals("fx:controller")) {
handleController(value, builder);
} else if (name.equals("fx:id")) {
final var type = node.getNodeName();
logger.debug("Found injected field " + value + " of type " + type);
if (type.equals("fx:include")) {
final var path = getRelativePath(builder.inputFile(), map.getNamedItem("source").getNodeValue()).normalize();
final var controllerClass = controllerMapping.get(path);
if (controllerClass == null) {
throw new MojoExecutionException("Cannot find controller for " + path);
}
builder.addInjectedField(value + "Controller", controllerClass);
} else {
builder.addInjectedField(value, type);
}
}
}
}
}
private void handleController(final String controllerClass, final CompilationInfo.Builder builder) throws MojoExecutionException {
final var subPath = controllerClass.replace(".", "/") + ".java";
final var path = project.getCompileSourceRoots().stream()
.map(s -> Paths.get(s).resolve(subPath))
.filter(Files::exists)
.findFirst()
.orElseThrow(() -> new MojoExecutionException("Cannot find controller " + controllerClass));
logger.info("Found controller " + controllerClass);
builder.controllerFile(path);
builder.controllerClass(controllerClass);
}
}

View File

@@ -0,0 +1,152 @@
package com.github.gtache.fxml.compiler.maven;
import com.github.gtache.fxml.compiler.ControllerInfo;
import com.github.gtache.fxml.compiler.impl.ClassesFinder;
import com.github.gtache.fxml.compiler.impl.ControllerInfoImpl;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.logging.Log;
import java.io.IOException;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Pattern;
/**
* Helper class for {@link FXMLCompilerMojo} to provides {@link ControllerInfo}
*/
class ControllerInfoProvider {
private static final Pattern IMPORT_PATTERN = Pattern.compile("import\\s+(?:static\\s+)?(?<import>[^;]+);");
private static final Set<String> JAVA_LANG_CLASSES;
static {
final var set = new HashSet<String>();
set.add("Object");
set.add("String");
set.add("Boolean");
set.add("Character");
set.add("Byte");
set.add("Short");
set.add("Integer");
set.add("Long");
set.add("Float");
set.add("Double");
JAVA_LANG_CLASSES = Set.copyOf(set);
}
private final Log logger;
ControllerInfoProvider(final Log logger) {
this.logger = Objects.requireNonNull(logger);
}
ControllerInfo getControllerInfo(final CompilationInfo info) throws MojoExecutionException {
try {
final var content = Files.readString(info.controllerFile());
final var imports = getImports(content);
final var propertyGenericTypes = new HashMap<String, List<String>>();
for (final var fieldInfo : info.injectedFields()) {
final var name = fieldInfo.name();
final var type = fieldInfo.type();
if (fillGenericTypes(type, name, content, imports, propertyGenericTypes)) {
logger.debug("Found injected field " + name + " of type " + type + " with generic types "
+ propertyGenericTypes.get(name) + " in controller " + info.controllerFile());
} else if (type.contains(".")) {
final var simpleName = type.substring(type.lastIndexOf('.') + 1);
if (fillGenericTypes(simpleName, name, content, imports, propertyGenericTypes)) {
logger.debug("Found injected field " + name + " of type " + simpleName + " with generic types "
+ propertyGenericTypes.get(name) + " in controller " + info.controllerFile());
}
} else {
throw new MojoExecutionException("Cannot find field " + name + "(" + type + ")" + " in controller " + info.controllerFile());
}
}
final var handlerHasArgument = new HashMap<String, Boolean>();
for (final var name : info.injectedMethods()) {
final var pattern = Pattern.compile("void\\s+" + Pattern.quote(name) + "\\s*\\((?<arg>[^)]*)\\)");
final var matcher = pattern.matcher(content);
if (matcher.find()) {
final var arg = matcher.group("arg");
handlerHasArgument.put(name, arg != null && !arg.isBlank());
logger.debug("Found injected method " + name + " with argument " + arg + " in controller " + info.controllerFile());
} else {
throw new MojoExecutionException("Cannot find method " + name + " in controller " + info.controllerFile());
}
}
return new ControllerInfoImpl(handlerHasArgument, propertyGenericTypes);
} catch (final IOException e) {
throw new MojoExecutionException("Error reading controller " + info.controllerFile(), e);
}
}
private static Imports getImports(final CharSequence content) throws MojoExecutionException {
final var resolved = new HashMap<String, String>();
final var unresolved = new HashSet<String>();
final var matcher = IMPORT_PATTERN.matcher(content);
while (matcher.find()) {
final var value = matcher.group("import");
if (value.endsWith(".*")) {
final var packagePath = value.substring(0, value.length() - 2);
try {
final var classes = ClassesFinder.getClasses(packagePath);
if (classes.isEmpty()) {
unresolved.add(packagePath);
} else {
classes.forEach(s -> resolved.put(s.substring(packagePath.length() + 1), s));
}
} catch (final IOException e) {
throw new MojoExecutionException("Error reading package " + packagePath, e);
}
} else {
final var simpleName = value.substring(value.lastIndexOf('.') + 1);
resolved.put(simpleName, value);
}
}
return new Imports(resolved, unresolved);
}
private static boolean fillGenericTypes(final String type, final String name, final CharSequence content, final Imports imports, final Map<? super String, ? super List<String>> propertyGenericTypes) throws MojoExecutionException {
final var pattern = Pattern.compile(Pattern.quote(type) + "(?<type><[^>]+>)?\\s+" + Pattern.quote(name) + "\\s*;");
final var matcher = pattern.matcher(content);
if (matcher.find()) {
final var genericTypes = matcher.group("type");
if (genericTypes != null && !genericTypes.isBlank()) {
if (genericTypes.equals("<>")) {
propertyGenericTypes.put(name, List.of());
} else {
final var split = genericTypes.replace("<", "").replace(">", "").split(",");
final var resolved = new ArrayList<String>();
for (final var s : split) {
final var trimmed = s.trim();
if (trimmed.contains(".") || JAVA_LANG_CLASSES.contains(trimmed)) {
resolved.add(trimmed);
} else {
final var imported = imports.imports().get(trimmed);
if (imported == null) {
throw new MojoExecutionException("Cannot find class " + trimmed + " probably in one of " + imports.packages() + " ; " +
"Use non-wildcard imports, use fully qualified name or put the classes in a dependency.");
} else {
resolved.add(imported);
}
}
}
propertyGenericTypes.put(name, resolved);
}
}
return true;
} else {
return false;
}
}
private record Imports(Map<String, String> imports, Set<String> packages) {
}
}

View File

@@ -1,32 +1,188 @@
package com.github.gtache.fxml.compiler.maven;
import com.github.gtache.fxml.compiler.ControllerInjection;
import com.github.gtache.fxml.compiler.GenerationException;
import com.github.gtache.fxml.compiler.impl.ControllerFieldInjectionTypes;
import com.github.gtache.fxml.compiler.impl.ControllerInjectionImpl;
import com.github.gtache.fxml.compiler.impl.ControllerMethodsInjectionType;
import com.github.gtache.fxml.compiler.impl.GenerationParametersImpl;
import com.github.gtache.fxml.compiler.impl.GenerationRequestImpl;
import com.github.gtache.fxml.compiler.impl.GeneratorImpl;
import com.github.gtache.fxml.compiler.impl.ResourceBundleInjectionImpl;
import com.github.gtache.fxml.compiler.impl.ResourceBundleInjectionTypes;
import com.github.gtache.fxml.compiler.parsing.ParseException;
import com.github.gtache.fxml.compiler.parsing.xml.DOMFXMLParser;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.xml.sax.SAXException;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.IOException;
import java.nio.file.FileVisitOption;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
/**
* Main mojo for FXML compiler
*/
@Mojo(name = "fxml-compile", defaultPhase = LifecyclePhase.GENERATE_SOURCES)
@Mojo(name = "compile", defaultPhase = LifecyclePhase.GENERATE_SOURCES)
public class FXMLCompilerMojo extends AbstractMojo {
private static final DocumentBuilderFactory DOCUMENT_BUILDER_FACTORY = DocumentBuilderFactory.newInstance();
@Parameter(defaultValue = "${project}", required = true, readonly = true)
private MavenProject project;
@Parameter(defaultValue = "${project.build.directory}/generated-sources", required = true)
@Parameter(property = "output-directory", defaultValue = "${project.build.directory}/generated-sources/java", required = true)
private Path outputDirectory;
@Parameter(property = "field-injection", defaultValue = "REFLECTION", required = true)
private ControllerFieldInjectionTypes fieldInjectionTypes;
@Parameter(property = "method-injection", defaultValue = "REFLECTION", required = true)
private ControllerMethodsInjectionType methodsInjectionType;
@Parameter(property = "bundle-injection", defaultValue = "CONSTRUCTOR", required = true)
private ResourceBundleInjectionTypes bundleInjectionType;
@Parameter(property = "bundle-map")
private Map<String, String> bundleMap;
@Override
public void execute() throws MojoExecutionException, MojoFailureException {
public void execute() throws MojoExecutionException {
final var fxmls = getAllFXMLs();
final var controllerMapping = createControllerMapping(fxmls);
final var mapping = createMapping(fxmls, controllerMapping);
compile(mapping);
}
private Map<Path, Path> getAllFXMLs() throws MojoExecutionException {
final var map = new HashMap<Path, Path>();
for (final var resource : project.getResources()) {
final var location = resource.getLocation("");
location.toString();
final var path = Paths.get(resource.getDirectory());
if (Files.isDirectory(path)) {
try (final var stream = Files.find(path, Integer.MAX_VALUE, (p, a) -> p.toString().endsWith(".fxml"), FileVisitOption.FOLLOW_LINKS)) {
final var curList = stream.toList();
getLog().info("Found " + curList);
for (final var p : curList) {
map.put(p, path);
}
} catch (final IOException e) {
throw new MojoExecutionException("Error reading resources", e);
}
} else {
getLog().info("Directory " + path + " does not exist");
}
}
return map;
}
private static Map<Path, String> createControllerMapping(final Map<? extends Path, ? extends Path> fxmls) throws MojoExecutionException {
final var mapping = new HashMap<Path, String>();
for (final var fxml : fxmls.keySet()) {
mapping.put(fxml, getControllerClass(fxml));
}
return mapping;
}
private static String getControllerClass(final Path fxml) throws MojoExecutionException {
try {
final var documentBuilder = DOCUMENT_BUILDER_FACTORY.newDocumentBuilder();
final var document = documentBuilder.parse(fxml.toFile());
document.getDocumentElement().normalize();
final var controller = document.getDocumentElement().getAttribute("fx:controller");
if (controller.isBlank()) {
throw new MojoExecutionException("Missing controller attribute for " + fxml);
} else {
return controller;
}
} catch (final SAXException | IOException | ParserConfigurationException e) {
throw new MojoExecutionException("Error parsing fxml at " + fxml, e);
}
}
private Map<Path, CompilationInfo> createMapping(final Map<? extends Path, ? extends Path> fxmls, final Map<? extends Path, String> controllerMapping) throws MojoExecutionException {
final var compilationInfoProvider = new CompilationInfoProvider(project, outputDirectory, getLog());
final var mapping = new HashMap<Path, CompilationInfo>();
for (final var entry : fxmls.entrySet()) {
final var info = compilationInfoProvider.getCompilationInfo(entry.getValue(), entry.getKey(), controllerMapping);
mapping.put(entry.getKey(), info);
}
return mapping;
}
private void compile(final Map<Path, CompilationInfo> mapping) throws MojoExecutionException {
final var generator = new GeneratorImpl();
final var parser = new DOMFXMLParser();
final var controllerInfoProvider = new ControllerInfoProvider(getLog());
try {
for (final var entry : mapping.entrySet()) {
final var inputPath = entry.getKey();
final var info = entry.getValue();
getLog().info("Parsing " + inputPath + " with " + parser.getClass().getSimpleName());
final var root = parser.parse(inputPath);
final var controllerInjection = getControllerInjection(mapping, info);
final var sourceToGeneratedClassName = getSourceToGeneratedClassName(mapping, info);
final var sourceToControllerName = getSourceToControllerName(mapping, info);
final var resourceBundleInjection = new ResourceBundleInjectionImpl(bundleInjectionType, getBundleName(info));
final var parameters = new GenerationParametersImpl(controllerInjection, sourceToGeneratedClassName, sourceToControllerName, resourceBundleInjection);
final var controllerInfo = controllerInfoProvider.getControllerInfo(info);
final var output = info.outputFile();
final var request = new GenerationRequestImpl(parameters, controllerInfo, root, info.outputClass());
getLog().info("Compiling " + inputPath);
final var content = generator.generate(request);
final var outputDirectory = output.getParent();
Files.createDirectories(outputDirectory);
Files.writeString(output, content);
getLog().info("Compiled " + inputPath + " to " + output);
}
} catch (final IOException | RuntimeException | ParseException | GenerationException e) {
throw new MojoExecutionException("Error compiling fxml", e);
}
project.addCompileSourceRoot(outputDirectory.toAbsolutePath().toString());
}
private String getBundleName(final CompilationInfo info) {
return bundleMap == null ? "" : bundleMap.getOrDefault(info.inputFile().toString(), "");
}
private static Map<String, String> getSourceToControllerName(final Map<Path, CompilationInfo> mapping, final CompilationInfo info) {
final var ret = new HashMap<String, String>();
for (final var entry : info.includes().entrySet()) {
ret.put(entry.getKey(), mapping.get(entry.getValue()).controllerClass());
}
return ret;
}
private static Map<String, String> getSourceToGeneratedClassName(final Map<Path, CompilationInfo> mapping, final CompilationInfo info) {
final var ret = new HashMap<String, String>();
for (final var entry : info.includes().entrySet()) {
ret.put(entry.getKey(), mapping.get(entry.getValue()).outputClass());
}
return ret;
}
private Map<String, ControllerInjection> getControllerInjection(final Map<Path, CompilationInfo> compilationInfoMapping, final CompilationInfo info) {
final var ret = new HashMap<String, ControllerInjection>();
ret.put(info.controllerClass(), getControllerInjection(info));
for (final var entry : info.includes().entrySet()) {
final var key = entry.getKey();
final var value = entry.getValue();
final var subInfo = compilationInfoMapping.get(value);
ret.put(key, getControllerInjection(subInfo));
}
return ret;
}
private ControllerInjection getControllerInjection(final CompilationInfo info) {
return new ControllerInjectionImpl(fieldInjectionTypes, methodsInjectionType, info.controllerClass());
}
}

View File

@@ -0,0 +1,16 @@
package com.github.gtache.fxml.compiler.maven;
import java.util.Objects;
/**
* Info about a field
*
* @param type The field type
* @param name The field name
*/
record FieldInfo(String type, String name) {
FieldInfo {
Objects.requireNonNull(type);
Objects.requireNonNull(name);
}
}