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