Connect minimal hosts to snapshots, target workers and shared Kit editing
This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
package de.neitzel.fxdesigner.kit;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
import javax.xml.parsers.*;
|
||||
import javax.xml.transform.*;
|
||||
import javax.xml.transform.dom.DOMSource;
|
||||
import javax.xml.transform.stream.StreamResult;
|
||||
import org.w3c.dom.*;
|
||||
|
||||
/**
|
||||
* Bounded scalar/layout projection. Original XML owns bindings, controllers and unknown metadata.
|
||||
*/
|
||||
public final class FxmlProjection {
|
||||
private static final String FX = "http://javafx.com/fxml/1";
|
||||
private static final Set<String> EDITABLE =
|
||||
Set.of(
|
||||
"caption",
|
||||
"text",
|
||||
"prefWidth",
|
||||
"prefHeight",
|
||||
"minWidth",
|
||||
"minHeight",
|
||||
"maxWidth",
|
||||
"maxHeight",
|
||||
"layoutX",
|
||||
"layoutY",
|
||||
"visible",
|
||||
"managed",
|
||||
"disable",
|
||||
"opacity",
|
||||
"alignment",
|
||||
"spacing",
|
||||
"hgap",
|
||||
"vgap",
|
||||
"GridPane.columnIndex",
|
||||
"GridPane.rowIndex",
|
||||
"GridPane.hgrow",
|
||||
"GridPane.vgrow");
|
||||
private final Document original;
|
||||
private Document baseline;
|
||||
private final Map<String, String> components = new LinkedHashMap<>();
|
||||
|
||||
public FxmlProjection(String text) throws Exception {
|
||||
original = parse(text);
|
||||
}
|
||||
|
||||
static Document parse(String text) throws Exception {
|
||||
var f = DocumentBuilderFactory.newInstance();
|
||||
f.setNamespaceAware(true);
|
||||
f.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
|
||||
f.setXIncludeAware(false);
|
||||
f.setExpandEntityReferences(false);
|
||||
return f.newDocumentBuilder().parse(new org.xml.sax.InputSource(new StringReader(text)));
|
||||
}
|
||||
|
||||
static String xml(Document d) throws Exception {
|
||||
var t = TransformerFactory.newInstance().newTransformer();
|
||||
var w = new StringWriter();
|
||||
t.transform(new DOMSource(d), new StreamResult(w));
|
||||
return w.toString();
|
||||
}
|
||||
|
||||
public Map<String, String> components() {
|
||||
return Map.copyOf(components);
|
||||
}
|
||||
|
||||
public String project() throws Exception {
|
||||
var d = (Document) original.cloneNode(true);
|
||||
var imports = new HashMap<String, String>();
|
||||
var remove = new ArrayList<Node>();
|
||||
for (Node n = d.getFirstChild(); n != null; n = n.getNextSibling())
|
||||
if (n instanceof ProcessingInstruction pi && pi.getTarget().equals("import")) {
|
||||
String type = pi.getData().trim();
|
||||
if (!type.startsWith("javafx.")) {
|
||||
if (type.endsWith(".*"))
|
||||
throw new IOException("Application wildcard imports need an explicit Component import");
|
||||
imports.put(type.substring(type.lastIndexOf('.') + 1), type);
|
||||
remove.add(n);
|
||||
}
|
||||
}
|
||||
remove.forEach(d::removeChild);
|
||||
d.insertBefore(
|
||||
d.createProcessingInstruction("import", SnapshotRegion.class.getName()),
|
||||
d.getDocumentElement());
|
||||
var definitions = d.getElementsByTagNameNS(FX, "define");
|
||||
while (definitions.getLength() > 0) {
|
||||
Node n = definitions.item(0);
|
||||
n.getParentNode().removeChild(n);
|
||||
}
|
||||
var nodes = d.getElementsByTagName("*");
|
||||
for (int i = 0; i < nodes.getLength(); i++) {
|
||||
var e = (Element) nodes.item(i);
|
||||
String tag = e.getTagName();
|
||||
if (FX.equals(e.getNamespaceURI()))
|
||||
throw new IOException("This minimal projection does not support " + tag);
|
||||
if (imports.containsKey(tag)) {
|
||||
String id = e.getAttributeNS(FX, "id");
|
||||
if (id.isBlank()) throw new IOException("Component projection requires fx:id");
|
||||
components.put(id, imports.get(tag));
|
||||
if (e.hasChildNodes())
|
||||
throw new IOException("Inline Component content is outside this projection");
|
||||
d.renameNode(e, null, "SnapshotRegion");
|
||||
var attrs = e.getAttributes();
|
||||
for (int a = attrs.getLength() - 1; a >= 0; a--) {
|
||||
var attr = (Attr) attrs.item(a);
|
||||
if (!EDITABLE.contains(attr.getName()) && !attr.getName().equals("fx:id"))
|
||||
e.removeAttributeNode(attr);
|
||||
}
|
||||
} else if (Character.isUpperCase(tag.charAt(0))
|
||||
&& tag.contains(".")
|
||||
&& !tag.startsWith("javafx."))
|
||||
throw new IOException("Unsupported application element: " + tag);
|
||||
e.removeAttributeNS(FX, "controller");
|
||||
var attrs = e.getAttributes();
|
||||
for (int a = attrs.getLength() - 1; a >= 0; a--) {
|
||||
var attr = (Attr) attrs.item(a);
|
||||
if (attr.getValue().startsWith("#")
|
||||
|| attr.getValue().startsWith("$")
|
||||
|| attr.getName().startsWith("on")) e.removeAttributeNode(attr);
|
||||
}
|
||||
}
|
||||
baseline = (Document) d.cloneNode(true);
|
||||
return xml(d);
|
||||
}
|
||||
|
||||
private static Map<String, Element> indexed(Document d) throws IOException {
|
||||
var result = new LinkedHashMap<String, Element>();
|
||||
result.put("@root", d.getDocumentElement());
|
||||
var nodes = d.getElementsByTagName("*");
|
||||
for (int i = 0; i < nodes.getLength(); i++) {
|
||||
var e = (Element) nodes.item(i);
|
||||
String id = e.getAttributeNS(FX, "id");
|
||||
if (!id.isBlank() && result.putIfAbsent(id, e) != null)
|
||||
throw new IOException("Duplicate fx:id");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static java.util.List<String> structure(Document document) {
|
||||
var result = new java.util.ArrayList<String>();
|
||||
var objects = new IdentityHashMap<Node, Integer>();
|
||||
var nodes = document.getElementsByTagName("*");
|
||||
for (int i = 0; i < nodes.getLength(); i++) {
|
||||
var e = (Element) nodes.item(i);
|
||||
String tag = e.getTagName();
|
||||
if (Character.isUpperCase(tag.charAt(0))) {
|
||||
Node parent = e.getParentNode();
|
||||
while (parent != null && !objects.containsKey(parent)) parent = parent.getParentNode();
|
||||
var fixed = new TreeMap<String, String>();
|
||||
if (e != document.getDocumentElement() && e.getAttributeNS(FX, "id").isBlank())
|
||||
for (int a = 0; a < e.getAttributes().getLength(); a++) {
|
||||
var attr = e.getAttributes().item(a);
|
||||
if (!attr.getNodeName().startsWith("xmlns"))
|
||||
fixed.put(attr.getNodeName(), attr.getNodeValue());
|
||||
}
|
||||
result.add(
|
||||
tag
|
||||
+ ":"
|
||||
+ e.getAttributeNS(FX, "id")
|
||||
+ ":"
|
||||
+ objects.getOrDefault(parent, -1)
|
||||
+ ":"
|
||||
+ fixed);
|
||||
objects.put(e, objects.size());
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public String restore(String projected) throws Exception {
|
||||
var changed = parse(projected);
|
||||
var before = indexed(baseline);
|
||||
var after = indexed(changed);
|
||||
var output = (Document) original.cloneNode(true);
|
||||
var targets = indexed(output);
|
||||
if (!before.keySet().equals(after.keySet()) || !structure(baseline).equals(structure(changed)))
|
||||
throw new IOException("Structural editing is outside this minimal projection");
|
||||
for (String id : before.keySet()) {
|
||||
var attributes = new HashSet<String>();
|
||||
for (Element element : List.of(before.get(id), after.get(id)))
|
||||
for (int i = 0; i < element.getAttributes().getLength(); i++)
|
||||
attributes.add(element.getAttributes().item(i).getNodeName());
|
||||
for (String attr : attributes)
|
||||
if (!EDITABLE.contains(attr)
|
||||
&& !attr.startsWith("xmlns")
|
||||
&& !before.get(id).getAttribute(attr).equals(after.get(id).getAttribute(attr)))
|
||||
throw new IOException("Edit " + attr + " in source; this projection does not support it");
|
||||
}
|
||||
for (String id : before.keySet())
|
||||
for (String attr : EDITABLE) {
|
||||
String old = before.get(id).getAttribute(attr), value = after.get(id).getAttribute(attr);
|
||||
if (!old.equals(value)) {
|
||||
var target = targets.get(id);
|
||||
String originalValue = target.getAttribute(attr);
|
||||
if (originalValue.startsWith("$") || originalValue.startsWith("#"))
|
||||
throw new IOException("Bound attributes must be edited in source");
|
||||
if (value.isEmpty()) target.removeAttribute(attr);
|
||||
else target.setAttribute(attr, value);
|
||||
}
|
||||
}
|
||||
return xml(output);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user