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