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

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

SonarQube - fix more code issues

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