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}