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