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.StringReader;
008import java.util.Collection;
009import java.util.Collections;
010import java.util.Date;
011import java.util.HashMap;
012import java.util.HashSet;
013import java.util.Map;
014import java.util.Optional;
015import java.util.Set;
016
017import javax.xml.parsers.ParserConfigurationException;
018
019import org.openstreetmap.josm.data.osm.Changeset;
020import org.openstreetmap.josm.data.osm.DataSet;
021import org.openstreetmap.josm.data.osm.OsmPrimitive;
022import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
023import org.openstreetmap.josm.data.osm.PrimitiveId;
024import org.openstreetmap.josm.data.osm.SimplePrimitiveId;
025import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
026import org.openstreetmap.josm.gui.progress.ProgressMonitor;
027import org.openstreetmap.josm.tools.CheckParameterUtil;
028import org.openstreetmap.josm.tools.Utils;
029import org.openstreetmap.josm.tools.XmlParsingException;
030import org.openstreetmap.josm.tools.XmlUtils;
031import org.xml.sax.Attributes;
032import org.xml.sax.InputSource;
033import org.xml.sax.Locator;
034import org.xml.sax.SAXException;
035import org.xml.sax.helpers.DefaultHandler;
036
037/**
038 * Helper class to process the OSM API server response to a "diff" upload.
039 * <p>
040 * New primitives (uploaded with negative id) will be assigned a positive id, etc.
041 * The goal is to have a clean state, just like a fresh download (assuming no
042 * concurrent uploads by other users have happened in the meantime).
043 * <p>
044 * @see <a href="https://wiki.openstreetmap.org/wiki/API_v0.6#Response_10">API 0.6 diff upload response</a>
045 */
046public class DiffResultProcessor {
047
048    static class DiffResultEntry {
049        long newId;
050        int newVersion;
051    }
052
053    /**
054     * mapping from old id to new id and version, the result of parsing the diff result
055     * replied by the server
056     */
057    private final Map<PrimitiveId, DiffResultEntry> diffResults = new HashMap<>();
058    /**
059     * the set of processed primitives *after* the new id, the new version and the new changeset id is set
060     */
061    private final Set<OsmPrimitive> processed;
062    /**
063     * the collection of primitives being uploaded
064     */
065    private final Collection<? extends OsmPrimitive> primitives;
066
067    /**
068     * Creates a diff result reader
069     *
070     * @param primitives the collection of primitives which have been uploaded. If null,
071     * assumes an empty collection.
072     */
073    public DiffResultProcessor(Collection<? extends OsmPrimitive> primitives) {
074        this.primitives = Optional.ofNullable(primitives).orElseGet(Collections::emptyList);
075        this.processed = new HashSet<>();
076    }
077
078    /**
079     * Parse the response from a diff upload to the OSM API.
080     *
081     * @param diffUploadResponse the response. Must not be null.
082     * @param progressMonitor a progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null
083     * @throws IllegalArgumentException if diffUploadRequest is null
084     * @throws XmlParsingException if the diffUploadRequest can't be parsed successfully
085     *
086     */
087    public void parse(String diffUploadResponse, ProgressMonitor progressMonitor) throws XmlParsingException {
088        if (progressMonitor == null) {
089            progressMonitor = NullProgressMonitor.INSTANCE;
090        }
091        CheckParameterUtil.ensureParameterNotNull(diffUploadResponse, "diffUploadResponse");
092        try {
093            progressMonitor.beginTask(tr("Parsing response from server..."));
094            InputSource inputSource = new InputSource(new StringReader(diffUploadResponse));
095            XmlUtils.parseSafeSAX(inputSource, new Parser());
096        } catch (XmlParsingException e) {
097            throw e;
098        } catch (IOException | ParserConfigurationException | SAXException e) {
099            throw new XmlParsingException(e);
100        } finally {
101            progressMonitor.finishTask();
102        }
103    }
104
105    /**
106     * Postprocesses the diff result read and parsed from the server.
107     *
108     * Uploaded objects are assigned their new id (if they got assigned a new
109     * id by the server), their new version (if the version was incremented),
110     * and the id of the changeset to which they were uploaded.
111     *
112     * @param cs the current changeset. Ignored if null.
113     * @param monitor the progress monitor. Set to {@link NullProgressMonitor#INSTANCE} if null
114     * @return the collection of processed primitives
115     */
116    protected Set<OsmPrimitive> postProcess(Changeset cs, ProgressMonitor monitor) {
117        if (monitor == null) {
118            monitor = NullProgressMonitor.INSTANCE;
119        }
120        DataSet ds = null;
121        if (!primitives.isEmpty()) {
122            ds = primitives.iterator().next().getDataSet();
123        }
124        boolean readOnly = false;
125        if (ds != null) {
126            readOnly = ds.isLocked();
127            if (readOnly) {
128                ds.unlock();
129            }
130            ds.beginUpdate();
131        }
132        try {
133            monitor.beginTask("Postprocessing uploaded data ...");
134            monitor.setTicksCount(primitives.size());
135            monitor.setTicks(0);
136            for (OsmPrimitive p : primitives) {
137                monitor.worked(1);
138                DiffResultEntry entry = diffResults.get(p.getPrimitiveId());
139                if (entry == null) {
140                    continue;
141                }
142                processed.add(p);
143                if (!p.isDeleted()) {
144                    p.setOsmId(entry.newId, entry.newVersion);
145                    p.setVisible(true);
146                } else {
147                    p.setVisible(false);
148                }
149                if (cs != null && !cs.isNew()) {
150                    p.setChangesetId(cs.getId());
151                    p.setUser(cs.getUser());
152                    // TODO is there a way to obtain the timestamp for non-closed changesets?
153                    p.setTimestamp(Utils.firstNonNull(cs.getClosedAt(), new Date()));
154                }
155            }
156            return processed;
157        } finally {
158            if (ds != null) {
159                ds.endUpdate();
160                if (readOnly) {
161                    ds.lock();
162                }
163            }
164            monitor.finishTask();
165        }
166    }
167
168    private class Parser extends DefaultHandler {
169        private Locator locator;
170
171        @Override
172        public void setDocumentLocator(Locator locator) {
173            this.locator = locator;
174        }
175
176        protected void throwException(String msg) throws XmlParsingException {
177            throw new XmlParsingException(msg).rememberLocation(locator);
178        }
179
180        @Override
181        public void startElement(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException {
182            try {
183                switch (qName) {
184                case "diffResult":
185                    // the root element, ignore
186                    break;
187                case "node":
188                case "way":
189                case "relation":
190                    PrimitiveId id = new SimplePrimitiveId(
191                            Long.parseLong(atts.getValue("old_id")),
192                            OsmPrimitiveType.fromApiTypeName(qName)
193                    );
194                    DiffResultEntry entry = new DiffResultEntry();
195                    if (atts.getValue("new_id") != null) {
196                        entry.newId = Long.parseLong(atts.getValue("new_id"));
197                    }
198                    if (atts.getValue("new_version") != null) {
199                        entry.newVersion = Integer.parseInt(atts.getValue("new_version"));
200                    }
201                    diffResults.put(id, entry);
202                    break;
203                default:
204                    throwException(tr("Unexpected XML element with name ''{0}''", qName));
205                }
206            } catch (NumberFormatException e) {
207                throw new XmlParsingException(e).rememberLocation(locator);
208            }
209        }
210    }
211
212    final Map<PrimitiveId, DiffResultEntry> getDiffResults() {
213        return new HashMap<>(diffResults);
214    }
215}