001package jmri.jmrit.roster; 002 003import java.awt.Component; 004import java.awt.event.ActionEvent; 005 006import java.io.File; 007import java.io.FileInputStream; 008import java.io.FileNotFoundException; 009import java.io.IOException; 010import java.nio.file.Files; 011import java.nio.file.Paths; 012import java.text.SimpleDateFormat; 013import java.util.zip.ZipEntry; 014import java.util.zip.ZipInputStream; 015 016import javax.swing.Icon; 017import javax.swing.JFileChooser; 018import javax.swing.filechooser.FileNameExtensionFilter; 019 020import jmri.util.FileUtil; 021import jmri.util.swing.JmriJOptionPane; 022import jmri.util.swing.WindowInterface; 023 024import org.jdom2.Element; 025 026/** 027 * Reload the entire JMRI Roster ({@link jmri.jmrit.roster.Roster}) from a file 028 * previously stored by {@link jmri.jmrit.roster.FullBackupExportAction}. 029 * <p> 030 * Does not currently handle importing the group(s) that the entry belongs to. 031 * 032 * @author Bob Jacobsen Copyright 2014, 2018 033 */ 034public class FullBackupImportAction extends ImportRosterItemAction { 035 036 //private Component _who; 037 public FullBackupImportAction(String s, WindowInterface wi) { 038 super(s, wi); 039 } 040 041 public FullBackupImportAction(String s, Icon i, WindowInterface wi) { 042 super(s, i, wi); 043 } 044 045 /** 046 * @param title Name of this action, e.g. in menus 047 * @param parent Component that action is associated with, used to ensure 048 * proper position in of dialog boxes 049 */ 050 public FullBackupImportAction(String title, Component parent) { 051 super(title, parent); 052 } 053 054 boolean acceptAll; 055 boolean acceptAllDup; 056 057 @Override 058 public void actionPerformed(ActionEvent e) { 059 060 // ensure preferences will be found for read 061 FileUtil.createDirectory(Roster.getDefault().getRosterFilesLocation()); 062 063 // make sure instance loaded 064 Roster.getDefault(); 065 066 // set up to read import file 067 ZipInputStream zipper = null; 068 FileInputStream inputfile = null; 069 070 JFileChooser chooser = new jmri.util.swing.JmriJFileChooser(); 071 072 String roster_filename_extension = "roster"; 073 FileNameExtensionFilter filter = new FileNameExtensionFilter( 074 "JMRI full roster files", roster_filename_extension); 075 chooser.addChoosableFileFilter(filter); 076 077 int returnVal = chooser.showOpenDialog(mParent); 078 if (returnVal != JFileChooser.APPROVE_OPTION) { 079 return; 080 } 081 082 String filename = chooser.getSelectedFile().getAbsolutePath(); 083 084 try { 085 086 inputfile = new FileInputStream(filename); 087 zipper = new ZipInputStream(inputfile) { 088 @Override 089 public void close() { 090 } // SaxReader calls close when reading XML stream, ignore 091 // and close directly later 092 }; 093 094 // now iterate through each item in the stream. The get next 095 // entry call will return a ZipEntry for each file in the 096 // stream 097 ZipEntry entry; 098 acceptAll = false; // skip prompting for each entry and accept all 099 acceptAllDup = false; // skip prompting for dups and accept all 100 SimpleDateFormat isoDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); // NOI18N ISO8601 101 102 while ((entry = zipper.getNextEntry()) != null) { 103 log.debug("Entry: {} len {} ({}) added {} content: {}", 104 entry.getName(), 105 entry.getSize(), 106 entry.getCompressedSize(), 107 isoDateFormat.format(entry.getTime()), 108 entry.getComment() 109 ); 110 111 // Once we get the entry from the stream, the stream is 112 // positioned read to read the raw data, and we keep 113 // reading until read returns 0 or less. 114 115 // find type and process 116 // Unfortunately, the comment field doesn't carry through (see debug above) 117 // so we check the filename 118 if (entry.getName().endsWith(".xml") || entry.getName().endsWith(".XML")) { 119 boolean retval = processRosterFile(zipper); 120 if (!retval) break; 121 } else { 122 processImageFile(zipper, entry, entry.getName()); 123 } 124 125 } 126 127 } catch (FileNotFoundException ex) { 128 log.error("Unable to find {}", filename, ex); 129 } catch (IOException ex) { 130 log.error("Unable to read {}", filename, ex); 131 } finally { 132 if (inputfile != null) { 133 try { 134 inputfile.close(); // zipper.close() is meaningless, see above, but this will do 135 } catch (IOException ex) { 136 log.error("Unable to close {}", filename, ex); 137 } 138 } 139 } 140 141 } 142 143 void processImageFile(ZipInputStream zipper, ZipEntry entry, String path) throws IOException { 144 if (! path.startsWith("roster/")) { 145 log.error("Can't cope with image files outside the roster/ directory: {}", path); 146 return; 147 } 148 String fullPath = Roster.getDefault().getRosterFilesLocation()+path.substring(7); 149 log.debug("fullpath: {}", fullPath); 150 if (new File(fullPath).exists()) { 151 log.info("skipping existing file: {}", path); 152 return; 153 } 154 // and finally copy into place 155 Files.copy(zipper, Paths.get(fullPath)); 156 } 157 158 /** 159 * @return true if OK to continue to next entry 160 * @param zipper Stream to receive output 161 * @throws IOException from underlying operations 162 */ 163 164 protected boolean processRosterFile(ZipInputStream zipper) throws IOException { 165 166 try { 167 LocoFile xfile = new LocoFile(); // need a dummy object to do this operation in next line 168 Element lroot = xfile.rootFromInputStream(zipper).clone(); 169 if (lroot.getChild("locomotive") == null) { 170 return true; // that's the roster file 171 } 172 mToID = lroot.getChild("locomotive").getAttributeValue("id"); 173 174 // see if user wants to do it 175 int retval = 2; // accept if acceptall 176 if (!acceptAll) { 177 retval = JmriJOptionPane.showOptionDialog(mParent, 178 Bundle.getMessage("ConfirmImportID", mToID), 179 Bundle.getMessage("ConfirmImport"), 180 JmriJOptionPane.DEFAULT_OPTION, 181 JmriJOptionPane.INFORMATION_MESSAGE, 182 null, 183 new Object[]{Bundle.getMessage("CancelImports"), 184 Bundle.getMessage("Skip"), 185 Bundle.getMessage("ButtonOK"), 186 Bundle.getMessage("ButtonAcceptAll")}, 187 null); 188 } 189 if (retval == 0 || retval == JmriJOptionPane.CLOSED_OPTION ) { 190 // array position 0 cancel case, or Dialog closed 191 return false; 192 } 193 if (retval == 1) { 194 // array position 1 skip case 195 return true; 196 } 197 if (retval == 3) { 198 // array position 3 accept all case 199 acceptAll = true; 200 } 201 202 // see if duplicate 203 RosterEntry currentEntry = Roster.getDefault().getEntryForId(mToID); 204 205 if (currentEntry != null) { 206 if (!acceptAllDup) { 207 retval = JmriJOptionPane.showOptionDialog(mParent, 208 Bundle.getMessage("ConfirmImportDup", mToID), 209 Bundle.getMessage("ConfirmImport"), 210 JmriJOptionPane.DEFAULT_OPTION, 211 JmriJOptionPane.INFORMATION_MESSAGE, 212 null, 213 new Object[]{Bundle.getMessage("CancelImports"), 214 Bundle.getMessage("Skip"), 215 Bundle.getMessage("ButtonOK"), 216 Bundle.getMessage("ButtonAcceptAll")}, 217 null); 218 } 219 if (retval == 0 || retval == JmriJOptionPane.CLOSED_OPTION ) { 220 // array position 0 cancel case or Dialog closed 221 return false; 222 } 223 if (retval == 1) { 224 // array position 1 skip case 225 return true; 226 } 227 if (retval == 3) { 228 // array position 3 accept all case 229 acceptAllDup = true; 230 } 231 232 // turn file into backup 233 LocoFile df = new LocoFile(); // need a dummy object to do this operation in next line 234 df.makeBackupFile(Roster.getDefault().getRosterFilesLocation() + currentEntry.getFileName()); 235 236 // delete entry 237 Roster.getDefault().removeEntry(currentEntry); 238 239 } 240 241 loadEntryFromElement(lroot); 242 addToEntryToRoster(); 243 244 // use the new roster 245 Roster.getDefault().reloadRosterFile(); 246 } catch (org.jdom2.JDOMException ex) { 247 log.error("Unable to parse entry", ex); 248 } 249 250 return true; 251 } 252 253 private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(FullBackupImportAction.class); 254}