[6380] | 1 | // License: GPL. For details, see LICENSE file.
|
---|
[1146] | 2 | package org.openstreetmap.josm.actions;
|
---|
| 3 |
|
---|
[2563] | 4 | import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
|
---|
[1146] | 5 | import static org.openstreetmap.josm.tools.I18n.tr;
|
---|
[2842] | 6 | import static org.openstreetmap.josm.tools.I18n.trn;
|
---|
[1146] | 7 |
|
---|
| 8 | import java.awt.event.ActionEvent;
|
---|
| 9 | import java.awt.event.KeyEvent;
|
---|
[4996] | 10 | import java.io.BufferedReader;
|
---|
[1146] | 11 | import java.io.File;
|
---|
| 12 | import java.io.IOException;
|
---|
[7082] | 13 | import java.nio.charset.StandardCharsets;
|
---|
[7315] | 14 | import java.nio.file.Files;
|
---|
[2702] | 15 | import java.util.ArrayList;
|
---|
[2047] | 16 | import java.util.Arrays;
|
---|
[2980] | 17 | import java.util.Collection;
|
---|
[2702] | 18 | import java.util.Collections;
|
---|
[3924] | 19 | import java.util.HashSet;
|
---|
[3710] | 20 | import java.util.LinkedHashSet;
|
---|
[2980] | 21 | import java.util.LinkedList;
|
---|
[2047] | 22 | import java.util.List;
|
---|
[4247] | 23 | import java.util.Set;
|
---|
[11986] | 24 | import java.util.concurrent.Future;
|
---|
[4996] | 25 | import java.util.regex.Matcher;
|
---|
| 26 | import java.util.regex.Pattern;
|
---|
[10212] | 27 | import java.util.regex.PatternSyntaxException;
|
---|
[1146] | 28 |
|
---|
| 29 | import javax.swing.JOptionPane;
|
---|
[2994] | 30 | import javax.swing.SwingUtilities;
|
---|
[2702] | 31 | import javax.swing.filechooser.FileFilter;
|
---|
[1146] | 32 |
|
---|
[12891] | 33 | import org.openstreetmap.josm.data.PreferencesUtils;
|
---|
[2980] | 34 | import org.openstreetmap.josm.gui.HelpAwareOptionPane;
|
---|
[12630] | 35 | import org.openstreetmap.josm.gui.MainApplication;
|
---|
| 36 | import org.openstreetmap.josm.gui.MapFrame;
|
---|
[13727] | 37 | import org.openstreetmap.josm.gui.Notification;
|
---|
[2047] | 38 | import org.openstreetmap.josm.gui.PleaseWaitRunnable;
|
---|
[12671] | 39 | import org.openstreetmap.josm.gui.io.importexport.AllFormatsImporter;
|
---|
| 40 | import org.openstreetmap.josm.gui.io.importexport.FileImporter;
|
---|
[7578] | 41 | import org.openstreetmap.josm.gui.widgets.AbstractFileChooser;
|
---|
[2047] | 42 | import org.openstreetmap.josm.io.OsmTransferException;
|
---|
[12846] | 43 | import org.openstreetmap.josm.spi.preferences.Config;
|
---|
[12620] | 44 | import org.openstreetmap.josm.tools.Logging;
|
---|
[2702] | 45 | import org.openstreetmap.josm.tools.MultiMap;
|
---|
[14138] | 46 | import org.openstreetmap.josm.tools.PlatformManager;
|
---|
[1146] | 47 | import org.openstreetmap.josm.tools.Shortcut;
|
---|
[11848] | 48 | import org.openstreetmap.josm.tools.Utils;
|
---|
[2047] | 49 | import org.xml.sax.SAXException;
|
---|
[1146] | 50 |
|
---|
| 51 | /**
|
---|
[7453] | 52 | * Open a file chooser dialog and select a file to import.
|
---|
[1146] | 53 | *
|
---|
| 54 | * @author imi
|
---|
[7453] | 55 | * @since 1146
|
---|
[1146] | 56 | */
|
---|
| 57 | public class OpenFileAction extends DiskAccessAction {
|
---|
| 58 |
|
---|
[5874] | 59 | /**
|
---|
| 60 | * The {@link ExtensionFileFilter} matching .url files
|
---|
| 61 | */
|
---|
[7859] | 62 | public static final ExtensionFileFilter URL_FILE_FILTER = new ExtensionFileFilter("url", "url", tr("URL Files") + " (*.url)");
|
---|
[4996] | 63 |
|
---|
[1169] | 64 | /**
|
---|
| 65 | * Create an open action. The name is "Open a file".
|
---|
| 66 | */
|
---|
| 67 | public OpenFileAction() {
|
---|
[1212] | 68 | super(tr("Open..."), "open", tr("Open a file."),
|
---|
[4982] | 69 | Shortcut.registerShortcut("system:open", tr("File: {0}", tr("Open...")), KeyEvent.VK_O, Shortcut.CTRL));
|
---|
[14397] | 70 | setHelpId(ht("/Action/Open"));
|
---|
[1169] | 71 | }
|
---|
[1146] | 72 |
|
---|
[6084] | 73 | @Override
|
---|
[1169] | 74 | public void actionPerformed(ActionEvent e) {
|
---|
[7578] | 75 | AbstractFileChooser fc = createAndOpenFileChooser(true, true, null);
|
---|
[1169] | 76 | if (fc == null)
|
---|
| 77 | return;
|
---|
| 78 | File[] files = fc.getSelectedFiles();
|
---|
[2702] | 79 | OpenFileTask task = new OpenFileTask(Arrays.asList(files), fc.getFileFilter());
|
---|
[3710] | 80 | task.setRecordHistory(true);
|
---|
[12634] | 81 | MainApplication.worker.submit(task);
|
---|
[1169] | 82 | }
|
---|
[1146] | 83 |
|
---|
[1879] | 84 | @Override
|
---|
| 85 | protected void updateEnabledState() {
|
---|
[7026] | 86 | setEnabled(true);
|
---|
[1879] | 87 | }
|
---|
[2047] | 88 |
|
---|
[2798] | 89 | /**
|
---|
| 90 | * Open a list of files. The complete list will be passed to batch importers.
|
---|
[9230] | 91 | * Filenames will not be saved in history.
|
---|
[2798] | 92 | * @param fileList A list of files
|
---|
[11986] | 93 | * @return the future task
|
---|
| 94 | * @since 11986 (return task)
|
---|
[2798] | 95 | */
|
---|
[11986] | 96 | public static Future<?> openFiles(List<File> fileList) {
|
---|
| 97 | return openFiles(fileList, false);
|
---|
[3924] | 98 | }
|
---|
| 99 |
|
---|
[9230] | 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)
|
---|
[11986] | 104 | * @return the future task
|
---|
| 105 | * @since 11986 (return task)
|
---|
[9230] | 106 | */
|
---|
[11986] | 107 | public static Future<?> openFiles(List<File> fileList, boolean recordHistory) {
|
---|
[2798] | 108 | OpenFileTask task = new OpenFileTask(fileList, null);
|
---|
[3924] | 109 | task.setRecordHistory(recordHistory);
|
---|
[12634] | 110 | return MainApplication.worker.submit(task);
|
---|
[2703] | 111 | }
|
---|
| 112 |
|
---|
[9230] | 113 | /**
|
---|
| 114 | * Task to open files.
|
---|
| 115 | */
|
---|
[6889] | 116 | public static class OpenFileTask extends PleaseWaitRunnable {
|
---|
[7453] | 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;
|
---|
[4310] | 122 | private boolean canceled;
|
---|
[8840] | 123 | private boolean recordHistory;
|
---|
[2047] | 124 |
|
---|
[9230] | 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 | */
|
---|
[8202] | 131 | public OpenFileTask(final List<File> files, final FileFilter fileFilter, final String title) {
|
---|
[3679] | 132 | super(title, false /* don't ignore exception */);
|
---|
[2702] | 133 | this.fileFilter = fileFilter;
|
---|
[8202] | 134 | this.files = new ArrayList<>(files.size());
|
---|
| 135 | for (final File file : files) {
|
---|
| 136 | if (file.exists()) {
|
---|
[14138] | 137 | this.files.add(PlatformManager.getPlatform().resolveFileLink(file));
|
---|
[8705] | 138 | } else if (file.getParentFile() != null) {
|
---|
[8202] | 139 | // try to guess an extension using the specified fileFilter
|
---|
[10601] | 140 | final File[] matchingFiles = file.getParentFile().listFiles((dir, name) ->
|
---|
| 141 | name.startsWith(file.getName()) && fileFilter != null && fileFilter.accept(new File(dir, name)));
|
---|
[8308] | 142 | if (matchingFiles != null && matchingFiles.length == 1) {
|
---|
[8202] | 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 | }
|
---|
[13727] | 149 | } else {
|
---|
| 150 | String message = tr("Unable to locate file ''{0}''.", file.getPath());
|
---|
| 151 | Logging.warn(message);
|
---|
| 152 | new Notification(message).show();
|
---|
[8202] | 153 | }
|
---|
| 154 | }
|
---|
[2047] | 155 | }
|
---|
[3679] | 156 |
|
---|
[9230] | 157 | /**
|
---|
| 158 | * Constructs a new {@code OpenFileTask}.
|
---|
| 159 | * @param files files to open
|
---|
| 160 | * @param fileFilter file filter
|
---|
| 161 | */
|
---|
[3679] | 162 | public OpenFileTask(List<File> files, FileFilter fileFilter) {
|
---|
| 163 | this(files, fileFilter, tr("Opening files"));
|
---|
| 164 | }
|
---|
| 165 |
|
---|
[3710] | 166 | /**
|
---|
[9230] | 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)
|
---|
[3710] | 169 | */
|
---|
| 170 | public void setRecordHistory(boolean recordHistory) {
|
---|
| 171 | this.recordHistory = recordHistory;
|
---|
| 172 | }
|
---|
| 173 |
|
---|
[9230] | 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 | */
|
---|
[3710] | 178 | public boolean isRecordHistory() {
|
---|
| 179 | return recordHistory;
|
---|
| 180 | }
|
---|
| 181 |
|
---|
[2047] | 182 | @Override
|
---|
| 183 | protected void cancel() {
|
---|
[4310] | 184 | this.canceled = true;
|
---|
[2047] | 185 | }
|
---|
| 186 |
|
---|
| 187 | @Override
|
---|
| 188 | protected void finish() {
|
---|
[12630] | 189 | MapFrame map = MainApplication.getMap();
|
---|
| 190 | if (map != null) {
|
---|
| 191 | map.repaint();
|
---|
[9707] | 192 | }
|
---|
[2047] | 193 | }
|
---|
| 194 |
|
---|
[2980] | 195 | protected void alertFilesNotMatchingWithImporter(Collection<File> files, FileImporter importer) {
|
---|
[10242] | 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(),
|
---|
[11848] | 201 | Utils.escapeReservedCharactersHTML(importer.filter.getDescription())
|
---|
[2980] | 202 | )
|
---|
[8390] | 203 | ).append("<br><ul>");
|
---|
[2980] | 204 | for (File f: files) {
|
---|
| 205 | msg.append("<li>").append(f.getAbsolutePath()).append("</li>");
|
---|
| 206 | }
|
---|
[10242] | 207 | msg.append("</ul></html>");
|
---|
[2980] | 208 |
|
---|
[3501] | 209 | HelpAwareOptionPane.showMessageDialogInEDT(
|
---|
[14153] | 210 | MainApplication.getMainFrame(),
|
---|
[3501] | 211 | msg.toString(),
|
---|
| 212 | tr("Warning"),
|
---|
| 213 | JOptionPane.WARNING_MESSAGE,
|
---|
[9972] | 214 | ht("/Action/Open#ImporterCantImportFiles")
|
---|
[3501] | 215 | );
|
---|
[2980] | 216 | }
|
---|
| 217 |
|
---|
| 218 | protected void alertFilesWithUnknownImporter(Collection<File> files) {
|
---|
[10242] | 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()
|
---|
[2980] | 224 | )
|
---|
[8390] | 225 | ).append("<br><ul>");
|
---|
[2980] | 226 | for (File f: files) {
|
---|
[8379] | 227 | msg.append("<li>").append(f.getAbsolutePath()).append(" (<i>")
|
---|
| 228 | .append(f.exists() ? tr("no importer") : tr("does not exist"))
|
---|
| 229 | .append("</i>)</li>");
|
---|
[2980] | 230 | }
|
---|
[10242] | 231 | msg.append("</ul></html>");
|
---|
[3530] | 232 |
|
---|
[3501] | 233 | HelpAwareOptionPane.showMessageDialogInEDT(
|
---|
[14153] | 234 | MainApplication.getMainFrame(),
|
---|
[3501] | 235 | msg.toString(),
|
---|
| 236 | tr("Warning"),
|
---|
| 237 | JOptionPane.WARNING_MESSAGE,
|
---|
[9972] | 238 | ht("/Action/Open#MissingImporterForFiles")
|
---|
[3501] | 239 | );
|
---|
[2980] | 240 | }
|
---|
| 241 |
|
---|
[2047] | 242 | @Override
|
---|
| 243 | protected void realRun() throws SAXException, IOException, OsmTransferException {
|
---|
| 244 | if (files == null || files.isEmpty()) return;
|
---|
[2702] | 245 |
|
---|
| 246 | /**
|
---|
| 247 | * Find the importer with the chosen file filter
|
---|
| 248 | */
|
---|
| 249 | FileImporter chosenImporter = null;
|
---|
[7865] | 250 | if (fileFilter != null) {
|
---|
[10407] | 251 | for (FileImporter importer : ExtensionFileFilter.getImporters()) {
|
---|
[7865] | 252 | if (fileFilter.equals(importer.filter)) {
|
---|
| 253 | chosenImporter = importer;
|
---|
| 254 | }
|
---|
[2702] | 255 | }
|
---|
| 256 | }
|
---|
| 257 | /**
|
---|
[3710] | 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.
|
---|
[2702] | 260 | */
|
---|
| 261 | if (chosenImporter instanceof AllFormatsImporter) {
|
---|
| 262 | chosenImporter = null;
|
---|
| 263 | }
|
---|
[2851] | 264 | getProgressMonitor().setTicksCount(files.size());
|
---|
[2702] | 265 |
|
---|
[2980] | 266 | if (chosenImporter != null) {
|
---|
[4247] | 267 | // The importer was explicitly chosen, so use it.
|
---|
[7005] | 268 | List<File> filesNotMatchingWithImporter = new LinkedList<>();
|
---|
| 269 | List<File> filesMatchingWithImporter = new LinkedList<>();
|
---|
[2994] | 270 | for (final File f : files) {
|
---|
[2702] | 271 | if (!chosenImporter.acceptFile(f)) {
|
---|
| 272 | if (f.isDirectory()) {
|
---|
[14153] | 273 | SwingUtilities.invokeLater(() -> JOptionPane.showMessageDialog(MainApplication.getMainFrame(), tr(
|
---|
[10601] | 274 | "<html>Cannot open directory ''{0}''.<br>Please select a file.</html>",
|
---|
| 275 | f.getAbsolutePath()), tr("Open file"), JOptionPane.ERROR_MESSAGE));
|
---|
[10242] | 276 | // TODO when changing to Java 6: Don't cancel the task here but use different modality. (Currently 2 dialogs
|
---|
[2994] | 277 | // would block each other.)
|
---|
| 278 | return;
|
---|
[2980] | 279 | } else {
|
---|
| 280 | filesNotMatchingWithImporter.add(f);
|
---|
| 281 | }
|
---|
| 282 | } else {
|
---|
| 283 | filesMatchingWithImporter.add(f);
|
---|
[2702] | 284 | }
|
---|
| 285 | }
|
---|
[2980] | 286 |
|
---|
| 287 | if (!filesNotMatchingWithImporter.isEmpty()) {
|
---|
| 288 | alertFilesNotMatchingWithImporter(filesNotMatchingWithImporter, chosenImporter);
|
---|
| 289 | }
|
---|
[2994] | 290 | if (!filesMatchingWithImporter.isEmpty()) {
|
---|
[2980] | 291 | importData(chosenImporter, filesMatchingWithImporter);
|
---|
| 292 | }
|
---|
| 293 | } else {
|
---|
| 294 | // find appropriate importer
|
---|
[7005] | 295 | MultiMap<FileImporter, File> importerMap = new MultiMap<>();
|
---|
| 296 | List<File> filesWithUnknownImporter = new LinkedList<>();
|
---|
| 297 | List<File> urlFiles = new LinkedList<>();
|
---|
[2980] | 298 | FILES: for (File f : files) {
|
---|
[10407] | 299 | for (FileImporter importer : ExtensionFileFilter.getImporters()) {
|
---|
[2980] | 300 | if (importer.acceptFile(f)) {
|
---|
[3924] | 301 | importerMap.put(importer, f);
|
---|
[2980] | 302 | continue FILES;
|
---|
[2702] | 303 | }
|
---|
| 304 | }
|
---|
[7859] | 305 | if (URL_FILE_FILTER.accept(f)) {
|
---|
[4996] | 306 | urlFiles.add(f);
|
---|
| 307 | } else {
|
---|
| 308 | filesWithUnknownImporter.add(f);
|
---|
| 309 | }
|
---|
[2980] | 310 | }
|
---|
| 311 | if (!filesWithUnknownImporter.isEmpty()) {
|
---|
| 312 | alertFilesWithUnknownImporter(filesWithUnknownImporter);
|
---|
| 313 | }
|
---|
[7005] | 314 | List<FileImporter> importers = new ArrayList<>(importerMap.keySet());
|
---|
[3924] | 315 | Collections.sort(importers);
|
---|
| 316 | Collections.reverse(importers);
|
---|
[3710] | 317 |
|
---|
[3924] | 318 | for (FileImporter importer : importers) {
|
---|
[7453] | 319 | importData(importer, new ArrayList<>(importerMap.get(importer)));
|
---|
[2702] | 320 | }
|
---|
[6069] | 321 |
|
---|
[14201] | 322 | Pattern urlPattern = Pattern.compile(".*(https?://.*)");
|
---|
[4996] | 323 | for (File urlFile: urlFiles) {
|
---|
[7315] | 324 | try (BufferedReader reader = Files.newBufferedReader(urlFile.toPath(), StandardCharsets.UTF_8)) {
|
---|
[4996] | 325 | String line;
|
---|
| 326 | while ((line = reader.readLine()) != null) {
|
---|
[14201] | 327 | Matcher m = urlPattern.matcher(line);
|
---|
[4996] | 328 | if (m.matches()) {
|
---|
| 329 | String url = m.group(1);
|
---|
[12643] | 330 | MainApplication.getMenu().openLocation.openUrl(false, url);
|
---|
[4996] | 331 | }
|
---|
| 332 | }
|
---|
[10212] | 333 | } catch (IOException | PatternSyntaxException | IllegalStateException | IndexOutOfBoundsException e) {
|
---|
[12620] | 334 | Logging.error(e);
|
---|
[4996] | 335 | }
|
---|
| 336 | }
|
---|
[7453] | 337 | }
|
---|
[3710] | 338 |
|
---|
[7453] | 339 | if (recordHistory) {
|
---|
[12846] | 340 | Collection<String> oldFileHistory = Config.getPref().getList("file-open.history");
|
---|
[7453] | 341 | fileHistory.addAll(oldFileHistory);
|
---|
| 342 | // remove the files which failed to load from the list
|
---|
| 343 | fileHistory.removeAll(failedAll);
|
---|
[12846] | 344 | int maxsize = Math.max(0, Config.getPref().getInt("file-open.history.max-size", 15));
|
---|
[12894] | 345 | PreferencesUtils.putListBounded(Config.getPref(), "file-open.history", maxsize, new ArrayList<>(fileHistory));
|
---|
[2702] | 346 | }
|
---|
| 347 | }
|
---|
| 348 |
|
---|
[9230] | 349 | /**
|
---|
| 350 | * Import data files with the given importer.
|
---|
| 351 | * @param importer file importer
|
---|
| 352 | * @param files data files to import
|
---|
| 353 | */
|
---|
[2702] | 354 | public void importData(FileImporter importer, List<File> files) {
|
---|
| 355 | if (importer.isBatchImporter()) {
|
---|
[4310] | 356 | if (canceled) return;
|
---|
[4393] | 357 | String msg = trn("Opening {0} file...", "Opening {0} files...", files.size(), files.size());
|
---|
[2980] | 358 | getProgressMonitor().setCustomText(msg);
|
---|
[2702] | 359 | getProgressMonitor().indeterminateSubTask(msg);
|
---|
[3679] | 360 | if (importer.importDataHandleExceptions(files, getProgressMonitor().createSubTaskMonitor(files.size(), false))) {
|
---|
| 361 | successfullyOpenedFiles.addAll(files);
|
---|
| 362 | }
|
---|
[2702] | 363 | } else {
|
---|
| 364 | for (File f : files) {
|
---|
[4310] | 365 | if (canceled) return;
|
---|
[2702] | 366 | getProgressMonitor().indeterminateSubTask(tr("Opening file ''{0}'' ...", f.getAbsolutePath()));
|
---|
[3679] | 367 | if (importer.importDataHandleExceptions(f, getProgressMonitor().createSubTaskMonitor(1, false))) {
|
---|
| 368 | successfullyOpenedFiles.add(f);
|
---|
| 369 | }
|
---|
[2702] | 370 | }
|
---|
[2047] | 371 | }
|
---|
[7453] | 372 | if (recordHistory && !importer.isBatchImporter()) {
|
---|
| 373 | for (File f : files) {
|
---|
| 374 | try {
|
---|
| 375 | if (successfullyOpenedFiles.contains(f)) {
|
---|
| 376 | fileHistory.add(f.getCanonicalPath());
|
---|
| 377 | } else {
|
---|
| 378 | failedAll.add(f.getCanonicalPath());
|
---|
| 379 | }
|
---|
| 380 | } catch (IOException e) {
|
---|
[12620] | 381 | Logging.warn(e);
|
---|
[7453] | 382 | }
|
---|
| 383 | }
|
---|
| 384 | }
|
---|
[2047] | 385 | }
|
---|
[3679] | 386 |
|
---|
[5874] | 387 | /**
|
---|
| 388 | * Replies the list of files that have been successfully opened.
|
---|
| 389 | * @return The list of files that have been successfully opened.
|
---|
| 390 | */
|
---|
[3679] | 391 | public List<File> getSuccessfullyOpenedFiles() {
|
---|
| 392 | return successfullyOpenedFiles;
|
---|
| 393 | }
|
---|
[2047] | 394 | }
|
---|
[2512] | 395 | }
|
---|