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