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.InvocationTargetException; 011import java.lang.reflect.Method; 012import java.lang.reflect.Modifier; 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.Stack; 020 021import javax.xml.XMLConstants; 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.Main; 029import org.openstreetmap.josm.io.CachedFile; 030import org.xml.sax.Attributes; 031import org.xml.sax.ContentHandler; 032import org.xml.sax.InputSource; 033import org.xml.sax.Locator; 034import org.xml.sax.SAXException; 035import org.xml.sax.SAXParseException; 036import org.xml.sax.XMLReader; 037import org.xml.sax.helpers.DefaultHandler; 038import org.xml.sax.helpers.XMLFilterImpl; 039 040/** 041 * An helper class that reads from a XML stream into specific objects. 042 * 043 * @author Imi 044 */ 045public class XmlObjectParser implements Iterable<Object> { 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 if (fieldName.startsWith(lang)) { 149 int l = lang.length(); 150 fieldName = "set" + fieldName.substring(l, l + 1).toUpperCase(Locale.ENGLISH) + fieldName.substring(l + 1); 151 } else { 152 fieldName = "set" + fieldName.substring(0, 1).toUpperCase(Locale.ENGLISH) + fieldName.substring(1); 153 } 154 Method m = entry.getMethod(fieldName); 155 if (m != null) { 156 m.invoke(c, new Object[]{getValueForClass(m.getParameterTypes()[0], value)}); 157 } 158 } 159 } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { 160 Main.error(e); // SAXException does not dump inner exceptions. 161 throwException(e); 162 } 163 } 164 165 private boolean parseBoolean(String s) { 166 return s != null 167 && !"0".equals(s) 168 && !s.startsWith("off") 169 && !s.startsWith("false") 170 && !s.startsWith("no"); 171 } 172 173 @Override 174 public void error(SAXParseException e) throws SAXException { 175 throwException(e); 176 } 177 178 @Override 179 public void fatalError(SAXParseException e) throws SAXException { 180 throwException(e); 181 } 182 } 183 184 private static class Entry { 185 private final Class<?> klass; 186 private final boolean onStart; 187 private final boolean both; 188 private final Map<String, Field> fields = new HashMap<>(); 189 private final Map<String, Method> methods = new HashMap<>(); 190 191 Entry(Class<?> klass, boolean onStart, boolean both) { 192 this.klass = klass; 193 this.onStart = onStart; 194 this.both = both; 195 } 196 197 Field getField(String s) { 198 if (fields.containsKey(s)) { 199 return fields.get(s); 200 } else { 201 try { 202 Field f = klass.getField(s); 203 fields.put(s, f); 204 return f; 205 } catch (NoSuchFieldException ex) { 206 fields.put(s, null); 207 return null; 208 } 209 } 210 } 211 212 Method getMethod(String s) { 213 if (methods.containsKey(s)) { 214 return methods.get(s); 215 } else { 216 for (Method m : klass.getMethods()) { 217 if (m.getName().equals(s) && m.getParameterTypes().length == 1) { 218 methods.put(s, m); 219 return m; 220 } 221 } 222 methods.put(s, null); 223 return null; 224 } 225 } 226 } 227 228 private final Map<String, Entry> mapping = new HashMap<>(); 229 private final DefaultHandler parser; 230 231 /** 232 * The queue of already parsed items from the parsing thread. 233 */ 234 private final List<Object> queue = new LinkedList<>(); 235 private Iterator<Object> queueIterator; 236 237 /** 238 * Constructs a new {@code XmlObjectParser}. 239 */ 240 public XmlObjectParser() { 241 parser = new Parser(); 242 } 243 244 private Iterable<Object> start(final Reader in, final ContentHandler contentHandler) throws SAXException, IOException { 245 try { 246 XMLReader reader = Utils.newSafeSAXParser().getXMLReader(); 247 reader.setContentHandler(contentHandler); 248 try { 249 // Do not load external DTDs (fix #8191) 250 reader.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); 251 } catch (SAXException e) { 252 // Exception very unlikely to happen, so no need to translate this 253 Main.error("Cannot disable 'load-external-dtd' feature: "+e.getMessage()); 254 } 255 reader.parse(new InputSource(in)); 256 queueIterator = queue.iterator(); 257 return this; 258 } catch (ParserConfigurationException e) { 259 // This should never happen ;-) 260 throw new RuntimeException(e); 261 } 262 } 263 264 /** 265 * Starts parsing from the given input reader, without validation. 266 * @param in The input reader 267 * @return iterable collection of objects 268 * @throws SAXException if any XML or I/O error occurs 269 */ 270 public Iterable<Object> start(final Reader in) throws SAXException { 271 try { 272 return start(in, parser); 273 } catch (IOException e) { 274 throw new SAXException(e); 275 } 276 } 277 278 /** 279 * Starts parsing from the given input reader, with XSD validation. 280 * @param in The input reader 281 * @param namespace default namespace 282 * @param schemaSource XSD schema 283 * @return iterable collection of objects 284 * @throws SAXException if any XML or I/O error occurs 285 */ 286 public Iterable<Object> startWithValidation(final Reader in, String namespace, String schemaSource) throws SAXException { 287 SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); 288 try (CachedFile cf = new CachedFile(schemaSource); InputStream mis = cf.getInputStream()) { 289 Schema schema = factory.newSchema(new StreamSource(mis)); 290 ValidatorHandler validator = schema.newValidatorHandler(); 291 validator.setContentHandler(parser); 292 validator.setErrorHandler(parser); 293 294 AddNamespaceFilter filter = new AddNamespaceFilter(namespace); 295 filter.setContentHandler(validator); 296 return start(in, filter); 297 } catch (IOException e) { 298 throw new SAXException(tr("Failed to load XML schema."), e); 299 } 300 } 301 302 public void map(String tagName, Class<?> klass) { 303 mapping.put(tagName, new Entry(klass, false, false)); 304 } 305 306 public void mapOnStart(String tagName, Class<?> klass) { 307 mapping.put(tagName, new Entry(klass, true, false)); 308 } 309 310 public void mapBoth(String tagName, Class<?> klass) { 311 mapping.put(tagName, new Entry(klass, false, true)); 312 } 313 314 public Object next() { 315 return queueIterator.next(); 316 } 317 318 public boolean hasNext() { 319 return queueIterator.hasNext(); 320 } 321 322 @Override 323 public Iterator<Object> iterator() { 324 return queue.iterator(); 325 } 326}