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.Optional; 020import java.util.Stack; 021 022import javax.xml.parsers.ParserConfigurationException; 023import javax.xml.transform.stream.StreamSource; 024import javax.xml.validation.Schema; 025import javax.xml.validation.SchemaFactory; 026import javax.xml.validation.ValidatorHandler; 027 028import org.openstreetmap.josm.io.CachedFile; 029import org.xml.sax.Attributes; 030import org.xml.sax.ContentHandler; 031import org.xml.sax.InputSource; 032import org.xml.sax.Locator; 033import org.xml.sax.SAXException; 034import org.xml.sax.SAXParseException; 035import org.xml.sax.XMLReader; 036import org.xml.sax.helpers.DefaultHandler; 037import org.xml.sax.helpers.XMLFilterImpl; 038 039/** 040 * An helper class that reads from a XML stream into specific objects. 041 * 042 * @author Imi 043 */ 044public class XmlObjectParser implements Iterable<Object> { 045 /** 046 * The language prefix to use 047 */ 048 public static final String lang = LanguageInfo.getLanguageCodeXML(); 049 050 private static class AddNamespaceFilter extends XMLFilterImpl { 051 052 private final String namespace; 053 054 AddNamespaceFilter(String namespace) { 055 this.namespace = namespace; 056 } 057 058 @Override 059 public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException { 060 if ("".equals(uri)) { 061 super.startElement(namespace, localName, qName, atts); 062 } else { 063 super.startElement(uri, localName, qName, atts); 064 } 065 } 066 } 067 068 private class Parser extends DefaultHandler { 069 private final Stack<Object> current = new Stack<>(); 070 private StringBuilder characters = new StringBuilder(64); 071 private Locator locator; 072 private final StringParser primitiveParsers = new StringParser(StringParser.DEFAULT) 073 .registerParser(boolean.class, this::parseBoolean) 074 .registerParser(Boolean.class, this::parseBoolean); 075 076 @Override 077 public void setDocumentLocator(Locator locator) { 078 this.locator = locator; 079 } 080 081 protected void throwException(Exception e) throws XmlParsingException { 082 throw new XmlParsingException(e).rememberLocation(locator); 083 } 084 085 @Override 086 public void startElement(String ns, String lname, String qname, Attributes a) throws SAXException { 087 final Entry entry = mapping.get(qname); 088 if (entry != null) { 089 Class<?> klass = entry.klass; 090 try { 091 current.push(klass.getConstructor().newInstance()); 092 } catch (ReflectiveOperationException e) { 093 throwException(e); 094 } 095 for (int i = 0; i < a.getLength(); ++i) { 096 setValue(entry, a.getQName(i), a.getValue(i)); 097 } 098 if (entry.onStart) { 099 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}