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
@@ -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())));
}
}