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