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.Arrays; 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.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.io.CachedFile; 028import org.xml.sax.Attributes; 029import org.xml.sax.ContentHandler; 030import org.xml.sax.InputSource; 031import org.xml.sax.Locator; 032import org.xml.sax.SAXException; 033import org.xml.sax.SAXParseException; 034import org.xml.sax.XMLReader; 035import org.xml.sax.helpers.DefaultHandler; 036import org.xml.sax.helpers.XMLFilterImpl; 037 038/** 039 * An helper class that reads from a XML stream into specific objects. 040 * 041 * @author Imi 042 */ 043public class XmlObjectParser implements Iterable<Object> { 044 /** 045 * The language prefix to use 046 */ 047 public static final String lang = LanguageInfo.getLanguageCodeXML(); 048 049 private static class AddNamespaceFilter extends XMLFilterImpl { 050 051 private final String namespace; 052 053 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 private class Parser extends DefaultHandler { 068 private final Stack<Object> current = new Stack<>(); 069 private StringBuilder characters = new StringBuilder(64); 070 071 private Locator locator; 072 073 @Override 074 public void setDocumentLocator(Locator locator) { 075 this.locator = locator; 076 } 077 078 protected void throwException(Exception e) throws XmlParsingException { 079 throw new XmlParsingException(e).rememberLocation(locator); 080 } 081 082 @Override 083 public void startElement(String ns, String lname, String qname, Attributes a) throws SAXException { 084 final Entry entry = mapping.get(qname); 085 if (entry != null) { 086 Class<?> klass = entry.klass; 087 try { 088 current.push(klass.getConstructor().newInstance()); 089 } catch (ReflectiveOperationException e) { 090 throwException(e); 091 } 092 for (int i = 0; i < a.getLength(); ++i) { 093 setValue(entry, a.getQName(i), a.getValue(i)); 094 } 095 if (entry.onStart) { 096 report(); 097 } 098 if (entry.both) { 099 queue.add(current.peek()); 100 } 101 } 102 } 103 104 @Override 105 public void endElement(String ns, String lname, String qname) throws SAXException { 106 final Entry entry = mapping.get(qname); 107 if (entry != null && !entry.onStart) { 108 report(); 109 } else if (entry != null && characters != null && !current.isEmpty()) { 110 setValue(entry, 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 (boolean.class.equals(klass)) 127 return parseBoolean(value); 128 else if (Integer.class.equals(klass)) 129 return Integer.valueOf(value); 130 else if (Long.class.equals(klass)) 131 return Long.valueOf(value); 132 else if (Float.class.equals(klass)) 133 return Float.valueOf(value); 134 else if (Double.class.equals(klass)) 135 return Double.valueOf(value); 136 return value; 137 } 138 139 private void setValue(Entry entry, String fieldName, String value) throws SAXException { 140 CheckParameterUtil.ensureParameterNotNull(entry, "entry"); 141 if ("class".equals(fieldName) || "default".equals(fieldName) || "throw".equals(fieldName) || 142 "new".equals(fieldName) || "null".equals(fieldName)) { 143 fieldName += '_'; 144 } 145 fieldName = fieldName.replace(':', '_'); 146 try { 147 Object c = current.peek(); 148 Field f = entry.getField(fieldName); 149 if (f == null && fieldName.startsWith(lang)) { 150 f = entry.getField("locale_" + fieldName.substring(lang.length())); 151 } 152 if (f != null && Modifier.isPublic(f.getModifiers()) && ( 153 String.class.equals(f.getType()) || boolean.class.equals(f.getType()) || 154 Float.class.equals(f.getType()) || Double.class.equals(f.getType()) || 155 Long.class.equals(f.getType()) || Integer.class.equals(f.getType()))) { 156 f.set(c, getValueForClass(f.getType(), value)); 157 } else { 158 String setter; 159 if (fieldName.startsWith(lang)) { 160 int l = lang.length(); 161 setter = "set" + fieldName.substring(l, l + 1).toUpperCase(Locale.ENGLISH) + fieldName.substring(l + 1); 162 } else { 163 setter = "set" + fieldName.substring(0, 1).toUpperCase(Locale.ENGLISH) + fieldName.substring(1); 164 } 165 Method m = entry.getMethod(setter); 166 if (m != null) { 167 m.invoke(c, getValueForClass(m.getParameterTypes()[0], value)); 168 } 169 } 170 } catch (ReflectiveOperationException | IllegalArgumentException e) { 171 Logging.error(e); // SAXException does not dump inner exceptions. 172 throwException(e); 173 } 174 } 175 176 private boolean parseBoolean(String s) { 177 return s != null 178 && !"0".equals(s) 179 && !s.startsWith("off") 180 && !s.startsWith("false") 181 && !s.startsWith("no"); 182 } 183 184 @Override 185 public void error(SAXParseException e) throws SAXException { 186 throwException(e); 187 } 188 189 @Override 190 public void fatalError(SAXParseException e) throws SAXException { 191 throwException(e); 192 } 193 } 194 195 private static class Entry { 196 private final Class<?> klass; 197 private final boolean onStart; 198 private final boolean both; 199 private final Map<String, Field> fields = new HashMap<>(); 200 private final Map<String, Method> methods = new HashMap<>(); 201 202 Entry(Class<?> klass, boolean onStart, boolean both) { 203 this.klass = klass; 204 this.onStart = onStart; 205 this.both = both; 206 } 207 208 Field getField(String s) { 209 return fields.computeIfAbsent(s, ignore -> Arrays.stream(klass.getFields()) 210 .filter(f -> f.getName().equals(s)) 211 .findFirst() 212 .orElse(null)); 213 } 214 215 Method getMethod(String s) { 216 return methods.computeIfAbsent(s, ignore -> Arrays.stream(klass.getMethods()) 217 .filter(m -> m.getName().equals(s) && m.getParameterTypes().length == 1) 218 .findFirst() 219 .orElse(null)); 220 } 221 } 222 223 private final Map<String, Entry> mapping = new HashMap<>(); 224 private final DefaultHandler parser; 225 226 /** 227 * The queue of already parsed items from the parsing thread. 228 */ 229 private final List<Object> queue = new LinkedList<>(); 230 private Iterator<Object> queueIterator; 231 232 /** 233 * Constructs a new {@code XmlObjectParser}. 234 */ 235 public XmlObjectParser() { 236 parser = new Parser(); 237 } 238 239 private Iterable<Object> start(final Reader in, final ContentHandler contentHandler) throws SAXException, IOException { 240 try { 241 XMLReader reader = XmlUtils.newSafeSAXParser().getXMLReader(); 242 reader.setContentHandler(contentHandler); 243 try { 244 // Do not load external DTDs (fix #8191) 245 reader.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); 246 } catch (SAXException e) { 247 // Exception very unlikely to happen, so no need to translate this 248 Logging.log(Logging.LEVEL_ERROR, "Cannot disable 'load-external-dtd' feature:", e); 249 } 250 reader.parse(new InputSource(in)); 251 queueIterator = queue.iterator(); 252 return this; 253 } catch (ParserConfigurationException e) { 254 throw new JosmRuntimeException(e); 255 } 256 } 257 258 /** 259 * Starts parsing from the given input reader, without validation. 260 * @param in The input reader 261 * @return iterable collection of objects 262 * @throws SAXException if any XML or I/O error occurs 263 */ 264 public Iterable<Object> start(final Reader in) throws SAXException { 265 try { 266 return start(in, parser); 267 } catch (IOException e) { 268 throw new SAXException(e); 269 } 270 } 271 272 /** 273 * Starts parsing from the given input reader, with XSD validation. 274 * @param in The input reader 275 * @param namespace default namespace 276 * @param schemaSource XSD schema 277 * @return iterable collection of objects 278 * @throws SAXException if any XML or I/O error occurs 279 */ 280 public Iterable<Object> startWithValidation(final Reader in, String namespace, String schemaSource) throws SAXException { 281 SchemaFactory factory = XmlUtils.newXmlSchemaFactory(); 282 try (CachedFile cf = new CachedFile(schemaSource); InputStream mis = cf.getInputStream()) { 283 Schema schema = factory.newSchema(new StreamSource(mis)); 284 ValidatorHandler validator = schema.newValidatorHandler(); 285 validator.setContentHandler(parser); 286 validator.setErrorHandler(parser); 287 288 AddNamespaceFilter filter = new AddNamespaceFilter(namespace); 289 filter.setContentHandler(validator); 290 return start(in, filter); 291 } catch (IOException e) { 292 throw new SAXException(tr("Failed to load XML schema."), e); 293 } 294 } 295 296 /** 297 * Add a new tag name to class type mapping 298 * @param tagName The tag name that should be converted to that class 299 * @param klass The class the XML elements should be converted to. 300 */ 301 public void map(String tagName, Class<?> klass) { 302 mapping.put(tagName, new Entry(klass, false, false)); 303 } 304 305 public void mapOnStart(String tagName, Class<?> klass) { 306 mapping.put(tagName, new Entry(klass, true, false)); 307 } 308 309 public void mapBoth(String tagName, Class<?> klass) { 310 mapping.put(tagName, new Entry(klass, false, true)); 311 } 312 313 /** 314 * Get the next element that was parsed 315 * @return The next object 316 */ 317 public Object next() { 318 return queueIterator.next(); 319 } 320 321 /** 322 * Check if there is a next parsed object available 323 * @return <code>true</code> if there is a next object 324 */ 325 public boolean hasNext() { 326 return queueIterator.hasNext(); 327 } 328 329 @Override 330 public Iterator<Object> iterator() { 331 return queue.iterator(); 332 } 333}