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        DataSet ds = getLayerManager().getEditDataSet();
061        if (ds == null) {
062            setEnabled(false);
063        } else {
064            updateEnabledState(ds.getAllSelected());
065        }
066    }
067
068    @Override
069    protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
070        setEnabled(selection != null && !selection.isEmpty());
071    }
072
073    protected Set<OsmPrimitive> getDeletedPrimitives(DataSet ds) {
074        Set<OsmPrimitive> ret = new HashSet<>();
075        for (OsmPrimitive p: ds.allPrimitives()) {
076            if (p.isDeleted() && !p.isNew() && p.isVisible() && p.isModified()) {
077                ret.add(p);
078            }
079        }
080        return ret;
081    }
082
083    protected Set<OsmPrimitive> getModifiedPrimitives(Collection<OsmPrimitive> primitives) {
084        Set<OsmPrimitive> ret = new HashSet<>();
085        for (OsmPrimitive p: primitives) {
086            if (p.isNewOrUndeleted()) {
087                ret.add(p);
088            } else if (p.isModified() && !p.isIncomplete()) {
089                ret.add(p);
090            }
091        }
092        return ret;
093    }
094
095    @Override
096    public void actionPerformed(ActionEvent e) {
097        OsmDataLayer editLayer = getLayerManager().getEditLayer();
098        if (!isEnabled())
099            return;
100        if (editLayer.isUploadDiscouraged()) {
101            if (UploadAction.warnUploadDiscouraged(editLayer)) {
102                return;
103            }
104        }
105        Collection<OsmPrimitive> modifiedCandidates = getModifiedPrimitives(editLayer.data.getAllSelected());
106        Collection<OsmPrimitive> deletedCandidates = getDeletedPrimitives(editLayer.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        UploadSelectionDialog dialog = new UploadSelectionDialog();
117        dialog.populate(
118                modifiedCandidates,
119                deletedCandidates
120        );
121        dialog.setVisible(true);
122        if (dialog.isCanceled())
123            return;
124        Collection<OsmPrimitive> toUpload = new UploadHullBuilder().build(dialog.getSelectedPrimitives());
125        if (toUpload.isEmpty()) {
126            JOptionPane.showMessageDialog(
127                    Main.parent,
128                    tr("No changes to upload."),
129                    tr("Warning"),
130                    JOptionPane.INFORMATION_MESSAGE
131            );
132            return;
133        }
134        uploadPrimitives(editLayer, toUpload);
135    }
136
137    /**
138     * Replies true if there is at least one non-new, deleted primitive in
139     * <code>primitives</code>
140     *
141     * @param primitives the primitives to scan
142     * @return true if there is at least one non-new, deleted primitive in
143     * <code>primitives</code>
144     */
145    protected boolean hasPrimitivesToDelete(Collection<OsmPrimitive> primitives) {
146        for (OsmPrimitive p: primitives) {
147            if (p.isDeleted() && p.isModified() && !p.isNew())
148                return true;
149        }
150        return false;
151    }
152
153    /**
154     * Uploads the primitives in <code>toUpload</code> to the server. Only
155     * uploads primitives which are either new, modified or deleted.
156     *
157     * Also checks whether <code>toUpload</code> has to be extended with
158     * deleted parents in order to avoid precondition violations on the server.
159     *
160     * @param layer the data layer from which we upload a subset of primitives
161     * @param toUpload the primitives to upload. If null or empty returns immediatelly
162     */
163    public void uploadPrimitives(OsmDataLayer layer, Collection<OsmPrimitive> toUpload) {
164        if (toUpload == null || toUpload.isEmpty()) return;
165        UploadHullBuilder builder = new UploadHullBuilder();
166        toUpload = builder.build(toUpload);
167        if (hasPrimitivesToDelete(toUpload)) {
168            // runs the check for deleted parents and then invokes
169            // processPostParentChecker()
170            //
171            Main.worker.submit(new DeletedParentsChecker(layer, toUpload));
172        } else {
173            processPostParentChecker(layer, toUpload);
174        }
175    }
176
177    protected void processPostParentChecker(OsmDataLayer layer, Collection<OsmPrimitive> toUpload) {
178        APIDataSet ds = new APIDataSet(toUpload);
179        UploadAction action = new UploadAction();
180        action.uploadData(layer, ds);
181    }
182
183    /**
184     * Computes the collection of primitives to upload, given a collection of candidate
185     * primitives.
186     * Some of the candidates are excluded, i.e. if they aren't modified.
187     * Other primitives are added. A typical case is a primitive which is new and and
188     * which is referred by a modified relation. In order to upload the relation the
189     * new primitive has to be uploaded as well, even if it isn't included in the
190     * list of candidate primitives.
191     *
192     */
193    static class UploadHullBuilder implements Visitor {
194        private Set<OsmPrimitive> hull;
195
196        UploadHullBuilder() {
197            hull = new HashSet<>();
198        }
199
200        @Override
201        public void visit(Node n) {
202            if (n.isNewOrUndeleted() || n.isModified() || n.isDeleted()) {
203                // upload new nodes as well as modified and deleted ones
204                hull.add(n);
205            }
206        }
207
208        @Override
209        public void visit(Way w) {
210            if (w.isNewOrUndeleted() || w.isModified() || w.isDeleted()) {
211                // upload new ways as well as modified and deleted ones
212                hull.add(w);
213                for (Node n: w.getNodes()) {
214                    // we upload modified nodes even if they aren't in the current
215                    // selection.
216                    n.accept(this);
217                }
218            }
219        }
220
221        @Override
222        public void visit(Relation r) {
223            if (r.isNewOrUndeleted() || r.isModified() || r.isDeleted()) {
224                hull.add(r);
225                for (OsmPrimitive p : r.getMemberPrimitives()) {
226                    // add new relation members. Don't include modified
227                    // relation members. r shouldn't refer to deleted primitives,
228                    // so wont check here for deleted primitives here
229                    //
230                    if (p.isNewOrUndeleted()) {
231                        p.accept(this);
232                    }
233                }
234            }
235        }
236
237        @Override
238        public void visit(Changeset cs) {
239            // do nothing
240        }
241
242        /**
243         * Builds the "hull" of primitives to be uploaded given a base collection
244         * of osm primitives.
245         *
246         * @param base the base collection. Must not be null.
247         * @return the "hull"
248         * @throws IllegalArgumentException if base is null
249         */
250        public Set<OsmPrimitive> build(Collection<OsmPrimitive> base) {
251            CheckParameterUtil.ensureParameterNotNull(base, "base");
252            hull = new HashSet<>();
253            for (OsmPrimitive p: base) {
254                p.accept(this);
255            }
256            return hull;
257        }
258    }
259
260    class DeletedParentsChecker extends PleaseWaitRunnable {
261        private boolean canceled;
262        private Exception lastException;
263        private final Collection<OsmPrimitive> toUpload;
264        private final OsmDataLayer layer;
265        private OsmServerBackreferenceReader reader;
266
267        /**
268         *
269         * @param layer the data layer for which a collection of selected primitives is uploaded
270         * @param toUpload the collection of primitives to upload
271         */
272        DeletedParentsChecker(OsmDataLayer layer, Collection<OsmPrimitive> toUpload) {
273            super(tr("Checking parents for deleted objects"));
274            this.toUpload = toUpload;
275            this.layer = layer;
276        }
277
278        @Override
279        protected void cancel() {
280            this.canceled = true;
281            synchronized (this) {
282                if (reader != null) {
283                    reader.cancel();
284                }
285            }
286        }
287
288        @Override
289        protected void finish() {
290            if (canceled)
291                return;
292            if (lastException != null) {
293                ExceptionUtil.explainException(lastException);
294                return;
295            }
296            Runnable r = new Runnable() {
297                @Override
298                public void run() {
299                    processPostParentChecker(layer, toUpload);
300                }
301            };
302            SwingUtilities.invokeLater(r);
303        }
304
305        /**
306         * Replies the collection of deleted OSM primitives for which we have to check whether
307         * there are dangling references on the server.
308         *
309         * @return primitives to check
310         */
311        protected Set<OsmPrimitive> getPrimitivesToCheckForParents() {
312            Set<OsmPrimitive> ret = new HashSet<>();
313            for (OsmPrimitive p: toUpload) {
314                if (p.isDeleted() && !p.isNewOrUndeleted()) {
315                    ret.add(p);
316                }
317            }
318            return ret;
319        }
320
321        @Override
322        protected void realRun() throws SAXException, IOException, OsmTransferException {
323            try {
324                Stack<OsmPrimitive> toCheck = new Stack<>();
325                toCheck.addAll(getPrimitivesToCheckForParents());
326                Set<OsmPrimitive> checked = new HashSet<>();
327                while (!toCheck.isEmpty()) {
328                    if (canceled) return;
329                    OsmPrimitive current = toCheck.pop();
330                    synchronized (this) {
331                        reader = new OsmServerBackreferenceReader(current);
332                    }
333                    getProgressMonitor().subTask(tr("Reading parents of ''{0}''", current.getDisplayName(DefaultNameFormatter.getInstance())));
334                    DataSet ds = reader.parseOsm(getProgressMonitor().createSubTaskMonitor(1, false));
335                    synchronized (this) {
336                        reader = null;
337                    }
338                    checked.add(current);
339                    getProgressMonitor().subTask(tr("Checking for deleted parents in the local dataset"));
340                    for (OsmPrimitive p: ds.allPrimitives()) {
341                        if (canceled) return;
342                        OsmPrimitive myDeletedParent = layer.data.getPrimitiveById(p);
343                        // our local dataset includes a deleted parent of a primitive we want
344                        // to delete. Include this parent in the collection of uploaded primitives
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 (OsmTransferException e) {
356                if (canceled)
357                    // ignore exception
358                    return;
359                lastException = e;
360            }
361        }
362    }
363}