/* * Copyright 2010-2013 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.gemfire.snapshot; import static org.apache.geode.cache.snapshot.SnapshotOptions.SnapshotFormat; import static org.springframework.data.gemfire.snapshot.SnapshotServiceFactoryBean.SnapshotServiceAdapter; import java.io.Closeable; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.File; import java.io.FileFilter; import java.io.FileOutputStream; import java.io.IOException; import java.util.Arrays; import java.util.List; import java.util.jar.JarFile; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.geode.cache.Cache; import org.apache.geode.cache.Region; import org.apache.geode.cache.snapshot.CacheSnapshotService; import org.apache.geode.cache.snapshot.RegionSnapshotService; import org.apache.geode.cache.snapshot.SnapshotFilter; import org.apache.geode.cache.snapshot.SnapshotOptions; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.context.ApplicationListener; import org.springframework.data.gemfire.snapshot.event.ExportSnapshotApplicationEvent; import org.springframework.data.gemfire.snapshot.event.SnapshotApplicationEvent; import org.springframework.data.gemfire.util.CollectionUtils; import org.springframework.util.Assert; import org.springframework.util.FileCopyUtils; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; /** * The SnapshotServiceFactoryBean class is a Spring FactoryBean used to configure and create an instance * of an appropriate GemFire Snapshot Service to perform data import and exports. A CacheSnapshotService is created * if the Region is not specified, otherwise a RegionSnapshotService is used based on the configured Region. * * @author John Blum * @see org.springframework.beans.factory.DisposableBean * @see org.springframework.beans.factory.FactoryBean * @see org.springframework.beans.factory.InitializingBean * @see org.springframework.context.ApplicationListener * @see org.springframework.data.gemfire.snapshot.SnapshotServiceFactoryBean.SnapshotServiceAdapter * @see org.apache.geode.cache.snapshot.CacheSnapshotService * @see org.apache.geode.cache.snapshot.RegionSnapshotService * @since 1.7.0 */ @SuppressWarnings("unused") public class SnapshotServiceFactoryBean<K, V> implements FactoryBean<SnapshotServiceAdapter<K, V>>, InitializingBean, DisposableBean, ApplicationListener<SnapshotApplicationEvent<K, V>> { protected static final SnapshotMetadata[] EMPTY_ARRAY = new SnapshotMetadata[0]; private Boolean suppressImportOnInit; private Cache cache; private Region<K, V> region; private SnapshotMetadata<K, V>[] exports; private SnapshotMetadata<K, V>[] imports; private SnapshotServiceAdapter<K, V> snapshotServiceAdapter; /* (non-Javadoc) */ @SuppressWarnings("unchecked") static <K, V> SnapshotMetadata<K, V>[] nullSafeArray(SnapshotMetadata<K, V>[] configurations) { return (configurations != null ? configurations : EMPTY_ARRAY); } /* (non-Javadoc) */ static boolean nullSafeIsDirectory(File file) { return (file != null && file.isDirectory()); } /* (non-Javadoc) */ static boolean nullSafeIsFile(File file) { return (file != null && file.isFile()); } /** * Sets a reference to the GemFire Cache for which the snapshot will be taken. * * @param cache the GemFire Cache used to create an instance of CacheSnapshotService. * @throws IllegalArgumentException if the Cache reference is null. * @see org.apache.geode.cache.Cache * @see #getCache() */ public void setCache(Cache cache) { Assert.notNull(cache, "The GemFire Cache must not be null"); this.cache = cache; } /** * Gets a reference to the GemFire Cache for which the snapshot will be taken. * * @return the GemFire Cache used to create an instance of CacheSnapshotService. * @throws IllegalStateException if the Cache argument is null. * @see org.apache.geode.cache.Cache * @see #setCache(Cache) */ protected Cache getCache() { Assert.state(cache != null, "The GemFire Cache was not properly initialized"); return cache; } /** * Sets the meta-data (location, filter and format) used to create a snapshot from the Cache or Region data. * * @param exports an array of snapshot meta-data used for each export. * @see SnapshotServiceFactoryBean.SnapshotMetadata */ public void setExports(SnapshotMetadata<K, V>[] exports) { this.exports = exports; } /** * Sets the meta-data (location, filter and format) used to create a snapshot from the Cache or Region data. * * @return an array of snapshot meta-data used for each export. * @see SnapshotServiceFactoryBean.SnapshotMetadata */ protected SnapshotMetadata<K, V>[] getExports() { return nullSafeArray(exports); } /** * Sets the meta-data (location, filter and format) used to read a data snapshot into an entire Cache * or individual Region. * * @param imports an array of snapshot meta-data used for each import. * @see SnapshotServiceFactoryBean.SnapshotMetadata */ public void setImports(SnapshotMetadata<K, V>[] imports) { this.imports = imports; } /** * Gets the meta-data (location, filter and format) used to read a data snapshot into an entire Cache * or individual Region. * * @return an array of snapshot meta-data used for each import. * @see SnapshotServiceFactoryBean.SnapshotMetadata */ protected SnapshotMetadata<K, V>[] getImports() { return nullSafeArray(imports); } /** * Sets a reference to the GemFire Region for which the snapshot will be taken. * * @param region the GemFire Region used to create an instance of the RegionSnapshotService. * @see org.apache.geode.cache.Region * @see #getRegion() */ public void setRegion(Region<K, V> region) { this.region = region; } /** * Gets a reference to the GemFire Region for which the snapshot will be taken. * * @return the GemFire Region used to create an instance of the RegionSnapshotService. * @see org.apache.geode.cache.Region * @see #getRegion() */ protected Region<K, V> getRegion() { return region; } /** * Sets a boolean condition to indicate whether importing on initialization should be suppressed. * * @param suppressImportOnInit a Boolean value to indicate whether importing on initialization should be suppressed. * @see #getSuppressImportOnInit() */ public void setSuppressImportOnInit(Boolean suppressImportOnInit) { this.suppressImportOnInit = suppressImportOnInit; } /** * Determines whether importing on initialization should be suppressed. * * @return a boolean value indicating whether import on initialization should be suppressed. * @see #setSuppressImportOnInit(Boolean) * @see #afterPropertiesSet() */ protected boolean getSuppressImportOnInit() { return Boolean.TRUE.equals(suppressImportOnInit); } /** * Gets the reference to the GemFire Snapshot Service created by this FactoryBean. * * @return the GemFire Snapshot Service created by this FactoryBean. * @throws Exception if the GemFire Snapshot Service failed to be created. * @see SnapshotServiceFactoryBean.SnapshotServiceAdapter */ @Override public SnapshotServiceAdapter<K, V> getObject() throws Exception { return snapshotServiceAdapter; } /** * Gets the type of Snapshot Service created by this FactoryBean. * * @return a Class object representing the type of Snapshot Service created by this FactoryBean. * @see SnapshotServiceFactoryBean.SnapshotServiceAdapter * @see SnapshotServiceFactoryBean.CacheSnapshotServiceAdapter * @see SnapshotServiceFactoryBean.RegionSnapshotServiceAdapter */ @Override public Class<?> getObjectType() { return (snapshotServiceAdapter != null ? snapshotServiceAdapter.getClass() : SnapshotServiceAdapter.class); } /** * Determines this this FactoryBean creates single GemFire Snapshot Service instances. * * @return true. */ @Override public boolean isSingleton() { return true; } /** * Constructs and initializes the GemFire Snapshot Service used to take a snapshot of the configured Cache * or Region if initialized. In addition, this initialization method will perform the actual import. * * @throws Exception if the construction and initialization of the GemFire Snapshot Service fails. * @see org.springframework.data.gemfire.snapshot.SnapshotServiceFactoryBean.SnapshotServiceAdapter * @see #getSuppressImportOnInit() * @see #getImports() * @see #create() */ @Override @SuppressWarnings("unchecked") public void afterPropertiesSet() throws Exception { snapshotServiceAdapter = create(); if (!getSuppressImportOnInit()) { snapshotServiceAdapter.doImport(getImports()); } } /** * Constructs an appropriate instance of the SnapshotServiceAdapter based on the FactoryBean configuration. If * a Region has not been specified, then a GemFire Snapshot Service for the Cache is constructed, otherwise * the GemFire Snapshot Service for the configured Region is used. * * @return a SnapshotServiceAdapter wrapping the appropriate GemFire Snapshot Service (either Cache or Region) * depending on the FactoryBean configuration. * @see #wrap(CacheSnapshotService) * @see #wrap(RegionSnapshotService) * @see #getRegion() */ protected SnapshotServiceAdapter create() { Region<K, V> region = getRegion(); return (region != null ? wrap(region.getSnapshotService()) : wrap(getCache().getSnapshotService())); } /** * Wraps the GemFire CacheSnapshotService into an appropriate Adapter to uniformly access snapshot operations * on the Cache and Regions alike. * * @param cacheSnapshotService the GemFire CacheSnapshotService to wrap. * @return a SnapshotServiceAdapter wrapping the GemFire CacheSnapshotService. * @see SnapshotServiceFactoryBean.SnapshotServiceAdapter * @see SnapshotServiceFactoryBean.CacheSnapshotServiceAdapter * @see org.apache.geode.cache.snapshot.CacheSnapshotService */ protected SnapshotServiceAdapter<Object, Object> wrap(CacheSnapshotService cacheSnapshotService) { return new CacheSnapshotServiceAdapter(cacheSnapshotService); } /** * Wraps GemFire's RegionSnapshotService into an appropriate Adapter to uniformly access snapshot operations * on the Cache and Regions alike. * * @param regionSnapshotService the GemFire RegionSnapshotService to wrap. * @return a SnapshotServiceAdapter wrapping the GemFire RegionSnapshotService. * @see SnapshotServiceFactoryBean.SnapshotServiceAdapter * @see SnapshotServiceFactoryBean.RegionSnapshotServiceAdapter * @see org.apache.geode.cache.snapshot.RegionSnapshotService */ protected SnapshotServiceAdapter<K, V> wrap(RegionSnapshotService<K, V> regionSnapshotService) { return new RegionSnapshotServiceAdapter<K, V>(regionSnapshotService); } /** * Performs an export of the GemFire Cache or Region if configured. * * @throws Exception if the Cache/Region data export operation fails. * @see org.springframework.data.gemfire.snapshot.SnapshotServiceFactoryBean.SnapshotServiceAdapter * @see #getExports() * @see #getObject() */ @Override public void destroy() throws Exception { getObject().doExport(getExports()); } /** * Listens for SnapshotApplicationEvents triggering a GemFire Cache-wide or Region data snapshot import/export * when details of the event match the criteria of this factory's constructed GemFire SnapshotService. * * @param event the SnapshotApplicationEvent triggering a GemFire Cache or Region data import/export. * @see org.springframework.data.gemfire.snapshot.SnapshotServiceFactoryBean.SnapshotServiceAdapter * @see org.springframework.data.gemfire.snapshot.event.ExportSnapshotApplicationEvent * @see org.springframework.data.gemfire.snapshot.event.ImportSnapshotApplicationEvent * @see org.springframework.data.gemfire.snapshot.event.SnapshotApplicationEvent * @see #isMatch(SnapshotApplicationEvent) * @see #resolveSnapshotMetadata(SnapshotApplicationEvent) * @see #getObject() */ @Override public void onApplicationEvent(SnapshotApplicationEvent<K, V> event) { try { if (isMatch(event)) { if (event instanceof ExportSnapshotApplicationEvent) { getObject().doExport(resolveSnapshotMetadata(event)); } else { getObject().doImport(resolveSnapshotMetadata(event)); } } } catch (Exception ignore) { } } /** * Determines whether the details of the given SnapshotApplicationEvent match the criteria of this factory * to trigger a GemFire Cache or Region data export. * * @param event the SnapshotApplicationEvent containing details of the application requested data export. * @return a boolean value indicating whether the application requested snapshot event details match * the criteria required by this factory to trigger a GemFire Cache or Region data export. * @see SnapshotApplicationEvent */ protected boolean isMatch(SnapshotApplicationEvent event) { return (event.isCacheSnapshotEvent() || event.matches(getRegion())); } /** * Resolves the SnapshotMetadata used to perform the GemFire Cache or Region data snapshot import/export. * If the event contains specific SnapshotMetadata, then this is preferred over the factory's own * "import" or "export" SnapshotMetadata. * * @param event the SnapshotApplicationEvent from which to resolve the SnapshotMetadata. * @return the resolved SnapshotMetadata, either from the event or this factory's configured imports/exports. * @see SnapshotApplicationEvent#getSnapshotMetadata() * @see #getExports() * @see #getImports() */ protected SnapshotMetadata<K, V>[] resolveSnapshotMetadata(SnapshotApplicationEvent<K, V> event) { SnapshotMetadata<K, V>[] eventSnapshotMetadata = event.getSnapshotMetadata(); return (!ObjectUtils.isEmpty(eventSnapshotMetadata) ? eventSnapshotMetadata : (event instanceof ExportSnapshotApplicationEvent ? getExports() : getImports())); } /** * The SnapshotServiceAdapter interface is an Adapter adapting both GemFire CacheSnapshotService * and RegionSnapshotService to treat them uniformly. * * @param <K> the class type of the Region key. * @param <V> the class type of the Region value. */ public interface SnapshotServiceAdapter<K, V> { SnapshotOptions<K, V> createOptions(); void doExport(SnapshotMetadata<K, V>... configurations); void doImport(SnapshotMetadata<K, V>... configurations); void load(File directory, SnapshotFormat format); void load(SnapshotFormat format, SnapshotOptions<K, V> options, File... snapshots); void save(File location, SnapshotFormat format); void save(File location, SnapshotFormat format, SnapshotOptions<K, V> options); } /** * SnapshotServiceAdapterSupport is an abstract base class for all SnapshotServiceAdapter implementations * encapsulating common reusable functionality. * * @param <K> the class type of the Cache Region key. * @param <V> the class type of the Cache Region value. * @see SnapshotServiceFactoryBean.SnapshotServiceAdapter */ protected static abstract class SnapshotServiceAdapterSupport<K, V> implements SnapshotServiceAdapter<K, V> { protected static final File TEMPORARY_DIRECTORY = new File(System.getProperty("java.io.tmpdir")); protected final Log log = createLog(); Log createLog() { return LogFactory.getLog(getClass()); } @Override public SnapshotOptions<K, V> createOptions() { throw new UnsupportedOperationException("not implemented"); } protected SnapshotOptions<K, V> createOptions(SnapshotFilter<K, V> filter) { return createOptions().setFilter(filter); } @Override public void doExport(SnapshotMetadata<K, V>... configurations) { for (SnapshotMetadata<K, V> configuration : nullSafeArray(configurations)) { save(configuration.getLocation(), configuration.getFormat(), createOptions(configuration.getFilter())); } } @Override public void doImport(SnapshotMetadata<K, V>... configurations) { for (SnapshotMetadata<K, V> configuration : nullSafeArray(configurations)) { load(configuration.getFormat(), createOptions(configuration.getFilter()), handleLocation(configuration)); } } protected abstract File[] handleLocation(SnapshotMetadata<K, V> configuration); protected File[] handleDirectoryLocation(File directory) { return directory.listFiles(new FileFilter() { @Override public boolean accept(File pathname) { return nullSafeIsFile(pathname); } }); } protected File[] handleFileLocation(File file) { if (ArchiveFileFilter.INSTANCE.accept(file)) { try { File extractedArchiveDirectory = new File(TEMPORARY_DIRECTORY, file.getName().replaceAll("\\.", "-")); Assert.state(extractedArchiveDirectory.isDirectory() || extractedArchiveDirectory.mkdirs(), String.format("Failed create directory (%1$s) in which to extract archive (%2$s)", extractedArchiveDirectory, file)); ZipFile zipFile = (ArchiveFileFilter.INSTANCE.isJarFile(file) ? new JarFile(file, false, JarFile.OPEN_READ) : new ZipFile(file, ZipFile.OPEN_READ)); for (ZipEntry entry : CollectionUtils.iterable(zipFile.entries())) { if (!entry.isDirectory()) { DataInputStream entryInputStream = new DataInputStream(zipFile.getInputStream(entry)); DataOutputStream entryOutputStream = new DataOutputStream(new FileOutputStream( new File(extractedArchiveDirectory, toSimpleFilename(entry.getName())))); try { FileCopyUtils.copy(entryInputStream, entryOutputStream); } finally { exceptionSuppressingClose(entryInputStream); exceptionSuppressingClose(entryOutputStream); } } } return handleDirectoryLocation(extractedArchiveDirectory); } catch (Throwable t) { throw new ImportSnapshotException(String.format( "Failed to extract archive (%1$s) to import", file), t); } } return new File[] { file }; } protected boolean exceptionSuppressingClose(Closeable closeable) { try { closeable.close(); return true; } catch (IOException ignore) { logDebug(ignore, "Failed to close (%1$s)", closeable); return false; } } protected void logDebug(Throwable t, String message, Object... arguments) { if (log.isDebugEnabled()) { log.debug(String.format(message, arguments), t); } } @Override public void load(File directory, SnapshotFormat format) { throw new UnsupportedOperationException("not implemented"); } @Override public void load(SnapshotFormat format, SnapshotOptions<K, V> options, File... snapshots) { throw new UnsupportedOperationException("not implemented"); } @Override public void save(File location, SnapshotFormat format) { throw new UnsupportedOperationException("not implemented"); } @Override public void save(File location, SnapshotFormat format, SnapshotOptions<K, V> options) { throw new UnsupportedOperationException("not implemented"); } protected String toSimpleFilename(String pathname) { int pathSeparatorIndex = String.valueOf(pathname).lastIndexOf(File.separator); pathname = (pathSeparatorIndex > -1 ? pathname.substring(pathSeparatorIndex + 1) : pathname); return StringUtils.trimWhitespace(pathname); } } /** * The CacheSnapshotServiceAdapter is a SnapshotServiceAdapter adapting GemFire's CacheSnapshotService. * * @see SnapshotServiceFactoryBean.SnapshotServiceAdapterSupport */ protected static class CacheSnapshotServiceAdapter extends SnapshotServiceAdapterSupport<Object, Object> { private final CacheSnapshotService snapshotService; public CacheSnapshotServiceAdapter(CacheSnapshotService snapshotService) { Assert.notNull(snapshotService, "The backing CacheSnapshotService must not be null"); this.snapshotService = snapshotService; } protected CacheSnapshotService getSnapshotService() { return snapshotService; } @Override public SnapshotOptions<Object, Object> createOptions() { return getSnapshotService().createOptions(); } @Override protected File[] handleLocation(SnapshotMetadata<Object, Object> configuration) { return (configuration.isFile() ? handleFileLocation(configuration.getLocation()) : handleDirectoryLocation(configuration.getLocation())); } @Override public void load(File directory, SnapshotFormat format) { try { getSnapshotService().load(directory, format); } catch (Throwable t) { throw new ImportSnapshotException(String.format( "Failed to load snapshots from directory (%1$s) in format (%2$s)", directory, format), t); } } @Override public void load(SnapshotFormat format, SnapshotOptions<Object, Object> options, File... snapshots) { try { getSnapshotService().load(snapshots, format, options); } catch (Throwable t) { throw new ImportSnapshotException(String.format( "Failed to load snapshots (%1$s) in format (%2$s) using options (%3$s)", Arrays.toString(snapshots), format, options), t); } } @Override public void save(File directory, SnapshotFormat format) { try { getSnapshotService().save(directory, format); } catch (Throwable t) { throw new ExportSnapshotException(String.format( "Failed to save snapshots to directory (%1$s) in format (%2$s)", directory, format), t); } } @Override public void save(File directory, SnapshotFormat format, SnapshotOptions<Object, Object> options) { try { getSnapshotService().save(directory, format, options); } catch (Throwable t) { throw new ExportSnapshotException(String.format( "Failed to save snapshots to directory (%1$s) in format (%2$s) using options (%3$s)", directory, format, options), t); } } } /** * The RegionSnapshotServiceAdapter is a SnapshotServiceAdapter adapting GemFire's RegionSnapshotService. * * @see SnapshotServiceFactoryBean.SnapshotServiceAdapterSupport */ protected static class RegionSnapshotServiceAdapter<K, V> extends SnapshotServiceAdapterSupport<K, V> { private final RegionSnapshotService<K, V> snapshotService; public RegionSnapshotServiceAdapter(RegionSnapshotService<K, V> snapshotService) { Assert.notNull(snapshotService, "The backing RegionSnapshotService must not be null"); this.snapshotService = snapshotService; } protected RegionSnapshotService<K, V> getSnapshotService() { return snapshotService; } @Override public SnapshotOptions<K, V> createOptions() { return getSnapshotService().createOptions(); } @Override protected File[] handleLocation(final SnapshotMetadata<K, V> configuration) { return new File[] { configuration.getLocation() }; } @Override public void load(File snapshot, SnapshotFormat format) { try { getSnapshotService().load(snapshot, format); } catch (Throwable t) { throw new ImportSnapshotException(String.format( "Failed to load snapshot from file (%1$s) in format (%2$s)", snapshot, format), t); } } @Override public void load(SnapshotFormat format, SnapshotOptions<K, V> options, File... snapshots) { try { for (File snapshot : snapshots) { getSnapshotService().load(snapshot, format, options); } } catch (Throwable t) { throw new ImportSnapshotException(String.format( "Failed to load snapshots (%1$s) in format (%2$s) using options (%3$s)", Arrays.toString(snapshots), format, options), t); } } @Override public void save(File snapshot, SnapshotFormat format) { try { getSnapshotService().save(snapshot, format); } catch (Throwable t) { throw new ExportSnapshotException(String.format( "Failed to save snapshot to file (%1$s) in format (%2$s)", snapshot, format), t); } } @Override public void save(File snapshot, SnapshotFormat format, SnapshotOptions<K, V> options) { try { getSnapshotService().save(snapshot, format, options); } catch (Throwable t) { throw new ExportSnapshotException(String.format( "Failed to save snapshot to file (%1$s) in format (%2$s) using options (%3$s)", snapshot, format, options), t); } } } /** * The SnapshotMetadata class encapsulates details of the GemFire Cache or Region data snapshot * on either import or export. * * @param <K> the class type of the Region key. * @param <V> the class type of the Region value. */ public static class SnapshotMetadata<K, V> { private final File location; private final SnapshotFilter<K, V> filter; private final SnapshotFormat format; public SnapshotMetadata(File location, SnapshotFormat format) { this(location, null, format); } public SnapshotMetadata(File location, SnapshotFilter<K, V> filter, SnapshotFormat format) { Assert.notNull(location, "Location must not be null"); this.location = location; this.filter = filter; this.format = format; } public boolean isDirectory() { return nullSafeIsDirectory(getLocation()); } public boolean isFile() { return nullSafeIsFile(getLocation()); } public File getLocation() { return location; } public boolean isFilterPresent() { return (getFilter() != null); } public SnapshotFilter<K, V> getFilter() { return filter; } public SnapshotFormat getFormat() { return (format != null ? format : SnapshotFormat.GEMFIRE); } @Override public String toString() { return String.format("{ @type = %1$s, location = %2$s, filter = %2$s, format = %4$s }", getClass().getName(), getLocation().getAbsolutePath(), getFilter(), getFormat()); } } /** * The ArchiveFileFilter class is a Java FileFilter implementation accepting any File that is either * a JAR file or ZIP file. * * @see java.io.File * @see java.io.FileFilter */ protected static final class ArchiveFileFilter implements FileFilter { protected static final ArchiveFileFilter INSTANCE = new ArchiveFileFilter(); protected static final List<String> ACCEPTED_FILE_EXTENSIONS = Arrays.asList("jar", "zip"); protected static final String FILE_EXTENSION_DOT_SEPARATOR = "."; protected boolean isJarFile(File file) { return "jar".equalsIgnoreCase(getFileExtension(file)); } protected String getFileExtension(File file) { String fileExtension = ""; if (nullSafeIsFile(file)) { String pathname = file.getAbsolutePath(); int fileExtensionIndex = pathname.lastIndexOf(FILE_EXTENSION_DOT_SEPARATOR); fileExtension = (fileExtensionIndex > -1 ? pathname.substring(fileExtensionIndex + 1) : ""); } return fileExtension.toLowerCase(); } @Override public boolean accept(final File pathname) { return ACCEPTED_FILE_EXTENSIONS.contains(getFileExtension(pathname)); } } }