source: josm/trunk/src/org/openstreetmap/josm/actions/OpenFileAction.java@ 13766

Last change on this file since 13766 was 13727, checked in by Don-vip, 6 years ago

fix #14718 - display a warning when a file cannot be found

  • Property svn:eol-style set to native
File size: 16.3 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.actions;
3
4import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
5import static org.openstreetmap.josm.tools.I18n.tr;
6import static org.openstreetmap.josm.tools.I18n.trn;
7
8import java.awt.event.ActionEvent;
9import java.awt.event.KeyEvent;
10import java.io.BufferedReader;
11import java.io.File;
12import java.io.IOException;
13import java.nio.charset.StandardCharsets;
14import java.nio.file.Files;
15import java.util.ArrayList;
16import java.util.Arrays;
17import java.util.Collection;
18import java.util.Collections;
19import java.util.HashSet;
20import java.util.LinkedHashSet;
21import java.util.LinkedList;
22import java.util.List;
23import java.util.Set;
24import java.util.concurrent.Future;
25import java.util.regex.Matcher;
26import java.util.regex.Pattern;
27import java.util.regex.PatternSyntaxException;
28
29import javax.swing.JOptionPane;
30import javax.swing.SwingUtilities;
31import javax.swing.filechooser.FileFilter;
32
33import org.openstreetmap.josm.Main;
34import org.openstreetmap.josm.data.PreferencesUtils;
35import org.openstreetmap.josm.gui.HelpAwareOptionPane;
36import org.openstreetmap.josm.gui.MainApplication;
37import org.openstreetmap.josm.gui.MapFrame;
38import org.openstreetmap.josm.gui.Notification;
39import org.openstreetmap.josm.gui.PleaseWaitRunnable;
40import org.openstreetmap.josm.gui.io.importexport.AllFormatsImporter;
41import org.openstreetmap.josm.gui.io.importexport.FileImporter;
42import org.openstreetmap.josm.gui.widgets.AbstractFileChooser;
43import org.openstreetmap.josm.io.OsmTransferException;
44import org.openstreetmap.josm.spi.preferences.Config;
45import org.openstreetmap.josm.tools.Logging;
46import org.openstreetmap.josm.tools.MultiMap;
47import org.openstreetmap.josm.tools.Shortcut;
48import org.openstreetmap.josm.tools.Utils;
49import org.xml.sax.SAXException;
50
51/**
52 * Open a file chooser dialog and select a file to import.
53 *
54 * @author imi
55 * @since 1146
56 */
57public class OpenFileAction extends DiskAccessAction {
58
59 /**
60 * The {@link ExtensionFileFilter} matching .url files
61 */
62 public static final ExtensionFileFilter URL_FILE_FILTER = new ExtensionFileFilter("url", "url", tr("URL Files") + " (*.url)");
63
64 /**
65 * Create an open action. The name is "Open a file".
66 */
67 public OpenFileAction() {
68 super(tr("Open..."), "open", tr("Open a file."),
69 Shortcut.registerShortcut("system:open", tr("File: {0}", tr("Open...")), KeyEvent.VK_O, Shortcut.CTRL));
70 putValue("help", ht("/Action/Open"));
71 }
72
73 @Override
74 public void actionPerformed(ActionEvent e) {
75 AbstractFileChooser fc = createAndOpenFileChooser(true, true, null);
76 if (fc == null)
77 return;
78 File[] files = fc.getSelectedFiles();
79 OpenFileTask task = new OpenFileTask(Arrays.asList(files), fc.getFileFilter());
80 task.setRecordHistory(true);
81 MainApplication.worker.submit(task);
82 }
83
84 @Override
85 protected void updateEnabledState() {
86 setEnabled(true);
87 }
88
89 /**
90 * Open a list of files. The complete list will be passed to batch importers.
91 * Filenames will not be saved in history.
92 * @param fileList A list of files
93 * @return the future task
94 * @since 11986 (return task)
95 */
96 public static Future<?> openFiles(List<File> fileList) {
97 return openFiles(fileList, false);
98 }
99
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(Main.platform.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 Main.parent,
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 Main.parent,
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(Main.parent, 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 for (File urlFile: urlFiles) {
323 try (BufferedReader reader = Files.newBufferedReader(urlFile.toPath(), StandardCharsets.UTF_8)) {
324 String line;
325 while ((line = reader.readLine()) != null) {
326 Matcher m = Pattern.compile(".*(https?://.*)").matcher(line);
327 if (m.matches()) {
328 String url = m.group(1);
329 MainApplication.getMenu().openLocation.openUrl(false, url);
330 }
331 }
332 } catch (IOException | PatternSyntaxException | IllegalStateException | IndexOutOfBoundsException e) {
333 Logging.error(e);
334 }
335 }
336 }
337
338 if (recordHistory) {
339 Collection<String> oldFileHistory = Config.getPref().getList("file-open.history");
340 fileHistory.addAll(oldFileHistory);
341 // remove the files which failed to load from the list
342 fileHistory.removeAll(failedAll);
343 int maxsize = Math.max(0, Config.getPref().getInt("file-open.history.max-size", 15));
344 PreferencesUtils.putListBounded(Config.getPref(), "file-open.history", maxsize, new ArrayList<>(fileHistory));
345 }
346 }
347
348 /**
349 * Import data files with the given importer.
350 * @param importer file importer
351 * @param files data files to import
352 */
353 public void importData(FileImporter importer, List<File> files) {
354 if (importer.isBatchImporter()) {
355 if (canceled) return;
356 String msg = trn("Opening {0} file...", "Opening {0} files...", files.size(), files.size());
357 getProgressMonitor().setCustomText(msg);
358 getProgressMonitor().indeterminateSubTask(msg);
359 if (importer.importDataHandleExceptions(files, getProgressMonitor().createSubTaskMonitor(files.size(), false))) {
360 successfullyOpenedFiles.addAll(files);
361 }
362 } else {
363 for (File f : files) {
364 if (canceled) return;
365 getProgressMonitor().indeterminateSubTask(tr("Opening file ''{0}'' ...", f.getAbsolutePath()));
366 if (importer.importDataHandleExceptions(f, getProgressMonitor().createSubTaskMonitor(1, false))) {
367 successfullyOpenedFiles.add(f);
368 }
369 }
370 }
371 if (recordHistory && !importer.isBatchImporter()) {
372 for (File f : files) {
373 try {
374 if (successfullyOpenedFiles.contains(f)) {
375 fileHistory.add(f.getCanonicalPath());
376 } else {
377 failedAll.add(f.getCanonicalPath());
378 }
379 } catch (IOException e) {
380 Logging.warn(e);
381 }
382 }
383 }
384 }
385
386 /**
387 * Replies the list of files that have been successfully opened.
388 * @return The list of files that have been successfully opened.
389 */
390 public List<File> getSuccessfullyOpenedFiles() {
391 return successfullyOpenedFiles;
392 }
393 }
394}
Note: See TracBrowser for help on using the repository browser.