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