All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.mathiewz.slick.util.xml.ObjectTreeParser Maven / Gradle / Ivy

Go to download

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 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 getClassForElementName(String name) {
        Class 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 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 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 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 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 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 clazz, String name) {
        Method[] methods = clazz.getDeclaredMethods();
        for (Method method : methods) {
            if (method.getName().equalsIgnoreCase(name)) {
                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 (Method method : methods) {
            if (method.getName().equalsIgnoreCase(name)) {
                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
     */
    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 clazz, String name, Class[] params) {
        try {
            return clazz.getMethod(name, params);
        } catch (SecurityException e) {
            return null;
        } catch (NoSuchMethodException e) {
            return null;
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy