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.Optional; 019import java.util.SortedMap; 020import java.util.TreeMap; 021 022import javax.xml.XMLConstants; 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; 028 029import org.openstreetmap.josm.io.CachedFile; 030import org.openstreetmap.josm.io.XmlStreamParsingException; 031import org.openstreetmap.josm.spi.preferences.ListListSetting; 032import org.openstreetmap.josm.spi.preferences.ListSetting; 033import org.openstreetmap.josm.spi.preferences.MapListSetting; 034import org.openstreetmap.josm.spi.preferences.Setting; 035import org.openstreetmap.josm.spi.preferences.StringSetting; 036import org.openstreetmap.josm.tools.Logging; 037import org.openstreetmap.josm.tools.XmlUtils; 038import org.xml.sax.SAXException; 039 040/** 041 * Loads preferences from XML. 042 */ 043public class PreferencesReader { 044 045 private final SortedMap<String, Setting<?>> settings = new TreeMap<>(); 046 private XMLStreamReader parser; 047 private int version; 048 private final Reader reader; 049 private final File file; 050 051 private final boolean defaults; 052 053 /** 054 * Constructs a new {@code PreferencesReader}. 055 * @param file the file 056 * @param defaults true when reading from the cache file for default preferences, 057 * false for the regular preferences config file 058 */ 059 public PreferencesReader(File file, boolean defaults) { 060 this.defaults = defaults; 061 this.reader = null; 062 this.file = file; 063 } 064 065 /** 066 * Constructs a new {@code PreferencesReader}. 067 * @param reader the {@link Reader} 068 * @param defaults true when reading from the cache file for default preferences, 069 * false for the regular preferences config file 070 */ 071 public PreferencesReader(Reader reader, boolean defaults) { 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 = XmlUtils.newXmlSchemaFactory().newSchema(new StreamSource(xsdStream)); 098 XmlUtils.newSafeValidator(schema).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 = XmlUtils.newSafeXMLInputFactory().createXMLStreamReader(reader); 127 doParse(); 128 } else { 129 try (BufferedReader in = Files.newBufferedReader(file.toPath(), StandardCharsets.UTF_8)) { 130 this.parser = XmlUtils.newSafeXMLInputFactory().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 Logging.log(Logging.LEVEL_DEBUG, e); 151 } 152 parseRoot(); 153 } else if (event == XMLStreamConstants.END_ELEMENT) { 154 return; 155 } 156 if (parser.hasNext()) { 157 event = parser.next(); 158 } else { 159 break; 160 } 161 } 162 parser.close(); 163 } 164 165 private void parseRoot() throws XMLStreamException { 166 while (true) { 167 int event = parser.next(); 168 if (event == XMLStreamConstants.START_ELEMENT) { 169 String localName = parser.getLocalName(); 170 switch(localName) { 171 case "tag": 172 StringSetting setting; 173 if (defaults && isNil()) { 174 setting = new StringSetting(null); 175 } else { 176 setting = new StringSetting(Optional.ofNullable(parser.getAttributeValue(null, "value")) 177 .orElseThrow(() -> new XMLStreamException(tr("value expected"), parser.getLocation()))); 178 } 179 if (defaults) { 180 setting.setTime(Math.round(Double.parseDouble(parser.getAttributeValue(null, "time")))); 181 } 182 settings.put(parser.getAttributeValue(null, "key"), setting); 183 jumpToEnd(); 184 break; 185 case "list": 186 case "lists": 187 case "maps": 188 parseToplevelList(); 189 break; 190 default: 191 throwException("Unexpected element: "+localName); 192 } 193 } else if (event == XMLStreamConstants.END_ELEMENT) { 194 return; 195 } 196 } 197 } 198 199 private void jumpToEnd() throws XMLStreamException { 200 while (true) { 201 int event = parser.next(); 202 if (event == XMLStreamConstants.START_ELEMENT) { 203 jumpToEnd(); 204 } else if (event == XMLStreamConstants.END_ELEMENT) { 205 return; 206 } 207 } 208 } 209 210 private void parseToplevelList() throws XMLStreamException { 211 String key = parser.getAttributeValue(null, "key"); 212 Long time = null; 213 if (defaults) { 214 time = Math.round(Double.parseDouble(parser.getAttributeValue(null, "time"))); 215 } 216 String name = parser.getLocalName(); 217 218 List<String> entries = null; 219 List<List<String>> lists = null; 220 List<Map<String, String>> maps = null; 221 if (defaults && isNil()) { 222 Setting<?> setting; 223 switch (name) { 224 case "lists": 225 setting = new ListListSetting(null); 226 break; 227 case "maps": 228 setting = new MapListSetting(null); 229 break; 230 default: 231 setting = new ListSetting(null); 232 break; 233 } 234 setting.setTime(time); 235 settings.put(key, setting); 236 jumpToEnd(); 237 } else { 238 while (true) { 239 int event = parser.next(); 240 if (event == XMLStreamConstants.START_ELEMENT) { 241 String localName = parser.getLocalName(); 242 switch(localName) { 243 case "entry": 244 if (entries == null) { 245 entries = new ArrayList<>(); 246 } 247 entries.add(parser.getAttributeValue(null, "value")); 248 jumpToEnd(); 249 break; 250 case "list": 251 if (lists == null) { 252 lists = new ArrayList<>(); 253 } 254 lists.add(parseInnerList()); 255 break; 256 case "map": 257 if (maps == null) { 258 maps = new ArrayList<>(); 259 } 260 maps.add(parseMap()); 261 break; 262 default: 263 throwException("Unexpected element: "+localName); 264 } 265 } else if (event == XMLStreamConstants.END_ELEMENT) { 266 break; 267 } 268 } 269 Setting<?> setting; 270 if (entries != null) { 271 setting = new ListSetting(Collections.unmodifiableList(entries)); 272 } else if (lists != null) { 273 setting = new ListListSetting(Collections.unmodifiableList(lists)); 274 } else if (maps != null) { 275 setting = new MapListSetting(Collections.unmodifiableList(maps)); 276 } else { 277 switch (name) { 278 case "lists": 279 setting = new ListListSetting(Collections.<List<String>>emptyList()); 280 break; 281 case "maps": 282 setting = new MapListSetting(Collections.<Map<String, String>>emptyList()); 283 break; 284 default: 285 setting = new ListSetting(Collections.<String>emptyList()); 286 break; 287 } 288 } 289 if (defaults) { 290 setting.setTime(time); 291 } 292 settings.put(key, setting); 293 } 294 } 295 296 private List<String> parseInnerList() throws XMLStreamException { 297 List<String> entries = new ArrayList<>(); 298 while (true) { 299 int event = parser.next(); 300 if (event == XMLStreamConstants.START_ELEMENT) { 301 if ("entry".equals(parser.getLocalName())) { 302 entries.add(parser.getAttributeValue(null, "value")); 303 jumpToEnd(); 304 } else { 305 throwException("Unexpected element: "+parser.getLocalName()); 306 } 307 } else if (event == XMLStreamConstants.END_ELEMENT) { 308 break; 309 } 310 } 311 return Collections.unmodifiableList(entries); 312 } 313 314 private Map<String, String> parseMap() throws XMLStreamException { 315 Map<String, String> map = new LinkedHashMap<>(); 316 while (true) { 317 int event = parser.next(); 318 if (event == XMLStreamConstants.START_ELEMENT) { 319 if ("tag".equals(parser.getLocalName())) { 320 map.put(parser.getAttributeValue(null, "key"), parser.getAttributeValue(null, "value")); 321 jumpToEnd(); 322 } else { 323 throwException("Unexpected element: "+parser.getLocalName()); 324 } 325 } else if (event == XMLStreamConstants.END_ELEMENT) { 326 break; 327 } 328 } 329 return Collections.unmodifiableMap(map); 330 } 331 332 /** 333 * Check if the current element is nil (meaning the value of the setting is null). 334 * @return true, if the current element is nil 335 * @see <a href="https://msdn.microsoft.com/en-us/library/2b314yt2(v=vs.85).aspx">Nillable Attribute on MS Developer Network</a> 336 */ 337 private boolean isNil() { 338 String nil = parser.getAttributeValue(XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI, "nil"); 339 return "true".equals(nil) || "1".equals(nil); 340 } 341 342 /** 343 * Throw XmlStreamParsingException with line and column number. 344 * 345 * Only use this for errors that should not be possible after schema validation. 346 * @param msg the error message 347 * @throws XmlStreamParsingException always 348 */ 349 private void throwException(String msg) throws XmlStreamParsingException { 350 throw new XmlStreamParsingException(msg, parser.getLocation()); 351 } 352}