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