/*! * 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() ); } } } }