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