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

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

XmlObjectParser: use Map.computeIfAbsent, Stream

  • 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.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.Stack;
20
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.io.CachedFile;
28import org.xml.sax.Attributes;
29import org.xml.sax.ContentHandler;
30import org.xml.sax.InputSource;
31import org.xml.sax.Locator;
32import org.xml.sax.SAXException;
33import org.xml.sax.SAXParseException;
34import org.xml.sax.XMLReader;
35import org.xml.sax.helpers.DefaultHandler;
36import org.xml.sax.helpers.XMLFilterImpl;
37
38/**
39 * An helper class that reads from a XML stream into specific objects.
40 *
41 * @author Imi
42 */
43public class XmlObjectParser implements Iterable<Object> {
44 /**
45 * The language prefix to use
46 */
47 public static final String lang = LanguageInfo.getLanguageCodeXML();
48
49 private static class AddNamespaceFilter extends XMLFilterImpl {
50
51 private final String namespace;
52
53 AddNamespaceFilter(String namespace) {
54 this.namespace = namespace;
55 }
56
57 @Override
58 public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException {
59 if ("".equals(uri)) {
60 super.startElement(namespace, localName, qName, atts);
61 } else {
62 super.startElement(uri, localName, qName, atts);
63 }
64 }
65 }
66
67 private class Parser extends DefaultHandler {
68 private final Stack<Object> current = new Stack<>();
69 private StringBuilder characters = new StringBuilder(64);
70
71 private Locator locator;
72
73 @Override
74 public void setDocumentLocator(Locator locator) {
75 this.locator = locator;
76 }
77
78 protected void throwException(Exception e) throws XmlParsingException {
79 throw new XmlParsingException(e).rememberLocation(locator);
80 }
81
82 @Override
83 public void startElement(String ns, String lname, String qname, Attributes a) throws SAXException {
84 final Entry entry = mapping.get(qname);
85 if (entry != null) {
86 Class<?> klass = entry.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(entry, a.getQName(i), a.getValue(i));
94 }
95 if (entry.onStart) {
96 report();
97 }
98 if (entry.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 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}
Note: See TracBrowser for help on using the repository browser.