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.event.ActionEvent;
015import java.awt.event.WindowAdapter;
016import java.awt.event.WindowEvent;
017import java.awt.image.BufferedImage;
018import java.beans.PropertyChangeEvent;
019import java.beans.PropertyChangeListener;
020import java.util.ArrayList;
021import java.util.List;
022import java.util.concurrent.CancellationException;
023import java.util.concurrent.ExecutionException;
024import java.util.concurrent.ExecutorService;
025import java.util.concurrent.Executors;
026import java.util.concurrent.Future;
027
028import javax.swing.AbstractAction;
029import javax.swing.DefaultListCellRenderer;
030import javax.swing.ImageIcon;
031import javax.swing.JButton;
032import javax.swing.JDialog;
033import javax.swing.JLabel;
034import javax.swing.JList;
035import javax.swing.JOptionPane;
036import javax.swing.JPanel;
037import javax.swing.JScrollPane;
038import javax.swing.ListCellRenderer;
039import javax.swing.WindowConstants;
040import javax.swing.event.TableModelEvent;
041import javax.swing.event.TableModelListener;
042
043import org.openstreetmap.josm.actions.SessionSaveAsAction;
044import org.openstreetmap.josm.actions.UploadAction;
045import org.openstreetmap.josm.gui.ExceptionDialogUtil;
046import org.openstreetmap.josm.gui.MainApplication;
047import org.openstreetmap.josm.gui.io.SaveLayersModel.Mode;
048import org.openstreetmap.josm.gui.layer.AbstractModifiableLayer;
049import org.openstreetmap.josm.gui.layer.Layer;
050import org.openstreetmap.josm.gui.progress.ProgressMonitor;
051import org.openstreetmap.josm.gui.progress.swing.SwingRenderingProgressMonitor;
052import org.openstreetmap.josm.gui.util.GuiHelper;
053import org.openstreetmap.josm.gui.util.WindowGeometry;
054import org.openstreetmap.josm.tools.GBC;
055import org.openstreetmap.josm.tools.ImageProvider;
056import org.openstreetmap.josm.tools.ImageProvider.ImageSizes;
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.LEADING);
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            new ImageProvider("cancel").getResource().attachImageIcon(this, true);
367            InputMapUtils.addEscapeAction(getRootPane(), this);
368        }
369
370        protected void cancelWhenInEditingModel() {
371            setUserAction(UserAction.CANCEL);
372            closeDialog();
373        }
374
375        public void cancel() {
376            switch(model.getMode()) {
377            case EDITING_DATA: cancelWhenInEditingModel();
378                break;
379            case UPLOADING_AND_SAVING: cancelSafeAndUploadTask();
380                break;
381            }
382        }
383
384        @Override
385        public void actionPerformed(ActionEvent e) {
386            cancel();
387        }
388    }
389
390    class DiscardAndProceedAction extends AbstractAction implements PropertyChangeListener {
391        DiscardAndProceedAction() {
392            initForReason(Reason.EXIT);
393        }
394
395        public void initForReason(Reason reason) {
396            switch (reason) {
397                case EXIT:
398                    putValue(NAME, tr("Exit now!"));
399                    putValue(SHORT_DESCRIPTION, tr("Exit JOSM without saving. Unsaved changes are lost."));
400                    new ImageProvider("exit").getResource().attachImageIcon(this, true);
401                    break;
402                case RESTART:
403                    putValue(NAME, tr("Restart now!"));
404                    putValue(SHORT_DESCRIPTION, tr("Restart JOSM without saving. Unsaved changes are lost."));
405                    new ImageProvider("restart").getResource().attachImageIcon(this, true);
406                    break;
407                case DELETE:
408                    putValue(NAME, tr("Delete now!"));
409                    putValue(SHORT_DESCRIPTION, tr("Delete layers without saving. Unsaved changes are lost."));
410                    new ImageProvider("dialogs", "delete").getResource().attachImageIcon(this, true);
411                    break;
412            }
413        }
414
415        @Override
416        public void actionPerformed(ActionEvent e) {
417            setUserAction(UserAction.PROCEED);
418            closeDialog();
419        }
420
421        @Override
422        public void propertyChange(PropertyChangeEvent evt) {
423            if (evt.getPropertyName().equals(SaveLayersModel.MODE_PROP)) {
424                Mode mode = (Mode) evt.getNewValue();
425                switch(mode) {
426                case EDITING_DATA: setEnabled(true);
427                    break;
428                case UPLOADING_AND_SAVING: setEnabled(false);
429                    break;
430                }
431            }
432        }
433    }
434
435    class SaveSessionAction extends SessionSaveAsAction {
436
437        SaveSessionAction() {
438            super(false, false);
439        }
440
441        @Override
442        public void actionPerformed(ActionEvent e) {
443            try {
444                saveSession();
445                setUserAction(UserAction.PROCEED);
446                closeDialog();
447            } catch (UserCancelException ignore) {
448                Logging.trace(ignore);
449            }
450        }
451    }
452
453    final class SaveAndProceedAction extends AbstractAction implements PropertyChangeListener {
454
455        private ImageResource actionImg;
456
457        SaveAndProceedAction() {
458            initForReason(Reason.EXIT);
459        }
460
461        ImageResource getImage(String name, boolean disabled) {
462            return new ImageProvider(name).setDisabled(disabled).setOptional(true).getResource();
463        }
464
465        public void initForReason(Reason reason) {
466            switch (reason) {
467                case EXIT:
468                    putValue(NAME, tr("Perform actions before exiting"));
469                    putValue(SHORT_DESCRIPTION, tr("Exit JOSM with saving. Unsaved changes are uploaded and/or saved."));
470                    actionImg = new ImageProvider("exit").getResource();
471                    break;
472                case RESTART:
473                    putValue(NAME, tr("Perform actions before restarting"));
474                    putValue(SHORT_DESCRIPTION, tr("Restart JOSM with saving. Unsaved changes are uploaded and/or saved."));
475                    actionImg = new ImageProvider("restart").getResource();
476                    break;
477                case DELETE:
478                    putValue(NAME, tr("Perform actions before deleting"));
479                    putValue(SHORT_DESCRIPTION, tr("Save/Upload layers before deleting. Unsaved changes are not lost."));
480                    actionImg = new ImageProvider("dialogs", "delete").getResource();
481                    break;
482            }
483            redrawIcon();
484        }
485
486        public void redrawIcon() {
487            ImageResource uploadImg = model.getLayersToUpload().isEmpty() ? getImage("upload", true) : getImage("upload", false);
488            ImageResource saveImg = model.getLayersToSave().isEmpty() ? getImage("save", true) : getImage("save", false);
489            attachImageIcon(SMALL_ICON, ImageSizes.SMALLICON, uploadImg, saveImg, actionImg);
490            attachImageIcon(LARGE_ICON_KEY, ImageSizes.LARGEICON, uploadImg, saveImg, actionImg);
491        }
492
493        private void attachImageIcon(String key, ImageSizes size, ImageResource uploadImg, ImageResource saveImg, ImageResource actionImg) {
494            Dimension dim = size.getImageDimension();
495            BufferedImage newIco = new BufferedImage(((int) dim.getWidth())*3, (int) dim.getHeight(), BufferedImage.TYPE_4BYTE_ABGR);
496            Graphics2D g = newIco.createGraphics();
497            drawImageIcon(g, 0, dim, uploadImg);
498            drawImageIcon(g, 1, dim, saveImg);
499            drawImageIcon(g, 2, dim, actionImg);
500            putValue(key, new ImageIcon(newIco));
501        }
502
503        private void drawImageIcon(Graphics2D g, int index, Dimension dim, ImageResource img) {
504            if (img != null) {
505                g.drawImage(img.getImageIcon(dim).getImage(),
506                        ((int) dim.getWidth())*index, 0, (int) dim.getWidth(), (int) dim.getHeight(), null);
507            }
508        }
509
510        @Override
511        public void actionPerformed(ActionEvent e) {
512            if (!confirmSaveLayerInfosOK(model))
513                return;
514            launchSafeAndUploadTask();
515        }
516
517        @Override
518        public void propertyChange(PropertyChangeEvent evt) {
519            if (evt.getPropertyName().equals(SaveLayersModel.MODE_PROP)) {
520                SaveLayersModel.Mode mode = (SaveLayersModel.Mode) evt.getNewValue();
521                switch(mode) {
522                case EDITING_DATA: setEnabled(true);
523                    break;
524                case UPLOADING_AND_SAVING: setEnabled(false);
525                    break;
526                }
527            }
528        }
529    }
530
531    /**
532     * This is the asynchronous task which uploads modified layers to the server and
533     * saves them to files, if requested by the user.
534     *
535     */
536    protected class SaveAndUploadTask implements Runnable {
537
538        private final SaveLayersModel model;
539        private final ProgressMonitor monitor;
540        private final ExecutorService worker;
541        private boolean canceled;
542        private AbstractIOTask currentTask;
543
544        public SaveAndUploadTask(SaveLayersModel model, ProgressMonitor monitor) {
545            this.model = model;
546            this.monitor = monitor;
547            this.worker = Executors.newSingleThreadExecutor(Utils.newThreadFactory(getClass() + "-%d", Thread.NORM_PRIORITY));
548        }
549
550        protected void uploadLayers(List<SaveLayerInfo> toUpload) {
551            for (final SaveLayerInfo layerInfo: toUpload) {
552                AbstractModifiableLayer layer = layerInfo.getLayer();
553                if (canceled) {
554                    model.setUploadState(layer, UploadOrSaveState.CANCELED);
555                    continue;
556                }
557                monitor.subTask(tr("Preparing layer ''{0}'' for upload ...", layerInfo.getName()));
558
559                if (!UploadAction.checkPreUploadConditions(layer)) {
560                    model.setUploadState(layer, UploadOrSaveState.FAILED);
561                    continue;
562                }
563
564                AbstractUploadDialog dialog = layer.getUploadDialog();
565                if (dialog != null) {
566                    dialog.setVisible(true);
567                    if (dialog.isCanceled()) {
568                        model.setUploadState(layer, UploadOrSaveState.CANCELED);
569                        continue;
570                    }
571                    dialog.rememberUserInput();
572                }
573
574                currentTask = layer.createUploadTask(monitor);
575                if (currentTask == null) {
576                    model.setUploadState(layer, UploadOrSaveState.FAILED);
577                    continue;
578                }
579                Future<?> currentFuture = worker.submit(currentTask);
580                try {
581                    // wait for the asynchronous task to complete
582                    currentFuture.get();
583                } catch (CancellationException e) {
584                    Logging.trace(e);
585                    model.setUploadState(layer, UploadOrSaveState.CANCELED);
586                } catch (InterruptedException | ExecutionException e) {
587                    Logging.error(e);
588                    model.setUploadState(layer, UploadOrSaveState.FAILED);
589                    ExceptionDialogUtil.explainException(e);
590                }
591                if (currentTask.isCanceled()) {
592                    model.setUploadState(layer, UploadOrSaveState.CANCELED);
593                } else if (currentTask.isFailed()) {
594                    Logging.error(currentTask.getLastException());
595                    ExceptionDialogUtil.explainException(currentTask.getLastException());
596                    model.setUploadState(layer, UploadOrSaveState.FAILED);
597                } else {
598                    model.setUploadState(layer, UploadOrSaveState.OK);
599                }
600                currentTask = null;
601            }
602        }
603
604        protected void saveLayers(List<SaveLayerInfo> toSave) {
605            for (final SaveLayerInfo layerInfo: toSave) {
606                if (canceled) {
607                    model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.CANCELED);
608                    continue;
609                }
610                // Check save preconditions earlier to avoid a blocking reentring call to EDT (see #10086)
611                if (layerInfo.isDoCheckSaveConditions()) {
612                    if (!layerInfo.getLayer().checkSaveConditions()) {
613                        continue;
614                    }
615                    layerInfo.setDoCheckSaveConditions(false);
616                }
617                currentTask = new SaveLayerTask(layerInfo, monitor);
618                Future<?> currentFuture = worker.submit(currentTask);
619
620                try {
621                    // wait for the asynchronous task to complete
622                    //
623                    currentFuture.get();
624                } catch (CancellationException e) {
625                    Logging.trace(e);
626                    model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.CANCELED);
627                } catch (InterruptedException | ExecutionException e) {
628                    Logging.error(e);
629                    model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.FAILED);
630                    ExceptionDialogUtil.explainException(e);
631                }
632                if (currentTask.isCanceled()) {
633                    model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.CANCELED);
634                } else if (currentTask.isFailed()) {
635                    if (currentTask.getLastException() != null) {
636                        Logging.error(currentTask.getLastException());
637                        ExceptionDialogUtil.explainException(currentTask.getLastException());
638                    }
639                    model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.FAILED);
640                } else {
641                    model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.OK);
642                }
643                this.currentTask = null;
644            }
645        }
646
647        protected void warnBecauseOfUnsavedData() {
648            int numProblems = model.getNumCancel() + model.getNumFailed();
649            if (numProblems == 0)
650                return;
651            Logging.warn(numProblems + " problems occurred during upload/save");
652            String msg = trn(
653                    "<html>An upload and/or save operation of one layer with modifications<br>"
654                    + "was canceled or has failed.</html>",
655                    "<html>Upload and/or save operations of {0} layers with modifications<br>"
656                    + "were canceled or have failed.</html>",
657                    numProblems,
658                    numProblems
659            );
660            JOptionPane.showMessageDialog(
661                    MainApplication.getMainFrame(),
662                    msg,
663                    tr("Incomplete upload and/or save"),
664                    JOptionPane.WARNING_MESSAGE
665            );
666        }
667
668        @Override
669        public void run() {
670            GuiHelper.runInEDTAndWait(() -> {
671                model.setMode(SaveLayersModel.Mode.UPLOADING_AND_SAVING);
672                List<SaveLayerInfo> toUpload = model.getLayersToUpload();
673                if (!toUpload.isEmpty()) {
674                    uploadLayers(toUpload);
675                }
676                List<SaveLayerInfo> toSave = model.getLayersToSave();
677                if (!toSave.isEmpty()) {
678                    saveLayers(toSave);
679                }
680                model.setMode(SaveLayersModel.Mode.EDITING_DATA);
681                if (model.hasUnsavedData()) {
682                    warnBecauseOfUnsavedData();
683                    model.setMode(Mode.EDITING_DATA);
684                    if (canceled) {
685                        setUserAction(UserAction.CANCEL);
686                        closeDialog();
687                    }
688                } else {
689                    setUserAction(UserAction.PROCEED);
690                    closeDialog();
691                }
692            });
693            worker.shutdownNow();
694        }
695
696        public void cancel() {
697            if (currentTask != null) {
698                currentTask.cancel();
699            }
700            worker.shutdown();
701            canceled = true;
702        }
703    }
704
705    @Override
706    public void tableChanged(TableModelEvent e) {
707        boolean dis = model.getLayersToSave().isEmpty() && model.getLayersToUpload().isEmpty();
708        if (saveAndProceedActionButton != null) {
709            saveAndProceedActionButton.setEnabled(!dis);
710        }
711        saveAndProceedAction.redrawIcon();
712    }
713}