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