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.XMLConstants; 021import javax.xml.parsers.ParserConfigurationException; 022import javax.xml.parsers.SAXParser; 023import javax.xml.parsers.SAXParserFactory; 024import javax.xml.transform.stream.StreamSource; 025import javax.xml.validation.Schema; 026import javax.xml.validation.SchemaFactory; 027import javax.xml.validation.ValidatorHandler; 028 029import org.openstreetmap.josm.Main; 030import org.openstreetmap.josm.io.CachedFile; 031import org.xml.sax.Attributes; 032import org.xml.sax.ContentHandler; 033import org.xml.sax.InputSource; 034import org.xml.sax.Locator; 035import org.xml.sax.SAXException; 036import org.xml.sax.SAXParseException; 037import org.xml.sax.XMLReader; 038import org.xml.sax.helpers.DefaultHandler; 039import org.xml.sax.helpers.XMLFilterImpl; 040 041/** 042 * An helper class that reads from a XML stream into specific objects. 043 * 044 * @author Imi 045 */ 046public class XmlObjectParser implements Iterable<Object> { 047 public static final String lang = LanguageInfo.getLanguageCodeXML(); 048 049 private static class AddNamespaceFilter extends XMLFilterImpl { 050 051 private final String namespace; 052 053 public AddNamespaceFilter(String namespace) { 054 this.namespace = namespace; 055 } 056 057 @Override 058 public void startElement (String uri, String localName, String qName, Attributes atts) throws SAXException { 059 if ("".equals(uri)) { 060 super.startElement(namespace, localName, qName, atts); 061 } else { 062 super.startElement(uri, localName, qName, atts); 063 } 064 065 } 066 067 } 068 069 private class Parser extends DefaultHandler { 070 Stack<Object> current = new Stack<>(); 071 StringBuilder characters = new StringBuilder(64); 072 073 private Locator locator; 074 075 @Override 076 public void setDocumentLocator(Locator locator) { 077 this.locator = locator; 078 } 079 080 protected void throwException(Exception e) throws XmlParsingException { 081 throw new XmlParsingException(e).rememberLocation(locator); 082 } 083 084 @Override 085 public void startElement(String ns, String lname, String qname, Attributes a) throws SAXException { 086 if (mapping.containsKey(qname)) { 087 Class<?> klass = mapping.get(qname).klass; 088 try { 089 current.push(klass.newInstance()); 090 } catch (Exception e) { 091 throwException(e); 092 } 093 for (int i = 0; i < a.getLength(); ++i) { 094 setValue(mapping.get(qname), a.getQName(i), a.getValue(i)); 095 } 096 if (mapping.get(qname).onStart) { 097 report(); 098 } 099 if (mapping.get(qname).both) { 100 queue.add(current.peek()); 101 } 102 } 103 } 104 105 @Override 106 public void endElement(String ns, String lname, String qname) throws SAXException { 107 if (mapping.containsKey(qname) && !mapping.get(qname).onStart) { 108 report(); 109 } else if (mapping.containsKey(qname) && characters != null && !current.isEmpty()) { 110 setValue(mapping.get(qname), 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 (klass == Boolean.TYPE) 127 return parseBoolean(value); 128 else if (klass == Integer.TYPE || klass == Long.TYPE) 129 return Long.parseLong(value); 130 else if (klass == Float.TYPE || klass == Double.TYPE) 131 return Double.parseDouble(value); 132 return value; 133 } 134 135 private void setValue(Entry entry, String fieldName, String value) throws SAXException { 136 CheckParameterUtil.ensureParameterNotNull(entry, "entry"); 137 if ("class".equals(fieldName) || "default".equals(fieldName) || "throw".equals(fieldName) || "new".equals(fieldName) || "null".equals(fieldName)) { 138 fieldName += "_"; 139 } 140 try { 141 Object c = current.peek(); 142 Field f = entry.getField(fieldName); 143 if (f == null && fieldName.startsWith(lang)) { 144 f = entry.getField("locale_" + fieldName.substring(lang.length())); 145 } 146 if (f != null && Modifier.isPublic(f.getModifiers()) && ( 147 String.class.equals(f.getType()) || boolean.class.equals(f.getType()))) { 148 f.set(c, getValueForClass(f.getType(), value)); 149 } else { 150 if (fieldName.startsWith(lang)) { 151 int l = lang.length(); 152 fieldName = "set" + fieldName.substring(l, l + 1).toUpperCase(Locale.ENGLISH) + fieldName.substring(l + 1); 153 } else { 154 fieldName = "set" + fieldName.substring(0, 1).toUpperCase(Locale.ENGLISH) + fieldName.substring(1); 155 } 156 Method m = entry.getMethod(fieldName); 157 if (m != null) { 158 m.invoke(c, new Object[]{getValueForClass(m.getParameterTypes()[0], value)}); 159 } 160 } 161 } catch (Exception e) { 162 Main.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 Class<?> klass; 188 boolean onStart; 189 boolean both; 190 private final Map<String, Field> fields = new HashMap<>(); 191 private final Map<String, Method> methods = new HashMap<>(); 192 193 public 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 if (fields.containsKey(s)) { 201 return fields.get(s); 202 } else { 203 try { 204 Field f = klass.getField(s); 205 fields.put(s, f); 206 return f; 207 } catch (NoSuchFieldException 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 Map<String, Entry> mapping = new HashMap<>(); 231 private DefaultHandler parser; 232 233 /** 234 * The queue of already parsed items from the parsing thread. 235 */ 236 private List<Object> queue = new LinkedList<>(); 237 private Iterator<Object> queueIterator = null; 238 239 /** 240 * Constructs a new {@code XmlObjectParser}. 241 */ 242 public XmlObjectParser() { 243 parser = new Parser(); 244 } 245 246 public XmlObjectParser(DefaultHandler handler) { 247 parser = handler; 248 } 249 250 private Iterable<Object> start(final Reader in, final ContentHandler contentHandler) throws SAXException, IOException { 251 try { 252 SAXParserFactory parserFactory = SAXParserFactory.newInstance(); 253 parserFactory.setNamespaceAware(true); 254 SAXParser saxParser = parserFactory.newSAXParser(); 255 XMLReader reader = saxParser.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 Main.error("Cannot disable 'load-external-dtd' feature: "+e.getMessage()); 263 } 264 reader.parse(new InputSource(in)); 265 queueIterator = queue.iterator(); 266 return this; 267 } catch (ParserConfigurationException e) { 268 // This should never happen ;-) 269 throw new RuntimeException(e); 270 } 271 } 272 273 /** 274 * Starts parsing from the given input reader, without validation. 275 * @param in The input reader 276 * @return iterable collection of objects 277 * @throws SAXException if any XML or I/O error occurs 278 */ 279 public Iterable<Object> start(final Reader in) throws SAXException { 280 try { 281 return start(in, parser); 282 } catch (IOException e) { 283 throw new SAXException(e); 284 } 285 } 286 287 /** 288 * Starts parsing from the given input reader, with XSD validation. 289 * @param in The input reader 290 * @param namespace default namespace 291 * @param schemaSource XSD schema 292 * @return iterable collection of objects 293 * @throws SAXException if any XML or I/O error occurs 294 */ 295 public Iterable<Object> startWithValidation(final Reader in, String namespace, String schemaSource) throws SAXException { 296 SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); 297 try (InputStream mis = new CachedFile(schemaSource).getInputStream()) { 298 Schema schema = factory.newSchema(new StreamSource(mis)); 299 ValidatorHandler validator = schema.newValidatorHandler(); 300 validator.setContentHandler(parser); 301 validator.setErrorHandler(parser); 302 303 AddNamespaceFilter filter = new AddNamespaceFilter(namespace); 304 filter.setContentHandler(validator); 305 return start(in, filter); 306 } catch(IOException e) { 307 throw new SAXException(tr("Failed to load XML schema."), e); 308 } 309 } 310 311 public void map(String tagName, Class<?> klass) { 312 mapping.put(tagName, new Entry(klass,false,false)); 313 } 314 315 public void mapOnStart(String tagName, Class<?> klass) { 316 mapping.put(tagName, new Entry(klass,true,false)); 317 } 318 319 public void mapBoth(String tagName, Class<?> klass) { 320 mapping.put(tagName, new Entry(klass,false,true)); 321 } 322 323 public Object next() { 324 return queueIterator.next(); 325 } 326 327 public boolean hasNext() { 328 return queueIterator.hasNext(); 329 } 330 331 @Override 332 public Iterator<Object> iterator() { 333 return queue.iterator(); 334 } 335}