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

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

see #15182 - code refactoring to avoid dependence on GUI packages from Preferences

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