001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.actions;
003
004import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
005import static org.openstreetmap.josm.tools.I18n.tr;
006import static org.openstreetmap.josm.tools.I18n.trn;
007
008import java.awt.event.ActionEvent;
009import java.awt.event.KeyEvent;
010import java.io.BufferedReader;
011import java.io.File;
012import java.io.IOException;
013import java.nio.charset.StandardCharsets;
014import java.nio.file.Files;
015import java.util.ArrayList;
016import java.util.Arrays;
017import java.util.Collection;
018import java.util.Collections;
019import java.util.HashSet;
020import java.util.LinkedHashSet;
021import java.util.LinkedList;
022import java.util.List;
023import java.util.Set;
024import java.util.regex.Matcher;
025import java.util.regex.Pattern;
026
027import javax.swing.JFileChooser;
028import javax.swing.JOptionPane;
029import javax.swing.SwingUtilities;
030import javax.swing.filechooser.FileFilter;
031
032import org.openstreetmap.josm.Main;
033import org.openstreetmap.josm.gui.HelpAwareOptionPane;
034import org.openstreetmap.josm.gui.PleaseWaitRunnable;
035import org.openstreetmap.josm.gui.help.HelpUtil;
036import org.openstreetmap.josm.io.AllFormatsImporter;
037import org.openstreetmap.josm.io.FileImporter;
038import org.openstreetmap.josm.io.OsmTransferException;
039import org.openstreetmap.josm.tools.MultiMap;
040import org.openstreetmap.josm.tools.Shortcut;
041import org.xml.sax.SAXException;
042
043/**
044 * Open a file chooser dialog and select an file to import. Then call the gpx-import driver. Finally
045 * open an internal frame into the main window with the gpx data shown.
046 *
047 * @author imi
048 */
049public class OpenFileAction extends DiskAccessAction {
050
051    /**
052     * The {@link ExtensionFileFilter} matching .url files
053     */
054    public static final ExtensionFileFilter urlFileFilter = new ExtensionFileFilter("url", "url", tr("URL Files") + " (*.url)");
055
056    /**
057     * Create an open action. The name is "Open a file".
058     */
059    public OpenFileAction() {
060        super(tr("Open..."), "open", tr("Open a file."),
061                Shortcut.registerShortcut("system:open", tr("File: {0}", tr("Open...")), KeyEvent.VK_O, Shortcut.CTRL));
062        putValue("help", ht("/Action/Open"));
063    }
064
065    @Override
066    public void actionPerformed(ActionEvent e) {
067        JFileChooser fc = createAndOpenFileChooser(true, true, null);
068        if (fc == null)
069            return;
070        File[] files = fc.getSelectedFiles();
071        OpenFileTask task = new OpenFileTask(Arrays.asList(files), fc.getFileFilter());
072        task.setRecordHistory(true);
073        Main.worker.submit(task);
074    }
075
076    @Override
077    protected void updateEnabledState() {
078        setEnabled(true);
079    }
080
081    /**
082     * Open a list of files. The complete list will be passed to batch importers.
083     * @param fileList A list of files
084     */
085    public static void openFiles(List<File> fileList) {
086        openFiles(fileList, false);
087    }
088
089    public static void openFiles(List<File> fileList, boolean recordHistory) {
090        OpenFileTask task = new OpenFileTask(fileList, null);
091        task.setRecordHistory(recordHistory);
092        Main.worker.submit(task);
093    }
094
095    public static class OpenFileTask extends PleaseWaitRunnable {
096        private List<File> files;
097        private List<File> successfullyOpenedFiles = new ArrayList<>();
098        private FileFilter fileFilter;
099        private boolean canceled;
100        private boolean recordHistory = false;
101
102        public OpenFileTask(List<File> files, FileFilter fileFilter, String title) {
103            super(title, false /* don't ignore exception */);
104            this.files = new ArrayList<>(files);
105            this.fileFilter = fileFilter;
106        }
107
108        public OpenFileTask(List<File> files, FileFilter fileFilter) {
109            this(files, fileFilter, tr("Opening files"));
110        }
111
112        /**
113         * save filename in history (for list of recently opened files)
114         * default: false
115         */
116        public void setRecordHistory(boolean recordHistory) {
117            this.recordHistory = recordHistory;
118        }
119
120        public boolean isRecordHistory() {
121            return recordHistory;
122        }
123
124        @Override
125        protected void cancel() {
126            this.canceled = true;
127        }
128
129        @Override
130        protected void finish() {
131            // do nothing
132        }
133
134        protected void alertFilesNotMatchingWithImporter(Collection<File> files, FileImporter importer) {
135            final StringBuilder msg = new StringBuilder();
136            msg.append("<html>");
137            msg.append(
138                    trn(
139                            "Cannot open {0} file with the file importer ''{1}''.",
140                            "Cannot open {0} files with the file importer ''{1}''.",
141                            files.size(),
142                            files.size(),
143                            importer.filter.getDescription()
144                    )
145            ).append("<br>");
146            msg.append("<ul>");
147            for (File f: files) {
148                msg.append("<li>").append(f.getAbsolutePath()).append("</li>");
149            }
150            msg.append("</ul>");
151
152            HelpAwareOptionPane.showMessageDialogInEDT(
153                    Main.parent,
154                    msg.toString(),
155                    tr("Warning"),
156                    JOptionPane.WARNING_MESSAGE,
157                    HelpUtil.ht("/Action/Open#ImporterCantImportFiles")
158            );
159        }
160
161        protected void alertFilesWithUnknownImporter(Collection<File> files) {
162            final StringBuilder msg = new StringBuilder();
163            msg.append("<html>");
164            msg.append(
165                    trn(
166                            "Cannot open {0} file because file does not exist or no suitable file importer is available.",
167                            "Cannot open {0} files because files do not exist or no suitable file importer is available.",
168                            files.size(),
169                            files.size()
170                    )
171            ).append("<br>");
172            msg.append("<ul>");
173            for (File f: files) {
174                msg.append("<li>");
175                msg.append(f.getAbsolutePath());
176                msg.append(" (<i>");
177                msg.append(f.exists() ? tr("no importer") : tr("does not exist"));
178                msg.append("</i>)</li>");
179            }
180            msg.append("</ul>");
181
182            HelpAwareOptionPane.showMessageDialogInEDT(
183                    Main.parent,
184                    msg.toString(),
185                    tr("Warning"),
186                    JOptionPane.WARNING_MESSAGE,
187                    HelpUtil.ht("/Action/Open#MissingImporterForFiles")
188            );
189        }
190
191        @Override
192        protected void realRun() throws SAXException, IOException, OsmTransferException {
193            if (files == null || files.isEmpty()) return;
194
195            /**
196             * Find the importer with the chosen file filter
197             */
198            FileImporter chosenImporter = null;
199            for (FileImporter importer : ExtensionFileFilter.importers) {
200                if (fileFilter == importer.filter) {
201                    chosenImporter = importer;
202                }
203            }
204            /**
205             * If the filter hasn't been changed in the dialog, chosenImporter is null now.
206             * When the filter has been set explicitly to AllFormatsImporter, treat this the same.
207             */
208            if (chosenImporter instanceof AllFormatsImporter) {
209                chosenImporter = null;
210            }
211            getProgressMonitor().setTicksCount(files.size());
212
213            if (chosenImporter != null) {
214                // The importer was explicitly chosen, so use it.
215                List<File> filesNotMatchingWithImporter = new LinkedList<>();
216                List<File> filesMatchingWithImporter = new LinkedList<>();
217                for (final File f : files) {
218                    if (!chosenImporter.acceptFile(f)) {
219                        if (f.isDirectory()) {
220                            SwingUtilities.invokeLater(new Runnable() {
221                                @Override
222                                public void run() {
223                                    JOptionPane.showMessageDialog(Main.parent, tr(
224                                            "<html>Cannot open directory ''{0}''.<br>Please select a file.</html>",
225                                            f.getAbsolutePath()), tr("Open file"), JOptionPane.ERROR_MESSAGE);
226                                }
227                            });
228                            // TODO when changing to Java 6: Don't cancel the
229                            // task here but use different modality. (Currently 2 dialogs
230                            // would block each other.)
231                            return;
232                        } else {
233                            filesNotMatchingWithImporter.add(f);
234                        }
235                    } else {
236                        filesMatchingWithImporter.add(f);
237                    }
238                }
239
240                if (!filesNotMatchingWithImporter.isEmpty()) {
241                    alertFilesNotMatchingWithImporter(filesNotMatchingWithImporter, chosenImporter);
242                }
243                if (!filesMatchingWithImporter.isEmpty()) {
244                    importData(chosenImporter, filesMatchingWithImporter);
245                }
246            } else {
247                // find appropriate importer
248                MultiMap<FileImporter, File> importerMap = new MultiMap<>();
249                List<File> filesWithUnknownImporter = new LinkedList<>();
250                List<File> urlFiles = new LinkedList<>();
251                FILES: for (File f : files) {
252                    for (FileImporter importer : ExtensionFileFilter.importers) {
253                        if (importer.acceptFile(f)) {
254                            importerMap.put(importer, f);
255                            continue FILES;
256                        }
257                    }
258                    if (urlFileFilter.accept(f)) {
259                        urlFiles.add(f);
260                    } else {
261                        filesWithUnknownImporter.add(f);
262                    }
263                }
264                if (!filesWithUnknownImporter.isEmpty()) {
265                    alertFilesWithUnknownImporter(filesWithUnknownImporter);
266                }
267                List<FileImporter> importers = new ArrayList<>(importerMap.keySet());
268                Collections.sort(importers);
269                Collections.reverse(importers);
270
271                Set<String> fileHistory = new LinkedHashSet<>();
272                Set<String> failedAll = new HashSet<>();
273
274                for (FileImporter importer : importers) {
275                    List<File> files = new ArrayList<>(importerMap.get(importer));
276                    importData(importer, files);
277                    // suppose all files will fail to load
278                    List<File> failedFiles = new ArrayList<>(files);
279
280                    if (recordHistory && !importer.isBatchImporter()) {
281                        // remove the files which didn't fail to load from the failed list
282                        failedFiles.removeAll(successfullyOpenedFiles);
283                        for (File f : successfullyOpenedFiles) {
284                            fileHistory.add(f.getCanonicalPath());
285                        }
286                        for (File f : failedFiles) {
287                            failedAll.add(f.getCanonicalPath());
288                        }
289                    }
290                }
291
292                for (File urlFile: urlFiles) {
293                    try (BufferedReader reader = Files.newBufferedReader(urlFile.toPath(), StandardCharsets.UTF_8)) {
294                        String line;
295                        while ((line = reader.readLine()) != null) {
296                            Matcher m = Pattern.compile(".*(https?://.*)").matcher(line);
297                            if (m.matches()) {
298                                String url = m.group(1);
299                                Main.main.menu.openLocation.openUrl(false, url);
300                            }
301                        }
302                    } catch (Exception e) {
303                        Main.error(e);
304                    }
305                }
306
307                if (recordHistory) {
308                    Collection<String> oldFileHistory = Main.pref.getCollection("file-open.history");
309                    fileHistory.addAll(oldFileHistory);
310                    // remove the files which failed to load from the list
311                    fileHistory.removeAll(failedAll);
312                    int maxsize = Math.max(0, Main.pref.getInteger("file-open.history.max-size", 15));
313                    Main.pref.putCollectionBounded("file-open.history", maxsize, fileHistory);
314                }
315            }
316        }
317
318        public void importData(FileImporter importer, List<File> files) {
319            if (importer.isBatchImporter()) {
320                if (canceled) return;
321                String msg = trn("Opening {0} file...", "Opening {0} files...", files.size(), files.size());
322                getProgressMonitor().setCustomText(msg);
323                getProgressMonitor().indeterminateSubTask(msg);
324                if (importer.importDataHandleExceptions(files, getProgressMonitor().createSubTaskMonitor(files.size(), false))) {
325                    successfullyOpenedFiles.addAll(files);
326                }
327            } else {
328                for (File f : files) {
329                    if (canceled) return;
330                    getProgressMonitor().indeterminateSubTask(tr("Opening file ''{0}'' ...", f.getAbsolutePath()));
331                    if (importer.importDataHandleExceptions(f, getProgressMonitor().createSubTaskMonitor(1, false))) {
332                        successfullyOpenedFiles.add(f);
333                    }
334                }
335            }
336        }
337
338        /**
339         * Replies the list of files that have been successfully opened.
340         * @return The list of files that have been successfully opened.
341         */
342        public List<File> getSuccessfullyOpenedFiles() {
343            return successfullyOpenedFiles;
344        }
345    }
346}