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 is 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 lastIdIterators.push(byId.get(ref).iterator()); 149 continue; 150 } 151 if (!(o instanceof TaggingPresetItem) && !checks.isEmpty()) { 152 all.getLast().data.addAll(checks); 153 checks.clear(); 154 } 155 if (o instanceof TaggingPresetMenu) { 156 TaggingPresetMenu tp = (TaggingPresetMenu) o; 157 if (tp == lastmenu) { 158 lastmenu = tp.group; 159 } else { 160 tp.group = lastmenu; 161 tp.setDisplayName(); 162 lastmenu = tp; 163 all.add(tp); 164 } 165 lastrole = null; 166 } else if (o instanceof TaggingPresetSeparator) { 167 TaggingPresetSeparator tp = (TaggingPresetSeparator) o; 168 tp.group = lastmenu; 169 all.add(tp); 170 lastrole = null; 171 } else if (o instanceof TaggingPreset) { 172 TaggingPreset tp = (TaggingPreset) o; 173 tp.group = lastmenu; 174 tp.setDisplayName(); 175 all.add(tp); 176 lastrole = null; 177 } else { 178 if (!all.isEmpty()) { 179 if (o instanceof TaggingPresetItems.Roles) { 180 all.getLast().data.add((TaggingPresetItem) o); 181 if (all.getLast().roles != null) { 182 throw new SAXException(tr("Roles cannot appear more than once")); 183 } 184 all.getLast().roles = (TaggingPresetItems.Roles) o; 185 lastrole = (TaggingPresetItems.Roles) o; 186 } else if (o instanceof TaggingPresetItems.Role) { 187 if (lastrole == null) 188 throw new SAXException(tr("Preset role element without parent")); 189 lastrole.roles.add((TaggingPresetItems.Role) o); 190 } else if (o instanceof TaggingPresetItems.Check) { 191 checks.add((TaggingPresetItems.Check) o); 192 } else if (o instanceof TaggingPresetItems.PresetListEntry) { 193 listEntries.add((TaggingPresetItems.PresetListEntry) o); 194 } else if (o instanceof TaggingPresetItems.CheckGroup) { 195 all.getLast().data.add((TaggingPresetItem) o); 196 ((TaggingPresetItems.CheckGroup) o).checks.addAll(checks); 197 checks.clear(); 198 } else { 199 if (!checks.isEmpty()) { 200 all.getLast().data.addAll(checks); 201 checks.clear(); 202 } 203 all.getLast().data.add((TaggingPresetItem) o); 204 if (o instanceof TaggingPresetItems.ComboMultiSelect) { 205 ((TaggingPresetItems.ComboMultiSelect) o).addListEntries(listEntries); 206 } else if (o instanceof TaggingPresetItems.Key) { 207 if (((TaggingPresetItems.Key) o).value == null) { 208 ((TaggingPresetItems.Key) o).value = ""; // Fix #8530 209 } 210 } 211 listEntries = new LinkedList<>(); 212 lastrole = null; 213 } 214 } else 215 throw new SAXException(tr("Preset sub element without parent")); 216 } 217 } 218 if (!all.isEmpty() && !checks.isEmpty()) { 219 all.getLast().data.addAll(checks); 220 checks.clear(); 221 } 222 return all; 223 } 224 225 public static Collection<TaggingPreset> readAll(String source, boolean validate) throws SAXException, IOException { 226 Collection<TaggingPreset> tp; 227 CachedFile cf = new CachedFile(source).setHttpAccept(PRESET_MIME_TYPES); 228 try ( 229 // zip may be null, but Java 7 allows it: https://blogs.oracle.com/darcy/entry/project_coin_null_try_with 230 InputStream zip = cf.findZipEntryInputStream("xml", "preset") 231 ) { 232 if (zip != null) { 233 zipIcons = cf.getFile(); 234 } 235 try (InputStreamReader r = new InputStreamReader(zip == null ? cf.getInputStream() : zip, StandardCharsets.UTF_8)) { 236 tp = readAll(new BufferedReader(r), validate); 237 } 238 } 239 return tp; 240 } 241 242 /** 243 * Reads all tagging presets from the given sources. 244 * @param sources Collection of tagging presets sources. 245 * @param validate if {@code true}, presets will be validated against XML schema 246 * @return Collection of all presets successfully read 247 */ 248 public static Collection<TaggingPreset> readAll(Collection<String> sources, boolean validate) { 249 return readAll(sources, validate, true); 250 } 251 252 /** 253 * Reads all tagging presets from the given sources. 254 * @param sources Collection of tagging presets sources. 255 * @param validate if {@code true}, presets will be validated against XML schema 256 * @param displayErrMsg if {@code true}, a blocking error message is displayed in case of I/O exception. 257 * @return Collection of all presets successfully read 258 */ 259 public static Collection<TaggingPreset> readAll(Collection<String> sources, boolean validate, boolean displayErrMsg) { 260 LinkedList<TaggingPreset> allPresets = new LinkedList<>(); 261 for(String source : sources) { 262 try { 263 allPresets.addAll(readAll(source, validate)); 264 } catch (IOException e) { 265 Main.error(e, false); 266 Main.error(source); 267 if (source.startsWith("http")) { 268 Main.addNetworkError(source, e); 269 } 270 if (displayErrMsg) { 271 JOptionPane.showMessageDialog( 272 Main.parent, 273 tr("Could not read tagging preset source: {0}",source), 274 tr("Error"), 275 JOptionPane.ERROR_MESSAGE 276 ); 277 } 278 } catch (SAXException e) { 279 Main.error(e); 280 Main.error(source); 281 JOptionPane.showMessageDialog( 282 Main.parent, 283 "<html>" + tr("Error parsing {0}: ", source) + "<br><br><table width=600>" + e.getMessage() + "</table></html>", 284 tr("Error"), 285 JOptionPane.ERROR_MESSAGE 286 ); 287 } 288 } 289 return allPresets; 290 } 291 292 /** 293 * Reads all tagging presets from sources stored in preferences. 294 * @param validate if {@code true}, presets will be validated against XML schema 295 * @param displayErrMsg if {@code true}, a blocking error message is displayed in case of I/O exception. 296 * @return Collection of all presets successfully read 297 */ 298 public static Collection<TaggingPreset> readFromPreferences(boolean validate, boolean displayErrMsg) { 299 return readAll(getPresetSources(), validate, displayErrMsg); 300 } 301 302 public static File getZipIcons() { 303 return zipIcons; 304 } 305}