-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathXMLUI.java
More file actions
262 lines (220 loc) · 11 KB
/
XMLUI.java
File metadata and controls
262 lines (220 loc) · 11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
package readiefur.xml_ui;
import java.awt.Component;
import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.xml.sax.SAXException;
import readiefur.xml_ui.attributes.BindingAttribute;
import readiefur.xml_ui.attributes.EventCallbackAttribute;
import readiefur.xml_ui.attributes.NamedComponentAttribute;
import readiefur.xml_ui.exceptions.InvalidXMLException;
import readiefur.xml_ui.factory.UIBuilderFactory;
import readiefur.xml_ui.interfaces.IRootComponent;
/**
* The base class for all XMLUI classes.
* @param <TRootComponent> The type of the root component.
*/
public class XMLUI<TRootComponent extends Component & IRootComponent>
{
private final Map<String, Component> namedComponents;
/*Originally I had this as a protected field however there isn't too much point in that because it would be
*restricting more than it's worth and in Java you can't hide fields like in C# how you can with the new keyword.*/
public final TRootComponent rootComponent;
protected XMLUI() throws IOException, ParserConfigurationException, SAXException, InvalidXMLException, IllegalArgumentException, IllegalAccessException
{
Map<String, String> xmlNamespaces = new HashMap<>();
Map<String, String> resources = new HashMap<>();
Map<String, Observable<String>> bindableMembers = new HashMap<>();
Map<String, Consumer<Object[]>> eventCallbacks = new HashMap<>();
//Gets the intermediate path to the class file which we will use to load the XML file.
InputStream xmlFileStream = this.getClass().getResourceAsStream(this.getClass().getSimpleName() + ".xml");
//#region Load the XML file
Element xmlRootElement;
try
{
//Setup the XML parser.
DocumentBuilderFactory factory = DocumentBuilderFactory.newDefaultInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
//Parse the XML file.
Document document = builder.parse(xmlFileStream);
//Get the root element.
xmlRootElement = document.getDocumentElement();
}
finally
{
xmlFileStream.close();
}
//#endregion
if (xmlRootElement.getAttributes() == null)
throw new IOException("The root element of the XML file does not have any attributes. (The root element must have a namespace defined).");
//#region Get the namespaces
for (int i = 0; i < xmlRootElement.getAttributes().getLength(); i++)
{
String attributeName = xmlRootElement.getAttributes().item(i).getNodeName();
if (attributeName.startsWith("xmlns"))
{
String namespaceName;
if (attributeName.startsWith("xmlns:"))
{
namespaceName = attributeName.substring(6);
if (namespaceName.isEmpty())
throw new InvalidXMLException("A key'd namespace cannot be empty.");
}
else
{
namespaceName = "";
}
String namespaceValue = xmlRootElement.getAttributes().item(i).getNodeValue();
if (xmlNamespaces.containsKey(namespaceName))
throw new InvalidXMLException("The namespace '" + namespaceName + "' is defined more than once.");
xmlNamespaces.put(namespaceName, namespaceValue);
}
}
//#endregion
//#region Get the resources
for (Node node : Helpers.GetElementNodes(xmlRootElement))
{
if (!node.getNodeName().equals(xmlRootElement.getNodeName() + ".Resources"))
continue;
for (Node resourceNode : Helpers.GetElementNodes(node))
{
if (!resourceNode.getNodeName().equals("Resource"))
throw new InvalidXMLException("The Resources group can only contain 'Resource' elements.");
if (!resourceNode.hasAttributes())
throw new InvalidXMLException("The Resource element does not have any attributes.");
if (resourceNode.hasChildNodes())
throw new InvalidXMLException("The Resource element cannot have any child nodes.");
Node resourceKey = resourceNode.getAttributes().getNamedItem("Key");
Node resourceValue = resourceNode.getAttributes().getNamedItem("Value");
if (resourceKey == null || resourceKey.getNodeValue() == null
|| resourceValue == null || resourceValue.getNodeValue() == null)
throw new InvalidXMLException("The Resource element must have a 'Key' and 'Value' attribute.");
String resourceKeyString = resourceKey.getNodeValue();
String resourceValueString = resourceValue.getNodeValue();
if (resources.containsKey(resourceKeyString))
throw new InvalidXMLException("The resource '" + resourceKeyString + "' is defined more than once.");
resources.put(resourceKeyString, resourceValueString);
}
break;
}
//#endregion
//#region Preprocess class fields
for (Field field : this.getClass().getDeclaredFields())
{
Annotation[] attributes = field.getAnnotations();
//We won't need to check for duplicate attributes because by default only one is allowed per member.
//Attributes that can be repeated appear under their own @Repeatable annotation.
for (Annotation attribute : attributes)
{
//Get the bindable members.
if (attribute instanceof BindingAttribute)
{
//Make sure that the method constrains to the requirements of ({@see BindingAttribute}).
if (field.getType() != Observable.class)
// || !field.getGenericType().getTypeName().equals(Observable.class.getCanonicalName() + "<" + String.class.getCanonicalName() + ">"))
throw new IllegalArgumentException(
"Binding fields must be of type Observable<String>. (" + this.getClass().getSimpleName() + "::" + field.getName() + ")");
//We must also construct them at this stage as in Java class members are initialized after the constructor is called (unlike C#).
field.setAccessible(true);
field.set(this, new Observable<String>(((BindingAttribute)attribute).DefaultValue()));
//Add the bindable member to the dictionary.
bindableMembers.put(field.getName(), (Observable<String>)field.get(this));
}
}
}
//#endregion
//#region Preprocess class methods
for (Method method : this.getClass().getDeclaredMethods())
{
Annotation[] attributes = method.getAnnotations();
for (Annotation attribute : attributes)
{
if (attribute instanceof EventCallbackAttribute)
{
//Make sure that the method constrains to the requirements of ({@see EventCallbackAttribute}).
if (method.getParameterCount() != 1
|| method.getParameterTypes()[0] != Object[].class)
throw new IllegalArgumentException(
"Event callback methods must have exactly one parameter of type Object[]. (" + this.getClass().getSimpleName() + "::" + method.getName() + ")");
method.setAccessible(true);
//Capture the variable so that it can be used in the lambda expression.
final Method capturedMethod = method;
eventCallbacks.put(method.getName(), args ->
{
/*If the object array is passed "as is", the values will get unwrapped and cause an "wrong number of arguments" exception.
* So we wrap the object array in another array to prevent this.
*/
try { capturedMethod.invoke(this, new Object[] { args }); }
catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex)
{
throw new RuntimeException(ex);
}
});
}
}
}
//#endregion
//#region Build the UI tree.
UIBuilderFactory uiBuilderFactory = new UIBuilderFactory(
xmlNamespaces,
resources,
bindableMembers,
eventCallbacks);
uiBuilderFactory.SetDoRootComponentCheckForNextCall(false);
rootComponent = (TRootComponent)uiBuilderFactory.ParseXMLNode(xmlRootElement);
namedComponents = uiBuilderFactory.GetNamedComponents();
//#endregion
//#region Set the class named components
for (Field field : this.getClass().getDeclaredFields())
{
Annotation[] attributes = field.getAnnotations();
for (Annotation attribute : attributes)
{
if (!(attribute instanceof NamedComponentAttribute))
continue;
if (!Component.class.isAssignableFrom(field.getType()))
throw new IllegalArgumentException(
"Named component fields must be of type Component. (" + this.getClass().getSimpleName() + "::" + field.getName() + ")");
if (!namedComponents.containsKey(field.getName()))
throw new InvalidXMLException("The named component '" + field.getName() + "' is not defined in the XML document.");
field.setAccessible(true);
field.set(this, namedComponents.get(field.getName()));
}
}
//#endregion
}
protected <T extends Component> T GetNamedComponent(String componentName, Class<T> componentClass)
{
return (T)namedComponents.get(componentName);
}
protected Component GetNamedComponent(String componentName)
{
return GetNamedComponent(componentName, Component.class);
}
/**
* Shorthand for {@link #GetNamedComponent(String, Class)}.
*/
protected <T extends Component> T Get(String componentName, Class<T> componentClass)
{
return GetNamedComponent(componentName, componentClass);
}
/**
* Shorthand for {@link #GetNamedComponent(String)}.
*/
protected Component Get(String componentName)
{
return GetNamedComponent(componentName);
}
}