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}