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