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