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            String.join(",", extensionsPlusArchive),
398            defaultExtension,
399            description + (!extensionsForDescription.isEmpty()
400                ? (" (" + String.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}