001 // License: GPL. Copyright 2007 by Immanuel Scholz and others
002 package org.openstreetmap.josm.tools;
003
004 import static org.openstreetmap.josm.tools.I18n.tr;
005
006 import java.io.IOException;
007 import java.io.Reader;
008 import java.lang.reflect.Field;
009 import java.lang.reflect.Method;
010 import java.lang.reflect.Modifier;
011 import java.util.HashMap;
012 import java.util.Iterator;
013 import java.util.LinkedList;
014 import java.util.Locale;
015 import java.util.Map;
016 import java.util.Stack;
017
018 import javax.xml.parsers.ParserConfigurationException;
019 import javax.xml.parsers.SAXParser;
020 import javax.xml.parsers.SAXParserFactory;
021 import javax.xml.transform.stream.StreamSource;
022 import javax.xml.validation.Schema;
023 import javax.xml.validation.SchemaFactory;
024 import javax.xml.validation.ValidatorHandler;
025
026 import org.openstreetmap.josm.io.MirroredInputStream;
027 import org.xml.sax.Attributes;
028 import org.xml.sax.ContentHandler;
029 import org.xml.sax.InputSource;
030 import org.xml.sax.Locator;
031 import org.xml.sax.SAXException;
032 import org.xml.sax.SAXParseException;
033 import org.xml.sax.XMLReader;
034 import org.xml.sax.helpers.DefaultHandler;
035 import org.xml.sax.helpers.XMLFilterImpl;
036
037 /**
038 * An helper class that reads from a XML stream into specific objects.
039 *
040 * @author Imi
041 */
042 public class XmlObjectParser implements Iterable<Object> {
043 public static class PresetParsingException extends SAXException {
044 private int columnNumber;
045 private int lineNumber;
046
047 public PresetParsingException() {
048 super();
049 }
050
051 public PresetParsingException(Exception e) {
052 super(e);
053 }
054
055 public PresetParsingException(String message, Exception e) {
056 super(message, e);
057 }
058
059 public PresetParsingException(String message) {
060 super(message);
061 }
062
063 public PresetParsingException rememberLocation(Locator locator) {
064 if (locator == null) return this;
065 this.columnNumber = locator.getColumnNumber();
066 this.lineNumber = locator.getLineNumber();
067 return this;
068 }
069
070 @Override
071 public String getMessage() {
072 String msg = super.getMessage();
073 if (lineNumber == 0 && columnNumber == 0)
074 return msg;
075 if (msg == null) {
076 msg = getClass().getName();
077 }
078 msg = msg + " " + tr("(at line {0}, column {1})", lineNumber, columnNumber);
079 return msg;
080 }
081
082 public int getColumnNumber() {
083 return columnNumber;
084 }
085
086 public int getLineNumber() {
087 return lineNumber;
088 }
089 }
090
091 public static final String lang = LanguageInfo.getLanguageCodeXML();
092
093 private static class AddNamespaceFilter extends XMLFilterImpl {
094
095 private final String namespace;
096
097 public AddNamespaceFilter(String namespace) {
098 this.namespace = namespace;
099 }
100
101 @Override
102 public void startElement (String uri, String localName, String qName, Attributes atts) throws SAXException {
103 if ("".equals(uri)) {
104 super.startElement(namespace, localName, qName, atts);
105 } else {
106 super.startElement(uri, localName, qName, atts);
107 }
108
109 }
110
111 }
112
113 private class Parser extends DefaultHandler {
114 Stack<Object> current = new Stack<Object>();
115 StringBuilder characters = new StringBuilder(64);
116
117 private Locator locator;
118
119 @Override
120 public void setDocumentLocator(Locator locator) {
121 this.locator = locator;
122 }
123
124 protected void throwException(Exception e) throws PresetParsingException{
125 throw new PresetParsingException(e).rememberLocation(locator);
126 }
127
128 @Override public void startElement(String ns, String lname, String qname, Attributes a) throws SAXException {
129 if (mapping.containsKey(qname)) {
130 Class<?> klass = mapping.get(qname).klass;
131 try {
132 current.push(klass.newInstance());
133 } catch (Exception e) {
134 throwException(e);
135 }
136 for (int i = 0; i < a.getLength(); ++i) {
137 setValue(mapping.get(qname), a.getQName(i), a.getValue(i));
138 }
139 if (mapping.get(qname).onStart) {
140 report();
141 }
142 if (mapping.get(qname).both) {
143 queue.add(current.peek());
144 }
145 }
146 }
147 @Override public void endElement(String ns, String lname, String qname) throws SAXException {
148 if (mapping.containsKey(qname) && !mapping.get(qname).onStart) {
149 report();
150 } else if (mapping.containsKey(qname) && characters != null && !current.isEmpty()) {
151 setValue(mapping.get(qname), qname, characters.toString().trim());
152 characters = new StringBuilder(64);
153 }
154 }
155 @Override public void characters(char[] ch, int start, int length) {
156 characters.append(ch, start, length);
157 }
158
159 private void report() {
160 queue.add(current.pop());
161 characters = new StringBuilder(64);
162 }
163
164 private Object getValueForClass(Class<?> klass, String value) {
165 if (klass == Boolean.TYPE)
166 return parseBoolean(value);
167 else if (klass == Integer.TYPE || klass == Long.TYPE)
168 return Long.parseLong(value);
169 else if (klass == Float.TYPE || klass == Double.TYPE)
170 return Double.parseDouble(value);
171 return value;
172 }
173
174 private void setValue(Entry entry, String fieldName, String value) throws SAXException {
175 if (entry == null) {
176 throw new NullPointerException("entry cannot be null");
177 }
178 if (fieldName.equals("class") || fieldName.equals("default") || fieldName.equals("throw") || fieldName.equals("new") || fieldName.equals("null")) {
179 fieldName += "_";
180 }
181 try {
182 Object c = current.peek();
183 Field f = entry.getField(fieldName);
184 if (f == null && fieldName.startsWith(lang)) {
185 f = entry.getField("locale_" + fieldName.substring(lang.length()));
186 }
187 if (f != null && Modifier.isPublic(f.getModifiers()) && String.class.equals(f.getType())) {
188 f.set(c, getValueForClass(f.getType(), value));
189 } else {
190 if (fieldName.startsWith(lang)) {
191 int l = lang.length();
192 fieldName = "set" + fieldName.substring(l, l + 1).toUpperCase(Locale.ENGLISH) + fieldName.substring(l + 1);
193 } else {
194 fieldName = "set" + fieldName.substring(0, 1).toUpperCase(Locale.ENGLISH) + fieldName.substring(1);
195 }
196 Method m = entry.getMethod(fieldName);
197 if (m != null) {
198 m.invoke(c, new Object[]{getValueForClass(m.getParameterTypes()[0], value)});
199 }
200 }
201 } catch (Exception e) {
202 e.printStackTrace(); // SAXException does not dump inner exceptions.
203 throwException(e);
204 }
205 }
206
207 private boolean parseBoolean(String s) {
208 return s != null
209 && !s.equals("0")
210 && !s.startsWith("off")
211 && !s.startsWith("false")
212 && !s.startsWith("no");
213 }
214
215 @Override
216 public void error(SAXParseException e) throws SAXException {
217 throwException(e);
218 }
219
220 @Override
221 public void fatalError(SAXParseException e) throws SAXException {
222 throwException(e);
223 }
224 }
225
226 private static class Entry {
227 Class<?> klass;
228 boolean onStart;
229 boolean both;
230 private final Map<String, Field> fields = new HashMap<String, Field>();
231 private final Map<String, Method> methods = new HashMap<String, Method>();
232
233 public Entry(Class<?> klass, boolean onStart, boolean both) {
234 this.klass = klass;
235 this.onStart = onStart;
236 this.both = both;
237 }
238
239 Field getField(String s) {
240 if (fields.containsKey(s)) {
241 return fields.get(s);
242 } else {
243 try {
244 Field f = klass.getField(s);
245 fields.put(s, f);
246 return f;
247 } catch (NoSuchFieldException ex) {
248 fields.put(s, null);
249 return null;
250 }
251 }
252 }
253
254 Method getMethod(String s) {
255 if (methods.containsKey(s)) {
256 return methods.get(s);
257 } else {
258 for (Method m : klass.getMethods()) {
259 if (m.getName().equals(s) && m.getParameterTypes().length == 1) {
260 methods.put(s, m);
261 return m;
262 }
263 }
264 methods.put(s, null);
265 return null;
266 }
267 }
268 }
269
270 private Map<String, Entry> mapping = new HashMap<String, Entry>();
271 private DefaultHandler parser;
272
273 /**
274 * The queue of already parsed items from the parsing thread.
275 */
276 private LinkedList<Object> queue = new LinkedList<Object>();
277 private Iterator<Object> queueIterator = null;
278
279 public XmlObjectParser() {
280 parser = new Parser();
281 }
282
283 public XmlObjectParser(DefaultHandler handler) {
284 parser = handler;
285 }
286
287 private Iterable<Object> start(final Reader in, final ContentHandler contentHandler) throws SAXException, IOException {
288 try {
289 SAXParserFactory parserFactory = SAXParserFactory.newInstance();
290 parserFactory.setNamespaceAware(true);
291 SAXParser saxParser = parserFactory.newSAXParser();
292 XMLReader reader = saxParser.getXMLReader();
293 reader.setContentHandler(contentHandler);
294 reader.parse(new InputSource(in));
295 queueIterator = queue.iterator();
296 return this;
297 } catch (ParserConfigurationException e) {
298 // This should never happen ;-)
299 throw new RuntimeException(e);
300 }
301 }
302
303 public Iterable<Object> start(final Reader in) throws SAXException {
304 try {
305 return start(in, parser);
306 } catch (IOException e) {
307 throw new SAXException(e);
308 }
309 }
310
311 public Iterable<Object> startWithValidation(final Reader in, String namespace, String schemaSource) throws SAXException {
312 try {
313 SchemaFactory factory = SchemaFactory.newInstance("http://www.w3.org/2001/XMLSchema");
314 Schema schema = factory.newSchema(new StreamSource(new MirroredInputStream(schemaSource)));
315 ValidatorHandler validator = schema.newValidatorHandler();
316 validator.setContentHandler(parser);
317 validator.setErrorHandler(parser);
318
319 AddNamespaceFilter filter = new AddNamespaceFilter(namespace);
320 filter.setContentHandler(validator);
321 return start(in, filter);
322 } catch(IOException e) {
323 throw new SAXException(tr("Failed to load XML schema."), e);
324 }
325 }
326
327 public void map(String tagName, Class<?> klass) {
328 mapping.put(tagName, new Entry(klass,false,false));
329 }
330
331 public void mapOnStart(String tagName, Class<?> klass) {
332 mapping.put(tagName, new Entry(klass,true,false));
333 }
334
335 public void mapBoth(String tagName, Class<?> klass) {
336 mapping.put(tagName, new Entry(klass,false,true));
337 }
338
339 public Object next() {
340 return queueIterator.next();
341 }
342
343 public boolean hasNext() {
344 return queueIterator.hasNext();
345 }
346
347 @Override
348 public Iterator<Object> iterator() {
349 return queue.iterator();
350 }
351 }