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