Extract Inflater interface

This commit is contained in:
2021-03-16 20:18:04 +01:00
parent e5b97546f6
commit 2b9624516d
3 changed files with 257 additions and 233 deletions

View File

@@ -113,9 +113,4 @@ public class Label extends BaseComponent {
gui.fill();
gui.putTextBox(x + paddingLeft, y + paddingTop, getWidth() - paddingLeft - paddingRight, text, bounds);
}
@Override
public String toString() {
return "Label[" + text + "]";
}
}

View File

@@ -0,0 +1,250 @@
package com.bartlomiejpluta.base.engine.gui.xml.inflater;
import com.bartlomiejpluta.base.api.game.context.Context;
import com.bartlomiejpluta.base.api.game.gui.base.GUI;
import com.bartlomiejpluta.base.api.game.gui.base.SizeMode;
import com.bartlomiejpluta.base.api.game.gui.component.Component;
import com.bartlomiejpluta.base.api.game.gui.window.Inflatable;
import com.bartlomiejpluta.base.api.game.gui.window.Ref;
import com.bartlomiejpluta.base.api.game.gui.window.Window;
import com.bartlomiejpluta.base.api.game.gui.window.WindowPosition;
import com.bartlomiejpluta.base.engine.error.AppException;
import com.bartlomiejpluta.base.engine.util.reflection.ClassLoader;
import lombok.SneakyThrows;
import org.codehaus.janino.ExpressionEvaluator;
import org.springframework.beans.factory.annotation.Autowired;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.File;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Stream;
@org.springframework.stereotype.Component
public class DefaultInflater implements Inflater {
private static final String[] IMPORTS = Stream.of(GUI.class, SizeMode.class, WindowPosition.class)
.map(Class::getCanonicalName)
.toArray(String[]::new);
private final DocumentBuilder builder;
private final ClassLoader loader;
@Autowired
@SneakyThrows
public DefaultInflater(ClassLoader loader) {
this.loader = loader;
var factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true);
this.builder = factory.newDocumentBuilder();
}
@Override
@SneakyThrows
public Component inflateComponent(String xml, Context context, GUI gui) {
var document = builder.parse(xml);
return inflateComponent(document.getDocumentElement(), context, gui);
}
@Override
@SneakyThrows
public Component inflateComponent(File file, Context context, GUI gui) {
var document = builder.parse(file);
return inflateComponent(document.getDocumentElement(), context, gui);
}
@Override
@SneakyThrows
public Component inflateComponent(InputStream is, Context context, GUI gui) {
var document = builder.parse(is);
return inflateComponent(document.getDocumentElement(), context, gui);
}
@Override
@SneakyThrows
public Window inflateWindow(String xml, Context context, GUI gui) {
var document = builder.parse(xml);
return inflateWindow(document.getDocumentElement(), context, gui);
}
@Override
@SneakyThrows
public Window inflateWindow(File file, Context context, GUI gui) {
var document = builder.parse(file);
return inflateWindow(document.getDocumentElement(), context, gui);
}
@Override
@SneakyThrows
public Window inflateWindow(InputStream is, Context context, GUI gui) {
var document = builder.parse(is);
return inflateWindow(document.getDocumentElement(), context, gui);
}
@SneakyThrows
private Window inflateWindow(Node root, Context context, GUI gui) {
var refs = new HashMap<String, Component>();
var uri = root.getNamespaceURI();
var name = root.getLocalName();
if (uri != null) {
name = uri + "." + name;
}
var canonicalName = name.replaceAll("\\*", "").replaceAll("\\.+", ".");
var windowClass = loader.loadClass(canonicalName);
var window = (Window) windowClass.getConstructor(Context.class, GUI.class).newInstance(context, gui);
var attributes = root.getAttributes();
// Set attributes via setter methods
for (int i = 0; i < attributes.getLength(); ++i) {
var attribute = attributes.item(i);
// Ignore namespaces definitions
if ("xmlns".equals(attribute.getPrefix())) {
continue;
}
var setterName = createSetterMethodName(attribute.getLocalName());
var value = parseValue(attribute.getNodeValue());
var setter = windowClass.getMethod(setterName, value.getClass());
setter.invoke(window, value);
}
// Parse window content
var children = root.getChildNodes();
boolean hasContent = false;
for (int i = 0; i < children.getLength(); ++i) {
var child = children.item(i);
if (child.getNodeType() != Node.ELEMENT_NODE) {
continue;
}
if (hasContent) {
throw new AppException("Window can have at most 1 child");
}
var content = parseNode(child, refs, context, gui);
window.setContent(content);
hasContent = true;
}
updateWindowRefs(window, refs);
if (window instanceof Inflatable) {
((Inflatable) window).onInflate();
}
return window;
}
@SneakyThrows
private void updateWindowRefs(Window window, Map<String, Component> refs) {
var windowClass = window.getClass();
for (var field : windowClass.getDeclaredFields()) {
if (field.isAnnotationPresent(Ref.class)) {
var ref = field.getAnnotation(Ref.class).value();
var referencedComponent = refs.get(ref);
if (referencedComponent == null) {
throw new AppException("The [%s] window is trying to reference component [%s] which does not exist", windowClass.getSimpleName(), ref);
}
field.setAccessible(true);
field.set(window, referencedComponent);
}
}
}
private Component inflateComponent(Node root, Context context, GUI gui) {
return parseNode(root, new HashMap<>(), context, gui);
}
@SneakyThrows
private Component parseNode(Node node, Map<String, Component> refs, Context context, GUI gui) {
var uri = node.getNamespaceURI();
var name = node.getLocalName();
if (uri != null) {
name = uri + "." + name;
}
var canonicalName = name.replaceAll("\\*", "").replaceAll("\\.+", ".");
var componentClass = loader.loadClass(canonicalName);
var component = createComponent(componentClass, node.getAttributes(), refs, context, gui);
var children = node.getChildNodes();
// The component is text-like node (e.g. <Label>Some text</Label>)
if (children.getLength() == 1) {
var child = children.item(0);
if (child.getNodeType() == Node.TEXT_NODE) {
var textSetter = componentClass.getMethod("setText", String.class);
textSetter.invoke(component, child.getNodeValue());
return component;
}
}
// The component has non-text children (e.g. <HLayout><Label>Some text</Label></HLayout>)
for (int i = 0; i < children.getLength(); ++i) {
var childNode = children.item(i);
if (childNode.getNodeType() != Node.ELEMENT_NODE) {
continue;
}
component.add(parseNode(childNode, refs, context, gui));
}
return component;
}
@SneakyThrows
private Component createComponent(Class<?> componentClass, NamedNodeMap attributes, Map<String, Component> refs, Context context, GUI gui) {
var component = (Component) componentClass.getConstructor(Context.class, GUI.class).newInstance(context, gui);
// Set attributes via setter methods
for (int i = 0; i < attributes.getLength(); ++i) {
var attribute = attributes.item(i);
// Ignore namespaces definitions
if ("xmlns".equals(attribute.getPrefix())) {
continue;
}
// Collect referencable components
if ("ref".equals(attribute.getLocalName())) {
refs.put(attribute.getNodeValue(), component);
continue;
}
var setterName = createSetterMethodName(attribute.getLocalName());
var value = parseValue(attribute.getNodeValue());
var setter = componentClass.getMethod(setterName, value.getClass());
setter.invoke(component, value);
}
return component;
}
private static String createSetterMethodName(String name) {
return "set" + name.substring(0, 1).toUpperCase() + name.substring(1);
}
@SneakyThrows
private Object parseValue(String value) {
var evaluator = new ExpressionEvaluator();
evaluator.setDefaultImports(IMPORTS);
evaluator.cook(value);
return evaluator.evaluate();
}
}

View File

@@ -2,243 +2,22 @@ package com.bartlomiejpluta.base.engine.gui.xml.inflater;
import com.bartlomiejpluta.base.api.game.context.Context;
import com.bartlomiejpluta.base.api.game.gui.base.GUI;
import com.bartlomiejpluta.base.api.game.gui.base.SizeMode;
import com.bartlomiejpluta.base.api.game.gui.component.Component;
import com.bartlomiejpluta.base.api.game.gui.window.Inflatable;
import com.bartlomiejpluta.base.api.game.gui.window.Ref;
import com.bartlomiejpluta.base.api.game.gui.window.Window;
import com.bartlomiejpluta.base.api.game.gui.window.WindowPosition;
import com.bartlomiejpluta.base.engine.error.AppException;
import com.bartlomiejpluta.base.engine.util.reflection.ClassLoader;
import lombok.SneakyThrows;
import org.codehaus.janino.ExpressionEvaluator;
import org.springframework.beans.factory.annotation.Autowired;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.File;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Stream;
@org.springframework.stereotype.Component
public class Inflater {
private static final String[] IMPORTS = Stream.of(GUI.class, SizeMode.class, WindowPosition.class)
.map(Class::getCanonicalName)
.toArray(String[]::new);
public interface Inflater {
Component inflateComponent(String xml, Context context, GUI gui);
private final DocumentBuilder builder;
private final ClassLoader loader;
Component inflateComponent(File file, Context context, GUI gui);
@Autowired
@SneakyThrows
public Inflater(ClassLoader loader) {
this.loader = loader;
Component inflateComponent(InputStream is, Context context, GUI gui);
var factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true);
this.builder = factory.newDocumentBuilder();
}
Window inflateWindow(String xml, Context context, GUI gui);
@SneakyThrows
public Component inflateComponent(String xml, Context context, GUI gui) {
var document = builder.parse(xml);
return inflateComponent(document.getDocumentElement(), context, gui);
}
Window inflateWindow(File file, Context context, GUI gui);
@SneakyThrows
public Component inflateComponent(File file, Context context, GUI gui) {
var document = builder.parse(file);
return inflateComponent(document.getDocumentElement(), context, gui);
}
@SneakyThrows
public Component inflateComponent(InputStream is, Context context, GUI gui) {
var document = builder.parse(is);
return inflateComponent(document.getDocumentElement(), context, gui);
}
@SneakyThrows
public Window inflateWindow(String xml, Context context, GUI gui) {
var document = builder.parse(xml);
return inflateWindow(document.getDocumentElement(), context, gui);
}
@SneakyThrows
public Window inflateWindow(File file, Context context, GUI gui) {
var document = builder.parse(file);
return inflateWindow(document.getDocumentElement(), context, gui);
}
@SneakyThrows
public Window inflateWindow(InputStream is, Context context, GUI gui) {
var document = builder.parse(is);
return inflateWindow(document.getDocumentElement(), context, gui);
}
@SneakyThrows
private Window inflateWindow(Node root, Context context, GUI gui) {
var refs = new HashMap<String, Component>();
var uri = root.getNamespaceURI();
var name = root.getLocalName();
if (uri != null) {
name = uri + "." + name;
}
var canonicalName = name.replaceAll("\\*", "").replaceAll("\\.+", ".");
var windowClass = loader.loadClass(canonicalName);
var window = (Window) windowClass.getConstructor(Context.class, GUI.class).newInstance(context, gui);
var attributes = root.getAttributes();
// Set attributes via setter methods
for (int i = 0; i < attributes.getLength(); ++i) {
var attribute = attributes.item(i);
// Ignore namespaces definitions
if ("xmlns".equals(attribute.getPrefix())) {
continue;
}
var setterName = createSetterMethodName(attribute.getLocalName());
var value = parseValue(attribute.getNodeValue());
var setter = windowClass.getMethod(setterName, value.getClass());
setter.invoke(window, value);
}
// Parse window content
var children = root.getChildNodes();
boolean hasContent = false;
for (int i = 0; i < children.getLength(); ++i) {
var child = children.item(i);
if (child.getNodeType() != Node.ELEMENT_NODE) {
continue;
}
if (hasContent) {
throw new AppException("Window can have at most 1 child");
}
var content = parseNode(child, refs, context, gui);
window.setContent(content);
hasContent = true;
}
updateWindowRefs(window, refs);
if (window instanceof Inflatable) {
((Inflatable) window).onInflate();
}
return window;
}
@SneakyThrows
private void updateWindowRefs(Window window, Map<String, Component> refs) {
var windowClass = window.getClass();
for (var field : windowClass.getDeclaredFields()) {
if (field.isAnnotationPresent(Ref.class)) {
var ref = field.getAnnotation(Ref.class).value();
var referencedComponent = refs.get(ref);
if (referencedComponent == null) {
throw new AppException("The [%s] window is trying to reference component [%s] which does not exist", windowClass.getSimpleName(), ref);
}
field.setAccessible(true);
field.set(window, referencedComponent);
}
}
}
private Component inflateComponent(Node root, Context context, GUI gui) {
return parseNode(root, new HashMap<>(), context, gui);
}
@SneakyThrows
private Component parseNode(Node node, Map<String, Component> refs, Context context, GUI gui) {
var uri = node.getNamespaceURI();
var name = node.getLocalName();
if (uri != null) {
name = uri + "." + name;
}
var canonicalName = name.replaceAll("\\*", "").replaceAll("\\.+", ".");
var componentClass = loader.loadClass(canonicalName);
var component = createComponent(componentClass, node.getAttributes(), refs, context, gui);
var children = node.getChildNodes();
// The component is text-like node (e.g. <Label>Some text</Label>)
if (children.getLength() == 1) {
var child = children.item(0);
if (child.getNodeType() == Node.TEXT_NODE) {
var textSetter = componentClass.getMethod("setText", String.class);
textSetter.invoke(component, child.getNodeValue());
return component;
}
}
// The component has non-text children (e.g. <HLayout><Label>Some text</Label></HLayout>)
for (int i = 0; i < children.getLength(); ++i) {
var childNode = children.item(i);
if (childNode.getNodeType() != Node.ELEMENT_NODE) {
continue;
}
component.add(parseNode(childNode, refs, context, gui));
}
return component;
}
@SneakyThrows
private Component createComponent(Class<?> componentClass, NamedNodeMap attributes, Map<String, Component> refs, Context context, GUI gui) {
var component = (Component) componentClass.getConstructor(Context.class, GUI.class).newInstance(context, gui);
// Set attributes via setter methods
for (int i = 0; i < attributes.getLength(); ++i) {
var attribute = attributes.item(i);
// Ignore namespaces definitions
if ("xmlns".equals(attribute.getPrefix())) {
continue;
}
// Collect referencable components
if ("ref".equals(attribute.getLocalName())) {
refs.put(attribute.getNodeValue(), component);
continue;
}
var setterName = createSetterMethodName(attribute.getLocalName());
var value = parseValue(attribute.getNodeValue());
var setter = componentClass.getMethod(setterName, value.getClass());
setter.invoke(component, value);
}
return component;
}
private static String createSetterMethodName(String name) {
return "set" + name.substring(0, 1).toUpperCase() + name.substring(1);
}
@SneakyThrows
private Object parseValue(String value) {
var evaluator = new ExpressionEvaluator();
evaluator.setDefaultImports(IMPORTS);
evaluator.cook(value);
return evaluator.evaluate();
}
Window inflateWindow(InputStream is, Context context, GUI gui);
}