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