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.XmlObjectParser; 046import org.xml.sax.SAXException; 047 048/** 049 * The tagging presets reader. 050 * @since 6068 051 */ 052public final class TaggingPresetReader { 053 054 /** 055 * The accepted MIME types sent in the HTTP Accept header. 056 * @since 6867 057 */ 058 public static final String PRESET_MIME_TYPES = 059 "application/xml, text/xml, text/plain; q=0.8, application/zip, application/octet-stream; q=0.5"; 060 061 private static volatile File zipIcons; 062 private static volatile boolean loadIcons = true; 063 064 /** 065 * Holds a reference to a chunk of items/objects. 066 */ 067 public static class Chunk { 068 /** The chunk id, can be referenced later */ 069 public String id; 070 } 071 072 /** 073 * Holds a reference to an earlier item/object. 074 */ 075 public static class Reference { 076 /** Reference matching a chunk id defined earlier **/ 077 public String ref; 078 } 079 080 static class HashSetWithLast<E> extends LinkedHashSet<E> { 081 protected transient E last; 082 083 @Override 084 public boolean add(E e) { 085 last = e; 086 return super.add(e); 087 } 088 089 /** 090 * Returns the last inserted element. 091 * @return the last inserted element 092 */ 093 public E getLast() { 094 return last; 095 } 096 } 097 098 /** 099 * 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 java.util.Optional<TaggingPreset> val = all.stream().filter(tp::equals).findFirst(); 228 if (val.isPresent()) 229 tp = (TaggingPresetMenu) val.get(); 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 && ((Key) o).value == null) { 283 ((Key) o).value = ""; // Fix #8530 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 | IllegalArgumentException 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}