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