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 ThreadingUtil.runOnGUIEventually(() -> {dialog.finish();}); 167 if (inputfile != null) { 168 try { 169 inputfile.close(); // zipper.close() is meaningless, see above, but this will do 170 } catch (IOException ex) { 171 log.error("Unable to close {}", filename, ex); 172 } 173 log.info("Reading backup done"); 174 } 175 } 176 177 } 178 179 void processImageFile(ZipInputStream zipper, ZipEntry entry, String path) throws IOException { 180 if (! path.startsWith("roster/")) { 181 log.error("Can't cope with image files outside the roster/ directory: {}", path); 182 return; 183 } 184 String fullPath = Roster.getDefault().getRosterFilesLocation()+path.substring(7); 185 log.debug("fullpath: {}", fullPath); 186 if (new File(fullPath).exists()) { 187 log.info("skipping existing file: {}", path); 188 return; 189 } 190 // and finally copy into place 191 Files.copy(zipper, Paths.get(fullPath)); 192 } 193 194 /** 195 * @param zipper Stream to receive output 196 * @return true if OK to continue to next entry 197 * @throws IOException from underlying operations 198 */ 199 200 protected boolean processRosterFile(ZipInputStream zipper) throws IOException { 201 202 try { 203 LocoFile xfile = new LocoFile(); // need a dummy object to do this operation in next line 204 Element lroot = xfile.rootFromInputStream(zipper).clone(); 205 if (lroot.getChild("locomotive") == null) { 206 return true; // that's the roster file 207 } 208 mToID = lroot.getChild("locomotive").getAttributeValue("id"); 209 210 // see if user wants to do it 211 int retval = 2; // accept if acceptall 212 if (!acceptAll) { 213 retval = JmriJOptionPane.showOptionDialog(mParent, 214 Bundle.getMessage("ConfirmImportID", mToID), 215 Bundle.getMessage("ConfirmImport"), 216 JmriJOptionPane.DEFAULT_OPTION, 217 JmriJOptionPane.INFORMATION_MESSAGE, 218 null, 219 new Object[]{Bundle.getMessage("CancelImports"), 220 Bundle.getMessage("Skip"), 221 Bundle.getMessage("ButtonOK"), 222 Bundle.getMessage("ButtonAcceptAll")}, 223 null); 224 } 225 if (retval == 0 || retval == JmriJOptionPane.CLOSED_OPTION ) { 226 // array position 0 cancel case, or Dialog closed 227 return false; 228 } 229 if (retval == 1) { 230 // array position 1 skip case 231 return true; 232 } 233 if (retval == 3) { 234 // array position 3 accept all case 235 acceptAll = true; 236 } 237 238 // see if duplicate 239 RosterEntry currentEntry = Roster.getDefault().getEntryForId(mToID); 240 241 if (currentEntry != null) { 242 if (!acceptAllDup) { 243 retval = JmriJOptionPane.showOptionDialog(mParent, 244 Bundle.getMessage("ConfirmImportDup", mToID), 245 Bundle.getMessage("ConfirmImport"), 246 JmriJOptionPane.DEFAULT_OPTION, 247 JmriJOptionPane.INFORMATION_MESSAGE, 248 null, 249 new Object[]{Bundle.getMessage("CancelImports"), 250 Bundle.getMessage("Skip"), 251 Bundle.getMessage("ButtonOK"), 252 Bundle.getMessage("ButtonAcceptAll")}, 253 null); 254 } 255 if (retval == 0 || retval == JmriJOptionPane.CLOSED_OPTION ) { 256 // array position 0 cancel case or Dialog closed 257 return false; 258 } 259 if (retval == 1) { 260 // array position 1 skip case 261 return true; 262 } 263 if (retval == 3) { 264 // array position 3 accept all case 265 acceptAllDup = true; 266 } 267 268 // turn file into backup 269 LocoFile df = new LocoFile(); // need a dummy object to do this operation in next line 270 df.makeBackupFile(Roster.getDefault().getRosterFilesLocation() + currentEntry.getFileName()); 271 272 // delete entry 273 Roster.getDefault().removeEntry(currentEntry); 274 275 } 276 277 loadEntryFromElement(lroot); 278 addToEntryToRoster(); 279 280 // use the new roster 281 Roster.getDefault().reloadRosterFile(); 282 } catch (org.jdom2.JDOMException ex) { 283 log.error("Unable to parse entry", ex); 284 } 285 286 return true; 287 } 288 289 private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(FullBackupImportAction.class); 290}