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}