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    }