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

Last change on this file since 17037 was 16188, checked in by simon04, 4 years ago

see #18954, see #18864 - fix SonarQube issues

  • Property svn:eol-style set to native
File size: 11.6 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.Arrays;
13import java.util.HashMap;
14import java.util.Iterator;
15import java.util.LinkedList;
16import java.util.List;
17import java.util.Locale;
18import java.util.Map;
19import java.util.Optional;
20import java.util.Stack;
21
22import javax.xml.parsers.ParserConfigurationException;
23import javax.xml.transform.stream.StreamSource;
24import javax.xml.validation.Schema;
25import javax.xml.validation.SchemaFactory;
26import javax.xml.validation.ValidatorHandler;
27
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 private Locator locator;
72 private final StringParser primitiveParsers = new StringParser(StringParser.DEFAULT)
73 .registerParser(boolean.class, this::parseBoolean)
74 .registerParser(Boolean.class, this::parseBoolean);
75
76 @Override
77 public void setDocumentLocator(Locator locator) {
78 this.locator = locator;
79 }
80
81 protected void throwException(Exception e) throws XmlParsingException {
82 throw new XmlParsingException(e).rememberLocation(locator);
83 }
84
85 @Override
86 public void startElement(String ns, String lname, String qname, Attributes a) throws SAXException {
87 final Entry entry = mapping.get(qname);
88 if (entry != null) {
89 Class<?> klass = entry.klass;
90 try {
91 current.push(klass.getConstructor().newInstance());
92 } catch (ReflectiveOperationException e) {
93 throwException(e);
94 }
95 for (int i = 0; i < a.getLength(); ++i) {
96 setValue(entry, a.getQName(i), a.getValue(i));
97 }
98 if (entry.onStart) {
99 report();
100 }
101 if (entry.both) {
102 queue.add(current.peek());
103 }
104 }
105 }
106
107 @Override
108 public void endElement(String ns, String lname, String qname) throws SAXException {
109 final Entry entry = mapping.get(qname);
110 if (entry != null && !entry.onStart) {
111 report();
112 } else if (entry != null && characters != null && !current.isEmpty()) {
113 setValue(entry, qname, characters.toString().trim());
114 characters = new StringBuilder(64);
115 }
116 }
117
118 @Override
119 public void characters(char[] ch, int start, int length) {
120 characters.append(ch, start, length);
121 }
122
123 private void report() {
124 queue.add(current.pop());
125 characters = new StringBuilder(64);
126 }
127
128 private void setValue(Entry entry, String fieldName, String value0) throws SAXException {
129 final String value = value0 != null ? value0.intern() : null;
130 CheckParameterUtil.ensureParameterNotNull(entry, "entry");
131 if ("class".equals(fieldName) || "default".equals(fieldName) || "throw".equals(fieldName) ||
132 "new".equals(fieldName) || "null".equals(fieldName)) {
133 fieldName += '_';
134 }
135 fieldName = fieldName.replace(':', '_');
136 try {
137 Object c = current.peek();
138 Field f = entry.getField(fieldName);
139 if (f == null && fieldName.startsWith(lang)) {
140 f = entry.getField("locale_" + fieldName.substring(lang.length()));
141 }
142 Optional<?> parsed = Optional.ofNullable(f)
143 .filter(field -> Modifier.isPublic(field.getModifiers()))
144 .flatMap(field -> primitiveParsers.tryParse(field.getType(), value));
145 if (parsed.isPresent()) {
146 f.set(c, parsed.get());
147 } else {
148 String setter;
149 if (fieldName.startsWith(lang)) {
150 int l = lang.length();
151 setter = "set" + fieldName.substring(l, l + 1).toUpperCase(Locale.ENGLISH) + fieldName.substring(l + 1);
152 } else {
153 setter = "set" + fieldName.substring(0, 1).toUpperCase(Locale.ENGLISH) + fieldName.substring(1);
154 }
155 Method m = entry.getMethod(setter);
156 if (m != null) {
157 parsed = primitiveParsers.tryParse(m.getParameterTypes()[0], value);
158 m.invoke(c, parsed.isPresent() ? parsed.get() : value);
159 }
160 }
161 } catch (ReflectiveOperationException | IllegalArgumentException e) {
162 Logging.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 return fields.computeIfAbsent(s, ignore -> Arrays.stream(klass.getFields())
201 .filter(f -> f.getName().equals(s))
202 .findFirst()
203 .orElse(null));
204 }
205
206 Method getMethod(String s) {
207 return methods.computeIfAbsent(s, ignore -> Arrays.stream(klass.getMethods())
208 .filter(m -> m.getName().equals(s) && m.getParameterTypes().length == 1)
209 .findFirst()
210 .orElse(null));
211 }
212 }
213
214 private final Map<String, Entry> mapping = new HashMap<>();
215 private final DefaultHandler parser;
216
217 /**
218 * The queue of already parsed items from the parsing thread.
219 */
220 private final List<Object> queue = new LinkedList<>();
221 private Iterator<Object> queueIterator;
222
223 /**
224 * Constructs a new {@code XmlObjectParser}.
225 */
226 public XmlObjectParser() {
227 parser = new Parser();
228 }
229
230 private Iterable<Object> start(final Reader in, final ContentHandler contentHandler) throws SAXException, IOException {
231 try {
232 XMLReader reader = XmlUtils.newSafeSAXParser().getXMLReader();
233 reader.setContentHandler(contentHandler);
234 try {
235 // Do not load external DTDs (fix #8191)
236 reader.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
237 } catch (SAXException e) {
238 // Exception very unlikely to happen, so no need to translate this
239 Logging.log(Logging.LEVEL_ERROR, "Cannot disable 'load-external-dtd' feature:", e);
240 }
241 reader.parse(new InputSource(in));
242 queueIterator = queue.iterator();
243 return this;
244 } catch (ParserConfigurationException e) {
245 throw new JosmRuntimeException(e);
246 }
247 }
248
249 /**
250 * Starts parsing from the given input reader, without validation.
251 * @param in The input reader
252 * @return iterable collection of objects
253 * @throws SAXException if any XML or I/O error occurs
254 */
255 public Iterable<Object> start(final Reader in) throws SAXException {
256 try {
257 return start(in, parser);
258 } catch (IOException e) {
259 throw new SAXException(e);
260 }
261 }
262
263 /**
264 * Starts parsing from the given input reader, with XSD validation.
265 * @param in The input reader
266 * @param namespace default namespace
267 * @param schemaSource XSD schema
268 * @return iterable collection of objects
269 * @throws SAXException if any XML or I/O error occurs
270 */
271 public Iterable<Object> startWithValidation(final Reader in, String namespace, String schemaSource) throws SAXException {
272 SchemaFactory factory = XmlUtils.newXmlSchemaFactory();
273 try (CachedFile cf = new CachedFile(schemaSource); InputStream mis = cf.getInputStream()) {
274 Schema schema = factory.newSchema(new StreamSource(mis));
275 ValidatorHandler validator = schema.newValidatorHandler();
276 validator.setContentHandler(parser);
277 validator.setErrorHandler(parser);
278
279 AddNamespaceFilter filter = new AddNamespaceFilter(namespace);
280 filter.setContentHandler(validator);
281 return start(in, filter);
282 } catch (IOException e) {
283 throw new SAXException(tr("Failed to load XML schema."), e);
284 }
285 }
286
287 /**
288 * Add a new tag name to class type mapping
289 * @param tagName The tag name that should be converted to that class
290 * @param klass The class the XML elements should be converted to.
291 */
292 public void map(String tagName, Class<?> klass) {
293 mapping.put(tagName, new Entry(klass, false, false));
294 }
295
296 public void mapOnStart(String tagName, Class<?> klass) {
297 mapping.put(tagName, new Entry(klass, true, false));
298 }
299
300 public void mapBoth(String tagName, Class<?> klass) {
301 mapping.put(tagName, new Entry(klass, false, true));
302 }
303
304 /**
305 * Get the next element that was parsed
306 * @return The next object
307 */
308 public Object next() {
309 return queueIterator.next();
310 }
311
312 /**
313 * Check if there is a next parsed object available
314 * @return <code>true</code> if there is a next object
315 */
316 public boolean hasNext() {
317 return queueIterator.hasNext();
318 }
319
320 @Override
321 public Iterator<Object> iterator() {
322 return queue.iterator();
323 }
324}
Note: See TracBrowser for help on using the repository browser.