001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.io;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005import static org.openstreetmap.josm.tools.I18n.trn;
006
007import java.awt.BorderLayout;
008import java.awt.Component;
009import java.awt.Dimension;
010import java.awt.Graphics2D;
011import java.awt.GraphicsEnvironment;
012import java.awt.GridBagConstraints;
013import java.awt.GridBagLayout;
014import java.awt.Image;
015import java.awt.event.ActionEvent;
016import java.awt.event.WindowAdapter;
017import java.awt.event.WindowEvent;
018import java.awt.image.BufferedImage;
019import java.beans.PropertyChangeEvent;
020import java.beans.PropertyChangeListener;
021import java.util.ArrayList;
022import java.util.List;
023import java.util.concurrent.CancellationException;
024import java.util.concurrent.ExecutionException;
025import java.util.concurrent.ExecutorService;
026import java.util.concurrent.Executors;
027import java.util.concurrent.Future;
028
029import javax.swing.AbstractAction;
030import javax.swing.DefaultListCellRenderer;
031import javax.swing.ImageIcon;
032import javax.swing.JButton;
033import javax.swing.JDialog;
034import javax.swing.JLabel;
035import javax.swing.JList;
036import javax.swing.JOptionPane;
037import javax.swing.JPanel;
038import javax.swing.JScrollPane;
039import javax.swing.ListCellRenderer;
040import javax.swing.WindowConstants;
041import javax.swing.event.TableModelEvent;
042import javax.swing.event.TableModelListener;
043
044import org.openstreetmap.josm.actions.SessionSaveAsAction;
045import org.openstreetmap.josm.actions.UploadAction;
046import org.openstreetmap.josm.gui.ExceptionDialogUtil;
047import org.openstreetmap.josm.gui.MainApplication;
048import org.openstreetmap.josm.gui.io.SaveLayersModel.Mode;
049import org.openstreetmap.josm.gui.layer.AbstractModifiableLayer;
050import org.openstreetmap.josm.gui.layer.Layer;
051import org.openstreetmap.josm.gui.progress.ProgressMonitor;
052import org.openstreetmap.josm.gui.progress.swing.SwingRenderingProgressMonitor;
053import org.openstreetmap.josm.gui.util.GuiHelper;
054import org.openstreetmap.josm.gui.util.WindowGeometry;
055import org.openstreetmap.josm.tools.GBC;
056import org.openstreetmap.josm.tools.ImageProvider;
057import org.openstreetmap.josm.tools.ImageResource;
058import org.openstreetmap.josm.tools.InputMapUtils;
059import org.openstreetmap.josm.tools.Logging;
060import org.openstreetmap.josm.tools.UserCancelException;
061import org.openstreetmap.josm.tools.Utils;
062
063/**
064 * Dialog that pops up when the user closes a layer with modified data.
065 *
066 * It asks for confirmation that all modification should be discarded and offers
067 * to save the layers to file or upload to server, depending on the type of layer.
068 */
069public class SaveLayersDialog extends JDialog implements TableModelListener {
070
071    /**
072     * The cause for requesting an action on unsaved modifications
073     */
074    public enum Reason {
075        /** deleting a layer */
076        DELETE,
077        /** exiting JOSM */
078        EXIT,
079        /** restarting JOSM */
080        RESTART
081    }
082
083    private enum UserAction {
084        /** save/upload layers was successful, proceed with operation */
085        PROCEED,
086        /** save/upload of layers was not successful or user canceled operation */
087        CANCEL
088    }
089
090    private final SaveLayersModel model = new SaveLayersModel();
091    private UserAction action = UserAction.CANCEL;
092    private final UploadAndSaveProgressRenderer pnlUploadLayers = new UploadAndSaveProgressRenderer();
093
094    private final SaveAndProceedAction saveAndProceedAction = new SaveAndProceedAction();
095    private final SaveSessionAction saveSessionAction = new SaveSessionAction();
096    private final DiscardAndProceedAction discardAndProceedAction = new DiscardAndProceedAction();
097    private final CancelAction cancelAction = new CancelAction();
098    private transient SaveAndUploadTask saveAndUploadTask;
099
100    private final JButton saveAndProceedActionButton = new JButton(saveAndProceedAction);
101
102    /**
103     * Asks user to perform "save layer" operations (save on disk and/or upload data to server) before data layers deletion.
104     *
105     * @param selectedLayers The layers to check. Only instances of {@link AbstractModifiableLayer} are considered.
106     * @param reason the cause for requesting an action on unsaved modifications
107     * @return {@code true} if there was nothing to save, or if the user wants to proceed to save operations.
108     *         {@code false} if the user cancels.
109     * @since 11093
110     */
111    public static boolean saveUnsavedModifications(Iterable<? extends Layer> selectedLayers, Reason reason) {
112        if (!GraphicsEnvironment.isHeadless()) {
113            SaveLayersDialog dialog = new SaveLayersDialog(MainApplication.getMainFrame());
114            List<AbstractModifiableLayer> layersWithUnmodifiedChanges = new ArrayList<>();
115            for (Layer l: selectedLayers) {
116                if (!(l instanceof AbstractModifiableLayer)) {
117                    continue;
118                }
119                AbstractModifiableLayer odl = (AbstractModifiableLayer) l;
120                if (odl.isModified() &&
121                        ((!odl.isSavable() && !odl.isUploadable()) ||
122                                odl.requiresSaveToFile() ||
123                                (odl.requiresUploadToServer() && !odl.isUploadDiscouraged()))) {
124                    layersWithUnmodifiedChanges.add(odl);
125                }
126            }
127            dialog.prepareForSavingAndUpdatingLayers(reason);
128            if (!layersWithUnmodifiedChanges.isEmpty()) {
129                dialog.getModel().populate(layersWithUnmodifiedChanges);
130                dialog.setVisible(true);
131                switch(dialog.getUserAction()) {
132                    case PROCEED: return true;
133                    case CANCEL:
134                    default: return false;
135                }
136            }
137            dialog.closeDialog();
138        }
139
140        return true;
141    }
142
143    /**
144     * Constructs a new {@code SaveLayersDialog}.
145     * @param parent parent component
146     */
147    public SaveLayersDialog(Component parent) {
148        super(GuiHelper.getFrameForComponent(parent), ModalityType.DOCUMENT_MODAL);
149        build();
150    }
151
152    /**
153     * builds the GUI
154     */
155    protected void build() {
156        WindowGeometry geometry = WindowGeometry.centerOnScreen(new Dimension(650, 300));
157        geometry.applySafe(this);
158        getContentPane().setLayout(new BorderLayout());
159
160        SaveLayersTable table = new SaveLayersTable(model);
161        JScrollPane pane = new JScrollPane(table);
162        model.addPropertyChangeListener(table);
163        table.getModel().addTableModelListener(this);
164
165        getContentPane().add(pane, BorderLayout.CENTER);
166        getContentPane().add(buildButtonRow(), BorderLayout.SOUTH);
167
168        addWindowListener(new WindowClosingAdapter());
169        setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
170    }
171
172    /**
173     * builds the button row
174     *
175     * @return the panel with the button row
176     */
177    protected JPanel buildButtonRow() {
178        JPanel pnl = new JPanel(new GridBagLayout());
179
180        model.addPropertyChangeListener(saveAndProceedAction);
181        pnl.add(saveAndProceedActionButton, GBC.std(0, 0).insets(5, 5, 0, 0).fill(GBC.HORIZONTAL));
182
183        pnl.add(new JButton(saveSessionAction), GBC.std(1, 0).insets(5, 5, 5, 0).fill(GBC.HORIZONTAL));
184
185        model.addPropertyChangeListener(discardAndProceedAction);
186        pnl.add(new JButton(discardAndProceedAction), GBC.std(0, 1).insets(5, 5, 0, 5).fill(GBC.HORIZONTAL));
187
188        pnl.add(new JButton(cancelAction), GBC.std(1, 1).insets(5, 5, 5, 5).fill(GBC.HORIZONTAL));
189
190        JPanel pnl2 = new JPanel(new BorderLayout());
191        pnl2.add(pnlUploadLayers, BorderLayout.CENTER);
192        model.addPropertyChangeListener(pnlUploadLayers);
193        pnl2.add(pnl, BorderLayout.SOUTH);
194        return pnl2;
195    }
196
197    public void prepareForSavingAndUpdatingLayers(final Reason reason) {
198        switch (reason) {
199            case EXIT:
200                setTitle(tr("Unsaved changes - Save/Upload before exiting?"));
201                break;
202            case DELETE:
203                setTitle(tr("Unsaved changes - Save/Upload before deleting?"));
204                break;
205            case RESTART:
206                setTitle(tr("Unsaved changes - Save/Upload before restarting?"));
207                break;
208        }
209        this.saveAndProceedAction.initForReason(reason);
210        this.discardAndProceedAction.initForReason(reason);
211    }
212
213    public UserAction getUserAction() {
214        return this.action;
215    }
216
217    public SaveLayersModel getModel() {
218        return model;
219    }
220
221    protected void launchSafeAndUploadTask() {
222        ProgressMonitor monitor = new SwingRenderingProgressMonitor(pnlUploadLayers);
223        monitor.beginTask(tr("Uploading and saving modified layers ..."));
224        this.saveAndUploadTask = new SaveAndUploadTask(model, monitor);
225        new Thread(saveAndUploadTask, saveAndUploadTask.getClass().getName()).start();
226    }
227
228    protected void cancelSafeAndUploadTask() {
229        if (this.saveAndUploadTask != null) {
230            this.saveAndUploadTask.cancel();
231        }
232        model.setMode(Mode.EDITING_DATA);
233    }
234
235    private static class LayerListWarningMessagePanel extends JPanel {
236        static final class LayerCellRenderer implements ListCellRenderer<SaveLayerInfo> {
237            private final DefaultListCellRenderer def = new DefaultListCellRenderer();
238
239            @Override
240            public Component getListCellRendererComponent(JList<? extends SaveLayerInfo> list, SaveLayerInfo info, int index,
241                    boolean isSelected, boolean cellHasFocus) {
242                def.setIcon(info.getLayer().getIcon());
243                def.setText(info.getName());
244                return def;
245            }
246        }
247
248        private final JLabel lblMessage = new JLabel();
249        private final JList<SaveLayerInfo> lstLayers = new JList<>();
250
251        LayerListWarningMessagePanel(String msg, List<SaveLayerInfo> infos) {
252            super(new GridBagLayout());
253            build();
254            lblMessage.setText(msg);
255            lstLayers.setListData(infos.toArray(new SaveLayerInfo[0]));
256        }
257
258        protected void build() {
259            GridBagConstraints gc = new GridBagConstraints();
260            gc.gridx = 0;
261            gc.gridy = 0;
262            gc.fill = GridBagConstraints.HORIZONTAL;
263            gc.weightx = 1.0;
264            gc.weighty = 0.0;
265            add(lblMessage, gc);
266            lblMessage.setHorizontalAlignment(JLabel.LEFT);
267            lstLayers.setCellRenderer(new LayerCellRenderer());
268            gc.gridx = 0;
269            gc.gridy = 1;
270            gc.fill = GridBagConstraints.HORIZONTAL;
271            gc.weightx = 1.0;
272            gc.weighty = 1.0;
273            add(lstLayers, gc);
274        }
275    }
276
277    private static void warn(String msg, List<SaveLayerInfo> infos, String title) {
278        JPanel panel = new LayerListWarningMessagePanel(msg, infos);
279        JOptionPane.showConfirmDialog(MainApplication.getMainFrame(), panel, title, JOptionPane.DEFAULT_OPTION, JOptionPane.WARNING_MESSAGE);
280    }
281
282    protected static void warnLayersWithConflictsAndUploadRequest(List<SaveLayerInfo> infos) {
283        warn(trn("<html>{0} layer has unresolved conflicts.<br>"
284                + "Either resolve them first or discard the modifications.<br>"
285                + "Layer with conflicts:</html>",
286                "<html>{0} layers have unresolved conflicts.<br>"
287                + "Either resolve them first or discard the modifications.<br>"
288                + "Layers with conflicts:</html>",
289                infos.size(),
290                infos.size()),
291             infos, tr("Unsaved data and conflicts"));
292    }
293
294    protected static void warnLayersWithoutFilesAndSaveRequest(List<SaveLayerInfo> infos) {
295        warn(trn("<html>{0} layer needs saving but has no associated file.<br>"
296                + "Either select a file for this layer or discard the changes.<br>"
297                + "Layer without a file:</html>",
298                "<html>{0} layers need saving but have no associated file.<br>"
299                + "Either select a file for each of them or discard the changes.<br>"
300                + "Layers without a file:</html>",
301                infos.size(),
302                infos.size()),
303             infos, tr("Unsaved data and missing associated file"));
304    }
305
306    protected static void warnLayersWithIllegalFilesAndSaveRequest(List<SaveLayerInfo> infos) {
307        warn(trn("<html>{0} layer needs saving but has an associated file<br>"
308                + "which cannot be written.<br>"
309                + "Either select another file for this layer or discard the changes.<br>"
310                + "Layer with a non-writable file:</html>",
311                "<html>{0} layers need saving but have associated files<br>"
312                + "which cannot be written.<br>"
313                + "Either select another file for each of them or discard the changes.<br>"
314                + "Layers with non-writable files:</html>",
315                infos.size(),
316                infos.size()),
317             infos, tr("Unsaved data non-writable files"));
318    }
319
320    static boolean confirmSaveLayerInfosOK(SaveLayersModel model) {
321        List<SaveLayerInfo> layerInfos = model.getLayersWithConflictsAndUploadRequest();
322        if (!layerInfos.isEmpty()) {
323            warnLayersWithConflictsAndUploadRequest(layerInfos);
324            return false;
325        }
326
327        layerInfos = model.getLayersWithoutFilesAndSaveRequest();
328        if (!layerInfos.isEmpty()) {
329            warnLayersWithoutFilesAndSaveRequest(layerInfos);
330            return false;
331        }
332
333        layerInfos = model.getLayersWithIllegalFilesAndSaveRequest();
334        if (!layerInfos.isEmpty()) {
335            warnLayersWithIllegalFilesAndSaveRequest(layerInfos);
336            return false;
337        }
338
339        return true;
340    }
341
342    protected void setUserAction(UserAction action) {
343        this.action = action;
344    }
345
346    /**
347     * Closes this dialog and frees all native screen resources.
348     */
349    public void closeDialog() {
350        setVisible(false);
351        saveSessionAction.destroy();
352        dispose();
353    }
354
355    class WindowClosingAdapter extends WindowAdapter {
356        @Override
357        public void windowClosing(WindowEvent e) {
358            cancelAction.cancel();
359        }
360    }
361
362    class CancelAction extends AbstractAction {
363        CancelAction() {
364            putValue(NAME, tr("Cancel"));
365            putValue(SHORT_DESCRIPTION, tr("Close this dialog and resume editing in JOSM"));
366            ImageResource resource = new ImageProvider("cancel").setOptional(true).getResource();
367            if (resource != null) {
368                resource.attachImageIcon(this, true);
369            }
370            InputMapUtils.addEscapeAction(getRootPane(), this);
371        }
372
373        protected void cancelWhenInEditingModel() {
374            setUserAction(UserAction.CANCEL);
375            closeDialog();
376        }
377
378        public void cancel() {
379            switch(model.getMode()) {
380            case EDITING_DATA: cancelWhenInEditingModel();
381                break;
382            case UPLOADING_AND_SAVING: cancelSafeAndUploadTask();
383                break;
384            }
385        }
386
387        @Override
388        public void actionPerformed(ActionEvent e) {
389            cancel();
390        }
391    }
392
393    class DiscardAndProceedAction extends AbstractAction implements PropertyChangeListener {
394        DiscardAndProceedAction() {
395            initForReason(Reason.EXIT);
396        }
397
398        public void initForReason(Reason reason) {
399            switch (reason) {
400                case EXIT:
401                    putValue(NAME, tr("Exit now!"));
402                    putValue(SHORT_DESCRIPTION, tr("Exit JOSM without saving. Unsaved changes are lost."));
403                    attachImageIcon(new ImageProvider("exit"));
404                    break;
405                case RESTART:
406                    putValue(NAME, tr("Restart now!"));
407                    putValue(SHORT_DESCRIPTION, tr("Restart JOSM without saving. Unsaved changes are lost."));
408                    attachImageIcon(new ImageProvider("restart"));
409                    break;
410                case DELETE:
411                    putValue(NAME, tr("Delete now!"));
412                    putValue(SHORT_DESCRIPTION, tr("Delete layers without saving. Unsaved changes are lost."));
413                    attachImageIcon(new ImageProvider("dialogs", "delete"));
414                    break;
415            }
416        }
417
418        private void attachImageIcon(ImageProvider provider) {
419            ImageResource resource = provider.setOptional(true).getResource();
420            if (resource != null) {
421                resource.attachImageIcon(this, true);
422            }
423        }
424
425        @Override
426        public void actionPerformed(ActionEvent e) {
427            setUserAction(UserAction.PROCEED);
428            closeDialog();
429        }
430
431        @Override
432        public void propertyChange(PropertyChangeEvent evt) {
433            if (evt.getPropertyName().equals(SaveLayersModel.MODE_PROP)) {
434                Mode mode = (Mode) evt.getNewValue();
435                switch(mode) {
436                case EDITING_DATA: setEnabled(true);
437                    break;
438                case UPLOADING_AND_SAVING: setEnabled(false);
439                    break;
440                }
441            }
442        }
443    }
444
445    class SaveSessionAction extends SessionSaveAsAction {
446
447        SaveSessionAction() {
448            super(false, false);
449        }
450
451        @Override
452        public void actionPerformed(ActionEvent e) {
453            try {
454                saveSession();
455                setUserAction(UserAction.PROCEED);
456                closeDialog();
457            } catch (UserCancelException ignore) {
458                Logging.trace(ignore);
459            }
460        }
461    }
462
463    final class SaveAndProceedAction extends AbstractAction implements PropertyChangeListener {
464        private static final int ICON_SIZE = 24;
465        private static final String BASE_ICON = "BASE_ICON";
466        private final transient Image save = getImage("save", false);
467        private final transient Image upld = getImage("upload", false);
468        private final transient Image saveDis = getImage("save", true);
469        private final transient Image upldDis = getImage("upload", true);
470
471        SaveAndProceedAction() {
472            initForReason(Reason.EXIT);
473        }
474
475        Image getImage(String name, boolean disabled) {
476            ImageIcon img = new ImageProvider(name).setDisabled(disabled).setOptional(true).get();
477            return img != null ? img.getImage() : null;
478        }
479
480        public void initForReason(Reason reason) {
481            switch (reason) {
482                case EXIT:
483                    putValue(NAME, tr("Perform actions before exiting"));
484                    putValue(SHORT_DESCRIPTION, tr("Exit JOSM with saving. Unsaved changes are uploaded and/or saved."));
485                    putValue(BASE_ICON, ImageProvider.getIfAvailable("exit"));
486                    break;
487                case RESTART:
488                    putValue(NAME, tr("Perform actions before restarting"));
489                    putValue(SHORT_DESCRIPTION, tr("Restart JOSM with saving. Unsaved changes are uploaded and/or saved."));
490                    putValue(BASE_ICON, ImageProvider.getIfAvailable("restart"));
491                    break;
492                case DELETE:
493                    putValue(NAME, tr("Perform actions before deleting"));
494                    putValue(SHORT_DESCRIPTION, tr("Save/Upload layers before deleting. Unsaved changes are not lost."));
495                    putValue(BASE_ICON, ImageProvider.getIfAvailable("dialogs", "delete"));
496                    break;
497            }
498            redrawIcon();
499        }
500
501        public void redrawIcon() {
502            ImageIcon base = ((ImageIcon) getValue(BASE_ICON));
503            BufferedImage newIco = new BufferedImage(ICON_SIZE*3, ICON_SIZE, BufferedImage.TYPE_4BYTE_ABGR);
504            Graphics2D g = newIco.createGraphics();
505            // CHECKSTYLE.OFF: SingleSpaceSeparator
506            g.drawImage(model.getLayersToUpload().isEmpty() ? upldDis : upld, ICON_SIZE*0, 0, ICON_SIZE, ICON_SIZE, null);
507            g.drawImage(model.getLayersToSave().isEmpty()   ? saveDis : save, ICON_SIZE*1, 0, ICON_SIZE, ICON_SIZE, null);
508            if (base != null) {
509                g.drawImage(base.getImage(),                                  ICON_SIZE*2, 0, ICON_SIZE, ICON_SIZE, null);
510            }
511            // CHECKSTYLE.ON: SingleSpaceSeparator
512            putValue(SMALL_ICON, new ImageIcon(newIco));
513        }
514
515        @Override
516        public void actionPerformed(ActionEvent e) {
517            if (!confirmSaveLayerInfosOK(model))
518                return;
519            launchSafeAndUploadTask();
520        }
521
522        @Override
523        public void propertyChange(PropertyChangeEvent evt) {
524            if (evt.getPropertyName().equals(SaveLayersModel.MODE_PROP)) {
525                SaveLayersModel.Mode mode = (SaveLayersModel.Mode) evt.getNewValue();
526                switch(mode) {
527                case EDITING_DATA: setEnabled(true);
528                    break;
529                case UPLOADING_AND_SAVING: setEnabled(false);
530                    break;
531                }
532            }
533        }
534    }
535
536    /**
537     * This is the asynchronous task which uploads modified layers to the server and
538     * saves them to files, if requested by the user.
539     *
540     */
541    protected class SaveAndUploadTask implements Runnable {
542
543        private final SaveLayersModel model;
544        private final ProgressMonitor monitor;
545        private final ExecutorService worker;
546        private boolean canceled;
547        private AbstractIOTask currentTask;
548
549        public SaveAndUploadTask(SaveLayersModel model, ProgressMonitor monitor) {
550            this.model = model;
551            this.monitor = monitor;
552            this.worker = Executors.newSingleThreadExecutor(Utils.newThreadFactory(getClass() + "-%d", Thread.NORM_PRIORITY));
553        }
554
555        protected void uploadLayers(List<SaveLayerInfo> toUpload) {
556            for (final SaveLayerInfo layerInfo: toUpload) {
557                AbstractModifiableLayer layer = layerInfo.getLayer();
558                if (canceled) {
559                    model.setUploadState(layer, UploadOrSaveState.CANCELED);
560                    continue;
561                }
562                monitor.subTask(tr("Preparing layer ''{0}'' for upload ...", layerInfo.getName()));
563
564                if (!UploadAction.checkPreUploadConditions(layer)) {
565                    model.setUploadState(layer, UploadOrSaveState.FAILED);
566                    continue;
567                }
568
569                AbstractUploadDialog dialog = layer.getUploadDialog();
570                if (dialog != null) {
571                    dialog.setVisible(true);
572                    if (dialog.isCanceled()) {
573                        model.setUploadState(layer, UploadOrSaveState.CANCELED);
574                        continue;
575                    }
576                    dialog.rememberUserInput();
577                }
578
579                currentTask = layer.createUploadTask(monitor);
580                if (currentTask == null) {
581                    model.setUploadState(layer, UploadOrSaveState.FAILED);
582                    continue;
583                }
584                Future<?> currentFuture = worker.submit(currentTask);
585                try {
586                    // wait for the asynchronous task to complete
587                    currentFuture.get();
588                } catch (CancellationException e) {
589                    Logging.trace(e);
590                    model.setUploadState(layer, UploadOrSaveState.CANCELED);
591                } catch (InterruptedException | ExecutionException e) {
592                    Logging.error(e);
593                    model.setUploadState(layer, UploadOrSaveState.FAILED);
594                    ExceptionDialogUtil.explainException(e);
595                }
596                if (currentTask.isCanceled()) {
597                    model.setUploadState(layer, UploadOrSaveState.CANCELED);
598                } else if (currentTask.isFailed()) {
599                    Logging.error(currentTask.getLastException());
600                    ExceptionDialogUtil.explainException(currentTask.getLastException());
601                    model.setUploadState(layer, UploadOrSaveState.FAILED);
602                } else {
603                    model.setUploadState(layer, UploadOrSaveState.OK);
604                }
605                currentTask = null;
606            }
607        }
608
609        protected void saveLayers(List<SaveLayerInfo> toSave) {
610            for (final SaveLayerInfo layerInfo: toSave) {
611                if (canceled) {
612                    model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.CANCELED);
613                    continue;
614                }
615                // Check save preconditions earlier to avoid a blocking reentring call to EDT (see #10086)
616                if (layerInfo.isDoCheckSaveConditions()) {
617                    if (!layerInfo.getLayer().checkSaveConditions()) {
618                        continue;
619                    }
620                    layerInfo.setDoCheckSaveConditions(false);
621                }
622                currentTask = new SaveLayerTask(layerInfo, monitor);
623                Future<?> currentFuture = worker.submit(currentTask);
624
625                try {
626                    // wait for the asynchronous task to complete
627                    //
628                    currentFuture.get();
629                } catch (CancellationException e) {
630                    Logging.trace(e);
631                    model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.CANCELED);
632                } catch (InterruptedException | ExecutionException e) {
633                    Logging.error(e);
634                    model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.FAILED);
635                    ExceptionDialogUtil.explainException(e);
636                }
637                if (currentTask.isCanceled()) {
638                    model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.CANCELED);
639                } else if (currentTask.isFailed()) {
640                    if (currentTask.getLastException() != null) {
641                        Logging.error(currentTask.getLastException());
642                        ExceptionDialogUtil.explainException(currentTask.getLastException());
643                    }
644                    model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.FAILED);
645                } else {
646                    model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.OK);
647                }
648                this.currentTask = null;
649            }
650        }
651
652        protected void warnBecauseOfUnsavedData() {
653            int numProblems = model.getNumCancel() + model.getNumFailed();
654            if (numProblems == 0)
655                return;
656            Logging.warn(numProblems + " problems occurred during upload/save");
657            String msg = trn(
658                    "<html>An upload and/or save operation of one layer with modifications<br>"
659                    + "was canceled or has failed.</html>",
660                    "<html>Upload and/or save operations of {0} layers with modifications<br>"
661                    + "were canceled or have failed.</html>",
662                    numProblems,
663                    numProblems
664            );
665            JOptionPane.showMessageDialog(
666                    MainApplication.getMainFrame(),
667                    msg,
668                    tr("Incomplete upload and/or save"),
669                    JOptionPane.WARNING_MESSAGE
670            );
671        }
672
673        @Override
674        public void run() {
675            GuiHelper.runInEDTAndWait(() -> {
676                model.setMode(SaveLayersModel.Mode.UPLOADING_AND_SAVING);
677                List<SaveLayerInfo> toUpload = model.getLayersToUpload();
678                if (!toUpload.isEmpty()) {
679                    uploadLayers(toUpload);
680                }
681                List<SaveLayerInfo> toSave = model.getLayersToSave();
682                if (!toSave.isEmpty()) {
683                    saveLayers(toSave);
684                }
685                model.setMode(SaveLayersModel.Mode.EDITING_DATA);
686                if (model.hasUnsavedData()) {
687                    warnBecauseOfUnsavedData();
688                    model.setMode(Mode.EDITING_DATA);
689                    if (canceled) {
690                        setUserAction(UserAction.CANCEL);
691                        closeDialog();
692                    }
693                } else {
694                    setUserAction(UserAction.PROCEED);
695                    closeDialog();
696                }
697            });
698            worker.shutdownNow();
699        }
700
701        public void cancel() {
702            if (currentTask != null) {
703                currentTask.cancel();
704            }
705            worker.shutdown();
706            canceled = true;
707        }
708    }
709
710    @Override
711    public void tableChanged(TableModelEvent e) {
712        boolean dis = model.getLayersToSave().isEmpty() && model.getLayersToUpload().isEmpty();
713        if (saveAndProceedActionButton != null) {
714            saveAndProceedActionButton.setEnabled(!dis);
715        }
716        saveAndProceedAction.redrawIcon();
717    }
718}