Generate versioned Model metadata and consume it in shared binding discovery
This commit is contained in:
+160
@@ -0,0 +1,160 @@
|
||||
package de.neitzel.fxdesigner.processor;
|
||||
|
||||
import de.neitzel.fxdesigner.protocol.ModelMetadata;
|
||||
import java.beans.Introspector;
|
||||
import java.util.*;
|
||||
import javax.annotation.processing.*;
|
||||
import javax.lang.model.SourceVersion;
|
||||
import javax.lang.model.element.*;
|
||||
import javax.lang.model.type.*;
|
||||
import javax.tools.*;
|
||||
|
||||
/** Explicitly selected Models only; no application classloading or code execution. */
|
||||
@SupportedAnnotationTypes("*")
|
||||
@SupportedOptions("designer.models")
|
||||
public final class ModelMetadataProcessor extends AbstractProcessor {
|
||||
private final Set<String> emitted = new HashSet<>();
|
||||
private final Set<Element> sources = new HashSet<>();
|
||||
|
||||
@Override
|
||||
public SourceVersion getSupportedSourceVersion() {
|
||||
return SourceVersion.latestSupported();
|
||||
}
|
||||
|
||||
@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;
|
||||
for (String name : selection.split(",")) {
|
||||
var type = processingEnv.getElementUtils().getTypeElement(name.trim());
|
||||
if (type == null) {
|
||||
processingEnv
|
||||
.getMessager()
|
||||
.printMessage(Diagnostic.Kind.ERROR, "Selected Model not found: " + name);
|
||||
} else
|
||||
try {
|
||||
emit(type, 0);
|
||||
} catch (Exception e) {
|
||||
processingEnv
|
||||
.getMessager()
|
||||
.printMessage(
|
||||
Diagnostic.Kind.ERROR, "Cannot generate Model metadata: " + e.getMessage(), type);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private String binary(TypeMirror type) {
|
||||
var types = processingEnv.getTypeUtils();
|
||||
type = types.erasure(type);
|
||||
if (type instanceof ArrayType a) {
|
||||
String c = binary(a.getComponentType());
|
||||
return "["
|
||||
+ switch (c) {
|
||||
case "boolean" -> "Z";
|
||||
case "byte" -> "B";
|
||||
case "short" -> "S";
|
||||
case "int" -> "I";
|
||||
case "long" -> "J";
|
||||
case "char" -> "C";
|
||||
case "float" -> "F";
|
||||
case "double" -> "D";
|
||||
default -> c.startsWith("[") ? c : "L" + c + ";";
|
||||
};
|
||||
}
|
||||
return type instanceof DeclaredType d
|
||||
? processingEnv.getElementUtils().getBinaryName((TypeElement) d.asElement()).toString()
|
||||
: type.toString();
|
||||
}
|
||||
|
||||
private ExecutableType signature(TypeElement owner, ExecutableElement method) {
|
||||
return (ExecutableType)
|
||||
processingEnv.getTypeUtils().asMemberOf((DeclaredType) owner.asType(), method);
|
||||
}
|
||||
|
||||
private boolean collection(TypeMirror type) {
|
||||
var types = processingEnv.getTypeUtils();
|
||||
return types.isAssignable(
|
||||
types.erasure(type),
|
||||
types.erasure(
|
||||
processingEnv.getElementUtils().getTypeElement("java.util.Collection").asType()));
|
||||
}
|
||||
|
||||
private String element(TypeMirror type) {
|
||||
if (!(type instanceof DeclaredType d) || d.getTypeArguments().size() != 1) return "";
|
||||
TypeMirror value = d.getTypeArguments().getFirst();
|
||||
if (value instanceof WildcardType w)
|
||||
value = w.getSuperBound() == null ? w.getExtendsBound() : null;
|
||||
return value instanceof DeclaredType declared && declared.getTypeArguments().isEmpty()
|
||||
? binary(value)
|
||||
: "";
|
||||
}
|
||||
|
||||
private void emit(TypeElement owner, int depth) throws Exception {
|
||||
String name = processingEnv.getElementUtils().getBinaryName(owner).toString();
|
||||
Element source = owner;
|
||||
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;
|
||||
var readers = new TreeMap<String, ExecutableElement>();
|
||||
var setters = new ArrayList<ExecutableElement>();
|
||||
for (var member : processingEnv.getElementUtils().getAllMembers(owner)) {
|
||||
if (!(member instanceof ExecutableElement m)
|
||||
|| m.getKind() != ElementKind.METHOD
|
||||
|| !m.getModifiers().contains(Modifier.PUBLIC)
|
||||
|| m.getModifiers().contains(Modifier.STATIC)) continue;
|
||||
String n = m.getSimpleName().toString();
|
||||
var sig = signature(owner, m);
|
||||
if (n.startsWith("set")
|
||||
&& n.length() > 3
|
||||
&& sig.getParameterTypes().size() == 1
|
||||
&& sig.getReturnType().getKind() == TypeKind.VOID) setters.add(m);
|
||||
if (!sig.getParameterTypes().isEmpty() || sig.getReturnType().getKind() == TypeKind.VOID)
|
||||
continue;
|
||||
boolean bool =
|
||||
n.startsWith("is") && n.length() > 2 && sig.getReturnType().getKind() == TypeKind.BOOLEAN;
|
||||
if (!bool && !(n.startsWith("get") && n.length() > 3)) continue;
|
||||
String property = Introspector.decapitalize(n.substring(bool ? 2 : 3));
|
||||
if (bool || !readers.containsKey(property)) readers.put(property, m);
|
||||
}
|
||||
var properties = new ArrayList<ModelMetadata.Property>();
|
||||
var nested = new ArrayList<TypeElement>();
|
||||
for (var entry : readers.entrySet()) {
|
||||
String property = entry.getKey();
|
||||
if (property.equals("class") || property.equals("latestMessage")) continue;
|
||||
var reader = entry.getValue();
|
||||
TypeMirror value = signature(owner, reader).getReturnType();
|
||||
var matching =
|
||||
setters.stream()
|
||||
.filter(
|
||||
m ->
|
||||
Introspector.decapitalize(m.getSimpleName().toString().substring(3))
|
||||
.equals(property)
|
||||
&& binary(signature(owner, m).getParameterTypes().getFirst())
|
||||
.equals(binary(value)))
|
||||
.toList();
|
||||
if (matching.size() != 1) continue;
|
||||
properties.add(
|
||||
new ModelMetadata.Property(
|
||||
property,
|
||||
binary(value),
|
||||
collection(value) ? element(value) : "",
|
||||
reader.getSimpleName().toString(),
|
||||
matching.getFirst().getSimpleName().toString()));
|
||||
if (value instanceof DeclaredType d && !collection(value))
|
||||
nested.add((TypeElement) d.asElement());
|
||||
}
|
||||
var resource =
|
||||
processingEnv
|
||||
.getFiler()
|
||||
.createResource(StandardLocation.CLASS_OUTPUT, "", ModelMetadata.resource(name), owner);
|
||||
try (var out = resource.openOutputStream()) {
|
||||
out.write(new ModelMetadata(name, properties).encode());
|
||||
}
|
||||
for (var n : nested) emit(n, depth + 1);
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
de.neitzel.fxdesigner.processor.ModelMetadataProcessor
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
package de.neitzel.fxdesigner.processor;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import de.neitzel.fxdesigner.protocol.ModelMetadata;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
import javax.tools.*;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
class ModelMetadataProcessorTest {
|
||||
@TempDir Path root;
|
||||
|
||||
private boolean compile(String source, String selected, Path output) throws Exception {
|
||||
Files.createDirectories(output);
|
||||
Path file = root.resolve("Person.java");
|
||||
Files.writeString(file, source);
|
||||
var compiler = ToolProvider.getSystemJavaCompiler();
|
||||
try (var files = compiler.getStandardFileManager(null, null, null)) {
|
||||
var task =
|
||||
compiler.getTask(
|
||||
null,
|
||||
files,
|
||||
null,
|
||||
List.of("-d", output.toString(), "-Adesigner.models=" + selected),
|
||||
null,
|
||||
files.getJavaFileObjects(file.toFile()));
|
||||
task.setProcessors(List.of(new ModelMetadataProcessor()));
|
||||
return task.call();
|
||||
}
|
||||
}
|
||||
|
||||
private String source(String property) {
|
||||
return "package example; class Base { public int getRank(){throw new Error();} public void"
|
||||
+ " setRank(int n){} } public class Person extends Base { static { if(true) throw"
|
||||
+ " new Error(); } public Person(){throw new Error();} public String get"
|
||||
+ property
|
||||
+ "(){throw new Error();} public void set"
|
||||
+ property
|
||||
+ "(String v){} public boolean isReady(){return true;} public boolean getReady(){return"
|
||||
+ " false;} public void setReady(boolean v){} public String getReadOnly(){return null;}"
|
||||
+ " public java.util.List<? extends Person> getChildren(){return null;} public void"
|
||||
+ " setChildren(java.util.List<? extends Person> v){} public Person getHome(){return null;}"
|
||||
+ " public void setHome(Person v){} }";
|
||||
}
|
||||
|
||||
@Test
|
||||
void emitsDeterministicInheritedAndCollectionSignaturesWithoutExecutingCode() throws Exception {
|
||||
Path a = root.resolve("a"), b = root.resolve("b");
|
||||
assertTrue(compile(source("Name"), "example.Person", a));
|
||||
assertTrue(compile(source("Name"), "example.Person", b));
|
||||
String resource = ModelMetadata.resource("example.Person");
|
||||
assertArrayEquals(
|
||||
Files.readAllBytes(a.resolve(resource)), Files.readAllBytes(b.resolve(resource)));
|
||||
try (var in = Files.newInputStream(a.resolve(resource))) {
|
||||
var metadata = ModelMetadata.read(in);
|
||||
assertEquals(
|
||||
List.of("children", "home", "name", "rank", "ready"),
|
||||
metadata.properties().stream().map(ModelMetadata.Property::name).toList());
|
||||
assertEquals("example.Person", metadata.properties().getFirst().elementType());
|
||||
assertEquals("isReady", metadata.properties().getLast().reader());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void freshBuildRemovesOldPropertiesAndDeselectedModels() throws Exception {
|
||||
Path output = root.resolve("out");
|
||||
assertTrue(compile(source("Name"), "example.Person", output));
|
||||
// The designer uses clean isolated builds, not incremental processor output directories.
|
||||
try (var paths = Files.walk(output)) {
|
||||
for (var p : paths.sorted(Comparator.reverseOrder()).toList()) Files.delete(p);
|
||||
}
|
||||
assertTrue(compile(source("Title"), "example.Person", output));
|
||||
try (var in = Files.newInputStream(output.resolve(ModelMetadata.resource("example.Person")))) {
|
||||
var names =
|
||||
ModelMetadata.read(in).properties().stream().map(ModelMetadata.Property::name).toList();
|
||||
assertTrue(names.contains("title"));
|
||||
assertFalse(names.contains("name"));
|
||||
}
|
||||
try (var paths = Files.walk(output)) {
|
||||
for (var p : paths.sorted(Comparator.reverseOrder()).toList()) Files.delete(p);
|
||||
}
|
||||
assertTrue(compile(source("Title"), "", output));
|
||||
assertFalse(Files.exists(output.resolve(ModelMetadata.resource("example.Person"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void dependentCompilationDoesNotDuplicateModelResources() throws Exception {
|
||||
Path main = root.resolve("main"), tests = root.resolve("tests");
|
||||
assertTrue(compile(source("Name"), "example.Person", main));
|
||||
Files.createDirectories(tests);
|
||||
Path file = root.resolve("Usage.java");
|
||||
Files.writeString(file, "package example; class Usage { Person value; }");
|
||||
var compiler = ToolProvider.getSystemJavaCompiler();
|
||||
try (var files = compiler.getStandardFileManager(null, null, null)) {
|
||||
var task =
|
||||
compiler.getTask(
|
||||
null,
|
||||
files,
|
||||
null,
|
||||
List.of(
|
||||
"-d",
|
||||
tests.toString(),
|
||||
"-classpath",
|
||||
main.toString(),
|
||||
"-Adesigner.models=example.Person"),
|
||||
null,
|
||||
files.getJavaFileObjects(file.toFile()));
|
||||
task.setProcessors(List.of(new ModelMetadataProcessor()));
|
||||
assertTrue(task.call());
|
||||
}
|
||||
assertFalse(Files.exists(tests.resolve(ModelMetadata.resource("example.Person"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void missingSelectionFailsTheBuild() throws Exception {
|
||||
assertFalse(compile(source("Name"), "example.Missing", root.resolve("out")));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user