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

Last change on this file since 18221 was 17636, checked in by GerdP, 3 years ago

fix #20578: JOSM is out of memory - While adding a local xml preset

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