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