001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.actions;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.event.ActionEvent;
007import java.io.File;
008import java.io.IOException;
009import java.nio.file.InvalidPathException;
010import java.util.Collection;
011import java.util.LinkedList;
012import java.util.List;
013
014import javax.swing.JFileChooser;
015import javax.swing.JOptionPane;
016import javax.swing.filechooser.FileFilter;
017
018import org.openstreetmap.josm.data.PreferencesUtils;
019import org.openstreetmap.josm.gui.ExtendedDialog;
020import org.openstreetmap.josm.gui.MainApplication;
021import org.openstreetmap.josm.gui.io.importexport.FileExporter;
022import org.openstreetmap.josm.gui.layer.Layer;
023import org.openstreetmap.josm.gui.layer.OsmDataLayer;
024import org.openstreetmap.josm.gui.util.GuiHelper;
025import org.openstreetmap.josm.gui.widgets.AbstractFileChooser;
026import org.openstreetmap.josm.spi.preferences.Config;
027import org.openstreetmap.josm.tools.Logging;
028import org.openstreetmap.josm.tools.Shortcut;
029import org.openstreetmap.josm.tools.Utils;
030
031/**
032 * Abstract superclass of save actions.
033 * @since 290
034 */
035public abstract class SaveActionBase extends DiskAccessAction {
036
037    /**
038     * Constructs a new {@code SaveActionBase}.
039     * @param name The action's text as displayed on the menu (if it is added to a menu)
040     * @param iconName The filename of the icon to use
041     * @param tooltip A longer description of the action that will be displayed in the tooltip
042     * @param shortcut A ready-created shortcut object or {@code null} if you don't want a shortcut
043     */
044    public SaveActionBase(String name, String iconName, String tooltip, Shortcut shortcut) {
045        super(name, iconName, tooltip, shortcut);
046    }
047
048    @Override
049    public void actionPerformed(ActionEvent e) {
050        if (!isEnabled())
051            return;
052        doSave();
053    }
054
055    /**
056     * Saves the active layer.
057     * @return {@code true} if the save operation succeeds
058     */
059    public boolean doSave() {
060        Layer layer = getLayerManager().getActiveLayer();
061        if (layer != null && layer.isSavable()) {
062            return doSave(layer);
063        }
064        return false;
065    }
066
067    /**
068     * Saves the given layer.
069     * @param layer layer to save
070     * @return {@code true} if the save operation succeeds
071     */
072    public boolean doSave(Layer layer) {
073        if (!layer.checkSaveConditions())
074            return false;
075        return doInternalSave(layer, getFile(layer));
076    }
077
078    /**
079     * Saves a layer to a given file.
080     * @param layer The layer to save
081     * @param file The destination file
082     * @param checkSaveConditions if {@code true}, checks preconditions before saving. Set it to {@code false} to skip it
083     * if preconditions have already been checked (as this check can prompt UI dialog in EDT it may be best in some cases
084     * to do it earlier).
085     * @return {@code true} if the layer has been successfully saved, {@code false} otherwise
086     * @since 7204
087     */
088    public static boolean doSave(Layer layer, File file, boolean checkSaveConditions) {
089        if (checkSaveConditions && !layer.checkSaveConditions())
090            return false;
091        return doInternalSave(layer, file);
092    }
093
094    private static boolean doInternalSave(Layer layer, File file) {
095        if (file == null)
096            return false;
097
098        try {
099            boolean exported = false;
100            boolean canceled = false;
101            for (FileExporter exporter : ExtensionFileFilter.getExporters()) {
102                if (exporter.acceptFile(file, layer)) {
103                    exporter.exportData(file, layer);
104                    exported = true;
105                    canceled = exporter.isCanceled();
106                    break;
107                }
108            }
109            if (!exported) {
110                GuiHelper.runInEDT(() ->
111                    JOptionPane.showMessageDialog(MainApplication.getMainFrame(), tr("No Exporter found! Nothing saved."), tr("Warning"),
112                        JOptionPane.WARNING_MESSAGE));
113                return false;
114            } else if (canceled) {
115                return false;
116            }
117            if (!layer.isRenamed()) {
118                layer.setName(file.getName());
119            }
120            layer.setAssociatedFile(file);
121            if (layer instanceof OsmDataLayer) {
122                ((OsmDataLayer) layer).onPostSaveToFile();
123            }
124            MainApplication.getMainFrame().repaint();
125        } catch (IOException | InvalidPathException e) {
126            showAndLogException(e);
127            return false;
128        }
129        addToFileOpenHistory(file);
130        return true;
131    }
132
133    protected abstract File getFile(Layer layer);
134
135    /**
136     * Refreshes the enabled state
137     *
138     */
139    @Override
140    protected void updateEnabledState() {
141        Layer activeLayer = getLayerManager().getActiveLayer();
142        setEnabled(activeLayer != null && activeLayer.isSavable());
143    }
144
145    /**
146     * Creates a new "Save" dialog for a single {@link ExtensionFileFilter} and makes it visible.<br>
147     * When the user has chosen a file, checks the file extension, and confirms overwrite if needed.
148     *
149     * @param title The dialog title
150     * @param filter The dialog file filter
151     * @return The output {@code File}
152     * @see DiskAccessAction#createAndOpenFileChooser(boolean, boolean, String, FileFilter, int, String)
153     * @since 5456
154     */
155    public static File createAndOpenSaveFileChooser(String title, ExtensionFileFilter filter) {
156        AbstractFileChooser fc = createAndOpenFileChooser(false, false, title, filter, JFileChooser.FILES_ONLY, null);
157        return checkFileAndConfirmOverWrite(fc, filter.getDefaultExtension());
158    }
159
160    /**
161     * Creates a new "Save" dialog for a given file extension and makes it visible.<br>
162     * When the user has chosen a file, checks the file extension, and confirms overwrite if needed.
163     *
164     * @param title The dialog title
165     * @param extension The file extension
166     * @return The output {@code File}
167     * @see DiskAccessAction#createAndOpenFileChooser(boolean, boolean, String, String)
168     */
169    public static File createAndOpenSaveFileChooser(String title, String extension) {
170        AbstractFileChooser fc = createAndOpenFileChooser(false, false, title, extension);
171        return checkFileAndConfirmOverWrite(fc, extension);
172    }
173
174    /**
175     * Checks if selected filename has the given extension. If not, adds the extension and asks for overwrite if filename exists.
176     *
177     * @param fc FileChooser where file was already selected
178     * @param extension file extension
179     * @return the {@code File} or {@code null} if the user cancelled the dialog.
180     */
181    public static File checkFileAndConfirmOverWrite(AbstractFileChooser fc, String extension) {
182        if (fc == null)
183            return null;
184        File file = fc.getSelectedFile();
185
186        FileFilter ff = fc.getFileFilter();
187        if (!ff.accept(file)) {
188            // Extension of another filefilter given ?
189            for (FileFilter cff : fc.getChoosableFileFilters()) {
190                if (cff.accept(file)) {
191                    fc.setFileFilter(cff);
192                    return file;
193                }
194            }
195            // No filefilter accepts current filename, add default extension
196            String fn = file.getPath();
197            if (extension != null && ff.accept(new File(fn + '.' + extension))) {
198                fn += '.' + extension;
199            } else if (ff instanceof ExtensionFileFilter) {
200                fn += '.' + ((ExtensionFileFilter) ff).getDefaultExtension();
201            }
202            file = new File(fn);
203            if (!fc.getSelectedFile().exists() && !confirmOverwrite(file))
204                return null;
205        }
206        return file;
207    }
208
209    /**
210     * Asks user to confirm overwiting a file.
211     * @param file file to overwrite
212     * @return {@code true} if the file can be written
213     */
214    public static boolean confirmOverwrite(File file) {
215        if (file == null || file.exists()) {
216            return new ExtendedDialog(
217                    MainApplication.getMainFrame(),
218                    tr("Overwrite"),
219                    tr("Overwrite"), tr("Cancel"))
220                .setContent(tr("File exists. Overwrite?"))
221                .setButtonIcons("save_as", "cancel")
222                .showDialog()
223                .getValue() == 1;
224        }
225        return true;
226    }
227
228    static void addToFileOpenHistory(File file) {
229        final String filepath;
230        try {
231            filepath = file.getCanonicalPath();
232        } catch (IOException ign) {
233            Logging.warn(ign);
234            return;
235        }
236
237        int maxsize = Math.max(0, Config.getPref().getInt("file-open.history.max-size", 15));
238        Collection<String> oldHistory = Config.getPref().getList("file-open.history");
239        List<String> history = new LinkedList<>(oldHistory);
240        history.remove(filepath);
241        history.add(0, filepath);
242        PreferencesUtils.putListBounded(Config.getPref(), "file-open.history", maxsize, history);
243    }
244
245    static void showAndLogException(Exception e) {
246        GuiHelper.runInEDT(() ->
247        JOptionPane.showMessageDialog(
248                MainApplication.getMainFrame(),
249                tr("<html>An error occurred while saving.<br>Error is:<br>{0}</html>",
250                        Utils.escapeReservedCharactersHTML(e.getClass().getSimpleName() + " - " + e.getMessage())),
251                tr("Error"),
252                JOptionPane.ERROR_MESSAGE
253                ));
254
255        Logging.error(e);
256    }
257}