001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.tagging.presets; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.io.BufferedReader; 007import java.io.File; 008import java.io.IOException; 009import java.io.InputStream; 010import java.io.InputStreamReader; 011import java.io.Reader; 012import java.util.ArrayDeque; 013import java.util.ArrayList; 014import java.util.Collection; 015import java.util.Deque; 016import java.util.HashMap; 017import java.util.Iterator; 018import java.util.LinkedHashSet; 019import java.util.LinkedList; 020import java.util.List; 021import java.util.Map; 022import java.util.Set; 023 024import javax.swing.JOptionPane; 025 026import org.openstreetmap.josm.Main; 027import org.openstreetmap.josm.gui.preferences.map.TaggingPresetPreference; 028import org.openstreetmap.josm.gui.tagging.presets.items.Check; 029import org.openstreetmap.josm.gui.tagging.presets.items.CheckGroup; 030import org.openstreetmap.josm.gui.tagging.presets.items.Combo; 031import org.openstreetmap.josm.gui.tagging.presets.items.ComboMultiSelect; 032import org.openstreetmap.josm.gui.tagging.presets.items.ItemSeparator; 033import org.openstreetmap.josm.gui.tagging.presets.items.Key; 034import org.openstreetmap.josm.gui.tagging.presets.items.Label; 035import org.openstreetmap.josm.gui.tagging.presets.items.Link; 036import org.openstreetmap.josm.gui.tagging.presets.items.MultiSelect; 037import org.openstreetmap.josm.gui.tagging.presets.items.Optional; 038import org.openstreetmap.josm.gui.tagging.presets.items.PresetLink; 039import org.openstreetmap.josm.gui.tagging.presets.items.Roles; 040import org.openstreetmap.josm.gui.tagging.presets.items.Roles.Role; 041import org.openstreetmap.josm.gui.tagging.presets.items.Space; 042import org.openstreetmap.josm.gui.tagging.presets.items.Text; 043import org.openstreetmap.josm.io.CachedFile; 044import org.openstreetmap.josm.io.UTFInputStreamReader; 045import org.openstreetmap.josm.tools.Predicates; 046import org.openstreetmap.josm.tools.Utils; 047import org.openstreetmap.josm.tools.XmlObjectParser; 048import org.xml.sax.SAXException; 049 050/** 051 * The tagging presets reader. 052 * @since 6068 053 */ 054public final class TaggingPresetReader { 055 056 /** 057 * The accepted MIME types sent in the HTTP Accept header. 058 * @since 6867 059 */ 060 public static final String PRESET_MIME_TYPES = 061 "application/xml, text/xml, text/plain; q=0.8, application/zip, application/octet-stream; q=0.5"; 062 063 private static volatile File zipIcons; 064 private static volatile boolean loadIcons = true; 065 066 /** 067 * Holds a reference to a chunk of items/objects. 068 */ 069 public static class Chunk { 070 /** The chunk id, can be referenced later */ 071 public String id; 072 } 073 074 /** 075 * Holds a reference to an earlier item/object. 076 */ 077 public static class Reference { 078 /** Reference matching a chunk id defined earlier **/ 079 public String ref; 080 } 081 082 static class HashSetWithLast<E> extends LinkedHashSet<E> { 083 protected transient E last; 084 085 @Override 086 public boolean add(E e) { 087 last = e; 088 return super.add(e); 089 } 090 091 /** 092 * Returns the last inserted element. 093 * @return the last inserted element 094 */ 095 public E getLast() { 096 return last; 097 } 098 } 099 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 TaggingPresetPreference.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 Main.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 tp = (TaggingPresetMenu) Utils.filter(all, Predicates.<TaggingPreset>equalTo(tp)).iterator().next(); 230 lastmenuOriginal.group = null; 231 } else { 232 tp.setDisplayName(); 233 all.add(tp); 234 lastmenuOriginal = null; 235 } 236 lastmenu = tp; 237 } 238 lastrole = null; 239 } else if (o instanceof TaggingPresetSeparator) { 240 TaggingPresetSeparator tp = (TaggingPresetSeparator) o; 241 tp.group = lastmenu; 242 all.add(tp); 243 lastrole = null; 244 } else if (o instanceof TaggingPreset) { 245 TaggingPreset tp = (TaggingPreset) o; 246 tp.group = lastmenu; 247 tp.setDisplayName(); 248 all.add(tp); 249 lastrole = null; 250 } else { 251 if (!all.isEmpty()) { 252 if (o instanceof Roles) { 253 all.getLast().data.add((TaggingPresetItem) o); 254 if (all.getLast().roles != null) { 255 throw new SAXException(tr("Roles cannot appear more than once")); 256 } 257 all.getLast().roles = (Roles) o; 258 lastrole = (Roles) o; 259 } else if (o instanceof Role) { 260 if (lastrole == null) 261 throw new SAXException(tr("Preset role element without parent")); 262 lastrole.roles.add((Role) o); 263 } else if (o instanceof Check) { 264 checks.add((Check) o); 265 } else if (o instanceof ComboMultiSelect.PresetListEntry) { 266 listEntries.add((ComboMultiSelect.PresetListEntry) o); 267 } else if (o instanceof CheckGroup) { 268 all.getLast().data.add((TaggingPresetItem) o); 269 // Make sure list of checks is empty to avoid adding checks several times 270 // when used in chunks (fix #10801) 271 ((CheckGroup) o).checks.clear(); 272 ((CheckGroup) o).checks.addAll(checks); 273 checks.clear(); 274 } else { 275 if (!checks.isEmpty()) { 276 all.getLast().data.addAll(checks); 277 checks.clear(); 278 } 279 all.getLast().data.add((TaggingPresetItem) o); 280 if (o instanceof ComboMultiSelect) { 281 ((ComboMultiSelect) o).addListEntries(listEntries); 282 } else if (o instanceof Key) { 283 if (((Key) o).value == null) { 284 ((Key) o).value = ""; // Fix #8530 285 } 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 Main.error(e, false); 364 Main.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 e) { 377 Main.error(e); 378 Main.error(source); 379 JOptionPane.showMessageDialog( 380 Main.parent, 381 "<html>" + tr("Error parsing {0}: ", source) + "<br><br><table width=600>" + e.getMessage() + "</table></html>", 382 tr("Error"), 383 JOptionPane.ERROR_MESSAGE 384 ); 385 } 386 } 387 return allPresets; 388 } 389 390 /** 391 * Reads all tagging presets from sources stored in preferences. 392 * @param validate if {@code true}, presets will be validated against XML schema 393 * @param displayErrMsg if {@code true}, a blocking error message is displayed in case of I/O exception. 394 * @return Collection of all presets successfully read 395 */ 396 public static Collection<TaggingPreset> readFromPreferences(boolean validate, boolean displayErrMsg) { 397 return readAll(getPresetSources(), validate, displayErrMsg); 398 } 399 400 public static File getZipIcons() { 401 return zipIcons; 402 } 403 404 /** 405 * Determines if icon images should be loaded. 406 * @return {@code true} if icon images should be loaded 407 */ 408 public static boolean isLoadIcons() { 409 return loadIcons; 410 } 411 412 /** 413 * Sets whether icon images should be loaded. 414 * @param loadIcons {@code true} if icon images should be loaded 415 */ 416 public static void setLoadIcons(boolean loadIcons) { 417 TaggingPresetReader.loadIcons = loadIcons; 418 } 419 420 private TaggingPresetReader() { 421 // Hide default constructor for utils classes 422 } 423}