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