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