source: josm/trunk/src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetReader.java@ 11848

Last change on this file since 11848 was 11848, checked in by Don-vip, 7 years ago

fix #14613 - Special HTML characters not escaped in GUI error messages

  • Property svn:eol-style set to native
File size: 17.4 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.tagging.presets;
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.InputStreamReader;
11import java.io.Reader;
12import java.util.ArrayDeque;
13import java.util.ArrayList;
14import java.util.Collection;
15import java.util.Deque;
16import java.util.HashMap;
17import java.util.Iterator;
18import java.util.LinkedHashSet;
19import java.util.LinkedList;
20import java.util.List;
21import java.util.Map;
22import java.util.Set;
23
24import javax.swing.JOptionPane;
25
26import org.openstreetmap.josm.Main;
27import org.openstreetmap.josm.gui.preferences.map.TaggingPresetPreference;
28import org.openstreetmap.josm.gui.tagging.presets.items.Check;
29import org.openstreetmap.josm.gui.tagging.presets.items.CheckGroup;
30import org.openstreetmap.josm.gui.tagging.presets.items.Combo;
31import org.openstreetmap.josm.gui.tagging.presets.items.ComboMultiSelect;
32import org.openstreetmap.josm.gui.tagging.presets.items.ItemSeparator;
33import org.openstreetmap.josm.gui.tagging.presets.items.Key;
34import org.openstreetmap.josm.gui.tagging.presets.items.Label;
35import org.openstreetmap.josm.gui.tagging.presets.items.Link;
36import org.openstreetmap.josm.gui.tagging.presets.items.MultiSelect;
37import org.openstreetmap.josm.gui.tagging.presets.items.Optional;
38import org.openstreetmap.josm.gui.tagging.presets.items.PresetLink;
39import org.openstreetmap.josm.gui.tagging.presets.items.Roles;
40import org.openstreetmap.josm.gui.tagging.presets.items.Roles.Role;
41import org.openstreetmap.josm.gui.tagging.presets.items.Space;
42import org.openstreetmap.josm.gui.tagging.presets.items.Text;
43import org.openstreetmap.josm.io.CachedFile;
44import org.openstreetmap.josm.io.UTFInputStreamReader;
45import org.openstreetmap.josm.tools.Utils;
46import org.openstreetmap.josm.tools.XmlObjectParser;
47import org.xml.sax.SAXException;
48
49/**
50 * The tagging presets reader.
51 * @since 6068
52 */
53public final class TaggingPresetReader {
54
55 /**
56 * The accepted MIME types sent in the HTTP Accept header.
57 * @since 6867
58 */
59 public static final String PRESET_MIME_TYPES =
60 "application/xml, text/xml, text/plain; q=0.8, application/zip, application/octet-stream; q=0.5";
61
62 private static volatile File zipIcons;
63 private static volatile boolean loadIcons = true;
64
65 /**
66 * Holds a reference to a chunk of items/objects.
67 */
68 public static class Chunk {
69 /** The chunk id, can be referenced later */
70 public String id;
71 }
72
73 /**
74 * Holds a reference to an earlier item/object.
75 */
76 public static class Reference {
77 /** Reference matching a chunk id defined earlier **/
78 public String ref;
79 }
80
81 static class HashSetWithLast<E> extends LinkedHashSet<E> {
82 protected transient E last;
83
84 @Override
85 public boolean add(E e) {
86 last = e;
87 return super.add(e);
88 }
89
90 /**
91 * Returns the last inserted element.
92 * @return the last inserted element
93 */
94 public E getLast() {
95 return last;
96 }
97 }
98
99 /**
100 * Returns the set of preset source URLs.
101 * @return The set of preset source URLs.
102 */
103 public static Set<String> getPresetSources() {
104 return new TaggingPresetPreference.PresetPrefHelper().getActiveUrls();
105 }
106
107 private static XmlObjectParser buildParser() {
108 XmlObjectParser parser = new XmlObjectParser();
109 parser.mapOnStart("item", TaggingPreset.class);
110 parser.mapOnStart("separator", TaggingPresetSeparator.class);
111 parser.mapBoth("group", TaggingPresetMenu.class);
112 parser.map("text", Text.class);
113 parser.map("link", Link.class);
114 parser.map("preset_link", PresetLink.class);
115 parser.mapOnStart("optional", Optional.class);
116 parser.mapOnStart("roles", Roles.class);
117 parser.map("role", Role.class);
118 parser.map("checkgroup", CheckGroup.class);
119 parser.map("check", Check.class);
120 parser.map("combo", Combo.class);
121 parser.map("multiselect", MultiSelect.class);
122 parser.map("label", Label.class);
123 parser.map("space", Space.class);
124 parser.map("key", Key.class);
125 parser.map("list_entry", ComboMultiSelect.PresetListEntry.class);
126 parser.map("item_separator", ItemSeparator.class);
127 parser.mapBoth("chunk", Chunk.class);
128 parser.map("reference", Reference.class);
129 return parser;
130 }
131
132 /**
133 * Reads all tagging presets from the input reader.
134 * @param in The input reader
135 * @param validate if {@code true}, XML validation will be performed
136 * @return collection of tagging presets
137 * @throws SAXException if any XML error occurs
138 */
139 public static Collection<TaggingPreset> readAll(Reader in, boolean validate) throws SAXException {
140 return readAll(in, validate, new HashSetWithLast<TaggingPreset>());
141 }
142
143 /**
144 * Reads all tagging presets from the input reader.
145 * @param in The input reader
146 * @param validate if {@code true}, XML validation will be performed
147 * @param all the accumulator for parsed tagging presets
148 * @return the accumulator
149 * @throws SAXException if any XML error occurs
150 */
151 static Collection<TaggingPreset> readAll(Reader in, boolean validate, HashSetWithLast<TaggingPreset> all) throws SAXException {
152 XmlObjectParser parser = buildParser();
153
154 /** to detect end of {@code <group>} */
155 TaggingPresetMenu lastmenu = null;
156 /** to detect end of reused {@code <group>} */
157 TaggingPresetMenu lastmenuOriginal = null;
158 Roles lastrole = null;
159 final List<Check> checks = new LinkedList<>();
160 List<ComboMultiSelect.PresetListEntry> listEntries = new LinkedList<>();
161 final Map<String, List<Object>> byId = new HashMap<>();
162 final Deque<String> lastIds = new ArrayDeque<>();
163 /** lastIdIterators contains non empty iterators of items to be handled before obtaining the next item from the XML parser */
164 final Deque<Iterator<Object>> lastIdIterators = new ArrayDeque<>();
165
166 if (validate) {
167 parser.startWithValidation(in, Main.getXMLBase()+"/tagging-preset-1.0", "resource://data/tagging-preset.xsd");
168 } else {
169 parser.start(in);
170 }
171 while (parser.hasNext() || !lastIdIterators.isEmpty()) {
172 final Object o;
173 if (!lastIdIterators.isEmpty()) {
174 // obtain elements from lastIdIterators with higher priority
175 o = lastIdIterators.peek().next();
176 if (!lastIdIterators.peek().hasNext()) {
177 // remove iterator if is empty
178 lastIdIterators.pop();
179 }
180 } else {
181 o = parser.next();
182 }
183 if (o instanceof Chunk) {
184 if (!lastIds.isEmpty() && ((Chunk) o).id.equals(lastIds.peek())) {
185 // pop last id on end of object, don't process further
186 lastIds.pop();
187 ((Chunk) o).id = null;
188 continue;
189 } else {
190 // if preset item contains an id, store a mapping for later usage
191 String lastId = ((Chunk) o).id;
192 lastIds.push(lastId);
193 byId.put(lastId, new ArrayList<>());
194 continue;
195 }
196 } else if (!lastIds.isEmpty()) {
197 // add object to mapping for later usage
198 byId.get(lastIds.peek()).add(o);
199 continue;
200 }
201 if (o instanceof Reference) {
202 // if o is a reference, obtain the corresponding objects from the mapping,
203 // and iterate over those before consuming the next element from parser.
204 final String ref = ((Reference) o).ref;
205 if (byId.get(ref) == null) {
206 throw new SAXException(tr("Reference {0} is being used before it was defined", ref));
207 }
208 Iterator<Object> it = byId.get(ref).iterator();
209 if (it.hasNext()) {
210 lastIdIterators.push(it);
211 } else {
212 Main.warn("Ignoring reference '"+ref+"' denoting an empty chunk");
213 }
214 continue;
215 }
216 if (!(o instanceof TaggingPresetItem) && !checks.isEmpty()) {
217 all.getLast().data.addAll(checks);
218 checks.clear();
219 }
220 if (o instanceof TaggingPresetMenu) {
221 TaggingPresetMenu tp = (TaggingPresetMenu) o;
222 if (tp == lastmenu || tp == lastmenuOriginal) {
223 lastmenu = tp.group;
224 } else {
225 tp.group = lastmenu;
226 if (all.contains(tp)) {
227 lastmenuOriginal = tp;
228 java.util.Optional<TaggingPreset> val = all.stream().filter(tp::equals).findFirst();
229 if (val.isPresent())
230 tp = (TaggingPresetMenu) val.get();
231 lastmenuOriginal.group = null;
232 } else {
233 tp.setDisplayName();
234 all.add(tp);
235 lastmenuOriginal = null;
236 }
237 lastmenu = tp;
238 }
239 lastrole = null;
240 } else if (o instanceof TaggingPresetSeparator) {
241 TaggingPresetSeparator tp = (TaggingPresetSeparator) o;
242 tp.group = lastmenu;
243 all.add(tp);
244 lastrole = null;
245 } else if (o instanceof TaggingPreset) {
246 TaggingPreset tp = (TaggingPreset) o;
247 tp.group = lastmenu;
248 tp.setDisplayName();
249 all.add(tp);
250 lastrole = null;
251 } else {
252 if (!all.isEmpty()) {
253 if (o instanceof Roles) {
254 all.getLast().data.add((TaggingPresetItem) o);
255 if (all.getLast().roles != null) {
256 throw new SAXException(tr("Roles cannot appear more than once"));
257 }
258 all.getLast().roles = (Roles) o;
259 lastrole = (Roles) o;
260 } else if (o instanceof Role) {
261 if (lastrole == null)
262 throw new SAXException(tr("Preset role element without parent"));
263 lastrole.roles.add((Role) o);
264 } else if (o instanceof Check) {
265 checks.add((Check) o);
266 } else if (o instanceof ComboMultiSelect.PresetListEntry) {
267 listEntries.add((ComboMultiSelect.PresetListEntry) o);
268 } else if (o instanceof CheckGroup) {
269 all.getLast().data.add((TaggingPresetItem) o);
270 // Make sure list of checks is empty to avoid adding checks several times
271 // when used in chunks (fix #10801)
272 ((CheckGroup) o).checks.clear();
273 ((CheckGroup) o).checks.addAll(checks);
274 checks.clear();
275 } else {
276 if (!checks.isEmpty()) {
277 all.getLast().data.addAll(checks);
278 checks.clear();
279 }
280 all.getLast().data.add((TaggingPresetItem) o);
281 if (o instanceof ComboMultiSelect) {
282 ((ComboMultiSelect) o).addListEntries(listEntries);
283 } else if (o instanceof Key && ((Key) o).value == null) {
284 ((Key) o).value = ""; // Fix #8530
285 }
286 listEntries = new LinkedList<>();
287 lastrole = null;
288 }
289 } else
290 throw new SAXException(tr("Preset sub element without parent"));
291 }
292 }
293 if (!all.isEmpty() && !checks.isEmpty()) {
294 all.getLast().data.addAll(checks);
295 checks.clear();
296 }
297 return all;
298 }
299
300 /**
301 * Reads all tagging presets from the given source.
302 * @param source a given filename, URL or internal resource
303 * @param validate if {@code true}, XML validation will be performed
304 * @return collection of tagging presets
305 * @throws SAXException if any XML error occurs
306 * @throws IOException if any I/O error occurs
307 */
308 public static Collection<TaggingPreset> readAll(String source, boolean validate) throws SAXException, IOException {
309 return readAll(source, validate, new HashSetWithLast<TaggingPreset>());
310 }
311
312 /**
313 * Reads all tagging presets from the given source.
314 * @param source a given filename, URL or internal resource
315 * @param validate if {@code true}, XML validation will be performed
316 * @param all the accumulator for parsed tagging presets
317 * @return the accumulator
318 * @throws SAXException if any XML error occurs
319 * @throws IOException if any I/O error occurs
320 */
321 static Collection<TaggingPreset> readAll(String source, boolean validate, HashSetWithLast<TaggingPreset> all)
322 throws SAXException, IOException {
323 Collection<TaggingPreset> tp;
324 try (
325 CachedFile cf = new CachedFile(source).setHttpAccept(PRESET_MIME_TYPES);
326 // zip may be null, but Java 7 allows it: https://blogs.oracle.com/darcy/entry/project_coin_null_try_with
327 InputStream zip = cf.findZipEntryInputStream("xml", "preset")
328 ) {
329 if (zip != null) {
330 zipIcons = cf.getFile();
331 }
332 try (InputStreamReader r = UTFInputStreamReader.create(zip == null ? cf.getInputStream() : zip)) {
333 tp = readAll(new BufferedReader(r), validate, all);
334 }
335 }
336 return tp;
337 }
338
339 /**
340 * Reads all tagging presets from the given sources.
341 * @param sources Collection of tagging presets sources.
342 * @param validate if {@code true}, presets will be validated against XML schema
343 * @return Collection of all presets successfully read
344 */
345 public static Collection<TaggingPreset> readAll(Collection<String> sources, boolean validate) {
346 return readAll(sources, validate, true);
347 }
348
349 /**
350 * Reads all tagging presets from the given sources.
351 * @param sources Collection of tagging presets sources.
352 * @param validate if {@code true}, presets will be validated against XML schema
353 * @param displayErrMsg if {@code true}, a blocking error message is displayed in case of I/O exception.
354 * @return Collection of all presets successfully read
355 */
356 public static Collection<TaggingPreset> readAll(Collection<String> sources, boolean validate, boolean displayErrMsg) {
357 HashSetWithLast<TaggingPreset> allPresets = new HashSetWithLast<>();
358 for (String source : sources) {
359 try {
360 readAll(source, validate, allPresets);
361 } catch (IOException e) {
362 Main.error(e, false);
363 Main.error(source);
364 if (source.startsWith("http")) {
365 Main.addNetworkError(source, e);
366 }
367 if (displayErrMsg) {
368 JOptionPane.showMessageDialog(
369 Main.parent,
370 tr("Could not read tagging preset source: {0}", source),
371 tr("Error"),
372 JOptionPane.ERROR_MESSAGE
373 );
374 }
375 } catch (SAXException | IllegalArgumentException e) {
376 Main.error(e);
377 Main.error(source);
378 JOptionPane.showMessageDialog(
379 Main.parent,
380 "<html>" + tr("Error parsing {0}: ", source) + "<br><br><table width=600>" +
381 Utils.escapeReservedCharactersHTML(e.getMessage()) + "</table></html>",
382 tr("Error"),
383 JOptionPane.ERROR_MESSAGE
384 );
385 }
386 }
387 return allPresets;
388 }
389
390 /**
391 * Reads all tagging presets from sources stored in preferences.
392 * @param validate if {@code true}, presets will be validated against XML schema
393 * @param displayErrMsg if {@code true}, a blocking error message is displayed in case of I/O exception.
394 * @return Collection of all presets successfully read
395 */
396 public static Collection<TaggingPreset> readFromPreferences(boolean validate, boolean displayErrMsg) {
397 return readAll(getPresetSources(), validate, displayErrMsg);
398 }
399
400 public static File getZipIcons() {
401 return zipIcons;
402 }
403
404 /**
405 * Determines if icon images should be loaded.
406 * @return {@code true} if icon images should be loaded
407 */
408 public static boolean isLoadIcons() {
409 return loadIcons;
410 }
411
412 /**
413 * Sets whether icon images should be loaded.
414 * @param loadIcons {@code true} if icon images should be loaded
415 */
416 public static void setLoadIcons(boolean loadIcons) {
417 TaggingPresetReader.loadIcons = loadIcons;
418 }
419
420 private TaggingPresetReader() {
421 // Hide default constructor for utils classes
422 }
423}
Note: See TracBrowser for help on using the repository browser.