package org.nnsoft.guice.autobind.scanner.asm;
/*
* Copyright 2012 The 99 Software Foundation
*
* 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.
*/
import static java.lang.Runtime.getRuntime;
import static java.lang.String.format;
import static java.lang.System.getProperty;
import static java.util.Collections.synchronizedSet;
import static java.util.concurrent.Executors.newFixedThreadPool;
import static java.util.logging.Level.FINE;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.SEVERE;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static java.util.regex.Pattern.compile;
import static org.nnsoft.guice.autobind.scanner.asm.AnnotationCollector.ASM_FLAGS;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.JarURLConnection;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import javax.inject.Inject;
import javax.inject.Named;
import org.nnsoft.guice.autobind.scanner.ClasspathScanner;
import org.nnsoft.guice.autobind.scanner.PackageFilter;
import org.nnsoft.guice.autobind.scanner.features.ScannerFeature;
import org.objectweb.asm.ClassReader;
/**
* This Implementation only uses the ASM-API to read all recognized classes. It
* doesn't depend on any further 3rd-Party libraries.
*/
public class ASMClasspathScanner
implements ClasspathScanner
{
private static final String LINE_SEPARATOR = getProperty( "line.separator" );
private final Logger _logger = getLogger( getClass().getName() );
@Inject
@Named( "classpath" )
private URL[] classPath;
private List<Pattern> patterns = new ArrayList<Pattern>();
private final Set<String> visited;
private final BlockingQueue<AnnotationCollector> collectors;
@Inject
public ASMClasspathScanner( Set<ScannerFeature> listeners, @Named( "packages" ) PackageFilter... filter )
{
int cores = getRuntime().availableProcessors();
this.collectors = new ArrayBlockingQueue<AnnotationCollector>( cores );
for ( int i = 0; i < cores; i++ )
{
try
{
collectors.put( new AnnotationCollector() );
}
catch ( InterruptedException e )
{
// ignore
}
}
for ( PackageFilter p : filter )
{
includePackage( p );
}
for ( ScannerFeature listener : listeners )
{
addFeature( listener );
}
visited = synchronizedSet( new HashSet<String>() );
}
@Override
public void addFeature( ScannerFeature feature )
{
for ( AnnotationCollector collector : collectors )
{
collector.addScannerFeature( feature );
}
}
@Override
public void removeFeature( ScannerFeature feature )
{
for ( AnnotationCollector collector : collectors )
{
collector.addScannerFeature( feature );
}
}
@Override
public List<ScannerFeature> getFeatures()
{
List<ScannerFeature> features;
try
{
AnnotationCollector collector = collectors.take();
features = collector.getScannerFeatures();
collectors.put( collector );
}
catch ( InterruptedException e )
{
// ignore
features = Collections.emptyList();
}
return features;
}
@Override
public void includePackage( final PackageFilter filter )
{
String packageName = filter.getPackage();
String pattern = ".*" + packageName.replace( ".", "/" );
if ( filter.deep() )
{
pattern = pattern + "/(?:\\w|/)*([A-Z](?:\\w|\\$)+)\\.class$";
}
else
{
pattern = pattern + "/([A-Z](?:\\w|\\$)+)\\.class$";
}
if ( _logger.isLoggable( FINE ) )
{
_logger.fine( format( "Including Package for scanning: %s generating Pattern: %s", packageName, pattern ) );
}
patterns.add( compile( pattern ) );
}
@Override
public void excludePackage( final PackageFilter filter )
{
// TODO Could use Predicate of Google
}
public void scan()
throws IOException
{
ExecutorService pool = newFixedThreadPool( getRuntime().availableProcessors() );
if ( _logger.isLoggable( INFO ) )
{
StringBuilder builder = new StringBuilder();
builder.append( "Using Root-Path for Classpath scanning:" ).append( LINE_SEPARATOR );
for ( URL url : classPath )
{
builder.append( url.toString() ).append( LINE_SEPARATOR );
}
_logger.log( INFO, builder.toString() );
}
List<Future<?>> futures = new ArrayList<Future<?>>();
for ( final URL url : classPath )
{
Future<?> task = pool.submit( new Runnable()
{
@Override
public void run()
{
try
{
if ( url.toString().startsWith( "jar:" ) )
{
visitJar( url );
return;
}
URI uri;
File entry;
try
{
uri = url.toURI();
entry = new File( uri );
if ( !entry.exists() )
{
if ( _logger.isLoggable( FINE ) )
{
_logger.log( FINE, format( "Skipping Entry %s, because it doesn't exists.", entry ) );
}
return;
}
}
catch ( URISyntaxException e )
{
// ignore
_logger.log( WARNING, format( "Using invalid URL for Classpath Scanning: %s", url ), e );
return;
}
catch ( Throwable e )
{
// ignore
_logger.log( SEVERE, format( "Using invalid URL for Classpath Scanning: ", url ), e );
return;
}
if ( entry.isDirectory() )
{
visitFolder( entry );
}
else
{
String path = uri.toString();
if ( matches( path ) )
{
if ( !visited.contains( entry.getAbsolutePath() ) )
{
visitClass( new FileInputStream( entry ) );
visited.add( entry.getAbsolutePath() );
}
}
else if ( path.endsWith( ".jar" ) )
{
visitJar( entry );
}
}
}
catch ( FileNotFoundException e )
{
if ( _logger.isLoggable( FINE ) )
{
_logger.log( FINE, format( "Skipping Entry %s, because it doesn't exists.", url ), e );
}
}
catch ( IOException e )
{
if ( _logger.isLoggable( FINE ) )
{
_logger.log( FINE, format( "Skipping Entry %s, because it couldn't be scanned.", url ), e );
}
}
catch ( Throwable e )
{
_logger.log( WARNING, format( "Skipping Entry %s, because it couldn't be scanned.", url ), e );
}
}
} );
futures.add( task );
}
for ( Future<?> future : futures )
{
try
{
future.get();
}
catch ( InterruptedException e )
{
throw new RuntimeException( e );
}
catch ( ExecutionException e )
{
_logger.log( SEVERE, e.getMessage(), e );
}
}
pool.shutdown();
destroy();
}
public void destroy()
{
classPath = null;
collectors.clear();
patterns.clear();
patterns = null;
visited.clear();
}
private void visitFolder( File folder )
throws IOException
{
if ( _logger.isLoggable( FINE ) )
{
_logger.log( FINE, format( "Scanning Folder: %s...", folder.getAbsolutePath() ) );
}
File[] files = folder.listFiles();
for ( File file : files )
{
if ( file.isDirectory() )
{
visitFolder( file );
}
else
{
String path = file.toURI().toString();
if ( matches( path ) )
{
if ( !visited.contains( file.getAbsolutePath() ) )
{
visitClass( new FileInputStream( file ) );
visited.add( file.getAbsolutePath() );
}
}
else if ( path.endsWith( ".jar" ) )
{
visitJar( file );
}
}
}
}
private void visitJar( URL url )
throws IOException
{
if ( _logger.isLoggable( FINE ) )
{
_logger.log( FINE, format( "Scanning JAR-File: %s", url ) );
}
JarURLConnection conn = (JarURLConnection) url.openConnection();
_visitJar( conn.getJarFile() );
}
private void visitJar( File file )
throws IOException
{
if ( _logger.isLoggable( FINE ) )
{
_logger.log( FINE, format( "Scanning JAR-File: %s", file.getAbsolutePath() ) );
}
JarFile jarFile = new JarFile( file );
_visitJar( jarFile );
}
private void _visitJar( JarFile jarFile )
throws IOException
{
Enumeration<JarEntry> jarEntries = jarFile.entries();
for ( JarEntry jarEntry = null; jarEntries.hasMoreElements(); )
{
jarEntry = jarEntries.nextElement();
String name = jarEntry.getName();
if ( !jarEntry.isDirectory() && matches( name ) )
{
if ( !visited.contains( name ) )
{
visitClass( jarFile.getInputStream( jarEntry ) );
visited.add( name );
}
}
}
}
private void visitClass( InputStream in )
throws IOException
{
InputStream bufIn = new BufferedInputStream( in );
try
{
ClassReader reader = new ClassReader( bufIn );
try
{
AnnotationCollector collector = collectors.take();
reader.accept( collector, ASM_FLAGS );
collectors.put( collector );
}
catch ( InterruptedException e )
{
// ignore
}
}finally
{
bufIn.close();
}
}
private boolean matches( String name )
{
boolean returned = false;
try
{
for ( Pattern pattern : patterns )
{
if ( pattern.matcher( name ).matches() )
{
return ( returned = true );
}
}
return returned;
}
finally
{
if ( _logger.isLoggable( Level.FINE ) )
{
_logger.log( FINE, format( "%s.matches(..) - \"%s\" -> %s",
getClass().getSimpleName(), name, returned ) );
}
}
}
}