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