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

Last change on this file since 10715 was 10715, checked in by simon04, 8 years ago

see #11390, see #12890 - Deprecate Predicates class

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