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