001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.io;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.io.IOException;
007import java.io.InputStream;
008import java.io.InputStreamReader;
009import java.io.StringReader;
010import java.nio.charset.StandardCharsets;
011
012import javax.xml.parsers.ParserConfigurationException;
013
014import org.openstreetmap.josm.data.osm.ChangesetDataSet;
015import org.openstreetmap.josm.data.osm.ChangesetDataSet.ChangesetModificationType;
016import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
017import org.openstreetmap.josm.gui.progress.ProgressMonitor;
018import org.openstreetmap.josm.tools.CheckParameterUtil;
019import org.openstreetmap.josm.tools.Logging;
020import org.openstreetmap.josm.tools.XmlParsingException;
021import org.openstreetmap.josm.tools.XmlUtils;
022import org.xml.sax.Attributes;
023import org.xml.sax.InputSource;
024import org.xml.sax.SAXException;
025import org.xml.sax.SAXParseException;
026
027/**
028 * Parser for OSM changeset content.
029 * @since 2688
030 */
031public class OsmChangesetContentParser {
032
033    private final InputSource source;
034    private final ChangesetDataSet data = new ChangesetDataSet();
035
036    private class Parser extends AbstractParser {
037        Parser(boolean useAnonymousUser) {
038            this.useAnonymousUser = useAnonymousUser;
039        }
040
041        /** the current change modification type */
042        private ChangesetDataSet.ChangesetModificationType currentModificationType;
043
044        @Override
045        protected void throwException(String message) throws XmlParsingException {
046            throw new XmlParsingException(message).rememberLocation(locator);
047        }
048
049        @Override
050        protected void throwException(String message, Exception e) throws XmlParsingException {
051            throw new XmlParsingException(message, e).rememberLocation(locator);
052        }
053
054        @Override
055        public void startElement(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException {
056            if (super.doStartElement(qName, atts)) {
057                // done
058                return;
059            }
060            switch (qName) {
061            case "osmChange":
062                // do nothing
063                break;
064            case "create":
065                currentModificationType = ChangesetModificationType.CREATED;
066                break;
067            case "modify":
068                currentModificationType = ChangesetModificationType.UPDATED;
069                break;
070            case "delete":
071                currentModificationType = ChangesetModificationType.DELETED;
072                break;
073            default:
074                Logging.warn(tr("Unsupported start element ''{0}'' in changeset content at position ({1},{2}). Skipping.",
075                        qName, locator.getLineNumber(), locator.getColumnNumber()));
076            }
077        }
078
079        @Override
080        public void endElement(String uri, String localName, String qName) throws SAXException {
081            switch (qName) {
082            case "node":
083            case "way":
084            case "relation":
085                if (currentModificationType == null) {
086                    // CHECKSTYLE.OFF: LineLength
087                    throwException(tr("Illegal document structure. Found node, way, or relation outside of ''create'', ''modify'', or ''delete''."));
088                    // CHECKSTYLE.ON: LineLength
089                }
090                data.put(currentPrimitive, currentModificationType);
091                break;
092            case "create":
093            case "modify":
094            case "delete":
095                currentModificationType = null;
096                break;
097            case "osmChange":
098            case "tag":
099            case "nd":
100            case "member":
101                // do nothing
102                break;
103            default:
104                Logging.warn(tr("Unsupported end element ''{0}'' in changeset content at position ({1},{2}). Skipping.",
105                        qName, locator.getLineNumber(), locator.getColumnNumber()));
106            }
107        }
108
109        @Override
110        public void error(SAXParseException e) throws SAXException {
111            throwException(null, e);
112        }
113
114        @Override
115        public void fatalError(SAXParseException e) throws SAXException {
116            throwException(null, e);
117        }
118    }
119
120    /**
121     * Constructs a new {@code OsmChangesetContentParser}.
122     *
123     * @param source the input stream with the changeset content as XML document. Must not be null.
124     * @throws IllegalArgumentException if source is {@code null}.
125     */
126    public OsmChangesetContentParser(InputStream source) {
127        CheckParameterUtil.ensureParameterNotNull(source, "source");
128        this.source = new InputSource(new InputStreamReader(source, StandardCharsets.UTF_8));
129    }
130
131    /**
132     * Constructs a new {@code OsmChangesetContentParser}.
133     *
134     * @param source the input stream with the changeset content as XML document. Must not be null.
135     * @throws IllegalArgumentException if source is {@code null}.
136     */
137    public OsmChangesetContentParser(String source) {
138        CheckParameterUtil.ensureParameterNotNull(source, "source");
139        this.source = new InputSource(new StringReader(source));
140    }
141
142    /**
143     * Parses the content.
144     *
145     * @param progressMonitor the progress monitor. Set to {@link NullProgressMonitor#INSTANCE} if null
146     * @return the parsed data
147     * @throws XmlParsingException if something went wrong. Check for chained
148     * exceptions.
149     */
150    public ChangesetDataSet parse(ProgressMonitor progressMonitor) throws XmlParsingException {
151        return parse(progressMonitor, false);
152    }
153
154    /**
155     * Parses the content.
156     *
157     * @param progressMonitor the progress monitor. Set to {@link NullProgressMonitor#INSTANCE} if null
158     * @param useAnonymousUser if true, replace all user information with the anonymous user
159     * @return the parsed data
160     * @throws XmlParsingException if something went wrong. Check for chained
161     * exceptions.
162     * @since 14946
163     */
164    public ChangesetDataSet parse(ProgressMonitor progressMonitor, boolean useAnonymousUser) throws XmlParsingException {
165        if (progressMonitor == null) {
166            progressMonitor = NullProgressMonitor.INSTANCE;
167        }
168        try {
169            progressMonitor.beginTask("");
170            progressMonitor.indeterminateSubTask(tr("Parsing changeset content ..."));
171            XmlUtils.parseSafeSAX(source, new Parser(useAnonymousUser));
172        } catch (XmlParsingException e) {
173            throw e;
174        } catch (ParserConfigurationException | SAXException | IOException e) {
175            throw new XmlParsingException(e);
176        } finally {
177            progressMonitor.finishTask();
178        }
179        return data;
180    }
181
182    /**
183     * Parses the content from the input source
184     *
185     * @return the parsed data
186     * @throws XmlParsingException if something went wrong. Check for chained
187     * exceptions.
188     */
189    public ChangesetDataSet parse() throws XmlParsingException {
190        return parse(null, false);
191    }
192}