001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.io.importexport;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.io.File;
007import java.io.IOException;
008import java.io.OutputStream;
009import java.io.OutputStreamWriter;
010import java.io.PrintWriter;
011import java.io.Writer;
012import java.nio.charset.StandardCharsets;
013import java.nio.file.AccessDeniedException;
014import java.nio.file.InvalidPathException;
015import java.text.MessageFormat;
016
017import javax.swing.JOptionPane;
018
019import org.openstreetmap.josm.actions.ExtensionFileFilter;
020import org.openstreetmap.josm.gui.MainApplication;
021import org.openstreetmap.josm.gui.layer.Layer;
022import org.openstreetmap.josm.gui.layer.OsmDataLayer;
023import org.openstreetmap.josm.io.Compression;
024import org.openstreetmap.josm.io.OsmWriter;
025import org.openstreetmap.josm.io.OsmWriterFactory;
026import org.openstreetmap.josm.spi.preferences.Config;
027import org.openstreetmap.josm.tools.Logging;
028import org.openstreetmap.josm.tools.Utils;
029
030/**
031 * Exports data to an .osm file.
032 * @since 1949
033 */
034public class OsmExporter extends FileExporter {
035
036    /**
037     * Constructs a new {@code OsmExporter}.
038     */
039    public OsmExporter() {
040        super(new ExtensionFileFilter(
041            "osm,xml", "osm", tr("OSM Server Files") + " (*.osm)"));
042    }
043
044    /**
045     * Constructs a new {@code OsmExporter}.
046     * @param filter The extension file filter
047     */
048    public OsmExporter(ExtensionFileFilter filter) {
049        super(filter);
050    }
051
052    @Override
053    public boolean acceptFile(File pathname, Layer layer) {
054        if (!(layer instanceof OsmDataLayer))
055            return false;
056        return super.acceptFile(pathname, layer);
057    }
058
059    @Override
060    public void exportData(File file, Layer layer) throws IOException {
061        exportData(file, layer, false);
062    }
063
064    /**
065     * Exports OSM data to the given file.
066     * @param file Output file
067     * @param layer Data layer. Must be an instance of {@link OsmDataLayer}.
068     * @param isAutosave if {@code true}, the potential backup file created if the output file already exists will be deleted
069     *                   after a successful export and post-save events won't be fired
070     * @throws IOException in case of IO errors
071     * @throws InvalidPathException when file name cannot be converted into a Path
072     * @throws IllegalArgumentException if {@code layer} is not an instance of {@code OsmDataLayer}
073     */
074    public void exportData(File file, Layer layer, boolean isAutosave) throws IOException {
075        if (!(layer instanceof OsmDataLayer)) {
076            throw new IllegalArgumentException(
077                    MessageFormat.format("Expected instance of OsmDataLayer. Got ''{0}''.", layer.getClass().getName()));
078        }
079        save(file, (OsmDataLayer) layer, isAutosave);
080    }
081
082    protected static OutputStream getOutputStream(File file) throws IOException {
083        return Compression.getCompressedFileOutputStream(file);
084    }
085
086    private void save(File file, OsmDataLayer layer, boolean isAutosave) throws IOException {
087        File tmpFile = null;
088        try {
089            if (file.exists() && !file.canWrite()) {
090                throw new AccessDeniedException(file.toString());
091            }
092
093            // use a tmp file because if something errors out in the process of writing the file,
094            // we might just end up with a truncated file.  That can destroy lots of work.
095            if (file.exists()) {
096                tmpFile = new File(file.getPath() + '~');
097                Utils.copyFile(file, tmpFile);
098            }
099
100            doSave(file, layer);
101            if ((isAutosave || !Config.getPref().getBoolean("save.keepbackup", false)) && tmpFile != null) {
102                Utils.deleteFile(tmpFile);
103            }
104            if (!isAutosave) {
105                layer.onPostSaveToFile();
106            }
107        } catch (IOException | InvalidPathException e) {
108            Logging.error(e);
109
110            try {
111                // if the file save failed, then the tempfile will not be deleted. So, restore the backup if we made one.
112                if (tmpFile != null && tmpFile.exists()) {
113                    Utils.copyFile(tmpFile, file);
114                }
115            } catch (IOException | InvalidPathException e2) {
116                Logging.error(e2);
117                JOptionPane.showMessageDialog(
118                        MainApplication.getMainFrame(),
119                        tr("<html>An error occurred while restoring backup file.<br>Error is:<br>{0}</html>",
120                                Utils.escapeReservedCharactersHTML(e2.getMessage())),
121                        tr("Error"),
122                        JOptionPane.ERROR_MESSAGE
123                );
124            }
125            // re-throw original error
126            throw e;
127        }
128    }
129
130    protected void doSave(File file, OsmDataLayer layer) throws IOException {
131        // create outputstream and wrap it with gzip, xz or bzip, if necessary
132        try (
133            OutputStream out = getOutputStream(file);
134            Writer writer = new OutputStreamWriter(out, StandardCharsets.UTF_8);
135            OsmWriter w = OsmWriterFactory.createOsmWriter(new PrintWriter(writer), false, layer.data.getVersion())
136        ) {
137            layer.data.getReadLock().lock();
138            try {
139                w.write(layer.data);
140            } finally {
141                layer.data.getReadLock().unlock();
142            }
143        }
144    }
145}