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