Discover Repeat collections and compatible Components in both hosts
This commit is contained in:
+30
-6
@@ -23,6 +23,8 @@ public final class BindingDiscovery {
|
||||
|
||||
private record TargetType(Class<?> type, boolean writable) {}
|
||||
|
||||
private final Map<String, Class<?>> controlTypes = new HashMap<>();
|
||||
private RepeatDiscovery repeats;
|
||||
private final Map<String, TargetType> targetTypes = new HashMap<>();
|
||||
|
||||
private BindingDiscovery(ClassLoader loader) {
|
||||
@@ -63,7 +65,7 @@ public final class BindingDiscovery {
|
||||
return new BindingDiscovery(loader).inspect(fxml, path, direction, control, property, resource);
|
||||
}
|
||||
|
||||
private static Document parse(String xml) throws Exception {
|
||||
static Document parse(String xml) throws Exception {
|
||||
if (xml.length() > 512 * 1024) throw new IOException("FXML metadata input exceeds limit");
|
||||
var f = javax.xml.parsers.DocumentBuilderFactory.newInstance();
|
||||
f.setNamespaceAware(true);
|
||||
@@ -75,7 +77,7 @@ public final class BindingDiscovery {
|
||||
return f.newDocumentBuilder().parse(new org.xml.sax.InputSource(new StringReader(xml)));
|
||||
}
|
||||
|
||||
private static String model(Document doc) {
|
||||
static String model(Document doc) {
|
||||
String model = "";
|
||||
for (Node n = doc.getFirstChild(); n != null; n = n.getNextSibling())
|
||||
if (n instanceof ProcessingInstruction pi && pi.getTarget().equals("neitzel-model")) {
|
||||
@@ -238,6 +240,7 @@ public final class BindingDiscovery {
|
||||
&& owner.controller != null
|
||||
? owner.controller
|
||||
: resolve(e, imports);
|
||||
controlTypes.put(id, control);
|
||||
String componentModel = "";
|
||||
var componentResource = control.getResource(control.getSimpleName() + ".fxml");
|
||||
if (componentResource != null)
|
||||
@@ -321,6 +324,7 @@ public final class BindingDiscovery {
|
||||
bypassesConverter(selected)
|
||||
? List.<BindingMetadata.Converter>of()
|
||||
: converters.candidates(source, chosen == null ? null : chosen.type(), direction);
|
||||
repeats = new RepeatDiscovery(loader, attributes, owner, incompleteModel, controlTypes, ids);
|
||||
return new BindingMetadata(
|
||||
model,
|
||||
selected,
|
||||
@@ -332,7 +336,10 @@ public final class BindingDiscovery {
|
||||
validate(doc, imports, ids, converters),
|
||||
owner.controller == null ? "" : owner.controller.getName(),
|
||||
owner.viewModel == null ? "" : owner.viewModel.getName(),
|
||||
owner.methods(chosen == null ? null : chosen.type()));
|
||||
owner.methods(chosen == null ? null : chosen.type()),
|
||||
repeats.collections(),
|
||||
repeats.candidates(selected),
|
||||
repeats.panes());
|
||||
}
|
||||
|
||||
private void selectOwner(Document doc, List<String> imports, String resource, Class<?> model)
|
||||
@@ -412,8 +419,9 @@ public final class BindingDiscovery {
|
||||
var nodes = doc.getElementsByTagName("*");
|
||||
for (int i = 0; i < nodes.getLength(); i++) {
|
||||
var e = (Element) nodes.item(i);
|
||||
if (!library(e, "DirectBinding", imports) && !library(e, "ComputedBinding", imports))
|
||||
continue;
|
||||
if (!library(e, "DirectBinding", imports)
|
||||
&& !library(e, "ComputedBinding", imports)
|
||||
&& !library(e, "Repeat", imports)) continue;
|
||||
Node parent = e.getParentNode();
|
||||
if (parent instanceof Element p && p.getTagName().equals("elements"))
|
||||
parent = p.getParentNode();
|
||||
@@ -423,6 +431,22 @@ public final class BindingDiscovery {
|
||||
|| !FX.equals(define.getNamespaceURI())
|
||||
|| !"define".equals(define.getLocalName())
|
||||
|| define.getParentNode() != doc.getDocumentElement()) continue;
|
||||
if (library(e, "Repeat", imports)) {
|
||||
String pane = e.getAttribute("pane");
|
||||
boolean nested = false;
|
||||
for (Node n = e.getFirstChild(); n != null; n = n.getNextSibling())
|
||||
if (n instanceof Element) nested = true;
|
||||
if (nested)
|
||||
problems.add(
|
||||
new Problem(pane, "children", "WARNING", "Nested Repeat requires Source review"));
|
||||
else {
|
||||
if (!occupied.add("Repeat:" + pane))
|
||||
problems.add(new Problem(pane, "children", "ERROR", "Duplicate Repeat pane"));
|
||||
problems.addAll(
|
||||
repeats.validate(pane, e.getAttribute("collection"), e.getAttribute("component")));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
String control = e.getAttribute("control"),
|
||||
property = e.getAttribute("property"),
|
||||
key = control + "." + property;
|
||||
@@ -555,7 +579,7 @@ public final class BindingDiscovery {
|
||||
return candidates.iterator().next();
|
||||
}
|
||||
|
||||
private static Class<?> propertyType(Type type, Map<TypeVariable<?>, Type> inherited) {
|
||||
static Class<?> propertyType(Type type, Map<TypeVariable<?>, Type> inherited) {
|
||||
Class<?> raw;
|
||||
var bindings = new HashMap<>(inherited);
|
||||
if (type instanceof ParameterizedType p && p.getRawType() instanceof Class<?> c) {
|
||||
|
||||
+350
@@ -0,0 +1,350 @@
|
||||
package de.neitzel.fxdesigner.target;
|
||||
|
||||
import de.neitzel.fxdesigner.protocol.BindingMetadata.*;
|
||||
import java.io.*;
|
||||
import java.lang.reflect.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
import java.util.jar.JarFile;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.layout.Pane;
|
||||
import org.w3c.dom.*;
|
||||
|
||||
/** Signature/resource-only Repeat checks. Never invokes an application accessor or constructor. */
|
||||
final class RepeatDiscovery {
|
||||
private final ClassLoader loader;
|
||||
private final Map<String, RepeatOption> collections = new TreeMap<>();
|
||||
private final Map<String, Class<?>> controls;
|
||||
private final Set<String> ids;
|
||||
private final boolean incomplete;
|
||||
private final Set<String> nonCollections = new HashSet<>();
|
||||
private final Map<String, ComponentInfo> components = new TreeMap<>();
|
||||
private final SortedSet<String> names = new TreeSet<>();
|
||||
|
||||
private record ComponentInfo(Class<?> model, Class<?> property, String declared, String error) {}
|
||||
|
||||
RepeatDiscovery(
|
||||
ClassLoader loader,
|
||||
List<Attribute> attributes,
|
||||
ControllerMetadata owner,
|
||||
boolean incomplete,
|
||||
Map<String, Class<?>> controls,
|
||||
Set<String> ids)
|
||||
throws Exception {
|
||||
this.loader = loader;
|
||||
this.controls = controls;
|
||||
this.ids = ids;
|
||||
this.incomplete = incomplete;
|
||||
if (!owner.dynamicProperties && !owner.unknownController)
|
||||
for (var a : attributes)
|
||||
if (Set.of("VALUE", "BUILTIN", "COMPLEX").contains(a.kind())) nonCollections.add(a.path());
|
||||
for (var a : attributes) {
|
||||
if (!a.kind().equals("COLLECTION")
|
||||
&& !a.kind().equals("VIEWMODEL")
|
||||
&& !a.kind().equals("UNRESOLVED")) continue;
|
||||
Class<?> raw;
|
||||
try {
|
||||
raw = a.type().equals("?") ? Object.class : load(a.type());
|
||||
} catch (ClassNotFoundException primitive) {
|
||||
continue;
|
||||
}
|
||||
String element = a.elementType();
|
||||
boolean wrapped = a.kind().equals("COLLECTION");
|
||||
if (!wrapped && owner.viewModel != null) {
|
||||
try {
|
||||
Method m = owner.viewModel.getMethod(a.path() + "Property");
|
||||
Type value = argument(m.getGenericReturnType(), ObservableValue.class);
|
||||
element = element(argument(value, Collection.class));
|
||||
} catch (NoSuchMethodException ignored) {
|
||||
}
|
||||
}
|
||||
if (raw != Object.class && !Collection.class.isAssignableFrom(raw)) {
|
||||
if (!owner.dynamicProperties && !owner.unknownController) nonCollections.add(a.path());
|
||||
continue;
|
||||
}
|
||||
if (owner.dynamicProperties || owner.unknownController) element = "";
|
||||
boolean unknown =
|
||||
owner.dynamicProperties
|
||||
|| owner.unknownController
|
||||
|| element.isEmpty()
|
||||
|| element.equals(Object.class.getName())
|
||||
|| (!wrapped && !ObservableList.class.isAssignableFrom(raw));
|
||||
collections.put(
|
||||
a.path(),
|
||||
new RepeatOption(
|
||||
a.path(),
|
||||
element.isEmpty() ? "?" : element,
|
||||
unknown ? "UNVERIFIED" : "COMPATIBLE",
|
||||
unknown
|
||||
? "Element type or ObservableList backing is unresolved"
|
||||
: "Declared collection element type; runtime values may be null"));
|
||||
}
|
||||
indexResources();
|
||||
}
|
||||
|
||||
private Class<?> load(String name) throws ClassNotFoundException {
|
||||
return Class.forName(name, false, loader);
|
||||
}
|
||||
|
||||
List<RepeatOption> collections() {
|
||||
return List.copyOf(collections.values());
|
||||
}
|
||||
|
||||
List<RepeatOption> panes() {
|
||||
return ids.stream()
|
||||
.sorted()
|
||||
.map(
|
||||
id -> {
|
||||
Class<?> type = controls.get(id);
|
||||
return new RepeatOption(
|
||||
id,
|
||||
type == null ? "?" : type.getName(),
|
||||
type == null
|
||||
? "UNVERIFIED"
|
||||
: Pane.class.isAssignableFrom(type) ? "COMPATIBLE" : "INCOMPATIBLE",
|
||||
type == null
|
||||
? "Pane type unavailable"
|
||||
: Pane.class.isAssignableFrom(type)
|
||||
? "JavaFX Pane"
|
||||
: "Repeat requires a JavaFX Pane");
|
||||
})
|
||||
.toList();
|
||||
}
|
||||
|
||||
List<RepeatOption> candidates(String path) {
|
||||
return names.stream().map(n -> component(n, path)).toList();
|
||||
}
|
||||
|
||||
RepeatOption collection(String path) {
|
||||
if (nonCollections.contains(path))
|
||||
return new RepeatOption(path, "?", "INCOMPATIBLE", "Repeat requires a collection: " + path);
|
||||
var c = collections.get(path);
|
||||
return c != null
|
||||
? c
|
||||
: new RepeatOption(
|
||||
path,
|
||||
"?",
|
||||
incomplete ? "UNVERIFIED" : "INCOMPATIBLE",
|
||||
incomplete ? "Collection path unresolved" : "No discoverable collection: " + path);
|
||||
}
|
||||
|
||||
RepeatOption component(String name, String path) {
|
||||
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");
|
||||
Class<?> element;
|
||||
try {
|
||||
element = source.type().equals("?") ? null : load(source.type());
|
||||
} catch (ClassNotFoundException e) {
|
||||
element = null;
|
||||
}
|
||||
if (element != null && element != Object.class) {
|
||||
for (var constraint : Arrays.asList(info.model(), info.property()))
|
||||
if (constraint != null
|
||||
&& constraint != Object.class
|
||||
&& !constraint.isAssignableFrom(element))
|
||||
return new RepeatOption(
|
||||
name,
|
||||
info.declared(),
|
||||
"INCOMPATIBLE",
|
||||
"Collection element "
|
||||
+ element.getName()
|
||||
+ " is incompatible with Component Model "
|
||||
+ constraint.getName());
|
||||
}
|
||||
boolean verified =
|
||||
source.status().equals("COMPATIBLE")
|
||||
&& element != null
|
||||
&& element != Object.class
|
||||
&& (info.model() != null || info.property() != null && info.property() != Object.class);
|
||||
return new RepeatOption(
|
||||
name,
|
||||
info.declared(),
|
||||
verified ? "COMPATIBLE" : "UNVERIFIED",
|
||||
verified
|
||||
? "Declared element fits Component Model; construction remains a runtime check"
|
||||
: "Collection or Component Model type is unresolved");
|
||||
}
|
||||
|
||||
List<Problem> validate(String pane, String path, String component) {
|
||||
var problems = new ArrayList<Problem>();
|
||||
if (!ids.contains(pane))
|
||||
problems.add(new Problem(pane, "children", "ERROR", "Unknown Repeat pane: " + pane));
|
||||
else if (!controls.containsKey(pane))
|
||||
problems.add(new Problem(pane, "children", "WARNING", "Repeat pane type unavailable"));
|
||||
else if (!Pane.class.isAssignableFrom(controls.get(pane)))
|
||||
problems.add(new Problem(pane, "children", "ERROR", "Repeat requires a JavaFX Pane"));
|
||||
var c = collection(path);
|
||||
if (!c.status().equals("COMPATIBLE"))
|
||||
problems.add(
|
||||
new Problem(
|
||||
pane,
|
||||
"children",
|
||||
c.status().equals("INCOMPATIBLE") ? "ERROR" : "WARNING",
|
||||
c.reason()));
|
||||
var target = component(component, path);
|
||||
if (!target.status().equals("COMPATIBLE"))
|
||||
problems.add(
|
||||
new Problem(
|
||||
pane,
|
||||
"children",
|
||||
target.status().equals("INCOMPATIBLE") ? "ERROR" : "WARNING",
|
||||
target.reason()));
|
||||
return problems;
|
||||
}
|
||||
|
||||
private ComponentInfo readComponent(String name) {
|
||||
String declared = "?";
|
||||
try {
|
||||
Class<?> type = load(name);
|
||||
if (!Parent.class.isAssignableFrom(type)
|
||||
|| !Modifier.isPublic(type.getModifiers())
|
||||
|| Modifier.isAbstract(type.getModifiers()))
|
||||
throw new IllegalArgumentException(
|
||||
"Repeat Component must be a public concrete JavaFX Parent");
|
||||
Method accessor = type.getMethod("modelProperty");
|
||||
if (!Property.class.isAssignableFrom(accessor.getReturnType())
|
||||
|| !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));
|
||||
}
|
||||
Element root = doc.getDocumentElement();
|
||||
if (!"http://javafx.com/fxml/1".equals(root.getNamespaceURI())
|
||||
|| !"root".equals(root.getLocalName()))
|
||||
throw new IllegalArgumentException("Component FXML requires fx:root");
|
||||
if (root.hasAttributeNS("http://javafx.com/fxml/1", "controller"))
|
||||
throw new IllegalArgumentException("Component FXML must not override its Controller");
|
||||
String base = root.getAttribute("type");
|
||||
var roots = new HashSet<Class<?>>();
|
||||
if (base.contains(".")) roots.add(load(base));
|
||||
else
|
||||
for (Node n = doc.getFirstChild(); n != null; n = n.getNextSibling())
|
||||
if (n instanceof ProcessingInstruction pi && pi.getTarget().equals("import")) {
|
||||
String imp = pi.getData().trim();
|
||||
try {
|
||||
if (imp.endsWith(".*")) roots.add(load(imp.substring(0, imp.length() - 1) + base));
|
||||
else if (imp.endsWith("." + base)) roots.add(load(imp));
|
||||
} catch (ClassNotFoundException ignored) {
|
||||
}
|
||||
}
|
||||
if (roots.size() != 1 || !roots.iterator().next().isAssignableFrom(type))
|
||||
throw new IllegalArgumentException("Component fx:root type is incompatible or unresolved");
|
||||
declared = BindingDiscovery.model(doc);
|
||||
Class<?> model = declared.isEmpty() ? null : load(declared);
|
||||
if (declared.isEmpty()) declared = property == null ? "?" : property.getName();
|
||||
return new ComponentInfo(model, property, declared, null);
|
||||
} catch (Exception | LinkageError e) {
|
||||
return new ComponentInfo(
|
||||
null, null, declared, "Component unavailable: " + name + ": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void indexResources() throws IOException {
|
||||
var entries = new LinkedHashSet<String>();
|
||||
for (String key : List.of("java.class.path", "jdk.module.path"))
|
||||
entries.addAll(Arrays.asList(System.getProperty(key, "").split(File.pathSeparator)));
|
||||
int count = 0;
|
||||
for (String entry : entries) {
|
||||
if (entry.isEmpty()) continue;
|
||||
Path path = Path.of(entry);
|
||||
if (Files.isDirectory(path)) {
|
||||
try (var files = Files.walk(path)) {
|
||||
var it = files.filter(p -> p.toString().endsWith(".fxml")).iterator();
|
||||
while (it.hasNext()) {
|
||||
if (++count > 4096) throw new IOException("Too many Component resources");
|
||||
index(path.relativize(it.next()).toString().replace(File.separatorChar, '/'));
|
||||
}
|
||||
}
|
||||
} else if (Files.isRegularFile(path) && entry.endsWith(".jar")) {
|
||||
try (var jar = new JarFile(path.toFile(), false, JarFile.OPEN_READ, Runtime.version())) {
|
||||
var it = jar.versionedStream().filter(e -> e.getName().endsWith(".fxml")).iterator();
|
||||
while (it.hasNext()) {
|
||||
if (++count > 4096) throw new IOException("Too many Component resources");
|
||||
index(it.next().getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void index(String resource) throws IOException {
|
||||
if (resource.startsWith("META-INF/")) return;
|
||||
String name = resource.substring(0, resource.length() - 5).replace('/', '.');
|
||||
try {
|
||||
Class<?> type = load(name);
|
||||
if (Parent.class.isAssignableFrom(type)) names.add(name);
|
||||
if (names.size() > 256) throw new IOException("Too many Repeat Components");
|
||||
} catch (ClassNotFoundException | LinkageError ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
private record Parameterized(Type raw, Type[] arguments) implements ParameterizedType {
|
||||
public Type getRawType() {
|
||||
return raw;
|
||||
}
|
||||
|
||||
public Type[] getActualTypeArguments() {
|
||||
return arguments.clone();
|
||||
}
|
||||
|
||||
public Type getOwnerType() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static Type substitute(Type t, Map<TypeVariable<?>, Type> vars) {
|
||||
if (t instanceof TypeVariable<?> v) return vars.getOrDefault(v, v);
|
||||
if (t instanceof ParameterizedType p)
|
||||
return new Parameterized(
|
||||
p.getRawType(),
|
||||
Arrays.stream(p.getActualTypeArguments())
|
||||
.map(a -> substitute(a, vars))
|
||||
.toArray(Type[]::new));
|
||||
return t;
|
||||
}
|
||||
|
||||
private static Type argument(Type type, Class<?> target) {
|
||||
if (type == null) return null;
|
||||
Class<?> raw;
|
||||
var vars = new HashMap<TypeVariable<?>, Type>();
|
||||
if (type instanceof ParameterizedType p && p.getRawType() instanceof Class<?> c) {
|
||||
raw = c;
|
||||
for (int i = 0; i < c.getTypeParameters().length; i++)
|
||||
vars.put(c.getTypeParameters()[i], p.getActualTypeArguments()[i]);
|
||||
} else if (type instanceof Class<?> c) raw = c;
|
||||
else return null;
|
||||
if (raw == target) return vars.get(raw.getTypeParameters()[0]);
|
||||
for (Type parent : raw.getGenericInterfaces()) {
|
||||
Type value = argument(substitute(parent, vars), target);
|
||||
if (value != null) return value;
|
||||
}
|
||||
return raw.getGenericSuperclass() == null
|
||||
? null
|
||||
: argument(substitute(raw.getGenericSuperclass(), vars), target);
|
||||
}
|
||||
|
||||
private static String element(Type t) {
|
||||
if (t instanceof Class<?> c) return c.getName();
|
||||
if (t instanceof WildcardType w
|
||||
&& w.getLowerBounds().length == 0
|
||||
&& w.getUpperBounds().length == 1
|
||||
&& w.getUpperBounds()[0] instanceof Class<?> c) return c.getName();
|
||||
return "";
|
||||
}
|
||||
}
|
||||
@@ -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-v3"),
|
||||
Set.of("discover-v1", "render-rgba-v1", "layout-v1", "interactive-v1", "bindings-v4"),
|
||||
Set.of()));
|
||||
var welcome = FrameCodec.read(socket.getInputStream());
|
||||
if (!(welcome instanceof Welcome w) || !session.equals(w.session()) || w.wire().major() != 1)
|
||||
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
package de.neitzel.fxdesigner.target;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import de.neitzel.fx.view.*;
|
||||
import java.util.*;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class RepeatDiscoveryTest {
|
||||
public static class Item {}
|
||||
|
||||
public static class Model {
|
||||
static {
|
||||
System.setProperty("repeat.model.initialized", "yes");
|
||||
}
|
||||
|
||||
public List<Item> getItems() {
|
||||
throw new AssertionError("getter");
|
||||
}
|
||||
|
||||
public void setItems(List<Item> v) {}
|
||||
|
||||
public List getRaw() {
|
||||
throw new AssertionError("getter");
|
||||
}
|
||||
|
||||
public void setRaw(List v) {}
|
||||
|
||||
public String getName() {
|
||||
throw new AssertionError("getter");
|
||||
}
|
||||
|
||||
public void setName(String v) {}
|
||||
}
|
||||
|
||||
public static class Replacement {
|
||||
public Replacement(Model m) {
|
||||
throw new AssertionError("constructor");
|
||||
}
|
||||
|
||||
public ListProperty<Item> itemsProperty() {
|
||||
throw new AssertionError("accessor");
|
||||
}
|
||||
|
||||
public ObjectProperty<List<Item>> uncertainProperty() {
|
||||
throw new AssertionError("accessor");
|
||||
}
|
||||
|
||||
public ObjectProperty<String> nameProperty() {
|
||||
throw new AssertionError("accessor");
|
||||
}
|
||||
}
|
||||
|
||||
@ViewModelClass(Replacement.class)
|
||||
public static class Controller {}
|
||||
|
||||
public static class Good extends VBox {
|
||||
static {
|
||||
System.setProperty("repeat.component.initialized", "yes");
|
||||
}
|
||||
|
||||
public ObjectProperty<Item> modelProperty() {
|
||||
throw new AssertionError("accessor");
|
||||
}
|
||||
}
|
||||
|
||||
public static class Wrong extends VBox {
|
||||
public ObjectProperty<Model> modelProperty() {
|
||||
throw new AssertionError("accessor");
|
||||
}
|
||||
}
|
||||
|
||||
public static class Unknown extends VBox {
|
||||
public ObjectProperty<Object> modelProperty() {
|
||||
throw new AssertionError("accessor");
|
||||
}
|
||||
}
|
||||
|
||||
public static class ReadOnly extends VBox {
|
||||
public ReadOnlyObjectProperty<Item> modelProperty() {
|
||||
throw new AssertionError("accessor");
|
||||
}
|
||||
}
|
||||
|
||||
private String xml(String collection, String component, String pane, String controller) {
|
||||
return "<?neitzel-model "
|
||||
+ Model.class.getName()
|
||||
+ "?><?import javafx.scene.layout.VBox?><?import javafx.scene.control.Label?>"
|
||||
+ "<VBox xmlns:fx='http://javafx.com/fxml/1'"
|
||||
+ (controller.isEmpty() ? "" : " fx:controller='" + controller + "'")
|
||||
+ "><fx:define><de.neitzel.fx.view.BindingList>"
|
||||
+ "<de.neitzel.fx.view.Repeat pane='"
|
||||
+ pane
|
||||
+ "' collection='"
|
||||
+ collection
|
||||
+ "' component='"
|
||||
+ component
|
||||
+ "'/></de.neitzel.fx.view.BindingList></fx:define><VBox fx:id='rows'/><Label"
|
||||
+ " fx:id='label'/></VBox>";
|
||||
}
|
||||
|
||||
private de.neitzel.fxdesigner.protocol.BindingMetadata inspect(
|
||||
String path, Class<?> type, String pane, String controller) throws Exception {
|
||||
return BindingDiscovery.inspect(
|
||||
xml(path, type.getName(), pane, controller), path, "view", getClass().getClassLoader());
|
||||
}
|
||||
|
||||
@Test
|
||||
void acceptsDeclaredTypesWithoutInitializingApplicationCode() throws Exception {
|
||||
var m = inspect("items", Good.class, "rows", "");
|
||||
assertTrue(m.problems().isEmpty(), m.problems().toString());
|
||||
assertTrue(
|
||||
m.collections().stream()
|
||||
.anyMatch(
|
||||
c ->
|
||||
c.name().equals("items")
|
||||
&& c.type().equals(Item.class.getName())
|
||||
&& c.status().equals("COMPATIBLE")));
|
||||
assertTrue(
|
||||
m.panes().stream()
|
||||
.anyMatch(p -> p.name().equals("label") && p.status().equals("INCOMPATIBLE")));
|
||||
assertNull(System.getProperty("repeat.model.initialized"));
|
||||
assertNull(System.getProperty("repeat.component.initialized"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsWrongModelPaneAndMissingCollection() throws Exception {
|
||||
assertTrue(
|
||||
inspect("items", Wrong.class, "rows", "").problems().stream()
|
||||
.anyMatch(p -> p.severity().equals("ERROR") && p.message().contains("incompatible")));
|
||||
assertTrue(
|
||||
inspect("items", Good.class, "label", "").problems().stream()
|
||||
.anyMatch(p -> p.message().contains("JavaFX Pane")));
|
||||
assertTrue(
|
||||
inspect("name", Good.class, "rows", "").problems().stream()
|
||||
.anyMatch(p -> p.severity().equals("ERROR") && p.message().contains("collection")));
|
||||
assertTrue(
|
||||
inspect("absent", Good.class, "rows", "").problems().stream()
|
||||
.anyMatch(p -> p.severity().equals("ERROR")));
|
||||
assertTrue(
|
||||
inspect("items", ReadOnly.class, "rows", "").problems().stream()
|
||||
.anyMatch(p -> p.message().contains("modelProperty")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void keepsUnknownElementAndComponentModelsUnverified() throws Exception {
|
||||
for (var m :
|
||||
List.of(
|
||||
inspect("raw", Good.class, "rows", ""), inspect("items", Unknown.class, "rows", ""))) {
|
||||
assertTrue(m.problems().stream().anyMatch(p -> p.severity().equals("WARNING")));
|
||||
assertTrue(
|
||||
m.problems().stream().noneMatch(p -> p.severity().equals("ERROR")),
|
||||
m.problems().toString());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void discoversReplacementObservableCollectionsAndWarnsForPlainListBacking() throws Exception {
|
||||
var m = inspect("items", Good.class, "rows", Controller.class.getName());
|
||||
assertTrue(m.problems().isEmpty(), m.problems().toString());
|
||||
assertTrue(
|
||||
m.collections().stream()
|
||||
.anyMatch(c -> c.name().equals("items") && c.status().equals("COMPATIBLE")));
|
||||
assertTrue(m.collections().stream().noneMatch(c -> c.name().equals("raw")));
|
||||
var uncertain = inspect("uncertain", Good.class, "rows", Controller.class.getName());
|
||||
assertTrue(uncertain.problems().stream().anyMatch(p -> p.severity().equals("WARNING")));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<fx:root xmlns:fx="http://javafx.com/fxml/1" type="javafx.scene.layout.VBox"/>
|
||||
@@ -0,0 +1 @@
|
||||
<fx:root xmlns:fx="http://javafx.com/fxml/1" type="javafx.scene.layout.VBox"/>
|
||||
@@ -0,0 +1 @@
|
||||
<fx:root xmlns:fx="http://javafx.com/fxml/1" type="javafx.scene.layout.VBox"/>
|
||||
@@ -0,0 +1 @@
|
||||
<fx:root xmlns:fx="http://javafx.com/fxml/1" type="javafx.scene.layout.VBox"/>
|
||||
Reference in New Issue
Block a user