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.HashMap; 013import java.util.Iterator; 014import java.util.LinkedList; 015import java.util.List; 016import java.util.Locale; 017import java.util.Map; 018import java.util.Stack; 019 020import javax.xml.parsers.ParserConfigurationException; 021import javax.xml.transform.stream.StreamSource; 022import javax.xml.validation.Schema; 023import javax.xml.validation.SchemaFactory; 024import javax.xml.validation.ValidatorHandler; 025 026import org.openstreetmap.josm.io.CachedFile; 027import org.xml.sax.Attributes; 028import org.xml.sax.ContentHandler; 029import org.xml.sax.InputSource; 030import org.xml.sax.Locator; 031import org.xml.sax.SAXException; 032import org.xml.sax.SAXParseException; 033import org.xml.sax.XMLReader; 034import org.xml.sax.helpers.DefaultHandler; 035import org.xml.sax.helpers.XMLFilterImpl; 036 037/** 038 * An helper class that reads from a XML stream into specific objects. 039 * 040 * @author Imi 041 */ 042public class XmlObjectParser implements Iterable<Object> { 043 /** 044 * The language prefix to use 045 */ 046 public static final String lang = LanguageInfo.getLanguageCodeXML(); 047 048 private static class AddNamespaceFilter extends XMLFilterImpl { 049 050 private final String namespace; 051 052 AddNamespaceFilter(String namespace) { 053 this.namespace = namespace; 054 } 055 056 @Override 057 public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException { 058 if ("".equals(uri)) { 059 super.startElement(namespace, localName, qName, atts); 060 } else { 061 super.startElement(uri, localName, qName, atts); 062 } 063 } 064 } 065 066 private class Parser extends DefaultHandler { 067 private final Stack<Object> current = new Stack<>(); 068 private StringBuilder characters = new StringBuilder(64); 069 070 private Locator locator; 071 072 @Override 073 public void setDocumentLocator(Locator locator) { 074 this.locator = locator; 075 } 076 077 protected void throwException(Exception e) throws XmlParsingException { 078 throw new XmlParsingException(e).rememberLocation(locator); 079 } 080 081 @Override 082 public void startElement(String ns, String lname, String qname, Attributes a) throws SAXException { 083 if (mapping.containsKey(qname)) { 084 Class<?> klass = mapping.get(qname).klass; 085 try { 086 current.push(klass.getConstructor().newInstance()); 087 } catch (ReflectiveOperationException e) { 088 throwException(e); 089 } 090 for (int i = 0; i < a.getLength(); ++i) { 091 setValue(mapping.get(qname), a.getQName(i), a.getValue(i)); 092 } 093 if (mapping.get(qname).onStart) { 094 report(); 095 } 096 if (mapping.get(qname).both) { 097 queue.add(current.peek()); 098 } 099 } 100 } 101 102 @Override 103 public void endElement(String ns, String lname, String qname) throws SAXException { 104 if (mapping.containsKey(qname) && !mapping.get(qname).onStart) { 105 report(); 106 } else if (mapping.containsKey(qname) && characters != null && !current.isEmpty()) { 107 setValue(mapping.get(qname), qname, characters.toString().trim()); 108 characters = new StringBuilder(64); 109 } 110 } 111 112 @Override 113 public void characters(char[] ch, int start, int length) { 114 characters.append(ch, start, length); 115 } 116 117 private void report() { 118 queue.add(current.pop()); 119 characters = new StringBuilder(64); 120 } 121 122 private Object getValueForClass(Class<?> klass, String value) { 123 if (klass == Boolean.TYPE) 124 return parseBoolean(value); 125 else if (klass == Integer.TYPE || klass == Long.TYPE) 126 return Long.valueOf(value); 127 else if (klass == Float.TYPE || klass == Double.TYPE) 128 return Double.valueOf(value); 129 return value; 130 } 131 132 private void setValue(Entry entry, String fieldName, String value) throws SAXException { 133 CheckParameterUtil.ensureParameterNotNull(entry, "entry"); 134 if ("class".equals(fieldName) || "default".equals(fieldName) || "throw".equals(fieldName) || 135 "new".equals(fieldName) || "null".equals(fieldName)) { 136 fieldName += '_'; 137 } 138 try { 139 Object c = current.peek(); 140 Field f = entry.getField(fieldName); 141 if (f == null && fieldName.startsWith(lang)) { 142 f = entry.getField("locale_" + fieldName.substring(lang.length())); 143 } 144 if (f != null && Modifier.isPublic(f.getModifiers()) && ( 145 String.class.equals(f.getType()) || boolean.class.equals(f.getType()))) { 146 f.set(c, getValueForClass(f.getType(), value)); 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 m.invoke(c, getValueForClass(m.getParameterTypes()[0], value)); 158 } 159 } 160 } catch (ReflectiveOperationException | IllegalArgumentException e) { 161 Logging.error(e); // SAXException does not dump inner exceptions. 162 throwException(e); 163 } 164 } 165 166 private boolean parseBoolean(String s) { 167 return s != null 168 && !"0".equals(s) 169 && !s.startsWith("off") 170 && !s.startsWith("false") 171 && !s.startsWith("no"); 172 } 173 174 @Override 175 public void error(SAXParseException e) throws SAXException { 176 throwException(e); 177 } 178 179 @Override 180 public void fatalError(SAXParseException e) throws SAXException { 181 throwException(e); 182 } 183 } 184 185 private static class Entry { 186 private final Class<?> klass; 187 private final boolean onStart; 188 private final boolean both; 189 private final Map<String, Field> fields = new HashMap<>(); 190 private final Map<String, Method> methods = new HashMap<>(); 191 192 Entry(Class<?> klass, boolean onStart, boolean both) { 193 this.klass = klass; 194 this.onStart = onStart; 195 this.both = both; 196 } 197 198 Field getField(String s) { 199 if (fields.containsKey(s)) { 200 return fields.get(s); 201 } else { 202 try { 203 Field f = klass.getField(s); 204 fields.put(s, f); 205 return f; 206 } catch (NoSuchFieldException ex) { 207 Logging.trace(ex); 208 fields.put(s, null); 209 return null; 210 } 211 } 212 } 213 214 Method getMethod(String s) { 215 if (methods.containsKey(s)) { 216 return methods.get(s); 217 } else { 218 for (Method m : klass.getMethods()) { 219 if (m.getName().equals(s) && m.getParameterTypes().length == 1) { 220 methods.put(s, m); 221 return m; 222 } 223 } 224 methods.put(s, null); 225 return null; 226 } 227 } 228 } 229 230 private final Map<String, Entry> mapping = new HashMap<>(); 231 private final DefaultHandler parser; 232 233 /** 234 * The queue of already parsed items from the parsing thread. 235 */ 236 private final List<Object> queue = new LinkedList<>(); 237 private Iterator<Object> queueIterator; 238 239 /** 240 * Constructs a new {@code XmlObjectParser}. 241 */ 242 public XmlObjectParser() { 243 parser = new Parser(); 244 } 245 246 private Iterable<Object> start(final Reader in, final ContentHandler contentHandler) throws SAXException, IOException { 247 try { 248 XMLReader reader = XmlUtils.newSafeSAXParser().getXMLReader(); 249 reader.setContentHandler(contentHandler); 250 try { 251 // Do not load external DTDs (fix #8191) 252 reader.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); 253 } catch (SAXException e) { 254 // Exception very unlikely to happen, so no need to translate this 255 Logging.log(Logging.LEVEL_ERROR, "Cannot disable 'load-external-dtd' feature:", e); 256 } 257 reader.parse(new InputSource(in)); 258 queueIterator = queue.iterator(); 259 return this; 260 } catch (ParserConfigurationException e) { 261 throw new JosmRuntimeException(e); 262 } 263 } 264 265 /** 266 * Starts parsing from the given input reader, without validation. 267 * @param in The input reader 268 * @return iterable collection of objects 269 * @throws SAXException if any XML or I/O error occurs 270 */ 271 public Iterable<Object> start(final Reader in) throws SAXException { 272 try { 273 return start(in, parser); 274 } catch (IOException e) { 275 throw new SAXException(e); 276 } 277 } 278 279 /** 280 * Starts parsing from the given input reader, with XSD validation. 281 * @param in The input reader 282 * @param namespace default namespace 283 * @param schemaSource XSD schema 284 * @return iterable collection of objects 285 * @throws SAXException if any XML or I/O error occurs 286 */ 287 public Iterable<Object> startWithValidation(final Reader in, String namespace, String schemaSource) throws SAXException { 288 SchemaFactory factory = XmlUtils.newXmlSchemaFactory(); 289 try (CachedFile cf = new CachedFile(schemaSource); InputStream mis = cf.getInputStream()) { 290 Schema schema = factory.newSchema(new StreamSource(mis)); 291 ValidatorHandler validator = schema.newValidatorHandler(); 292 validator.setContentHandler(parser); 293 validator.setErrorHandler(parser); 294 295 AddNamespaceFilter filter = new AddNamespaceFilter(namespace); 296 filter.setContentHandler(validator); 297 return start(in, filter); 298 } catch (IOException e) { 299 throw new SAXException(tr("Failed to load XML schema."), e); 300 } 301 } 302 303 /** 304 * Add a new tag name to class type mapping 305 * @param tagName The tag name that should be converted to that class 306 * @param klass The class the XML elements should be converted to. 307 */ 308 public void map(String tagName, Class<?> klass) { 309 mapping.put(tagName, new Entry(klass, false, false)); 310 } 311 312 public void mapOnStart(String tagName, Class<?> klass) { 313 mapping.put(tagName, new Entry(klass, true, false)); 314 } 315 316 public void mapBoth(String tagName, Class<?> klass) { 317 mapping.put(tagName, new Entry(klass, false, true)); 318 } 319 320 /** 321 * Get the next element that was parsed 322 * @return The next object 323 */ 324 public Object next() { 325 return queueIterator.next(); 326 } 327 328 /** 329 * Check if there is a next parsed object available 330 * @return <code>true</code> if there is a next object 331 */ 332 public boolean hasNext() { 333 return queueIterator.hasNext(); 334 } 335 336 @Override 337 public Iterator<Object> iterator() { 338 return queue.iterator(); 339 } 340}