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