Generate typed Java ViewModels and discover declarative registry defaults
This commit is contained in:
@@ -6,6 +6,8 @@
|
||||
<dependencies>
|
||||
<dependency><groupId>de.neitzel.fxdesigner</groupId><artifactId>designer-contracts</artifactId><version>${project.version}</version></dependency>
|
||||
<dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter</artifactId><scope>test</scope></dependency>
|
||||
<dependency><groupId>de.neitzel.lib</groupId><artifactId>fx</artifactId><version>1.0-SNAPSHOT</version><scope>test</scope></dependency>
|
||||
<dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.42</version><scope>test</scope></dependency>
|
||||
</dependencies>
|
||||
<build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><configuration><proc>none</proc></configuration></plugin></plugins></build>
|
||||
</project>
|
||||
|
||||
+72
-6
@@ -11,10 +11,23 @@ import javax.tools.*;
|
||||
|
||||
/** Explicitly selected Models only; no application classloading or code execution. */
|
||||
@SupportedAnnotationTypes("*")
|
||||
@SupportedOptions("designer.models")
|
||||
@SupportedOptions({
|
||||
"designer.models",
|
||||
"designer.viewModels",
|
||||
"designer.viewModelModels",
|
||||
"designer.generatedPackage",
|
||||
"designer.viewModelDefaults"
|
||||
})
|
||||
public final class ModelMetadataProcessor extends AbstractProcessor {
|
||||
private final Set<String> emitted = new HashSet<>();
|
||||
private final Set<Element> sources = new HashSet<>();
|
||||
private final Map<String, ModelMetadata> metadata = new TreeMap<>();
|
||||
private final Map<String, TypeElement> modelTypes = new TreeMap<>();
|
||||
private boolean delayed;
|
||||
private boolean generated;
|
||||
|
||||
private boolean typed() {
|
||||
return "true".equals(processingEnv.getOptions().get("designer.viewModels"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public SourceVersion getSupportedSourceVersion() {
|
||||
@@ -24,9 +37,43 @@ public final class ModelMetadataProcessor extends AbstractProcessor {
|
||||
@Override
|
||||
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment round) {
|
||||
if (round.processingOver()) return false;
|
||||
sources.addAll(round.getRootElements());
|
||||
String selection = processingEnv.getOptions().getOrDefault("designer.models", "");
|
||||
if (selection == null || selection.isBlank()) return false;
|
||||
if (typed()) {
|
||||
var selectedTypes =
|
||||
Arrays.stream(selection.split(","))
|
||||
.map(String::trim)
|
||||
.map(processingEnv.getElementUtils()::getTypeElement)
|
||||
.toList();
|
||||
if (selectedTypes.stream().allMatch(java.util.Objects::nonNull)
|
||||
&& selectedTypes.stream()
|
||||
.noneMatch(t -> com.sun.source.util.Trees.instance(processingEnv).getPath(t) != null))
|
||||
return false;
|
||||
}
|
||||
// Give accessor-generating processors (including Lombok) a completed source round.
|
||||
if (typed() && !delayed) {
|
||||
delayed = true;
|
||||
try (var out =
|
||||
processingEnv
|
||||
.getFiler()
|
||||
.createSourceFile(
|
||||
processingEnv
|
||||
.getOptions()
|
||||
.getOrDefault("designer.generatedPackage", "generated.viewmodels")
|
||||
+ ".ProcessorRound")
|
||||
.openWriter()) {
|
||||
out.write(
|
||||
"package "
|
||||
+ processingEnv
|
||||
.getOptions()
|
||||
.getOrDefault("designer.generatedPackage", "generated.viewmodels")
|
||||
+ "; final class ProcessorRound {} ");
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (generated) return false;
|
||||
for (String name : selection.split(",")) {
|
||||
var type = processingEnv.getElementUtils().getTypeElement(name.trim());
|
||||
if (type == null) {
|
||||
@@ -43,6 +90,16 @@ public final class ModelMetadataProcessor extends AbstractProcessor {
|
||||
Diagnostic.Kind.ERROR, "Cannot generate Model metadata: " + e.getMessage(), type);
|
||||
}
|
||||
}
|
||||
if (typed()) {
|
||||
generated = true;
|
||||
try {
|
||||
new ViewModelGenerator(processingEnv, metadata, modelTypes).generate();
|
||||
} catch (Exception e) {
|
||||
processingEnv
|
||||
.getMessager()
|
||||
.printMessage(Diagnostic.Kind.ERROR, "Cannot generate ViewModels: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -98,8 +155,10 @@ public final class ModelMetadataProcessor extends AbstractProcessor {
|
||||
while (source.getEnclosingElement() instanceof TypeElement)
|
||||
source = source.getEnclosingElement();
|
||||
// Test compilation and dependent modules must not duplicate metadata for classpath Models.
|
||||
if (!sources.contains(source) || depth > 4 || name.startsWith("java.") || !emitted.add(name))
|
||||
return;
|
||||
if (com.sun.source.util.Trees.instance(processingEnv).getPath(source) == null
|
||||
|| depth > (typed() ? 256 : 4)
|
||||
|| name.startsWith("java.")
|
||||
|| !emitted.add(name)) return;
|
||||
var readers = new TreeMap<String, ExecutableElement>();
|
||||
var setters = new ArrayList<ExecutableElement>();
|
||||
for (var member : processingEnv.getElementUtils().getAllMembers(owner)) {
|
||||
@@ -148,12 +207,19 @@ public final class ModelMetadataProcessor extends AbstractProcessor {
|
||||
if (value instanceof DeclaredType d && !collection(value))
|
||||
nested.add((TypeElement) d.asElement());
|
||||
}
|
||||
var description = new ModelMetadata(name, properties);
|
||||
metadata.put(name, description);
|
||||
modelTypes.put(name, owner);
|
||||
if (typed()) {
|
||||
TypeMirror parent = owner.getSuperclass();
|
||||
if (parent instanceof DeclaredType d) emit((TypeElement) d.asElement(), depth + 1);
|
||||
}
|
||||
var resource =
|
||||
processingEnv
|
||||
.getFiler()
|
||||
.createResource(StandardLocation.CLASS_OUTPUT, "", ModelMetadata.resource(name), owner);
|
||||
try (var out = resource.openOutputStream()) {
|
||||
out.write(new ModelMetadata(name, properties).encode());
|
||||
out.write(description.encode());
|
||||
}
|
||||
for (var n : nested) emit(n, depth + 1);
|
||||
}
|
||||
|
||||
+318
@@ -0,0 +1,318 @@
|
||||
package de.neitzel.fxdesigner.processor;
|
||||
|
||||
import de.neitzel.fxdesigner.protocol.ModelMetadata;
|
||||
import java.util.*;
|
||||
import javax.annotation.processing.ProcessingEnvironment;
|
||||
import javax.lang.model.element.*;
|
||||
import javax.lang.model.type.DeclaredType;
|
||||
import javax.tools.StandardLocation;
|
||||
|
||||
/** Generates accessors from the same Model description packaged for discovery. */
|
||||
final class ViewModelGenerator {
|
||||
private final ProcessingEnvironment env;
|
||||
private final Map<String, ModelMetadata> models;
|
||||
private final Map<String, TypeElement> types;
|
||||
private final String pkg;
|
||||
private final Map<String, String> names = new TreeMap<>();
|
||||
|
||||
ViewModelGenerator(
|
||||
ProcessingEnvironment env,
|
||||
Map<String, ModelMetadata> models,
|
||||
Map<String, TypeElement> types) {
|
||||
this.env = env;
|
||||
this.models = models;
|
||||
this.types = types;
|
||||
pkg = env.getOptions().getOrDefault("designer.generatedPackage", "generated.viewmodels");
|
||||
if (!javax.lang.model.SourceVersion.isName(pkg))
|
||||
throw new IllegalArgumentException("Invalid generated package: " + pkg);
|
||||
}
|
||||
|
||||
private boolean assignable(String name, String parent) {
|
||||
var a = env.getElementUtils().getTypeElement(name.replace('$', '.'));
|
||||
var b = env.getElementUtils().getTypeElement(parent);
|
||||
return a != null
|
||||
&& b != null
|
||||
&& env.getTypeUtils()
|
||||
.isAssignable(
|
||||
env.getTypeUtils().erasure(a.asType()), env.getTypeUtils().erasure(b.asType()));
|
||||
}
|
||||
|
||||
private boolean simple(String name) {
|
||||
var t = env.getElementUtils().getTypeElement(name.replace('$', '.'));
|
||||
return t == null
|
||||
|| t.getKind() == ElementKind.ENUM
|
||||
|| name.startsWith("java.")
|
||||
|| assignable(name, "java.lang.Number")
|
||||
|| assignable(name, "java.util.Date")
|
||||
|| assignable(name, "java.util.Calendar")
|
||||
|| assignable(name, "java.time.temporal.TemporalAccessor")
|
||||
|| assignable(name, "java.time.temporal.TemporalAmount");
|
||||
}
|
||||
|
||||
private String javaType(String binary) {
|
||||
if (!binary.startsWith("[")) return binary.replace('$', '.');
|
||||
String c = binary.substring(1);
|
||||
return (c.startsWith("L")
|
||||
? c.substring(1, c.length() - 1).replace('$', '.')
|
||||
: switch (c) {
|
||||
case "I" -> "int";
|
||||
case "J" -> "long";
|
||||
case "Z" -> "boolean";
|
||||
case "B" -> "byte";
|
||||
case "S" -> "short";
|
||||
case "C" -> "char";
|
||||
case "F" -> "float";
|
||||
case "D" -> "double";
|
||||
default -> javaType(c);
|
||||
})
|
||||
+ "[]";
|
||||
}
|
||||
|
||||
private String value(ModelMetadata.Property p) {
|
||||
if (names.containsKey(p.type())) return names.get(p.type());
|
||||
if (assignable(p.type(), "java.util.Collection") && !p.elementType().isEmpty())
|
||||
return "javafx.collections.ObservableList<" + javaType(p.elementType()) + ">";
|
||||
return javaType(p.type());
|
||||
}
|
||||
|
||||
private String specialized(String type) {
|
||||
return switch (type) {
|
||||
case "java.lang.String" -> "String";
|
||||
case "int", "java.lang.Integer" -> "Integer";
|
||||
case "long", "java.lang.Long" -> "Long";
|
||||
case "float", "java.lang.Float" -> "Float";
|
||||
case "double", "java.lang.Double" -> "Double";
|
||||
case "boolean", "java.lang.Boolean" -> "Boolean";
|
||||
default -> "";
|
||||
};
|
||||
}
|
||||
|
||||
private String boxed(String type) {
|
||||
return switch (type) {
|
||||
case "byte" -> "java.lang.Byte";
|
||||
case "short" -> "java.lang.Short";
|
||||
case "char" -> "java.lang.Character";
|
||||
default -> type;
|
||||
};
|
||||
}
|
||||
|
||||
private String property(ModelMetadata.Property p) {
|
||||
String scalar = specialized(p.type());
|
||||
return scalar.isEmpty()
|
||||
? "javafx.beans.property.Property<" + boxed(value(p)) + ">"
|
||||
: "javafx.beans.property." + scalar + "Property";
|
||||
}
|
||||
|
||||
private String selectedModel(String name) {
|
||||
var type = env.getElementUtils().getTypeElement(name.replace('$', '.'));
|
||||
if (type == null)
|
||||
throw new IllegalArgumentException("Selected ViewModel Model not found: " + name);
|
||||
return env.getElementUtils().getBinaryName(type).toString();
|
||||
}
|
||||
|
||||
void generate() throws Exception {
|
||||
var required = new HashSet<String>();
|
||||
var queue = new ArrayDeque<String>();
|
||||
String selected =
|
||||
env.getOptions()
|
||||
.getOrDefault("designer.viewModelModels", env.getOptions().get("designer.models"));
|
||||
for (String name : selected.split(",", -1)) {
|
||||
String binary = selectedModel(name.trim());
|
||||
if (!models.containsKey(binary))
|
||||
throw new IllegalArgumentException(
|
||||
"Selected ViewModel Model has no source metadata: " + name);
|
||||
queue.add(binary);
|
||||
}
|
||||
while (!queue.isEmpty()) {
|
||||
String name = queue.remove();
|
||||
if (!models.containsKey(name) || simple(name) || !required.add(name)) continue;
|
||||
for (var p : models.get(name).properties())
|
||||
if (models.containsKey(p.type())
|
||||
&& !simple(p.type())
|
||||
&& !models.get(p.type()).properties().isEmpty()) queue.add(p.type());
|
||||
var type = types.get(name);
|
||||
if (type.getSuperclass() instanceof DeclaredType d)
|
||||
queue.add(env.getElementUtils().getBinaryName((TypeElement) d.asElement()).toString());
|
||||
}
|
||||
for (var m : models.values())
|
||||
if (required.contains(m.model())) {
|
||||
var type = types.get(m.model());
|
||||
var methods =
|
||||
env.getElementUtils().getAllMembers(type).stream()
|
||||
.filter(
|
||||
e ->
|
||||
e instanceof ExecutableElement
|
||||
&& e.getModifiers().contains(Modifier.PUBLIC))
|
||||
.map(e -> e.getSimpleName().toString())
|
||||
.collect(java.util.stream.Collectors.toSet());
|
||||
if (methods.contains("getLatestMessage") && methods.contains("setLatestMessage"))
|
||||
throw new IllegalArgumentException(
|
||||
m.model() + ".latestMessage conflicts with ViewModel.getLatestMessage");
|
||||
if (!type.getModifiers().contains(Modifier.PUBLIC))
|
||||
throw new IllegalArgumentException("Generated Model must be public: " + m.model());
|
||||
if (!type.getTypeParameters().isEmpty())
|
||||
throw new IllegalArgumentException(
|
||||
"Generic Model needs a concrete declaration: " + m.model());
|
||||
for (TypeElement c = type; c != null; ) {
|
||||
if (env.getElementUtils().getTypeElement(c.getQualifiedName() + "BeanInfo") != null)
|
||||
throw new IllegalArgumentException(
|
||||
"Custom BeanInfo requires explicit mapping: " + m.model());
|
||||
c = c.getSuperclass() instanceof DeclaredType d ? (TypeElement) d.asElement() : null;
|
||||
}
|
||||
String name = pkg + "." + type.getSimpleName() + "ViewModel";
|
||||
if (names.containsValue(name))
|
||||
throw new IllegalArgumentException("Generated class name collision: " + name);
|
||||
names.put(m.model(), name);
|
||||
}
|
||||
var defaults = new TreeMap<String, String>();
|
||||
String settings = env.getOptions().getOrDefault("designer.viewModelDefaults", "");
|
||||
if (settings != null && !settings.isBlank())
|
||||
for (String item : settings.split(",")) {
|
||||
var pair = item.split("=", -1);
|
||||
if (pair.length != 2)
|
||||
throw new IllegalArgumentException("Invalid ViewModel default: " + item);
|
||||
pair[0] = selectedModel(pair[0].trim());
|
||||
pair[1] = pair[1].trim();
|
||||
if (!names.containsKey(pair[0])
|
||||
|| !javax.lang.model.SourceVersion.isName(pair[1])
|
||||
|| defaults.put(pair[0], pair[1]) != null)
|
||||
throw new IllegalArgumentException("Invalid/conflicting ViewModel default: " + item);
|
||||
}
|
||||
var runtime = env.getElementUtils().getTypeElement("de.neitzel.fx.view.ViewModel");
|
||||
if (runtime == null)
|
||||
throw new IllegalArgumentException(
|
||||
"NeitzelLib FX with generated ViewModel support is required");
|
||||
var reserved = new HashSet<String>();
|
||||
for (var member : env.getElementUtils().getAllMembers(runtime))
|
||||
if (member instanceof ExecutableElement m) reserved.add(m.getSimpleName().toString());
|
||||
for (var entry : names.entrySet()) {
|
||||
var model = models.get(entry.getKey());
|
||||
var type = types.get(entry.getKey());
|
||||
String parentModel =
|
||||
type.getSuperclass() instanceof DeclaredType d
|
||||
? env.getElementUtils().getBinaryName((TypeElement) d.asElement()).toString()
|
||||
: "";
|
||||
String parent = names.getOrDefault(parentModel, "de.neitzel.fx.view.ViewModel");
|
||||
var inherited = new HashMap<String, ModelMetadata.Property>();
|
||||
if (names.containsKey(parentModel))
|
||||
for (var p : models.get(parentModel).properties()) inherited.put(p.name(), p);
|
||||
var source =
|
||||
new StringBuilder(
|
||||
"package "
|
||||
+ pkg
|
||||
+ ";\n@SuppressWarnings({\"unchecked\",\"rawtypes\"})\npublic class "
|
||||
+ entry.getValue().substring(pkg.length() + 1)
|
||||
+ " extends "
|
||||
+ parent
|
||||
+ " {\n");
|
||||
source
|
||||
.append(" public ")
|
||||
.append(entry.getValue().substring(pkg.length() + 1))
|
||||
.append('(')
|
||||
.append(javaType(model.model()))
|
||||
.append(" model) { super(model); }\n");
|
||||
for (var p : model.properties()) {
|
||||
if (!simple(p.type()) && !names.containsKey(p.type())) {
|
||||
var child = env.getElementUtils().getTypeElement(p.type().replace('$', '.'));
|
||||
var members =
|
||||
env.getElementUtils().getAllMembers(child).stream()
|
||||
.filter(
|
||||
e ->
|
||||
e instanceof ExecutableElement
|
||||
&& e.getModifiers().contains(Modifier.PUBLIC))
|
||||
.map(e -> e.getSimpleName().toString())
|
||||
.collect(java.util.stream.Collectors.toSet());
|
||||
if (members.stream()
|
||||
.anyMatch(n -> n.startsWith("get") && members.contains("set" + n.substring(3))))
|
||||
throw new IllegalArgumentException(
|
||||
"Nested Model requires an available generated mapping: "
|
||||
+ model.model()
|
||||
+ "."
|
||||
+ p.name());
|
||||
}
|
||||
String cap = Character.toUpperCase(p.name().charAt(0)) + p.name().substring(1);
|
||||
for (String n : List.of(p.name() + "Property", "get" + cap, "set" + cap))
|
||||
if (reserved.contains(n))
|
||||
throw new IllegalArgumentException(
|
||||
model.model() + "." + p.name() + " conflicts with ViewModel." + n);
|
||||
if (inherited.containsKey(p.name())) {
|
||||
var old = inherited.get(p.name());
|
||||
if (!property(old).equals(property(p)) || !value(old).equals(value(p)))
|
||||
throw new IllegalArgumentException(
|
||||
"Incompatible writable Attribute narrowing: " + model.model() + "." + p.name());
|
||||
continue;
|
||||
}
|
||||
source
|
||||
.append(" public ")
|
||||
.append(property(p))
|
||||
.append(' ')
|
||||
.append(p.name())
|
||||
.append("Property() { return (")
|
||||
.append(property(p))
|
||||
.append(") (javafx.beans.property.Property<?>) storedProperty(\"")
|
||||
.append(p.name())
|
||||
.append("\"); }\n");
|
||||
source
|
||||
.append(" public ")
|
||||
.append(value(p))
|
||||
.append(" get")
|
||||
.append(cap)
|
||||
.append("() { return ")
|
||||
.append(p.name())
|
||||
.append("Property().")
|
||||
.append(specialized(p.type()).isEmpty() ? "getValue()" : "get()")
|
||||
.append("; }\n");
|
||||
source
|
||||
.append(" public void set")
|
||||
.append(cap)
|
||||
.append('(')
|
||||
.append(value(p))
|
||||
.append(" value) { ")
|
||||
.append(p.name())
|
||||
.append("Property().setValue(value); }\n");
|
||||
}
|
||||
source.append("}\n");
|
||||
try (var out = env.getFiler().createSourceFile(entry.getValue(), type).openWriter()) {
|
||||
out.write(source.toString());
|
||||
}
|
||||
}
|
||||
if (names.isEmpty()) return;
|
||||
String registry = pkg + ".GeneratedViewModelRegistry";
|
||||
var source = new StringBuilder("package " + pkg + ";\n");
|
||||
for (var entry : names.entrySet())
|
||||
source
|
||||
.append("@de.neitzel.fx.view.ViewModelRegistration(model=")
|
||||
.append(javaType(entry.getKey()))
|
||||
.append(".class,generated=")
|
||||
.append(entry.getValue())
|
||||
.append(".class,defaultType=")
|
||||
.append(defaults.getOrDefault(entry.getKey(), entry.getValue()))
|
||||
.append(".class)\n");
|
||||
source.append(
|
||||
"public final class GeneratedViewModelRegistry implements"
|
||||
+ " de.neitzel.fx.view.ViewModelRegistry {\n");
|
||||
int index = 0;
|
||||
for (var e : defaults.entrySet())
|
||||
source
|
||||
.append(" private Class<? extends ")
|
||||
.append(names.get(e.getKey()))
|
||||
.append("> check")
|
||||
.append(index++)
|
||||
.append("() { return ")
|
||||
.append(e.getValue())
|
||||
.append(".class; }\n");
|
||||
source.append("}\n");
|
||||
try (var out = env.getFiler().createSourceFile(registry).openWriter()) {
|
||||
out.write(source.toString());
|
||||
}
|
||||
try (var out =
|
||||
env.getFiler()
|
||||
.createResource(
|
||||
StandardLocation.CLASS_OUTPUT,
|
||||
"",
|
||||
"META-INF/services/de.neitzel.fx.view.ViewModelRegistry")
|
||||
.openWriter()) {
|
||||
out.write(registry + "\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
+197
@@ -0,0 +1,197 @@
|
||||
package de.neitzel.fxdesigner.processor;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
import javax.annotation.processing.Processor;
|
||||
import javax.tools.*;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
class ViewModelGenerationTest {
|
||||
@TempDir Path root;
|
||||
String diagnostics;
|
||||
|
||||
boolean compile(
|
||||
String source, String custom, boolean lombok, boolean reverse, String... extraOptions)
|
||||
throws Exception {
|
||||
Path src = root.resolve("src"), out = root.resolve("out"), gen = root.resolve("generated");
|
||||
Files.createDirectories(src);
|
||||
Files.createDirectories(out);
|
||||
Files.createDirectories(gen);
|
||||
Files.writeString(src.resolve("Person.java"), source);
|
||||
var inputs = new ArrayList<java.io.File>();
|
||||
inputs.add(src.resolve("Person.java").toFile());
|
||||
if (custom != null) {
|
||||
Files.writeString(src.resolve("Custom.java"), custom);
|
||||
inputs.add(src.resolve("Custom.java").toFile());
|
||||
}
|
||||
try (var paths = Files.list(src)) {
|
||||
inputs.clear();
|
||||
inputs.addAll(paths.filter(p -> p.toString().endsWith(".java")).map(Path::toFile).toList());
|
||||
}
|
||||
var compiler = ToolProvider.getSystemJavaCompiler();
|
||||
var messages = new DiagnosticCollector<JavaFileObject>();
|
||||
try (var files = compiler.getStandardFileManager(null, null, null)) {
|
||||
var options =
|
||||
new ArrayList<>(
|
||||
List.of(
|
||||
"-classpath",
|
||||
System.getProperty("java.class.path"),
|
||||
"-d",
|
||||
out.toString(),
|
||||
"-s",
|
||||
gen.toString(),
|
||||
"-Adesigner.models=sample.Person",
|
||||
"-Adesigner.viewModels=true",
|
||||
"-Adesigner.generatedPackage=sample.generated"));
|
||||
options.addAll(List.of(extraOptions));
|
||||
if (custom != null) options.add("-Adesigner.viewModelDefaults=sample.Person=sample.Custom");
|
||||
var task =
|
||||
compiler.getTask(
|
||||
null, files, messages, options, null, files.getJavaFileObjectsFromFiles(inputs));
|
||||
var processors = new ArrayList<Processor>();
|
||||
processors.add(new ModelMetadataProcessor());
|
||||
if (lombok)
|
||||
processors.add(
|
||||
(Processor)
|
||||
Class.forName("lombok.launch.AnnotationProcessorHider$AnnotationProcessor")
|
||||
.getConstructor()
|
||||
.newInstance());
|
||||
if (reverse) Collections.reverse(processors);
|
||||
task.setProcessors(processors);
|
||||
boolean success = task.call();
|
||||
diagnostics = messages.getDiagnostics().toString();
|
||||
return success;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void nestedSelectionsAreNormalizedAndUnknownSelectionsFail() throws Exception {
|
||||
String source =
|
||||
"package sample; public class Person { public static class Child { public String"
|
||||
+ " getName(){return null;} public void setName(String n){} } }";
|
||||
assertTrue(
|
||||
compile(
|
||||
source,
|
||||
null,
|
||||
false,
|
||||
false,
|
||||
"-Adesigner.models=sample.Person.Child",
|
||||
"-Adesigner.viewModelModels=sample.Person.Child"),
|
||||
diagnostics);
|
||||
assertTrue(Files.exists(root.resolve("generated/sample/generated/ChildViewModel.java")));
|
||||
assertFalse(compile(source, null, false, false, "-Adesigner.viewModelModels=sample.Missing"));
|
||||
assertTrue(diagnostics.contains("Selected ViewModel Model not found"), diagnostics);
|
||||
}
|
||||
|
||||
@Test
|
||||
void cleanCompilationSupportsHandwrittenSubclassAndStoredAccessors() throws Exception {
|
||||
assertTrue(
|
||||
compile(
|
||||
"package sample; public class Person { public String getName(){return null;} public"
|
||||
+ " void setName(String n){} public Person getChild(){return null;} public void"
|
||||
+ " setChild(Person p){} }",
|
||||
"package sample; public class Custom extends sample.generated.PersonViewModel { public"
|
||||
+ " Custom(Person p){super(p);} public String greeting(){return getName();} }",
|
||||
false,
|
||||
false));
|
||||
String generated =
|
||||
Files.readString(root.resolve("generated/sample/generated/PersonViewModel.java"));
|
||||
assertTrue(generated.contains("storedProperty(\"name\")"));
|
||||
assertTrue(generated.contains("Property<sample.generated.PersonViewModel>"));
|
||||
assertTrue(
|
||||
Files.exists(root.resolve("out/META-INF/services/de.neitzel.fx.view.ViewModelRegistry")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void lombokAccessorsWorkInBothProcessorOrders() throws Exception {
|
||||
for (boolean reverse : List.of(false, true)) {
|
||||
Path previous = root;
|
||||
root = Files.createDirectory(previous.resolve("order" + reverse));
|
||||
assertTrue(
|
||||
compile(
|
||||
"package sample; @lombok.Getter @lombok.Setter public class Person { private String"
|
||||
+ " name; }",
|
||||
null,
|
||||
true,
|
||||
reverse));
|
||||
assertTrue(
|
||||
Files.readString(root.resolve("generated/sample/generated/PersonViewModel.java"))
|
||||
.contains("nameProperty"));
|
||||
root = previous;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void reservedNamesAndInvalidCustomDefaultsFailCompilation() throws Exception {
|
||||
assertFalse(
|
||||
compile(
|
||||
"package sample; public class Person { public String getLatestMessage(){return null;}"
|
||||
+ " public void setLatestMessage(String s){} }",
|
||||
null,
|
||||
false,
|
||||
false));
|
||||
assertFalse(
|
||||
compile(
|
||||
"package sample; public class Person { public String getName(){return null;} public"
|
||||
+ " void setName(String s){} }",
|
||||
"package sample; public class Custom extends de.neitzel.fx.view.ViewModel { public"
|
||||
+ " Custom(Person p){super(p);} }",
|
||||
false,
|
||||
false));
|
||||
}
|
||||
|
||||
@Test
|
||||
void removedAttributesBreakCustomCodeWithoutRewritingIt() throws Exception {
|
||||
String custom =
|
||||
"package sample; public class Custom extends sample.generated.PersonViewModel { public"
|
||||
+ " Custom(Person p){super(p);} public String title(){return getName();} }";
|
||||
assertTrue(
|
||||
compile(
|
||||
"package sample; public class Person {public String getName(){return null;} public void"
|
||||
+ " setName(String v){} }",
|
||||
custom,
|
||||
false,
|
||||
false));
|
||||
for (String dir : List.of("out", "generated"))
|
||||
try (var paths = Files.walk(root.resolve(dir))) {
|
||||
for (var p : paths.sorted(Comparator.reverseOrder()).toList()) Files.delete(p);
|
||||
}
|
||||
assertFalse(
|
||||
compile(
|
||||
"package sample; public class Person {public String getTitle(){return null;} public"
|
||||
+ " void setTitle(String v){} }",
|
||||
custom,
|
||||
false,
|
||||
false));
|
||||
assertEquals(custom, Files.readString(root.resolve("src/Custom.java")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void writableNarrowingAndCustomBeanInfoAreDiagnosed() throws Exception {
|
||||
Files.createDirectories(root.resolve("src"));
|
||||
Files.writeString(
|
||||
root.resolve("src/Parent.java"),
|
||||
"package sample; public class Parent {public Object getValue(){return null;} public void"
|
||||
+ " setValue(Object v){} }");
|
||||
assertFalse(
|
||||
compile(
|
||||
"package sample; public class Person extends Parent {public String getValue(){return"
|
||||
+ " null;} public void setValue(String v){} }",
|
||||
null,
|
||||
false,
|
||||
false));
|
||||
assertTrue(diagnostics.contains("Incompatible writable Attribute narrowing"), diagnostics);
|
||||
assertFalse(
|
||||
compile(
|
||||
"package sample; public class Person { public String getName(){return null;} public"
|
||||
+ " void setName(String n){} } class PersonBeanInfo extends"
|
||||
+ " java.beans.SimpleBeanInfo {}",
|
||||
null,
|
||||
false,
|
||||
false));
|
||||
assertTrue(diagnostics.contains("Custom BeanInfo"), diagnostics);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user