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}