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