001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.tagging; 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.LinkedList; 019import java.util.List; 020import java.util.Map; 021import java.util.Set; 022 023import javax.swing.JOptionPane; 024 025import org.openstreetmap.josm.Main; 026import org.openstreetmap.josm.gui.preferences.map.TaggingPresetPreference; 027import org.openstreetmap.josm.io.CachedFile; 028import org.openstreetmap.josm.io.UTFInputStreamReader; 029import org.openstreetmap.josm.tools.XmlObjectParser; 030import org.xml.sax.SAXException; 031 032/** 033 * The tagging presets reader. 034 * @since 6068 035 */ 036public final class TaggingPresetReader { 037 038 /** 039 * The accepted MIME types sent in the HTTP Accept header. 040 * @since 6867 041 */ 042 public static final String PRESET_MIME_TYPES = "application/xml, text/xml, text/plain; q=0.8, application/zip, application/octet-stream; q=0.5"; 043 044 private TaggingPresetReader() { 045 // Hide default constructor for utils classes 046 } 047 048 private static File zipIcons = null; 049 050 /** 051 * Returns the set of preset source URLs. 052 * @return The set of preset source URLs. 053 */ 054 public static Set<String> getPresetSources() { 055 return new TaggingPresetPreference.PresetPrefHelper().getActiveUrls(); 056 } 057 058 /** 059 * Holds a reference to a chunk of items/objects. 060 */ 061 public static class Chunk { 062 /** The chunk id, can be referenced later */ 063 public String id; 064 } 065 066 /** 067 * Holds a reference to an earlier item/object. 068 */ 069 public static class Reference { 070 /** Reference matching a chunk id defined earlier **/ 071 public String ref; 072 } 073 074 private static XmlObjectParser buildParser() { 075 XmlObjectParser parser = new XmlObjectParser(); 076 parser.mapOnStart("item", TaggingPreset.class); 077 parser.mapOnStart("separator", TaggingPresetSeparator.class); 078 parser.mapBoth("group", TaggingPresetMenu.class); 079 parser.map("text", TaggingPresetItems.Text.class); 080 parser.map("link", TaggingPresetItems.Link.class); 081 parser.map("preset_link", TaggingPresetItems.PresetLink.class); 082 parser.mapOnStart("optional", TaggingPresetItems.Optional.class); 083 parser.mapOnStart("roles", TaggingPresetItems.Roles.class); 084 parser.map("role", TaggingPresetItems.Role.class); 085 parser.map("checkgroup", TaggingPresetItems.CheckGroup.class); 086 parser.map("check", TaggingPresetItems.Check.class); 087 parser.map("combo", TaggingPresetItems.Combo.class); 088 parser.map("multiselect", TaggingPresetItems.MultiSelect.class); 089 parser.map("label", TaggingPresetItems.Label.class); 090 parser.map("space", TaggingPresetItems.Space.class); 091 parser.map("key", TaggingPresetItems.Key.class); 092 parser.map("list_entry", TaggingPresetItems.PresetListEntry.class); 093 parser.map("item_separator", TaggingPresetItems.ItemSeparator.class); 094 parser.mapBoth("chunk", Chunk.class); 095 parser.map("reference", Reference.class); 096 return parser; 097 } 098 099 /** 100 * Reads all tagging presets from the input reader. 101 * @param in The input reader 102 * @param validate if {@code true}, XML validation will be performed 103 * @return collection of tagging presets 104 * @throws SAXException if any XML error occurs 105 */ 106 public static Collection<TaggingPreset> readAll(Reader in, boolean validate) throws SAXException { 107 XmlObjectParser parser = buildParser(); 108 109 Deque<TaggingPreset> all = new LinkedList<>(); 110 TaggingPresetMenu lastmenu = null; 111 TaggingPresetItems.Roles lastrole = null; 112 final List<TaggingPresetItems.Check> checks = new LinkedList<>(); 113 List<TaggingPresetItems.PresetListEntry> listEntries = new LinkedList<>(); 114 final Map<String, List<Object>> byId = new HashMap<>(); 115 final Deque<String> lastIds = new ArrayDeque<>(); 116 /** lastIdIterators contains non empty iterators of items to be handled before obtaining the next item from the XML parser */ 117 final Deque<Iterator<Object>> lastIdIterators = new ArrayDeque<>(); 118 119 if (validate) { 120 parser.startWithValidation(in, Main.getXMLBase()+"/tagging-preset-1.0", "resource://data/tagging-preset.xsd"); 121 } else { 122 parser.start(in); 123 } 124 while (parser.hasNext() || !lastIdIterators.isEmpty()) { 125 final Object o; 126 if (!lastIdIterators.isEmpty()) { 127 // obtain elements from lastIdIterators with higher priority 128 o = lastIdIterators.peek().next(); 129 if (!lastIdIterators.peek().hasNext()) { 130 // remove iterator if is empty 131 lastIdIterators.pop(); 132 } 133 } else { 134 o = parser.next(); 135 } 136 if (o instanceof Chunk) { 137 if (!lastIds.isEmpty() && ((Chunk) o).id.equals(lastIds.peek())) { 138 // pop last id on end of object, don't process further 139 lastIds.pop(); 140 ((Chunk) o).id = null; 141 continue; 142 } else { 143 // if preset item contains an id, store a mapping for later usage 144 String lastId = ((Chunk) o).id; 145 lastIds.push(lastId); 146 byId.put(lastId, new ArrayList<>()); 147 continue; 148 } 149 } else if (!lastIds.isEmpty()) { 150 // add object to mapping for later usage 151 byId.get(lastIds.peek()).add(o); 152 continue; 153 } 154 if (o instanceof Reference) { 155 // if o is a reference, obtain the corresponding objects from the mapping, 156 // and iterate over those before consuming the next element from parser. 157 final String ref = ((Reference) o).ref; 158 if (byId.get(ref) == null) { 159 throw new SAXException(tr("Reference {0} is being used before it was defined", ref)); 160 } 161 Iterator<Object> it = byId.get(ref).iterator(); 162 if (it.hasNext()) { 163 lastIdIterators.push(it); 164 } else { 165 Main.warn("Ignoring reference '"+ref+"' denoting an empty chunk"); 166 } 167 continue; 168 } 169 if (!(o instanceof TaggingPresetItem) && !checks.isEmpty()) { 170 all.getLast().data.addAll(checks); 171 checks.clear(); 172 } 173 if (o instanceof TaggingPresetMenu) { 174 TaggingPresetMenu tp = (TaggingPresetMenu) o; 175 if (tp == lastmenu) { 176 lastmenu = tp.group; 177 } else { 178 tp.group = lastmenu; 179 tp.setDisplayName(); 180 lastmenu = tp; 181 all.add(tp); 182 } 183 lastrole = null; 184 } else if (o instanceof TaggingPresetSeparator) { 185 TaggingPresetSeparator tp = (TaggingPresetSeparator) o; 186 tp.group = lastmenu; 187 all.add(tp); 188 lastrole = null; 189 } else if (o instanceof TaggingPreset) { 190 TaggingPreset tp = (TaggingPreset) o; 191 tp.group = lastmenu; 192 tp.setDisplayName(); 193 all.add(tp); 194 lastrole = null; 195 } else { 196 if (!all.isEmpty()) { 197 if (o instanceof TaggingPresetItems.Roles) { 198 all.getLast().data.add((TaggingPresetItem) o); 199 if (all.getLast().roles != null) { 200 throw new SAXException(tr("Roles cannot appear more than once")); 201 } 202 all.getLast().roles = (TaggingPresetItems.Roles) o; 203 lastrole = (TaggingPresetItems.Roles) o; 204 } else if (o instanceof TaggingPresetItems.Role) { 205 if (lastrole == null) 206 throw new SAXException(tr("Preset role element without parent")); 207 lastrole.roles.add((TaggingPresetItems.Role) o); 208 } else if (o instanceof TaggingPresetItems.Check) { 209 checks.add((TaggingPresetItems.Check) o); 210 } else if (o instanceof TaggingPresetItems.PresetListEntry) { 211 listEntries.add((TaggingPresetItems.PresetListEntry) o); 212 } else if (o instanceof TaggingPresetItems.CheckGroup) { 213 all.getLast().data.add((TaggingPresetItem) o); 214 // Make sure list of checks is empty to avoid adding checks several times 215 // when used in chunks (fix #10801) 216 ((TaggingPresetItems.CheckGroup) o).checks.clear(); 217 ((TaggingPresetItems.CheckGroup) o).checks.addAll(checks); 218 checks.clear(); 219 } else { 220 if (!checks.isEmpty()) { 221 all.getLast().data.addAll(checks); 222 checks.clear(); 223 } 224 all.getLast().data.add((TaggingPresetItem) o); 225 if (o instanceof TaggingPresetItems.ComboMultiSelect) { 226 ((TaggingPresetItems.ComboMultiSelect) o).addListEntries(listEntries); 227 } else if (o instanceof TaggingPresetItems.Key) { 228 if (((TaggingPresetItems.Key) o).value == null) { 229 ((TaggingPresetItems.Key) o).value = ""; // Fix #8530 230 } 231 } 232 listEntries = new LinkedList<>(); 233 lastrole = null; 234 } 235 } else 236 throw new SAXException(tr("Preset sub element without parent")); 237 } 238 } 239 if (!all.isEmpty() && !checks.isEmpty()) { 240 all.getLast().data.addAll(checks); 241 checks.clear(); 242 } 243 return all; 244 } 245 246 /** 247 * Reads all tagging presets from the given source. 248 * @param source a given filename, URL or internal resource 249 * @param validate if {@code true}, XML validation will be performed 250 * @return collection of tagging presets 251 * @throws SAXException if any XML error occurs 252 * @throws IOException if any I/O error occurs 253 */ 254 public static Collection<TaggingPreset> readAll(String source, boolean validate) throws SAXException, IOException { 255 Collection<TaggingPreset> tp; 256 CachedFile cf = new CachedFile(source).setHttpAccept(PRESET_MIME_TYPES); 257 try ( 258 // zip may be null, but Java 7 allows it: https://blogs.oracle.com/darcy/entry/project_coin_null_try_with 259 InputStream zip = cf.findZipEntryInputStream("xml", "preset") 260 ) { 261 if (zip != null) { 262 zipIcons = cf.getFile(); 263 } 264 try (InputStreamReader r = UTFInputStreamReader.create(zip == null ? cf.getInputStream() : zip)) { 265 tp = readAll(new BufferedReader(r), validate); 266 } 267 } 268 return tp; 269 } 270 271 /** 272 * Reads all tagging presets from the given sources. 273 * @param sources Collection of tagging presets sources. 274 * @param validate if {@code true}, presets will be validated against XML schema 275 * @return Collection of all presets successfully read 276 */ 277 public static Collection<TaggingPreset> readAll(Collection<String> sources, boolean validate) { 278 return readAll(sources, validate, true); 279 } 280 281 /** 282 * Reads all tagging presets from the given sources. 283 * @param sources Collection of tagging presets sources. 284 * @param validate if {@code true}, presets will be validated against XML schema 285 * @param displayErrMsg if {@code true}, a blocking error message is displayed in case of I/O exception. 286 * @return Collection of all presets successfully read 287 */ 288 public static Collection<TaggingPreset> readAll(Collection<String> sources, boolean validate, boolean displayErrMsg) { 289 LinkedList<TaggingPreset> allPresets = new LinkedList<>(); 290 for(String source : sources) { 291 try { 292 allPresets.addAll(readAll(source, validate)); 293 } catch (IOException e) { 294 Main.error(e, false); 295 Main.error(source); 296 if (source.startsWith("http")) { 297 Main.addNetworkError(source, e); 298 } 299 if (displayErrMsg) { 300 JOptionPane.showMessageDialog( 301 Main.parent, 302 tr("Could not read tagging preset source: {0}",source), 303 tr("Error"), 304 JOptionPane.ERROR_MESSAGE 305 ); 306 } 307 } catch (SAXException e) { 308 Main.error(e); 309 Main.error(source); 310 JOptionPane.showMessageDialog( 311 Main.parent, 312 "<html>" + tr("Error parsing {0}: ", source) + "<br><br><table width=600>" + e.getMessage() + "</table></html>", 313 tr("Error"), 314 JOptionPane.ERROR_MESSAGE 315 ); 316 } 317 } 318 return allPresets; 319 } 320 321 /** 322 * Reads all tagging presets from sources stored in preferences. 323 * @param validate if {@code true}, presets will be validated against XML schema 324 * @param displayErrMsg if {@code true}, a blocking error message is displayed in case of I/O exception. 325 * @return Collection of all presets successfully read 326 */ 327 public static Collection<TaggingPreset> readFromPreferences(boolean validate, boolean displayErrMsg) { 328 return readAll(getPresetSources(), validate, displayErrMsg); 329 } 330 331 public static File getZipIcons() { 332 return zipIcons; 333 } 334}