package org.newdawn.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 org.newdawn.slick.util.Log;
import org.newdawn.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 HashMap nameToClass = new HashMap();
/** The default package where classes will be searched for */
private String defaultPackage;
/** The list of elements to ignore */
private 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 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
* @throws SlickXMLException Indicates a failure to parse the XML, most likely the
* XML is malformed in some way.
*/
public Object parse(String ref) throws SlickXMLException {
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
* @throws SlickXMLException Indicates a failure to parse the XML, most likely the
* XML is malformed in some way.
*/
public Object parse(String name, InputStream in) throws SlickXMLException {
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
* @throws SlickXMLException Indicates a failure to parse the XML, most likely the
* XML is malformed in some way.
*/
public Object parseOnto(String ref, Object target) throws SlickXMLException {
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
* @throws SlickXMLException Indicates a failure to parse the XML, most likely the
* XML is malformed in some way.
*/
public Object parseOnto(String name, InputStream in, Object target) throws SlickXMLException {
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 getClassForElementName(String name) {
Class clazz = (Class) 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
* @throws SlickXMLException
*/
private Object traverse(XMLElement current) throws SlickXMLException {
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
* @throws SlickXMLException
*/
private Object traverse(XMLElement current, Object instance) throws SlickXMLException {
String name = current.getName();
if (ignored.contains(name)) {
return null;
}
Class 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();
Method elementNameMethod = getMethod(clazz, "setXMLElementName", new Class[] {String.class});
if (elementNameMethod != null) {
invoke(elementNameMethod, instance, new Object[] {name});
}
Method contentMethod = getMethod(clazz, "setXMLElementContent", new Class[] {String.class});
if (contentMethod != null) {
invoke(contentMethod, instance, new Object[] {current.getContent()});
}
}
String[] attrs = current.getAttributeNames();
for (int i=0;i<attrs.length;i++) {
String methodName = "set"+attrs[i];
Method method = findMethod(clazz, methodName);
if (method == null) {
Field field = findField(clazz, attrs[i]);
if (field != null) {
String value = current.getAttribute(attrs[i]);
Object typedValue = typeValue(value, field.getType());
setField(field, instance, typedValue);
} else {
Log.info("Unable to find property on: "+clazz+" for attribute: "+attrs[i]);
}
} else {
String value = current.getAttribute(attrs[i]);
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
* @throws SlickXMLException Indicates there is no automatic way of converting the value to the type
*/
private Object typeValue(String value, Class clazz) throws SlickXMLException {
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 mapPrimitive(Class 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 RuntimeException("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 clazz, String name) {
Field[] fields = clazz.getDeclaredFields();
for (int i=0;i<fields.length;i++) {
if (fields[i].getName().equalsIgnoreCase(name)) {
if (fields[i].getType().isPrimitive()) {
return fields[i];
}
if (fields[i].getType() == String.class) {
return fields[i];
}
}
}
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 clazz, String name) {
Method[] methods = clazz.getDeclaredMethods();
for (int i=0;i<methods.length;i++) {
if (methods[i].getName().equalsIgnoreCase(name)) {
Method method = methods[i];
Class[] 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 clazz, String name, Class parameter) {
Method[] methods = clazz.getDeclaredMethods();
for (int i=0;i<methods.length;i++) {
if (methods[i].getName().equalsIgnoreCase(name)) {
Method method = methods[i];
Class[] 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
* @throws SlickXMLException Indicates a failure to set or access the field
*/
private void setField(Field field, Object instance, Object value) throws SlickXMLException {
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
* @throws SlickXMLException Indicates a failure to call or access the method
*/
private void invoke(Method method, Object instance, Object[] params) throws SlickXMLException {
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 clazz, String name, Class[] params) {
try {
return clazz.getMethod(name, params);
} catch (SecurityException e) {
return null;
} catch (NoSuchMethodException e) {
return null;
}
}
}