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}