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