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.Arrays;
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.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            final Entry entry = mapping.get(qname);
085            if (entry != null) {
086                Class<?> klass = entry.klass;
087                try {
088                    current.push(klass.getConstructor().newInstance());
089                } catch (ReflectiveOperationException e) {
090                    throwException(e);
091                }
092                for (int i = 0; i < a.getLength(); ++i) {
093                    setValue(entry, a.getQName(i), a.getValue(i));
094                }
095                if (entry.onStart) {
096                    report();
097                }
098                if (entry.both) {
099                    queue.add(current.peek());
100                }
101            }
102        }
103
104        @Override
105        public void endElement(String ns, String lname, String qname) throws SAXException {
106            final Entry entry = mapping.get(qname);
107            if (entry != null && !entry.onStart) {
108                report();
109            } else if (entry != null && characters != null && !current.isEmpty()) {
110                setValue(entry, 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 (boolean.class.equals(klass))
127                return parseBoolean(value);
128            else if (Integer.class.equals(klass))
129                return Integer.valueOf(value);
130            else if (Long.class.equals(klass))
131                return Long.valueOf(value);
132            else if (Float.class.equals(klass))
133                return Float.valueOf(value);
134            else if (Double.class.equals(klass))
135                return Double.valueOf(value);
136            return value;
137        }
138
139        private void setValue(Entry entry, String fieldName, String value) throws SAXException {
140            CheckParameterUtil.ensureParameterNotNull(entry, "entry");
141            if ("class".equals(fieldName) || "default".equals(fieldName) || "throw".equals(fieldName) ||
142                    "new".equals(fieldName) || "null".equals(fieldName)) {
143                fieldName += '_';
144            }
145            fieldName = fieldName.replace(':', '_');
146            try {
147                Object c = current.peek();
148                Field f = entry.getField(fieldName);
149                if (f == null && fieldName.startsWith(lang)) {
150                    f = entry.getField("locale_" + fieldName.substring(lang.length()));
151                }
152                if (f != null && Modifier.isPublic(f.getModifiers()) && (
153                        String.class.equals(f.getType()) || boolean.class.equals(f.getType()) ||
154                        Float.class.equals(f.getType()) || Double.class.equals(f.getType()) ||
155                        Long.class.equals(f.getType()) || Integer.class.equals(f.getType()))) {
156                    f.set(c, getValueForClass(f.getType(), value));
157                } else {
158                    String setter;
159                    if (fieldName.startsWith(lang)) {
160                        int l = lang.length();
161                        setter = "set" + fieldName.substring(l, l + 1).toUpperCase(Locale.ENGLISH) + fieldName.substring(l + 1);
162                    } else {
163                        setter = "set" + fieldName.substring(0, 1).toUpperCase(Locale.ENGLISH) + fieldName.substring(1);
164                    }
165                    Method m = entry.getMethod(setter);
166                    if (m != null) {
167                        m.invoke(c, getValueForClass(m.getParameterTypes()[0], value));
168                    }
169                }
170            } catch (ReflectiveOperationException | IllegalArgumentException e) {
171                Logging.error(e); // SAXException does not dump inner exceptions.
172                throwException(e);
173            }
174        }
175
176        private boolean parseBoolean(String s) {
177            return s != null
178                    && !"0".equals(s)
179                    && !s.startsWith("off")
180                    && !s.startsWith("false")
181                    && !s.startsWith("no");
182        }
183
184        @Override
185        public void error(SAXParseException e) throws SAXException {
186            throwException(e);
187        }
188
189        @Override
190        public void fatalError(SAXParseException e) throws SAXException {
191            throwException(e);
192        }
193    }
194
195    private static class Entry {
196        private final Class<?> klass;
197        private final boolean onStart;
198        private final boolean both;
199        private final Map<String, Field> fields = new HashMap<>();
200        private final Map<String, Method> methods = new HashMap<>();
201
202        Entry(Class<?> klass, boolean onStart, boolean both) {
203            this.klass = klass;
204            this.onStart = onStart;
205            this.both = both;
206        }
207
208        Field getField(String s) {
209            return fields.computeIfAbsent(s, ignore -> Arrays.stream(klass.getFields())
210                    .filter(f -> f.getName().equals(s))
211                    .findFirst()
212                    .orElse(null));
213        }
214
215        Method getMethod(String s) {
216            return methods.computeIfAbsent(s, ignore -> Arrays.stream(klass.getMethods())
217                    .filter(m -> m.getName().equals(s) && m.getParameterTypes().length == 1)
218                    .findFirst()
219                    .orElse(null));
220        }
221    }
222
223    private final Map<String, Entry> mapping = new HashMap<>();
224    private final DefaultHandler parser;
225
226    /**
227     * The queue of already parsed items from the parsing thread.
228     */
229    private final List<Object> queue = new LinkedList<>();
230    private Iterator<Object> queueIterator;
231
232    /**
233     * Constructs a new {@code XmlObjectParser}.
234     */
235    public XmlObjectParser() {
236        parser = new Parser();
237    }
238
239    private Iterable<Object> start(final Reader in, final ContentHandler contentHandler) throws SAXException, IOException {
240        try {
241            XMLReader reader = XmlUtils.newSafeSAXParser().getXMLReader();
242            reader.setContentHandler(contentHandler);
243            try {
244                // Do not load external DTDs (fix #8191)
245                reader.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
246            } catch (SAXException e) {
247                // Exception very unlikely to happen, so no need to translate this
248                Logging.log(Logging.LEVEL_ERROR, "Cannot disable 'load-external-dtd' feature:", e);
249            }
250            reader.parse(new InputSource(in));
251            queueIterator = queue.iterator();
252            return this;
253        } catch (ParserConfigurationException e) {
254            throw new JosmRuntimeException(e);
255        }
256    }
257
258    /**
259     * Starts parsing from the given input reader, without validation.
260     * @param in The input reader
261     * @return iterable collection of objects
262     * @throws SAXException if any XML or I/O error occurs
263     */
264    public Iterable<Object> start(final Reader in) throws SAXException {
265        try {
266            return start(in, parser);
267        } catch (IOException e) {
268            throw new SAXException(e);
269        }
270    }
271
272    /**
273     * Starts parsing from the given input reader, with XSD validation.
274     * @param in The input reader
275     * @param namespace default namespace
276     * @param schemaSource XSD schema
277     * @return iterable collection of objects
278     * @throws SAXException if any XML or I/O error occurs
279     */
280    public Iterable<Object> startWithValidation(final Reader in, String namespace, String schemaSource) throws SAXException {
281        SchemaFactory factory = XmlUtils.newXmlSchemaFactory();
282        try (CachedFile cf = new CachedFile(schemaSource); InputStream mis = cf.getInputStream()) {
283            Schema schema = factory.newSchema(new StreamSource(mis));
284            ValidatorHandler validator = schema.newValidatorHandler();
285            validator.setContentHandler(parser);
286            validator.setErrorHandler(parser);
287
288            AddNamespaceFilter filter = new AddNamespaceFilter(namespace);
289            filter.setContentHandler(validator);
290            return start(in, filter);
291        } catch (IOException e) {
292            throw new SAXException(tr("Failed to load XML schema."), e);
293        }
294    }
295
296    /**
297     * Add a new tag name to class type mapping
298     * @param tagName The tag name that should be converted to that class
299     * @param klass The class the XML elements should be converted to.
300     */
301    public void map(String tagName, Class<?> klass) {
302        mapping.put(tagName, new Entry(klass, false, false));
303    }
304
305    public void mapOnStart(String tagName, Class<?> klass) {
306        mapping.put(tagName, new Entry(klass, true, false));
307    }
308
309    public void mapBoth(String tagName, Class<?> klass) {
310        mapping.put(tagName, new Entry(klass, false, true));
311    }
312
313    /**
314     * Get the next element that was parsed
315     * @return The next object
316     */
317    public Object next() {
318        return queueIterator.next();
319    }
320
321    /**
322     * Check if there is a next parsed object available
323     * @return <code>true</code> if there is a next object
324     */
325    public boolean hasNext() {
326        return queueIterator.hasNext();
327    }
328
329    @Override
330    public Iterator<Object> iterator() {
331        return queue.iterator();
332    }
333}