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