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 (boolean.class.equals(klass)) 124 return parseBoolean(value); 125 else if (Integer.class.equals(klass)) 126 return Integer.valueOf(value); 127 else if (Long.class.equals(klass)) 128 return Long.valueOf(value); 129 else if (Float.class.equals(klass)) 130 return Float.valueOf(value); 131 else if (Double.class.equals(klass)) 132 return Double.valueOf(value); 133 return value; 134 } 135 136 private void setValue(Entry entry, String fieldName, String value) throws SAXException { 137 CheckParameterUtil.ensureParameterNotNull(entry, "entry"); 138 if ("class".equals(fieldName) || "default".equals(fieldName) || "throw".equals(fieldName) || 139 "new".equals(fieldName) || "null".equals(fieldName)) { 140 fieldName += '_'; 141 } 142 fieldName = fieldName.replace(':', '_'); 143 try { 144 Object c = current.peek(); 145 Field f = entry.getField(fieldName); 146 if (f == null && fieldName.startsWith(lang)) { 147 f = entry.getField("locale_" + fieldName.substring(lang.length())); 148 } 149 if (f != null && Modifier.isPublic(f.getModifiers()) && ( 150 String.class.equals(f.getType()) || boolean.class.equals(f.getType()) || 151 Float.class.equals(f.getType()) || Double.class.equals(f.getType()) || 152 Long.class.equals(f.getType()) || Integer.class.equals(f.getType()))) { 153 f.set(c, getValueForClass(f.getType(), value)); 154 } else { 155 String setter; 156 if (fieldName.startsWith(lang)) { 157 int l = lang.length(); 158 setter = "set" + fieldName.substring(l, l + 1).toUpperCase(Locale.ENGLISH) + fieldName.substring(l + 1); 159 } else { 160 setter = "set" + fieldName.substring(0, 1).toUpperCase(Locale.ENGLISH) + fieldName.substring(1); 161 } 162 Method m = entry.getMethod(setter); 163 if (m != null) { 164 m.invoke(c, getValueForClass(m.getParameterTypes()[0], value)); 165 } 166 } 167 } catch (ReflectiveOperationException | IllegalArgumentException e) { 168 Logging.error(e); // SAXException does not dump inner exceptions. 169 throwException(e); 170 } 171 } 172 173 private boolean parseBoolean(String s) { 174 return s != null 175 && !"0".equals(s) 176 && !s.startsWith("off") 177 && !s.startsWith("false") 178 && !s.startsWith("no"); 179 } 180 181 @Override 182 public void error(SAXParseException e) throws SAXException { 183 throwException(e); 184 } 185 186 @Override 187 public void fatalError(SAXParseException e) throws SAXException { 188 throwException(e); 189 } 190 } 191 192 private static class Entry { 193 private final Class<?> klass; 194 private final boolean onStart; 195 private final boolean both; 196 private final Map<String, Field> fields = new HashMap<>(); 197 private final Map<String, Method> methods = new HashMap<>(); 198 199 Entry(Class<?> klass, boolean onStart, boolean both) { 200 this.klass = klass; 201 this.onStart = onStart; 202 this.both = both; 203 } 204 205 Field getField(String s) { 206 if (fields.containsKey(s)) { 207 return fields.get(s); 208 } else { 209 try { 210 Field f = klass.getField(s); 211 fields.put(s, f); 212 return f; 213 } catch (NoSuchFieldException ex) { 214 Logging.trace(ex); 215 fields.put(s, null); 216 return null; 217 } 218 } 219 } 220 221 Method getMethod(String s) { 222 if (methods.containsKey(s)) { 223 return methods.get(s); 224 } else { 225 for (Method m : klass.getMethods()) { 226 if (m.getName().equals(s) && m.getParameterTypes().length == 1) { 227 methods.put(s, m); 228 return m; 229 } 230 } 231 methods.put(s, null); 232 return null; 233 } 234 } 235 } 236 237 private final Map<String, Entry> mapping = new HashMap<>(); 238 private final DefaultHandler parser; 239 240 /** 241 * The queue of already parsed items from the parsing thread. 242 */ 243 private final List<Object> queue = new LinkedList<>(); 244 private Iterator<Object> queueIterator; 245 246 /** 247 * Constructs a new {@code XmlObjectParser}. 248 */ 249 public XmlObjectParser() { 250 parser = new Parser(); 251 } 252 253 private Iterable<Object> start(final Reader in, final ContentHandler contentHandler) throws SAXException, IOException { 254 try { 255 XMLReader reader = XmlUtils.newSafeSAXParser().getXMLReader(); 256 reader.setContentHandler(contentHandler); 257 try { 258 // Do not load external DTDs (fix #8191) 259 reader.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); 260 } catch (SAXException e) { 261 // Exception very unlikely to happen, so no need to translate this 262 Logging.log(Logging.LEVEL_ERROR, "Cannot disable 'load-external-dtd' feature:", e); 263 } 264 reader.parse(new InputSource(in)); 265 queueIterator = queue.iterator(); 266 return this; 267 } catch (ParserConfigurationException e) { 268 throw new JosmRuntimeException(e); 269 } 270 } 271 272 /** 273 * Starts parsing from the given input reader, without validation. 274 * @param in The input reader 275 * @return iterable collection of objects 276 * @throws SAXException if any XML or I/O error occurs 277 */ 278 public Iterable<Object> start(final Reader in) throws SAXException { 279 try { 280 return start(in, parser); 281 } catch (IOException e) { 282 throw new SAXException(e); 283 } 284 } 285 286 /** 287 * Starts parsing from the given input reader, with XSD validation. 288 * @param in The input reader 289 * @param namespace default namespace 290 * @param schemaSource XSD schema 291 * @return iterable collection of objects 292 * @throws SAXException if any XML or I/O error occurs 293 */ 294 public Iterable<Object> startWithValidation(final Reader in, String namespace, String schemaSource) throws SAXException { 295 SchemaFactory factory = XmlUtils.newXmlSchemaFactory(); 296 try (CachedFile cf = new CachedFile(schemaSource); InputStream mis = cf.getInputStream()) { 297 Schema schema = factory.newSchema(new StreamSource(mis)); 298 ValidatorHandler validator = schema.newValidatorHandler(); 299 validator.setContentHandler(parser); 300 validator.setErrorHandler(parser); 301 302 AddNamespaceFilter filter = new AddNamespaceFilter(namespace); 303 filter.setContentHandler(validator); 304 return start(in, filter); 305 } catch (IOException e) { 306 throw new SAXException(tr("Failed to load XML schema."), e); 307 } 308 } 309 310 /** 311 * Add a new tag name to class type mapping 312 * @param tagName The tag name that should be converted to that class 313 * @param klass The class the XML elements should be converted to. 314 */ 315 public void map(String tagName, Class<?> klass) { 316 mapping.put(tagName, new Entry(klass, false, false)); 317 } 318 319 public void mapOnStart(String tagName, Class<?> klass) { 320 mapping.put(tagName, new Entry(klass, true, false)); 321 } 322 323 public void mapBoth(String tagName, Class<?> klass) { 324 mapping.put(tagName, new Entry(klass, false, true)); 325 } 326 327 /** 328 * Get the next element that was parsed 329 * @return The next object 330 */ 331 public Object next() { 332 return queueIterator.next(); 333 } 334 335 /** 336 * Check if there is a next parsed object available 337 * @return <code>true</code> if there is a next object 338 */ 339 public boolean hasNext() { 340 return queueIterator.hasNext(); 341 } 342 343 @Override 344 public Iterator<Object> iterator() { 345 return queue.iterator(); 346 } 347}