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