/*!
* This program is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software
* Foundation.
*
* You should have received a copy of the GNU Lesser General Public License along with this
* program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
* or from the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Lesser General Public License for more details.
*
* Copyright (c) 2002-2013 Pentaho Corporation.. All rights reserved.
*/
package org.pentaho.reporting.libraries.base.util;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.StringTokenizer;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
/**
* The class-query tool loads classes using a classloader and calls "processClass" for each class encountered. This is
* highly expensive and sometimes dangerous excercise as the classloading may trigger static initializers and may
* exhaust the "permgen" space of the Virtual machine.
* <p/>
* If possible anyhow, do not use this class.
*
* @author Thomas Morgner
*/
public abstract class ClassQueryTool {
/**
* A logger.
*/
private static final Log logger = LogFactory.getLog( ClassQueryTool.class );
/**
* The default constructor.
*/
protected ClassQueryTool() {
}
/**
* Processes a single class-file entry. The method will try to load the given entry as java-class and if that
* successeds will then call the "processClass" method to let the real implementation handle the class.
*
* @param classLoader the classloader that should be used for class- and resource loading.
* @param entryName the file name in the classpath.
*/
protected void processEntry( final ClassLoader classLoader, final String entryName ) {
if ( entryName == null ) {
throw new NullPointerException();
}
if ( classLoader == null ) {
throw new NullPointerException();
}
if ( entryName.endsWith( ".class" ) == false ) {
return;
}
final String className = entryName.substring( 0, entryName.length() - 6 ).replace( '/', '.' );
if ( isValidClass( className ) == false ) {
return;
}
try {
final Class c = Class.forName( className, false, classLoader );
processClass( classLoader, c );
} catch ( NoClassDefFoundError ndef ) {
// Ignore silently. This happens a lot if the classpath is incomplete.
} catch ( Throwable e ) {
// ignore ..
logger.debug( "At class '" + className + "': " + e );
}
}
/**
* Checks, whether the class is valid. If the class-name is not considered valid by this method, the class will not be
* processed. Use this to pre-filter the class-stream as loading classes is expensive.
*
* @param className the name of the class.
* @return true, if the class should be processed, false otherwise.
*/
protected boolean isValidClass( final String className ) {
return true;
}
/**
* The handler method that is called for every class encountered on the classpath.
*
* @param classLoader the classloader used to load the class.
* @param c the class that should be handled.
*/
protected abstract void processClass( final ClassLoader classLoader, final Class c );
/**
* Processes a single jar file. The Jar file is processed in the order of the entries contained within the
* ZIP-directory.
*
* @param classLoader the classloader
* @param jarFile the URL pointing to the jar file to be parsed.
*/
private void processJarFile( final ClassLoader classLoader, final URL jarFile ) {
try {
final ZipInputStream zf = new ZipInputStream( jarFile.openStream() );
ZipEntry ze;
while ( ( ze = zf.getNextEntry() ) != null ) {
if ( !ze.isDirectory() ) {
processEntry( classLoader, ze.getName() );
}
}
zf.close();
} catch ( final IOException e1 ) {
logger.debug( "Caught IO-Exception while processing file " + jarFile, e1 );
}
}
/**
* Processes all entries from a given directory, ignoring any subdirectory contents. If the directory contains
* sub-directories these directories are not searched for JAR or ZIP files.
* <p/>
* In addition to the directory given as parameter, the direcories and JAR/ZIP-files on the classpath are also
* searched for entries.
* <p/>
* If directory is null, only the classpath is searched.
*
* @param directory the directory to be searched, or null to just use the classpath.
* @throws IOException if an error occured while loading the resources from the directory.
* @throws SecurityException if access to the system properties or access to the classloader is restricted.
* @noinspection AccessOfSystemProperties
*/
public void processDirectory( final File directory ) throws IOException {
final ArrayList<URL> allURLs = new ArrayList<URL>();
final ArrayList<URL> jarURLs = new ArrayList<URL>();
final ArrayList<File> directoryURLs = new ArrayList<File>();
final String classpath = System.getProperty( "java.class.path" );
final String pathSeparator = System.getProperty( "path.separator" );
final StringTokenizer tokenizer = new StringTokenizer( classpath, pathSeparator );
while ( tokenizer.hasMoreTokens() ) {
final String pathElement = tokenizer.nextToken();
final File directoryOrJar = new File( pathElement );
final File file = directoryOrJar.getAbsoluteFile();
if ( file.isDirectory() && file.exists() && file.canRead() ) {
allURLs.add( file.toURI().toURL() );
directoryURLs.add( file );
continue;
}
if ( !file.isFile() || ( file.exists() == false ) || ( file.canRead() == false ) ) {
continue;
}
final String fileName = file.getName();
if ( fileName.endsWith( ".jar" ) || fileName.endsWith( ".zip" ) ) {
allURLs.add( file.toURI().toURL() );
jarURLs.add( file.toURI().toURL() );
}
}
if ( directory != null && directory.isDirectory() ) {
final File[] driverFiles = directory.listFiles();
for ( int i = 0; i < driverFiles.length; i++ ) {
final File file = driverFiles[ i ];
if ( file.isDirectory() && file.exists() && file.canRead() ) {
allURLs.add( file.toURI().toURL() );
directoryURLs.add( file );
continue;
}
if ( !file.isFile() || ( file.exists() == false ) || ( file.canRead() == false ) ) {
continue;
}
final String fileName = file.getName();
if ( fileName.endsWith( ".jar" ) || fileName.endsWith( ".zip" ) ) {
allURLs.add( file.toURI().toURL() );
jarURLs.add( file.toURI().toURL() );
}
}
}
final URL[] urlsArray = jarURLs.toArray( new URL[ jarURLs.size() ] );
final File[] dirsArray = directoryURLs.toArray( new File[ directoryURLs.size() ] );
final URL[] allArray = allURLs.toArray( new URL[ allURLs.size() ] );
for ( int i = 0; i < allArray.length; i++ ) {
final URL url = allArray[ i ];
logger.debug( url );
}
for ( int i = 0; i < urlsArray.length; i++ ) {
final URL url = urlsArray[ i ];
final URLClassLoader classLoader = new URLClassLoader( allArray );
processJarFile( classLoader, url );
}
for ( int i = 0; i < dirsArray.length; i++ ) {
final File file = dirsArray[ i ];
final URLClassLoader classLoader = new URLClassLoader( allArray );
processDirectory( classLoader, file, "" );
}
}
/**
* Processes all entries from a given directory. If the directory contains sub-directories these directories are
* processed in recursive depth-first mannor.
*
* @param classLoader the classloader to be used for loading classes.
* @param file the directory to be searched.
* @param pathPrefix the path prefix used to construct absolute filenames within the classpath.
*/
private void processDirectory( final URLClassLoader classLoader, final File file, final String pathPrefix ) {
final File[] files = file.listFiles();
for ( int i = 0; i < files.length; i++ ) {
final File subFile = files[ i ];
if ( subFile.exists() == false || subFile.canRead() == false ) {
continue;
}
if ( subFile.isDirectory() ) {
processDirectory( classLoader, subFile, pathPrefix + subFile.getName() + '/' );
} else if ( subFile.isFile() ) {
processEntry( classLoader, pathPrefix + subFile.getName() );
}
}
}
}