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