001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.actions;
003
004import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
005import static org.openstreetmap.josm.tools.I18n.tr;
006
007import java.awt.event.ActionEvent;
008import java.awt.event.KeyEvent;
009import java.io.IOException;
010import java.util.Collection;
011import java.util.HashSet;
012import java.util.Set;
013import java.util.Stack;
014
015import javax.swing.JOptionPane;
016import javax.swing.SwingUtilities;
017
018import org.openstreetmap.josm.Main;
019import org.openstreetmap.josm.data.APIDataSet;
020import org.openstreetmap.josm.data.osm.Changeset;
021import org.openstreetmap.josm.data.osm.DataSet;
022import org.openstreetmap.josm.data.osm.Node;
023import org.openstreetmap.josm.data.osm.OsmPrimitive;
024import org.openstreetmap.josm.data.osm.Relation;
025import org.openstreetmap.josm.data.osm.Way;
026import org.openstreetmap.josm.data.osm.visitor.Visitor;
027import org.openstreetmap.josm.gui.DefaultNameFormatter;
028import org.openstreetmap.josm.gui.PleaseWaitRunnable;
029import org.openstreetmap.josm.gui.io.UploadSelectionDialog;
030import org.openstreetmap.josm.gui.layer.OsmDataLayer;
031import org.openstreetmap.josm.io.OsmServerBackreferenceReader;
032import org.openstreetmap.josm.io.OsmTransferException;
033import org.openstreetmap.josm.tools.CheckParameterUtil;
034import org.openstreetmap.josm.tools.ExceptionUtil;
035import org.openstreetmap.josm.tools.Shortcut;
036import org.xml.sax.SAXException;
037
038/**
039 * Uploads the current selection to the server.
040 * @since 2250
041 */
042public class UploadSelectionAction extends JosmAction {
043    /**
044     * Constructs a new {@code UploadSelectionAction}.
045     */
046    public UploadSelectionAction() {
047        super(
048                tr("Upload selection"),
049                "uploadselection",
050                tr("Upload all changes in the current selection to the OSM server."),
051                // CHECKSTYLE.OFF: LineLength
052                Shortcut.registerShortcut("file:uploadSelection", tr("File: {0}", tr("Upload selection")), KeyEvent.VK_U, Shortcut.ALT_CTRL_SHIFT),
053                // CHECKSTYLE.ON: LineLength
054                true);
055        putValue("help", ht("/Action/UploadSelection"));
056    }
057
058    @Override
059    protected void updateEnabledState() {
060        if (getCurrentDataSet() == null) {
061            setEnabled(false);
062        } else {
063            updateEnabledState(getCurrentDataSet().getAllSelected());
064        }
065    }
066
067    @Override
068    protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
069        setEnabled(selection != null && !selection.isEmpty());
070    }
071
072    protected Set<OsmPrimitive> getDeletedPrimitives(DataSet ds) {
073        Set<OsmPrimitive> ret = new HashSet<>();
074        for (OsmPrimitive p: ds.allPrimitives()) {
075            if (p.isDeleted() && !p.isNew() && p.isVisible() && p.isModified()) {
076                ret.add(p);
077            }
078        }
079        return ret;
080    }
081
082    protected Set<OsmPrimitive> getModifiedPrimitives(Collection<OsmPrimitive> primitives) {
083        Set<OsmPrimitive> ret = new HashSet<>();
084        for (OsmPrimitive p: primitives) {
085            if (p.isNewOrUndeleted()) {
086                ret.add(p);
087            } else if (p.isModified() && !p.isIncomplete()) {
088                ret.add(p);
089            }
090        }
091        return ret;
092    }
093
094    @Override
095    public void actionPerformed(ActionEvent e) {
096        if (!isEnabled())
097            return;
098        if (getEditLayer().isUploadDiscouraged()) {
099            if (UploadAction.warnUploadDiscouraged(getEditLayer())) {
100                return;
101            }
102        }
103        UploadHullBuilder builder = new UploadHullBuilder();
104        UploadSelectionDialog dialog = new UploadSelectionDialog();
105        Collection<OsmPrimitive> modifiedCandidates = getModifiedPrimitives(getEditLayer().data.getAllSelected());
106        Collection<OsmPrimitive> deletedCandidates = getDeletedPrimitives(getEditLayer().data);
107        if (modifiedCandidates.isEmpty() && deletedCandidates.isEmpty()) {
108            JOptionPane.showMessageDialog(
109                    Main.parent,
110                    tr("No changes to upload."),
111                    tr("Warning"),
112                    JOptionPane.INFORMATION_MESSAGE
113            );
114            return;
115        }
116        dialog.populate(
117                modifiedCandidates,
118                deletedCandidates
119        );
120        dialog.setVisible(true);
121        if (dialog.isCanceled())
122            return;
123        Collection<OsmPrimitive> toUpload = builder.build(dialog.getSelectedPrimitives());
124        if (toUpload.isEmpty()) {
125            JOptionPane.showMessageDialog(
126                    Main.parent,
127                    tr("No changes to upload."),
128                    tr("Warning"),
129                    JOptionPane.INFORMATION_MESSAGE
130            );
131            return;
132        }
133        uploadPrimitives(getEditLayer(), toUpload);
134    }
135
136    /**
137     * Replies true if there is at least one non-new, deleted primitive in
138     * <code>primitives</code>
139     *
140     * @param primitives the primitives to scan
141     * @return true if there is at least one non-new, deleted primitive in
142     * <code>primitives</code>
143     */
144    protected boolean hasPrimitivesToDelete(Collection<OsmPrimitive> primitives) {
145        for (OsmPrimitive p: primitives) {
146            if (p.isDeleted() && p.isModified() && !p.isNew())
147                return true;
148        }
149        return false;
150    }
151
152    /**
153     * Uploads the primitives in <code>toUpload</code> to the server. Only
154     * uploads primitives which are either new, modified or deleted.
155     *
156     * Also checks whether <code>toUpload</code> has to be extended with
157     * deleted parents in order to avoid precondition violations on the server.
158     *
159     * @param layer the data layer from which we upload a subset of primitives
160     * @param toUpload the primitives to upload. If null or empty returns immediatelly
161     */
162    public void uploadPrimitives(OsmDataLayer layer, Collection<OsmPrimitive> toUpload) {
163        if (toUpload == null || toUpload.isEmpty()) return;
164        UploadHullBuilder builder = new UploadHullBuilder();
165        toUpload = builder.build(toUpload);
166        if (hasPrimitivesToDelete(toUpload)) {
167            // runs the check for deleted parents and then invokes
168            // processPostParentChecker()
169            //
170            Main.worker.submit(new DeletedParentsChecker(layer, toUpload));
171        } else {
172            processPostParentChecker(layer, toUpload);
173        }
174    }
175
176    protected void processPostParentChecker(OsmDataLayer layer, Collection<OsmPrimitive> toUpload) {
177        APIDataSet ds = new APIDataSet(toUpload);
178        UploadAction action = new UploadAction();
179        action.uploadData(layer, ds);
180    }
181
182    /**
183     * Computes the collection of primitives to upload, given a collection of candidate
184     * primitives.
185     * Some of the candidates are excluded, i.e. if they aren't modified.
186     * Other primitives are added. A typical case is a primitive which is new and and
187     * which is referred by a modified relation. In order to upload the relation the
188     * new primitive has to be uploaded as well, even if it isn't included in the
189     * list of candidate primitives.
190     *
191     */
192    static class UploadHullBuilder implements Visitor {
193        private Set<OsmPrimitive> hull;
194
195        UploadHullBuilder() {
196            hull = new HashSet<>();
197        }
198
199        @Override
200        public void visit(Node n) {
201            if (n.isNewOrUndeleted() || n.isModified() || n.isDeleted()) {
202                // upload new nodes as well as modified and deleted ones
203                hull.add(n);
204            }
205        }
206
207        @Override
208        public void visit(Way w) {
209            if (w.isNewOrUndeleted() || w.isModified() || w.isDeleted()) {
210                // upload new ways as well as modified and deleted ones
211                hull.add(w);
212                for (Node n: w.getNodes()) {
213                    // we upload modified nodes even if they aren't in the current
214                    // selection.
215                    n.accept(this);
216                }
217            }
218        }
219
220        @Override
221        public void visit(Relation r) {
222            if (r.isNewOrUndeleted() || r.isModified() || r.isDeleted()) {
223                hull.add(r);
224                for (OsmPrimitive p : r.getMemberPrimitives()) {
225                    // add new relation members. Don't include modified
226                    // relation members. r shouldn't refer to deleted primitives,
227                    // so wont check here for deleted primitives here
228                    //
229                    if (p.isNewOrUndeleted()) {
230                        p.accept(this);
231                    }
232                }
233            }
234        }
235
236        @Override
237        public void visit(Changeset cs) {
238            // do nothing
239        }
240
241        /**
242         * Builds the "hull" of primitives to be uploaded given a base collection
243         * of osm primitives.
244         *
245         * @param base the base collection. Must not be null.
246         * @return the "hull"
247         * @throws IllegalArgumentException if base is null
248         */
249        public Set<OsmPrimitive> build(Collection<OsmPrimitive> base) {
250            CheckParameterUtil.ensureParameterNotNull(base, "base");
251            hull = new HashSet<>();
252            for (OsmPrimitive p: base) {
253                p.accept(this);
254            }
255            return hull;
256        }
257    }
258
259    class DeletedParentsChecker extends PleaseWaitRunnable {
260        private boolean canceled;
261        private Exception lastException;
262        private Collection<OsmPrimitive> toUpload;
263        private OsmDataLayer layer;
264        private OsmServerBackreferenceReader reader;
265
266        /**
267         *
268         * @param layer the data layer for which a collection of selected primitives is uploaded
269         * @param toUpload the collection of primitives to upload
270         */
271        DeletedParentsChecker(OsmDataLayer layer, Collection<OsmPrimitive> toUpload) {
272            super(tr("Checking parents for deleted objects"));
273            this.toUpload = toUpload;
274            this.layer = layer;
275        }
276
277        @Override
278        protected void cancel() {
279            this.canceled = true;
280            synchronized (this) {
281                if (reader != null) {
282                    reader.cancel();
283                }
284            }
285        }
286
287        @Override
288        protected void finish() {
289            if (canceled)
290                return;
291            if (lastException != null) {
292                ExceptionUtil.explainException(lastException);
293                return;
294            }
295            Runnable r = new Runnable() {
296                @Override
297                public void run() {
298                    processPostParentChecker(layer, toUpload);
299                }
300            };
301            SwingUtilities.invokeLater(r);
302        }
303
304        /**
305         * Replies the collection of deleted OSM primitives for which we have to check whether
306         * there are dangling references on the server.
307         *
308         * @return primitives to check
309         */
310        protected Set<OsmPrimitive> getPrimitivesToCheckForParents() {
311            Set<OsmPrimitive> ret = new HashSet<>();
312            for (OsmPrimitive p: toUpload) {
313                if (p.isDeleted() && !p.isNewOrUndeleted()) {
314                    ret.add(p);
315                }
316            }
317            return ret;
318        }
319
320        @Override
321        protected void realRun() throws SAXException, IOException, OsmTransferException {
322            try {
323                Stack<OsmPrimitive> toCheck = new Stack<>();
324                toCheck.addAll(getPrimitivesToCheckForParents());
325                Set<OsmPrimitive> checked = new HashSet<>();
326                while (!toCheck.isEmpty()) {
327                    if (canceled) return;
328                    OsmPrimitive current = toCheck.pop();
329                    synchronized (this) {
330                        reader = new OsmServerBackreferenceReader(current);
331                    }
332                    getProgressMonitor().subTask(tr("Reading parents of ''{0}''", current.getDisplayName(DefaultNameFormatter.getInstance())));
333                    DataSet ds = reader.parseOsm(getProgressMonitor().createSubTaskMonitor(1, false));
334                    synchronized (this) {
335                        reader = null;
336                    }
337                    checked.add(current);
338                    getProgressMonitor().subTask(tr("Checking for deleted parents in the local dataset"));
339                    for (OsmPrimitive p: ds.allPrimitives()) {
340                        if (canceled) return;
341                        OsmPrimitive myDeletedParent = layer.data.getPrimitiveById(p);
342                        // our local dataset includes a deleted parent of a primitive we want
343                        // to delete. Include this parent in the collection of uploaded primitives
344                        //
345                        if (myDeletedParent != null && myDeletedParent.isDeleted()) {
346                            if (!toUpload.contains(myDeletedParent)) {
347                                toUpload.add(myDeletedParent);
348                            }
349                            if (!checked.contains(myDeletedParent)) {
350                                toCheck.push(myDeletedParent);
351                            }
352                        }
353                    }
354                }
355            } catch (Exception e) {
356                if (canceled)
357                    // ignore exception
358                    return;
359                lastException = e;
360            }
361        }
362    }
363}