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