com.github.mathiewz.slick.util.xml.ObjectTreeParser Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of modernized-slick Show documentation
Show all versions of modernized-slick Show documentation
The main purpose of this libraryis to modernize and maintain the slick2D library.
The newest version!
package com.github.mathiewz.slick.util.xml;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import com.github.mathiewz.slick.SlickException;
import com.github.mathiewz.slick.util.Log;
import com.github.mathiewz.slick.util.ResourceLoader;
/**
* Provides a method of parsing XML into an existing data model. This does not
* provide the same functionality as JAXB or the variety of XML bindings out there. This
* is a utility to map XML onto an existing data model. The idea being that the design level
* model should not be driven by the XML schema thats defined. The two arn't always equal
* and often you end up with a set of class that represent your XML that you then have
* to traverse to extract into your normal data model.
*
* This utility hopes to take a piece of XML and map it onto a previously designed data
* model. At the moment it's way to tied to the structure of the XML but this will
* hopefully change with time.
*
* XML element names must be mapped to class names. This can be done in two ways either:
*
* - Specify an explict mapping with addElementMapping()
* - Specify the default package name and use the element name as the class name
*
* Each attribute in an element is mapped into a property of the element class, preferably
* through a set<AttrName< bean method, but alternatively by direct injection into private
* fields.
*
* Each child element is added to the target class by call the method add() on it with a single
* parameter of the type generated for the child element.
*
* Classes can optionally implement setXMLElementName(String) and setXMLElementContent(String) to
* recieve the name and content respectively of the XMLElement they were parsed from. This can
* help when mapping two elements to a single class.
*
* To reiterate, I'm not sure this is a good idea yet. It helps me as a utility since I've done
* this several times in the past but in the general case it may not be perfect. Consider a custom
* parser using XMLParser or JAXB (et al) seriously instead.
*
* @author kevin
*
*/
public class ObjectTreeParser {
/** The mapping of XML element names to class names */
private final HashMap> nameToClass = new HashMap<>();
/** The default package where classes will be searched for */
private String defaultPackage;
/** The list of elements to ignore */
private final ArrayList ignored = new ArrayList<>();
/** The name of the method to add an child object to it's parent */
private String addMethod = "add";
/**
* Create an object tree parser with no default package
*/
public ObjectTreeParser() {
}
/**
* Create an object tree parser specifing the default package
* where classes will be search for using the XML element name
*
* @param defaultPackage
* The default package to be searched
*/
public ObjectTreeParser(String defaultPackage) {
this.defaultPackage = defaultPackage;
}
/**
* Add a mapping between XML element name and class name
*
* @param elementName
* The name of the XML element
* @param elementClass
* The class to be created for the given element
*/
public void addElementMapping(String elementName, Class extends Object> elementClass) {
nameToClass.put(elementName, elementClass);
}
/**
* Add a name to the list of elements ignored
*
* @param elementName
* The name to ignore
*/
public void addIgnoredElement(String elementName) {
ignored.add(elementName);
}
/**
* Set the name of the method to use to add child objects to their
* parents. This is sometimes useful to not clash with the existing
* data model methods.
*
* @param methodName
* The name of the method to call
*/
public void setAddMethodName(String methodName) {
addMethod = methodName;
}
/**
* Set the default package which will be search for classes by their XML
* element name.
*
* @param defaultPackage
* The default package to be searched
*/
public void setDefaultPackage(String defaultPackage) {
this.defaultPackage = defaultPackage;
}
/**
* Parse the XML document located by the slick resource loader using the
* reference given.
*
* @param ref
* The reference to the XML document
* @return The root element of the newly parse document
*/
public Object parse(String ref) {
return parse(ref, ResourceLoader.getResourceAsStream(ref));
}
/**
* Parse the XML document that can be read from the given input stream
*
* @param name
* The name of the document
* @param in
* The input stream from which the document can be read
* @return The root element of the newly parse document
*/
public Object parse(String name, InputStream in) {
XMLParser parser = new XMLParser();
XMLElement root = parser.parse(name, in);
return traverse(root);
}
/**
* Parse the XML document located by the slick resource loader using the
* reference given.
*
* @param ref
* The reference to the XML document
* @param target
* The top level object that represents the root node
* @return The root element of the newly parse document
*/
public Object parseOnto(String ref, Object target) {
return parseOnto(ref, ResourceLoader.getResourceAsStream(ref), target);
}
/**
* Parse the XML document that can be read from the given input stream
*
* @param name
* The name of the document
* @param in
* The input stream from which the document can be read
* @param target
* The top level object that represents the root node
* @return The root element of the newly parse document
*/
public Object parseOnto(String name, InputStream in, Object target) {
XMLParser parser = new XMLParser();
XMLElement root = parser.parse(name, in);
return traverse(root, target);
}
/**
* Deterine the name of the class that should be used for a given
* XML element name.
*
* @param name
* The name of the XML element
* @return The class to be used or null if none can be found
*/
private Class extends Object> getClassForElementName(String name) {
Class extends Object> clazz = nameToClass.get(name);
if (clazz != null) {
return clazz;
}
if (defaultPackage != null) {
try {
return Class.forName(defaultPackage + "." + name);
} catch (ClassNotFoundException e) {
// ignore, it's just not there
}
}
return null;
}
/**
* Traverse the XML element specified generating the appropriate object structure
* for it and it's children
*
* @param current
* The XML element to process
* @return The object created for the given element
*/
private Object traverse(XMLElement current) {
return traverse(current, null);
}
/**
* Traverse the XML element specified generating the appropriate object structure
* for it and it's children
*
* @param current
* The XML element to process
* @param instance
* The instance to parse onto, normally null
* @return The object created for the given element
*/
private Object traverse(XMLElement current, Object instance) {
String name = current.getName();
if (ignored.contains(name)) {
return null;
}
Class extends Object> clazz;
if (instance == null) {
clazz = getClassForElementName(name);
} else {
clazz = instance.getClass();
}
if (clazz == null) {
throw new SlickXMLException("Unable to map element " + name + " to a class, define the mapping");
}
try {
if (instance == null) {
instance = clazz.newInstance();
@SuppressWarnings("unchecked")
Method elementNameMethod = getMethod(clazz, "setXMLElementName", new Class[] { String.class });
if (elementNameMethod != null) {
invoke(elementNameMethod, instance, new Object[] { name });
}
@SuppressWarnings("unchecked")
Method contentMethod = getMethod(clazz, "setXMLElementContent", new Class[] { String.class });
if (contentMethod != null) {
invoke(contentMethod, instance, new Object[] { current.getContent() });
}
}
String[] attrs = current.getAttributeNames();
for (String attr : attrs) {
String methodName = "set" + attr;
Method method = findMethod(clazz, methodName);
if (method == null) {
Field field = findField(clazz, attr);
if (field != null) {
String value = current.getAttribute(attr);
Object typedValue = typeValue(value, field.getType());
setField(field, instance, typedValue);
} else {
Log.info("Unable to find property on: " + clazz + " for attribute: " + attr);
}
} else {
String value = current.getAttribute(attr);
Object typedValue = typeValue(value, method.getParameterTypes()[0]);
invoke(method, instance, new Object[] { typedValue });
}
}
XMLElementList children = current.getChildren();
for (int i = 0; i < children.size(); i++) {
XMLElement element = children.get(i);
Object child = traverse(element);
if (child != null) {
String methodName = addMethod;
Method method = findMethod(clazz, methodName, child.getClass());
if (method == null) {
Log.info("Unable to find method to add: " + child + " to " + clazz);
} else {
invoke(method, instance, new Object[] { child });
}
}
}
return instance;
} catch (InstantiationException e) {
throw new SlickXMLException("Unable to instance " + clazz + " for element " + name + ", no zero parameter constructor?", e);
} catch (IllegalAccessException e) {
throw new SlickXMLException("Unable to instance " + clazz + " for element " + name + ", no zero parameter constructor?", e);
}
}
/**
* Convert a given value to a given type
*
* @param value
* The value to convert
* @param clazz
* The class that the returned object must be
* @return The value as the given type
*/
private Object typeValue(String value, Class extends Object> clazz) {
if (clazz == String.class) {
return value;
}
try {
clazz = mapPrimitive(clazz);
return clazz.getConstructor(new Class[] { String.class }).newInstance(new Object[] { value });
} catch (Exception e) {
throw new SlickXMLException("Failed to convert: " + value + " to the expected primitive type: " + clazz, e);
}
}
/**
* Map a primitive class type to it's real object wrapper
*
* @param clazz
* The primitive type class
* @return The object wrapper class
*/
private Class extends Object> mapPrimitive(Class extends Object> clazz) {
if (clazz == Integer.TYPE) {
return Integer.class;
}
if (clazz == Double.TYPE) {
return Double.class;
}
if (clazz == Float.TYPE) {
return Float.class;
}
if (clazz == Boolean.TYPE) {
return Boolean.class;
}
if (clazz == Long.TYPE) {
return Long.class;
}
throw new SlickException("Unsupported primitive: " + clazz);
}
/**
* Find a field in a class by it's name. Note that this method is
* only needed because the general reflection method is case
* sensitive
*
* @param clazz
* The clazz to search
* @param name
* The name of the field to search for
* @return The field or null if none could be located
*/
private Field findField(Class extends Object> clazz, String name) {
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
if (field.getName().equalsIgnoreCase(name)) {
if (field.getType().isPrimitive()) {
return field;
}
if (field.getType() == String.class) {
return field;
}
}
}
return null;
}
/**
* Find a method in a class by it's name. Note that this method is
* only needed because the general reflection method is case
* sensitive
*
* @param clazz
* The clazz to search
* @param name
* The name of the method to search for
* @return The method or null if none could be located
*/
private Method findMethod(Class extends Object> clazz, String name) {
Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
if (method.getName().equalsIgnoreCase(name)) {
Class extends Object>[] params = method.getParameterTypes();
if (params.length == 1) {
return method;
}
}
}
return null;
}
/**
* Find a method on a class with a single given parameter.
*
* @param clazz
* The clazz to search through
* @param name
* The name of the method to locate
* @param parameter
* The type the single parameter must have
* @return The method or null if none could be located
*/
private Method findMethod(Class extends Object> clazz, String name, Class extends Object> parameter) {
Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
if (method.getName().equalsIgnoreCase(name)) {
Class extends Object>[] params = method.getParameterTypes();
if (params.length == 1) {
if (method.getParameterTypes()[0].isAssignableFrom(parameter)) {
return method;
}
}
}
}
return null;
}
/**
* Set a field value on a object instance
*
* @param field
* The field to be set
* @param instance
* The instance of the object to set it on
* @param value
* The value to set
*/
private void setField(Field field, Object instance, Object value) {
try {
field.setAccessible(true);
field.set(instance, value);
} catch (IllegalArgumentException e) {
throw new SlickXMLException("Failed to set: " + field + " for an XML attribute, is it valid?", e);
} catch (IllegalAccessException e) {
throw new SlickXMLException("Failed to set: " + field + " for an XML attribute, is it valid?", e);
} finally {
field.setAccessible(false);
}
}
/**
* Call a method on a object
*
* @param method
* The method to call
* @param instance
* The objet to call the method on
* @param params
* The parameters to pass
*/
private void invoke(Method method, Object instance, Object[] params) {
try {
method.setAccessible(true);
method.invoke(instance, params);
} catch (IllegalArgumentException e) {
throw new SlickXMLException("Failed to invoke: " + method + " for an XML attribute, is it valid?", e);
} catch (IllegalAccessException e) {
throw new SlickXMLException("Failed to invoke: " + method + " for an XML attribute, is it valid?", e);
} catch (InvocationTargetException e) {
throw new SlickXMLException("Failed to invoke: " + method + " for an XML attribute, is it valid?", e);
} finally {
method.setAccessible(false);
}
}
/**
* Get a method on a given class. Only here for tidy purposes,
* hides the the big exceptions.
*
* @param clazz
* The class to search
* @param name
* The name of the method
* @param params
* The parameters for the method
* @return The method or null if none can be found
*/
private Method getMethod(Class extends Object> clazz, String name, Class extends Object>[] params) {
try {
return clazz.getMethod(name, params);
} catch (SecurityException e) {
return null;
} catch (NoSuchMethodException e) {
return null;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy