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}