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