Generate versioned Model metadata and consume it in shared binding discovery

This commit is contained in:
2026-11-13 19:08:30 +01:00
parent 9bc4569407
commit adf432b98b
20 changed files with 700 additions and 15 deletions
+1 -1
View File
@@ -6,7 +6,7 @@ Shared Scene Builder Kit designer for NeitzelLib FX, with minimal standalone and
The first workflow is runnable in both hosts: Build and Refresh publishes immutable snapshots; authenticated target workers discover examples and render parent/Component FXML drafts into the shared Kit canvas. Source and supported canvas edits use host-owned documents and undo/redo. See [the connected workflow](docs/first-connected-workflow.md) for architecture, launch commands, evidence and limits. The first workflow is runnable in both hosts: Build and Refresh publishes immutable snapshots; authenticated target workers discover examples and render parent/Component FXML drafts into the shared Kit canvas. Source and supported canvas edits use host-owned documents and undo/redo. See [the connected workflow](docs/first-connected-workflow.md) for architecture, launch commands, evidence and limits.
The repository builds six shared Maven modules, a Maven capture plugin and a separate IntelliJ adapter plugin. Application code runs only in target JVMs. The shared UI now applies [source-preserving edits](docs/source-preserving-projection.md), supports nested/anonymous Components and fx:root bodies, and provides Source/Design/Split modes. Retained [target sessions, target sizing, interactive preview and Component/Row navigation](docs/target-runtime-sessions.md) now extend that workflow. The shared [binding discovery browser](docs/binding-discovery.md) now lists declared Model paths and direction-aware target compatibility. The [DirectBinding inspector](docs/direct-binding-inspector.md) creates, edits and removes declarations through source-preserving host Undo/Redo. Shared [Converter discovery and binding validation](docs/converter-validation.md) now adds capability-based proposals, document diagnostics and preview gating. [Custom ViewModel discovery and Computed-binding validation](docs/viewmodel-computed-validation.md) now follow the explicitly selected Controller or Component in both hosts. The [Computed-binding inspector](docs/computed-binding-inspector.md) now creates, changes and removes Computed declarations with method suggestions and shared source-preserving Undo/Redo. The [Repeat inspector](docs/repeat-binding-inspector.md) adds source-preserving collection declarations with host Undo/Redo and [collection/Component compatibility discovery](docs/repeat-compatibility.md). The shared [Table/Column inspector](docs/table-column-inspector.md) adds source-preserving Table configuration, ordered Column authoring and explicit Row navigation, now with [Table-specific compatibility and declared Row alignment discovery](docs/table-compatibility.md). The shared [reference-safe Structure editor](docs/reference-safe-structure.md) now renames identifiers, copies subtrees with fresh identifiers and deletes associated bindings through source-preserving host Undo/Redo, including destination validation for cross-View paste. The [shared Problems panel and IntelliJ Problems integration](docs/shared-diagnostics.md) now expose current diagnostics with authoritative FXML navigation and editor-scoped cleanup. Shared [source highlighting and diagnostic quick fixes](docs/source-highlighting-fixes.md) now add explicit source-preserving corrections with host Undo/Redo and native IntelliJ Alt+Enter actions. Broader discovery, generation and product packaging remain later work. The repository builds six shared Maven modules, a Maven capture plugin, a separate Model metadata processor and an IntelliJ adapter plugin. Application code runs only in target JVMs. The shared UI now applies [source-preserving edits](docs/source-preserving-projection.md), supports nested/anonymous Components and fx:root bodies, and provides Source/Design/Split modes. Retained [target sessions, target sizing, interactive preview and Component/Row navigation](docs/target-runtime-sessions.md) now extend that workflow. The shared [binding discovery browser](docs/binding-discovery.md) now lists declared Model paths and direction-aware target compatibility. The [DirectBinding inspector](docs/direct-binding-inspector.md) creates, edits and removes declarations through source-preserving host Undo/Redo. Shared [Converter discovery and binding validation](docs/converter-validation.md) now adds capability-based proposals, document diagnostics and preview gating. [Custom ViewModel discovery and Computed-binding validation](docs/viewmodel-computed-validation.md) now follow the explicitly selected Controller or Component in both hosts. The [Computed-binding inspector](docs/computed-binding-inspector.md) now creates, changes and removes Computed declarations with method suggestions and shared source-preserving Undo/Redo. The [Repeat inspector](docs/repeat-binding-inspector.md) adds source-preserving collection declarations with host Undo/Redo and [collection/Component compatibility discovery](docs/repeat-compatibility.md). The shared [Table/Column inspector](docs/table-column-inspector.md) adds source-preserving Table configuration, ordered Column authoring and explicit Row navigation, now with [Table-specific compatibility and declared Row alignment discovery](docs/table-compatibility.md). The shared [reference-safe Structure editor](docs/reference-safe-structure.md) now renames identifiers, copies subtrees with fresh identifiers and deletes associated bindings through source-preserving host Undo/Redo, including destination validation for cross-View paste. The [shared Problems panel and IntelliJ Problems integration](docs/shared-diagnostics.md) now expose current diagnostics with authoritative FXML navigation and editor-scoped cleanup. Shared [source highlighting and diagnostic quick fixes](docs/source-highlighting-fixes.md) now add explicit source-preserving corrections with host Undo/Redo and native IntelliJ Alt+Enter actions. Opt-in [generated Model metadata](docs/generated-model-metadata.md) now feeds the shared discovery and binding authoring workflows from immutable build snapshots. Java ViewModel/form generation and product packaging remain later work.
Configuration 1.0 schemas, context selection, local JDK mappings and draft protocol validation are now executable. See [configuration and protocol](docs/configuration-and-protocol.md) for formats and validation rules. Configuration 1.0 schemas, context selection, local JDK mappings and draft protocol validation are now executable. See [configuration and protocol](docs/configuration-and-protocol.md) for formats and validation rules.
@@ -0,0 +1,84 @@
package de.neitzel.fxdesigner.protocol;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.*;
/** Build-produced Model signatures, independent of host toolkit and application runtime. */
public record ModelMetadata(String model, List<Property> properties) {
public static final String VERSION = "1.0";
public static final int LIMIT = 256 * 1024;
public record Property(
String name, String type, String elementType, String reader, String writer) {
public Property {
for (String value : List.of(name, type, reader, writer))
if (!value.matches("[\\w.$\\[;]+"))
throw new IllegalArgumentException("Invalid Model signature");
if (!elementType.isEmpty() && !elementType.matches("[\\w.$\\[;]+"))
throw new IllegalArgumentException("Invalid element type");
}
}
public ModelMetadata {
if (!model.matches("[\\w$]+(\\.[\\w$]+)+"))
throw new IllegalArgumentException("Invalid Model type");
properties = List.copyOf(properties);
if (properties.size() > 256
|| properties.stream().map(Property::name).distinct().count() != properties.size())
throw new IllegalArgumentException("Duplicate or excessive Model properties");
}
public static String resource(String model) {
return "META-INF/neitzel-fx-designer/models/"
+ new ModelMetadata(model, List.of()).model()
+ ".properties";
}
public byte[] encode() {
var p = new TreeMap<String, String>();
p.put("schemaVersion", VERSION);
p.put("model", model);
p.put("count", "" + properties.size());
for (int i = 0; i < properties.size(); i++) {
var a = properties.get(i);
String k = "property." + i + ".";
p.put(k + "name", a.name());
p.put(k + "type", a.type());
p.put(k + "element", a.elementType());
p.put(k + "reader", a.reader());
p.put(k + "writer", a.writer());
}
var text = new StringBuilder();
p.forEach((k, v) -> text.append(k).append('=').append(v).append('\n'));
return text.toString().getBytes(StandardCharsets.UTF_8);
}
public static ModelMetadata read(InputStream input) throws IOException {
byte[] bytes = input.readNBytes(LIMIT + 1);
if (bytes.length > LIMIT) throw new IOException("Model metadata exceeds limit");
try {
var p = new Properties();
p.load(new InputStreamReader(new ByteArrayInputStream(bytes), StandardCharsets.UTF_8));
if (!VERSION.equals(p.getProperty("schemaVersion")))
throw new IllegalArgumentException("Unsupported Model metadata schema");
int count = Integer.parseInt(p.getProperty("count"));
if (count < 0 || count > 256 || p.size() != 3 + 5 * count)
throw new IllegalArgumentException("Invalid Model metadata fields");
var attributes = new ArrayList<Property>();
for (int i = 0; i < count; i++) {
String k = "property." + i + ".";
attributes.add(
new Property(
p.getProperty(k + "name"),
p.getProperty(k + "type"),
p.getProperty(k + "element"),
p.getProperty(k + "reader"),
p.getProperty(k + "writer")));
}
return new ModelMetadata(p.getProperty("model"), attributes);
} catch (RuntimeException e) {
throw new IOException("Invalid Model metadata: " + e.getMessage(), e);
}
}
}
@@ -0,0 +1,35 @@
package de.neitzel.fxdesigner.protocol;
import static org.junit.jupiter.api.Assertions.*;
import java.io.*;
import java.util.*;
import org.junit.jupiter.api.Test;
class ModelMetadataTest {
@Test
void deterministicRoundTripAndSchemaValidation() throws Exception {
var m =
new ModelMetadata(
"app.Person",
List.of(
new ModelMetadata.Property("name", "java.lang.String", "", "getName", "setName")));
assertEquals(m, ModelMetadata.read(new ByteArrayInputStream(m.encode())));
assertThrows(
IOException.class,
() ->
ModelMetadata.read(
new ByteArrayInputStream(
new String(m.encode(), java.nio.charset.StandardCharsets.UTF_8)
.replace("schemaVersion=1.0", "schemaVersion=9.0")
.getBytes(java.nio.charset.StandardCharsets.UTF_8))));
assertThrows(
IOException.class,
() -> ModelMetadata.read(new ByteArrayInputStream(new byte[ModelMetadata.LIMIT + 1])));
assertThrows(
IllegalArgumentException.class,
() ->
new ModelMetadata(
"app.Person", List.of(m.properties().getFirst(), m.properties().getFirst())));
}
}
@@ -1901,6 +1901,15 @@ public final class ProjectionHostChecks {
&& ((TextField) pane.lookup("#direct-path")).getText().equals("rank")), && ((TextField) pane.lookup("#direct-path")).getText().equals("rank")),
"Discovery did not populate binding form"); "Discovery did not populate binding form");
check(documentText(document).equals(full), "Discovery handoff changed source before Apply"); check(documentText(document).equals(full), "Discovery handoff changed source before Apply");
check(
fx(
() ->
((Label) pane.lookup("#binding-status"))
.getText()
.contains("Generated Model metadata 1.0: fixture.model.Person")),
"Host discovery did not use generated Person metadata");
System.out.println(
"PASS generated Model metadata: shared discovery and source-preserving binding authoring");
converterEdits(pane, document, full); converterEdits(pane, document, full);
directBindingEdits(pane, document, full); directBindingEdits(pane, document, full);
computedBindingEdits(pane, document, full); computedBindingEdits(pane, document, full);
+11
View File
@@ -0,0 +1,11 @@
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<parent><groupId>de.neitzel.fxdesigner</groupId><artifactId>neitzel-fx-designer</artifactId><version>0.1.0-SNAPSHOT</version></parent>
<artifactId>designer-model-processor</artifactId>
<properties><module.java.release>21</module.java.release></properties>
<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>
</dependencies>
<build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><configuration><proc>none</proc></configuration></plugin></plugins></build>
</project>
@@ -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);
}
}
@@ -0,0 +1 @@
de.neitzel.fxdesigner.processor.ModelMetadataProcessor
@@ -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")));
}
}
@@ -10,12 +10,14 @@ import javafx.beans.property.Property;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
import org.w3c.dom.*; import org.w3c.dom.*;
/** Signature-only fallback. Never invokes a getter, property accessor, provider or constructor. */ /** Generated-first, non-initializing discovery with shared compiled-signature fallback. */
public final class BindingDiscovery { public final class BindingDiscovery {
private static final String FX = "http://javafx.com/fxml/1"; private static final String FX = "http://javafx.com/fxml/1";
private final ClassLoader loader; private final ClassLoader loader;
private final List<String> diagnostics = new ArrayList<>(); private final List<String> diagnostics = new ArrayList<>();
private boolean incompleteModel; private boolean incompleteModel;
private final Map<Class<?>, List<PropertyDescriptor>> beanCache = new HashMap<>();
private final Map<String, String> generatedElements = new HashMap<>();
private ControllerMetadata owner; private ControllerMetadata owner;
private final List<Problem> selectionProblems = new ArrayList<>(); private final List<Problem> selectionProblems = new ArrayList<>();
private final Map<String, Class<?>> paths = new TreeMap<>(); private final Map<String, Class<?>> paths = new TreeMap<>();
@@ -94,22 +96,89 @@ public final class BindingDiscovery {
} }
private List<PropertyDescriptor> beans(Class<?> type) throws Exception { private List<PropertyDescriptor> beans(Class<?> type) throws Exception {
if (beanCache.containsKey(type)) return beanCache.get(type);
var result = modelBeans(type);
beanCache.put(type, result);
return result;
}
private List<PropertyDescriptor> modelBeans(Class<?> type) throws Exception {
if (type == Object.class || type.isInterface()) return List.of(); if (type == Object.class || type.isInterface()) return List.of();
// Explicit BeanInfo can execute application code; it is not a safe fallback source. // Custom BeanInfo is executable and may change runtime semantics. Never override it silently.
for (Class<?> c = type; c != null && c != Object.class; c = c.getSuperclass()) { for (Class<?> c = type; c != null && c != Object.class; c = c.getSuperclass()) {
try { try {
load(c.getName() + "BeanInfo"); load(c.getName() + "BeanInfo");
incompleteModel = true; incompleteModel = true;
diagnostics.add("Custom BeanInfo requires generated metadata: " + c.getName()); diagnostics.add(
"Custom BeanInfo requires explicit runtime metadata support: " + c.getName());
return List.of(); return List.of();
} catch (ClassNotFoundException absent) { } catch (ClassNotFoundException absent) {
} }
} }
return Arrays.stream( var compiled =
Introspector.getBeanInfo(type, Object.class, Introspector.IGNORE_ALL_BEANINFO) Arrays.stream(
.getPropertyDescriptors()) Introspector.getBeanInfo(type, Object.class, Introspector.IGNORE_ALL_BEANINFO)
.sorted(Comparator.comparing(PropertyDescriptor::getName)) .getPropertyDescriptors())
.toList(); .sorted(Comparator.comparing(PropertyDescriptor::getName))
.toList();
String resource = de.neitzel.fxdesigner.protocol.ModelMetadata.resource(type.getName());
var resources =
new LinkedHashSet<java.net.URL>(Collections.list(loader.getResources(resource)));
var own = type.getResource("/" + resource);
if (own != null) resources.add(own);
if (resources.isEmpty()) {
diagnostics.add("Compiled Model signatures: " + type.getName());
return compiled;
}
try {
if (resources.size() != 1) throw new IOException("Duplicate Model metadata resources");
de.neitzel.fxdesigner.protocol.ModelMetadata metadata;
try (var in = resources.iterator().next().openStream()) {
metadata = de.neitzel.fxdesigner.protocol.ModelMetadata.read(in);
}
if (!metadata.model().equals(type.getName())) throw new IOException("Wrong Model identity");
var eligible = new TreeMap<String, PropertyDescriptor>();
for (var p : compiled)
if (!p.getName().equals("latestMessage")
&& p.getReadMethod() != null
&& p.getWriteMethod() != null
&& p.getPropertyType() != null) eligible.put(p.getName(), p);
if (!eligible
.keySet()
.equals(
metadata.properties().stream()
.map(de.neitzel.fxdesigner.protocol.ModelMetadata.Property::name)
.collect(java.util.stream.Collectors.toSet())))
throw new IOException("Stale Model property inventory");
var result = new ArrayList<PropertyDescriptor>();
for (var p : metadata.properties()) {
var actual = eligible.get(p.name());
String element =
Collection.class.isAssignableFrom(actual.getPropertyType())
? element(actual.getReadMethod().getGenericReturnType())
: "";
if (!actual.getPropertyType().getName().equals(p.type())
|| !actual.getReadMethod().getName().equals(p.reader())
|| !actual.getWriteMethod().getName().equals(p.writer())
|| !element.equals(p.elementType()))
throw new IOException("Stale Model signature: " + p.name());
generatedElements.put(type.getName() + "." + p.name(), p.elementType());
result.add(actual);
}
diagnostics.add(
"Generated Model metadata "
+ de.neitzel.fxdesigner.protocol.ModelMetadata.VERSION
+ ": "
+ type.getName());
return List.copyOf(result);
} catch (Exception error) {
incompleteModel = true;
String message =
"Generated Model metadata rejected for " + type.getName() + ": " + error.getMessage();
diagnostics.add(message);
selectionProblems.add(new Problem("", "", "ERROR", message));
return List.of();
}
} }
private static boolean simple(Class<?> c) { private static boolean simple(Class<?> c) {
@@ -171,7 +240,11 @@ public final class BindingDiscovery {
path, path,
c.getName(), c.getName(),
kind, kind,
kind.equals("COLLECTION") ? element(p.getReadMethod().getGenericReturnType()) : "")); kind.equals("COLLECTION")
? generatedElements.getOrDefault(
type.getName() + "." + p.getName(),
element(p.getReadMethod().getGenericReturnType()))
: ""));
if (kind.equals("COMPLEX")) attributes(c, path + ".", next, depth + 1); if (kind.equals("COMPLEX")) attributes(c, path + ".", next, depth + 1);
} }
paths.put(prefix + "latestMessage", String.class); paths.put(prefix + "latestMessage", String.class);
@@ -0,0 +1,103 @@
package de.neitzel.fxdesigner.target;
import static org.junit.jupiter.api.Assertions.*;
import de.neitzel.fxdesigner.protocol.ModelMetadata;
import java.net.*;
import java.nio.file.*;
import java.util.*;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
class GeneratedModelMetadataTest {
@TempDir Path root;
public static class Model {
static {
System.setProperty("generated.model.initialized", "yes");
}
public Model() {
throw new AssertionError();
}
public String getName() {
throw new AssertionError();
}
public void setName(String v) {
throw new AssertionError();
}
}
String fxml() {
return "<?neitzel-model "
+ Model.class.getName()
+ "?><?import javafx.scene.control.TextField?><TextField"
+ " xmlns:fx='http://javafx.com/fxml/1' fx:id='input'/>";
}
ModelMetadata metadata() {
return new ModelMetadata(
Model.class.getName(),
List.of(new ModelMetadata.Property("name", "java.lang.String", "", "getName", "setName")));
}
void write(Path base, byte[] bytes) throws Exception {
Path p = base.resolve(ModelMetadata.resource(Model.class.getName()));
Files.createDirectories(p.getParent());
Files.write(p, bytes);
}
@Test
void generatedAndFallbackHaveSameAuthoringSemanticsWithoutInitialization() throws Exception {
var fallback = BindingDiscovery.inspect(fxml(), "name", "both", getClass().getClassLoader());
write(root, metadata().encode());
try (var loader =
new URLClassLoader(new URL[] {root.toUri().toURL()}, getClass().getClassLoader())) {
var generated = BindingDiscovery.inspect(fxml(), "name", "both", loader);
assertEquals(fallback.attributes(), generated.attributes());
assertEquals(fallback.targets(), generated.targets());
assertEquals(fallback.problems(), generated.problems());
assertTrue(
generated.diagnostics().stream()
.anyMatch(d -> d.startsWith("Generated Model metadata 1.0:")));
assertNull(System.getProperty("generated.model.initialized"));
}
}
@Test
void staleMalformedAndDuplicateMetadataAreErrorsNotSilentFallback() throws Exception {
for (byte[] bytes :
List.of(
"schemaVersion=9.0".getBytes(),
new ModelMetadata(Model.class.getName(), List.of()).encode(),
new ModelMetadata(
Model.class.getName(),
List.of(new ModelMetadata.Property("name", "int", "", "getName", "setName")))
.encode())) {
write(root, bytes);
try (var loader =
new URLClassLoader(new URL[] {root.toUri().toURL()}, getClass().getClassLoader())) {
var result = BindingDiscovery.inspect(fxml(), "name", "both", loader);
assertTrue(
result.problems().stream()
.anyMatch(
p ->
p.severity().equals("ERROR") && p.message().contains("metadata rejected")));
assertFalse(result.attributes().stream().anyMatch(a -> a.path().equals("name")));
}
}
write(root, metadata().encode());
Path second = root.resolve("second");
write(second, metadata().encode());
try (var loader =
new URLClassLoader(
new URL[] {root.toUri().toURL(), second.toUri().toURL()},
getClass().getClassLoader())) {
assertTrue(
BindingDiscovery.inspect(fxml(), "name", "both", loader).problems().stream()
.anyMatch(p -> p.message().contains("Duplicate Model metadata")));
}
}
}
+2 -2
View File
@@ -14,7 +14,7 @@ For Person the browser exposes `name`, `rank`, `home`, `home.street`, and `visit
The fresh target worker loads classes without initialization and inspects signatures. It never constructs Models, Controllers, providers, controls or Converters, never invokes getters/property accessors, and never starts JavaFX. Application types stay outside both hosts. The independently negotiated `bindings-v5` capability adds the BINDINGS operation; it uses bounded UTF-8 metadata, build/context identity and document revisions, with zero image dimensions. Existing wire operation ordinals remain unchanged. The fresh target worker loads classes without initialization and inspects signatures. It never constructs Models, Controllers, providers, controls or Converters, never invokes getters/property accessors, and never starts JavaFX. Application types stay outside both hosts. The independently negotiated `bindings-v5` capability adds the BINDINGS operation; it uses bounded UTF-8 metadata, build/context identity and document revisions, with zero image dimensions. Existing wire operation ordinals remain unchanged.
Model fallback follows NeitzelLib's public read/write JavaBean Attribute rules and simple/complex/collection classification. Nested paths expand to depth four with cycle diagnostics; collection paths expose declared element types without pretending a collection is a single nested Model. Explicit custom BeanInfo is not executed: types requiring it report a generated-metadata diagnostic. Custom/replacement ViewModels, built-in message properties and Computed methods now follow the [signature-based selection rules](viewmodel-computed-validation.md). Generated metadata and arbitrary dynamic property APIs remain unresolved. Model fallback follows NeitzelLib's public read/write JavaBean Attribute rules and simple/complex/collection classification. Nested paths expand to depth four with cycle diagnostics; collection paths expose declared element types without pretending a collection is a single nested Model. Explicit custom BeanInfo is not executed: types requiring it remain unverified, including when ordinary generated bean signatures exist. Custom/replacement ViewModels, built-in message properties and Computed methods now follow the [signature-based selection rules](viewmodel-computed-validation.md). Generated metadata is now supported by the [opt-in processor](generated-model-metadata.md); arbitrary dynamic property APIs remain unresolved.
Targets come from public instance JavaFX observable-property accessors. The runtime requires writable `Property` accessors, so read-only targets remain visible as incompatible. Generic observable signatures and getter types identify the value type; incomplete or contradictory signatures remain unresolved. Component `model` targets use the Component resource's explicit Model declaration instead of treating Object as a specific type. Component resource metadata comes from the selected build; current-document metadata comes from the captured draft. Targets come from public instance JavaFX observable-property accessors. The runtime requires writable `Property` accessors, so read-only targets remain visible as incompatible. Generic observable signatures and getter types identify the value type; incomplete or contradictory signatures remain unresolved. Component `model` targets use the Component resource's explicit Model declaration instead of treating Object as a specific type. Component resource metadata comes from the selected build; current-document metadata comes from the captured draft.
@@ -24,7 +24,7 @@ Compatibility uses boxed declared types and target-runtime assignability: `view`
Discovery owns a separate workflow so it does not replace the canvas or interactive worker. It uses a leased successful build and a captured current-document overlay. Generation and document-revision checks reject obsolete replies. Document changes clear the target list and require rediscovery; Build and Refresh invalidates the panel. Explicit retained-build selection is identified in the displayed provenance. Closed editors cancel their metadata worker. Discovery owns a separate workflow so it does not replace the canvas or interactive worker. It uses a leased successful build and a captured current-document overlay. Generation and document-revision checks reject obsolete replies. Document changes clear the target list and require rediscovery; Build and Refresh invalidates the panel. Explicit retained-build selection is identified in the displayed provenance. Closed editors cancel their metadata worker.
FXML parsing disables external entities and DOCTYPE. Inputs, scene-node counts, path expansion, target counts and protocol payloads are bounded. Ambiguous imports, duplicate IDs, missing classes, invalid XML and absent declarations produce diagnostics. Attached-property binding discovery, open child-draft metadata overlays, generated metadata precedence and complete collection compatibility remain later extensions, not silently inferred support. FXML parsing disables external entities and DOCTYPE. Inputs, scene-node counts, path expansion, target counts and protocol payloads are bounded. Ambiguous imports, duplicate IDs, missing classes, invalid XML and absent declarations produce diagnostics. Attached-property binding discovery, open child-draft metadata overlays, broader custom metadata and complete collection compatibility remain later extensions, not silently inferred support.
## Qualification ## Qualification
+52
View File
@@ -0,0 +1,52 @@
# Generated Model metadata for discovery and authoring
Applications can now opt into the separate `designer-model-processor` annotation processor. It emits versioned Model property descriptions during compilation, without loading or executing application classes. Both hosts consume those descriptions through the same isolated target discovery worker. The shared browser identifies **Generated Model metadata 1.0** or **Compiled Model signatures** in its provenance. Direct bindings, nested paths, Repeat collections and Table discovery use the same resulting Attribute description.
## Application setup
Configure the application module's Maven compiler plugin (3.14.1 in the qualified fixtures):
```xml
<configuration>
<annotationProcessorPaths>
<path>
<groupId>de.neitzel.fxdesigner</groupId>
<artifactId>designer-model-processor</artifactId>
<version>0.1.0-SNAPSHOT</version>
</path>
</annotationProcessorPaths>
<compilerArgs>
<arg>-Adesigner.models=com.example.Person,com.example.Address</arg>
</compilerArgs>
</configuration>
```
Keep the selection in the checked-in POM. Merge this configuration with any existing annotation processor paths and compiler arguments. No Model annotation, runtime dependency, FXML rewrite, or host-specific configuration is needed. The explicit FXML `neitzel-model` processing instruction remains authoritative for each View. A missing selected type fails compilation. An empty selection generates nothing. Only Models belonging to the current compilation source set are emitted; test compilations and dependent modules do not duplicate resources for already compiled Models.
The processor emits public read/write JavaBean signatures for selected types and nested bean types, with inherited members, boolean `is` precedence and declared collection element types. Read-only properties and the runtime's reserved `latestMessage` are excluded from the generated property list. Target discovery supplies built-in properties and applies the existing simple/complex/collection rules, path depth/cycle limits, ViewModel selection and direction-aware compatibility. Metadata does not initialize Models, call accessors, construct Controllers, or run custom BeanInfo.
## Format and integrity
Each Model has one UTF-8 resource at `META-INF/neitzel-fx-designer/models/<binary-class-name>.properties` in the class output and packaged application JAR. Schema `1.0` contains the Model identity and an ordered list of names, binary value types, declared collection element types, readers and writers. Output is deterministic and contains no timestamp or machine paths. The shared `ModelMetadata` contract bounds input size to 256 KiB and property count to 256; unknown versions and invalid structures are rejected.
Target discovery prefers generated descriptions and verifies the complete eligible property inventory and signatures against the captured compiled type. This integrity check uses non-initializing reflection, not application execution. No claim of reflection-free discovery or improved performance is made. Absent metadata uses the existing compiled-signature fallback. Present but malformed, duplicate, incompatible or stale metadata is an error, with no silent fallback; normal invalid-draft saving remains available and existing validation gates block new previews. Custom BeanInfo semantics remain unverified and are not silently replaced by generated ordinary-bean signatures. Unsupported generic signatures remain subject to conservative validation rather than guessed types.
Build and Refresh already performs `clean package` in an isolated reactor copy. It captures generated resources with the same immutable outputs and dependency identities used for preview. Changed Models or selection settings invalidate the build input identity. Clean rebuilds remove obsolete properties and deselected metadata; callers building outside the designer must also use a clean build after removals or selection changes. Arbitrary incremental javac output cleanup is not implemented. A failed build cannot activate partial metadata, and explicit last-successful-build use keeps its earlier metadata and outdated label.
## Boundaries and validation
This is build-time Model metadata for existing discovery and binding authoring. It does not generate Java ViewModel subclasses, a ViewModel registry, editable FXML forms, labels/control defaults, arbitrary BeanInfo metadata, Java source origins or project-wide Model selection UI. Those retain their later roadmap gates. The processor is a Java 21 build artifact depending only on the contracts; it is not packaged in either host or required on the application runtime path. Wire DTOs and protocol versions do not change.
Automated tests compile real source with the processor, verify deterministic inherited/collection signatures and non-execution, clean-build removal, missing selections, schema bounds, generated/fallback equivalence, stale signatures and duplicate resources. Real classpath and module-path fixture processes require generated Person and nested Address metadata and retain their non-initialization assertions. The live shared-host scenario requires generated metadata provenance before handing discovery to the binding inspector, then runs existing source-preserving binding authoring and Undo/Redo tests in standalone and registered IntelliJ. No manual testing is requested; earlier skipped manual checks remain skipped.
Run the bootstrap and registered IntelliJ qualification commands in the [README](../README.md).
### Qualification — 2026-11-13
The clean shared build passes 135 Maven tests and ten IntelliJ adapter tests. The real Java 21 fixtures pass 36 target processes with 518 checks, including generated Person/Address resource use in both launch modes and non-initialization assertions. Native standalone classpath and module-path scenarios pass generated discovery, existing Direct/Computed/Repeat/Table authoring, source-preserving edits and Undo/Redo. The lifecycle suite retains its 48-cycle and abrupt-owner-death checks.
The first fixture integration exposed duplicate resources from Maven test compilation inheriting the processor configuration. Restricting emission to the current compilation's source types fixed this; a dependent-compilation regression now verifies that compiled Models are not emitted twice. Generated output remains part of the normal immutable snapshot and packaged module resources.
The full bootstrap passed, including Java 21 bytecode/linkage, plugin packaging exclusions and documentation checks. Registered IntelliJ classpath qualification also passed generated discovery, source-preserving authoring, native diagnostics/Undo/Redo, exclusive JavaFX 26 natives and worker shutdown. The first registered module-path attempt passed generated discovery but timed out at the existing Table preview-gating assertion; it is retained as reliability evidence. An unchanged module-path-only rerun of the registered qualification harness passed all assertions, exclusive JavaFX 26 native checks and worker shutdown (`/tmp/model-metadata-ide-modulepath-retry.log`). The startup/persistence/preview-gate reliability follow-up remains open; no assertion was weakened. No manual testing was used.
Local evidence: `/tmp/model-metadata-bootstrap2.log`, `/tmp/model-metadata-registered-ide.log`, `designer-target-tools/target/preview-fixtures/`, `target/connected-workflow/` and `target/ide-workflow/`. Earlier development evidence is retained in `/tmp/model-metadata-build.log`, `/tmp/model-metadata-build2.log` and `/tmp/model-metadata-bootstrap.log`. Generated qualification artifacts are not repository inputs.
+1
View File
@@ -27,6 +27,7 @@ The artifact names below are established by work package 3. See [implemented bou
| `designer-intellij` | FXML editor, IDE Documents/commands, project/build/navigation integration and focus | Adapts the same core/UI; qualify build and packaging against the selected IDE SDK | | `designer-intellij` | FXML editor, IDE Documents/commands, project/build/navigation integration and focus | Adapts the same core/UI; qualify build and packaging against the selected IDE SDK |
| `designer-preview-api` | Typed example descriptors and fresh preview-session lifecycle | Small project-facing artifact; no Kit or IntelliJ dependency | | `designer-preview-api` | Typed example descriptors and fresh preview-session lifecycle | Small project-facing artifact; no Kit or IntelliJ dependency |
| `designer-target-tools` | Discovery and NeitzelLib loading/rendering adapter in the application runtime | Uses contracts/preview API and target-compatible library dependencies; never execute application classes in a host | | `designer-target-tools` | Discovery and NeitzelLib loading/rendering adapter in the application runtime | Uses contracts/preview API and target-compatible library dependencies; never execute application classes in a host |
| `designer-model-processor` | Opt-in build-time Model signature resources | Contracts only; no application execution or host dependency |
| `designer-maven-plugin` | Effective project/dependency/runtime capture in the selected Maven build JVM | No Kit/IntelliJ or target application runtime dependency | | `designer-maven-plugin` | Effective project/dependency/runtime capture in the selected Maven build JVM | No Kit/IntelliJ or target application runtime dependency |
| `integration-fixtures/` | Real classpath and module-path Maven applications, examples and failure cases | Same scenarios consumed by both hosts; normal artifact dependencies | | `integration-fixtures/` | Real classpath and module-path Maven applications, examples and failure cases | Same scenarios consumed by both hosts; normal artifact dependencies |
+4
View File
@@ -145,3 +145,7 @@ The [shared diagnostics slice](shared-diagnostics.md) adds a View-wide Problems
## Source highlighting and diagnostic quick fixes ## Source highlighting and diagnostic quick fixes
The [shared highlighting/fixes slice](source-highlighting-fixes.md) adds current source underlines, explicit direction corrections and conservative declaration removal. Both hosts apply lexical edits through their authoritative Undo/Redo transaction. IntelliJ adds native XML annotations and Alt+Enter actions, with revision and publication guards. Generated metadata, richer Java/Column/Row origins, further semantic fixes and project-wide diagnostics remain open. The [shared highlighting/fixes slice](source-highlighting-fixes.md) adds current source underlines, explicit direction corrections and conservative declaration removal. Both hosts apply lexical edits through their authoritative Undo/Redo transaction. IntelliJ adds native XML annotations and Alt+Enter actions, with revision and publication guards. Generated metadata, richer Java/Column/Row origins, further semantic fixes and project-wide diagnostics remain open.
## Generated Model metadata
The [generated Model metadata slice](generated-model-metadata.md) adds an opt-in compiler processor, versioned deterministic resources, generated-first target discovery with signature integrity checks, and shared provenance in both hosts. Existing binding authoring consumes the resulting Attributes without host-specific logic or new undo paths. Missing metadata retains compiled fallback; invalid present metadata is diagnosed. Java ViewModel/registry and FXML form generation, custom BeanInfo semantics and broader source origins remain later work.
@@ -26,4 +26,11 @@
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
</dependencies> </dependencies>
<build><plugins><plugin>
<groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths><path><groupId>de.neitzel.fxdesigner</groupId><artifactId>designer-model-processor</artifactId><version>0.1.0-SNAPSHOT</version></path></annotationProcessorPaths>
<compilerArgs><arg>-Adesigner.models=fixture.model.Person,fixture.model.Address</arg></compilerArgs>
</configuration>
</plugin></plugins></build>
</project> </project>
@@ -70,6 +70,15 @@ public final class PreviewChecks {
xml = new String(in.readAllBytes(), java.nio.charset.StandardCharsets.UTF_8); xml = new String(in.readAllBytes(), java.nio.charset.StandardCharsets.UTF_8);
} }
var metadata = BindingDiscovery.inspect(xml, "home", "both", loader); var metadata = BindingDiscovery.inspect(xml, "home", "both", loader);
check(
metadata.diagnostics().contains("Generated Model metadata 1.0: fixture.model.Person"),
"Generated Person metadata not used: " + metadata.diagnostics());
check(
metadata.diagnostics().contains("Generated Model metadata 1.0: fixture.model.Address"),
"Generated nested metadata not used");
check(
metadata.problems().stream().noneMatch(p -> p.message().contains("metadata rejected")),
"Generated metadata rejected");
check( check(
metadata.attributes().stream().anyMatch(a -> a.path().equals("home.street")), metadata.attributes().stream().anyMatch(a -> a.path().equals("home.street")),
"Nested metadata"); "Nested metadata");
@@ -20,4 +20,11 @@
<scope>compile</scope> <scope>compile</scope>
</dependency> </dependency>
</dependencies> </dependencies>
<build><plugins><plugin>
<groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths><path><groupId>de.neitzel.fxdesigner</groupId><artifactId>designer-model-processor</artifactId><version>0.1.0-SNAPSHOT</version></path></annotationProcessorPaths>
<compilerArgs><arg>-Adesigner.models=fixture.model.Person,fixture.model.Address</arg></compilerArgs>
</configuration>
</plugin></plugins></build>
</project> </project>
@@ -70,6 +70,15 @@ public final class PreviewChecks {
xml = new String(in.readAllBytes(), java.nio.charset.StandardCharsets.UTF_8); xml = new String(in.readAllBytes(), java.nio.charset.StandardCharsets.UTF_8);
} }
var metadata = BindingDiscovery.inspect(xml, "home", "both", loader); var metadata = BindingDiscovery.inspect(xml, "home", "both", loader);
check(
metadata.diagnostics().contains("Generated Model metadata 1.0: fixture.model.Person"),
"Generated Person metadata not used: " + metadata.diagnostics());
check(
metadata.diagnostics().contains("Generated Model metadata 1.0: fixture.model.Address"),
"Generated nested metadata not used");
check(
metadata.problems().stream().noneMatch(p -> p.message().contains("metadata rejected")),
"Generated metadata rejected");
check( check(
metadata.attributes().stream().anyMatch(a -> a.path().equals("home.street")), metadata.attributes().stream().anyMatch(a -> a.path().equals("home.street")),
"Nested metadata"); "Nested metadata");
+1 -1
View File
@@ -8,7 +8,7 @@
<version>0.1.0-SNAPSHOT</version> <version>0.1.0-SNAPSHOT</version>
<packaging>pom</packaging> <packaging>pom</packaging>
<modules> <modules>
<module>designer-contracts</module><module>designer-preview-api</module> <module>designer-contracts</module><module>designer-model-processor</module><module>designer-preview-api</module>
<module>designer-core</module><module>designer-kit-ui</module> <module>designer-core</module><module>designer-kit-ui</module>
<module>designer-standalone</module><module>designer-target-tools</module><module>designer-maven-plugin</module> <module>designer-standalone</module><module>designer-target-tools</module><module>designer-maven-plugin</module>
</modules> </modules>
+2 -2
View File
@@ -24,7 +24,7 @@ wrapper = ROOT / ("mvnw.cmd" if os.name == "nt" else "mvnw")
run([sys.executable, ROOT / "scripts/check-neitzellib-artifact.py"]) run([sys.executable, ROOT / "scripts/check-neitzellib-artifact.py"])
run([wrapper, "-B", "-ntp", "clean", "install"]) run([wrapper, "-B", "-ntp", "clean", "install"])
run([wrapper, "-B", "-ntp", "-pl", "designer-target-tools", "org.apache.maven.plugins:maven-dependency-plugin:3.6.1:build-classpath", "-DincludeScope=runtime", "-Dmdep.outputFile=target/runtime-classpath.txt"]) run([wrapper, "-B", "-ntp", "-pl", "designer-target-tools", "org.apache.maven.plugins:maven-dependency-plugin:3.6.1:build-classpath", "-DincludeScope=runtime", "-Dmdep.outputFile=target/runtime-classpath.txt"])
for module in ("contracts", "preview-api", "core", "kit-ui", "standalone", "target-tools", "maven-plugin"): for module in ("contracts", "model-processor", "preview-api", "core", "kit-ui", "standalone", "target-tools", "maven-plugin"):
name = "designer-" + module name = "designer-" + module
expected = 69 if module in ("kit-ui", "standalone") else 65 expected = 69 if module in ("kit-ui", "standalone") else 65
with zipfile.ZipFile(ROOT / name / "target" / (name + "-0.1.0-SNAPSHOT.jar")) as jar: with zipfile.ZipFile(ROOT / name / "target" / (name + "-0.1.0-SNAPSHOT.jar")) as jar:
@@ -46,7 +46,7 @@ run([sys.executable, ROOT / "scripts/check-connected-workflow.py", "--idea-home"
with zipfile.ZipFile(next((ROOT / "designer-intellij/build/distributions").glob("*.zip"))) as plugin: with zipfile.ZipFile(next((ROOT / "designer-intellij/build/distributions").glob("*.zip"))) as plugin:
names = plugin.namelist() names = plugin.namelist()
assert any("javafx-graphics-26-mac" in n for n in names), "Missing host JavaFX implementation" assert any("javafx-graphics-26-mac" in n for n in names), "Missing host JavaFX implementation"
assert not any("designer-target-tools" in n or "designer-preview-api" in n or "designer-maven-plugin" in n or "/fx-1.0" in n for n in names), "Target dependency in IDE host" assert not any("designer-target-tools" in n or "designer-preview-api" in n or "designer-model-processor" in n or "designer-maven-plugin" in n or "/fx-1.0" in n for n in names), "Target dependency in IDE host"
for document in [ROOT / "README.md", ROOT / "AGENTS.md", ROOT / "THIRD-PARTY-NOTICES.md", *sorted((ROOT / "docs").rglob("*.md"))]: for document in [ROOT / "README.md", ROOT / "AGENTS.md", ROOT / "THIRD-PARTY-NOTICES.md", *sorted((ROOT / "docs").rglob("*.md"))]:
content = document.read_text(encoding="utf-8") content = document.read_text(encoding="utf-8")
for example in re.findall(r"```json\n(.*?)\n```", content, re.S): for example in re.findall(r"```json\n(.*?)\n```", content, re.S):