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}