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