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