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