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