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