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