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