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.RtkLibImporter; 030import org.openstreetmap.josm.gui.io.importexport.WMSLayerImporter; 031import org.openstreetmap.josm.gui.widgets.AbstractFileChooser; 032import org.openstreetmap.josm.io.session.SessionImporter; 033import org.openstreetmap.josm.tools.Logging; 034import org.openstreetmap.josm.tools.Utils; 035 036/** 037 * A file filter that filters after the extension. Also includes a list of file 038 * filters used in JOSM. 039 * @since 32 040 */ 041public class ExtensionFileFilter extends FileFilter implements java.io.FileFilter { 042 043 /** 044 * List of supported formats for import. 045 * @since 4869 046 */ 047 private static final ArrayList<FileImporter> importers; 048 049 /** 050 * List of supported formats for export. 051 * @since 4869 052 */ 053 private static final ArrayList<FileExporter> exporters; 054 055 // add some file types only if the relevant classes are there. 056 // this gives us the option to painlessly drop them from the .jar 057 // and build JOSM versions without support for these formats 058 059 static { 060 061 importers = new ArrayList<>(); 062 063 final List<Class<? extends FileImporter>> importerNames = Arrays.asList( 064 OsmImporter.class, 065 OsmChangeImporter.class, 066 GpxImporter.class, 067 NMEAImporter.class, 068 RtkLibImporter.class, 069 NoteImporter.class, 070 JpgImporter.class, 071 WMSLayerImporter.class, 072 AllFormatsImporter.class, 073 SessionImporter.class 074 ); 075 076 for (final Class<? extends FileImporter> importerClass : importerNames) { 077 try { 078 FileImporter importer = importerClass.getConstructor().newInstance(); 079 importers.add(importer); 080 } catch (ReflectiveOperationException e) { 081 Logging.debug(e); 082 } catch (ServiceConfigurationError e) { 083 // error seen while initializing WMSLayerImporter in plugin unit tests: 084 // - 085 // ServiceConfigurationError: javax.imageio.spi.ImageWriterSpi: 086 // Provider com.sun.media.imageioimpl.plugins.jpeg.CLibJPEGImageWriterSpi could not be instantiated 087 // Caused by: java.lang.IllegalArgumentException: vendorName == null! 088 // at javax.imageio.spi.IIOServiceProvider.<init>(IIOServiceProvider.java:76) 089 // at javax.imageio.spi.ImageReaderWriterSpi.<init>(ImageReaderWriterSpi.java:231) 090 // at javax.imageio.spi.ImageWriterSpi.<init>(ImageWriterSpi.java:213) 091 // at com.sun.media.imageioimpl.plugins.jpeg.CLibJPEGImageWriterSpi.<init>(CLibJPEGImageWriterSpi.java:84) 092 // - 093 // This is a very strange behaviour of JAI: 094 // http://thierrywasyl.wordpress.com/2009/07/24/jai-how-to-solve-vendorname-null-exception/ 095 // - 096 // that can lead to various problems, see #8583 comments 097 Logging.error(e); 098 } 099 } 100 101 exporters = new ArrayList<>(); 102 103 final List<Class<? extends FileExporter>> exporterClasses = Arrays.asList( 104 org.openstreetmap.josm.gui.io.importexport.GpxExporter.class, 105 org.openstreetmap.josm.gui.io.importexport.OsmExporter.class, 106 org.openstreetmap.josm.gui.io.importexport.OsmGzipExporter.class, 107 org.openstreetmap.josm.gui.io.importexport.OsmBzip2Exporter.class, 108 org.openstreetmap.josm.gui.io.importexport.OsmXzExporter.class, 109 org.openstreetmap.josm.gui.io.importexport.GeoJSONExporter.class, 110 org.openstreetmap.josm.gui.io.importexport.WMSLayerExporter.class, 111 org.openstreetmap.josm.gui.io.importexport.NoteExporter.class, 112 org.openstreetmap.josm.gui.io.importexport.ValidatorErrorExporter.class 113 ); 114 115 for (final Class<? extends FileExporter> exporterClass : exporterClasses) { 116 try { 117 FileExporter exporter = exporterClass.getConstructor().newInstance(); 118 exporters.add(exporter); 119 MainApplication.getLayerManager().addAndFireActiveLayerChangeListener(exporter); 120 } catch (ReflectiveOperationException e) { 121 Logging.debug(e); 122 } catch (ServiceConfigurationError e) { 123 // see above in importers initialization 124 Logging.error(e); 125 } 126 } 127 } 128 129 private final String extensions; 130 private final String description; 131 private final String defaultExtension; 132 133 protected static void sort(List<ExtensionFileFilter> filters) { 134 filters.sort(new Comparator<ExtensionFileFilter>() { 135 private AllFormatsImporter all = new AllFormatsImporter(); 136 @Override 137 public int compare(ExtensionFileFilter o1, ExtensionFileFilter o2) { 138 if (o1.getDescription().equals(all.filter.getDescription())) return 1; 139 if (o2.getDescription().equals(all.filter.getDescription())) return -1; 140 return o1.getDescription().compareTo(o2.getDescription()); 141 } 142 } 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 List<ExtensionFileFilter> filters = new LinkedList<>(); 249 for (FileImporter importer : importers) { 250 filters.add(importer.filter); 251 } 252 sort(filters); 253 return filters; 254 } 255 256 /** 257 * Replies an ordered list of enabled {@link ExtensionFileFilter}s for exporting. 258 * The list is ordered according to their description, an {@link AllFormatsImporter} 259 * is append at the end. 260 * 261 * @return an ordered list of enabled {@link ExtensionFileFilter}s for exporting. 262 * @since 2029 263 */ 264 public static List<ExtensionFileFilter> getExportExtensionFileFilters() { 265 List<ExtensionFileFilter> filters = new LinkedList<>(); 266 for (FileExporter exporter : exporters) { 267 if (filters.contains(exporter.filter) || !exporter.isEnabled()) { 268 continue; 269 } 270 filters.add(exporter.filter); 271 } 272 sort(filters); 273 return filters; 274 } 275 276 /** 277 * Replies the default {@link ExtensionFileFilter} for a given extension 278 * 279 * @param extension the extension 280 * @return the default {@link ExtensionFileFilter} for a given extension 281 * @since 2029 282 */ 283 public static ExtensionFileFilter getDefaultImportExtensionFileFilter(String extension) { 284 if (extension == null) return new AllFormatsImporter().filter; 285 for (FileImporter importer : importers) { 286 if (extension.equals(importer.filter.getDefaultExtension())) 287 return importer.filter; 288 } 289 return new AllFormatsImporter().filter; 290 } 291 292 /** 293 * Replies the default {@link ExtensionFileFilter} for a given extension 294 * 295 * @param extension the extension 296 * @return the default {@link ExtensionFileFilter} for a given extension 297 * @since 2029 298 */ 299 public static ExtensionFileFilter getDefaultExportExtensionFileFilter(String extension) { 300 if (extension == null) return new AllFormatsImporter().filter; 301 for (FileExporter exporter : exporters) { 302 if (extension.equals(exporter.filter.getDefaultExtension())) 303 return exporter.filter; 304 } 305 // if extension did not match defaultExtension of any exporter, 306 // scan all supported extensions 307 File file = new File("file." + extension); 308 for (FileExporter exporter : exporters) { 309 if (exporter.filter.accept(file)) 310 return exporter.filter; 311 } 312 return new AllFormatsImporter().filter; 313 } 314 315 /** 316 * Applies the choosable {@link FileFilter} to a {@link AbstractFileChooser} before using the 317 * file chooser for selecting a file for reading. 318 * 319 * @param fileChooser the file chooser 320 * @param extension the default extension 321 * @param additionalTypes matching types will additionally be added to the "file type" combobox. 322 * @since 14668 (signature) 323 */ 324 public static void applyChoosableImportFileFilters( 325 AbstractFileChooser fileChooser, String extension, Predicate<ExtensionFileFilter> additionalTypes) { 326 for (ExtensionFileFilter filter: getImportExtensionFileFilters()) { 327 328 if (additionalTypes.test(filter) || filter.acceptName("file."+extension)) { 329 fileChooser.addChoosableFileFilter(filter); 330 } 331 } 332 fileChooser.setFileFilter(getDefaultImportExtensionFileFilter(extension)); 333 } 334 335 /** 336 * Applies the choosable {@link FileFilter} to a {@link AbstractFileChooser} before using the 337 * file chooser for selecting a file for writing. 338 * 339 * @param fileChooser the file chooser 340 * @param extension the default extension 341 * @param additionalTypes matching types will additionally be added to the "file type" combobox. 342 * @since 14668 (signature) 343 */ 344 public static void applyChoosableExportFileFilters( 345 AbstractFileChooser fileChooser, String extension, Predicate<ExtensionFileFilter> additionalTypes) { 346 for (ExtensionFileFilter filter: getExportExtensionFileFilters()) { 347 if (additionalTypes.test(filter) || filter.acceptName("file."+extension)) { 348 fileChooser.addChoosableFileFilter(filter); 349 } 350 } 351 fileChooser.setFileFilter(getDefaultExportExtensionFileFilter(extension)); 352 } 353 354 /** 355 * Construct an extension file filter by giving the extension to check after. 356 * @param extension The comma-separated list of file extensions 357 * @param defaultExtension The default extension 358 * @param description A short textual description of the file type 359 * @since 1169 360 */ 361 public ExtensionFileFilter(String extension, String defaultExtension, String description) { 362 this.extensions = extension; 363 this.defaultExtension = defaultExtension; 364 this.description = description; 365 } 366 367 /** 368 * Construct an extension file filter with the extensions supported by {@link org.openstreetmap.josm.io.Compression} 369 * automatically added to the {@code extensions}. The specified {@code extensions} will be added to the description 370 * in the form {@code old-description (*.ext1, *.ext2)}. 371 * @param extensions The comma-separated list of file extensions 372 * @param defaultExtension The default extension 373 * @param description A short textual description of the file type without supported extensions in parentheses 374 * @param addArchiveExtension Whether to also add the archive extensions to the description 375 * @param archiveExtensions List of extensions to be added 376 * @return The constructed filter 377 */ 378 public static ExtensionFileFilter newFilterWithArchiveExtensions(String extensions, String defaultExtension, 379 String description, AddArchiveExtension addArchiveExtension, List<String> archiveExtensions) { 380 final Collection<String> extensionsPlusArchive = new LinkedHashSet<>(); 381 final Collection<String> extensionsForDescription = new LinkedHashSet<>(); 382 for (String e : extensions.split(",")) { 383 extensionsPlusArchive.add(e); 384 if (addArchiveExtension != AddArchiveExtension.NONE) { 385 extensionsForDescription.add("*." + e); 386 } 387 for (String extension : archiveExtensions) { 388 extensionsPlusArchive.add(e + '.' + extension); 389 if (addArchiveExtension == AddArchiveExtension.ALL) { 390 extensionsForDescription.add("*." + e + '.' + extension); 391 } 392 } 393 } 394 return new ExtensionFileFilter( 395 Utils.join(",", extensionsPlusArchive), 396 defaultExtension, 397 description + (!extensionsForDescription.isEmpty() 398 ? (" (" + Utils.join(", ", extensionsForDescription) + ')') 399 : "") 400 ); 401 } 402 403 /** 404 * Construct an extension file filter with the extensions supported by {@link org.openstreetmap.josm.io.Compression} 405 * automatically added to the {@code extensions}. The specified {@code extensions} will be added to the description 406 * in the form {@code old-description (*.ext1, *.ext2)}. 407 * @param extensions The comma-separated list of file extensions 408 * @param defaultExtension The default extension 409 * @param description A short textual description of the file type without supported extensions in parentheses 410 * @param addArchiveExtensionsToDescription Whether to also add the archive extensions to the description 411 * @return The constructed filter 412 */ 413 public static ExtensionFileFilter newFilterWithArchiveExtensions( 414 String extensions, String defaultExtension, String description, boolean addArchiveExtensionsToDescription) { 415 416 List<String> archiveExtensions = Arrays.asList("gz", "bz", "bz2", "xz", "zip"); 417 return newFilterWithArchiveExtensions( 418 extensions, 419 defaultExtension, 420 description, 421 addArchiveExtensionsToDescription ? AddArchiveExtension.ALL : AddArchiveExtension.BASE, 422 archiveExtensions 423 ); 424 } 425 426 /** 427 * Returns true if this file filter accepts the given filename. 428 * @param filename The filename to check after 429 * @return true if this file filter accepts the given filename (i.e if this filename ends with one of the extensions) 430 * @since 1169 431 */ 432 public boolean acceptName(String filename) { 433 return Utils.hasExtension(filename, extensions.split(",")); 434 } 435 436 @Override 437 public boolean accept(File pathname) { 438 if (pathname.isDirectory()) 439 return true; 440 return acceptName(pathname.getName()); 441 } 442 443 @Override 444 public String getDescription() { 445 return description; 446 } 447 448 /** 449 * Replies the comma-separated list of file extensions of this file filter. 450 * @return the comma-separated list of file extensions of this file filter, as a String 451 * @since 5131 452 */ 453 public String getExtensions() { 454 return extensions; 455 } 456 457 /** 458 * Replies the default file extension of this file filter. 459 * @return the default file extension of this file filter 460 * @since 2029 461 */ 462 public String getDefaultExtension() { 463 return defaultExtension; 464 } 465 466 @Override 467 public int hashCode() { 468 return Objects.hash(extensions, description, defaultExtension); 469 } 470 471 @Override 472 public boolean equals(Object obj) { 473 if (this == obj) return true; 474 if (obj == null || getClass() != obj.getClass()) return false; 475 ExtensionFileFilter that = (ExtensionFileFilter) obj; 476 return Objects.equals(extensions, that.extensions) && 477 Objects.equals(description, that.description) && 478 Objects.equals(defaultExtension, that.defaultExtension); 479 } 480}