package se.dolkow.tangiblexml;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import android.util.Pair;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.Collection;
import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT;
import static org.xmlpull.v1.XmlPullParser.FEATURE_PROCESS_NAMESPACES;
import static org.xmlpull.v1.XmlPullParser.START_DOCUMENT;
import static org.xmlpull.v1.XmlPullParser.START_TAG;
import static se.dolkow.tangiblexml.Util.TAG;
/**
* Parse XML into objects.
*
* The parser object is reusable and reentrant.
*
* @param <E> the type of the result
*/
public class Parser<E> {
private final @NonNull FieldPutter<Holder<E>,E> putter;
private final @NonNull ParserNode<Holder<E>> root;
private final @NonNull String rootName;
private final @NonNull TangibleFieldCache cache = TangibleFieldCache.getInstance();
static boolean debug = false;
public Parser(@NonNull Class<E> resultClass) throws SetupException {
Trace.beginSection("parser init " + resultClass.getSimpleName());
TangibleRoot annot = resultClass.getAnnotation(TangibleRoot.class);
if (annot == null) {
String cname = resultClass.getSimpleName();
String aname = TangibleRoot.class.getSimpleName();
throw new SetupException(cname + " doesn't have annotation " + aname);
}
String path = annot.value();
if (!path.startsWith("/")) {
String msg = "BUG: path for " + resultClass.getSimpleName() + " is not absolute";
throw new SetupException(msg);
}
String[] parts = path.substring(1).split("/");
try {
putter = new FieldPutter<>(Holder.class.getField("value"));
} catch (NoSuchFieldException e) {
throw new SetupException("BUG: can't find Holder.value field", e);
}
rootName = parts[0];
InnerNode<Holder<E>> builder = new InnerNode<>();
builder.extend(parts, putter, resultClass);
if (builder.children.size() != 1) {
throw new SetupException("Need exactly one root, found " + builder.children.size());
}
final ParserNode<Holder<E>> root = builder.children.get(rootName);
if (root == null) {
Object act = builder.children.keySet().toArray()[0];
throw new SetupException("Unexpected root name " + act + ", wanted " + rootName);
}
this.root = root;
if (debug) {
StringBuilder sb = new StringBuilder();
root.dump(sb, 0, rootName);
Log.d(TAG, sb.toString());
}
Trace.endSection();
}
public static void setDebug(boolean on) {
debug = on;
}
public @NonNull E parse(@NonNull XmlPullParser xml)
throws ConversionException, InputException, ValueCountException,
NotSupportedException, InvalidFieldException, ReflectionException {
Trace.beginSection(getClass().getSimpleName() + " parse");
Holder<E> holder = new Holder<>();
try {
xml.setFeature(FEATURE_PROCESS_NAMESPACES, false);
xml.require(START_DOCUMENT, null, null);
while (xml.getEventType() != END_DOCUMENT) {
if (xml.getEventType() != START_TAG) {
xml.next();
continue;
}
String name = xml.getName();
if (name.equals(rootName)) {
root.parse(xml, holder);
} else {
Util.skip(xml);
}
}
E result = putter.get(holder);
if (result == null) {
throw new ValueCountException("Failed to find result node");
}
validate(result);
return result;
} catch (IOException e) {
throw new InputException(e);
} catch (XmlPullParserException e) {
throw new InputException(e);
} finally {
Trace.endSection();
}
}
private void validate(@NonNull Object obj)
throws ValueCountException, ReflectionException, InvalidFieldException {
if (obj instanceof Iterable) {
for (Object child : (Iterable)obj) {
validate(child);
}
}
try {
for (Pair<Field,TangibleField> pair : cache.get(obj.getClass())) {
final Field f = pair.first;
final TangibleField tang = pair.second;
Object child = f.get(obj);
if (tang.required()) {
if (child == null) {
String msg = obj + "is missing required field " + f;
msg += ", expected at " + tang.value();
throw new ValueCountException(msg);
} else if (child instanceof Collection && ((Collection)child).isEmpty()) {
String msg = f + " in " + obj + " is empty";
msg += ", expected to find elements at " + tang.value();
throw new ValueCountException(msg);
}
}
if (child != null) {
validate(child);
}
}
} catch (IllegalAccessException e) {
throw new ReflectionException(e);
}
}
private static class Holder<E> {
@SuppressWarnings("unused")
public @Nullable E value;
}
}