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

Last change on this file since 17414 was 16505, checked in by GerdP, 4 years ago

see #19296: Actions should avoid to install listeners which are not needed
tbc

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