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