source: josm/trunk/src/org/openstreetmap/josm/tools/XmlObjectParser.java@ 12519

Last change on this file since 12519 was 12382, checked in by michael2402, 7 years ago

More documentation for the tools package

  • Property svn:eol-style set to native
File size: 11.9 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.tools;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.io.IOException;
7import java.io.InputStream;
8import java.io.Reader;
9import java.lang.reflect.Field;
10import java.lang.reflect.Method;
11import java.lang.reflect.Modifier;
12import java.util.HashMap;
13import java.util.Iterator;
14import java.util.LinkedList;
15import java.util.List;
16import java.util.Locale;
17import java.util.Map;
18import java.util.Stack;
19
20import javax.xml.XMLConstants;
21import javax.xml.parsers.ParserConfigurationException;
22import javax.xml.transform.stream.StreamSource;
23import javax.xml.validation.Schema;
24import javax.xml.validation.SchemaFactory;
25import javax.xml.validation.ValidatorHandler;
26
27import org.openstreetmap.josm.Main;
28import org.openstreetmap.josm.io.CachedFile;
29import org.xml.sax.Attributes;
30import org.xml.sax.ContentHandler;
31import org.xml.sax.InputSource;
32import org.xml.sax.Locator;
33import org.xml.sax.SAXException;
34import org.xml.sax.SAXParseException;
35import org.xml.sax.XMLReader;
36import org.xml.sax.helpers.DefaultHandler;
37import org.xml.sax.helpers.XMLFilterImpl;
38
39/**
40 * An helper class that reads from a XML stream into specific objects.
41 *
42 * @author Imi
43 */
44public class XmlObjectParser implements Iterable<Object> {
45 /**
46 * The language prefix to use
47 */
48 public static final String lang = LanguageInfo.getLanguageCodeXML();
49
50 private static class AddNamespaceFilter extends XMLFilterImpl {
51
52 private final String namespace;
53
54 AddNamespaceFilter(String namespace) {
55 this.namespace = namespace;
56 }
57
58 @Override
59 public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException {
60 if ("".equals(uri)) {
61 super.startElement(namespace, localName, qName, atts);
62 } else {
63 super.startElement(uri, localName, qName, atts);
64 }
65 }
66 }
67
68 private class Parser extends DefaultHandler {
69 private final Stack<Object> current = new Stack<>();
70 private StringBuilder characters = new StringBuilder(64);
71
72 private Locator locator;
73
74 @Override
75 public void setDocumentLocator(Locator locator) {
76 this.locator = locator;
77 }
78
79 protected void throwException(Exception e) throws XmlParsingException {
80 throw new XmlParsingException(e).rememberLocation(locator);
81 }
82
83 @Override
84 public void startElement(String ns, String lname, String qname, Attributes a) throws SAXException {
85 if (mapping.containsKey(qname)) {
86 Class<?> klass = mapping.get(qname).klass;
87 try {
88 current.push(klass.getConstructor().newInstance());
89 } catch (ReflectiveOperationException e) {
90 throwException(e);
91 }
92 for (int i = 0; i < a.getLength(); ++i) {
93 setValue(mapping.get(qname), a.getQName(i), a.getValue(i));
94 }
95 if (mapping.get(qname).onStart) {
96 report();
97 }
98 if (mapping.get(qname).both) {
99 queue.add(current.peek());
100 }
101 }
102 }
103
104 @Override
105 public void endElement(String ns, String lname, String qname) throws SAXException {
106 if (mapping.containsKey(qname) && !mapping.get(qname).onStart) {
107 report();
108 } else if (mapping.containsKey(qname) && characters != null && !current.isEmpty()) {
109 setValue(mapping.get(qname), qname, characters.toString().trim());
110 characters = new StringBuilder(64);
111 }
112 }
113
114 @Override
115 public void characters(char[] ch, int start, int length) {
116 characters.append(ch, start, length);
117 }
118
119 private void report() {
120 queue.add(current.pop());
121 characters = new StringBuilder(64);
122 }
123
124 private Object getValueForClass(Class<?> klass, String value) {
125 if (klass == Boolean.TYPE)
126 return parseBoolean(value);
127 else if (klass == Integer.TYPE || klass == Long.TYPE)
128 return Long.valueOf(value);
129 else if (klass == Float.TYPE || klass == Double.TYPE)
130 return Double.valueOf(value);
131 return value;
132 }
133
134 private void setValue(Entry entry, String fieldName, String value) throws SAXException {
135 CheckParameterUtil.ensureParameterNotNull(entry, "entry");
136 if ("class".equals(fieldName) || "default".equals(fieldName) || "throw".equals(fieldName) ||
137 "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, getValueForClass(m.getParameterTypes()[0], value));
159 }
160 }
161 } catch (ReflectiveOperationException | IllegalArgumentException 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 private final Class<?> klass;
188 private final boolean onStart;
189 private final boolean both;
190 private final Map<String, Field> fields = new HashMap<>();
191 private final Map<String, Method> methods = new HashMap<>();
192
193 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 Main.trace(ex);
209 fields.put(s, null);
210 return null;
211 }
212 }
213 }
214
215 Method getMethod(String s) {
216 if (methods.containsKey(s)) {
217 return methods.get(s);
218 } else {
219 for (Method m : klass.getMethods()) {
220 if (m.getName().equals(s) && m.getParameterTypes().length == 1) {
221 methods.put(s, m);
222 return m;
223 }
224 }
225 methods.put(s, null);
226 return null;
227 }
228 }
229 }
230
231 private final Map<String, Entry> mapping = new HashMap<>();
232 private final DefaultHandler parser;
233
234 /**
235 * The queue of already parsed items from the parsing thread.
236 */
237 private final List<Object> queue = new LinkedList<>();
238 private Iterator<Object> queueIterator;
239
240 /**
241 * Constructs a new {@code XmlObjectParser}.
242 */
243 public XmlObjectParser() {
244 parser = new Parser();
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(e, "Cannot disable 'load-external-dtd' feature:");
257 }
258 reader.parse(new InputSource(in));
259 queueIterator = queue.iterator();
260 return this;
261 } catch (ParserConfigurationException e) {
262 throw new JosmRuntimeException(e);
263 }
264 }
265
266 /**
267 * Starts parsing from the given input reader, without validation.
268 * @param in The input reader
269 * @return iterable collection of objects
270 * @throws SAXException if any XML or I/O error occurs
271 */
272 public Iterable<Object> start(final Reader in) throws SAXException {
273 try {
274 return start(in, parser);
275 } catch (IOException e) {
276 throw new SAXException(e);
277 }
278 }
279
280 /**
281 * Starts parsing from the given input reader, with XSD validation.
282 * @param in The input reader
283 * @param namespace default namespace
284 * @param schemaSource XSD schema
285 * @return iterable collection of objects
286 * @throws SAXException if any XML or I/O error occurs
287 */
288 public Iterable<Object> startWithValidation(final Reader in, String namespace, String schemaSource) throws SAXException {
289 SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
290 try (CachedFile cf = new CachedFile(schemaSource); InputStream mis = cf.getInputStream()) {
291 Schema schema = factory.newSchema(new StreamSource(mis));
292 ValidatorHandler validator = schema.newValidatorHandler();
293 validator.setContentHandler(parser);
294 validator.setErrorHandler(parser);
295
296 AddNamespaceFilter filter = new AddNamespaceFilter(namespace);
297 filter.setContentHandler(validator);
298 return start(in, filter);
299 } catch (IOException e) {
300 throw new SAXException(tr("Failed to load XML schema."), e);
301 }
302 }
303
304 /**
305 * Add a new tag name to class type mapping
306 * @param tagName The tag name that should be converted to that class
307 * @param klass The class the XML elements should be converted to.
308 */
309 public void map(String tagName, Class<?> klass) {
310 mapping.put(tagName, new Entry(klass, false, false));
311 }
312
313 public void mapOnStart(String tagName, Class<?> klass) {
314 mapping.put(tagName, new Entry(klass, true, false));
315 }
316
317 public void mapBoth(String tagName, Class<?> klass) {
318 mapping.put(tagName, new Entry(klass, false, true));
319 }
320
321 /**
322 * Get the next element that was parsed
323 * @return The next object
324 */
325 public Object next() {
326 return queueIterator.next();
327 }
328
329 /**
330 * Check if there is a next parsed object available
331 * @return <code>true</code> if there is a next object
332 */
333 public boolean hasNext() {
334 return queueIterator.hasNext();
335 }
336
337 @Override
338 public Iterator<Object> iterator() {
339 return queue.iterator();
340 }
341}
Note: See TracBrowser for help on using the repository browser.