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}