package com.alexgilleran.icesoap.parser.impl;
import java.lang.reflect.Field;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.management.modelmbean.XMLParseException;
import com.alexgilleran.icesoap.annotation.XMLField;
import com.alexgilleran.icesoap.annotation.XMLObject;
import com.alexgilleran.icesoap.exception.ClassDefException;
import com.alexgilleran.icesoap.exception.XMLParsingException;
import com.alexgilleran.icesoap.parser.IceSoapParser;
import com.alexgilleran.icesoap.parser.XPathPullParser;
import com.alexgilleran.icesoap.parser.processor.Processor;
import com.alexgilleran.icesoap.xpath.XPathRepository;
import com.alexgilleran.icesoap.xpath.XPathRepository.XPathRecord;
import com.alexgilleran.icesoap.xpath.elements.XPathElement;
/**
* Implementation of {@link IceSoapParser} for parsing an individual object.
*
* This takes in a class, then uses the {@link XMLObject} and {@link XMLField}
* annotations on it to determine the xpath of this object in SOAP calls, and
* the xpaths of each field within the object.
*
* Once it has these, every time it gets a call from
* {@link BaseIceSoapParserImpl#onNewTag(XPathPullParser, Object)}, it looks up
* the current xpath in its repository of {@link XPathElement}s to see if it has
* a field that matches it - if it does, it looks at the type of the field.
*
* <li>For basic types (primitives, {@link String}, {@link BigDecimal}, as
* defined by {@link #TEXT_NODE_CLASSES}), it gets the value from the parser and
* sets that field to the value - if the value is not an XML Text field an
* exception is thrown.</li> <li>For complex types annotated by
* {@link XMLObject}, it will instantiate another parser to parse an instance of
* this object, then set the instance to that field and continue parsing.</li>
* <li>If the field is a {@link List}, every time an element is encountered that
* matches the XPath specified in the {@link List} field's {@link XMLField}
* annotation, it will parse that object and add it to the list. This will work
* even if there's elements in between. Note that this is different to how it
* used to work in 1.0.4 and previous.
*
* @author Alex Gilleran
*
* @param <ReturnType>
* The type of the object being parsed.
*/
public class IceSoapParserImpl<ReturnType> extends BaseIceSoapParserImpl<ReturnType> {
/** Null character **/
private static final char PRIMITIVE_NULL_CHAR = '\0';
/** Equivalent to null for primitive number types (0) **/
private static final int PRIMITIVE_NULL_NUMBER = 0;
/**
* An {@link XPathRepository} that maps xpaths to the fields represented by
* them.
*/
private XPathRepository<Field> fieldXPaths;
/**
* The class of ReturnType.
*/
private Class<ReturnType> targetClass;
/**
* Types that can be parsed by simply getting the text value from a node or
* attribute.
*/
@SuppressWarnings("unchecked")
private static final Set<Class<?>> TEXT_NODE_CLASSES = new HashSet<Class<?>>(Arrays.asList(long.class, float.class,
int.class, double.class, boolean.class, BigDecimal.class, String.class, Date.class));
/** Maintains a cache of instantiated parsers for reuse **/
private HashMap<XPathElement, BaseIceSoapParserImpl<?>> parserCache = new HashMap<XPathElement, BaseIceSoapParserImpl<?>>();
/**
* Instantiates a new parser.
*
* @param targetClass
* The class of the object to parse - note that this must have a
* zero-arg constructor
*/
public IceSoapParserImpl(Class<ReturnType> targetClass) {
this(targetClass, retrieveRootXPaths(targetClass));
}
/**
* Instantiates a new parser. * @param targetClass The class of the object
* to parse.
*
* @param targetClass
* The class of the object to parse - note that this must have a
* zero-arg constructor
* @param rootXPath
* A root XPath to parse within - the parser will traverse the
* XML document until it finds this XPath and keep parsing until
* it finds the end, then finish. Note that the xml node
* described by this {@link XPathElement} can be outside the node
* specified by the {@link XMLObject} field of targetClass or the
* same, but cannot be within it.
*/
public IceSoapParserImpl(Class<ReturnType> targetClass, XPathElement rootXPath) {
this(targetClass, new XPathRepository<XPathElement>(rootXPath, rootXPath));
}
/**
* Instantiates a new parser. * @param targetClass The class of the object
* to parse.
*
* @param targetClass
* The class of the object to parse - note that this must have a
* zero-arg constructor
* @param rootXPaths
* The root XPath(s) to parse within - the parser will traverse
* the XML document until it finds one of these and keep parsing
* until it finds the end, then finish.
*/
public IceSoapParserImpl(Class<ReturnType> targetClass, XPathRepository<XPathElement> rootXPaths) {
super(rootXPaths);
this.targetClass = targetClass;
fieldXPaths = getFieldXPaths(targetClass);
}
/**
* Gets the xpaths declared with the {@link XMLField} annotation on a class.
*
* @param targetClass
* The class to get xpaths for.
* @return An {@link XPathRepository} linking xpaths to fields.
*/
private XPathRepository<Field> getFieldXPaths(Class<ReturnType> targetClass) {
XPathRepository<Field> fieldXPaths = new XPathRepository<Field>();
Class<?> currentClass = targetClass;
while (!currentClass.equals(Object.class)) {
addXPathFieldsToRepo(currentClass, fieldXPaths);
currentClass = currentClass.getSuperclass();
}
return fieldXPaths;
}
/**
* Adds the fields from the specified class to the passed
* {@link XPathRepository}, with the XPaths specified in the
* {@link XMLField} annotations.
*
* @param targetClass
* The class to draw fields from
* @param fieldXPaths
* The repository to add fields too
*/
private void addXPathFieldsToRepo(Class<?> targetClass, XPathRepository<Field> fieldXPaths) {
for (Field field : targetClass.getDeclaredFields()) {
XMLField xPath = field.getAnnotation(XMLField.class);
if (xPath != null) {
// Annotation is not present: do nothing for this field.
XPathRepository<XPathElement> xpathsFromField;
if (!xPath.value().equals(XMLField.DEFAULT_XPATH_STRING)) {
// If the XPath has a value specified, compile it
xpathsFromField = compileXPath(xPath, field);
addRootToRelativeXPaths(xpathsFromField);
} else {
// XPath has no value - set to the root value
xpathsFromField = getRootXPaths();
}
for (XPathElement element : xpathsFromField.keySet()) {
fieldXPaths.put(element, field);
}
}
}
}
/**
* Go through the supplied XPaths and for relative ones, add the root XPaths
* to the front of them so they're no longer relative and can be matched
* against.
*
* @param xpaths
* An {@link XPathRepository} of the xpaths to check and modify
* if necessary.
*/
private void addRootToRelativeXPaths(XPathRepository<XPathElement> xpaths) {
// Make a copy of the repo keyset, as we may need to concurrently remove
// xpaths from the repo while looping through.
Set<XPathElement> xpathsSet = new HashSet<XPathElement>(xpaths.keySet());
for (XPathElement thisXPath : xpathsSet) {
XPathElement firstXPathElement = thisXPath.getFirstElement();
if (firstXPathElement.isRelative()) {
// If the xpath is relative, we want to add the root xpath(s) of
// the object to the start of it.
if (getRootXPaths().keySet().size() == 1) {
thisXPath.getFirstElement().setPreviousElement(getRootXPaths().keySet().iterator().next());
} else {
// As there are multiple root xpaths, we need to create an
// new instance of this field xpath for each root xpath
// (using the existing field xpath object as a prototype)
// and add the enclosing xpath to the start of each.
// Remove the existing xpath from the repo (we'll add a
// modified clone of it later)
xpaths.remove(thisXPath);
for (XPathElement rootXPath : getRootXPaths().keySet()) {
// Copy the relative xpath for the field
XPathElement element = thisXPath.clone();
// Append the new xpath to this root xpath.
element.getFirstElement().setPreviousElement(rootXPath);
// Put the new element in the xpaths repository.
xpaths.put(element, element);
}
}
}
}
}
/**
* {@inheritDoc}
*
* Uses {@link Class#newInstance()} to instantiate a new instance of
* ReturnType.
*/
@Override
public ReturnType initializeParsedObject() {
try {
return targetClass.newInstance();
} catch (InstantiationException e) {
throwInitializationException(e);
} catch (IllegalAccessException e) {
throwInitializationException(e);
}
return null;
}
/**
* Throws a {@link ClassDefException} encountered when initializing the
* class.
*
* @param cause
* The root exception.
*/
private void throwInitializationException(Throwable cause) {
throw new ClassDefException(
"An exception was encountered while trying to instantiate a new instance of "
+ targetClass.getName()
+ ". This is probably because it doesn't implement a zero-arg constructor. To fix this, either change it so it has a zero-arg constructor, extend "
+ getClass().getSimpleName()
+ " and override the initializeParsedObject method, or make sure to always pass an existing object to the parser.",
cause);
}
/**
* {@inheritDoc}
*
* This method works by getting the current xpath from the
* {@link XPathPullParser} and seeing if there are any fields that match it.
* If there aren't, it does nothing. If there are, it checks the type of the
* field - if it's a type that can be set from a text node value, it gets
* the value from the node and sets it to the field. If it's a complex
* field, it calls {@link #getParserForField(Type, Class, XPathPullParser)}
* to parse it.
*
* @throws XMLParsingException
*/
@Override
protected ReturnType onNewTag(XPathPullParser xmlPullParser, ReturnType objectToModify) throws XMLParsingException {
// Get the field to set and the XPath it was stored against
XPathRecord<Field> xPathRecord = fieldXPaths.getFullRecord(xmlPullParser.getCurrentElement());
if (xPathRecord != null) {
Object valueToSet = null;
if (xmlPullParser.isCurrentValueXsiNil()) {
setFieldToNull(objectToModify, xPathRecord.getValue());
} else if (needsParser(xPathRecord.getValue())) {
// If a new parser is needed and the value is not nil (null),
// create the parser and set the value to the parsed value, else
// set it to the null above.
valueToSet = getParserForField(xPathRecord.getValue(), xmlPullParser, xPathRecord.getKey()).parse(
xmlPullParser);
setField(objectToModify, xPathRecord.getValue(), valueToSet);
}
}
return objectToModify;
}
/**
* {@inheritDoc}
*/
@Override
protected ReturnType onText(XPathPullParser pullParser, ReturnType objectToModify) throws XMLParsingException {
Field fieldToSet = fieldXPaths.get(pullParser.getCurrentElement());
if (fieldToSet != null) {
try {
String textNodeValue = pullParser.getCurrentValue();
XMLField annotation = fieldToSet.getAnnotation(XMLField.class);
boolean hasProcessor = hasProcessor(fieldToSet);
if (!needsParser(fieldToSet)) {
Object valueToSet;
if (hasProcessor) {
Processor processor = annotation.processor().newInstance();
valueToSet = processor.process(textNodeValue);
} else {
valueToSet = convertToFieldType(fieldToSet, textNodeValue);
}
setField(objectToModify, fieldToSet, valueToSet);
}
} catch (InstantiationException e) {
throw new XMLParsingException(e);
} catch (IllegalAccessException e) {
throw new XMLParsingException(e);
}
}
return objectToModify;
}
/**
* Checks if a field is annotated with a processor.
*
* @param field
* The field to check
* @return Whether or not it has a processor.
*/
protected boolean hasProcessor(Field field) {
return !field.getAnnotation(XMLField.class).processor().equals(Processor.class);
}
/**
* Determines whether a parser needs to be created - this is true if the
* field's type is not derived from a text node or a list of types derived
* from text nodes and will not be processed with a processor.
*
* @param fieldToSet
* @return
*/
private boolean needsParser(Field fieldToSet) {
// Is it a text node?
if (TEXT_NODE_CLASSES.contains(fieldToSet.getType())) {
return false;
}
// Does it have a processor to deal with it rather than a generic
// parser?
if (hasProcessor(fieldToSet)) {
return false;
}
// Is it a list of text nodes?
if (List.class.isAssignableFrom(fieldToSet.getType())
&& TEXT_NODE_CLASSES.contains(getListItemClass(fieldToSet.getGenericType()))) {
return false;
}
// No to all of the above, .'. it needs a parser
return true;
}
/**
* Given a class, attempts to find the appropriate parser for the class. If
* the class is an implementation of {@link List}, it attempts to get the
* class that the item is a list of and instantiate a {@link IceSoapParser}
* to parse that class.
*
* @param <ObjectType>
* The type of the object to create a parser for
* @param typeToParse
* The type to parse (as a {@link Type}
* @param classToParse
* The class to parse (as a {@link Class} - this should be the
* same as typeToParse.
* @param pullParser
* The pull parser used to do the parsing.
* @return A new instance of {@link IceSoapParser}
*/
private BaseIceSoapParserImpl<?> getParserForField(Field field, XPathPullParser pullParser, XPathElement fieldXPath) {
Class<?> classForParser = field.getType();
if (List.class.isAssignableFrom(field.getType())) {
// Class to parse is a list - find out the parameterized type of the
// list and create a parser for that, then wrap a ListParser around
// it.
classForParser = getListItemClass(field.getGenericType());
}
BaseIceSoapParserImpl<?> parserForClass = parserCache.get(fieldXPath);
if (parserForClass == null) {
parserForClass = new IceSoapParserImpl(classForParser, fieldXPath);
parserCache.put(fieldXPath, parserForClass);
}
// The type is not a list - create a parser
return parserForClass;
}
/**
* Sets the supplied field of the supplied object to null, substituting
* equivalent values if the field's type is a primitive and cannot be null.
*
* @param objectToModify
* Object to set the field on.
* @param fieldToSet
* Field in the object to set the value of.
*/
private void setFieldToNull(ReturnType objectToModify, Field fieldToSet) {
Class<?> type = fieldToSet.getType();
Object value = null;
if (type == int.class || type == long.class || type == double.class || type == float.class) {
value = PRIMITIVE_NULL_NUMBER;
} else if (type == boolean.class) {
value = false;
} else if (type == char.class) {
value = PRIMITIVE_NULL_CHAR;
}
setField(objectToModify, fieldToSet, value);
}
/**
* Sets the supplied {@link Field} in the supplied object to the supplied
* value, handling setting of accessibility and reflection exceptions.
*
* @param objectToModify
* The object to set the value on
* @param fieldToSet
* The field (as a {@link Field} to set
* @param valueToSet
* The value to set to the field.
*/
private void setField(ReturnType objectToModify, Field fieldToSet, Object valueToSet) {
try {
boolean isAccessibleBefore = fieldToSet.isAccessible();
fieldToSet.setAccessible(true);
if (List.class.isAssignableFrom(fieldToSet.getType())) {
Object valueOfField = fieldToSet.get(objectToModify);
if (valueOfField == null) {
List<Object> newList = new ArrayList<Object>();
newList.add(valueToSet);
valueToSet = newList;
} else {
((List<Object>) valueOfField).add(valueToSet);
valueToSet = valueOfField;
}
}
fieldToSet.set(objectToModify, valueToSet);
fieldToSet.setAccessible(isAccessibleBefore);
} catch (IllegalArgumentException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* Accepts a {@link String} taken from an XML parser and converts it into a
* primitive or primitive-esque (e.g. {@link BigDecimal}) type with the
* correct method.
*
* @param field
* The field to get the appropriate type from.
* @param valueString
* The string to parse to the correct type.
* @return The string's value as the appropriate type.
* @throws XMLParseException
*/
private Object convertToFieldType(Field field, String valueString) throws XMLParsingException {
XMLField annotation = field.getAnnotation(XMLField.class);
if (int.class.isAssignableFrom(field.getType())) {
return Integer.parseInt(valueString);
} else if (long.class.isAssignableFrom(field.getType())) {
return Long.parseLong(valueString);
} else if (float.class.isAssignableFrom(field.getType())) {
return Float.parseFloat(valueString);
} else if (double.class.isAssignableFrom(field.getType())) {
return Double.parseDouble(valueString);
} else if (boolean.class.isAssignableFrom(field.getType())) {
return Boolean.parseBoolean(valueString);
} else if (BigDecimal.class.isAssignableFrom(field.getType())) {
return new BigDecimal(valueString);
} else if (Date.class.isAssignableFrom(field.getType())) {
try {
return new SimpleDateFormat(annotation.dateFormat()).parse(valueString);
} catch (ParseException e) {
throw new XMLParsingException("Encountered date parsing exception when parsing " + field.toString()
+ " with format " + field.getAnnotation(XMLField.class).dateFormat() + " for value "
+ valueString, e);
}
} else {
return valueString;
}
}
}