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
101                        // process image files if present
102                        if (entry.getImagePath() != null && ! entry.getImagePath().isEmpty())
103                            copyFileToStream(entry.getImagePath(), "roster", zipper, "image: "+entry.getId());
104                        if (entry.getIconPath() != null && ! entry.getIconPath().isEmpty())
105                            copyFileToStream(entry.getIconPath(), "roster", zipper, "icon: "+entry.getId());
106
107                        // store the roster entry itself
108                        copyFileToStream(entry.getPathName(), "roster", zipper, "roster: "+entry.getId());
109
110                    } catch (FileNotFoundException ex) {
111                        log.error("Unable to find file in entry {}", entry.getId(), ex);
112                    } catch (IOException ex) {
113                        log.error("Unable to write during entry {}", entry.getId(), ex);
114                    } catch (Exception ex) {
115                        log.error("Unexpected exception during entry {}", entry.getId(), ex);
116                    }
117                }
118
119                // Now the full roster entry
120                copyFileToStream(Roster.getDefault().getRosterIndexPath(), null, zipper, null);
121
122                zipper.setComment("Roster file saved from DecoderPro " + jmri.Version.name());
123
124                zipper.close();
125
126            } catch (FileNotFoundException ex) {
127                log.error("Unable to find file {}", filename, ex);
128            } catch (IOException ex) {
129                log.error("Unable to write to {}", filename, ex);
130            }
131        } finally {
132            ThreadingUtil.runOnGUIEventually(() -> {dialog.finish();});
133            log.info("Writing backup done");
134        }
135    }
136
137    /**
138     * Copy a file to an entry in a zip file.
139     * <p>
140     * The basename of the source file will be used in the zip file, placed in
141     * the directory of the zip file specified by dirname. If dirname is null,
142     * the file will be placed in the root level of the zip file.
143     *
144     * @param filename the file to copy
145     * @param dirname  the zip file "directory" to place this file in
146     * @param zipper   the ZipOutputStream
147     */
148    private void copyFileToStream(String filename, String dirname, ZipOutputStream zipper, String comment)
149            throws IOException {
150
151        log.debug("write: {}", filename);
152
153        File file = new File(filename);
154        String entryName;
155
156        if (dirname != null) {
157            entryName = dirname + "/" + file.getName();
158        } else {
159            entryName = file.getName();
160        }
161
162        ZipEntry zipEntry = new ZipEntry(entryName);
163
164        zipEntry.setTime(file.lastModified());
165        zipEntry.setSize(file.length());
166        if (comment != null) {
167            zipEntry.setComment(comment);
168        }
169
170        zipper.putNextEntry(zipEntry);
171
172        FileInputStream fis = new FileInputStream(file);
173        try {
174            int c;
175            while ((c = fis.read()) != -1) {
176                zipper.write(c);
177            }
178        } finally {
179            fis.close();
180        }
181
182        zipper.closeEntry();
183    }
184
185    // never invoked, because we overrode actionPerformed above
186    @Override
187    public jmri.util.swing.JmriPanel makePanel() {
188        throw new IllegalArgumentException("Should not be invoked");
189    }
190
191    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(FullBackupExportAction.class);
192}