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

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

code cleanup / javadoc

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