001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.actions; 003 004import java.io.File; 005import java.util.ArrayList; 006import java.util.Arrays; 007import java.util.Collection; 008import java.util.Collections; 009import java.util.Comparator; 010import java.util.LinkedHashSet; 011import java.util.LinkedList; 012import java.util.List; 013import java.util.Objects; 014import java.util.ServiceConfigurationError; 015 016import javax.swing.filechooser.FileFilter; 017 018import org.openstreetmap.josm.gui.MainApplication; 019import org.openstreetmap.josm.gui.io.importexport.AllFormatsImporter; 020import org.openstreetmap.josm.gui.io.importexport.FileExporter; 021import org.openstreetmap.josm.gui.io.importexport.FileImporter; 022import org.openstreetmap.josm.gui.io.importexport.GpxImporter; 023import org.openstreetmap.josm.gui.io.importexport.JpgImporter; 024import org.openstreetmap.josm.gui.io.importexport.NMEAImporter; 025import org.openstreetmap.josm.gui.io.importexport.NoteImporter; 026import org.openstreetmap.josm.gui.io.importexport.OsmChangeImporter; 027import org.openstreetmap.josm.gui.io.importexport.OsmImporter; 028import org.openstreetmap.josm.gui.io.importexport.WMSLayerImporter; 029import org.openstreetmap.josm.gui.widgets.AbstractFileChooser; 030import org.openstreetmap.josm.io.session.SessionImporter; 031import org.openstreetmap.josm.tools.Logging; 032import org.openstreetmap.josm.tools.Utils; 033 034/** 035 * A file filter that filters after the extension. Also includes a list of file 036 * filters used in JOSM. 037 * @since 32 038 */ 039public class ExtensionFileFilter extends FileFilter implements java.io.FileFilter { 040 041 /** 042 * List of supported formats for import. 043 * @since 4869 044 */ 045 private static final ArrayList<FileImporter> importers; 046 047 /** 048 * List of supported formats for export. 049 * @since 4869 050 */ 051 private static final ArrayList<FileExporter> exporters; 052 053 // add some file types only if the relevant classes are there. 054 // this gives us the option to painlessly drop them from the .jar 055 // and build JOSM versions without support for these formats 056 057 static { 058 059 importers = new ArrayList<>(); 060 061 final List<Class<? extends FileImporter>> importerNames = Arrays.asList( 062 OsmImporter.class, 063 OsmChangeImporter.class, 064 GpxImporter.class, 065 NMEAImporter.class, 066 NoteImporter.class, 067 JpgImporter.class, 068 WMSLayerImporter.class, 069 AllFormatsImporter.class, 070 SessionImporter.class 071 ); 072 073 for (final Class<? extends FileImporter> importerClass : importerNames) { 074 try { 075 FileImporter importer = importerClass.getConstructor().newInstance(); 076 importers.add(importer); 077 } catch (ReflectiveOperationException e) { 078 Logging.debug(e); 079 } catch (ServiceConfigurationError e) { 080 // error seen while initializing WMSLayerImporter in plugin unit tests: 081 // - 082 // ServiceConfigurationError: javax.imageio.spi.ImageWriterSpi: 083 // Provider com.sun.media.imageioimpl.plugins.jpeg.CLibJPEGImageWriterSpi could not be instantiated 084 // Caused by: java.lang.IllegalArgumentException: vendorName == null! 085 // at javax.imageio.spi.IIOServiceProvider.<init>(IIOServiceProvider.java:76) 086 // at javax.imageio.spi.ImageReaderWriterSpi.<init>(ImageReaderWriterSpi.java:231) 087 // at javax.imageio.spi.ImageWriterSpi.<init>(ImageWriterSpi.java:213) 088 // at com.sun.media.imageioimpl.plugins.jpeg.CLibJPEGImageWriterSpi.<init>(CLibJPEGImageWriterSpi.java:84) 089 // - 090 // This is a very strange behaviour of JAI: 091 // http://thierrywasyl.wordpress.com/2009/07/24/jai-how-to-solve-vendorname-null-exception/ 092 // - 093 // that can lead to various problems, see #8583 comments 094 Logging.error(e); 095 } 096 } 097 098 exporters = new ArrayList<>(); 099 100 final List<Class<? extends FileExporter>> exporterClasses = Arrays.asList( 101 org.openstreetmap.josm.gui.io.importexport.GpxExporter.class, 102 org.openstreetmap.josm.gui.io.importexport.OsmExporter.class, 103 org.openstreetmap.josm.gui.io.importexport.OsmGzipExporter.class, 104 org.openstreetmap.josm.gui.io.importexport.OsmBzip2Exporter.class, 105 org.openstreetmap.josm.gui.io.importexport.OsmXzExporter.class, 106 org.openstreetmap.josm.gui.io.importexport.GeoJSONExporter.class, 107 org.openstreetmap.josm.gui.io.importexport.WMSLayerExporter.class, 108 org.openstreetmap.josm.gui.io.importexport.NoteExporter.class, 109 org.openstreetmap.josm.gui.io.importexport.ValidatorErrorExporter.class 110 ); 111 112 for (final Class<? extends FileExporter> exporterClass : exporterClasses) { 113 try { 114 FileExporter exporter = exporterClass.getConstructor().newInstance(); 115 exporters.add(exporter); 116 MainApplication.getLayerManager().addAndFireActiveLayerChangeListener(exporter); 117 } catch (ReflectiveOperationException e) { 118 Logging.debug(e); 119 } catch (ServiceConfigurationError e) { 120 // see above in importers initialization 121 Logging.error(e); 122 } 123 } 124 } 125 126 private final String extensions; 127 private final String description; 128 private final String defaultExtension; 129 130 protected static void sort(List<ExtensionFileFilter> filters) { 131 filters.sort(new Comparator<ExtensionFileFilter>() { 132 private AllFormatsImporter all = new AllFormatsImporter(); 133 @Override 134 public int compare(ExtensionFileFilter o1, ExtensionFileFilter o2) { 135 if (o1.getDescription().equals(all.filter.getDescription())) return 1; 136 if (o2.getDescription().equals(all.filter.getDescription())) return -1; 137 return o1.getDescription().compareTo(o2.getDescription()); 138 } 139 } 140 ); 141 } 142 143 /** 144 * Strategy to determine if extensions must be added to the description. 145 */ 146 public enum AddArchiveExtension { 147 /** No extension is added */ 148 NONE, 149 /** Only base extension is added */ 150 BASE, 151 /** All extensions are added (base + archives) */ 152 ALL 153 } 154 155 /** 156 * Adds a new file importer at the end of the global list. This importer will be evaluated after core ones. 157 * @param importer new file importer 158 * @since 10407 159 */ 160 public static void addImporter(FileImporter importer) { 161 if (importer != null) { 162 importers.add(importer); 163 } 164 } 165 166 /** 167 * Adds a new file importer at the beginning of the global list. This importer will be evaluated before core ones. 168 * @param importer new file importer 169 * @since 10407 170 */ 171 public static void addImporterFirst(FileImporter importer) { 172 if (importer != null) { 173 importers.add(0, importer); 174 } 175 } 176 177 /** 178 * Adds a new file exporter at the end of the global list. This exporter will be evaluated after core ones. 179 * @param exporter new file exporter 180 * @since 10407 181 */ 182 public static void addExporter(FileExporter exporter) { 183 if (exporter != null) { 184 exporters.add(exporter); 185 } 186 } 187 188 /** 189 * Adds a new file exporter at the beginning of the global list. This exporter will be evaluated before core ones. 190 * @param exporter new file exporter 191 * @since 10407 192 */ 193 public static void addExporterFirst(FileExporter exporter) { 194 if (exporter != null) { 195 exporters.add(0, exporter); 196 } 197 } 198 199 /** 200 * Returns the list of file importers. 201 * @return unmodifiable list of file importers 202 * @since 10407 203 */ 204 public static List<FileImporter> getImporters() { 205 return Collections.unmodifiableList(importers); 206 } 207 208 /** 209 * Returns the list of file exporters. 210 * @return unmodifiable list of file exporters 211 * @since 10407 212 */ 213 public static List<FileExporter> getExporters() { 214 return Collections.unmodifiableList(exporters); 215 } 216 217 /** 218 * Updates the {@link AllFormatsImporter} that is contained in the importers list. If 219 * you do not use the importers variable directly, you don't need to call this. 220 * <p> 221 * Updating the AllFormatsImporter is required when plugins add new importers that 222 * support new file extensions. The old AllFormatsImporter doesn't include the new 223 * extensions and thus will not display these files. 224 * 225 * @since 5131 226 */ 227 public static void updateAllFormatsImporter() { 228 for (int i = 0; i < importers.size(); i++) { 229 if (importers.get(i) instanceof AllFormatsImporter) { 230 importers.set(i, new AllFormatsImporter()); 231 } 232 } 233 } 234 235 /** 236 * Replies an ordered list of {@link ExtensionFileFilter}s for importing. 237 * The list is ordered according to their description, an {@link AllFormatsImporter} 238 * is append at the end. 239 * 240 * @return an ordered list of {@link ExtensionFileFilter}s for importing. 241 * @since 2029 242 */ 243 public static List<ExtensionFileFilter> getImportExtensionFileFilters() { 244 updateAllFormatsImporter(); 245 List<ExtensionFileFilter> filters = new LinkedList<>(); 246 for (FileImporter importer : importers) { 247 filters.add(importer.filter); 248 } 249 sort(filters); 250 return filters; 251 } 252 253 /** 254 * Replies an ordered list of enabled {@link ExtensionFileFilter}s for exporting. 255 * The list is ordered according to their description, an {@link AllFormatsImporter} 256 * is append at the end. 257 * 258 * @return an ordered list of enabled {@link ExtensionFileFilter}s for exporting. 259 * @since 2029 260 */ 261 public static List<ExtensionFileFilter> getExportExtensionFileFilters() { 262 List<ExtensionFileFilter> filters = new LinkedList<>(); 263 for (FileExporter exporter : exporters) { 264 if (filters.contains(exporter.filter) || !exporter.isEnabled()) { 265 continue; 266 } 267 filters.add(exporter.filter); 268 } 269 sort(filters); 270 return filters; 271 } 272 273 /** 274 * Replies the default {@link ExtensionFileFilter} for a given extension 275 * 276 * @param extension the extension 277 * @return the default {@link ExtensionFileFilter} for a given extension 278 * @since 2029 279 */ 280 public static ExtensionFileFilter getDefaultImportExtensionFileFilter(String extension) { 281 if (extension == null) return new AllFormatsImporter().filter; 282 for (FileImporter importer : importers) { 283 if (extension.equals(importer.filter.getDefaultExtension())) 284 return importer.filter; 285 } 286 return new AllFormatsImporter().filter; 287 } 288 289 /** 290 * Replies the default {@link ExtensionFileFilter} for a given extension 291 * 292 * @param extension the extension 293 * @return the default {@link ExtensionFileFilter} for a given extension 294 * @since 2029 295 */ 296 public static ExtensionFileFilter getDefaultExportExtensionFileFilter(String extension) { 297 if (extension == null) return new AllFormatsImporter().filter; 298 for (FileExporter exporter : exporters) { 299 if (extension.equals(exporter.filter.getDefaultExtension())) 300 return exporter.filter; 301 } 302 // if extension did not match defaultExtension of any exporter, 303 // scan all supported extensions 304 File file = new File("file." + extension); 305 for (FileExporter exporter : exporters) { 306 if (exporter.filter.accept(file)) 307 return exporter.filter; 308 } 309 return new AllFormatsImporter().filter; 310 } 311 312 /** 313 * Applies the choosable {@link FileFilter} to a {@link AbstractFileChooser} before using the 314 * file chooser for selecting a file for reading. 315 * 316 * @param fileChooser the file chooser 317 * @param extension the default extension 318 * @param allTypes If true, all the files types known by JOSM will be proposed in the "file type" combobox. 319 * If false, only the file filters that include {@code extension} will be proposed 320 * @since 5438 321 */ 322 public static void applyChoosableImportFileFilters(AbstractFileChooser fileChooser, String extension, boolean allTypes) { 323 for (ExtensionFileFilter filter: getImportExtensionFileFilters()) { 324 325 if (allTypes || filter.acceptName("file."+extension)) { 326 fileChooser.addChoosableFileFilter(filter); 327 } 328 } 329 fileChooser.setFileFilter(getDefaultImportExtensionFileFilter(extension)); 330 } 331 332 /** 333 * Applies the choosable {@link FileFilter} to a {@link AbstractFileChooser} before using the 334 * file chooser for selecting a file for writing. 335 * 336 * @param fileChooser the file chooser 337 * @param extension the default extension 338 * @param allTypes If true, all the files types known by JOSM will be proposed in the "file type" combobox. 339 * If false, only the file filters that include {@code extension} will be proposed 340 * @since 5438 341 */ 342 public static void applyChoosableExportFileFilters(AbstractFileChooser fileChooser, String extension, boolean allTypes) { 343 for (ExtensionFileFilter filter: getExportExtensionFileFilters()) { 344 if (allTypes || filter.acceptName("file."+extension)) { 345 fileChooser.addChoosableFileFilter(filter); 346 } 347 } 348 fileChooser.setFileFilter(getDefaultExportExtensionFileFilter(extension)); 349 } 350 351 /** 352 * Construct an extension file filter by giving the extension to check after. 353 * @param extension The comma-separated list of file extensions 354 * @param defaultExtension The default extension 355 * @param description A short textual description of the file type 356 * @since 1169 357 */ 358 public ExtensionFileFilter(String extension, String defaultExtension, String description) { 359 this.extensions = extension; 360 this.defaultExtension = defaultExtension; 361 this.description = description; 362 } 363 364 /** 365 * Construct an extension file filter with the extensions supported by {@link org.openstreetmap.josm.io.Compression} 366 * automatically added to the {@code extensions}. The specified {@code extensions} will be added to the description 367 * in the form {@code old-description (*.ext1, *.ext2)}. 368 * @param extensions The comma-separated list of file extensions 369 * @param defaultExtension The default extension 370 * @param description A short textual description of the file type without supported extensions in parentheses 371 * @param addArchiveExtension Whether to also add the archive extensions to the description 372 * @param archiveExtensions List of extensions to be added 373 * @return The constructed filter 374 */ 375 public static ExtensionFileFilter newFilterWithArchiveExtensions(String extensions, String defaultExtension, 376 String description, AddArchiveExtension addArchiveExtension, List<String> archiveExtensions) { 377 final Collection<String> extensionsPlusArchive = new LinkedHashSet<>(); 378 final Collection<String> extensionsForDescription = new LinkedHashSet<>(); 379 for (String e : extensions.split(",")) { 380 extensionsPlusArchive.add(e); 381 if (addArchiveExtension != AddArchiveExtension.NONE) { 382 extensionsForDescription.add("*." + e); 383 } 384 for (String extension : archiveExtensions) { 385 extensionsPlusArchive.add(e + '.' + extension); 386 if (addArchiveExtension == AddArchiveExtension.ALL) { 387 extensionsForDescription.add("*." + e + '.' + extension); 388 } 389 } 390 } 391 return new ExtensionFileFilter( 392 Utils.join(",", extensionsPlusArchive), 393 defaultExtension, 394 description + (!extensionsForDescription.isEmpty() 395 ? (" (" + Utils.join(", ", extensionsForDescription) + ')') 396 : "") 397 ); 398 } 399 400 /** 401 * Construct an extension file filter with the extensions supported by {@link org.openstreetmap.josm.io.Compression} 402 * automatically added to the {@code extensions}. The specified {@code extensions} will be added to the description 403 * in the form {@code old-description (*.ext1, *.ext2)}. 404 * @param extensions The comma-separated list of file extensions 405 * @param defaultExtension The default extension 406 * @param description A short textual description of the file type without supported extensions in parentheses 407 * @param addArchiveExtensionsToDescription Whether to also add the archive extensions to the description 408 * @return The constructed filter 409 */ 410 public static ExtensionFileFilter newFilterWithArchiveExtensions( 411 String extensions, String defaultExtension, String description, boolean addArchiveExtensionsToDescription) { 412 413 List<String> archiveExtensions = Arrays.asList("gz", "bz", "bz2", "xz", "zip"); 414 return newFilterWithArchiveExtensions( 415 extensions, 416 defaultExtension, 417 description, 418 addArchiveExtensionsToDescription ? AddArchiveExtension.ALL : AddArchiveExtension.BASE, 419 archiveExtensions 420 ); 421 } 422 423 /** 424 * Returns true if this file filter accepts the given filename. 425 * @param filename The filename to check after 426 * @return true if this file filter accepts the given filename (i.e if this filename ends with one of the extensions) 427 * @since 1169 428 */ 429 public boolean acceptName(String filename) { 430 return Utils.hasExtension(filename, extensions.split(",")); 431 } 432 433 @Override 434 public boolean accept(File pathname) { 435 if (pathname.isDirectory()) 436 return true; 437 return acceptName(pathname.getName()); 438 } 439 440 @Override 441 public String getDescription() { 442 return description; 443 } 444 445 /** 446 * Replies the comma-separated list of file extensions of this file filter. 447 * @return the comma-separated list of file extensions of this file filter, as a String 448 * @since 5131 449 */ 450 public String getExtensions() { 451 return extensions; 452 } 453 454 /** 455 * Replies the default file extension of this file filter. 456 * @return the default file extension of this file filter 457 * @since 2029 458 */ 459 public String getDefaultExtension() { 460 return defaultExtension; 461 } 462 463 @Override 464 public int hashCode() { 465 return Objects.hash(extensions, description, defaultExtension); 466 } 467 468 @Override 469 public boolean equals(Object obj) { 470 if (this == obj) return true; 471 if (obj == null || getClass() != obj.getClass()) return false; 472 ExtensionFileFilter that = (ExtensionFileFilter) obj; 473 return Objects.equals(extensions, that.extensions) && 474 Objects.equals(description, that.description) && 475 Objects.equals(defaultExtension, that.defaultExtension); 476 } 477}