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.FlowLayout;
011import java.awt.Graphics2D;
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.List;
022import java.util.concurrent.CancellationException;
023import java.util.concurrent.ExecutorService;
024import java.util.concurrent.Executors;
025import java.util.concurrent.Future;
026
027import javax.swing.AbstractAction;
028import javax.swing.DefaultListCellRenderer;
029import javax.swing.ImageIcon;
030import javax.swing.JButton;
031import javax.swing.JComponent;
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.KeyStroke;
039import javax.swing.ListCellRenderer;
040import javax.swing.WindowConstants;
041import javax.swing.event.TableModelEvent;
042import javax.swing.event.TableModelListener;
043
044import org.openstreetmap.josm.Main;
045import org.openstreetmap.josm.actions.UploadAction;
046import org.openstreetmap.josm.gui.ExceptionDialogUtil;
047import org.openstreetmap.josm.gui.io.SaveLayersModel.Mode;
048import org.openstreetmap.josm.gui.layer.AbstractModifiableLayer;
049import org.openstreetmap.josm.gui.progress.ProgressMonitor;
050import org.openstreetmap.josm.gui.progress.SwingRenderingProgressMonitor;
051import org.openstreetmap.josm.gui.util.GuiHelper;
052import org.openstreetmap.josm.tools.ImageProvider;
053import org.openstreetmap.josm.tools.WindowGeometry;
054
055public class SaveLayersDialog extends JDialog implements TableModelListener {
056    public static enum UserAction {
057        /** save/upload layers was successful, proceed with operation */
058        PROCEED,
059        /** save/upload of layers was not successful or user canceled operation */
060        CANCEL
061    }
062
063    private SaveLayersModel model;
064    private UserAction action = UserAction.CANCEL;
065    private UploadAndSaveProgressRenderer pnlUploadLayers;
066
067    private SaveAndProceedAction saveAndProceedAction;
068    private DiscardAndProceedAction discardAndProceedAction;
069    private CancelAction cancelAction;
070    private SaveAndUploadTask saveAndUploadTask;
071
072    /**
073     * builds the GUI
074     */
075    protected void build() {
076        WindowGeometry geometry = WindowGeometry.centerOnScreen(new Dimension(650,300));
077        geometry.applySafe(this);
078        getContentPane().setLayout(new BorderLayout());
079
080        model = new SaveLayersModel();
081        SaveLayersTable table = new SaveLayersTable(model);
082        JScrollPane pane = new JScrollPane(table);
083        model.addPropertyChangeListener(table);
084        table.getModel().addTableModelListener(this);
085
086        getContentPane().add(pane, BorderLayout.CENTER);
087        getContentPane().add(buildButtonRow(), BorderLayout.SOUTH);
088
089        addWindowListener(new WindowClosingAdapter());
090        setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
091    }
092
093    private JButton saveAndProceedActionButton = null;
094
095    /**
096     * builds the button row
097     *
098     * @return the panel with the button row
099     */
100    protected JPanel buildButtonRow() {
101        JPanel pnl = new JPanel();
102        pnl.setLayout(new FlowLayout(FlowLayout.CENTER));
103
104        saveAndProceedAction = new SaveAndProceedAction();
105        model.addPropertyChangeListener(saveAndProceedAction);
106        pnl.add(saveAndProceedActionButton = new JButton(saveAndProceedAction));
107
108        discardAndProceedAction = new DiscardAndProceedAction();
109        model.addPropertyChangeListener(discardAndProceedAction);
110        pnl.add(new JButton(discardAndProceedAction));
111
112        cancelAction = new CancelAction();
113        pnl.add(new JButton(cancelAction));
114
115        JPanel pnl2 = new JPanel();
116        pnl2.setLayout(new BorderLayout());
117        pnl2.add(pnlUploadLayers = new UploadAndSaveProgressRenderer(), BorderLayout.CENTER);
118        model.addPropertyChangeListener(pnlUploadLayers);
119        pnl2.add(pnl, BorderLayout.SOUTH);
120        return pnl2;
121    }
122
123    public void prepareForSavingAndUpdatingLayersBeforeExit() {
124        setTitle(tr("Unsaved changes - Save/Upload before exiting?"));
125        this.saveAndProceedAction.initForSaveAndExit();
126        this.discardAndProceedAction.initForDiscardAndExit();
127    }
128
129    public void prepareForSavingAndUpdatingLayersBeforeDelete() {
130        setTitle(tr("Unsaved changes - Save/Upload before deleting?"));
131        this.saveAndProceedAction.initForSaveAndDelete();
132        this.discardAndProceedAction.initForDiscardAndDelete();
133    }
134
135    public SaveLayersDialog(Component parent) {
136        super(JOptionPane.getFrameForComponent(parent), ModalityType.DOCUMENT_MODAL);
137        build();
138    }
139
140    public UserAction getUserAction() {
141        return this.action;
142    }
143
144    public SaveLayersModel getModel() {
145        return model;
146    }
147
148    protected void launchSafeAndUploadTask() {
149        ProgressMonitor monitor = new SwingRenderingProgressMonitor(pnlUploadLayers);
150        monitor.beginTask(tr("Uploading and saving modified layers ..."));
151        this.saveAndUploadTask = new SaveAndUploadTask(model, monitor);
152        new Thread(saveAndUploadTask).start();
153    }
154
155    protected void cancelSafeAndUploadTask() {
156        if (this.saveAndUploadTask != null) {
157            this.saveAndUploadTask.cancel();
158        }
159        model.setMode(Mode.EDITING_DATA);
160    }
161
162    private static class  LayerListWarningMessagePanel extends JPanel {
163        private JLabel lblMessage;
164        private JList<SaveLayerInfo> lstLayers;
165
166        protected void build() {
167            setLayout(new GridBagLayout());
168            GridBagConstraints gc = new GridBagConstraints();
169            gc.gridx = 0;
170            gc.gridy = 0;
171            gc.fill = GridBagConstraints.HORIZONTAL;
172            gc.weightx = 1.0;
173            gc.weighty = 0.0;
174            add(lblMessage = new JLabel(), gc);
175            lblMessage.setHorizontalAlignment(JLabel.LEFT);
176            lstLayers = new JList<>();
177            lstLayers.setCellRenderer(
178                    new ListCellRenderer<SaveLayerInfo>() {
179                        final DefaultListCellRenderer def = new DefaultListCellRenderer();
180                        @Override
181                        public Component getListCellRendererComponent(JList<? extends SaveLayerInfo> list, SaveLayerInfo info, int index,
182                                boolean isSelected, boolean cellHasFocus) {
183                            def.setIcon(info.getLayer().getIcon());
184                            def.setText(info.getName());
185                            return def;
186                        }
187                    }
188            );
189            gc.gridx = 0;
190            gc.gridy = 1;
191            gc.fill = GridBagConstraints.HORIZONTAL;
192            gc.weightx = 1.0;
193            gc.weighty = 1.0;
194            add(lstLayers,gc);
195        }
196
197        public LayerListWarningMessagePanel(String msg, List<SaveLayerInfo> infos) {
198            build();
199            lblMessage.setText(msg);
200            lstLayers.setListData(infos.toArray(new SaveLayerInfo[0]));
201        }
202    }
203
204    protected void warnLayersWithConflictsAndUploadRequest(List<SaveLayerInfo> infos) {
205        String msg = trn("<html>{0} layer has unresolved conflicts.<br>"
206                + "Either resolve them first or discard the modifications.<br>"
207                + "Layer with conflicts:</html>",
208                "<html>{0} layers have unresolved conflicts.<br>"
209                + "Either resolve them first or discard the modifications.<br>"
210                + "Layers with conflicts:</html>",
211                infos.size(),
212                infos.size());
213        JOptionPane.showConfirmDialog(
214                Main.parent,
215                new LayerListWarningMessagePanel(msg, infos),
216                tr("Unsaved data and conflicts"),
217                JOptionPane.DEFAULT_OPTION,
218                JOptionPane.WARNING_MESSAGE
219        );
220    }
221
222    protected void warnLayersWithoutFilesAndSaveRequest(List<SaveLayerInfo> infos) {
223        String msg = trn("<html>{0} layer needs saving but has no associated file.<br>"
224                + "Either select a file for this layer or discard the changes.<br>"
225                + "Layer without a file:</html>",
226                "<html>{0} layers need saving but have no associated file.<br>"
227                + "Either select a file for each of them or discard the changes.<br>"
228                + "Layers without a file:</html>",
229                infos.size(),
230                infos.size());
231        JOptionPane.showConfirmDialog(
232                Main.parent,
233                new LayerListWarningMessagePanel(msg, infos),
234                tr("Unsaved data and missing associated file"),
235                JOptionPane.DEFAULT_OPTION,
236                JOptionPane.WARNING_MESSAGE
237        );
238    }
239
240    protected void warnLayersWithIllegalFilesAndSaveRequest(List<SaveLayerInfo> infos) {
241        String msg = trn("<html>{0} layer needs saving but has an associated file<br>"
242                + "which cannot be written.<br>"
243                + "Either select another file for this layer or discard the changes.<br>"
244                + "Layer with a non-writable file:</html>",
245                "<html>{0} layers need saving but have associated files<br>"
246                + "which cannot be written.<br>"
247                + "Either select another file for each of them or discard the changes.<br>"
248                + "Layers with non-writable files:</html>",
249                infos.size(),
250                infos.size());
251        JOptionPane.showConfirmDialog(
252                Main.parent,
253                new LayerListWarningMessagePanel(msg, infos),
254                tr("Unsaved data non-writable files"),
255                JOptionPane.DEFAULT_OPTION,
256                JOptionPane.WARNING_MESSAGE
257        );
258    }
259
260    protected boolean confirmSaveLayerInfosOK() {
261        List<SaveLayerInfo> layerInfos = model.getLayersWithConflictsAndUploadRequest();
262        if (!layerInfos.isEmpty()) {
263            warnLayersWithConflictsAndUploadRequest(layerInfos);
264            return false;
265        }
266
267        layerInfos = model.getLayersWithoutFilesAndSaveRequest();
268        if (!layerInfos.isEmpty()) {
269            warnLayersWithoutFilesAndSaveRequest(layerInfos);
270            return false;
271        }
272
273        layerInfos = model.getLayersWithIllegalFilesAndSaveRequest();
274        if (!layerInfos.isEmpty()) {
275            warnLayersWithIllegalFilesAndSaveRequest(layerInfos);
276            return false;
277        }
278
279        return true;
280    }
281
282    protected void setUserAction(UserAction action) {
283        this.action = action;
284    }
285
286    /**
287     * Closes this dialog and frees all native screen resources.
288     */
289    public void closeDialog() {
290        setVisible(false);
291        dispose();
292    }
293
294    class WindowClosingAdapter extends WindowAdapter {
295        @Override
296        public void windowClosing(WindowEvent e) {
297            cancelAction.cancel();
298        }
299    }
300
301    class CancelAction extends AbstractAction {
302        public CancelAction() {
303            putValue(NAME, tr("Cancel"));
304            putValue(SHORT_DESCRIPTION, tr("Close this dialog and resume editing in JOSM"));
305            putValue(SMALL_ICON, ImageProvider.get("cancel"));
306            getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
307            .put(KeyStroke.getKeyStroke("ESCAPE"), "ESCAPE");
308            getRootPane().getActionMap().put("ESCAPE", this);
309        }
310
311        protected void cancelWhenInEditingModel() {
312            setUserAction(UserAction.CANCEL);
313            closeDialog();
314        }
315
316        protected void cancelWhenInSaveAndUploadingMode() {
317            cancelSafeAndUploadTask();
318        }
319
320        public void cancel() {
321            switch(model.getMode()) {
322            case EDITING_DATA: cancelWhenInEditingModel(); break;
323            case UPLOADING_AND_SAVING: cancelSafeAndUploadTask(); break;
324            }
325        }
326
327        @Override
328        public void actionPerformed(ActionEvent e) {
329            cancel();
330        }
331    }
332
333    class DiscardAndProceedAction extends AbstractAction  implements PropertyChangeListener {
334        public DiscardAndProceedAction() {
335            initForDiscardAndExit();
336        }
337
338        public void initForDiscardAndExit() {
339            putValue(NAME, tr("Exit now!"));
340            putValue(SHORT_DESCRIPTION, tr("Exit JOSM without saving. Unsaved changes are lost."));
341            putValue(SMALL_ICON, ImageProvider.get("exit"));
342        }
343
344        public void initForDiscardAndDelete() {
345            putValue(NAME, tr("Delete now!"));
346            putValue(SHORT_DESCRIPTION, tr("Delete layers without saving. Unsaved changes are lost."));
347            putValue(SMALL_ICON, ImageProvider.get("dialogs", "delete"));
348        }
349
350        @Override
351        public void actionPerformed(ActionEvent e) {
352            setUserAction(UserAction.PROCEED);
353            closeDialog();
354        }
355        @Override
356        public void propertyChange(PropertyChangeEvent evt) {
357            if (evt.getPropertyName().equals(SaveLayersModel.MODE_PROP)) {
358                Mode mode = (Mode)evt.getNewValue();
359                switch(mode) {
360                case EDITING_DATA: setEnabled(true); break;
361                case UPLOADING_AND_SAVING: setEnabled(false); break;
362                }
363            }
364        }
365    }
366
367    final class SaveAndProceedAction extends AbstractAction implements PropertyChangeListener {
368        private static final int is = 24; // icon size
369        private static final String BASE_ICON = "BASE_ICON";
370        private final Image save = ImageProvider.get("save").getImage();
371        private final Image upld = ImageProvider.get("upload").getImage();
372        private final Image saveDis = new BufferedImage(is, is, BufferedImage.TYPE_4BYTE_ABGR);
373        private final Image upldDis = new BufferedImage(is, is, BufferedImage.TYPE_4BYTE_ABGR);
374
375        public SaveAndProceedAction() {
376            // get disabled versions of icons
377            new JLabel(ImageProvider.get("save")).getDisabledIcon().paintIcon(new JPanel(), saveDis.getGraphics(), 0, 0);
378            new JLabel(ImageProvider.get("upload")).getDisabledIcon().paintIcon(new JPanel(), upldDis.getGraphics(), 0, 0);
379            initForSaveAndExit();
380        }
381
382        public void initForSaveAndExit() {
383            putValue(NAME, tr("Perform actions before exiting"));
384            putValue(SHORT_DESCRIPTION, tr("Exit JOSM with saving. Unsaved changes are uploaded and/or saved."));
385            putValue(BASE_ICON, ImageProvider.get("exit"));
386            redrawIcon();
387        }
388
389        public void initForSaveAndDelete() {
390            putValue(NAME, tr("Perform actions before deleting"));
391            putValue(SHORT_DESCRIPTION, tr("Save/Upload layers before deleting. Unsaved changes are not lost."));
392            putValue(BASE_ICON, ImageProvider.get("dialogs", "delete"));
393            redrawIcon();
394        }
395
396        public void redrawIcon() {
397            try { // Can fail if model is not yet setup properly
398                Image base = ((ImageIcon) getValue(BASE_ICON)).getImage();
399                BufferedImage newIco = new BufferedImage(is*3, is, BufferedImage.TYPE_4BYTE_ABGR);
400                Graphics2D g = newIco.createGraphics();
401                g.drawImage(model.getLayersToUpload().isEmpty() ? upldDis : upld, is*0, 0, is, is, null);
402                g.drawImage(model.getLayersToSave().isEmpty()   ? saveDis : save, is*1, 0, is, is, null);
403                g.drawImage(base,                                                 is*2, 0, is, is, null);
404                putValue(SMALL_ICON, new ImageIcon(newIco));
405            } catch(Exception e) {
406                putValue(SMALL_ICON, getValue(BASE_ICON));
407            }
408        }
409
410        @Override
411        public void actionPerformed(ActionEvent e) {
412            if (! confirmSaveLayerInfosOK())
413                return;
414            launchSafeAndUploadTask();
415        }
416
417        @Override
418        public void propertyChange(PropertyChangeEvent evt) {
419            if (evt.getPropertyName().equals(SaveLayersModel.MODE_PROP)) {
420                SaveLayersModel.Mode mode = (SaveLayersModel.Mode)evt.getNewValue();
421                switch(mode) {
422                case EDITING_DATA: setEnabled(true); break;
423                case UPLOADING_AND_SAVING: setEnabled(false); break;
424                }
425            }
426        }
427    }
428
429    /**
430     * This is the asynchronous task which uploads modified layers to the server and
431     * saves them to files, if requested by the user.
432     *
433     */
434    protected class SaveAndUploadTask implements Runnable {
435
436        private SaveLayersModel model;
437        private ProgressMonitor monitor;
438        private ExecutorService worker;
439        private boolean canceled;
440        private Future<?> currentFuture;
441        private AbstractIOTask currentTask;
442
443        public SaveAndUploadTask(SaveLayersModel model, ProgressMonitor monitor) {
444            this.model = model;
445            this.monitor = monitor;
446            this.worker = Executors.newSingleThreadExecutor();
447        }
448
449        protected void uploadLayers(List<SaveLayerInfo> toUpload) {
450            for (final SaveLayerInfo layerInfo: toUpload) {
451                AbstractModifiableLayer layer = layerInfo.getLayer();
452                if (canceled) {
453                    model.setUploadState(layer, UploadOrSaveState.CANCELED);
454                    continue;
455                }
456                monitor.subTask(tr("Preparing layer ''{0}'' for upload ...", layerInfo.getName()));
457
458                if (!UploadAction.checkPreUploadConditions(layer)) {
459                    model.setUploadState(layer, UploadOrSaveState.FAILED);
460                    continue;
461                }
462
463                AbstractUploadDialog dialog = layer.getUploadDialog();
464                if (dialog != null) {
465                    dialog.setVisible(true);
466                    if (dialog.isCanceled()) {
467                        model.setUploadState(layer, UploadOrSaveState.CANCELED);
468                        continue;
469                    }
470                    dialog.rememberUserInput();
471                }
472
473                currentTask = layer.createUploadTask(monitor);
474                if (currentTask == null) {
475                    model.setUploadState(layer, UploadOrSaveState.FAILED);
476                    continue;
477                }
478                currentFuture = worker.submit(currentTask);
479                try {
480                    // wait for the asynchronous task to complete
481                    //
482                    currentFuture.get();
483                } catch(CancellationException e) {
484                    model.setUploadState(layer, UploadOrSaveState.CANCELED);
485                } catch(Exception e) {
486                    Main.error(e);
487                    model.setUploadState(layer, UploadOrSaveState.FAILED);
488                    ExceptionDialogUtil.explainException(e);
489                }
490                if (currentTask.isCanceled()) {
491                    model.setUploadState(layer, UploadOrSaveState.CANCELED);
492                } else if (currentTask.isFailed()) {
493                    Main.error(currentTask.getLastException());
494                    ExceptionDialogUtil.explainException(currentTask.getLastException());
495                    model.setUploadState(layer, UploadOrSaveState.FAILED);
496                } else {
497                    model.setUploadState(layer, UploadOrSaveState.OK);
498                }
499                currentTask = null;
500                currentFuture = null;
501            }
502        }
503
504        protected void saveLayers(List<SaveLayerInfo> toSave) {
505            for (final SaveLayerInfo layerInfo: toSave) {
506                if (canceled) {
507                    model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.CANCELED);
508                    continue;
509                }
510                // Check save preconditions earlier to avoid a blocking reentring call to EDT (see #10086)
511                if (layerInfo.isDoCheckSaveConditions()) {
512                    if (!layerInfo.getLayer().checkSaveConditions()) {
513                        continue;
514                    }
515                    layerInfo.setDoCheckSaveConditions(false);
516                }
517                currentTask = new SaveLayerTask(layerInfo, monitor);
518                currentFuture = worker.submit(currentTask);
519
520                try {
521                    // wait for the asynchronous task to complete
522                    //
523                    currentFuture.get();
524                } catch(CancellationException e) {
525                    model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.CANCELED);
526                } catch(Exception e) {
527                    Main.error(e);
528                    model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.FAILED);
529                    ExceptionDialogUtil.explainException(e);
530                }
531                if (currentTask.isCanceled()) {
532                    model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.CANCELED);
533                } else if (currentTask.isFailed()) {
534                    if (currentTask.getLastException() != null) {
535                        Main.error(currentTask.getLastException());
536                        ExceptionDialogUtil.explainException(currentTask.getLastException());
537                    }
538                    model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.FAILED);
539                } else {
540                    model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.OK);
541                }
542                this.currentTask = null;
543                this.currentFuture = null;
544            }
545        }
546
547        protected void warnBecauseOfUnsavedData() {
548            int numProblems = model.getNumCancel() + model.getNumFailed();
549            if (numProblems == 0) return;
550            String msg = trn(
551                    "<html>An upload and/or save operation of one layer with modifications<br>"
552                    + "was canceled or has failed.</html>",
553                    "<html>Upload and/or save operations of {0} layers with modifications<br>"
554                    + "were canceled or have failed.</html>",
555                    numProblems,
556                    numProblems
557            );
558            JOptionPane.showMessageDialog(
559                    Main.parent,
560                    msg,
561                    tr("Incomplete upload and/or save"),
562                    JOptionPane.WARNING_MESSAGE
563            );
564        }
565
566        @Override
567        public void run() {
568            GuiHelper.runInEDTAndWait(new Runnable() {
569                @Override
570                public void run() {
571                    model.setMode(SaveLayersModel.Mode.UPLOADING_AND_SAVING);
572                    List<SaveLayerInfo> toUpload = model.getLayersToUpload();
573                    if (!toUpload.isEmpty()) {
574                        uploadLayers(toUpload);
575                    }
576                    List<SaveLayerInfo> toSave = model.getLayersToSave();
577                    if (!toSave.isEmpty()) {
578                        saveLayers(toSave);
579                    }
580                    model.setMode(SaveLayersModel.Mode.EDITING_DATA);
581                    if (model.hasUnsavedData()) {
582                        warnBecauseOfUnsavedData();
583                        model.setMode(Mode.EDITING_DATA);
584                        if (canceled) {
585                            setUserAction(UserAction.CANCEL);
586                            closeDialog();
587                        }
588                    } else {
589                        setUserAction(UserAction.PROCEED);
590                        closeDialog();
591                    }
592                }
593            });
594        }
595
596        public void cancel() {
597            if (currentTask != null) {
598                currentTask.cancel();
599            }
600            canceled = true;
601        }
602    }
603
604    @Override
605    public void tableChanged(TableModelEvent arg0) {
606        boolean dis = model.getLayersToSave().isEmpty() && model.getLayersToUpload().isEmpty();
607        if(saveAndProceedActionButton != null) {
608            saveAndProceedActionButton.setEnabled(!dis);
609        }
610        saveAndProceedAction.redrawIcon();
611    }
612}