Discover Table compatibility and declared Row alignment
This commit is contained in:
+8
-2
@@ -325,6 +325,9 @@ public final class BindingDiscovery {
|
||||
? List.<BindingMetadata.Converter>of()
|
||||
: converters.candidates(source, chosen == null ? null : chosen.type(), direction);
|
||||
repeats = new RepeatDiscovery(loader, attributes, owner, incompleteModel, controlTypes, ids);
|
||||
var tables = new TableDiscovery(loader, repeats, modelType, owner);
|
||||
var problems = new ArrayList<>(validate(doc, imports, ids, converters));
|
||||
problems.addAll(tables.validate(doc));
|
||||
return new BindingMetadata(
|
||||
model,
|
||||
selected,
|
||||
@@ -333,13 +336,16 @@ public final class BindingDiscovery {
|
||||
targets,
|
||||
diagnostics,
|
||||
candidates,
|
||||
validate(doc, imports, ids, converters),
|
||||
problems,
|
||||
owner.controller == null ? "" : owner.controller.getName(),
|
||||
owner.viewModel == null ? "" : owner.viewModel.getName(),
|
||||
owner.methods(chosen == null ? null : chosen.type()),
|
||||
repeats.collections(),
|
||||
repeats.candidates(selected),
|
||||
repeats.panes());
|
||||
repeats.panes(),
|
||||
tables.collections(),
|
||||
tables.rows(selected),
|
||||
tables.columns(selected));
|
||||
}
|
||||
|
||||
private void selectOwner(Document doc, List<String> imports, String resource, Class<?> model)
|
||||
|
||||
+16
-10
@@ -133,10 +133,13 @@ final class RepeatDiscovery {
|
||||
}
|
||||
|
||||
RepeatOption component(String name, String path) {
|
||||
return component(name, collection(path));
|
||||
}
|
||||
|
||||
RepeatOption component(String name, RepeatOption source) {
|
||||
ComponentInfo info = components.computeIfAbsent(name, this::readComponent);
|
||||
if (info.error() != null)
|
||||
return new RepeatOption(name, info.declared(), "INCOMPATIBLE", info.error());
|
||||
var source = collection(path);
|
||||
if (source.status().equals("INCOMPATIBLE"))
|
||||
return new RepeatOption(
|
||||
name, info.declared(), "UNVERIFIED", "Choose a collection to compare element types");
|
||||
@@ -215,15 +218,7 @@ final class RepeatDiscovery {
|
||||
|| !ControllerMetadata.accessible(accessor.getDeclaringClass(), true))
|
||||
throw new IllegalArgumentException("Component needs an accessible writable modelProperty");
|
||||
Class<?> property = BindingDiscovery.propertyType(accessor.getGenericReturnType(), Map.of());
|
||||
var resource = type.getResource(type.getSimpleName() + ".fxml");
|
||||
if (resource == null)
|
||||
throw new IllegalArgumentException("Component FXML unavailable: " + name);
|
||||
Document doc;
|
||||
try (var in = resource.openStream()) {
|
||||
byte[] bytes = in.readNBytes(512 * 1024 + 1);
|
||||
if (bytes.length > 512 * 1024) throw new IOException("Component FXML exceeds limit");
|
||||
doc = BindingDiscovery.parse(new String(bytes, StandardCharsets.UTF_8));
|
||||
}
|
||||
Document doc = resource(type);
|
||||
Element root = doc.getDocumentElement();
|
||||
if (!"http://javafx.com/fxml/1".equals(root.getNamespaceURI())
|
||||
|| !"root".equals(root.getLocalName()))
|
||||
@@ -255,6 +250,17 @@ final class RepeatDiscovery {
|
||||
}
|
||||
}
|
||||
|
||||
static Document resource(Class<?> type) throws Exception {
|
||||
var resource = type.getResource(type.getSimpleName() + ".fxml");
|
||||
if (resource == null)
|
||||
throw new IllegalArgumentException("Component FXML unavailable: " + type.getName());
|
||||
try (var in = resource.openStream()) {
|
||||
byte[] bytes = in.readNBytes(512 * 1024 + 1);
|
||||
if (bytes.length > 512 * 1024) throw new IOException("Component FXML exceeds limit");
|
||||
return BindingDiscovery.parse(new String(bytes, StandardCharsets.UTF_8));
|
||||
}
|
||||
}
|
||||
|
||||
private void indexResources() throws IOException {
|
||||
var entries = new LinkedHashSet<String>();
|
||||
for (String key : List.of("java.class.path", "jdk.module.path"))
|
||||
|
||||
@@ -0,0 +1,351 @@
|
||||
package de.neitzel.fxdesigner.target;
|
||||
|
||||
import de.neitzel.fx.view.ViewModel;
|
||||
import de.neitzel.fxdesigner.protocol.BindingMetadata.*;
|
||||
import java.beans.*;
|
||||
import java.lang.reflect.*;
|
||||
import java.time.temporal.*;
|
||||
import java.util.*;
|
||||
import org.w3c.dom.*;
|
||||
|
||||
/** Table's bean-Attribute rules and declared Row positions; never executes application code. */
|
||||
final class TableDiscovery {
|
||||
private static final String FX = "http://javafx.com/fxml/1", P = "de.neitzel.fx.view.";
|
||||
private final ClassLoader loader;
|
||||
private final RepeatDiscovery repeats;
|
||||
private final Class<?> model;
|
||||
private final boolean unknownOwner;
|
||||
|
||||
TableDiscovery(
|
||||
ClassLoader loader, RepeatDiscovery repeats, Class<?> model, ControllerMetadata owner) {
|
||||
this.loader = loader;
|
||||
this.repeats = repeats;
|
||||
this.model =
|
||||
owner.viewModel != null && !ViewModel.class.isAssignableFrom(owner.viewModel)
|
||||
? owner.viewModel
|
||||
: model;
|
||||
unknownOwner = owner.unknownController || owner.viewModel == null;
|
||||
}
|
||||
|
||||
private Class<?> load(String name) throws ClassNotFoundException {
|
||||
return Class.forName(name, false, loader);
|
||||
}
|
||||
|
||||
// Custom BeanInfo is executable. Absence of safe evidence must not be reported as
|
||||
// incompatibility.
|
||||
private List<PropertyDescriptor> beans(Class<?> type) throws Exception {
|
||||
if (type == null || type == Object.class || type.isInterface())
|
||||
throw new IllegalArgumentException("Bean Attributes unavailable");
|
||||
for (Class<?> c = type; c != null && c != Object.class; c = c.getSuperclass())
|
||||
try {
|
||||
load(c.getName() + "BeanInfo");
|
||||
throw new IllegalArgumentException("Custom BeanInfo requires generated metadata");
|
||||
} catch (ClassNotFoundException ignored) {
|
||||
}
|
||||
return Arrays.stream(
|
||||
Introspector.getBeanInfo(type, Object.class, Introspector.IGNORE_ALL_BEANINFO)
|
||||
.getPropertyDescriptors())
|
||||
.sorted(Comparator.comparing(PropertyDescriptor::getName))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private static boolean writable(PropertyDescriptor p) {
|
||||
return p.getReadMethod() != null && p.getWriteMethod() != null;
|
||||
}
|
||||
|
||||
private static Class<?> argument(Type t) {
|
||||
if (!(t instanceof ParameterizedType p) || p.getActualTypeArguments().length != 1) return null;
|
||||
Type a = p.getActualTypeArguments()[0];
|
||||
if (a instanceof Class<?> c) return c;
|
||||
if (a instanceof WildcardType w
|
||||
&& w.getUpperBounds().length == 1
|
||||
&& w.getUpperBounds()[0] instanceof Class<?> c) return c;
|
||||
return null;
|
||||
}
|
||||
|
||||
RepeatOption collection(String path) {
|
||||
if (unknownOwner)
|
||||
return option(path, "?", "UNVERIFIED", "Table Model behind ViewModel is unresolved");
|
||||
try {
|
||||
var p = beans(model).stream().filter(b -> b.getName().equals(path)).findFirst().orElse(null);
|
||||
if (p == null || !writable(p))
|
||||
return option(
|
||||
path,
|
||||
"?",
|
||||
"INCOMPATIBLE",
|
||||
"Table requires a direct read/write bean collection Attribute: " + path);
|
||||
if (!Collection.class.isAssignableFrom(p.getReadMethod().getReturnType()))
|
||||
return option(path, "?", "INCOMPATIBLE", "Table Attribute is not a collection: " + path);
|
||||
Class<?> element = argument(p.getReadMethod().getGenericReturnType());
|
||||
if (element == null)
|
||||
return option(
|
||||
path, "?", "INCOMPATIBLE", "Table cannot read the collection's declared element class");
|
||||
var backing = repeats.collection(path);
|
||||
return option(
|
||||
path,
|
||||
element.getName(),
|
||||
backing.status(),
|
||||
"Table bean element type; " + backing.reason());
|
||||
} catch (Exception | LinkageError e) {
|
||||
return option(path, "?", "UNVERIFIED", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
List<RepeatOption> collections() {
|
||||
var names = new TreeSet<String>();
|
||||
repeats.collections().forEach(c -> names.add(c.name()));
|
||||
try {
|
||||
for (var b : beans(model))
|
||||
if (b.getReadMethod() != null
|
||||
&& Collection.class.isAssignableFrom(b.getReadMethod().getReturnType()))
|
||||
names.add(b.getName());
|
||||
} catch (Exception | LinkageError ignored) {
|
||||
}
|
||||
return names.stream().map(this::collection).toList();
|
||||
}
|
||||
|
||||
List<RepeatOption> rows(String path) {
|
||||
var source = collection(path);
|
||||
return repeats.candidates(path).stream().map(c -> repeats.component(c.name(), source)).toList();
|
||||
}
|
||||
|
||||
private static boolean simple(Class<?> type) {
|
||||
return type == String.class
|
||||
|| type == Boolean.class
|
||||
|| type.isEnum()
|
||||
|| type.isPrimitive() && type != char.class && type != void.class
|
||||
|| Number.class.isAssignableFrom(type)
|
||||
|| Date.class.isAssignableFrom(type)
|
||||
|| Calendar.class.isAssignableFrom(type)
|
||||
|| TemporalAccessor.class.isAssignableFrom(type)
|
||||
|| TemporalAmount.class.isAssignableFrom(type);
|
||||
}
|
||||
|
||||
List<RepeatOption> columns(String path) {
|
||||
var c = collection(path);
|
||||
if (c.type().equals("?")) return List.of();
|
||||
try {
|
||||
var result = new ArrayList<RepeatOption>();
|
||||
for (var b : beans(load(c.type()))) {
|
||||
Class<?> type = b.getPropertyType();
|
||||
boolean ok = writable(b) && type != null && simple(type);
|
||||
result.add(
|
||||
option(
|
||||
b.getName(),
|
||||
type == null ? "?" : type.getName(),
|
||||
ok ? "COMPATIBLE" : "INCOMPATIBLE",
|
||||
ok
|
||||
? "Simple read/write Table Attribute"
|
||||
: "Table Columns require simple read/write bean Attributes"));
|
||||
if (result.size() > 256) throw new IllegalArgumentException("Too many Table Attributes");
|
||||
}
|
||||
return List.copyOf(result);
|
||||
} catch (Exception | LinkageError e) {
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
private RepeatOption column(String path, String attribute) {
|
||||
var c = collection(path);
|
||||
if (c.type().equals("?"))
|
||||
return option(
|
||||
attribute, "?", "UNVERIFIED", "Column type unavailable without collection element type");
|
||||
try {
|
||||
var b =
|
||||
beans(load(c.type())).stream()
|
||||
.filter(p -> p.getName().equals(attribute))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
if (b == null || !writable(b) || b.getPropertyType() == null || !simple(b.getPropertyType()))
|
||||
return option(
|
||||
attribute,
|
||||
"?",
|
||||
"INCOMPATIBLE",
|
||||
"Column requires a simple read/write Attribute: " + attribute);
|
||||
return option(
|
||||
attribute, b.getPropertyType().getName(), "COMPATIBLE", "Simple Table Attribute");
|
||||
} catch (Exception | LinkageError e) {
|
||||
return option(attribute, "?", "UNVERIFIED", "Column type unverified: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
List<Problem> validate(Document document) {
|
||||
var result = new ArrayList<Problem>();
|
||||
var all = document.getElementsByTagName("*");
|
||||
for (int i = 0; i < all.getLength(); i++) {
|
||||
var table = (Element) all.item(i);
|
||||
if (!named(table, "Table")) continue;
|
||||
String id = table.getAttributeNS(FX, "id");
|
||||
if (id.isEmpty()) id = "Table #" + i;
|
||||
String path = table.getAttribute("collection"), row = table.getAttribute("row");
|
||||
if (children(table).stream()
|
||||
.anyMatch(e -> Set.of("collection", "row").contains(e.getTagName()))) {
|
||||
result.add(
|
||||
new Problem(id, "Table", "WARNING", "Nested Table properties require Source review"));
|
||||
continue;
|
||||
}
|
||||
var c = collection(path);
|
||||
add(result, id, "collection", c);
|
||||
var r = repeats.component(row, c);
|
||||
add(result, id, "row", r);
|
||||
var wrappers =
|
||||
children(table).stream().filter(e -> e.getTagName().equals("columns")).toList();
|
||||
if (wrappers.size() > 1
|
||||
|| !wrappers.isEmpty() && children(table).stream().anyMatch(e -> named(e, "Column"))) {
|
||||
result.add(new Problem(id, "columns", "ERROR", "Mixed Column containers"));
|
||||
continue;
|
||||
}
|
||||
var columns =
|
||||
children(wrappers.isEmpty() ? table : wrappers.getFirst()).stream()
|
||||
.filter(e -> named(e, "Column"))
|
||||
.toList();
|
||||
for (var col : columns) {
|
||||
if (!children(col).isEmpty())
|
||||
result.add(
|
||||
new Problem(
|
||||
id, "columns", "WARNING", "Nested Column properties require Source review"));
|
||||
else add(result, id, "columns", column(path, col.getAttribute("attribute")));
|
||||
}
|
||||
if (!r.status().equals("INCOMPATIBLE")) result.add(alignment(id, row, columns));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Problem alignment(String id, String row, List<Element> columns) {
|
||||
String prefix = "Declared Column/Row alignment: ";
|
||||
try {
|
||||
var type = load(row);
|
||||
var doc = RepeatDiscovery.resource(type);
|
||||
var root = doc.getDocumentElement();
|
||||
var owner = new ControllerMetadata(type, false, null);
|
||||
if (owner.viewModel != ViewModel.class)
|
||||
throw new IllegalArgumentException("custom Row ViewModel may change binding semantics");
|
||||
String base = root.getAttribute("type");
|
||||
if (!(base.equals("javafx.scene.layout.HBox")
|
||||
|| base.equals("HBox")
|
||||
&& imports(doc).stream()
|
||||
.anyMatch(
|
||||
v ->
|
||||
v.equals("javafx.scene.layout.HBox")
|
||||
|| v.equals("javafx.scene.layout.*"))))
|
||||
throw new IllegalArgumentException("Row is not declared as an HBox");
|
||||
var fields = new ArrayList<Element>();
|
||||
for (var child : children(root)) {
|
||||
if (FX.equals(child.getNamespaceURI()) && "define".equals(child.getLocalName())) continue;
|
||||
if (child.getTagName().equals("children")) fields.addAll(children(child));
|
||||
else if (!child.getTagName().contains(":")
|
||||
&& Character.isUpperCase(
|
||||
child.getTagName().charAt(child.getTagName().lastIndexOf('.') + 1)))
|
||||
fields.add(child);
|
||||
else throw new IllegalArgumentException("Row contains unresolved property/include content");
|
||||
}
|
||||
if (fields.size() < columns.size())
|
||||
return new Problem(
|
||||
id,
|
||||
"alignment",
|
||||
"WARNING",
|
||||
prefix + "mismatch — fewer declared Row fields than Columns. Open Row Component.");
|
||||
var bindings = new ArrayList<Element>();
|
||||
for (var define : children(root))
|
||||
if (FX.equals(define.getNamespaceURI()) && "define".equals(define.getLocalName()))
|
||||
for (var list : children(define))
|
||||
if (named(list, "BindingList")) {
|
||||
var content = children(list);
|
||||
if (content.size() == 1 && content.getFirst().getTagName().equals("elements"))
|
||||
content = children(content.getFirst());
|
||||
bindings.addAll(
|
||||
content.stream()
|
||||
.filter(e -> named(e, "DirectBinding") || named(e, "ComputedBinding"))
|
||||
.toList());
|
||||
}
|
||||
for (int i = 0; i < columns.size(); i++) {
|
||||
var col = columns.get(i);
|
||||
var field = fields.get(i);
|
||||
String fieldId = field.getAttributeNS(FX, "id");
|
||||
int identities = 0;
|
||||
var rowNodes = doc.getElementsByTagName("*");
|
||||
for (int j = 0; j < rowNodes.getLength(); j++)
|
||||
if (!fieldId.isEmpty()
|
||||
&& fieldId.equals(((Element) rowNodes.item(j)).getAttributeNS(FX, "id")))
|
||||
identities++;
|
||||
if (identities != 1) throw new IllegalArgumentException("field has no unique fx:id");
|
||||
var matches =
|
||||
bindings.stream()
|
||||
.filter(b -> !fieldId.isEmpty() && fieldId.equals(b.getAttribute("control")))
|
||||
.toList();
|
||||
if (!children(col).isEmpty()
|
||||
|| matches.size() != 1
|
||||
|| !named(matches.getFirst(), "DirectBinding")
|
||||
|| !children(matches.getFirst()).isEmpty())
|
||||
throw new IllegalArgumentException(
|
||||
"field " + (i + 1) + " has no unique direct Attribute binding");
|
||||
var binding = matches.getFirst();
|
||||
String path = binding.getAttribute("path");
|
||||
if (path.isEmpty()
|
||||
|| binding.getAttribute("property").isEmpty()
|
||||
|| !Set.of("view", "model", "both").contains(binding.getAttribute("direction"))
|
||||
|| !binding.getAttribute("converter").isEmpty())
|
||||
throw new IllegalArgumentException("field binding requires runtime interpretation");
|
||||
if (!path.equals(col.getAttribute("attribute")))
|
||||
return new Problem(
|
||||
id,
|
||||
"alignment",
|
||||
"WARNING",
|
||||
prefix
|
||||
+ "mismatch at Column "
|
||||
+ (i + 1)
|
||||
+ " ("
|
||||
+ col.getAttribute("attribute")
|
||||
+ " versus Row "
|
||||
+ path
|
||||
+ "). Open Row Component.");
|
||||
}
|
||||
return new Problem(
|
||||
id,
|
||||
"alignment",
|
||||
"INFO",
|
||||
prefix
|
||||
+ "matches direct bindings; constructor/runtime layout changes remain unverified.");
|
||||
} catch (Exception | LinkageError e) {
|
||||
return new Problem(
|
||||
id,
|
||||
"alignment",
|
||||
"WARNING",
|
||||
prefix + "unverified — " + e.getMessage() + ". Open Row Component.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void add(List<Problem> out, String id, String property, RepeatOption v) {
|
||||
if (!v.status().equals("COMPATIBLE"))
|
||||
out.add(
|
||||
new Problem(
|
||||
id, property, v.status().equals("INCOMPATIBLE") ? "ERROR" : "WARNING", v.reason()));
|
||||
}
|
||||
|
||||
private static RepeatOption option(String name, String type, String status, String reason) {
|
||||
return new RepeatOption(name, type, status, reason == null ? "Unavailable metadata" : reason);
|
||||
}
|
||||
|
||||
private static List<Element> children(Element e) {
|
||||
var out = new ArrayList<Element>();
|
||||
for (Node n = e.getFirstChild(); n != null; n = n.getNextSibling())
|
||||
if (n instanceof Element c) out.add(c);
|
||||
return out;
|
||||
}
|
||||
|
||||
private static List<String> imports(Document d) {
|
||||
var out = new ArrayList<String>();
|
||||
for (Node n = d.getFirstChild(); n != null; n = n.getNextSibling())
|
||||
if (n instanceof ProcessingInstruction p && p.getTarget().equals("import"))
|
||||
out.add(p.getData().trim());
|
||||
return out;
|
||||
}
|
||||
|
||||
private static boolean named(Element e, String name) {
|
||||
if (e.getTagName().equals(P + name)) return true;
|
||||
var imports = imports(e.getOwnerDocument());
|
||||
return e.getTagName().equals(name)
|
||||
&& (imports.contains(P + name) || imports.contains(P + "*"))
|
||||
&& imports.stream().noneMatch(s -> s.endsWith("." + name) && !s.equals(P + name));
|
||||
}
|
||||
}
|
||||
@@ -92,7 +92,7 @@ public final class WorkerMain {
|
||||
new Version(1, 0),
|
||||
new Version(1, 0),
|
||||
new Version(1, 0),
|
||||
Set.of("discover-v1", "render-rgba-v1", "layout-v1", "interactive-v1", "bindings-v4"),
|
||||
Set.of("discover-v1", "render-rgba-v1", "layout-v1", "interactive-v1", "bindings-v5"),
|
||||
Set.of()));
|
||||
var welcome = FrameCodec.read(socket.getInputStream());
|
||||
if (!(welcome instanceof Welcome w) || !session.equals(w.session()) || w.wire().major() != 1)
|
||||
|
||||
+222
@@ -0,0 +1,222 @@
|
||||
package de.neitzel.fxdesigner.target;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import de.neitzel.fx.view.ViewModelClass;
|
||||
import java.util.*;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.scene.layout.HBox;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class TableDiscoveryTest {
|
||||
public static class Item {
|
||||
static {
|
||||
System.setProperty("table.item.initialized", "yes");
|
||||
}
|
||||
|
||||
public String getStreet() {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
public void setStreet(String v) {}
|
||||
|
||||
public int getNumber() {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
public void setNumber(int v) {}
|
||||
|
||||
public Item getNested() {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
public void setNested(Item v) {}
|
||||
|
||||
public char getLetter() {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
public void setLetter(char v) {}
|
||||
|
||||
public String getReadOnly() {
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
|
||||
public static class Model {
|
||||
static {
|
||||
System.setProperty("table.model.initialized", "yes");
|
||||
}
|
||||
|
||||
public List<Item> getItems() {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
public void setItems(List<Item> v) {}
|
||||
|
||||
public List getRaw() {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
public void setRaw(List v) {}
|
||||
|
||||
public String getName() {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
public void setName(String v) {}
|
||||
}
|
||||
|
||||
public static class TableRow extends HBox {
|
||||
static {
|
||||
System.setProperty("table.row.initialized", "yes");
|
||||
}
|
||||
|
||||
public ObjectProperty<Item> modelProperty() {
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
|
||||
public static class UnknownRow extends HBox {
|
||||
public ObjectProperty<Item> modelProperty() {
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
|
||||
public static class DuplicateRow extends TableRow {}
|
||||
|
||||
public static class BeanModel extends Model {}
|
||||
|
||||
public static class BeanModelBeanInfo extends java.beans.SimpleBeanInfo {
|
||||
static {
|
||||
System.setProperty("table.beaninfo.initialized", "yes");
|
||||
}
|
||||
|
||||
public BeanModelBeanInfo() {
|
||||
throw new AssertionError("BeanInfo executed");
|
||||
}
|
||||
}
|
||||
|
||||
public static class Replacement {
|
||||
public Replacement(Model m) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
public ListProperty<Item> itemsProperty() {
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
|
||||
@ViewModelClass(Replacement.class)
|
||||
public static class Controller {}
|
||||
|
||||
private String xml(String path, String row, String cols, String controller) {
|
||||
return "<?neitzel-model "
|
||||
+ Model.class.getName()
|
||||
+ "?><?import de.neitzel.fx.view.*?><?import javafx.scene.layout.VBox?>"
|
||||
+ "<VBox xmlns:fx='http://javafx.com/fxml/1'"
|
||||
+ (controller.isEmpty() ? "" : " fx:controller='" + controller + "'")
|
||||
+ "><Table fx:id='t' collection='"
|
||||
+ path
|
||||
+ "' row='"
|
||||
+ row
|
||||
+ "'><columns>"
|
||||
+ cols
|
||||
+ "</columns></Table></VBox>";
|
||||
}
|
||||
|
||||
private String col(String attr) {
|
||||
return "<Column header='" + attr + "' attribute='" + attr + "'/>";
|
||||
}
|
||||
|
||||
private de.neitzel.fxdesigner.protocol.BindingMetadata inspect(
|
||||
String path, Class<?> row, String cols, String controller) throws Exception {
|
||||
return BindingDiscovery.inspect(
|
||||
xml(path, row.getName(), cols, controller), path, "view", getClass().getClassLoader());
|
||||
}
|
||||
|
||||
@Test
|
||||
void discoversTableTypesAndDeclaredAlignmentWithoutInitialization() throws Exception {
|
||||
var m = inspect("items", TableRow.class, col("street") + col("number"), "");
|
||||
assertTrue(
|
||||
m.problems().stream().noneMatch(p -> p.severity().equals("ERROR")),
|
||||
m.problems().toString());
|
||||
assertTrue(
|
||||
m.problems().stream()
|
||||
.anyMatch(p -> p.property().equals("alignment") && p.severity().equals("INFO")));
|
||||
assertTrue(
|
||||
m.tableColumns().stream()
|
||||
.anyMatch(c -> c.name().equals("street") && c.status().equals("COMPATIBLE")));
|
||||
for (String name : List.of("nested", "letter", "readOnly"))
|
||||
assertTrue(
|
||||
m.tableColumns().stream()
|
||||
.anyMatch(c -> c.name().equals(name) && c.status().equals("INCOMPATIBLE")));
|
||||
for (String key :
|
||||
List.of("table.model.initialized", "table.item.initialized", "table.row.initialized"))
|
||||
assertNull(System.getProperty(key));
|
||||
}
|
||||
|
||||
@Test
|
||||
void warnsOnReorderedAndUnknownFieldsWithoutChangingXml() throws Exception {
|
||||
var mismatch = inspect("items", TableRow.class, col("number") + col("street"), "");
|
||||
assertTrue(
|
||||
mismatch.problems().stream()
|
||||
.anyMatch(p -> p.severity().equals("WARNING") && p.message().contains("mismatch")));
|
||||
var unknown = inspect("items", UnknownRow.class, col("street"), "");
|
||||
assertTrue(
|
||||
unknown.problems().stream()
|
||||
.anyMatch(p -> p.property().equals("alignment") && p.message().contains("unverified")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void keepsCustomBeanInfoAndDuplicateFieldIdsUnverified() throws Exception {
|
||||
String text =
|
||||
xml("items", TableRow.class.getName(), col("street"), "")
|
||||
.replace(Model.class.getName(), BeanModel.class.getName());
|
||||
var bean = BindingDiscovery.inspect(text, "items", "view", getClass().getClassLoader());
|
||||
assertTrue(
|
||||
bean.problems().stream()
|
||||
.anyMatch(p -> p.property().equals("collection") && p.severity().equals("WARNING")));
|
||||
assertNull(System.getProperty("table.beaninfo.initialized"));
|
||||
var duplicate = inspect("items", DuplicateRow.class, col("street"), "");
|
||||
assertTrue(
|
||||
duplicate.problems().stream()
|
||||
.anyMatch(
|
||||
p -> p.property().equals("alignment") && p.message().contains("unique fx:id")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsMissingComplexReadOnlyAndUnsupportedScalarAttributes() throws Exception {
|
||||
for (String name : List.of("absent", "nested", "letter", "readOnly", "nested.street")) {
|
||||
var m = inspect("items", TableRow.class, col(name), "");
|
||||
assertTrue(
|
||||
m.problems().stream()
|
||||
.anyMatch(p -> p.property().equals("columns") && p.severity().equals("ERROR")),
|
||||
name);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void tableNeedsBeanMetadataEvenWhenReplacementHasListProperty() throws Exception {
|
||||
var m = inspect("items", TableRow.class, col("street"), Controller.class.getName());
|
||||
assertTrue(
|
||||
m.problems().stream()
|
||||
.anyMatch(p -> p.property().equals("collection") && p.severity().equals("ERROR")));
|
||||
for (String path : List.of("raw", "name", "missing", "items.street"))
|
||||
assertTrue(
|
||||
inspect(path, TableRow.class, col("street"), "").problems().stream()
|
||||
.anyMatch(p -> p.property().equals("collection") && p.severity().equals("ERROR")),
|
||||
path);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsWrongRowModelAndKeepsUnknownSourceSyntaxUnverified() throws Exception {
|
||||
var wrong = inspect("items", RepeatDiscoveryTest.Wrong.class, col("street"), "");
|
||||
assertTrue(
|
||||
wrong.problems().stream()
|
||||
.anyMatch(p -> p.property().equals("row") && p.severity().equals("ERROR")));
|
||||
var nested =
|
||||
inspect("items", TableRow.class, "<Column><attribute>street</attribute></Column>", "");
|
||||
assertTrue(nested.problems().stream().anyMatch(p -> p.message().contains("Nested Column")));
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
<?neitzel-model de.neitzel.fxdesigner.target.TableDiscoveryTest$Item?>
|
||||
<?import javafx.scene.control.TextField?><?import de.neitzel.fx.view.*?>
|
||||
<fx:root xmlns:fx="http://javafx.com/fxml/1" type="javafx.scene.layout.HBox"><fx:define><BindingList><DirectBinding control="street" property="text" path="street" direction="both"/></BindingList></fx:define><TextField fx:id="street"/><TextField fx:id="street"/></fx:root>
|
||||
@@ -0,0 +1,3 @@
|
||||
<?neitzel-model de.neitzel.fxdesigner.target.TableDiscoveryTest$Item?>
|
||||
<?import javafx.scene.control.TextField?><?import de.neitzel.fx.view.*?>
|
||||
<fx:root xmlns:fx="http://javafx.com/fxml/1" type="javafx.scene.layout.HBox"><fx:define><BindingList><DirectBinding control="street" property="text" path="street" direction="both"/><DirectBinding control="number" property="text" path="number" direction="both"/></BindingList></fx:define><children><TextField fx:id="street"/><TextField fx:id="number"/></children></fx:root>
|
||||
@@ -0,0 +1,3 @@
|
||||
<?neitzel-model de.neitzel.fxdesigner.target.TableDiscoveryTest$Item?>
|
||||
<?import javafx.scene.control.TextField?>
|
||||
<fx:root xmlns:fx="http://javafx.com/fxml/1" type="javafx.scene.layout.HBox"><TextField fx:id="street"/></fx:root>
|
||||
Reference in New Issue
Block a user