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