// License: GPL. Copyright 2007 by Immanuel Scholz and others
package org.openstreetmap.josm.tools;
import static org.openstreetmap.josm.tools.I18n.tr;
import java.io.Reader;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Stack;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import javax.xml.parsers.SAXParserFactory;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.DefaultHandler;
/**
* An helper class that reads from a XML stream into specific objects.
*
* @author Imi
*/
public class XmlObjectParser implements Iterable<Object> {
public static class PresetParsingException extends SAXException {
private int columnNumber;
private int lineNumber;
public PresetParsingException() {
super();
}
public PresetParsingException(Exception e) {
super(e);
}
public PresetParsingException(String message, Exception e) {
super(message, e);
}
public PresetParsingException(String message) {
super(message);
}
public PresetParsingException rememberLocation(Locator locator) {
if (locator == null) return this;
this.columnNumber = locator.getColumnNumber();
this.lineNumber = locator.getLineNumber();
return this;
}
@Override
public String getMessage() {
String msg = super.getMessage();
if (lineNumber == 0 && columnNumber == 0)
return msg;
if (msg == null) {
msg = getClass().getName();
}
msg = msg + " " + tr("(at line {0}, column {1})", lineNumber, columnNumber);
return msg;
}
public int getColumnNumber() {
return columnNumber;
}
public int getLineNumber() {
return lineNumber;
}
}
public static final String lang = LanguageInfo.getLanguageCodeXML();
public static class Uniform<T> implements Iterable<T>{
private Iterator<Object> iterator;
/**
* @param klass This has to be specified since generics are ereased from
* class files so the JVM cannot deduce T itself.
*/
public Uniform(Reader input, String tagname, Class<T> klass) {
XmlObjectParser parser = new XmlObjectParser();
parser.map(tagname, klass);
parser.start(input);
iterator = parser.iterator();
}
public Iterator<T> iterator() {
return new Iterator<T>(){
public boolean hasNext() {return iterator.hasNext();}
@SuppressWarnings("unchecked") public T next() {return (T)iterator.next();}
public void remove() {iterator.remove();}
};
}
}
private class Parser extends DefaultHandler {
Stack<Object> current = new Stack<Object>();
String characters = "";
private Locator locator;
@Override
public void setDocumentLocator(Locator locator) {
this.locator = locator;
}
protected void throwException(Exception e) throws PresetParsingException{
throw new PresetParsingException(e).rememberLocation(locator);
}
@Override public void startElement(String ns, String lname, String qname, Attributes a) throws SAXException {
if (mapping.containsKey(qname)) {
Class<?> klass = mapping.get(qname).klass;
try {
current.push(klass.newInstance());
} catch (Exception e) {
throwException(e);
}
for (int i = 0; i < a.getLength(); ++i) {
setValue(a.getQName(i), a.getValue(i));
}
if (mapping.get(qname).onStart) {
report();
}
if (mapping.get(qname).both)
{
try {
queue.put(current.peek());
} catch (InterruptedException e) {
}
}
}
}
@Override public void endElement(String ns, String lname, String qname) throws SAXException {
if (mapping.containsKey(qname) && !mapping.get(qname).onStart) {
report();
} else if (characters != null && !current.isEmpty()) {
setValue(qname, characters.trim());
characters = "";
}
}
@Override public void characters(char[] ch, int start, int length) {
String s = new String(ch, start, length);
characters += s;
}
private void report() {
try {
queue.put(current.pop());
} catch (InterruptedException e) {
}
characters = "";
}
private Object getValueForClass(Class<?> klass, String value) {
if (klass == Boolean.TYPE)
return parseBoolean(value);
else if (klass == Integer.TYPE || klass == Long.TYPE)
return Long.parseLong(value);
else if (klass == Float.TYPE || klass == Double.TYPE)
return Double.parseDouble(value);
return value;
}
private void setValue(String fieldName, String value) throws SAXException {
if (fieldName.equals("class") || fieldName.equals("default") || fieldName.equals("throw") || fieldName.equals("new") || fieldName.equals("null")) {
fieldName += "_";
}
try {
Object c = current.peek();
Field f = null;
try {
f = c.getClass().getField(fieldName);
} catch (NoSuchFieldException e) {
if(fieldName.startsWith(lang))
{
String locfieldName = "locale_" +
fieldName.substring(lang.length());
try {
f = c.getClass().getField(locfieldName);
} catch (NoSuchFieldException ex) {
}
}
}
if (f != null && Modifier.isPublic(f.getModifiers())) {
f.set(c, getValueForClass(f.getType(), value));
} else {
if(fieldName.startsWith(lang))
{
int l = lang.length();
fieldName = "set" + fieldName.substring(l,l+1).toUpperCase() + fieldName.substring(l+1);
}
else
{
fieldName = "set" + fieldName.substring(0,1).toUpperCase() + fieldName.substring(1);
}
Method[] methods = c.getClass().getDeclaredMethods();
for (Method m : methods) {
if (m.getName().equals(fieldName) && m.getParameterTypes().length == 1) {
m.invoke(c, new Object[]{getValueForClass(m.getParameterTypes()[0], value)});
return;
}
}
}
} catch (Exception e) {
e.printStackTrace(); // SAXException does not dump inner exceptions.
throwException(e);
}
}
private boolean parseBoolean(String s) {
return s != null &&
!s.equals("0") &&
!s.startsWith("off") &&
!s.startsWith("false") &&
!s.startsWith("no");
}
@Override
public void error(SAXParseException e) throws SAXException {
throwException(e);
}
@Override
public void fatalError(SAXParseException e) throws SAXException {
throwException(e);
}
}
private static class Entry {
Class<?> klass;
boolean onStart;
boolean both;
public Entry(Class<?> klass, boolean onStart, boolean both) {
super();
this.klass = klass;
this.onStart = onStart;
this.both = both;
}
}
private Map<String, Entry> mapping = new HashMap<String, Entry>();
private Parser parser;
/**
* The queue of already parsed items from the parsing thread.
*/
private BlockingQueue<Object> queue = new ArrayBlockingQueue<Object>(10);
/**
* This stores one item retrieved from the queue to give hasNext a chance.
* So this is also the object that will be returned on the next call to next().
*/
private Object lookAhead = null;
/**
* This object represent the end of the stream (null is not allowed as
* member in class Queue).
*/
private Object EOS = new Object();
public XmlObjectParser() {
parser = new Parser();
}
public Iterable<Object> start(final Reader in) {
new Thread("XML Reader"){
@Override public void run() {
try {
SAXParserFactory.newInstance().newSAXParser().parse(new InputSource(in), parser);
} catch (Exception e) {
try {
queue.put(e);
} catch (InterruptedException e1) {
}
}
parser = null;
try {
queue.put(EOS);
} catch (InterruptedException e) {
}
}
}.start();
return this;
}
public void map(String tagName, Class<?> klass) {
mapping.put(tagName, new Entry(klass,false,false));
}
public void mapOnStart(String tagName, Class<?> klass) {
mapping.put(tagName, new Entry(klass,true,false));
}
public void mapBoth(String tagName, Class<?> klass) {
mapping.put(tagName, new Entry(klass,false,true));
}
/**
* @return The next object from the xml stream or <code>null</code>,
* if no more objects.
*/
public Object next() throws SAXException {
fillLookAhead();
if (lookAhead == EOS)
throw new NoSuchElementException();
Object o = lookAhead;
lookAhead = null;
return o;
}
private void fillLookAhead() throws SAXException {
if (lookAhead != null)
return;
try {
lookAhead = queue.take();
if (lookAhead instanceof SAXException)
throw (SAXException)lookAhead;
else if (lookAhead instanceof RuntimeException)
throw (RuntimeException)lookAhead;
else if (lookAhead instanceof Exception)
throw new SAXException((Exception)lookAhead);
} catch (InterruptedException e) {
throw new RuntimeException("XmlObjectParser must not be interrupted.", e);
}
}
public boolean hasNext() throws SAXException {
fillLookAhead();
return lookAhead != EOS;
}
public Iterator<Object> iterator() {
return new Iterator<Object>(){
public boolean hasNext() {
try {
return XmlObjectParser.this.hasNext();
} catch (SAXException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
public Object next() {
try {
return XmlObjectParser.this.next();
} catch (SAXException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
}