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