Package scanner and its tests
The package scanner is a helper that can list and load classes from a package, optionally recursing into sub-packages.
This commit is contained in:
parent
97c8e1274a
commit
ae5a3d5d1a
6 changed files with 394 additions and 0 deletions
279
src/main/java/info/ebenoit/ebul/reflection/PackageScanner.java
Normal file
279
src/main/java/info/ebenoit/ebul/reflection/PackageScanner.java
Normal file
|
@ -0,0 +1,279 @@
|
|||
package info.ebenoit.ebul.reflection;
|
||||
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.net.URLDecoder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashSet;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarFile;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* This class scan a package to find the classes it contains.
|
||||
*
|
||||
* <p>
|
||||
* <b><u>Warning</u>: not thread safe</b>
|
||||
*
|
||||
* @author <a href="mailto:ebenoit@ebenoit.info">E. Benoît</a>
|
||||
*/
|
||||
public class PackageScanner
|
||||
{
|
||||
|
||||
/** The name of the package */
|
||||
private final String name;
|
||||
/** <code>true</code> if classes in sub-packages will be listed as well */
|
||||
private final boolean recursive;
|
||||
/** The class loader to use */
|
||||
private final ClassLoader loader;
|
||||
/** The cached results */
|
||||
private ArrayList< String > classes;
|
||||
|
||||
|
||||
/**
|
||||
* Initialises the scanner using the current thread's default class loader.
|
||||
*
|
||||
* @param name
|
||||
* the name of the package to scan
|
||||
* @param recursive
|
||||
* <code>true</code> if sub-packages are to be listed, <code>false</code> if they are to be ignored
|
||||
*/
|
||||
public PackageScanner( final String name , final boolean recursive )
|
||||
{
|
||||
this( name , recursive , Thread.currentThread( ).getContextClassLoader( ) );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Initialises the scanner using a custom class loader.
|
||||
*
|
||||
* @param name
|
||||
* the name of the package to scan
|
||||
* @param recursive
|
||||
* <code>true</code> if sub-packages are to be listed, <code>false</code> if they are to be ignored
|
||||
* @param loader
|
||||
* the class loader to use when scanning and loading classes
|
||||
*/
|
||||
public PackageScanner( final String name , final boolean recursive , final ClassLoader loader )
|
||||
{
|
||||
this.name = name;
|
||||
this.recursive = recursive;
|
||||
this.loader = loader;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return the name of the package to scan
|
||||
*/
|
||||
public String getPackageName( )
|
||||
{
|
||||
return this.name;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return <code>true</code> if sub-packages are listed, <code>false</code> if they are ignored
|
||||
*/
|
||||
public boolean isRecursive( )
|
||||
{
|
||||
return this.recursive;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Clear the cached results.
|
||||
*
|
||||
* @return the package scanner
|
||||
*/
|
||||
public PackageScanner clearCache( )
|
||||
{
|
||||
this.classes = null;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the list of class names. If the method had already been called, the cached results will be returned.
|
||||
*
|
||||
* @return the list of canonical class names
|
||||
* @throws IOException
|
||||
* if an I/O error occurs while scanning the packages
|
||||
*/
|
||||
public ArrayList< String > getClassNames( )
|
||||
throws IOException
|
||||
{
|
||||
if ( this.classes == null ) {
|
||||
this.findAllClasses( );
|
||||
}
|
||||
return this.classes;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the list of classes. The names will be retrieves using {@link #getClassNames()} and loaded using the
|
||||
* scanner's configured class loader.
|
||||
*
|
||||
* @return the classes
|
||||
* @throws IOException
|
||||
* if an I/O error occurs while scanning the packages
|
||||
* @throws ClassNotFoundException
|
||||
* if one of the classes cannot be loaded.
|
||||
*/
|
||||
public ArrayList< Class< ? > > loadClasses( )
|
||||
throws IOException , ClassNotFoundException
|
||||
{
|
||||
final ArrayList< Class< ? > > classes = new ArrayList< >( );
|
||||
for ( final String name : this.getClassNames( ) ) {
|
||||
classes.add( this.loader.loadClass( name ) );
|
||||
}
|
||||
return classes;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Finds classes matching a specific filter.
|
||||
*
|
||||
* @param filter
|
||||
* the predicate to use as a filter
|
||||
* @return the list of matching classes
|
||||
* @throws IOException
|
||||
* if an I/O error occurs while scanning the packages
|
||||
* @throws ClassNotFoundException
|
||||
* if one of the classes cannot be loaded.
|
||||
*/
|
||||
public ArrayList< Class< ? > > findClasses( final Predicate< Class< ? > > filter )
|
||||
throws IOException , ClassNotFoundException
|
||||
{
|
||||
final ArrayList< Class< ? > > classes = new ArrayList< >( );
|
||||
for ( final String name : this.getClassNames( ) ) {
|
||||
final Class< ? > loadedClass = this.loader.loadClass( name );
|
||||
if ( filter.test( loadedClass ) ) {
|
||||
classes.add( loadedClass );
|
||||
}
|
||||
}
|
||||
return classes;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Internal method that list classes from the package
|
||||
*
|
||||
* @throws IOException
|
||||
* if an I/O error occurs while scanning the packages
|
||||
*/
|
||||
private void findAllClasses( )
|
||||
throws IOException
|
||||
{
|
||||
this.classes = new ArrayList< >( );
|
||||
|
||||
final String packagePath = this.name.replace( '.' , '/' );
|
||||
final HashSet< String > uniqueNames = new HashSet< >( );
|
||||
final Enumeration< URL > en = this.loader.getResources( packagePath );
|
||||
final HashSet< URL > urls = new HashSet< >( );
|
||||
|
||||
while ( en.hasMoreElements( ) ) {
|
||||
final URL url = en.nextElement( );
|
||||
if ( !urls.add( url ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( url.getProtocol( ).equals( "jar" ) ) {
|
||||
this.scanPackageJar( packagePath , url , uniqueNames );
|
||||
} else {
|
||||
try {
|
||||
this.scanPackageFiles( new File( url.toURI( ).getPath( ) ) , this.name , uniqueNames );
|
||||
} catch ( final URISyntaxException e ) {
|
||||
throw new RuntimeException( "internal error" , e );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Scan a JAR file for matching classes
|
||||
*
|
||||
* @param packagePath
|
||||
* path matching the package
|
||||
* @param url
|
||||
* URL of the JAR file
|
||||
* @param names
|
||||
* set of class names that were already found
|
||||
* @throws IOException
|
||||
* if decoding the URL or opening the JAR file fails
|
||||
*/
|
||||
private void scanPackageJar( String packagePath , final URL url , final HashSet< String > names )
|
||||
throws IOException
|
||||
{
|
||||
String jarFileName = URLDecoder.decode( url.getFile( ) , "UTF-8" );
|
||||
jarFileName = jarFileName.substring( 5 , jarFileName.indexOf( "!" ) );
|
||||
packagePath += "/";
|
||||
|
||||
final JarFile jf = new JarFile( jarFileName );
|
||||
try {
|
||||
final Enumeration< JarEntry > jarEntries = jf.entries( );
|
||||
while ( jarEntries.hasMoreElements( ) ) {
|
||||
String entryName = jarEntries.nextElement( ).getName( );
|
||||
if ( ! ( entryName.startsWith( packagePath ) && entryName.endsWith( ".class" ) ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
entryName = entryName.substring( packagePath.length( ) , entryName.lastIndexOf( '.' ) );
|
||||
if ( entryName.endsWith( "/package-info" ) || !this.recursive && entryName.indexOf( '/' ) != -1 ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
entryName = entryName.replace( '/' , '.' );
|
||||
if ( names.add( entryName ) ) {
|
||||
this.classes.add( entryName );
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
jf.close( );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Scan a directory for matching classes
|
||||
*
|
||||
* @param directory
|
||||
* directory to scan
|
||||
* @param pack
|
||||
* name of the package the directory corresponds to
|
||||
* @param names
|
||||
* set of class names that were already found
|
||||
*/
|
||||
private void scanPackageFiles( final File directory , final String pack , final HashSet< String > names )
|
||||
{
|
||||
for ( final File actual : directory.listFiles( ) ) {
|
||||
String entryName = actual.getName( );
|
||||
if ( actual.isDirectory( ) ) {
|
||||
if ( this.recursive ) {
|
||||
this.scanPackageFiles( actual , pack + "." + entryName , names );
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( !entryName.endsWith( ".class" ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
entryName = entryName.substring( 0 , entryName.lastIndexOf( '.' ) );
|
||||
if ( "package-info".equals( entryName ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
entryName = pack + "." + entryName;
|
||||
if ( names.add( entryName ) ) {
|
||||
this.classes.add( entryName );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
package info.ebenoit.ebul.reflection;
|
||||
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Tests for the {@link PackageScanner} class
|
||||
*
|
||||
* @author <a href="mailto:ebenoit@ebenoit.info">E. Benoît</a>
|
||||
*/
|
||||
public class TestPackageScanner
|
||||
{
|
||||
|
||||
/** Test: list the class names in the test package, without recursion */
|
||||
@Test
|
||||
public void testGetClassNamesNoRecursion( )
|
||||
throws Exception
|
||||
{
|
||||
PackageScanner scanner = new PackageScanner( "test" , false );
|
||||
ArrayList< String > result = scanner.getClassNames( );
|
||||
|
||||
ArrayList< String > expected = new ArrayList< >( );
|
||||
expected.add( "test.PSTest1" );
|
||||
|
||||
assertEquals( expected , result );
|
||||
}
|
||||
|
||||
|
||||
/** Test: list the class names in the test package, with recursion into test.sub */
|
||||
@Test
|
||||
public void testGetClassNamesRecursion( )
|
||||
throws Exception
|
||||
{
|
||||
PackageScanner scanner = new PackageScanner( "test" , true );
|
||||
ArrayList< String > result = scanner.getClassNames( );
|
||||
result.sort( ( String a , String b ) -> a.compareTo( b ) );
|
||||
|
||||
ArrayList< String > expected = new ArrayList< >( );
|
||||
expected.add( "test.PSTest1" );
|
||||
expected.add( "test.sub.PSTest2" );
|
||||
|
||||
assertEquals( expected , result );
|
||||
}
|
||||
|
||||
|
||||
/** Test: load all classes in test and test.sub */
|
||||
@Test
|
||||
public void testLoadClasses( )
|
||||
throws Exception
|
||||
{
|
||||
PackageScanner scanner = new PackageScanner( "test" , true );
|
||||
ArrayList< Class< ? > > result = scanner.loadClasses( );
|
||||
result.sort( ( Class< ? > a , Class< ? > b ) -> a.getCanonicalName( ).compareTo( b.getCanonicalName( ) ) );
|
||||
|
||||
ArrayList< Class< ? > > expected = new ArrayList< >( );
|
||||
expected.add( test.PSTest1.class );
|
||||
expected.add( test.sub.PSTest2.class );
|
||||
|
||||
assertEquals( expected , result );
|
||||
}
|
||||
|
||||
|
||||
/** Test: find classes in test and test.sub that implement {@link Serializable} */
|
||||
@Test
|
||||
public void testFindClasses( )
|
||||
throws Exception
|
||||
{
|
||||
PackageScanner scanner = new PackageScanner( "test" , true );
|
||||
ArrayList< Class< ? > > result = scanner.findClasses( a -> Serializable.class.isAssignableFrom( a ) );
|
||||
|
||||
ArrayList< Class< ? > > expected = new ArrayList< >( );
|
||||
expected.add( test.sub.PSTest2.class );
|
||||
|
||||
assertEquals( expected , result );
|
||||
}
|
||||
|
||||
}
|
6
src/test/java/test/PSTest1.java
Normal file
6
src/test/java/test/PSTest1.java
Normal file
|
@ -0,0 +1,6 @@
|
|||
package test;
|
||||
|
||||
public class PSTest1
|
||||
{
|
||||
|
||||
}
|
6
src/test/java/test/package-info.java
Normal file
6
src/test/java/test/package-info.java
Normal file
|
@ -0,0 +1,6 @@
|
|||
/**
|
||||
* Package used for the package scanner test.
|
||||
*
|
||||
* @author <a href="mailto:ebenoit@ebenoit.info">E. Benoît</a>
|
||||
*/
|
||||
package test;
|
13
src/test/java/test/sub/PSTest2.java
Normal file
13
src/test/java/test/sub/PSTest2.java
Normal file
|
@ -0,0 +1,13 @@
|
|||
package test.sub;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
|
||||
|
||||
@SuppressWarnings( "serial" )
|
||||
public class PSTest2
|
||||
implements Serializable
|
||||
{
|
||||
|
||||
}
|
6
src/test/java/test/sub/package-info.java
Normal file
6
src/test/java/test/sub/package-info.java
Normal file
|
@ -0,0 +1,6 @@
|
|||
/**
|
||||
* Sub-package used for the package scanner test.
|
||||
*
|
||||
* @author <a href="mailto:ebenoit@ebenoit.info">E. Benoît</a>
|
||||
*/
|
||||
package test.sub;
|
Loading…
Reference in a new issue