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.InputStream;
008import java.io.Reader;
009import java.lang.reflect.Field;
010import java.lang.reflect.Method;
011import java.lang.reflect.Modifier;
012import java.util.HashMap;
013import java.util.Iterator;
014import java.util.LinkedList;
015import java.util.List;
016import java.util.Locale;
017import java.util.Map;
018import java.util.Stack;
019
020import javax.xml.XMLConstants;
021import javax.xml.parsers.ParserConfigurationException;
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.CachedFile;
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 final String lang = LanguageInfo.getLanguageCodeXML();
046
047    private static class AddNamespaceFilter extends XMLFilterImpl {
048
049        private final String namespace;
050
051        AddNamespaceFilter(String namespace) {
052            this.namespace = namespace;
053        }
054
055        @Override
056        public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException {
057            if ("".equals(uri)) {
058                super.startElement(namespace, localName, qName, atts);
059            } else {
060                super.startElement(uri, localName, qName, atts);
061            }
062        }
063    }
064
065    private class Parser extends DefaultHandler {
066        private Stack<Object> current = new Stack<>();
067        private StringBuilder characters = new StringBuilder(64);
068
069        private Locator locator;
070
071        @Override
072        public void setDocumentLocator(Locator locator) {
073            this.locator = locator;
074        }
075
076        protected void throwException(Exception e) throws XmlParsingException {
077            throw new XmlParsingException(e).rememberLocation(locator);
078        }
079
080        @Override
081        public void startElement(String ns, String lname, String qname, Attributes a) throws SAXException {
082            if (mapping.containsKey(qname)) {
083                Class<?> klass = mapping.get(qname).klass;
084                try {
085                    current.push(klass.newInstance());
086                } catch (Exception e) {
087                    throwException(e);
088                }
089                for (int i = 0; i < a.getLength(); ++i) {
090                    setValue(mapping.get(qname), a.getQName(i), a.getValue(i));
091                }
092                if (mapping.get(qname).onStart) {
093                    report();
094                }
095                if (mapping.get(qname).both) {
096                    queue.add(current.peek());
097                }
098            }
099        }
100
101        @Override
102        public void endElement(String ns, String lname, String qname) throws SAXException {
103            if (mapping.containsKey(qname) && !mapping.get(qname).onStart) {
104                report();
105            } else if (mapping.containsKey(qname) && characters != null && !current.isEmpty()) {
106                setValue(mapping.get(qname), qname, characters.toString().trim());
107                characters  = new StringBuilder(64);
108            }
109        }
110
111        @Override
112        public void characters(char[] ch, int start, int length) {
113            characters.append(ch, start, length);
114        }
115
116        private void report() {
117            queue.add(current.pop());
118            characters  = new StringBuilder(64);
119        }
120
121        private Object getValueForClass(Class<?> klass, String value) {
122            if (klass == Boolean.TYPE)
123                return parseBoolean(value);
124            else if (klass == Integer.TYPE || klass == Long.TYPE)
125                return Long.valueOf(value);
126            else if (klass == Float.TYPE || klass == Double.TYPE)
127                return Double.valueOf(value);
128            return value;
129        }
130
131        private void setValue(Entry entry, String fieldName, String value) throws SAXException {
132            CheckParameterUtil.ensureParameterNotNull(entry, "entry");
133            if ("class".equals(fieldName) || "default".equals(fieldName) || "throw".equals(fieldName) ||
134                    "new".equals(fieldName) || "null".equals(fieldName)) {
135                fieldName += '_';
136            }
137            try {
138                Object c = current.peek();
139                Field f = entry.getField(fieldName);
140                if (f == null && fieldName.startsWith(lang)) {
141                    f = entry.getField("locale_" + fieldName.substring(lang.length()));
142                }
143                if (f != null && Modifier.isPublic(f.getModifiers()) && (
144                        String.class.equals(f.getType()) || boolean.class.equals(f.getType()))) {
145                    f.set(c, getValueForClass(f.getType(), value));
146                } else {
147                    if (fieldName.startsWith(lang)) {
148                        int l = lang.length();
149                        fieldName = "set" + fieldName.substring(l, l + 1).toUpperCase(Locale.ENGLISH) + fieldName.substring(l + 1);
150                    } else {
151                        fieldName = "set" + fieldName.substring(0, 1).toUpperCase(Locale.ENGLISH) + fieldName.substring(1);
152                    }
153                    Method m = entry.getMethod(fieldName);
154                    if (m != null) {
155                        m.invoke(c, new Object[]{getValueForClass(m.getParameterTypes()[0], value)});
156                    }
157                }
158            } catch (Exception e) {
159                Main.error(e); // SAXException does not dump inner exceptions.
160                throwException(e);
161            }
162        }
163
164        private boolean parseBoolean(String s) {
165            return s != null
166                    && !"0".equals(s)
167                    && !s.startsWith("off")
168                    && !s.startsWith("false")
169                    && !s.startsWith("no");
170        }
171
172        @Override
173        public void error(SAXParseException e) throws SAXException {
174            throwException(e);
175        }
176
177        @Override
178        public void fatalError(SAXParseException e) throws SAXException {
179            throwException(e);
180        }
181    }
182
183    private static class Entry {
184        private Class<?> klass;
185        private boolean onStart;
186        private boolean both;
187        private final Map<String, Field> fields = new HashMap<>();
188        private final Map<String, Method> methods = new HashMap<>();
189
190        Entry(Class<?> klass, boolean onStart, boolean both) {
191            this.klass = klass;
192            this.onStart = onStart;
193            this.both = both;
194        }
195
196        Field getField(String s) {
197            if (fields.containsKey(s)) {
198                return fields.get(s);
199            } else {
200                try {
201                    Field f = klass.getField(s);
202                    fields.put(s, f);
203                    return f;
204                } catch (NoSuchFieldException ex) {
205                    fields.put(s, null);
206                    return null;
207                }
208            }
209        }
210
211        Method getMethod(String s) {
212            if (methods.containsKey(s)) {
213                return methods.get(s);
214            } else {
215                for (Method m : klass.getMethods()) {
216                    if (m.getName().equals(s) && m.getParameterTypes().length == 1) {
217                        methods.put(s, m);
218                        return m;
219                    }
220                }
221                methods.put(s, null);
222                return null;
223            }
224        }
225    }
226
227    private Map<String, Entry> mapping = new HashMap<>();
228    private DefaultHandler parser;
229
230    /**
231     * The queue of already parsed items from the parsing thread.
232     */
233    private List<Object> queue = new LinkedList<>();
234    private Iterator<Object> queueIterator;
235
236    /**
237     * Constructs a new {@code XmlObjectParser}.
238     */
239    public XmlObjectParser() {
240        parser = new Parser();
241    }
242
243    public XmlObjectParser(DefaultHandler handler) {
244        parser = handler;
245    }
246
247    private Iterable<Object> start(final Reader in, final ContentHandler contentHandler) throws SAXException, IOException {
248        try {
249            XMLReader reader = Utils.newSafeSAXParser().getXMLReader();
250            reader.setContentHandler(contentHandler);
251            try {
252                // Do not load external DTDs (fix #8191)
253                reader.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
254            } catch (SAXException e) {
255                // Exception very unlikely to happen, so no need to translate this
256                Main.error("Cannot disable 'load-external-dtd' feature: "+e.getMessage());
257            }
258            reader.parse(new InputSource(in));
259            queueIterator = queue.iterator();
260            return this;
261        } catch (ParserConfigurationException e) {
262            // This should never happen ;-)
263            throw new RuntimeException(e);
264        }
265    }
266
267    /**
268     * Starts parsing from the given input reader, without validation.
269     * @param in The input reader
270     * @return iterable collection of objects
271     * @throws SAXException if any XML or I/O error occurs
272     */
273    public Iterable<Object> start(final Reader in) throws SAXException {
274        try {
275            return start(in, parser);
276        } catch (IOException e) {
277            throw new SAXException(e);
278        }
279    }
280
281    /**
282     * Starts parsing from the given input reader, with XSD validation.
283     * @param in The input reader
284     * @param namespace default namespace
285     * @param schemaSource XSD schema
286     * @return iterable collection of objects
287     * @throws SAXException if any XML or I/O error occurs
288     */
289    public Iterable<Object> startWithValidation(final Reader in, String namespace, String schemaSource) throws SAXException {
290        SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
291        try (InputStream mis = new CachedFile(schemaSource).getInputStream()) {
292            Schema schema = factory.newSchema(new StreamSource(mis));
293            ValidatorHandler validator = schema.newValidatorHandler();
294            validator.setContentHandler(parser);
295            validator.setErrorHandler(parser);
296
297            AddNamespaceFilter filter = new AddNamespaceFilter(namespace);
298            filter.setContentHandler(validator);
299            return start(in, filter);
300        } catch (IOException e) {
301            throw new SAXException(tr("Failed to load XML schema."), e);
302        }
303    }
304
305    public void map(String tagName, Class<?> klass) {
306        mapping.put(tagName, new Entry(klass, false, false));
307    }
308
309    public void mapOnStart(String tagName, Class<?> klass) {
310        mapping.put(tagName, new Entry(klass, true, false));
311    }
312
313    public void mapBoth(String tagName, Class<?> klass) {
314        mapping.put(tagName, new Entry(klass, false, true));
315    }
316
317    public Object next() {
318        return queueIterator.next();
319    }
320
321    public boolean hasNext() {
322        return queueIterator.hasNext();
323    }
324
325    @Override
326    public Iterator<Object> iterator() {
327        return queue.iterator();
328    }
329}