001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.data.preferences; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.io.BufferedReader; 007import java.io.File; 008import java.io.IOException; 009import java.io.InputStream; 010import java.io.Reader; 011import java.nio.charset.StandardCharsets; 012import java.nio.file.Files; 013import java.util.ArrayList; 014import java.util.Collections; 015import java.util.LinkedHashMap; 016import java.util.List; 017import java.util.Map; 018import java.util.SortedMap; 019import java.util.TreeMap; 020 021import javax.xml.XMLConstants; 022import javax.xml.stream.XMLInputFactory; 023import javax.xml.stream.XMLStreamConstants; 024import javax.xml.stream.XMLStreamException; 025import javax.xml.stream.XMLStreamReader; 026import javax.xml.transform.stream.StreamSource; 027import javax.xml.validation.Schema; 028import javax.xml.validation.SchemaFactory; 029import javax.xml.validation.Validator; 030 031import org.openstreetmap.josm.Main; 032import org.openstreetmap.josm.io.CachedFile; 033import org.openstreetmap.josm.io.XmlStreamParsingException; 034import org.xml.sax.SAXException; 035 036/** 037 * Loads preferences from XML. 038 */ 039public class PreferencesReader { 040 041 private final SortedMap<String, Setting<?>> settings = new TreeMap<>(); 042 private XMLStreamReader parser; 043 private int version; 044 private final Reader reader; 045 private final File file; 046 047 private final boolean defaults; 048 049 /** 050 * Constructs a new {@code PreferencesReader}. 051 * @param file the file 052 * @param defaults true when reading from the cache file for default preferences, 053 * false for the regular preferences config file 054 * @throws IOException if any I/O error occurs 055 * @throws XMLStreamException if any XML stream error occurs 056 */ 057 public PreferencesReader(File file, boolean defaults) throws IOException, XMLStreamException { 058 this.defaults = defaults; 059 this.reader = null; 060 this.file = file; 061 } 062 063 /** 064 * Constructs a new {@code PreferencesReader}. 065 * @param reader the {@link Reader} 066 * @param defaults true when reading from the cache file for default preferences, 067 * false for the regular preferences config file 068 * @throws XMLStreamException if any XML stream error occurs 069 */ 070 public PreferencesReader(Reader reader, boolean defaults) throws XMLStreamException { 071 this.defaults = defaults; 072 this.reader = reader; 073 this.file = null; 074 } 075 076 /** 077 * Validate the XML. 078 * @param f the file 079 * @throws IOException if any I/O error occurs 080 * @throws SAXException if any SAX error occurs 081 */ 082 public static void validateXML(File f) throws IOException, SAXException { 083 try (BufferedReader in = Files.newBufferedReader(f.toPath(), StandardCharsets.UTF_8)) { 084 validateXML(in); 085 } 086 } 087 088 /** 089 * Validate the XML. 090 * @param in the {@link Reader} 091 * @throws IOException if any I/O error occurs 092 * @throws SAXException if any SAX error occurs 093 */ 094 public static void validateXML(Reader in) throws IOException, SAXException { 095 try (CachedFile cf = new CachedFile("resource://data/preferences.xsd"); InputStream xsdStream = cf.getInputStream()) { 096 Schema schema = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI).newSchema(new StreamSource(xsdStream)); 097 Validator validator = schema.newValidator(); 098 validator.validate(new StreamSource(in)); 099 } 100 } 101 102 /** 103 * Return the parsed preferences as a settings map 104 * @return the parsed preferences as a settings map 105 */ 106 public SortedMap<String, Setting<?>> getSettings() { 107 return settings; 108 } 109 110 /** 111 * Return the version from the XML root element. 112 * (Represents the JOSM version when the file was written.) 113 * @return the version 114 */ 115 public int getVersion() { 116 return version; 117 } 118 119 /** 120 * Parse preferences. 121 * @throws XMLStreamException if any XML parsing error occurs 122 * @throws IOException if any I/O error occurs 123 */ 124 public void parse() throws XMLStreamException, IOException { 125 if (reader != null) { 126 this.parser = XMLInputFactory.newInstance().createXMLStreamReader(reader); 127 doParse(); 128 } else { 129 try (BufferedReader in = Files.newBufferedReader(file.toPath(), StandardCharsets.UTF_8)) { 130 this.parser = XMLInputFactory.newInstance().createXMLStreamReader(in); 131 doParse(); 132 } 133 } 134 } 135 136 private void doParse() throws XMLStreamException { 137 int event = parser.getEventType(); 138 while (true) { 139 if (event == XMLStreamConstants.START_ELEMENT) { 140 String topLevelElementName = defaults ? "preferences-defaults" : "preferences"; 141 String localName = parser.getLocalName(); 142 if (!topLevelElementName.equals(localName)) { 143 throw new XMLStreamException( 144 tr("Expected element ''{0}'', but got ''{1}''", topLevelElementName, localName), 145 parser.getLocation()); 146 } 147 try { 148 version = Integer.parseInt(parser.getAttributeValue(null, "version")); 149 } catch (NumberFormatException e) { 150 if (Main.isDebugEnabled()) { 151 Main.debug(e.getMessage()); 152 } 153 } 154 parseRoot(); 155 } else if (event == XMLStreamConstants.END_ELEMENT) { 156 return; 157 } 158 if (parser.hasNext()) { 159 event = parser.next(); 160 } else { 161 break; 162 } 163 } 164 parser.close(); 165 } 166 167 private void parseRoot() throws XMLStreamException { 168 while (true) { 169 int event = parser.next(); 170 if (event == XMLStreamConstants.START_ELEMENT) { 171 String localName = parser.getLocalName(); 172 switch(localName) { 173 case "tag": 174 StringSetting setting; 175 if (defaults && isNil()) { 176 setting = new StringSetting(null); 177 } else { 178 String value = parser.getAttributeValue(null, "value"); 179 if (value == null) { 180 throw new XMLStreamException(tr("value expected"), parser.getLocation()); 181 } 182 setting = new StringSetting(value); 183 } 184 if (defaults) { 185 setting.setTime(Math.round(Double.parseDouble(parser.getAttributeValue(null, "time")))); 186 } 187 settings.put(parser.getAttributeValue(null, "key"), setting); 188 jumpToEnd(); 189 break; 190 case "list": 191 case "lists": 192 case "maps": 193 parseToplevelList(); 194 break; 195 default: 196 throwException("Unexpected element: "+localName); 197 } 198 } else if (event == XMLStreamConstants.END_ELEMENT) { 199 return; 200 } 201 } 202 } 203 204 private void jumpToEnd() throws XMLStreamException { 205 while (true) { 206 int event = parser.next(); 207 if (event == XMLStreamConstants.START_ELEMENT) { 208 jumpToEnd(); 209 } else if (event == XMLStreamConstants.END_ELEMENT) { 210 return; 211 } 212 } 213 } 214 215 private void parseToplevelList() throws XMLStreamException { 216 String key = parser.getAttributeValue(null, "key"); 217 Long time = null; 218 if (defaults) { 219 time = Math.round(Double.parseDouble(parser.getAttributeValue(null, "time"))); 220 } 221 String name = parser.getLocalName(); 222 223 List<String> entries = null; 224 List<List<String>> lists = null; 225 List<Map<String, String>> maps = null; 226 if (defaults && isNil()) { 227 Setting<?> setting; 228 switch (name) { 229 case "lists": 230 setting = new ListListSetting(null); 231 break; 232 case "maps": 233 setting = new MapListSetting(null); 234 break; 235 default: 236 setting = new ListSetting(null); 237 break; 238 } 239 setting.setTime(time); 240 settings.put(key, setting); 241 jumpToEnd(); 242 } else { 243 while (true) { 244 int event = parser.next(); 245 if (event == XMLStreamConstants.START_ELEMENT) { 246 String localName = parser.getLocalName(); 247 switch(localName) { 248 case "entry": 249 if (entries == null) { 250 entries = new ArrayList<>(); 251 } 252 entries.add(parser.getAttributeValue(null, "value")); 253 jumpToEnd(); 254 break; 255 case "list": 256 if (lists == null) { 257 lists = new ArrayList<>(); 258 } 259 lists.add(parseInnerList()); 260 break; 261 case "map": 262 if (maps == null) { 263 maps = new ArrayList<>(); 264 } 265 maps.add(parseMap()); 266 break; 267 default: 268 throwException("Unexpected element: "+localName); 269 } 270 } else if (event == XMLStreamConstants.END_ELEMENT) { 271 break; 272 } 273 } 274 Setting<?> setting; 275 if (entries != null) { 276 setting = new ListSetting(Collections.unmodifiableList(entries)); 277 } else if (lists != null) { 278 setting = new ListListSetting(Collections.unmodifiableList(lists)); 279 } else if (maps != null) { 280 setting = new MapListSetting(Collections.unmodifiableList(maps)); 281 } else { 282 switch (name) { 283 case "lists": 284 setting = new ListListSetting(Collections.<List<String>>emptyList()); 285 break; 286 case "maps": 287 setting = new MapListSetting(Collections.<Map<String, String>>emptyList()); 288 break; 289 default: 290 setting = new ListSetting(Collections.<String>emptyList()); 291 break; 292 } 293 } 294 if (defaults) { 295 setting.setTime(time); 296 } 297 settings.put(key, setting); 298 } 299 } 300 301 private List<String> parseInnerList() throws XMLStreamException { 302 List<String> entries = new ArrayList<>(); 303 while (true) { 304 int event = parser.next(); 305 if (event == XMLStreamConstants.START_ELEMENT) { 306 if ("entry".equals(parser.getLocalName())) { 307 entries.add(parser.getAttributeValue(null, "value")); 308 jumpToEnd(); 309 } else { 310 throwException("Unexpected element: "+parser.getLocalName()); 311 } 312 } else if (event == XMLStreamConstants.END_ELEMENT) { 313 break; 314 } 315 } 316 return Collections.unmodifiableList(entries); 317 } 318 319 private Map<String, String> parseMap() throws XMLStreamException { 320 Map<String, String> map = new LinkedHashMap<>(); 321 while (true) { 322 int event = parser.next(); 323 if (event == XMLStreamConstants.START_ELEMENT) { 324 if ("tag".equals(parser.getLocalName())) { 325 map.put(parser.getAttributeValue(null, "key"), parser.getAttributeValue(null, "value")); 326 jumpToEnd(); 327 } else { 328 throwException("Unexpected element: "+parser.getLocalName()); 329 } 330 } else if (event == XMLStreamConstants.END_ELEMENT) { 331 break; 332 } 333 } 334 return Collections.unmodifiableMap(map); 335 } 336 337 /** 338 * Check if the current element is nil (meaning the value of the setting is null). 339 * @return true, if the current element is nil 340 * @see <a href="https://msdn.microsoft.com/en-us/library/2b314yt2(v=vs.85).aspx">Nillable Attribute on MS Developer Network</a> 341 */ 342 private boolean isNil() { 343 String nil = parser.getAttributeValue(XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI, "nil"); 344 return "true".equals(nil) || "1".equals(nil); 345 } 346 347 /** 348 * Throw XmlStreamParsingException with line and column number. 349 * 350 * Only use this for errors that should not be possible after schema validation. 351 * @param msg the error message 352 * @throws XmlStreamParsingException always 353 */ 354 private void throwException(String msg) throws XmlStreamParsingException { 355 throw new XmlStreamParsingException(msg, parser.getLocation()); 356 } 357}