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