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.ServiceConfigurationError;
014
015import javax.swing.filechooser.FileFilter;
016
017import org.openstreetmap.josm.Main;
018import org.openstreetmap.josm.gui.MapView;
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    public static final ArrayList<FileImporter> importers;
037
038    /**
039     * List of supported formats for export.
040     * @since 4869
041     */
042    public 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.OsmGzipImporter.class,
055                org.openstreetmap.josm.io.OsmZipImporter.class,
056                org.openstreetmap.josm.io.OsmChangeImporter.class,
057                org.openstreetmap.josm.io.GpxImporter.class,
058                org.openstreetmap.josm.io.NMEAImporter.class,
059                org.openstreetmap.josm.io.NoteImporter.class,
060                org.openstreetmap.josm.io.OsmBzip2Importer.class,
061                org.openstreetmap.josm.io.JpgImporter.class,
062                org.openstreetmap.josm.io.WMSLayerImporter.class,
063                org.openstreetmap.josm.io.AllFormatsImporter.class,
064                org.openstreetmap.josm.io.session.SessionImporter.class
065        );
066
067        for (final Class<? extends FileImporter> importerClass : importerNames) {
068            try {
069                FileImporter importer = importerClass.newInstance();
070                importers.add(importer);
071                MapView.addLayerChangeListener(importer);
072            } catch (Exception e) {
073                if (Main.isDebugEnabled()) {
074                    Main.debug(e.getMessage());
075                }
076            } catch (ServiceConfigurationError e) {
077                // error seen while initializing WMSLayerImporter in plugin unit tests:
078                // -
079                // ServiceConfigurationError: javax.imageio.spi.ImageWriterSpi:
080                // Provider com.sun.media.imageioimpl.plugins.jpeg.CLibJPEGImageWriterSpi could not be instantiated
081                // Caused by: java.lang.IllegalArgumentException: vendorName == null!
082                //      at javax.imageio.spi.IIOServiceProvider.<init>(IIOServiceProvider.java:76)
083                //      at javax.imageio.spi.ImageReaderWriterSpi.<init>(ImageReaderWriterSpi.java:231)
084                //      at javax.imageio.spi.ImageWriterSpi.<init>(ImageWriterSpi.java:213)
085                //      at com.sun.media.imageioimpl.plugins.jpeg.CLibJPEGImageWriterSpi.<init>(CLibJPEGImageWriterSpi.java:84)
086                // -
087                // This is a very strange behaviour of JAI:
088                // http://thierrywasyl.wordpress.com/2009/07/24/jai-how-to-solve-vendorname-null-exception/
089                // -
090                // that can lead to various problems, see #8583 comments
091                Main.error(e);
092            }
093        }
094
095        exporters = new ArrayList<>();
096
097        final List<Class<? extends FileExporter>> exporterClasses = Arrays.asList(
098                org.openstreetmap.josm.io.GpxExporter.class,
099                org.openstreetmap.josm.io.OsmExporter.class,
100                org.openstreetmap.josm.io.OsmGzipExporter.class,
101                org.openstreetmap.josm.io.OsmBzip2Exporter.class,
102                org.openstreetmap.josm.io.GeoJSONExporter.CurrentProjection.class, // needs to be considered earlier than GeoJSONExporter
103                org.openstreetmap.josm.io.GeoJSONExporter.class,
104                org.openstreetmap.josm.io.WMSLayerExporter.class,
105                org.openstreetmap.josm.io.NoteExporter.class
106        );
107
108        for (final Class<? extends FileExporter> exporterClass : exporterClasses) {
109            try {
110                FileExporter exporter = exporterClass.newInstance();
111                exporters.add(exporter);
112                MapView.addLayerChangeListener(exporter);
113            } catch (Exception e) {
114                if (Main.isDebugEnabled()) {
115                    Main.debug(e.getMessage());
116                }
117            } catch (ServiceConfigurationError e) {
118                // see above in importers initialization
119                Main.error(e);
120            }
121        }
122    }
123
124    private final String extensions;
125    private final String description;
126    private final String defaultExtension;
127
128    protected static void sort(List<ExtensionFileFilter> filters) {
129        Collections.sort(
130                filters,
131                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     * 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        return new AllFormatsImporter().filter;
229    }
230
231    /**
232     * Applies the choosable {@link FileFilter} to a {@link AbstractFileChooser} before using the
233     * file chooser for selecting a file for reading.
234     *
235     * @param fileChooser the file chooser
236     * @param extension the default extension
237     * @param allTypes If true, all the files types known by JOSM will be proposed in the "file type" combobox.
238     *                 If false, only the file filters that include {@code extension} will be proposed
239     * @since 5438
240     */
241    public static void applyChoosableImportFileFilters(AbstractFileChooser fileChooser, String extension, boolean allTypes) {
242        for (ExtensionFileFilter filter: getImportExtensionFileFilters()) {
243            if (allTypes || filter.acceptName("file."+extension)) {
244                fileChooser.addChoosableFileFilter(filter);
245            }
246        }
247        fileChooser.setFileFilter(getDefaultImportExtensionFileFilter(extension));
248    }
249
250    /**
251     * Applies the choosable {@link FileFilter} to a {@link AbstractFileChooser} before using the
252     * file chooser for selecting a file for writing.
253     *
254     * @param fileChooser the file chooser
255     * @param extension the default extension
256     * @param allTypes If true, all the files types known by JOSM will be proposed in the "file type" combobox.
257     *                 If false, only the file filters that include {@code extension} will be proposed
258     * @since 5438
259     */
260    public static void applyChoosableExportFileFilters(AbstractFileChooser fileChooser, String extension, boolean allTypes) {
261        for (ExtensionFileFilter filter: getExportExtensionFileFilters()) {
262            if (allTypes || filter.acceptName("file."+extension)) {
263                fileChooser.addChoosableFileFilter(filter);
264            }
265        }
266        fileChooser.setFileFilter(getDefaultExportExtensionFileFilter(extension));
267    }
268
269    /**
270     * Construct an extension file filter by giving the extension to check after.
271     * @param extension The comma-separated list of file extensions
272     * @param defaultExtension The default extension
273     * @param description A short textual description of the file type
274     * @since 1169
275     */
276    public ExtensionFileFilter(String extension, String defaultExtension, String description) {
277        this.extensions = extension;
278        this.defaultExtension = defaultExtension;
279        this.description = description;
280    }
281
282    /**
283     * Construct an extension file filter with the extensions supported by {@link org.openstreetmap.josm.io.Compression}
284     * automatically added to the {@code extensions}. The specified {@code extensions} will be added to the description
285     * in the form {@code old-description (*.ext1, *.ext2)}.
286     * @param extensions The comma-separated list of file extensions
287     * @param defaultExtension The default extension
288     * @param description A short textual description of the file type without supported extensions in parentheses
289     * @param addArchiveExtensionsToDescription Whether to also add the archive extensions to the description
290     * @return The constructed filter
291     */
292    public static ExtensionFileFilter newFilterWithArchiveExtensions(
293            String extensions, String defaultExtension, String description, boolean addArchiveExtensionsToDescription) {
294        final Collection<String> extensionsPlusArchive = new LinkedHashSet<>();
295        final Collection<String> extensionsForDescription = new LinkedHashSet<>();
296        for (String e : extensions.split(",")) {
297            extensionsPlusArchive.add(e);
298            extensionsPlusArchive.add(e + ".gz");
299            extensionsPlusArchive.add(e + ".bz2");
300            extensionsForDescription.add("*." + e);
301            if (addArchiveExtensionsToDescription) {
302                extensionsForDescription.add("*." + e + ".gz");
303                extensionsForDescription.add("*." + e + ".bz2");
304            }
305        }
306        return new ExtensionFileFilter(Utils.join(",", extensionsPlusArchive), defaultExtension,
307                description + " (" + Utils.join(", ", extensionsForDescription) + ")");
308    }
309
310    /**
311     * Returns true if this file filter accepts the given filename.
312     * @param filename The filename to check after
313     * @return true if this file filter accepts the given filename (i.e if this filename ends with one of the extensions)
314     * @since 1169
315     */
316    public boolean acceptName(String filename) {
317        return Utils.hasExtension(filename, extensions.split(","));
318    }
319
320    @Override
321    public boolean accept(File pathname) {
322        if (pathname.isDirectory())
323            return true;
324        return acceptName(pathname.getName());
325    }
326
327    @Override
328    public String getDescription() {
329        return description;
330    }
331
332    /**
333     * Replies the comma-separated list of file extensions of this file filter.
334     * @return the comma-separated list of file extensions of this file filter, as a String
335     * @since 5131
336     */
337    public String getExtensions() {
338        return extensions;
339    }
340
341    /**
342     * Replies the default file extension of this file filter.
343     * @return the default file extension of this file filter
344     * @since 2029
345     */
346    public String getDefaultExtension() {
347        return defaultExtension;
348    }
349
350    @Override
351    public int hashCode() {
352        final int prime = 31;
353        int result = 1;
354        result = prime * result + ((defaultExtension == null) ? 0 : defaultExtension.hashCode());
355        result = prime * result + ((description == null) ? 0 : description.hashCode());
356        result = prime * result + ((extensions == null) ? 0 : extensions.hashCode());
357        return result;
358    }
359
360    @Override
361    public boolean equals(Object obj) {
362        if (this == obj)
363            return true;
364        if (obj == null)
365            return false;
366        if (getClass() != obj.getClass())
367            return false;
368        ExtensionFileFilter other = (ExtensionFileFilter) obj;
369        if (defaultExtension == null) {
370            if (other.defaultExtension != null)
371                return false;
372        } else if (!defaultExtension.equals(other.defaultExtension))
373            return false;
374        if (description == null) {
375            if (other.description != null)
376                return false;
377        } else if (!description.equals(other.description))
378            return false;
379        if (extensions == null) {
380            if (other.extensions != null)
381                return false;
382        } else if (!extensions.equals(other.extensions))
383            return false;
384        return true;
385    }
386}