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