001package jmri.jmrit.roster;
002
003import java.awt.Component;
004import java.awt.event.ActionEvent;
005import java.io.File;
006import java.io.FileInputStream;
007import java.io.FileNotFoundException;
008import java.io.FileOutputStream;
009import java.io.IOException;
010import java.util.zip.ZipEntry;
011import java.util.zip.ZipOutputStream;
012import javax.swing.Icon;
013import javax.swing.JFileChooser;
014import javax.swing.filechooser.FileNameExtensionFilter;
015
016import jmri.util.ThreadingUtil;
017import jmri.util.swing.CountingBusyDialog;
018import jmri.util.swing.JmriAbstractAction;
019import jmri.util.swing.WindowInterface;
020
021/**
022 * Offer an easy mechanism to save the entire roster contents from one instance
023 * of DecoderPro. The result is a zip format file, containing all of the roster
024 * entries plus the overall roster.xml index file.
025 *
026 * @author david d zuhn
027 *
028 */
029public class FullBackupExportAction
030        extends JmriAbstractAction {
031
032    // parent component for GUI
033    public FullBackupExportAction(String s, WindowInterface wi) {
034        super(s, wi);
035        _parent = wi.getFrame();
036    }
037
038    public FullBackupExportAction(String s, Icon i, WindowInterface wi) {
039        super(s, i, wi);
040        _parent = wi.getFrame();
041    }
042    private Component _parent;
043    private String filename;
044    private CountingBusyDialog dialog;
045
046    /**
047     * @param s      Name of this action, e.g. in menus
048     * @param parent Component that action is associated with, used to ensure
049     *               proper position in of dialog boxes
050     */
051    public FullBackupExportAction(String s, Component parent) {
052        super(s);
053        _parent = parent;
054    }
055
056    @Override
057    public void actionPerformed(ActionEvent e) {
058
059        String roster_filename_extension = "roster";
060
061        JFileChooser chooser = new jmri.util.swing.JmriJFileChooser();
062        FileNameExtensionFilter filter = new FileNameExtensionFilter(
063                "JMRI full roster files", roster_filename_extension);
064        chooser.setFileFilter(filter);
065
066        int returnVal = chooser.showSaveDialog(_parent);
067        if (returnVal != JFileChooser.APPROVE_OPTION) {
068            return;
069        }
070
071        filename = chooser.getSelectedFile().getAbsolutePath();
072
073        if (!filename.endsWith("."+roster_filename_extension)) {
074            filename = filename.concat("."+roster_filename_extension);
075        }
076
077        new Thread(() -> {run();}).start();
078    }
079
080    /**
081     * Actually do the copying
082     */
083    public void run() {
084        try {
085
086            Roster roster = Roster.getDefault();
087
088            dialog = new CountingBusyDialog(null, "Exporting Roster", false, roster.getAllEntries().size());
089            ThreadingUtil.runOnGUIEventually(() -> {dialog.start();});
090
091            try (ZipOutputStream zipper = new ZipOutputStream(new FileOutputStream(filename))) {
092
093                // create a zip file roster entry for each entry in the main roster
094                int count = 0;
095                for (RosterEntry entry : roster.getAllEntries()) {
096                    count++;
097                    final int thisCount = count;
098                    ThreadingUtil.runOnGUIEventually(() -> {dialog.count(thisCount);});
099                    try {
100                        copyFileToStream(entry.getPathName(), "roster", zipper, "roster: "+entry.getId());
101
102                        // process image files if present
103                        if (entry.getImagePath() != null && ! entry.getImagePath().isEmpty())
104                            copyFileToStream(entry.getImagePath(), "roster", zipper, "image: "+entry.getId());
105                        if (entry.getIconPath() != null && ! entry.getIconPath().isEmpty())
106                            copyFileToStream(entry.getIconPath(), "roster", zipper, "icon: "+entry.getId());
107
108                    } catch (FileNotFoundException ex) {
109                        log.error("Unable to find file in entry {}", entry.getId(), ex);
110                    } catch (IOException ex) {
111                        log.error("Unable to write during entry {}", entry.getId(), ex);
112                    } catch (Exception ex) {
113                        log.error("Unexpected exception during entry {}", entry.getId(), ex);
114                    }
115                }
116
117                // Now the full roster entry
118                copyFileToStream(Roster.getDefault().getRosterIndexPath(), null, zipper, null);
119
120                zipper.setComment("Roster file saved from DecoderPro " + jmri.Version.name());
121
122                zipper.close();
123
124            } catch (FileNotFoundException ex) {
125                log.error("Unable to find file {}", filename, ex);
126            } catch (IOException ex) {
127                log.error("Unable to write to {}", filename, ex);
128            }
129        } finally {
130            ThreadingUtil.runOnGUIEventually(() -> {dialog.finish();});
131            log.info("Writing backup done");
132        }
133    }
134
135    /**
136     * Copy a file to an entry in a zip file.
137     * <p>
138     * The basename of the source file will be used in the zip file, placed in
139     * the directory of the zip file specified by dirname. If dirname is null,
140     * the file will be placed in the root level of the zip file.
141     *
142     * @param filename the file to copy
143     * @param dirname  the zip file "directory" to place this file in
144     * @param zipper   the ZipOutputStream
145     */
146    private void copyFileToStream(String filename, String dirname, ZipOutputStream zipper, String comment)
147            throws IOException {
148
149        log.debug("write: {}", filename);
150
151        File file = new File(filename);
152        String entryName;
153
154        if (dirname != null) {
155            entryName = dirname + "/" + file.getName();
156        } else {
157            entryName = file.getName();
158        }
159
160        ZipEntry zipEntry = new ZipEntry(entryName);
161
162        zipEntry.setTime(file.lastModified());
163        zipEntry.setSize(file.length());
164        if (comment != null) {
165            zipEntry.setComment(comment);
166        }
167
168        zipper.putNextEntry(zipEntry);
169
170        FileInputStream fis = new FileInputStream(file);
171        try {
172            int c;
173            while ((c = fis.read()) != -1) {
174                zipper.write(c);
175            }
176        } finally {
177            fis.close();
178        }
179
180        zipper.closeEntry();
181    }
182
183    // never invoked, because we overrode actionPerformed above
184    @Override
185    public jmri.util.swing.JmriPanel makePanel() {
186        throw new IllegalArgumentException("Should not be invoked");
187    }
188
189    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(FullBackupExportAction.class);
190}