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