source: josm/trunk/src/org/openstreetmap/josm/data/preferences/PreferencesReader.java@ 13908

Last change on this file since 13908 was 13901, checked in by Don-vip, 6 years ago

add new XmlUtils class with more "safe factories" methods

  • Property svn:eol-style set to native
File size: 12.9 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.data.preferences;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.io.BufferedReader;
7import java.io.File;
8import java.io.IOException;
9import java.io.InputStream;
10import java.io.Reader;
11import java.nio.charset.StandardCharsets;
12import java.nio.file.Files;
13import java.util.ArrayList;
14import java.util.Collections;
15import java.util.LinkedHashMap;
16import java.util.List;
17import java.util.Map;
18import java.util.Optional;
19import java.util.SortedMap;
20import java.util.TreeMap;
21
22import javax.xml.XMLConstants;
23import javax.xml.stream.XMLStreamConstants;
24import javax.xml.stream.XMLStreamException;
25import javax.xml.stream.XMLStreamReader;
26import javax.xml.transform.stream.StreamSource;
27import javax.xml.validation.Schema;
28import javax.xml.validation.Validator;
29
30import org.openstreetmap.josm.io.CachedFile;
31import org.openstreetmap.josm.io.XmlStreamParsingException;
32import org.openstreetmap.josm.spi.preferences.ListListSetting;
33import org.openstreetmap.josm.spi.preferences.ListSetting;
34import org.openstreetmap.josm.spi.preferences.MapListSetting;
35import org.openstreetmap.josm.spi.preferences.Setting;
36import org.openstreetmap.josm.spi.preferences.StringSetting;
37import org.openstreetmap.josm.tools.Logging;
38import org.openstreetmap.josm.tools.XmlUtils;
39import org.xml.sax.SAXException;
40
41/**
42 * Loads preferences from XML.
43 */
44public class PreferencesReader {
45
46 private final SortedMap<String, Setting<?>> settings = new TreeMap<>();
47 private XMLStreamReader parser;
48 private int version;
49 private final Reader reader;
50 private final File file;
51
52 private final boolean defaults;
53
54 /**
55 * Constructs a new {@code PreferencesReader}.
56 * @param file the file
57 * @param defaults true when reading from the cache file for default preferences,
58 * false for the regular preferences config file
59 */
60 public PreferencesReader(File file, boolean defaults) {
61 this.defaults = defaults;
62 this.reader = null;
63 this.file = file;
64 }
65
66 /**
67 * Constructs a new {@code PreferencesReader}.
68 * @param reader the {@link Reader}
69 * @param defaults true when reading from the cache file for default preferences,
70 * false for the regular preferences config file
71 */
72 public PreferencesReader(Reader reader, boolean defaults) {
73 this.defaults = defaults;
74 this.reader = reader;
75 this.file = null;
76 }
77
78 /**
79 * Validate the XML.
80 * @param f the file
81 * @throws IOException if any I/O error occurs
82 * @throws SAXException if any SAX error occurs
83 */
84 public static void validateXML(File f) throws IOException, SAXException {
85 try (BufferedReader in = Files.newBufferedReader(f.toPath(), StandardCharsets.UTF_8)) {
86 validateXML(in);
87 }
88 }
89
90 /**
91 * Validate the XML.
92 * @param in the {@link Reader}
93 * @throws IOException if any I/O error occurs
94 * @throws SAXException if any SAX error occurs
95 */
96 public static void validateXML(Reader in) throws IOException, SAXException {
97 try (CachedFile cf = new CachedFile("resource://data/preferences.xsd"); InputStream xsdStream = cf.getInputStream()) {
98 Schema schema = XmlUtils.newXmlSchemaFactory().newSchema(new StreamSource(xsdStream));
99 Validator validator = schema.newValidator();
100 validator.validate(new StreamSource(in));
101 }
102 }
103
104 /**
105 * Return the parsed preferences as a settings map
106 * @return the parsed preferences as a settings map
107 */
108 public SortedMap<String, Setting<?>> getSettings() {
109 return settings;
110 }
111
112 /**
113 * Return the version from the XML root element.
114 * (Represents the JOSM version when the file was written.)
115 * @return the version
116 */
117 public int getVersion() {
118 return version;
119 }
120
121 /**
122 * Parse preferences.
123 * @throws XMLStreamException if any XML parsing error occurs
124 * @throws IOException if any I/O error occurs
125 */
126 public void parse() throws XMLStreamException, IOException {
127 if (reader != null) {
128 this.parser = XmlUtils.newSafeXMLInputFactory().createXMLStreamReader(reader);
129 doParse();
130 } else {
131 try (BufferedReader in = Files.newBufferedReader(file.toPath(), StandardCharsets.UTF_8)) {
132 this.parser = XmlUtils.newSafeXMLInputFactory().createXMLStreamReader(in);
133 doParse();
134 }
135 }
136 }
137
138 private void doParse() throws XMLStreamException {
139 int event = parser.getEventType();
140 while (true) {
141 if (event == XMLStreamConstants.START_ELEMENT) {
142 String topLevelElementName = defaults ? "preferences-defaults" : "preferences";
143 String localName = parser.getLocalName();
144 if (!topLevelElementName.equals(localName)) {
145 throw new XMLStreamException(
146 tr("Expected element ''{0}'', but got ''{1}''", topLevelElementName, localName),
147 parser.getLocation());
148 }
149 try {
150 version = Integer.parseInt(parser.getAttributeValue(null, "version"));
151 } catch (NumberFormatException e) {
152 Logging.log(Logging.LEVEL_DEBUG, e);
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 setting = new StringSetting(Optional.ofNullable(parser.getAttributeValue(null, "value"))
179 .orElseThrow(() -> new XMLStreamException(tr("value expected"), parser.getLocation())));
180 }
181 if (defaults) {
182 setting.setTime(Math.round(Double.parseDouble(parser.getAttributeValue(null, "time"))));
183 }
184 settings.put(parser.getAttributeValue(null, "key"), setting);
185 jumpToEnd();
186 break;
187 case "list":
188 case "lists":
189 case "maps":
190 parseToplevelList();
191 break;
192 default:
193 throwException("Unexpected element: "+localName);
194 }
195 } else if (event == XMLStreamConstants.END_ELEMENT) {
196 return;
197 }
198 }
199 }
200
201 private void jumpToEnd() throws XMLStreamException {
202 while (true) {
203 int event = parser.next();
204 if (event == XMLStreamConstants.START_ELEMENT) {
205 jumpToEnd();
206 } else if (event == XMLStreamConstants.END_ELEMENT) {
207 return;
208 }
209 }
210 }
211
212 private void parseToplevelList() throws XMLStreamException {
213 String key = parser.getAttributeValue(null, "key");
214 Long time = null;
215 if (defaults) {
216 time = Math.round(Double.parseDouble(parser.getAttributeValue(null, "time")));
217 }
218 String name = parser.getLocalName();
219
220 List<String> entries = null;
221 List<List<String>> lists = null;
222 List<Map<String, String>> maps = null;
223 if (defaults && isNil()) {
224 Setting<?> setting;
225 switch (name) {
226 case "lists":
227 setting = new ListListSetting(null);
228 break;
229 case "maps":
230 setting = new MapListSetting(null);
231 break;
232 default:
233 setting = new ListSetting(null);
234 break;
235 }
236 setting.setTime(time);
237 settings.put(key, setting);
238 jumpToEnd();
239 } else {
240 while (true) {
241 int event = parser.next();
242 if (event == XMLStreamConstants.START_ELEMENT) {
243 String localName = parser.getLocalName();
244 switch(localName) {
245 case "entry":
246 if (entries == null) {
247 entries = new ArrayList<>();
248 }
249 entries.add(parser.getAttributeValue(null, "value"));
250 jumpToEnd();
251 break;
252 case "list":
253 if (lists == null) {
254 lists = new ArrayList<>();
255 }
256 lists.add(parseInnerList());
257 break;
258 case "map":
259 if (maps == null) {
260 maps = new ArrayList<>();
261 }
262 maps.add(parseMap());
263 break;
264 default:
265 throwException("Unexpected element: "+localName);
266 }
267 } else if (event == XMLStreamConstants.END_ELEMENT) {
268 break;
269 }
270 }
271 Setting<?> setting;
272 if (entries != null) {
273 setting = new ListSetting(Collections.unmodifiableList(entries));
274 } else if (lists != null) {
275 setting = new ListListSetting(Collections.unmodifiableList(lists));
276 } else if (maps != null) {
277 setting = new MapListSetting(Collections.unmodifiableList(maps));
278 } else {
279 switch (name) {
280 case "lists":
281 setting = new ListListSetting(Collections.<List<String>>emptyList());
282 break;
283 case "maps":
284 setting = new MapListSetting(Collections.<Map<String, String>>emptyList());
285 break;
286 default:
287 setting = new ListSetting(Collections.<String>emptyList());
288 break;
289 }
290 }
291 if (defaults) {
292 setting.setTime(time);
293 }
294 settings.put(key, setting);
295 }
296 }
297
298 private List<String> parseInnerList() throws XMLStreamException {
299 List<String> entries = new ArrayList<>();
300 while (true) {
301 int event = parser.next();
302 if (event == XMLStreamConstants.START_ELEMENT) {
303 if ("entry".equals(parser.getLocalName())) {
304 entries.add(parser.getAttributeValue(null, "value"));
305 jumpToEnd();
306 } else {
307 throwException("Unexpected element: "+parser.getLocalName());
308 }
309 } else if (event == XMLStreamConstants.END_ELEMENT) {
310 break;
311 }
312 }
313 return Collections.unmodifiableList(entries);
314 }
315
316 private Map<String, String> parseMap() throws XMLStreamException {
317 Map<String, String> map = new LinkedHashMap<>();
318 while (true) {
319 int event = parser.next();
320 if (event == XMLStreamConstants.START_ELEMENT) {
321 if ("tag".equals(parser.getLocalName())) {
322 map.put(parser.getAttributeValue(null, "key"), parser.getAttributeValue(null, "value"));
323 jumpToEnd();
324 } else {
325 throwException("Unexpected element: "+parser.getLocalName());
326 }
327 } else if (event == XMLStreamConstants.END_ELEMENT) {
328 break;
329 }
330 }
331 return Collections.unmodifiableMap(map);
332 }
333
334 /**
335 * Check if the current element is nil (meaning the value of the setting is null).
336 * @return true, if the current element is nil
337 * @see <a href="https://msdn.microsoft.com/en-us/library/2b314yt2(v=vs.85).aspx">Nillable Attribute on MS Developer Network</a>
338 */
339 private boolean isNil() {
340 String nil = parser.getAttributeValue(XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI, "nil");
341 return "true".equals(nil) || "1".equals(nil);
342 }
343
344 /**
345 * Throw XmlStreamParsingException with line and column number.
346 *
347 * Only use this for errors that should not be possible after schema validation.
348 * @param msg the error message
349 * @throws XmlStreamParsingException always
350 */
351 private void throwException(String msg) throws XmlStreamParsingException {
352 throw new XmlStreamParsingException(msg, parser.getLocation());
353 }
354}
Note: See TracBrowser for help on using the repository browser.